protocol

package
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 5 Imported by: 0

README

pkg/protocol — Pasture's public protocol types

This package is the public, importable surface of Pasture: the shared protocol types (TaskTracker, PhaseId, AuditEvent, SessionEntry, AgentCategories, ContextKind, …) that pasture's own binaries and any external consumer build on.

Stability: v0.x — pre-1.0, no external API guarantees yet

Pasture is unversioned (0 git tags). There is no semver guarantee on this Go API today.

  • External consumers: pin a commit pseudo-version — go get github.com/dayvidpham/pasture@<commit>. Do not assume the surface is stable between commits.
  • Stabilization trigger: when a real external consumer actually imports this package (the anticipated first ones are an orchestrator UI app, or peasant's analytics / taxonomy — SessionEntry is already aligned with peasant's schema), the consumed surface will be frozen and v1.0.0 cut.
Stability tiers
Tier Types Depend on it?
Stable TaskTracker, PhaseId, AuditEvent (heavily used internally) yes, but still pre-1.0
Experimental SessionEntry (aligned with peasant; will move), newer ACP types expect changes

Internal consumers — this is the anti-drift contract

The pasture binaries (pastured, pasture-msg, pasture, pasture-release) all import this package as their single source of truth, together with the signal/query name constants in internal/temporal/constants.go. That shared contract is what keeps pastured (which registers signals/queries) and pasture-msg (which sends them) from drifting. Import these types directly (not via internal/types aliases) and never hardcode a signal/query string.

Full policy

See pasture/docs/VERSIONING.md for the complete three-channel consumption + versioning policy (Claude Code plugin, external Go, inter-tool) and the R5 status.

Documentation

Overview

Package protocol defines the public convergence types for the Pasture multi-agent orchestration protocol.

These types are consumed by all other packages (formatters, handlers, workflows, audit, ACP adapter). They are designed to be serialization-safe for Temporal's JSONPlainPayloadConverter and standard encoding/json.

JSON tags use camelCase to match Python aura-protocol output format.

Index

Constants

This section is empty.

Variables

AllAutomatonRoles is the ordered slice of all valid AutomatonRole values. Useful for iteration, completeness checks, and parameterised tests.

AllContextKinds is the ordered slice of all valid ContextKind values.

Useful for parameterised tests and Scenario 10's enum-membership assertion (the test confirms that ResearcherNoteContext is NOT present).

AllEventTypes is the ordered slice of all valid EventType values.

AllPastureRoles is the ordered slice of all valid PastureRole values.

AllPhaseIds is the ordered slice of all valid PhaseId values (pipeline + terminal). Useful for iteration, completeness checks, and building lookup tables.

DefaultPipeline is the standard 12-phase pasture protocol pipeline. The index in this slice determines the pX number (0-based index + 1).

Functions

func MustHaveImpl

func MustHaveImpl()

MustHaveImpl panics if RegisterOpenTaskTracker has not yet been called. Call this at program startup (e.g. in a TestMain or an init() of a top-level package) to fail fast with a clear error rather than discovering the missing wiring the first time OpenTaskTracker is called.

Example — add this to your main package or TestMain:

func init() { protocol.MustHaveImpl() }

In-tree callers (cmd/pasture, cmd/pastured, internal/handlers) already import internal/tasks directly and therefore never need this guard. The guard exists for completeness and for any future in-module integration tests that construct main-like binaries without going through the handler layer.

func RegisterOpenTaskTracker

func RegisterOpenTaskTracker(impl func(dbPath string) (TaskTracker, error))

RegisterOpenTaskTracker is called by internal/tasks's init() to wire the constructor implementation. It is exported only so the internal package can assign through it; external packages MUST NOT call it directly (doing so is a programming error and will overwrite the implementation).

This indirection keeps (TaskTracker, OpenTaskTracker) co-located in pkg/protocol (PROPOSAL-2 §7.4) while the body lives in internal/tasks (UAT-1 placement binding). Without it, OpenTaskTracker's body would import internal/tasks — forbidden because internal/tasks already imports pkg/protocol for the TaskTracker type, which would create an import cycle.

See MustHaveImpl for a startup-time guard that panics if this function has not been called.

Types

type AuditEvent

type AuditEvent struct {
	EpochId   string         `json:"epochId"`
	Phase     PhaseId        `json:"phase"`
	Role      string         `json:"role"`
	EventType EventType      `json:"eventType"`
	Payload   map[string]any `json:"payload"`
	Timestamp time.Time      `json:"timestamp"`
}

AuditEvent is a generic audit trail event emitted by epoch workflows and activities. JSON tags use camelCase to match Python aura-protocol output.

type AutomatonRole

type AutomatonRole string

AutomatonRole is the strongly-typed pasture-side category for SoftwareAgent instances that represent rules-based automata (PROPOSAL-2 §7.6, URD R8).

Wire values are stable strings stored in pasture_agent_categories.automaton_role. The enum has exactly 6 values (None + 5 concrete categories); UAT-1 dropped the earlier generic "Derivation" catch-all in favor of the two first-class values ConsensusReached and CreateFollowup.

const (
	// AutomatonRoleNone marks a SoftwareAgent that has no pasture-side
	// automaton role (e.g. the pastured daemon process itself).
	AutomatonRoleNone AutomatonRole = "None"

	// AutomatonRoleConstraintChecker is the canonical name for the
	// constraint-checker automaton (pasture/automaton/check-constraints).
	AutomatonRoleConstraintChecker AutomatonRole = "ConstraintChecker"

	// AutomatonRoleTransitionGate covers the 3 transition gate kinds
	// (consensus, vote-threshold, exit-condition).
	AutomatonRoleTransitionGate AutomatonRole = "TransitionGate"

	// AutomatonRoleHookHandler covers all Claude-Code-hook-event handlers
	// (per Pasture URD D7's hook list).
	AutomatonRoleHookHandler AutomatonRole = "HookHandler"

	// AutomatonRoleConsensusReached is the synthesized event emitted when
	// all reviewers have voted ACCEPT during a phase transition. UAT-1
	// promoted this from a Derivation child to a first-class category.
	AutomatonRoleConsensusReached AutomatonRole = "ConsensusReached"

	// AutomatonRoleCreateFollowup synthesizes follow-up epics from
	// PROPOSAL findings during ratification. UAT-1 promoted this to a
	// first-class category alongside ConsensusReached.
	AutomatonRoleCreateFollowup AutomatonRole = "CreateFollowup"
)

func (AutomatonRole) IsValid

func (r AutomatonRole) IsValid() bool

IsValid reports whether r is a known AutomatonRole value.

Membership is tested via switch (not slice scan) so the compiler can flag missing cases when the enum grows.

func (AutomatonRole) String

func (r AutomatonRole) String() string

String returns the wire-format string value of r.

type Context

type Context struct {
	Kind      ContextKind `json:"kind"`
	ContextId string      `json:"contextId"`
}

Context carries a typed (Kind, ContextId) pair as returned by TaskTracker.EventContexts. PROPOSAL-2 §7.5.

ContextId's shape varies per Kind:

  • ContextEpoch / ContextSlice / ContextReview / ContextFollowup: a Provenance TaskID string ("namespace--uuid").
  • ContextGit: a git commit SHA (or remote ref).
  • ContextSkill: a skill run ID.
  • ContextSession: a Claude Code session ID.
  • ContextNone: unused.

type ContextKind

type ContextKind string

ContextKind enumerates the kinds of context an audit event may attach to.

Wire values are stable strings stored in context_edges.context_kind. The enum has exactly 8 values (None + 7 concrete kinds) — note that PROPOSAL-2 Scenario 10 asserts ResearcherNoteContext is NOT a member.

const (
	// ContextNone is the zero value indicating "no context" — used by
	// callers building empty Context structs and by IsValid() to allow the
	// zero value through.
	ContextNone ContextKind = "None"

	// ContextEpoch attaches an event to an epoch (Provenance TaskID for the
	// originating REQUEST). Wire format of context_id: "namespace--uuid".
	ContextEpoch ContextKind = "EpochContext"

	// ContextSlice attaches an event to an implementation slice (Beads
	// SLICE-N task ID).
	ContextSlice ContextKind = "SliceContext"

	// ContextReview attaches an event to a review cycle.
	ContextReview ContextKind = "ReviewContext"

	// ContextFollowup attaches an event to a FOLLOWUP_SLICE-N task.
	ContextFollowup ContextKind = "FollowupContext"

	// ContextGit attaches a free-floating git event (commit, push, rebase)
	// using the commit SHA (or remote ref) as context_id.
	ContextGit ContextKind = "GitContext"

	// ContextSkill attaches a /pasture:* skill invocation; context_id is the
	// skill run ID.
	ContextSkill ContextKind = "SkillContext"

	// ContextSession attaches a Claude Code session event; context_id is
	// the session ID.
	ContextSession ContextKind = "SessionContext"
)

func (ContextKind) IsValid

func (k ContextKind) IsValid() bool

IsValid reports whether k is a known ContextKind value.

Used by Scenario 10 to assert IsValid("ResearcherNoteContext") == false.

func (ContextKind) String

func (k ContextKind) String() string

String returns the wire-format string value of k.

type EventType

type EventType string

EventType classifies an audit event in the dual-write trail.

const (
	EventPhaseTransition    EventType = "PhaseTransition"
	EventPhaseAdvance       EventType = "PhaseAdvance"
	EventVoteRecorded       EventType = "VoteRecorded"
	EventConstraintChecked  EventType = "ConstraintChecked"
	EventSliceStarted       EventType = "SliceStarted"
	EventSliceCompleted     EventType = "SliceCompleted"
	EventSessionRegistered  EventType = "SessionRegistered"
	EventReviewCycleStarted EventType = "ReviewCycleStarted"
)

func (EventType) IsValid

func (e EventType) IsValid() bool

IsValid reports whether e is a known EventType value.

type PastureRole

type PastureRole string

PastureRole mirrors the PROV-O Role concept for non-automaton agents (humans and MLAgents acting in epoch roles). PROPOSAL-2 §7.6 / URD R8.

Wire values are stable strings stored in pasture_agent_categories.pasture_role.

const (
	// PastureRoleNone marks an agent without a pasture-side role
	// (e.g. SoftwareAgents whose categorisation is fully captured by
	// AutomatonRole).
	PastureRoleNone PastureRole = "None"

	// PastureRoleArchitect — owns proposal authoring (Phases 3-7).
	PastureRoleArchitect PastureRole = "Architect"

	// PastureRoleSupervisor — owns IMPL_PLAN, slice decomposition,
	// worker dispatch, and code-review coordination (Phases 8-10).
	PastureRoleSupervisor PastureRole = "Supervisor"

	// PastureRoleWorker — implements vertical slices in Phase 9.
	PastureRoleWorker PastureRole = "Worker"

	// PastureRoleReviewer — performs code/plan reviews (Phases 4, 5, 10).
	PastureRoleReviewer PastureRole = "Reviewer"
)

func (PastureRole) IsValid

func (r PastureRole) IsValid() bool

IsValid reports whether r is a known PastureRole value.

func (PastureRole) String

func (r PastureRole) String() string

String returns the wire-format string value of r.

type PhaseId

type PhaseId string

PhaseId identifies a phase in the 12-phase epoch lifecycle by its name. The position (pX number) is determined by the phase's index in a Pipeline, not by the value of the PhaseId itself.

const (
	PhaseRequest      PhaseId = "request"
	PhaseElicit       PhaseId = "elicit"
	PhasePropose      PhaseId = "propose"
	PhaseReview       PhaseId = "review"
	PhasePlanReview   PhaseId = "plan-review"
	PhaseRatify       PhaseId = "ratify"
	PhaseHandoff      PhaseId = "handoff"
	PhaseImplPlan     PhaseId = "impl-plan"
	PhaseWorkerSlices PhaseId = "worker-slices"
	PhaseCodeReview   PhaseId = "code-review"
	PhaseImplUAT      PhaseId = "impl-uat"
	PhaseLanding      PhaseId = "landing"
	PhaseComplete     PhaseId = "complete"
)

func ParsePhaseId

func ParsePhaseId(s string) (PhaseId, error)

ParsePhaseId parses a flexible phase input string into a PhaseId.

Supported formats (case-insensitive):

  • Name only: "request", "elicit", "propose", "review", "plan-review", "ratify", "handoff", "impl-plan", "worker-slices", "code-review", "impl-uat", "landing", "complete"
  • pX format: "p1", "p2", ..., "p12" (resolved via DefaultPipeline)
  • pX-name: "p1-request", "p2-elicit", ... (legacy; name portion used)
  • Number only: "1", "2", ..., "12"

Returns an error if the input does not match any known format.

func (PhaseId) IsValid

func (p PhaseId) IsValid() bool

IsValid reports whether p is a known PhaseId value.

func (PhaseId) String

func (p PhaseId) String() string

String returns the wire-format string value of p.

type Pipeline

type Pipeline []PhaseId

Pipeline is an ordered sequence of phases. The 0-based index of a PhaseId in the slice determines its 1-based phase number (pX). PhaseComplete is a terminal state and is NOT included in the pipeline itself.

func (Pipeline) Contains

func (p Pipeline) Contains(id PhaseId) bool

Contains reports whether the pipeline contains id.

func (Pipeline) Index

func (p Pipeline) Index(id PhaseId) int

Index returns the 0-based index of id in the pipeline, or -1 if not found.

func (Pipeline) Next

func (p Pipeline) Next(id PhaseId) PhaseId

Next returns the next phase after id in the pipeline. Returns PhaseComplete if id is the last phase or not found.

func (Pipeline) PhaseAt

func (p Pipeline) PhaseAt(number int) (PhaseId, bool)

PhaseAt returns the PhaseId at the given 1-based phase number. Returns ("", false) if number is out of range.

func (Pipeline) PhaseNumber

func (p Pipeline) PhaseNumber(id PhaseId) int

PhaseNumber returns the 1-based phase number for id, or -1 if not found.

type SessionEntry

type SessionEntry struct {
	SessionId      string  `json:"sessionId"`
	EntryIndex     int     `json:"entryIndex"`
	Provider       string  `json:"provider"`
	EntryType      string  `json:"entryType"`
	Role           string  `json:"role"`
	TimestampMs    *int64  `json:"timestampMs,omitempty"`
	ContentPreview *string `json:"contentPreview,omitempty"` // max 500 chars
	TokensIn       *int    `json:"tokensIn,omitempty"`
	TokensOut      *int    `json:"tokensOut,omitempty"`
	HasToolUse     bool    `json:"hasToolUse"`
	ToolKind       *string `json:"toolKind,omitempty"`     // ACP-aligned tool classification
	ToolNamesCsv   *string `json:"toolNamesCsv,omitempty"` // comma-separated tool names
	HasThinking    bool    `json:"hasThinking"`
	IsError        bool    `json:"isError"`
	StopReason     *string `json:"stopReason,omitempty"`    // ACP per-turn stop reason
	RawByteLength  *int    `json:"rawByteLength,omitempty"` // raw JSON byte count
	ToolCallId     *string `json:"toolCallId,omitempty"`    // MCP correlation
	EntryId        *string `json:"entryId,omitempty"`       // provider-native ID
	ParentEntryId  *string `json:"parentEntryId,omitempty"` // parent entry link
	Depth          int     `json:"depth"`                   // 0 = message, 1 = content part
	ParentIndex    *int    `json:"parentIndex,omitempty"`   // entryIndex of parent (nil for depth=0)
	ToolInput      *string `json:"toolInput,omitempty"`     // tool_use input JSON
	ToolOutput     *string `json:"toolOutput,omitempty"`    // tool_result output JSON
	Extra          *string `json:"extra,omitempty"`         // JSON overflow for provider-specific data
}

SessionEntry represents a single indexed entry within a session transcript.

Aligned with agent-data-leverage pkg/schema.SessionEntry schema. All optional fields use pointer types to distinguish absent from zero-value. JSON tags use camelCase aligned with the ACP content model and Python output.

Maps 1:1 to a row in the session_entries table in the audit SQLite backend.

type TaskTracker

type TaskTracker interface {
	// ─── Embedded: Provenance task CRUD, edges, labels, comments,
	// agents (Human/ML/Software), activities. See provenance.Tracker. ───
	provenance.Tracker

	// RecordEvent persists a single audit event. Returns an error if the
	// underlying store is unavailable or the write fails. The caller
	// (typically a Temporal activity) is responsible for retry policy.
	//
	// PROPOSAL-2 §7.11: workflows call this then immediately call
	// AttachContext with ContextEpoch. Free-floating events use other
	// ContextKind values (Git/Skill/Session).
	//
	// Note: callers that need the inserted event_id (so they can attach
	// context_edges rows in the same logical step) should prefer
	// RecordEventReturningId — it bundles the write + id-recovery in a
	// single call, removing the post-write SELECT MAX(id) round-trip the
	// S9 free-floating helpers had to do as a workaround.
	RecordEvent(ctx context.Context, event AuditEvent) error

	// RecordEventReturningId persists a single audit event and returns the
	// audit_events.id of the just-inserted row. The implementation reads the
	// id from sql.Result.LastInsertId on the SAME INSERT statement that wrote
	// the row, so the returned id is race-safe under any level of write
	// contention — independent of the D11 "low write contention" deployment
	// binding. Returns the new id and a nil error on success; on failure
	// returns 0 and an actionable *pasterrors.StructuredError.
	//
	// This is the canonical RecordEvent entry point for workflow activities
	// (PROPOSAL-2 §7.11): RecordTransition and RecordAuditEvent call this
	// then immediately call AttachContext(eventId, ContextEpoch, epochId)
	// to record the event-to-epoch correlation. Free-floating helpers
	// (RecordGitEvent / RecordSkillEvent / RecordSessionEvent) also use it
	// in place of the older SELECT MAX(id) workaround that this method
	// supersedes (Phase 11 R1-B per finding aura-plugins-d1h6y).
	//
	// Behaviour for non-SQLite trail backends (e.g. *audit.InMemoryAuditTrail
	// used in tests): the returned id is a synthetic per-trail monotonic
	// counter — it is NOT a real audit_events row id and MUST NOT be
	// persisted across processes. The counter is incremented atomically per
	// call so concurrent test goroutines always observe distinct ids,
	// matching the SQLite trail's per-statement-LastInsertId guarantee.
	// AttachContext on an in-memory trail is a no-op anyway (no
	// context_edges table backing it), so the synthetic id is only
	// meaningful for AttachContext-relative assertions in unit tests that
	// exercise the workflow integration path without paying for a real
	// SQLite file.
	RecordEventReturningId(ctx context.Context, event AuditEvent) (int64, error)

	// QueryEvents returns audit events filtered by epoch and (optionally)
	// phase / role. Results are returned in chronological order. epochId
	// is required and is always part of the WHERE clause.
	//
	// Note: this is the legacy v1 query path; new callers should prefer
	// Timeline(ctx, ContextEpoch, epochId) which uses the context_edges
	// JOIN and works for all ContextKind values, not just epoch.
	QueryEvents(ctx context.Context, epochId string, phase *PhaseId, role *string) ([]AuditEvent, error)

	// RecordSessionEntries persists a batch of SessionEntry records
	// atomically (single transaction). Nil or empty slices are no-ops.
	RecordSessionEntries(ctx context.Context, entries []SessionEntry) error

	// QuerySessionEntries returns all session entries for sessionId in
	// insertion order. Returns an empty (non-nil) slice when no entries
	// exist for sessionId.
	QuerySessionEntries(ctx context.Context, sessionId string) ([]SessionEntry, error)

	// SetAgentCategories upserts the (automaton, pasture-role) pair for
	// the given agent into pasture_agent_categories. Idempotent: a second
	// call with the same id replaces the row. Both AutomatonRole and
	// PastureRole MUST be valid enum values (see IsValid); a nil/zero
	// value is permitted and stored as the literal "None".
	//
	// Returns *pasterrors.StructuredError{Category: CategoryStorage} on
	// write failure, or {Category: CategoryValidation} if either enum
	// value is unknown.
	SetAgentCategories(id provenance.AgentID, automaton AutomatonRole, pastureRole PastureRole) error

	// AgentCategories returns the (automaton, pasture-role) pair stored
	// for id. Returns ("None", "None", nil) if no row exists for id.
	AgentCategories(id provenance.AgentID) (AutomatonRole, PastureRole, error)

	// AttachContext adds a row to context_edges binding eventId to the
	// (kind, contextId) pair. The (event_id, context_kind, context_id)
	// triple is the BCNF composite primary key — duplicate inserts are
	// idempotent (returns nil; the existing row is preserved).
	//
	// kind MUST be a valid ContextKind (kind.IsValid()); contextId MUST
	// be non-empty. Validation failures return CategoryValidation.
	AttachContext(ctx context.Context, eventId int64, kind ContextKind, contextId string) error

	// EventContexts returns the typed contexts attached to eventId, in
	// insertion order. Returns an empty (non-nil) slice when no edges
	// exist for eventId.
	EventContexts(ctx context.Context, eventId int64) ([]Context, error)

	// Timeline returns all events whose context_edges row matches the
	// (kind, contextId) pair, in chronological order. The intended usage:
	//
	//   events := tracker.Timeline(ctx, ContextEpoch, epochId)
	//   events := tracker.Timeline(ctx, ContextGit, "<sha>")
	//
	// A nil/empty contextId returns an empty slice (no error).
	Timeline(ctx context.Context, kind ContextKind, contextId string) ([]AuditEvent, error)

	// Close releases all resources held by the tracker. It is safe to call
	// Close multiple times; the second and subsequent calls return nil.
	//
	// Note: provenance.Tracker also declares Close(), and the embedded
	// method satisfies this interface requirement; implementations MUST
	// however ensure both subsystems (the provenance.Tracker AND the
	// underlying audit.Trail's *sql.DB) are closed exactly once.
	Close() error
}

TaskTracker is the unified Pasture workflow-record façade. Implementations wrap a provenance.Tracker (task CRUD, edges, labels, comments, agents, activities) and an audit.Trail (event recording, query, session entries), both opened against the same SQLite file at ~/.local/share/pasture/pasture.db.

The interface adds 6 pasture-only methods on top of the 28 + 4 inherited:

  • Agent categorisation (R8): SetAgentCategories, AgentCategories
  • Context attachment (R9): AttachContext, EventContexts, Timeline
  • Lifecycle: Close (closes both wrapped subsystems exactly once)

The constructor OpenTaskTracker is the supported way to obtain an instance; see its doc comment for error semantics. Callers MUST call Close on the returned tracker.

All methods are safe for concurrent use; the SQLite file is opened in WAL mode with busy_timeout=5000 (PROPOSAL-2 §10.3 / D11 binding). The cross-subsystem race test (BLOCKER B3) in internal/tasks proves this.

func OpenTaskTracker

func OpenTaskTracker(dbPath string) (TaskTracker, error)

OpenTaskTracker opens the unified SQLite database at dbPath and returns a wrapped TaskTracker. See the OpenTaskTracker var doc comment above for the full contract (errors, side effects, lifecycle, wiring requirement).

If the implementation has not been registered (internal/tasks not imported), this function returns a descriptive error. Call MustHaveImpl() at startup to catch this condition at init time rather than at the first call site.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL