core

package
v0.0.0-...-39bba70 Latest Latest
Warning

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

Go to latest
Published: May 13, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core holds adaptor-agnostic domain types — the shared vocabulary every adapter, transport, and server handler speaks. Nothing in core may import internal/adapter, internal/transport, or internal/server.

Organizing hierarchy (gm-26n4)

Gemba is organized into a single nested hierarchy:

Workspace (≡ Project)            — one .gemba/, one beads database
└── Repository[]                 — one or more git repos owned by the workspace
    └── WorkItem[]               — every bead is associated with exactly one repository

"Workspace" and "project" are interchangeable terminology in code and docs; the codebase uses workspace because it predates the project concept and the organizing principle is the same. A WorkItem can span multiple repositories (gm-kdh3): WorkItem.RepositoryIDs lists every repo it touches and WorkItem.PrimaryRepositoryID is the spawn-cwd anchor. Existing beads filed before gm-26n4 use RepositoryUnspecified or empty fields; the spawn path rejects polecat work on those until the operator backfills.

Contents:

  • types.go WorkItem, AgentRef, Relationship, Evidence (+ their enums)
  • repository.go Repository, RepositoryID, RepositoryRegistry (gm-26n4), BeadPrefix routing (gm-d2ts), auto-derive (gm-i4bd)
  • state.go StateCategory ∈ backlog|unstarted|started|completed|canceled
  • derived.go DerivedSignals + Derive(item, escalations, manifest) — pure
  • dod.go DefinitionOfDone (informational-only; never blocks)
  • budget.go Sprint, TokenBudget (three-tier inform/warn/stop)
  • workplane.go WorkPlane interface + CapabilityManifest (gm-e3.2)
  • orchestration.go OrchestrationPlaneAdaptor + OrchestrationCapabilityManifest (gm-e3.3)
  • codegen.go CoreTypesTS — source of truth emitted to web/src/types

Regenerate the TypeScript mirror via `make gen`.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Package core: see doc.go for the overview.

Index

Constants

View Source
const (
	DescriptionFormatPlain    = "plain"
	DescriptionFormatMarkdown = "markdown"
)

Known DescriptionFormat values. Keep in lockstep with the SPA's renderer registry (web/src/components/board/descriptionRenderers.tsx).

View Source
const (
	WorkItemEventCreated          = "workitem_created"
	WorkItemEventUpdated          = "workitem_updated"
	WorkItemEventClosed           = "workitem_closed"
	WorkItemEventEvidenceAttached = "workitem_evidence_attached"
)

Canonical WorkPlaneEvent.Kind values. Adaptors SHOULD use these tokens; any other value is passed through verbatim by the events canonicaliser.

View Source
const KindMilestone = "milestone"

WorkItem is the adaptor-agnostic view of a unit of work. Every WorkPlaneAdaptor must be able to project its native record (Beads issue, Jira issue, GitHub issue, LangGraph task) onto this shape.

Design notes:

  • Status holds the adaptor's own word for the current state so the UI can show "In Review" instead of "started" when it matters. StateCategory carries the normalized bucket used for lane placement.
  • DoD is informational-only; core never blocks transitions on it. That rule is locked by gm-root DD #? / DD-10.
  • Custom is an escape hatch for adaptor-specific fields that don't map onto any cross-cutting primitive. The UI only renders them inside `web/src/extensions/<adaptor-id>/` (gm-root DD-4).

Canonical cross-adaptor tokens for WorkItem.Kind (and WorkItemFilter.Kinds). Adaptors MAY emit additional kinds beyond these constants; the set here exists so core-layer callers (filters, the SPA's lane chrome) don't hard-code string literals for cross- cutting concepts.

KindMilestone is Gemba-native: there is no native "milestone" type in bd. The Beads adaptor encodes a milestone as `-t epic` + label "type:milestone" and projects that convention onto KindMilestone on read (gm-root.3 / gm-root.3.5). Filtering WorkItemFilter.Kinds to {KindMilestone} returns only the label-flagged beads.

View Source
const ProtocolVersion = "1.0.0"

ProtocolVersion is the adaptor contract version the core advertises at startup. Every adaptor MUST populate CapabilityManifest.ProtocolVersion (WorkPlane) or OrchestrationCapabilityManifest.OrchestrationAPIVersion (OrchestrationPlane) with the exact same value.

Mismatches are rejected at registration time (gm-e3.4) with an actionable error, before any query is served. Bump only when the semantic contract changes in a way adaptors must track — adding new optional manifest fields does not warrant a bump; changing a method signature or an error contract does.

Resolves DD-12.

View Source
const WorkPlaneEmitterBuffer = 64

WorkPlaneEmitterBuffer is the per-subscriber channel buffer depth. Matches the hub's default — the fan-out chains hub → client are the real drop point, not this inner emitter, so we keep the buffer modest.

Variables

AgentNativeAPIs is the authoritative set.

ConcurrencyModels is the authoritative set.

View Source
var CoreTypesTS = GenerateTS()

CoreTypesTS is the adaptor-agnostic domain surface expressed in TypeScript. It is the single source of truth that drives the generated `web/src/types/core.gen.ts`.

Most of it is emitted by walking Go structs via reflection (see GenerateTS); the small amount of hand-maintained text — enum unions, AdaptorError (whose wire shape is shaped by MarshalJSON, not the Go struct) and the FLAG_NAMES const — is concatenated around the reflection output.

This value is written verbatim by `cmd/gen-core-types`; tests in this package assert shape-level parity against the Go types. The file under web/ should only be regenerated via `go generate ./core` (or `make gen`) (or `make gen`) so that humans don't hand-edit a generated artifact.

View Source
var ErrBeadAlreadyClaimed = errors.New("core: bead already claimed by another session")

ErrBeadAlreadyClaimed is the sentinel adaptors return from StartSession (or its inline-claim equivalent) when the bead the caller asked for has already been claimed by another session (gm-e3.8). The auto-dispatch daemon treats this error as a soft skip on the inline-claim path: rather than failing the tick, the daemon picks the next candidate from the ranked ready set.

Adaptors MUST wrap this sentinel inside a tagged *AdaptorError so the boundary-error contract (Conformance Group F) still holds. The canonical wrapping is KindValidation with Cause=ErrBeadAlreadyClaimed — KindValidation already names "the input failed a precondition," and "another session got there first" is exactly that. Callers branch on the sentinel via IsAlreadyClaimedError, never on kind/message text.

gt's adaptor maps `gt sling`'s "bead-already-hooked" rejection to this sentinel; native maps the bead-double-claim case from its pane spawn path. Both paths are distinct from the cap-race retry in internal/server/sessions.go (pane saturation), which keeps surfacing as a plain KindValidation without the sentinel.

View Source
var ErrNotFound = errors.New("core: not found")

ErrNotFound is the sentinel error WorkPlane implementations return when a lookup id does not exist in the backend. Adaptors MAY wrap it with context (errors.Wrap / fmt.Errorf with %w) so long as errors.Is(err, ErrNotFound) still holds.

View Source
var ErrUnsupported = errors.New("core: unsupported by adaptor")

ErrUnsupported is the sentinel error WorkPlane implementations return when the caller requests a feature group the manifest opts out of (for example ReadBudgetRollup on a non-budget-enforced adaptor). The UI uses errors.Is to decide whether to hide the control versus surface a fatal error.

OrchestratorHooks is the authoritative set.

QueryLanguages is the authoritative set.

SchemaEnforcements is the authoritative set — keep in lockstep with any SPA enum that gates on these tokens.

SessionStatuses is the exhaustive set of valid SessionStatus values. Used by the TS codegen and by exhaustive-switch helpers.

VersioningTransports is the authoritative set.

Functions

func AssertAdaptorError

func AssertAdaptorError(err error) error

AssertAdaptorError is the Conformance Group F (error semantics) helper: given an error observed from an adaptor-boundary call, it returns nil when the error carries a tagged AdaptorError of a valid kind with the retryable field populated, and returns a descriptive error otherwise.

Conformance suites SHOULD call this against every non-nil error they observe from an adaptor method. An adaptor that raises a bare errors.New() or a fmt.Errorf() without a tagged envelope fails Group F and must be fixed before it can ship.

The empty-message case is tolerated (an adaptor may only carry a Kind), but the kind itself must be in the canonical set.

func AssertBoundaryValidation

func AssertBoundaryValidation(err error) error

AssertBoundaryValidation is the Conformance Group F extension for the transport-boundary decoding contract (gm-io4, t3code audit):

  1. A malformed input passed to a transport decoder MUST surface as a KindValidation AdaptorError — not a bare json.SyntaxError, not a fmt.Errorf, not KindRequestFailed (that is for transport-level failures, not shape failures).
  2. The error's Detail MUST carry a ValidationIssue under "issue".

An adaptor that decodes input itself and re-runs these checks is failing the contract: decode belongs AT the boundary. Conformance suites use this to assert their transport passes the boundary rule; AssertAdaptorError covers the broader nine-kind contract already.

func AssertCapabilityDenied

func AssertCapabilityDenied(err error) error

AssertCapabilityDenied is the Conformance Group E helper for the capability-enforcement rule: given an error observed from an adaptor boundary call the adaptor was expected to refuse, it returns nil when the error is a properly-tagged capability_denied AdaptorError, and a descriptive error otherwise.

Conformance suites wire this after calling an undeclared op against an adaptor. An adaptor that returns a bare error, a legacy ErrUnsupported sentinel without a tagged envelope, or silently succeeds fails Group E and must be fixed before it can ship.

Note: callers that ALSO want to assert Group F (tagged-error shape) should still call AssertAdaptorError; this helper is the stricter "denied for the right reason" check. A capability_denied kind is required here because the generic KindUnsupported is reserved for coarse feature-group opt-out at the adaptor's discretion — the capability guard uses the more specific discriminator so the SPA can distinguish "adaptor refused because manifest said so" from "adaptor technically could do this but chose not to".

func EnforceCapability

func EnforceCapability(m CapabilityManifest, op Operation) error

EnforceCapability is the error-returning sibling of CheckCapability. When the op is denied, it returns a tagged *AdaptorError with Kind=capability_denied so callers can branch on errors.Is / core.AsAdaptorError without touching the human-readable message.

Adaptors MUST call this (or produce an equivalent capability_denied error) on any boundary method the manifest opts out of; the guard is the primary check but adaptor-side fail-fast is the defense-in-depth requirement from the t3code lesson.

func GenerateTS

func GenerateTS() string

GenerateTS renders the current TypeScript projection of the core domain types. Output is deterministic and byte-stable across runs so the file is safe to commit.

func IsAlreadyClaimedError

func IsAlreadyClaimedError(err error) bool

IsAlreadyClaimedError reports whether err is (or wraps) the ErrBeadAlreadyClaimed sentinel. Use this at dispatch sites instead of branching on AdaptorError.Kind — the kind is incidental to the "soft skip and try next candidate" decision; the sentinel is the stable signal.

func IsKnownOperation

func IsKnownOperation(op Operation) bool

IsKnownOperation reports whether op is one of the canonical Operations. An unknown op is treated as a denied-by-default by the guard: a caller that asks about a made-up op shouldn't silently get Allowed=true.

func IsRetryable

func IsRetryable(err error) bool

IsRetryable reports whether err carries a tagged AdaptorError whose Retryable flag is true. A non-tagged error is NOT retryable: the caller is expected to either wrap it or treat it as terminal.

func LoadRepositoriesDir

func LoadRepositoriesDir(dir string) (map[RepositoryID]*Repository, error)

LoadRepositoriesDir reads every *.toml file under dir and returns the parsed repositories keyed by ID. A missing dir yields an empty map (so a fresh workspace boots cleanly); other I/O errors propagate. Mirrors internal/core/persona.LoadDir semantics.

func WorkspaceWorktreePath

func WorkspaceWorktreePath(w Workspace) string

WorkspaceWorktreePath returns the worktree filesystem path for the given Workspace (gm-s47n.2.6). Reads the typed WorktreePath field first; falls back to the legacy ProviderMetadata["worktree_path"] or ProviderMetadata["worktree"] keys some adaptors set today, so a rolling migration doesn't strand the planner.

Returns "" when the kind isn't a worktree or the path is unset. Callers that need to detect workspace collision should compare the returned string after canonicalisation (e.g. filepath.Clean).

Types

type AdaptorError

type AdaptorError struct {
	Kind      ErrorKind      `json:"_kind"`
	Retryable bool           `json:"retryable"`
	Message   string         `json:"message"`
	Cause     error          `json:"-"`
	Detail    map[string]any `json:"detail,omitempty"`
}

AdaptorError is the discriminated-tagged error every adaptor-boundary method MUST return on failure (gm-faz, resolves DD-12 + DD-13 per the t3code audit). The JSON shape is wire-stable:

{
  "_kind":     "rate_limited",
  "retryable": true,
  "message":   "provider quota exhausted for session sess-42",
  "cause":     "http 429",
  "detail":    {"retry_after_seconds": 30}
}

The _kind discriminator drives the TS union emitted by cmd/gen-core-types; retryable replaces every historical call site that tried to guess intent from error strings.

Construction patterns:

return nil, core.NewAdaptorError(core.KindValidation,
    "StateMap missing native status %q", native)

return nil, core.WrapAdaptorError(core.KindRequestFailed, err,
    "bd describe failed")

return nil, &core.AdaptorError{Kind: core.KindRateLimited,
    Retryable: false, Message: "retry-after 0, permanent"}

AdaptorError implements error, errors.Unwrap (via Cause), and a custom Is so the legacy sentinels ErrNotFound / ErrUnsupported stay compatible with errors.Is.

func AsAdaptorError

func AsAdaptorError(err error) *AdaptorError

AsAdaptorError extracts the nearest *AdaptorError in err's Unwrap chain, returning nil when no tagged error is present. This is the canonical accessor for retry loops and the mutation gate — prefer it over string-matching on err.Error().

if ae := core.AsAdaptorError(err); ae != nil && ae.Retryable {
    time.Sleep(backoff); continue
}

func NewAdaptorError

func NewAdaptorError(kind ErrorKind, format string, args ...any) *AdaptorError

NewAdaptorError constructs a tagged error with Retryable defaulted from kind.RetryableDefault(). Message is formatted fmt.Sprintf style.

func NewAlreadyClaimedError

func NewAlreadyClaimedError(format string, args ...any) *AdaptorError

NewAlreadyClaimedError constructs a tagged AdaptorError carrying ErrBeadAlreadyClaimed as Cause. Adaptors should use this helper at the boundary so call sites can rely on errors.Is.

func NewValidationError

func NewValidationError(issue ValidationIssue) *AdaptorError

NewValidationError builds a KindValidation *AdaptorError whose Detail carries the structured ValidationIssue under the "issue" key. This is the single canonical constructor transport decoders MUST use so the UI can render validation failures without string-matching on Message.

return nil, core.NewValidationError(core.ValidationIssue{
    Path: "end_session.mode", Code: "enum",
    Reason: `must be one of ["completed","failed","canceled"]`,
})

func WrapAdaptorError

func WrapAdaptorError(kind ErrorKind, cause error, format string, args ...any) *AdaptorError

WrapAdaptorError wraps a lower-level error (subprocess exit, HTTP client failure) with a tagged envelope. The wrapped error becomes Cause so errors.Unwrap and errors.Is keep working.

func (*AdaptorError) Error

func (e *AdaptorError) Error() string

Error renders the tagged error as "kind: message: cause" so test failures and logs stay greppable. Callers MUST NOT parse this string — branch on Kind / Retryable instead.

func (*AdaptorError) Is

func (e *AdaptorError) Is(target error) bool

Is makes the legacy sentinels (ErrNotFound, ErrUnsupported) match a tagged AdaptorError of the corresponding kind. This lets older call sites keep using errors.Is(err, core.ErrNotFound) while new adaptors return a proper AdaptorError underneath.

func (*AdaptorError) MarshalJSON

func (e *AdaptorError) MarshalJSON() ([]byte, error)

MarshalJSON emits the wire shape with the Cause flattened to its Error() string. Keeping Cause as an opaque string on the wire avoids leaking stack traces or unrelated structured types into the SPA; the Go side keeps the typed Cause available through Unwrap.

func (*AdaptorError) UnmarshalJSON

func (e *AdaptorError) UnmarshalJSON(data []byte) error

UnmarshalJSON rehydrates an AdaptorError from the wire. Cause is reconstituted as a plain errors.New-style error since the original typed chain cannot cross the wire.

func (*AdaptorError) Unwrap

func (e *AdaptorError) Unwrap() error

Unwrap exposes Cause so errors.Is / errors.As walk past the tagged envelope to the transport-level error.

type AgentFilter

type AgentFilter struct {
	ParentID  *AgentID  `json:"parent_id,omitempty"`
	Kind      AgentKind `json:"agent_kind,omitempty"`
	GroupID   string    `json:"group_id,omitempty"`
	Workspace string    `json:"workspace,omitempty"`
}

AgentFilter narrows ListAgents. All fields are optional; zero value returns everything the adaptor knows about.

type AgentGroup

type AgentGroup struct {
	ID          string         `json:"id"`
	DisplayName string         `json:"display_name"`
	Mode        GroupMode      `json:"mode"`
	Members     GroupMembers   `json:"members"`
	Repository  []string       `json:"repository,omitempty"`
	Extension   map[string]any `json:"extension,omitempty"`
}

AgentGroup is the adaptor-agnostic view of a collection of agents (gm-root DD-7). The Mode field picks the union arm carried by Members.

type AgentID

type AgentID string

AgentID is the workspace-qualified identifier for an agent. Same shape rules as WorkItemID: "<workspace>/<repo-or-scope>/<native-id>" (e.g. "gemba/polecats/jasper", "langgraph/run-42/node-a").

type AgentKind

type AgentKind string

AgentKind distinguishes an automated agent from a human operator. The UI renders the two with distinct visual treatment (gm-e12.4 Agents dashboard depends on this); capability manifests may also gate action types on Kind.

const (
	// AgentKindAgent — an automated actor (polecat, LangGraph node,
	// Gas Town crew member, etc.).
	AgentKindAgent AgentKind = "agent"
	// AgentKindHuman — a human operator.
	AgentKindHuman AgentKind = "human"
)

type AgentNativeAPI

type AgentNativeAPI string

AgentNativeAPI names the authoritative programmatic surface agents speak to. R7.

const (
	// AgentAPICLI — a CLI binary that agents shell out to.
	AgentAPICLI AgentNativeAPI = "cli"
	// AgentAPIJSONAPI — the adaptor's own HTTP/JSON surface.
	AgentAPIJSONAPI AgentNativeAPI = "json-api"
	// AgentAPIMCP — a first-class MCP server agents connect to.
	AgentAPIMCP AgentNativeAPI = "mcp"
	// AgentAPIRESTOnly — only a REST surface intended for the
	// adaptor's human web UI. No agent-native affordances; agents
	// must scrape or script around it. **Below the minimum bar.**
	AgentAPIRESTOnly AgentNativeAPI = "rest-only"
)

type AgentRef

type AgentRef struct {
	ID        AgentID   `json:"id"`
	Name      string    `json:"name"`
	Kind      AgentKind `json:"agent_kind"`
	ParentID  *AgentID  `json:"parent_id,omitempty"`
	Role      string    `json:"role,omitempty"`
	Workspace string    `json:"workspace,omitempty"`
	// Dialect is the optional adaptor-declared agent-runtime dialect
	// ("claude", "codex", "copilot", "opencode", "gemini", …) that
	// drives per-dialect arg builders and session UI. Free-form on
	// purpose — core does NOT enum this (gm-gsh / Foolery lesson):
	// adaptors invent new dialects faster than a core enum can keep
	// up, and an unknown dialect must degrade gracefully to "generic
	// agent" rendering rather than fail a JSON decode.
	Dialect *string `json:"dialect,omitempty"`
}

AgentRef is the adaptor-agnostic view of an agent — a polecat, a LangGraph node, a Gas Town crew member, or a human user. Core doesn't care which; capability manifests decide what actions that agent can be the subject of.

ParentID carries parent-agent federation (gm-root DD-1): orchestrators with hierarchical structures (Gas Town Mayor → Polecats; LangGraph supervisor → subgraph nodes) populate it so the UI can render agent hierarchies without adaptor-specific hacks. Nil means the agent is top-level or standalone.

type Assignment

type Assignment struct {
	ID          string              `json:"id"`
	WorkItemID  WorkItemID          `json:"work_item_id"`
	AgentID     AgentID             `json:"agent_id"`
	WorkspaceID string              `json:"workspace_id,omitempty"`
	SessionID   string              `json:"session_id,omitempty"`
	Status      AssignmentStatus    `json:"status"`
	StartedAt   *time.Time          `json:"started_at,omitempty"`
	EndedAt     *time.Time          `json:"ended_at,omitempty"`
	Cost        *CostMeter          `json:"cost,omitempty"`
	Escalations []EscalationRequest `json:"escalations,omitempty"`
}

Assignment binds an agent to a work item via an optional workspace. Cost accumulates here across the assignment's session lifetime; open escalations hang off it so the UI can badge cards without walking sessions.

type AssignmentStatus

type AssignmentStatus string

AssignmentStatus is the lifecycle tag on an Assignment.

const (
	AssignmentPending  AssignmentStatus = "pending"
	AssignmentActive   AssignmentStatus = "active"
	AssignmentPaused   AssignmentStatus = "paused"
	AssignmentFinished AssignmentStatus = "finished"
	AssignmentFailed   AssignmentStatus = "failed"
	AssignmentCanceled AssignmentStatus = "canceled"
)

type AssignmentStrategy

type AssignmentStrategy string

AssignmentStrategy names one of the three observed ways an orchestrator hands work to an agent (domain.md §3.4). Adaptors declare which they support; Gemba's canonical strategy is `pull`, with push and hook as adaptor-supported alternatives.

const (
	// StrategyPush — caller pushes work to a named agent.
	StrategyPush AssignmentStrategy = "push"
	// StrategyPull — agent claims from a shared queue (canonical).
	StrategyPull AssignmentStrategy = "pull"
	// StrategyHook — adaptor fires a side-effect on state transition
	// (Gas Town's hook model, GitHub webhook).
	StrategyHook AssignmentStrategy = "hook"
)

type BeadBranch

type BeadBranch struct {
	// RepositoryID names the repo this branch lives in. MUST appear
	// in [WorkItem.RepositoryIDs] — entries for unknown repos are
	// rejected by [WorkItem.ValidateBranches] to catch typos that
	// would otherwise silently miss the spawn lookup.
	RepositoryID RepositoryID `json:"repository_id"`

	// Branch is the git branch name. Non-empty; whitespace is not
	// trimmed (the operator is responsible for naming hygiene; the
	// loader doesn't reformat).
	Branch string `json:"branch"`
}

BeadBranch maps a repository to the git branch this bead's work happens on inside it (gm-ou02). A multi-repo bead may carry one entry per touched repository; a single-repo bead typically carries at most one. When the spawn path needs a branch for a polecat and no entry matches, it derives one as `<bead-id>-<slugified-title>` from the corresponding [Repository.DefaultBranch].

type BudgetRollup

type BudgetRollup struct {
	SprintID   string               `json:"sprint_id"`
	Budget     TokenBudget          `json:"budget"`
	Tier       BudgetTier           `json:"tier"`
	ByWorkItem map[WorkItemID]int64 `json:"by_work_item,omitempty"`
	CapturedAt time.Time            `json:"captured_at"`
}

BudgetRollup is the aggregated token consumption for a sprint, suitable for the UI's budget dashboard (gm-e12.3 and friends).

Used/Limit/Tier are convenience projections of the underlying TokenBudget at read time; ByWorkItem lets the UI break the total down without a second query.

type BudgetTier

type BudgetTier string

BudgetTier names the three enforcement thresholds a TokenBudget carries. They fire in order as consumption grows. "stop" is not a hard-block by core; the orchestration adaptor decides what "stop" means for its own runtime (pause spawns, refuse new runs, etc).

const (
	// BudgetInform — the soft notice tier. UI surfaces a hint.
	BudgetInform BudgetTier = "inform"
	// BudgetWarn — the louder tier. UI surfaces a warning banner.
	BudgetWarn BudgetTier = "warn"
	// BudgetStop — the blocking tier. Orchestrator should refuse new work.
	BudgetStop BudgetTier = "stop"
)

type CapabilityManifest

type CapabilityManifest struct {
	AdaptorName    string `json:"adaptor_name"`
	AdaptorVersion string `json:"adaptor_version"`
	// ProtocolVersion is the core contract version the adaptor was built
	// against. Version negotiation (gm-e3.4) compares this to the core's
	// advertised core_version and fails startup on mismatch.
	ProtocolVersion string `json:"protocol_version"`

	// Transport is the wire protocol the adaptor speaks. Exactly one of
	// api|jsonl|mcp per adaptor in v1.
	Transport Transport `json:"transport"`

	// StateMap translates the adaptor's native statuses to the five core
	// StateCategory buckets. Required; an adaptor with no states is not a
	// valid WorkPlane.
	StateMap StateMap `json:"state_map"`

	// EdgeExtensions declares non-core relationship kinds (anything
	// beyond blocks / parent_child / relates_to).
	EdgeExtensions []EdgeExtension `json:"edge_extensions,omitempty"`

	// FieldExtensions declares non-core fields the adaptor emits on
	// WorkItem.Custom.
	FieldExtensions []FieldExtension `json:"field_extensions,omitempty"`

	// RelationshipExtensions declares per-edge metadata the adaptor
	// attaches to every Relationship record.
	RelationshipExtensions []RelationshipExtension `json:"relationship_extensions,omitempty"`

	// SprintNative — adaptor emits first-class Sprint records. When
	// false, core treats ListSprints as "may return empty" and hides the
	// sprint lane chrome in the UI.
	SprintNative bool `json:"sprint_native"`

	// TokenBudgetEnforced — adaptor carries a real TokenBudget with
	// three-tier enforcement (gm-root DD-14). When false, the UI may
	// still render a budget if one is configured, but the "stop" tier
	// has no runtime effect.
	TokenBudgetEnforced bool `json:"token_budget_enforced"`

	// EvidenceSynthesisRequired — adaptor expects the core to synthesize
	// Evidence records from transport-level artifacts (commits, test
	// runs) rather than receive them pre-built (gm-root DD-13). When
	// false, the adaptor provides its own Evidence in GetWorkItem.
	EvidenceSynthesisRequired bool `json:"evidence_synthesis_required"`

	// ReadOnly — adaptor cannot service mutations. CreateWorkItem,
	// UpdateWorkItem, and AttachEvidence all fail with KindReadOnly;
	// the UI MUST hide write controls rather than disable them. Used
	// by explicit read-only runtime modes and lower-layer read-only
	// adaptors. Dolt URL mode leaves this false unless
	// --beads-read-only is active.
	ReadOnly bool `json:"read_only"`

	// DescriptionFormat declares the content type of WorkItem.Description
	// so the SPA can pick the correct renderer ("plain" → preformatted
	// text, "markdown" → markdown with GFM extensions). Adaptors that
	// don't set it fall through to "plain" on the SPA side; beads-backed
	// adaptors (bd CLI, dolt SQL) default to "markdown" since that's
	// what `bd` edits. Unknown values MUST be treated as "plain" by the
	// UI so a future format can ship without breaking older clients.
	DescriptionFormat string `json:"description_format,omitempty"`

	// SchemaEnforcement declares whether the adaptor's store enforces
	// the core WorkItem schema natively (Dolt SQL, Postgres, typed API)
	// or the adaptor synthesizes it on top of an unstructured substrate
	// (flat Markdown, YAML frontmatter, free-form JSON). R1.
	SchemaEnforcement SchemaEnforcement `json:"schema_enforcement,omitempty"`

	// QueryLanguages enumerates the query surfaces the adaptor exposes
	// beyond the baseline ListWorkItems filter. Callers use this to
	// decide whether to push predicates down (e.g. sql-subset, jsonpath)
	// vs. post-filter in-process. R2.
	QueryLanguages []QueryLanguage `json:"query_languages,omitempty"`

	// DependencyGraphNative — the adaptor's store models the work-item
	// dependency graph as first-class edges (rather than free-form
	// labels or comment conventions). True for bd, Jira, GitHub
	// dependency graph. R3 edges.
	DependencyGraphNative bool `json:"dependency_graph_native,omitempty"`

	// ReadySetQuery — the adaptor exposes a native "ready set" query
	// (e.g. `bd ready`, a materialised view) so an orchestrator can ask
	// "what's unblocked right now?" without walking the graph
	// client-side. R3 native ready-set.
	ReadySetQuery bool `json:"ready_set_query,omitempty"`

	// VersioningTransport lists the versioned transport surfaces the
	// adaptor supports — for importing / exporting / diffing state
	// against another instance. Empty (or ["none"]) means non-versioned.
	// R4.
	VersioningTransport []VersioningTransport `json:"versioning_transport,omitempty"`

	// ConcurrencyModel describes how the adaptor resolves simultaneous
	// writes from N agents. "dolt-merge" and "git-merge" are
	// content-aware, three-way merges. "mvcc" is row-level MVCC
	// (Postgres style). "optimistic" is compare-and-swap without a
	// merge (last-writer-wins on conflict). R5.
	ConcurrencyModel ConcurrencyModel `json:"concurrency_model,omitempty"`

	// AgentSessionDecoupling — work-item state survives an agent
	// session's death. If an agent crashes mid-task, a second agent
	// can pick up exactly where the first left off. **MUST be true for
	// a conforming agentic-data-plane adaptor.** R6.
	AgentSessionDecoupling bool `json:"agent_session_decoupling,omitempty"`

	// AgentNativeAPI names the authoritative programmatic surface
	// agents speak to. "cli" is shell-friendly; "json-api" is the
	// adaptor's own HTTP surface; "mcp" is a first-class MCP server;
	// "rest-only" is below the bar — the adaptor has no agent-native
	// entry point and only a human web UI. R7.
	AgentNativeAPI AgentNativeAPI `json:"agent_native_api,omitempty"`

	// OrchestratorHooks lists the subscription / coordination hooks
	// an orchestrator can rely on. Each value is a guarantee — absent
	// means "no, the orchestrator has to simulate this client-side".
	// R8.
	OrchestratorHooks []OrchestratorHook `json:"orchestrator_hooks,omitempty"`
}

CapabilityManifest is the declarative description every WorkPlane adaptor returns from Describe. It tells core (and the UI) which transport the adaptor speaks, how to normalize its native statuses, what extensions it carries beyond the core primitives, and which optional feature groups it opts into.

The manifest is the single source of truth the capability-negotiation UI consults before rendering adaptor-specific controls (gm-e11.4 / gm-root DD-15): controls for unsupported capabilities are hidden, not disabled, so the operator never sees a button they can't use.

func (CapabilityManifest) Flags

func (m CapabilityManifest) Flags() Flags

Flags returns the flat-boolean projection of the manifest. Pure and deterministic: no side effects, no I/O, no randomness. Callers may call it on every render — it allocates only the returned struct.

The method is defined on the value receiver so both pointer and value CapabilityManifest values can call it; the manifest is small enough that the copy cost is negligible next to the UI work downstream.

func (CapabilityManifest) MinimumBar

func (m CapabilityManifest) MinimumBar() (ok bool, reasons []string)

MinimumBar reports whether the manifest clears the agentic data-plane minimum bar (gm-ekr, domain.md §1.0). Returns false plus a list of human-readable reasons when any required criterion is missing; callers (registration, orchestrator bind) use the bool to decide between full-capability and reduced-capability mode.

The current bar (kept tight on purpose — below-bar adaptors still register, so this is a classification, not an admission gate):

  • agent_session_decoupling MUST be true. A store that can't survive an agent session's death isn't an agentic data plane; it's a task list with a task runner glued on.
  • agent_native_api MUST NOT be "rest-only". An adaptor with only a human web UI can't be safely driven by a fleet of agents.

Other R-fields are advisory: they narrow what capabilities the orchestrator can rely on, but they don't disqualify the adaptor.

func (CapabilityManifest) Validate

func (m CapabilityManifest) Validate() error

Validate applies structural checks that every manifest must satisfy. Adaptors should call this in the startup path so doctor reports a clean failure before the transport is bound.

type ClaimModel

type ClaimModel string

ClaimModel declares how the adaptor handles the atomic-claim race between sessions competing for the same ready bead (gm-e3.8). Set on the manifest at adaptor construction so the planner's auto-dispatch daemon can branch its dispatch path accordingly.

The empty value (zero ClaimModel) is treated as ClaimModelInline so existing manifests that pre-date the field keep their pre-gm-e3.8 behavior — every adaptor in tree today claims atomically inside its spawn primitive (gt sling, native pane spawn).

const (
	// ClaimModelInline: claim happens inside StartSession. The
	// adaptor's spawn primitive is atomic with the hook (e.g. gt
	// sling's bead-already-hooked rejection, native's pane-spawn
	// path). The planner does NOT call ClaimNextReady; it picks a
	// candidate, calls StartSession, and treats an "already claimed"
	// error as a soft skip — pick the next candidate. ClaimNextReady
	// / ReleaseReservation may legitimately return KindUnsupported
	// for inline-claim adaptors.
	ClaimModelInline ClaimModel = "inline"

	// ClaimModelTwoPhase: separate reservation step. The planner
	// calls ClaimNextReady to obtain a TTL'd Reservation, then
	// StartSession to convert. Reservation auto-releases if the
	// session never spawns. Reserved for adaptors with explicit
	// hold-without-spawn semantics (none in tree today).
	ClaimModelTwoPhase ClaimModel = "two_phase"
)

func (ClaimModel) Resolved

func (c ClaimModel) Resolved() ClaimModel

Resolved returns the effective ClaimModel for this value. Empty (zero) defaults to ClaimModelInline so legacy manifests round-trip without explicit declarations and keep their prior behavior. Unknown values surface as-is (callers can branch / log).

type ConcurrencyModel

type ConcurrencyModel string

ConcurrencyModel names how the adaptor resolves simultaneous writes from N agents. R5.

const (
	// ConcurrencyOptimistic — compare-and-swap; conflict → fail.
	ConcurrencyOptimistic ConcurrencyModel = "optimistic"
	// ConcurrencyMVCC — row-level multi-version concurrency (Postgres
	// style).
	ConcurrencyMVCC ConcurrencyModel = "mvcc"
	// ConcurrencyGitMerge — three-way merge resolution against a git
	// working tree.
	ConcurrencyGitMerge ConcurrencyModel = "git-merge"
	// ConcurrencyDoltMerge — three-way merge resolution against Dolt.
	// Content-aware: row-level merges survive across simultaneous
	// table writes.
	ConcurrencyDoltMerge ConcurrencyModel = "dolt-merge"
)

type ConfirmNonce

type ConfirmNonce string

ConfirmNonce is the opaque token a caller echoes to confirm a mutating call that Gemba wants to be idempotent (pause, resume, end).

type CostAxis

type CostAxis string

CostAxis names a dimension the orchestration adaptor meters against. Gemba aggregates into a canonical CostMeter (gm-root DD-4); adaptors MUST declare at least one axis and emit samples against it.

const (
	// CostTokens — LLM tokens consumed (sum of prompt+completion).
	CostTokens CostAxis = "tokens"
	// CostWallclock — elapsed wall-clock seconds of session runtime.
	CostWallclock CostAxis = "wallclock"
	// CostDollarsNative — adaptor-native cost unit (e.g. Devin ACUs).
	// The manifest carries a configurable conversion rate.
	CostDollarsNative CostAxis = "dollars_native"
)

type CostMeter

type CostMeter struct {
	TokensTotal      int64   `json:"tokens_total"`
	WallclockSeconds float64 `json:"wallclock_seconds"`
	DollarsEst       float64 `json:"dollars_est"`
}

CostMeter is the aggregated three-axis cost on an Assignment. Gemba computes this from CostSamples; adaptors SHOULD NOT populate it directly.

type CostSample

type CostSample struct {
	At               time.Time `json:"at"`
	Tokens           *int64    `json:"tokens,omitempty"`
	WallclockSeconds *float64  `json:"wallclock_seconds,omitempty"`
	DollarsNative    *float64  `json:"dollars_native,omitempty"`
	DollarsEst       *float64  `json:"dollars_est,omitempty"`
}

CostSample is a single metered tick from an adaptor. One of Tokens, WallclockSeconds, DollarsNative may be set (or all three); the axis key in the manifest's cost_axes[] tells the UI which are authoritative.

type DefaultGitRunner

type DefaultGitRunner struct{}

DefaultGitRunner shells to the `git` binary on PATH. Returned errors include the git stderr output for diagnosis.

func (DefaultGitRunner) RemoteURL

func (DefaultGitRunner) RemoteURL(workspaceDir, remote string) (string, error)

RemoteURL runs `git -C <dir> remote get-url <remote>`.

func (DefaultGitRunner) SymbolicRef

func (DefaultGitRunner) SymbolicRef(workspaceDir string) (string, error)

SymbolicRef runs `git -C <dir> symbolic-ref --short HEAD`.

type DefinitionOfDone

type DefinitionOfDone struct {
	AcceptanceCriteria []string `json:"acceptance_criteria"`
	Notes              string   `json:"notes,omitempty"`
	Version            string   `json:"version,omitempty"`
}

DefinitionOfDone is the informational-only DoD surface: the list of acceptance criteria, human notes, and a schema-ish version string.

"Informational-only" is a gm-root DoD rule and a locked architectural decision: Gemba never blocks a state transition because a DoD item is unchecked. The UI may render the checklist and surface drift, but the operator (or the adaptor's own rules) decides.

Version is a free-form tag adaptors set when the shape of their DoD template changes (e.g. "v2", "2026-04"). The UI uses it to avoid rendering stale criteria against a newer work item.

type DerivedSignals

type DerivedSignals struct {
	// AgentClaimable — an automated agent can pick this card up now.
	AgentClaimable bool `json:"agent_claimable"`

	// HumanActionRequired — at least one blocking escalation is open
	// against this card; forward progress needs a human.
	HumanActionRequired bool `json:"human_action_required"`

	// ReviewPending — an open escalation is waiting on a human review
	// or approval (HITL approval, permission prompt, MCP elicitation,
	// A2A input-required).
	ReviewPending bool `json:"review_pending"`
}

DerivedSignals are UI-facing booleans computed from a WorkItem plus the open escalations attached to it. They answer the three questions the Kanban, backlog, and agents dashboards ask most often:

  • "Which cards can I hand to an agent right now?" → AgentClaimable
  • "Which cards are stuck waiting on a human?" → HumanActionRequired
  • "Which cards are blocked on a review decision?" → ReviewPending

The foolery spike (docs/prior-art/foolery.md) demonstrated the value of these flat booleans — Foolery's Beat carries `isAgentClaimable`, `requiresHumanAction`, and `nextActionState` precomputed, and every lane / filter site reads them directly instead of rebuilding the predicate from native status plus escalation state. Gemba's equivalent lives here.

DerivedSignals are emitted on the wire as WorkItem.Derived. They are the output of a pure function: callers MUST always recompute from the current item + escalation list rather than persist them across reconnects.

func Derive

func Derive(item WorkItem, escalations []EscalationRequest, manifest CapabilityManifest) DerivedSignals

Derive computes DerivedSignals from a WorkItem, the open escalations attached to it, and the WorkPlane's CapabilityManifest. Pure: no I/O, no side effects, deterministic in its inputs.

Rules (locked; any change needs a bead and a truth-table update in derived_test.go):

  1. AgentClaimable is true iff ALL of: - item.StateCategory == StateUnstarted, - item.Assignee == nil (no one holds the card), - no provided escalation has State=open AND Urgency=blocking, - manifest.StateMap is non-empty (else the adaptor hasn't declared how to normalise its native statuses — gm-cpk's HasDeclaredState=false — and StateCategory can't be trusted enough to invite agents to pick it up).

  2. HumanActionRequired is true iff any provided escalation has State=open AND Urgency=blocking. The manifest is not consulted: an escalation is real regardless of the adaptor's state-map hygiene.

  3. ReviewPending is true iff any provided escalation has State=open AND Source ∈ { hitl_approval, permission_prompt, mcp_elicitation, a2a_input_required } — the sources whose resolution hands control back to a human reviewer. Urgency is ignored here so advisory approvals still surface as "waiting on review" in the UI.

escalations is expected to contain only the escalations that belong to item (callers filter by WorkItemID at the adaptor boundary). Derive does not re-filter.

type DispatchStatus

type DispatchStatus string

DispatchStatus is the planner soft-block enum (gm-s47n.1.1, work-planning.md §4 Layer 0). The empty string is treated as DispatchReady — the default for any bead that hasn't been explicitly soft-blocked. The five non-empty values map onto the vocabulary the operator types into a "dispatch:" label.

const (
	// DispatchReady — the bead is eligible for auto-dispatch.
	// Default when the field is unset; only DispatchReady beads
	// appear in the planner's "what's next" surface.
	DispatchReady DispatchStatus = "ready"
	// DispatchAwaitingDesign — operator wants a design pass before
	// the bead is dispatchable. Visible in `bd list`; suppressed
	// from auto-dispatch.
	DispatchAwaitingDesign DispatchStatus = "awaiting-design"
	// DispatchAwaitingVendor — blocked on an external dependency
	// outside the operator's control (vendor SDK, third-party API).
	DispatchAwaitingVendor DispatchStatus = "awaiting-vendor"
	// DispatchAwaitingReview — the bead's prior dispatch produced
	// output that needs operator review before further work.
	DispatchAwaitingReview DispatchStatus = "awaiting-review"
	// DispatchNotNow — operator-specified deferral with no concrete
	// trigger. Distinct from awaiting-* in that no automated event
	// will lift the soft-block; the operator does it manually.
	DispatchNotNow DispatchStatus = "not-now"
)

func (DispatchStatus) Effective

func (s DispatchStatus) Effective() DispatchStatus

Effective returns DispatchReady when s is the empty string and the value verbatim otherwise. Consumers (the planner's selection pass, the SPA's dispatch chip) call this to avoid scattering the "empty string means ready" rule across call sites.

func (DispatchStatus) IsValid

func (s DispatchStatus) IsValid() bool

IsValid reports whether s is one of the five canonical statuses or the empty string (which the consumers normalise to DispatchReady). Round-trippers reject anything else.

type EdgeExtension

type EdgeExtension struct {
	Name        string `json:"name"`
	Directed    bool   `json:"directed"`
	Inverse     string `json:"inverse,omitempty"`
	Description string `json:"description,omitempty"`
}

EdgeExtension declares an adaptor-native relationship kind that is not one of the three core edges (gm-root DD-9). The capability-negotiation UI renders the extension's Name when the adaptor's own extension widget is loaded, and falls back to "relates_to" semantics otherwise.

Inverse is the name of the extension edge that the adaptor emits when the same logical link is walked backwards ("blocks" ↔ "blocked_by"). Leave empty when the edge is symmetric or has no defined inverse.

type ErrorKind

type ErrorKind string

ErrorKind is the closed set of discriminators every adaptor-boundary error carries (gm-faz). The t3code spike showed that string-matching on free-form error messages to decide "retry or fail?" is the single biggest source of adaptor rot: a provider renames an exception string and every call site that grepped for "rate limit" stops retrying.

Adaptors MUST map every failure they surface to one of these kinds and set Retryable to its canonical value (see RetryableDefault) — or an explicit override when the transport genuinely knows better (e.g. a 429 that carried Retry-After=0). Conformance Group F (AssertAdaptorError) fails any adaptor that returns a bare error from a boundary method.

const (
	// KindValidation — input failed the adaptor's schema / precondition
	// check (bad WorkItemPatch, illegal state transition, malformed
	// manifest). NOT retryable: the caller must fix the input.
	KindValidation ErrorKind = "validation"
	// KindSessionNotFound — the session/assignment/work-item id the
	// caller referenced does not exist (or has been garbage-collected).
	// NOT retryable: the caller has stale state.
	KindSessionNotFound ErrorKind = "session_not_found"
	// KindSessionClosed — the session exists but has reached a terminal
	// state (completed/failed/canceled). NOT retryable.
	KindSessionClosed ErrorKind = "session_closed"
	// KindRequestFailed — a wire-level call to the adaptor's transport
	// failed (connection refused, 5xx, timeout mid-stream). Retryable by
	// default: the next attempt may succeed after the transient issue
	// clears.
	KindRequestFailed ErrorKind = "request_failed"
	// KindProcessFailed — the adaptor's subprocess/backend raised a
	// structured failure that is NOT a transport glitch (bd exit code 2,
	// LangGraph node raised, MCP tool returned isError=true). Default
	// NOT retryable; callers that know a specific subclass is transient
	// may set Retryable=true at construction.
	KindProcessFailed ErrorKind = "process_failed"
	// KindRateLimited — the adaptor is throttling (HTTP 429, provider
	// quota). Retryable after the cool-down period; adaptors SHOULD
	// populate Detail["retry_after_seconds"] when they know it.
	KindRateLimited ErrorKind = "rate_limited"
	// KindUnsupported — the caller requested a feature this adaptor
	// opted out of in its CapabilityManifest (ReadBudgetRollup on a
	// non-budget adaptor, PeekSession screenshot when peek_modes lacks
	// it). NOT retryable: the UI should hide the control rather than
	// retry.
	KindUnsupported ErrorKind = "unsupported"
	// KindCapabilityDenied — the adaptor CAN do the thing but the
	// caller lacks authority (agent tried to mutate another agent's
	// assignment, permission prompt was denied). NOT retryable by the
	// same actor.
	KindCapabilityDenied ErrorKind = "capability_denied"
	// KindAdaptorDegraded — the adaptor's backend is temporarily
	// unhealthy (Dolt hung, subprocess supervisor restarting). The
	// gm-b1 mutation gate surfaces this verbatim to the SPA banner.
	// Retryable once the /api/adaptors probe reports healthy again.
	KindAdaptorDegraded ErrorKind = "adaptor_degraded"
	// KindReadOnly — the adaptor is operating in a read-only mode and
	// cannot service the mutation the caller requested (the --dolt-url
	// SQL connector is the canonical case: it opens a direct Dolt
	// connection and deliberately refuses every write so two gemba
	// processes can't race to mutate the same beads schema). NOT
	// retryable: the caller must target a writable adaptor instead.
	KindReadOnly ErrorKind = "read_only"
)

func (ErrorKind) RetryableDefault

func (k ErrorKind) RetryableDefault() bool

RetryableDefault returns the canonical retry disposition for a kind. Adaptors SHOULD construct errors via NewAdaptorError which applies this default; the explicit Retryable field on AdaptorError lets the transport override when it genuinely knows better (a 429 with Retry-After=0, a 5xx served from a cached error page that will never clear, …).

func (ErrorKind) String

func (k ErrorKind) String() string

String satisfies fmt.Stringer and always returns the lowercase token.

func (ErrorKind) Valid

func (k ErrorKind) Valid() bool

Valid reports whether k is one of the nine canonical error kinds.

type EscalationChannel

type EscalationChannel string

EscalationChannel identifies how an EscalationRequest reached Gemba. The response path branches on this: ChannelNotification escalations answer back via the agent's Notification reply (SendKeys yes/no/ free-text); ChannelToolCall escalations answer back by writing the operator's reply as the next UserPromptSubmit (gm-97w7.1).

const (
	// ChannelNotification — surfaced via an agent-infra hook
	// (Claude Notification, MCP elicitation, A2A input-required).
	ChannelNotification EscalationChannel = "notification"
	// ChannelToolCall — surfaced via an explicit tool call the skill
	// instructed the agent to make. Today's primary path is the
	// `gemba-ask` CLI binary (gm-97w7.1); the MCP-tool variant lands
	// in a follow-up and uses the same channel token.
	ChannelToolCall EscalationChannel = "tool_call"
)

type EscalationFilter

type EscalationFilter struct {
	AssignmentID string            `json:"assignment_id,omitempty"`
	WorkItemID   WorkItemID        `json:"work_item_id,omitempty"`
	AgentID      AgentID           `json:"agent_id,omitempty"`
	Source       EscalationKind    `json:"source,omitempty"`
	Urgency      EscalationUrgency `json:"urgency,omitempty"`
	Workspace    string            `json:"workspace,omitempty"`
}

EscalationFilter narrows ListOpenEscalations.

Workspace was added in gm-vch2 so the Gemba walk agenda can scope a cross-worker escalation listing to a specific workspace (matching AgentFilter.Workspace's semantics). Adaptors that don't model per-workspace escalations MAY ignore this field; the canonical in-memory adaptor (native) treats it as a no-op until per-workspace scope rides through the EscalationRequest payload (declared-state / observed-state diff per gm-root §Novel-Mechanism §8).

type EscalationKind

type EscalationKind string

EscalationKind identifies a source an adaptor may raise an EscalationRequest from (gm-root DD-6). Adaptors declare the subset they emit so the UI can wire the right badges, inbox categories, and Kanban overlays.

const (
	// EscalationMCPElicitation — MCP server requested structured input.
	EscalationMCPElicitation EscalationKind = "mcp_elicitation"
	// EscalationA2AInputRequired — A2A task transitioned to input-required.
	EscalationA2AInputRequired EscalationKind = "a2a_input_required"
	// EscalationPermissionPrompt — agent tool-use permission request.
	EscalationPermissionPrompt EscalationKind = "permission_prompt"
	// EscalationHITLApproval — human-in-the-loop approval gate.
	EscalationHITLApproval EscalationKind = "hitl_approval"
	// EscalationOrchestratorPause — orchestrator paused autonomously
	// (e.g. conflict detected, auto-resolve failed).
	EscalationOrchestratorPause EscalationKind = "orchestrator_pause"
	// EscalationBlocker — Manager-authored skill surfaced a "## Blockers"
	// section in the assistant's transcript (gm-97w7.1). Always backed
	// by Channel=ChannelTranscript. Urgency is stamped by the scanner
	// from (kind, interaction_mode).
	EscalationBlocker EscalationKind = "blocker"
	// EscalationQuestion — Coach- or Manager-authored skill surfaced a
	// "## Questions" section in the assistant's transcript (gm-97w7.1).
	// Always Channel=ChannelTranscript; Urgency mode-dependent.
	EscalationQuestion EscalationKind = "question"
	// EscalationWitnessFinding — a witness pipeline finding promoted to
	// an EscalationRequest so the gemba walk agenda surfaces it
	// alongside other cross-worker escalations (gm-vch2). Today the
	// witness rig writes findings into mail; the lister in
	// internal/walk/sources/witness.go does the mail → EscalationRequest
	// translation. Once witness emits escalation.raised events natively
	// the canonical kind stays the same — only the bridge changes.
	EscalationWitnessFinding EscalationKind = "witness_finding"
	// EscalationRefineryRejection — a refinery rig merge rejection
	// promoted to an EscalationRequest so the operator's gemba walk
	// surfaces "this PR was rejected and needs a human" alongside
	// other escalations (gm-vch2). The refinery rig's reject path
	// must publish through the OrchestrationPlane carrying this kind;
	// see internal/walk/sources/refinery.go for the typed lister and
	// the upstream-publish follow-up.
	EscalationRefineryRejection EscalationKind = "refinery_rejection"
	// EscalationBeadsDegraded — synthetic escalation minted by the
	// walk's BeadsDegradedLister (gm-vch2) when an adaptor probe
	// reports Healthy=false. The escalation persists for the lifetime
	// of the degraded span (stable id) and disappears on the first
	// healthy probe afterwards.
	EscalationBeadsDegraded EscalationKind = "beads_degraded"
	// EscalationBeadDoneWithDirtyWorktree — bridge cleanliness check
	// (session-pool.md §5.4.2) refused a Working→Ready transition
	// because the agent emitted `bead-done` while the worktree had
	// uncommitted changes. The session stays in SessionWorking until
	// the operator triages. Surfaces a contract violation rather than
	// silently mask it.
	EscalationBeadDoneWithDirtyWorktree EscalationKind = "bead_done_with_dirty_worktree"
	// EscalationSessionStalledTurn — synthetic escalation raised by the
	// reconcile loop (session-pool.md §9) when a session reports its
	// bead is closed in beads but Session.ActiveTurnID remains set —
	// the agent crashed mid-`bead-done` emit. Operators triage the
	// stuck session.
	EscalationSessionStalledTurn EscalationKind = "session_stalled_turn"
)

type EscalationOption

type EscalationOption struct {
	Value string `json:"value"`
	Label string `json:"label"`
}

EscalationOption is a single choice in a multiple-choice escalation.

type EscalationRequest

type EscalationRequest struct {
	ID           string                `json:"id"`
	Source       EscalationKind        `json:"source"`
	Channel      EscalationChannel     `json:"channel,omitempty"`
	AssignmentID string                `json:"assignment_id,omitempty"`
	WorkItemID   WorkItemID            `json:"work_item_id,omitempty"`
	AgentID      AgentID               `json:"agent_id,omitempty"`
	Urgency      EscalationUrgency     `json:"urgency"`
	Title        string                `json:"title"`
	Prompt       string                `json:"prompt"`
	Options      []EscalationOption    `json:"options,omitempty"`
	Deadline     *time.Time            `json:"deadline,omitempty"`
	State        EscalationState       `json:"state"`
	Resolution   *EscalationResolution `json:"resolution,omitempty"`
	CreatedAt    time.Time             `json:"created_at"`
}

EscalationRequest is the unified shape MCP elicitation, A2A input-required, permission prompts, HITL approvals and orchestrator pauses all map onto (gm-root DD-6).

type EscalationResolution

type EscalationResolution struct {
	Kind       EscalationResolutionKind `json:"kind"`
	Value      any                      `json:"value,omitempty"`
	ResolvedBy AgentID                  `json:"resolved_by"`
	ResolvedAt time.Time                `json:"resolved_at"`
}

EscalationResolution is the outcome recorded when a human answers an escalation.

type EscalationResolutionKind

type EscalationResolutionKind string

EscalationResolutionKind names the four canonical resolutions.

const (
	ResolutionApprove EscalationResolutionKind = "approve"
	ResolutionDeny    EscalationResolutionKind = "deny"
	ResolutionModify  EscalationResolutionKind = "modify"
	ResolutionDefer   EscalationResolutionKind = "defer"
)

type EscalationState

type EscalationState string

EscalationState is the lifecycle tag on an EscalationRequest.

const (
	EscalationOpen     EscalationState = "open"
	EscalationResolved EscalationState = "resolved"
	EscalationCanceled EscalationState = "canceled"
	EscalationExpired  EscalationState = "expired"
)

type EscalationUrgency

type EscalationUrgency string

EscalationUrgency names whether an escalation suspends the session until it's answered.

const (
	UrgencyBlocking EscalationUrgency = "blocking"
	UrgencyAdvisory EscalationUrgency = "advisory"
)

type EstimatedSize

type EstimatedSize string

EstimatedSize is the rough bucket the bead's complexity falls into (gm-s47n.1.1). The empty string means "unestimated"; the planner treats unestimated beads as SizeMedium for the purpose of session-runway comparison so a missing estimate doesn't hide a bead from auto-dispatch entirely.

const (
	SizeSmall  EstimatedSize = "small"
	SizeMedium EstimatedSize = "medium"
	SizeLarge  EstimatedSize = "large"
)

func (EstimatedSize) Effective

func (s EstimatedSize) Effective() EstimatedSize

Effective returns SizeMedium when s is the empty string and the value verbatim otherwise. Mirrors DispatchStatus.Effective so "unestimated bead" and "explicitly-medium bead" land at the same runway-comparison input without scattering the default across the planner.

func (EstimatedSize) IsValid

func (s EstimatedSize) IsValid() bool

IsValid reports whether s is one of the three canonical sizes or the empty string (unestimated).

type EventDelivery

type EventDelivery string

EventDelivery names how an adaptor pushes OrchestrationEvents.

const (
	// EventDeliverySSE — server-sent events stream.
	EventDeliverySSE EventDelivery = "sse"
	// EventDeliveryPush — adaptor POSTs events back to Gemba.
	EventDeliveryPush EventDelivery = "push"
	// EventDeliveryPoll — Gemba polls; adaptor returns deltas.
	EventDeliveryPoll EventDelivery = "poll"
)

type Evidence

type Evidence struct {
	ID         string         `json:"id"`
	Kind       EvidenceKind   `json:"kind"`
	Source     string         `json:"source"`
	Ref        string         `json:"ref,omitempty"`
	Summary    string         `json:"summary,omitempty"`
	CapturedAt time.Time      `json:"captured_at"`
	Payload    map[string]any `json:"payload,omitempty"`
}

Evidence is a single artifact captured against a work item: a commit that implements it, a test run that verified it, a URL that documents it. Evidence is append-only from the UI's perspective; adaptors may reconstruct the list on every read.

type EvidenceKind

type EvidenceKind string

EvidenceKind enumerates the categories of artifact Gemba understands. Adaptors MAY attach opaque payloads by setting Kind = EvidenceCustom and populating Payload.

const (
	// EvidenceCommit — a VCS commit (SHA in Ref, repo URL in Source).
	EvidenceCommit EvidenceKind = "commit"
	// EvidenceLog — a log line, transcript excerpt, or streamed output.
	EvidenceLog EvidenceKind = "log"
	// EvidenceTestResult — output of a test run (pass/fail/duration).
	EvidenceTestResult EvidenceKind = "test_result"
	// EvidenceURL — an external link (PR, dashboard, dashboard screenshot).
	EvidenceURL EvidenceKind = "url"
	// EvidenceFile — a file artifact (path in Ref, adaptor knows how to read).
	EvidenceFile EvidenceKind = "file"
	// EvidenceCustom — adaptor-defined shape. Inspect Source + Payload.
	EvidenceCustom EvidenceKind = "custom"
)

type FieldExtension

type FieldExtension struct {
	Name        string `json:"name"`
	Type        string `json:"type"`
	Description string `json:"description,omitempty"`
}

FieldExtension declares an adaptor-native field that core does not know about. The UI will only render it inside `web/src/extensions/<adaptor-id>/` (gm-root DD-4).

Name is the JSON key the adaptor emits on WorkItem.Custom. Type is a free-form hint for the UI renderer ("string", "number", "duration", "url", "markdown", ...); the UI is responsible for mapping unknown types to a safe default widget.

type Flags

type Flags struct {
	// HasSprints — the adaptor emits first-class Sprint records. When
	// false, the UI hides the sprint lane chrome and ListSprints-backed
	// widgets. Derived from CapabilityManifest.SprintNative.
	HasSprints bool `json:"has_sprints"`

	// HasBudget — the adaptor enforces a TokenBudget with the three-tier
	// inform/warn/stop ladder (gm-root DD-14). When false, the UI may
	// still display a configured budget but the stop tier has no runtime
	// effect. Derived from CapabilityManifest.TokenBudgetEnforced.
	HasBudget bool `json:"has_budget"`

	// HasEvidence — core synthesizes Evidence records from transport
	// artifacts for this adaptor (gm-root DD-13). When true, the UI can
	// expect the evidence panel to populate even for backends that do
	// not natively emit Evidence; when false, evidence comes from the
	// adaptor itself. Either way the Evidence panel is renderable, so
	// consumers that only gate visibility should treat this as "on".
	// Derived from CapabilityManifest.EvidenceSynthesisRequired.
	HasEvidence bool `json:"has_evidence"`

	// HasDeclaredState — the manifest declares a non-empty StateMap, so
	// the UI can translate native statuses to the five core
	// StateCategory buckets without guessing. Effectively always true
	// for a Validate()-clean manifest, but surfaced explicitly so the UI
	// can fall back gracefully against a manifest that slipped through.
	// Derived from len(CapabilityManifest.StateMap) > 0.
	HasDeclaredState bool `json:"has_declared_state"`

	// HasExtensionEdges — the adaptor declares at least one
	// EdgeExtension beyond the three core edges. When false, the UI
	// does not need to load `web/src/extensions/<adaptor-id>/` edge
	// widgets. Derived from len(CapabilityManifest.EdgeExtensions) > 0.
	HasExtensionEdges bool `json:"has_extension_edges"`

	// HasExtensionFields — the adaptor declares at least one
	// FieldExtension on WorkItem.Custom. Derived from
	// len(CapabilityManifest.FieldExtensions) > 0.
	HasExtensionFields bool `json:"has_extension_fields"`

	// HasExtensionRelFields — the adaptor declares at least one
	// RelationshipExtension (per-edge metadata). Derived from
	// len(CapabilityManifest.RelationshipExtensions) > 0.
	HasExtensionRelFields bool `json:"has_extension_rel_fields"`
}

Flags is a flat-boolean projection of CapabilityManifest intended for UI gating (gm-root DD-15, gm-cpk). The rich CapabilityManifest is the right shape for server-side decisions — StateMap, EdgeExtensions, FieldExtensions, and so on carry the structural detail core needs to route work — but it is unwieldy for simple JSX gates such as `<Capability has="has_sprints">`. Flags answers one question per boolean: "should this UI control be considered available for the active work-plane?".

The foolery spike (docs/prior-art/foolery.md) demonstrated that a flat `BackendCapabilities` struct with named booleans (`canSync`, `canDelete`, ...) is directly consumable in JSX. Flags is gemba's equivalent and gives a well-defined upgrade path for external UI consumers (VS Code, Foolery-backend, third-party dashboards) that want the projection without re-deriving it from the rich manifest.

Flags is pure: the value is deterministic from the manifest. Callers MUST treat it as a cache — always regenerate from the current manifest rather than persisting it across reconnects.

type GitRunner

type GitRunner interface {
	// SymbolicRef returns the current branch (e.g. "main"). An empty
	// string + nil error means "detached HEAD" (rare in workspace
	// roots); the caller falls back to "main".
	SymbolicRef(workspaceDir string) (string, error)
	// RemoteURL returns the URL of the named remote (typically
	// "origin"). An empty string + nil error means the remote is not
	// configured.
	RemoteURL(workspaceDir, remote string) (string, error)
}

GitRunner is the small interface deriveRepositoryFromWorkspace uses to read git state. The default shells to the `git` binary; tests inject a fake to avoid filesystem + subprocess setup.

type GroupMembers

type GroupMembers struct {
	Static          []AgentID `json:"static,omitempty"`
	PoolCheckURL    string    `json:"pool_check_url,omitempty"`
	PoolMin         *int      `json:"pool_min,omitempty"`
	PoolMax         *int      `json:"pool_max,omitempty"`
	GraphTopologyID string    `json:"graph_topology_id,omitempty"`
	GraphResolved   []AgentID `json:"graph_resolved,omitempty"`
}

GroupMembers is the mode-tagged union of member descriptors. Only the subset matching Mode is meaningful; the rest MUST be zero.

type GroupMode

type GroupMode string

GroupMode names the three ways an orchestrator may present a group of agents. Per gm-root DD-7, every AgentGroup declares exactly one:

  • static — enumerated members (Gas Town convoy, CrewAI crew).
  • pool — elastic members behind an opaque check endpoint.
  • graph — topology-defined (LangGraph subgraph, ADK hierarchy).
const (
	// GroupStatic — membership enumerated at registration.
	GroupStatic GroupMode = "static"
	// GroupPool — membership derived from a live check endpoint.
	GroupPool GroupMode = "pool"
	// GroupGraph — membership derived from a topology the adaptor owns.
	GroupGraph GroupMode = "graph"
)

type GuardDecision

type GuardDecision struct {
	Allowed bool   `json:"allowed"`
	Reason  string `json:"reason,omitempty"`
}

GuardDecision is the pure output of CheckCapability. A caller that only needs a boolean can check Allowed; a caller that wants to surface a reason (logs, 403 response body, SPA banner) uses Reason.

Allowed=true implies Reason is empty; Allowed=false guarantees a non-empty Reason so the error path never loses its explanation.

func CheckCapability

func CheckCapability(m CapabilityManifest, op Operation) GuardDecision

CheckCapability reports whether op is allowed for a manifest. Pure: no side effects, no I/O, no randomness. Safe to call on every request.

The function is the primary capability gate (gm-4qf). The rule is: core enforces AT THE PORT — before a call reaches the adaptor — by translating the adaptor's declarative CapabilityManifest into a per-op allow/deny decision. Adaptors themselves MUST also fail-fast on undeclared ops (defense in depth); see EnforceCapability for the canonical error construction.

Lesson from the t3code audit: every adapter implementing its own enforcement drifts into four subtly different behaviors. Gating centrally here removes that drift and keeps the decision one Go-testable pure function away from the contract.

type IsolationCapabilities

type IsolationCapabilities struct {
	FSScoped        bool `json:"fs_scoped"`
	NetIsolated     bool `json:"net_isolated"`
	CPULimited      bool `json:"cpu_limited"`
	MemLimited      bool `json:"mem_limited"`
	SnapshotRestore bool `json:"snapshot_restore"`
}

IsolationCapabilities declares what a given workspace kind actually enforces. fs_scoped is the single MUST from gm-root DD-5; the rest are declared per-kind on the manifest and inspected by the negotiation layer when an assignment specifies required isolation.

type NopShader

type NopShader struct{}

NopShader is the default when no orchestrator config is loaded. Identity transform on both directions; satisfies the Shader interface so callers never need a nil-check.

func (NopShader) DecodeFromRead

func (NopShader) DecodeFromRead(_ context.Context, item WorkItem) (WorkItem, error)

func (NopShader) Describe

func (NopShader) Describe() ShaderManifest

func (NopShader) EncodeForWrite

func (NopShader) EncodeForWrite(_ context.Context, _ WriteOp, item WorkItem) (WorkItem, error)

type Operation

type Operation string

Operation is the closed set of adaptor-boundary operations whose availability depends on the adaptor's CapabilityManifest (gm-4qf, resolves DD-12 of the t3code audit). Only operations the manifest actually gates appear here; unconditionally-available operations (Describe, Get/List/Create/Update WorkItem) are included for completeness so callers can run the guard uniformly across every boundary call.

Values are stable wire tokens used as map keys and `Detail["operation"]` on AdaptorError payloads. Keep snake_case so SPA consumers can branch on them without a translation table.

const (
	// OpDescribe — Describe(). Always allowed; listed so uniform guard
	// wrappers can name every boundary method.
	OpDescribe Operation = "describe"
	// OpListWorkItems — ListWorkItems(). Always allowed.
	OpListWorkItems Operation = "list_work_items"
	// OpGetWorkItem — GetWorkItem(). Always allowed.
	OpGetWorkItem Operation = "get_work_item"
	// OpCreateWorkItem — CreateWorkItem(). Always allowed.
	OpCreateWorkItem Operation = "create_work_item"
	// OpUpdateWorkItem — UpdateWorkItem(). Always allowed.
	OpUpdateWorkItem Operation = "update_work_item"
	// OpAttachEvidence — AttachEvidence(). Gated by
	// CapabilityManifest.EvidenceSynthesisRequired: when the manifest
	// declares that the adaptor provides its own Evidence
	// (EvidenceSynthesisRequired=false), core MUST NOT call AttachEvidence.
	OpAttachEvidence Operation = "attach_evidence"
	// OpListSprints — ListSprints(). Gated by
	// CapabilityManifest.SprintNative.
	OpListSprints Operation = "list_sprints"
	// OpReadBudgetRollup — ReadBudgetRollup(). Gated by BOTH SprintNative
	// (rollups are per-sprint) and TokenBudgetEnforced (no budget, no
	// rollup).
	OpReadBudgetRollup Operation = "read_budget_rollup"
)

type OrchestrationCapabilityManifest

type OrchestrationCapabilityManifest struct {
	AdaptorID               string                                  `json:"adaptor_id"`
	AdaptorVersion          string                                  `json:"adaptor_version"`
	OrchestrationAPIVersion string                                  `json:"orchestration_api_version"`
	Transport               Transport                               `json:"transport"`
	WorkspaceKinds          []WorkspaceKind                         `json:"workspace_kinds"`
	DefaultWorkspaceKind    WorkspaceKind                           `json:"default_workspace_kind,omitempty"`
	PerKindIsolation        map[WorkspaceKind]IsolationCapabilities `json:"per_kind_isolation,omitempty"`
	GroupModes              []GroupMode                             `json:"group_modes"`
	AssignmentStrategies    []AssignmentStrategy                    `json:"assignment_strategies,omitempty"`
	// ClaimModel declares how the adaptor handles the atomic-claim
	// race between sessions competing for the same ready bead
	// (gm-e3.8). Empty/missing defaults to ClaimModelInline — every
	// in-tree adaptor today (gt, native, noop) claims atomically
	// inside its spawn primitive, so the default keeps pre-gm-e3.8
	// behavior identical. Use ClaimModel.Resolved() at read sites
	// to apply the default safely.
	ClaimModel          ClaimModel       `json:"claim_model,omitempty"`
	CostAxes            []CostAxis       `json:"cost_axes"`
	NativeCostUnit      string           `json:"native_cost_unit,omitempty"`
	NativeCostToDollars *float64         `json:"native_cost_to_dollars,omitempty"`
	EscalationKinds     []EscalationKind `json:"escalation_kinds"`
	PeekModes           []PeekMode       `json:"peek_modes"`
	EventDelivery       EventDelivery    `json:"event_delivery,omitempty"`
	Extension           map[string]any   `json:"extension,omitempty"`
}

OrchestrationCapabilityManifest is the describe() payload every OrchestrationPlaneAdaptor returns at registration. The capability- negotiation UI (gm-e11.4) reads this to hide controls the adaptor cannot service; the registry (gm-e3.4) uses it for version matching.

The six bead-required axes — transport, workspace_kinds, group_modes, cost_axes, escalation_kinds, peek_modes — are all non-omitempty so that a manifest serialised by one adaptor and parsed by the UI can never silently default to "all off".

type OrchestrationEvent

type OrchestrationEvent struct {
	ID           string         `json:"id"`
	Kind         string         `json:"kind"`
	At           time.Time      `json:"at"`
	AssignmentID string         `json:"assignment_id,omitempty"`
	SessionID    string         `json:"session_id,omitempty"`
	AgentID      AgentID        `json:"agent_id,omitempty"`
	Payload      map[string]any `json:"payload,omitempty"`
}

OrchestrationEvent is the streamed envelope covering session lifecycle, cost samples, escalation state transitions, and potential_conflict signals (domain.md §3.5).

type OrchestrationPlaneAdaptor

type OrchestrationPlaneAdaptor interface {
	// Describe returns the capability manifest.
	Describe() OrchestrationCapabilityManifest

	// DeclaredState returns the topology the adaptor's configuration
	// requests. For config-driven orchestrators this is cheap; for
	// pure-runtime adaptors without a declared form it MAY return an
	// empty topology with only CapturedAt set.
	DeclaredState(ctx context.Context) (WorkspaceTopology, error)

	// ObservedState returns the topology currently running. Callers
	// diff against DeclaredState to surface drift.
	ObservedState(ctx context.Context) (WorkspaceTopology, error)

	// ListAgents returns the agents the adaptor currently knows about,
	// filtered by f.
	ListAgents(ctx context.Context, f AgentFilter) ([]AgentRef, error)
	// ReadAgent returns a single agent, or nil if unknown.
	ReadAgent(ctx context.Context, id AgentID) (*AgentRef, error)

	// ListGroups returns every declared AgentGroup.
	ListGroups(ctx context.Context) ([]AgentGroup, error)
	// ResolveGroupMembers returns the current members of a group. For
	// pool groups this may call the check_endpoint; for graph groups
	// the adaptor resolves the topology.
	ResolveGroupMembers(ctx context.Context, groupID string) ([]AgentRef, error)

	// ClaimNextReady atomically reserves the next ready work item for
	// claimant. Returns (nil, nil) when no work is available (the
	// caller distinguishes this from an error). On a non-nil reservation,
	// the adaptor MUST emit a `reservation_claimed` OrchestrationEvent.
	ClaimNextReady(ctx context.Context, f ReadyFilter, claimant AgentRef) (*Reservation, error)
	// ReleaseReservation releases a reservation without converting it.
	// MUST emit a `reservation_released` OrchestrationEvent on success.
	ReleaseReservation(ctx context.Context, reservationID string) error

	// StartSession starts a session against an existing assignment and
	// returns the live Session. The adaptor MUST reject calls whose
	// assignmentID is unknown (conformance B.3) and MUST emit a
	// `session_transition` event with the new session's state on success.
	StartSession(ctx context.Context, assignmentID string, prompt SessionPrompt) (Session, error)
	// PauseSession moves a non-terminal session to Suspended (user-
	// initiated pause). Idempotent under the same nonce. MUST emit a
	// `session_transition` event (first call only; replays under the
	// same nonce emit nothing).
	PauseSession(ctx context.Context, sessionID string, nonce ConfirmNonce) (Session, error)
	// ResumeSession moves a Suspended session back to whichever of the
	// observable states applies (typically Working). MUST emit a
	// `session_transition` event on the first call.
	ResumeSession(ctx context.Context, sessionID string, nonce ConfirmNonce) (Session, error)
	// EndSession terminates a session with the given mode. Idempotent
	// in two reinforcing senses (conformance B.4, t3code audit):
	//
	//   1. Same-nonce replay is a no-op: calling EndSession twice with
	//      the same (sessionID, nonce) MUST return the same Session and
	//      emit zero additional events on the replay.
	//   2. Terminal state is absorbing: calling EndSession on an already
	//      terminal session (Status completed / failed) MUST be a no-op
	//      even under a fresh nonce — return the terminal Session, do
	//      not error, do not emit a second event. This protects against
	//      multiple defensive callers (reaper, stop-stale, explicit
	//      stop) racing without any one of them having to check status
	//      first.
	//
	// On a first-time close, adaptors MUST populate Session.CloseReason
	// with the typed SessionCloseReason cause before emitting the
	// `session_transition` event. The caller-supplied mode is the
	// *intent*; CloseReason is the *recorded cause* (which may
	// disagree — e.g. mode=completed + reason=transport_error).
	EndSession(ctx context.Context, sessionID string, mode SessionEndMode, nonce ConfirmNonce) (Session, error)
	// RecycleSession resets a session's context window without tearing
	// down its pane (session-pool.md §5.2). Same pool slot, new
	// session id, fresh profile. Adaptors that don't support pool
	// semantics return KindUnsupported. The adaptor MUST refuse on a
	// dirty worktree (§5.2 step 2) — that path converts the request
	// into an end-and-respawn rather than `git reset --hard`. On
	// success the adaptor MUST emit a `session.recycled` event with
	// the prior + new session ids and the trigger reason.
	//
	// The session must be in Status=Ready (idle pool member) for
	// recycle to be valid. Mid-bead recycle is rejected with
	// KindValidation. The pane id and worktree path remain stable
	// across the recycle; only the logical session id changes.
	//
	// Returns the new core.Session record (post-recycle, freshly
	// minted id, Status=SessionInitializing).
	RecycleSession(ctx context.Context, sessionID string) (Session, error)
	// PeekSession returns a snapshot of a live session. The populated
	// fields are gated by manifest.peek_modes.
	PeekSession(ctx context.Context, sessionID string) (SessionPeek, error)
	// ListSessions returns every session the adaptor currently knows
	// about, filtered by f. Drives operator-visible session inventory
	// (e.g. /sessions in the SPA). Returns an empty slice (not nil) when
	// the adaptor has no live sessions. Adaptors MAY include recently
	// closed (terminal) sessions as a courtesy — gm-native.15 callers
	// filter client-side.
	ListSessions(ctx context.Context, f SessionFilter) ([]Session, error)
	// ListPendingRequests returns every open EscalationRequest
	// (permission prompt, HITL approval, MCP elicitation,
	// A2A input-required, orchestrator pause) currently attached to
	// the given session, in the canonical EscalationRequest shape
	// (DD-6). Every adaptor MUST expose this — without a common
	// method, the UI cannot offer generic "inspect what this session
	// is waiting on" recovery, and each adaptor re-invents its own
	// pending-request tracking (t3code audit). Unknown sessionID
	// returns KindSessionNotFound. Returns an empty slice (not nil)
	// when the session is running normally with nothing pending.
	ListPendingRequests(ctx context.Context, sessionID string) ([]EscalationRequest, error)

	// AcquireWorkspace provisions a workspace matching req. Errors
	// (rather than silently downgrading) if no supported kind satisfies
	// required_isolation. MUST emit a `workspace_acquired` event on
	// success.
	AcquireWorkspace(ctx context.Context, req WorkspaceRequest) (Workspace, error)
	// ReleaseWorkspace releases a workspace; idempotent. MUST emit a
	// `workspace_released` event on the first call.
	ReleaseWorkspace(ctx context.Context, workspaceID string) error
	// InspectWorkspace returns the current status of a workspace.
	InspectWorkspace(ctx context.Context, workspaceID string) (Workspace, error)

	// ListOpenEscalations returns escalations in state=open matching f.
	ListOpenEscalations(ctx context.Context, f EscalationFilter) ([]EscalationRequest, error)
	// ResolveEscalation records a resolution and, for blocking
	// escalations, unblocks the associated session. MUST emit an
	// `escalation_resolved` event on the first call under a given nonce;
	// for blocking escalations that resume the session, MUST also emit a
	// `session_transition` event.
	ResolveEscalation(ctx context.Context, escalationID string, r EscalationResolution, nonce ConfirmNonce) (EscalationRequest, error)

	// Subscribe streams OrchestrationEvents matching f. The adaptor
	// closes the channel when ctx is cancelled or the underlying
	// transport disconnects.
	Subscribe(ctx context.Context, f SubscribeFilter) (<-chan OrchestrationEvent, error)
}

OrchestrationPlaneAdaptor is the interface every agent-runtime adaptor (Gas Town, LangGraph, CrewAI, OpenHands, Devin, Factory, …) implements. It mirrors domain.md §3.7 plus gm-root §Novel-Mechanism §8 (declared_state / observed_state) for desired-vs-actual reconciliation.

Method contract summary:

  • Describe returns the capability manifest. Callers MAY cache it for the lifetime of the adaptor registration.
  • DeclaredState reports the topology the adaptor's configuration asks for (Gas City's `city.toml`, Gas Town's `gastown.toml`, LangGraph's static graph). ObservedState reports what the adaptor actually sees running. Gemba diffs the two to surface drift.
  • Claim/Start/Pause/Resume/End model the assignment lifecycle; mutating calls take a ConfirmNonce for idempotency.
  • Subscribe returns an async iterator of OrchestrationEvents; the adaptor closes the channel on ctx cancellation.
  • Every state-changing method (ClaimNextReady, ReleaseReservation, StartSession, PauseSession, ResumeSession, EndSession, AcquireWorkspace, ReleaseWorkspace, ResolveEscalation) MUST emit a matching OrchestrationEvent on Subscribe within the adaptor's declared latency budget (default 250ms for SSE/push, 5s for poll). A successful mutation with no matching event is a conformance failure (domain.md §3.8 Group E). See docs/adaptors/orchestration.md and the Foolery-spike lesson (docs/prior-art/foolery.md) — the UI's 500ms freshness bar is unmeetable when state updates require client polling.

Scope-first session lifecycle (domain.md §3.7, t3code audit): every Session lifecycle MUST be bounded by an explicit acquire → use → release scope owned by the caller. Adaptors MAY NOT own child processes or long-lived resources outside of an active scope — per- adaptor scope ownership is drift waiting to happen. Callers close the scope via EndSession (or scope teardown on context cancellation) and the adaptor MUST release any process, workspace lease, transport socket, or pending-request queue associated with the session.

Implementations MUST be safe for concurrent use.

type OrchestratorHook

type OrchestratorHook string

OrchestratorHook is one coordination guarantee an adaptor declares. Each value is a promise — absent means "orchestrator has to simulate this client-side". R8.

const (
	// HookReadySetSubscribe — the adaptor streams ready-set deltas
	// (new items becoming ready, items leaving the ready set) as
	// they happen, without polling.
	HookReadySetSubscribe OrchestratorHook = "ready-set-subscribe"
	// HookClaimAtomic — claim operations are atomic: two agents
	// racing to claim the same item produce exactly one winner.
	HookClaimAtomic OrchestratorHook = "claim-atomic"
	// HookEscalationIngest — the adaptor accepts structured
	// escalations as first-class records (not free-form comments).
	HookEscalationIngest OrchestratorHook = "escalation-ingest"
	// HookWorkCompleteAck — a work-complete write gets a
	// round-tripped ack so the orchestrator can distinguish
	// "write accepted" from "write in flight".
	HookWorkCompleteAck OrchestratorHook = "work-complete-ack"
	// HookPoolBulkDispatch — the adaptor accepts bulk dispatch of
	// N work items to a pool of agents in one round-trip (vs.
	// per-item RPCs).
	HookPoolBulkDispatch OrchestratorHook = "pool-bulk-dispatch"
)

type PeekMode

type PeekMode string

PeekMode names a live-introspection mode an orchestrator supports for running sessions. Gas Town's tmux capture-pane is a `transcript` peek; a screenshot UI is `screenshot`; a structured event dump is `structured`.

const (
	// PeekTranscript — plain-text tail of the session transcript.
	PeekTranscript PeekMode = "transcript"
	// PeekScreenshot — rendered snapshot (image bytes or URL).
	PeekScreenshot PeekMode = "screenshot"
	// PeekStructured — structured event/state dump.
	PeekStructured PeekMode = "structured"
)

type QueryLanguage

type QueryLanguage string

QueryLanguage names an optional query surface beyond the baseline WorkItemFilter. R2.

const (
	// QueryFilterOnly — only the structured WorkItemFilter surface.
	// Every conforming adaptor supports this implicitly; declaring it
	// means "no other query language is exposed".
	QueryFilterOnly QueryLanguage = "filter-only"
	// QueryJSONPath — JSONPath expressions against WorkItem shape.
	QueryJSONPath QueryLanguage = "jsonpath"
	// QuerySQLSubset — the adaptor's native SQL surface (e.g. Dolt,
	// Postgres). Typically read-only from the agent side.
	QuerySQLSubset QueryLanguage = "sql-subset"
	// QueryGraphQL — a GraphQL surface over the work items.
	QueryGraphQL QueryLanguage = "graphql"
)

type ReadyFilter

type ReadyFilter struct {
	Labels      []string `json:"labels,omitempty"`
	Kind        string   `json:"kind,omitempty"`
	MaxPriority *int     `json:"max_priority,omitempty"`
	SprintID    string   `json:"sprint_id,omitempty"`
	Repository  string   `json:"repository,omitempty"`
}

ReadyFilter narrows ClaimNextReady to the subset of ready work an agent is willing to pick up.

type Relationship

type Relationship struct {
	Kind RelationshipKind `json:"kind"`
	From WorkItemID       `json:"from"`
	To   WorkItemID       `json:"to"`
}

Relationship is a typed edge between two work items. Edges are stored on the source item's Relationships slice; adaptors are responsible for emitting the inverse if they want both directions walkable without a full scan. Only the three kinds above are valid core edges; adaptor extensions flow through CapabilityManifest (see gm-e3.2).

type RelationshipExtension

type RelationshipExtension struct {
	Name        string `json:"name"`
	Type        string `json:"type"`
	Description string `json:"description,omitempty"`
}

RelationshipExtension declares an adaptor-native field on the Relationship record itself (not a new edge kind). Use this when the adaptor carries metadata on every edge — Jira link categories, beads edge confidence scores, LangGraph data-flow contracts — that the UI extension may want to render alongside the core edge.

type RelationshipKind

type RelationshipKind string

RelationshipKind enumerates the directed edges the two planes share. Per gm-root DD-9, core recognises exactly three kinds; any richer adaptor-native edge type (Beads's 7 edges, Jira's link catalogue, LangGraph's control/data flow) either maps onto one of these at the adaptor boundary or is declared through the adaptor's CapabilityManifest and rendered as "relates_to" in the core Kanban UI.

const (
	// RelBlocks — directed: From blocks To (From must complete before
	// To can start). Adaptors whose native edge is "depends_on" map the
	// inverse to this edge at their boundary.
	RelBlocks RelationshipKind = "blocks"
	// RelParentChild — directed: From is the parent of To (epic→story,
	// supervisor→subtask, orchestrator→polecat work).
	RelParentChild RelationshipKind = "parent_child"
	// RelRelatesTo — advisory cross-reference. No ordering semantics.
	// This is the edge the capability-negotiation UI falls back to when
	// an adaptor declares a non-core edge type that core should still
	// render as "associated with".
	RelRelatesTo RelationshipKind = "relates_to"
)

type Repository

type Repository struct {
	// ID is the slug ("gemba", "frontend", "infra"). Matches the
	// filename stem of the `.gemba/repositories/<id>.toml` file the
	// repository was loaded from. Appears as an entry in
	// [WorkItem.RepositoryIDs] and (when applicable) as the value of
	// [WorkItem.PrimaryRepositoryID].
	ID RepositoryID `toml:"id" json:"id"`

	// Path is the absolute filesystem path to the canonical worktree
	// root. Required + must be absolute so spawn paths are
	// reproducible across sessions.
	Path string `toml:"path" json:"path"`

	// DefaultBranch is the branch new work checks out from when no
	// other branch is specified. Conventionally "main".
	DefaultBranch string `toml:"default_branch" json:"default_branch"`

	// URL is the origin URL (https or ssh form). Optional — the spawn
	// path uses Path; URL is metadata for the UI ("View on GitHub").
	URL string `toml:"url,omitempty" json:"url,omitempty"`

	// WorktreesDir is the directory new git worktrees are created in
	// when polecats run in parallel against this repo. Optional;
	// empty means "spawn in Path directly" (single-worktree mode,
	// suitable for solo work). Relative paths resolve against Path.
	WorktreesDir string `toml:"worktrees_dir,omitempty" json:"worktrees_dir,omitempty"`

	// BeadPrefix is the bead-id prefix that identifies this
	// repository in cross-repo contexts (gm-d2ts). With multi-repo
	// beads (gm-kdh3) and N repos in one workspace, a bare ID like
	// `gm-e3` is no longer unique without a repo qualifier; the
	// prefix carries that identity directly:
	//
	//	gemba    → "gm"  (gm-e3, gm-518)
	//	frontend → "fe"  (fe-e3, fe-bug-44)
	//	backend  → "be"  (be-e3, be-perf-12)
	//
	// Validate enforces 2–8 chars of [a-z0-9-] and uniqueness across
	// the workspace's [RepositoryRegistry]. The bd adaptor uses
	// [RepositoryRegistry.GetByPrefix] to route an inbound bead to
	// its repository when the operator hasn't explicitly tagged it
	// with a `repo:*` label.
	BeadPrefix string `toml:"bead_prefix" json:"bead_prefix"`
	// contains filtered or unexported fields
}

Repository is one git repository associated with a workspace (gm-26n4). A workspace (≡ project) holds one beads database and ONE-OR-MORE repositories. Every WorkItem is associated with one or more [Repository]s via [WorkItem.RepositoryIDs] (gm-kdh3); the spawn path reads [WorkItem.PrimaryRepositoryID] to pick the working directory and worktree pool for an agent session.

Repository is the on-disk reality (path, branch, origin); the abstract notion of "what work this is" stays on WorkItem. Splitting the two lets one repository back many work items without storing per-bead path/branch metadata, and lets the same WorkItem schema project across adaptors that have nothing like a filesystem (LangGraph runs, Jira issues with no linked repo) by leaving RepositoryID = RepositoryUnspecified.

func LoadRepositoryFile

func LoadRepositoryFile(path string) (*Repository, error)

LoadRepositoryFile reads a single `.gemba/repositories/<id>.toml` file. The file's basename (minus .toml) MUST match the file's declared `id` field — same invariant the persona loader enforces.

func (Repository) ResolveWorktreesDir

func (r Repository) ResolveWorktreesDir() string

ResolveWorktreesDir returns the absolute path to the worktree pool for r. Returns r.Path when WorktreesDir is empty (single-worktree mode); otherwise joins r.Path with WorktreesDir for relative values.

func (Repository) SourcePath

func (r Repository) SourcePath() string

SourcePath returns the on-disk file the Repository was loaded from, or "" when constructed programmatically (tests).

func (Repository) Validate

func (r Repository) Validate() error

Validate checks the structural invariants the loader and spawn paths rely on. Does NOT check that Path exists on disk — a repo can be declared before it is cloned, and the caller (spawn) is the right place to surface "checkout missing" errors.

type RepositoryID

type RepositoryID string

RepositoryID is the slug naming a Repository inside a workspace (gm-26n4). One workspace may hold many repositories; every WorkItem is associated with exactly one. The slug appears as the second segment of WorkItemID and as the filename stem of the `.gemba/repositories/<id>.toml` file the Repository is loaded from.

const RepositoryUnspecified RepositoryID = "unspecified"

RepositoryUnspecified is the sentinel value [WorkItem.PrimaryRepositoryID] takes when a bead was filed before repository tracking landed (gm-26n4) or when an adaptor cannot derive a repository for a native record. Spawn paths reject "unspecified" beads with a caller-visible error so the operator backfills before working.

type RepositoryRegistry

type RepositoryRegistry struct {
	// contains filtered or unexported fields
}

RepositoryRegistry is the in-memory lookup table. Built once at startup (or on workspace switch) and treated as read-only for concurrent reads thereafter — same contract as internal/core/persona.Registry.

func LoadRepositoryRegistry

func LoadRepositoryRegistry(dir string) (*RepositoryRegistry, error)

LoadRepositoryRegistry constructs a RepositoryRegistry from every repository TOML file under dir. Uses Register so prefix uniqueness is enforced as files load (a workspace with two repos sharing a prefix is unbootable — the second registration fails).

When dir is empty (no TOMLs) AND the workspace appears to be a git repository, LoadRepositoryRegistryWithAutoDerive is the right entry point — it materializes a default Repository so single-repo workspaces don't require operator-authored TOML files (gm-i4bd). LoadRepositoryRegistry alone leaves the registry empty.

func LoadRepositoryRegistryWithAutoDerive

func LoadRepositoryRegistryWithAutoDerive(repositoriesDir, workspaceDir string, gitRunner GitRunner) (*RepositoryRegistry, error)

LoadRepositoryRegistryWithAutoDerive is the entry point that spares the common case (one workspace = one repository) the boilerplate of authoring `.gemba/repositories/<id>.toml` (gm-i4bd).

Behavior:

  1. If repositoriesDir contains any *.toml file, defer to LoadRepositoryRegistry. Operator-authored config wins; no auto-derive happens. This is the multi-repo path.

  2. Else, if workspaceDir contains a `.git/` directory, materialize a single Repository with:

    - ID = lowercased basename of workspaceDir (sanitized) - Path = workspaceDir (made absolute) - DefaultBranch = HEAD's symbolic-ref short name; "main" on error - URL = `git remote get-url origin`; empty on error - BeadPrefix = first 2 lowercase letters of the ID; "wp" fallback

    The result is a one-Repository registry the rest of Gemba treats identically to an operator-declared single-repo workspace. The auto-derived Repository is marked internally so the SPA can surface "auto-detected" badging (a `gemba repo persist` CLI to materialize a real .toml is a follow-up bead).

  3. Else, return an empty registry. The workspace has no repos; the spawn path will reject any consult that requires one with a clear error.

gitRunner is the function used to invoke git. Pass nil to use DefaultGitRunner (shells to `git`); tests inject a fake.

func NewRepositoryRegistry

func NewRepositoryRegistry() *RepositoryRegistry

NewRepositoryRegistry returns an empty RepositoryRegistry.

func (*RepositoryRegistry) Get

Get returns the repository registered under id. The bool is false when no repository is registered under that id.

func (*RepositoryRegistry) GetByPrefix

func (r *RepositoryRegistry) GetByPrefix(prefix string) (*Repository, bool)

GetByPrefix returns the repository whose [Repository.BeadPrefix] equals prefix (gm-d2ts). The bool is false when no registered repository claims that prefix. Used by the bd adaptor to route an inbound bead like `fe-e3` to the "frontend" repository when the operator has not yet (or chooses not to) tag the bead with an explicit `repo:*` label.

func (*RepositoryRegistry) List

func (r *RepositoryRegistry) List() []RepositoryID

List returns the registered repository IDs in ascending order.

func (*RepositoryRegistry) Register

func (r *RepositoryRegistry) Register(repo *Repository) error

Register adds r to the registry. Returns an error when r is nil, r.ID is empty, r.Validate fails, another repo is already registered under r.ID, or another repo has already claimed r.BeadPrefix (gm-d2ts). Re-registering the same pointer is a no-op so init paths can be defensive without erroring out.

type Reservation

type Reservation struct {
	ID         string     `json:"id"`
	WorkItemID WorkItemID `json:"work_item_id"`
	AgentID    AgentID    `json:"agent_id"`
	ExpiresAt  time.Time  `json:"expires_at"`
	Nonce      string     `json:"nonce"`
}

Reservation is the short-lived claim returned by ClaimNextReady. A reservation is converted to an Assignment by the matching WorkPlane claim; if the adaptor's TTL expires first, the reservation releases itself (domain.md §3.4 step 5 + conformance B.2).

type SchemaEnforcement

type SchemaEnforcement string

SchemaEnforcement classifies how the adaptor's store enforces the core WorkItem schema. R1.

const (
	// SchemaNative — the substrate enforces the schema (SQL columns,
	// typed API, required fields). A malformed write fails at write
	// time, not on read.
	SchemaNative SchemaEnforcement = "native"
	// SchemaSynthesized — the substrate is unstructured (flat file,
	// free-form JSON) and the adaptor reconstructs the schema on read
	// with best-effort projection.
	SchemaSynthesized SchemaEnforcement = "synthesized"
)

type Session

type Session struct {
	ID               string              `json:"id"`
	AssignmentID     string              `json:"assignment_id"`
	AgentID          AgentID             `json:"agent_id"`
	Status           SessionStatus       `json:"status"`
	ActiveTurnID     string              `json:"active_turn_id,omitempty"`
	StartedAt        time.Time           `json:"started_at"`
	EndedAt          *time.Time          `json:"ended_at,omitempty"`
	LastHeartbeat    *time.Time          `json:"last_heartbeat,omitempty"`
	TranscriptRef    string              `json:"transcript_ref,omitempty"`
	CloseReason      *SessionCloseReason `json:"close_reason,omitempty"`
	CostSamples      []CostSample        `json:"cost_samples,omitempty"`
	ProviderMetadata map[string]any      `json:"provider_metadata,omitempty"`

	// Persona names the persona the session is running (session-pool.md
	// §3.2). Pool key is `(rig, persona)`, so daemons filter idle
	// sessions by this field. Populated from the SessionPrompt
	// extension key `gemba:persona_id` at StartSession; empty when
	// the session has no persona binding (today's manual flow). The
	// adaptor must persist this through recycle so a recycled session
	// stays bound to its pool.
	Persona string `json:"persona,omitempty"`
}

Session is an ephemeral run of an agent against an assignment (C10 — sessions are ephemeral, identities persist). Adaptors append cost samples and heartbeats; Gemba aggregates on read.

ActiveTurnID names the in-flight turn (a model generation, a tool call round-trip) the session is currently servicing. When non-empty, reapers, idle-kill timers, and stop-stale sweeps MUST skip the session — killing mid-turn corrupts transcripts and loses pending escalations. The adaptor sets this on turn start and clears it on turn completion; callers treat empty as "safe to interrupt". This field is mandated by the contract (t3code audit lesson) so that active-turn protection cannot be forgotten per-adaptor.

CloseReason carries the typed reason a session reached a terminal status. It is set by the adaptor at the moment the session closes and MUST be present on the Session payload of any terminal `session_transition` event so callers can decide resume vs restart vs give-up without parsing free-form strings (DD-6 / t3code audit).

type SessionCloseReason

type SessionCloseReason string

SessionCloseReason is the typed cause carried on a closed Session. Adaptors MUST set exactly one of these values on Session.CloseReason before emitting the terminal `session_transition` event. Callers branch on the reason to choose resume vs restart vs give-up; without a typed value each caller invents ad-hoc stderr heuristics (t3code audit finding).

const (
	// CloseProviderExit — the agent-runtime provider process exited
	// cleanly or crashed outside of Gemba's control.
	CloseProviderExit SessionCloseReason = "provider_exit"
	// CloseTransportError — the wire link to the adaptor dropped
	// (socket closed, TLS error, HTTP stream reset).
	CloseTransportError SessionCloseReason = "transport_error"
	// CloseUserStop — a human or orchestrator issued an explicit stop.
	// This is the companion to SessionEndCanceled / SessionEndCompleted
	// when the caller asked for the close.
	CloseUserStop SessionCloseReason = "user_stop"
	// CloseIdleTimeout — idle-kill timer fired because no turn was
	// active and the heartbeat age exceeded the adaptor's threshold.
	CloseIdleTimeout SessionCloseReason = "idle_timeout"
	// CloseFatalStderr — the provider emitted a stderr signature the
	// adaptor classifies as unrecoverable (OOM, panic, assertion).
	CloseFatalStderr SessionCloseReason = "fatal_stderr"
	// CloseProtocolError — the provider violated the adaptor's wire
	// contract (malformed JSONL, unknown MCP method, version mismatch).
	CloseProtocolError SessionCloseReason = "protocol_error"
	// CloseBudgetStop — a cost budget hit its ceiling and the budget
	// enforcer asked the adaptor to end the session.
	CloseBudgetStop SessionCloseReason = "budget_stop"
	// CloseEscalationPause — a blocking escalation expired or resolved
	// to a decision that terminates the session rather than resumes it.
	CloseEscalationPause SessionCloseReason = "escalation_pause"
)

func (SessionCloseReason) String

func (r SessionCloseReason) String() string

String satisfies fmt.Stringer and always returns the lowercase token.

func (*SessionCloseReason) UnmarshalJSON

func (r *SessionCloseReason) UnmarshalJSON(data []byte) error

UnmarshalJSON rejects unknown close reasons so adaptors can't quietly expand the enum on the wire past what consumers understand.

func (SessionCloseReason) Valid

func (r SessionCloseReason) Valid() bool

Valid reports whether r is one of the canonical close reasons.

type SessionEndMode

type SessionEndMode string

SessionEndMode names the terminal intent when EndSession is called. This is the *caller's request*; SessionCloseReason is the *recorded cause* attached to the Session once it has closed. They often line up (caller requests `canceled` → adaptor records `user_stop`), but a provider-driven close (e.g. transport error during a caller's `completed` request) MUST still set the true close reason.

const (
	SessionEndCompleted SessionEndMode = "completed"
	SessionEndFailed    SessionEndMode = "failed"
	SessionEndCanceled  SessionEndMode = "canceled"
)

type SessionFilter

type SessionFilter struct {
	Status          []SessionStatus `json:"status,omitempty"`
	AgentID         AgentID         `json:"agent_id,omitempty"`
	IncludeTerminal bool            `json:"include_terminal,omitempty"`
}

SessionFilter narrows ListSessions. Empty filter means "every session the adaptor currently tracks." Status, when non-empty, matches sessions whose Status equals one of the listed values (logical OR). AgentID, when non-empty, matches the AgentID field exactly. IncludeTerminal — when true, terminal sessions (completed/failed) are included; default behavior excludes them so the SPA's live-pane list isn't cluttered with stale rows.

type SessionPeek

type SessionPeek struct {
	SessionID  string         `json:"session_id"`
	Status     SessionStatus  `json:"status"`
	CapturedAt time.Time      `json:"captured_at"`
	Transcript string         `json:"transcript,omitempty"`
	Screenshot []byte         `json:"screenshot,omitempty"`
	Structured map[string]any `json:"structured,omitempty"`
}

SessionPeek is the snapshot shape returned by peek_session. The set of populated fields is gated by the manifest's peek_modes[]; callers MUST tolerate any subset.

type SessionPrompt

type SessionPrompt struct {
	Text      string         `json:"text,omitempty"`
	Extension map[string]any `json:"extension,omitempty"`
}

SessionPrompt carries the seed input to start a session. Free-form text covers the common case; structured payloads (for MCP-style initialisation) flow through Extension.

type SessionStatus

type SessionStatus string

SessionStatus is the observable lifecycle tag on a Session (gm-d044).

The non-terminal values are observable: an adaptor reports them based on structured signals from the agent (preamble install + the `gemba-state` tool sentinel per gm-cdph). Operators reading the SPA can tell at a glance whether a session is spinning up, idle, doing work, or asking for input.

  • Initializing → hook installed; preamble / skills injection in flight. Ends when the agent acknowledges by emitting `ready` or `working`.
  • Ready → session is alive and idle; no bead assigned.
  • Working → a bead or active work item has been dispatched and the agent is executing.
  • Prompting → the agent has surfaced a question or blocker and is waiting for operator input (ui-spec §4.8 / gm-97w7).
  • Stalled → no observable progress for the configured window; UI-side heuristic today (gm-r1l1), adaptor ticker later (gm-dccy).
  • Suspended → user-initiated pause via PauseSession. Kept for the existing Pause/Resume contract; orthogonal to the initializing/ready/working/prompting observable set.
  • Completed → terminal; clean finish.
  • Failed → terminal; error or abort.
const (
	SessionInitializing SessionStatus = "initializing"
	SessionReady        SessionStatus = "ready"
	SessionWorking      SessionStatus = "working"
	SessionPrompting    SessionStatus = "prompting"
	SessionStalled      SessionStatus = "stalled"
	SessionSuspended    SessionStatus = "suspended"
	SessionCompleted    SessionStatus = "completed"
	SessionFailed       SessionStatus = "failed"
)

type Shader

type Shader interface {
	// EncodeForWrite rewrites a native WorkItem into the form the
	// underlying adaptor should persist. op tells the shader whether
	// the call originated from CreateWorkItem or UpdateWorkItem; some
	// transforms (e.g. id-minting) only fire on Create.
	EncodeForWrite(ctx context.Context, op WriteOp, item WorkItem) (WorkItem, error)

	// DecodeFromRead is the inverse: turn an adaptor-stored WorkItem
	// back into the native shape the SPA expects. MUST be a pure
	// function of item — the SPA calls Get / List independently and
	// expects identical output regardless of order.
	DecodeFromRead(ctx context.Context, item WorkItem) (WorkItem, error)

	// Describe returns the shader's identity + advertised contract.
	// Idempotent and side-effect-free.
	Describe() ShaderManifest
}

Shader is the encode/decode pair every orchestrator-flavour implements. Every method MUST be safe to call on the zero value and MUST NOT mutate the input WorkItem in place — return a new value if any field changes.

type ShaderManifest

type ShaderManifest struct {
	// Name identifies the shader implementation ("nop", "gastown", …).
	Name string `json:"name"`
	// EncodedFields lists which WorkItem fields the shader rewrites
	// on write. Lets the UI hint to operators which values they're
	// editing in native vs encoded form. Common entries: "title",
	// "labels", "description". Empty for NopShader.
	EncodedFields []string `json:"encoded_fields,omitempty"`
}

ShaderManifest is the declarative description every shader returns from Describe. Surfaced through /api/capabilities so the SPA can show "shader: gastown" in the workspace banner without poking the shader directly.

type Sprint

type Sprint struct {
	ID        string       `json:"id"`
	Name      string       `json:"name"`
	StartsAt  time.Time    `json:"starts_at"`
	EndsAt    time.Time    `json:"ends_at"`
	Goal      string       `json:"goal,omitempty"`
	WorkItems []WorkItemID `json:"work_items,omitempty"`
	Budget    *TokenBudget `json:"budget,omitempty"`
}

Sprint is a time-boxed container of work that carries an optional TokenBudget. A sprint is not required — adaptors without Scrum-shaped vocabulary (GitHub Issues, raw beads-only setups) may simply omit WorkItem.SprintID. The locked DoD at gm-root / DD-14 keeps Sprint and TokenBudget live in v1 with three-tier enforcement.

type StateCategory

type StateCategory string

StateCategory is the adaptor-agnostic normalization of a work item's lifecycle position. Every adaptor maps its native statuses ("open", "in_progress", "done", "To Do", "In Review", ...) onto one of these six buckets so the UI can render a consistent Kanban board without knowing Beads-specific or Jira-specific vocabulary.

The native status string is preserved on WorkItem.Status; StateCategory is strictly a secondary, normalized view of it.

Canonical column order (ui-spec §4.3):

Backlog → Next Up (Unstarted) → Staged → In Progress (Started) → Done (Completed) → Canceled
const (
	// StateBacklog — formally tracked but not yet triaged into a sprint or
	// an active queue. Includes deferred, icebox, parking-lot style states.
	StateBacklog StateCategory = "backlog"

	// StateUnstarted — triaged and ready to pick up, but no one is working
	// on it yet. "open", "ready", "To Do". Renders as "Next Up" in the
	// UI per ui-spec §4.3.
	StateUnstarted StateCategory = "unstarted"

	// StateStaged — explicitly staged for execution this cycle. The
	// operator has decided "yes, work this next" but no one has started
	// yet. Distinct from StateUnstarted (a passive ready-pile) — Staged
	// is an active commitment. ui-spec §4.3 calls this column "Staged".
	// Adaptors map to it via convention (bd: a `staged:true` label or
	// equivalent) — backends without a native staging concept may simply
	// never emit this value.
	StateStaged StateCategory = "staged"

	// StateStarted — actively in progress. "in_progress", "In Review",
	// "Doing", "hooked", "pinned". Renders as "In Progress" per ui-spec.
	StateStarted StateCategory = "started"

	// StateCompleted — finished successfully. "closed", "done", "merged".
	// Renders as "Done" per ui-spec.
	StateCompleted StateCategory = "completed"

	// StateCanceled — terminally closed without completion. "won't fix",
	// "duplicate", "canceled", "rejected".
	StateCanceled StateCategory = "canceled"
)

func (StateCategory) String

func (s StateCategory) String() string

String satisfies fmt.Stringer and always returns the lowercase token.

func (*StateCategory) UnmarshalJSON

func (s *StateCategory) UnmarshalJSON(data []byte) error

UnmarshalJSON rejects unknown category strings at decode time so that bad adaptor output surfaces as a parse error instead of silently painting the UI with an invalid state.

func (StateCategory) Valid

func (s StateCategory) Valid() bool

Valid reports whether s is one of the five canonical categories.

type StateMap

type StateMap map[string]StateCategory

StateMap is the adaptor's declared translation from its native status tokens to the five core StateCategory buckets. Every native status the adaptor can emit must appear as a key; missing keys surface as conformance failures and force the UI onto an unknown-state fallback.

The map is declarative: core does not attempt to infer categories from status names. This keeps lane placement deterministic and keeps the UI free of adaptor-specific vocabulary (gm-root DD-4).

func (StateMap) Validate

func (m StateMap) Validate() error

Validate reports the first native status whose target bucket is not a valid StateCategory. Adaptors should run this inside their init path so a malformed map fails at startup rather than at first query.

type SubscribeFilter

type SubscribeFilter struct {
	AssignmentID string     `json:"assignment_id,omitempty"`
	SessionID    string     `json:"session_id,omitempty"`
	Kinds        []string   `json:"kinds,omitempty"`
	Since        *time.Time `json:"since,omitempty"`
}

SubscribeFilter narrows Subscribe to a subset of OrchestrationEvents.

type TokenBudget

type TokenBudget struct {
	Limit  int64 `json:"limit"`
	Used   int64 `json:"used"`
	Inform int64 `json:"inform"`
	Warn   int64 `json:"warn"`
	Stop   int64 `json:"stop"`
}

TokenBudget is a consumable budget denominated in tokens. Thresholds are absolute token counts, not percentages, so adaptors with native cost telemetry don't have to round-trip through floats. Must hold 0 <= Inform <= Warn <= Stop <= Limit; see Validate.

Used is the current cumulative consumption. Transport/read calls set it; the structure carries no accumulator logic itself.

func (TokenBudget) Remaining

func (b TokenBudget) Remaining() int64

Remaining returns Limit - Used, clamped at zero.

func (TokenBudget) Tier

func (b TokenBudget) Tier() BudgetTier

Tier reports which threshold the current Used value has crossed. It returns the empty string when Used has not yet reached Inform.

func (*TokenBudget) UnmarshalJSON

func (b *TokenBudget) UnmarshalJSON(data []byte) error

UnmarshalJSON wraps the default decode with a Validate check so that invalid budgets surface at the adaptor boundary rather than later in the UI where a negative threshold would be much harder to diagnose.

func (TokenBudget) Validate

func (b TokenBudget) Validate() error

Validate enforces the threshold ordering invariant. Callers should run it when accepting budget configs from users or adaptor manifests.

type Transport

type Transport string

Transport names the wire protocol an adaptor uses to talk to the core (gm-root DD-12). Exactly one of these three values is valid per adaptor in v1; multi-transport adaptors are explicitly out of scope.

const (
	// TransportAPI — HTTP + JSON request/response.
	TransportAPI Transport = "api"
	// TransportJSONL — newline-delimited JSON over stdio (in-process or
	// subprocess).
	TransportJSONL Transport = "jsonl"
	// TransportMCP — Model Context Protocol. Recommended-not-required.
	TransportMCP Transport = "mcp"
)

func (Transport) String

func (t Transport) String() string

String satisfies fmt.Stringer and always returns the lowercase token.

func (*Transport) UnmarshalJSON

func (t *Transport) UnmarshalJSON(data []byte) error

UnmarshalJSON rejects unknown transports at decode time so that a bad manifest fails at the adaptor boundary, not later when we try to route.

func (Transport) Valid

func (t Transport) Valid() bool

Valid reports whether t is one of the three canonical transports.

type ValidationIssue

type ValidationIssue struct {
	Path   string `json:"path,omitempty"`
	Reason string `json:"reason"`
	Code   string `json:"code,omitempty"`
}

ValidationIssue is the structured `issue` payload a transport layer attaches to a KindValidation AdaptorError (gm-io4). The transport decoder — chi handler, jsonl frame pump, MCP tool-call unmarshal — produces this shape so downstream adaptors never see malformed input and the UI gets a stable, render-ready description of what failed.

Path is a dotted JSON pointer ("work_item.priority") identifying the field; Reason is a short human-readable cause ("missing required field", "must be one of [completed failed canceled]"); Code is an optional machine-friendly tag ("required", "enum", "type").

func ValidationIssueOf

func ValidationIssueOf(err error) (ValidationIssue, bool)

ValidationIssueOf extracts the ValidationIssue from err's tagged AdaptorError Detail, returning the zero value and false when the error is not a validation error or has no structured issue. The UI and the conformance suite use this to assert that validation failures surface with a structured shape — never a bare message.

type VersioningTransport

type VersioningTransport string

VersioningTransport names a versioned-import/export surface. R4.

const (
	// VersioningNone — the adaptor has no cross-instance versioning
	// transport. Writes are authoritative; history is whatever the
	// substrate retains.
	VersioningNone VersioningTransport = "none"
	// VersioningGit — the store is (or is backed by) a git repo.
	VersioningGit VersioningTransport = "git"
	// VersioningDolt — Dolt SQL with branches / merges.
	VersioningDolt VersioningTransport = "dolt"
	// VersioningJSONL — newline-delimited JSON export/import, the
	// common lowest-form-factor versioning transport.
	VersioningJSONL VersioningTransport = "jsonl"
	// VersioningNativeSQLiteExport — a native SQLite file the
	// adaptor produces for transport / backup.
	VersioningNativeSQLiteExport VersioningTransport = "native-sqlite-export"
)

type WorkItem

type WorkItem struct {
	ID WorkItemID `json:"id"`
	// PrimaryRepositoryID names the [Repository] this work item lives
	// in primarily (gm-kdh3). The spawn path reads this to pick the
	// working directory and worktree pool for an agent session.
	// Empty (or [RepositoryUnspecified]) means the bead has not been
	// backfilled since repository tracking landed; spawn rejects with
	// a clear error so the operator sets it explicitly. Use the
	// [WorkItem.RepositoryID] method (no arg) when you only need the
	// primary id and don't care about plurality.
	PrimaryRepositoryID RepositoryID `json:"primary_repository_id,omitempty"`

	// RepositoryIDs lists every [Repository] this work item touches
	// (gm-kdh3). A multi-repo bead — common for cross-cutting work
	// like a backend API change with a frontend client update — has
	// more than one entry. Validation rules:
	//   - PrimaryRepositoryID, when set, MUST appear in this slice
	//   - this slice non-empty + PrimaryRepositoryID empty is invalid
	// [WorkItem.NormalizeRepositories] auto-promotes a sole
	// PrimaryRepositoryID into a single-element slice for back-
	// compat with the gm-26n4 single-repo shape.
	RepositoryIDs []RepositoryID `json:"repository_ids,omitempty"`

	// Branches names the git branch this bead's work happens on, per
	// repository (gm-ou02). When empty, the spawn path derives a
	// branch as `<bead-id>-<slugified-title>` from each repo's
	// default branch. When non-empty, the spawn path uses the
	// explicit value for the named repo and errors on a missing entry
	// for a repo in RepositoryIDs.
	Branches []BeadBranch `json:"branches,omitempty"`

	// AdditionalReadPaths extends the default read surface a session
	// gets when working this bead (gm-v8vr). Glob-style patterns;
	// resolved as additional read-only allowances on top of cwd +
	// sibling Repository.Path entries + workspace .gemba/ + standard
	// tooling whitelist. Used when a bead legitimately needs to peek
	// outside the workspace surface — e.g. a polecat that has to
	// reference vendored docs at `~/notes/` or a config file under
	// `~/.aws/credentials` (which is sensitive: justify in bead
	// notes). Surfaces in bd via labels of the form `read:<glob>`.
	AdditionalReadPaths []string `json:"additional_read_paths,omitempty"`

	// AdditionalWritePaths grants writes outside cwd. Very rare —
	// the operator must justify in bead notes. Surfaces via labels
	// `write:<glob>`.
	AdditionalWritePaths []string          `json:"additional_write_paths,omitempty"`
	Kind                 string            `json:"kind"`
	Title                string            `json:"title"`
	Description          string            `json:"description,omitempty"`
	Status               string            `json:"status"`
	StateCategory        StateCategory     `json:"state_category"`
	Priority             *int              `json:"priority,omitempty"`
	Owner                *AgentRef         `json:"owner,omitempty"`
	Assignee             *AgentRef         `json:"assignee,omitempty"`
	Labels               []string          `json:"labels,omitempty"`
	Relationships        []Relationship    `json:"relationships,omitempty"`
	Evidence             []Evidence        `json:"evidence,omitempty"`
	DoD                  *DefinitionOfDone `json:"dod,omitempty"`
	SprintID             *string           `json:"sprint_id,omitempty"`
	CreatedAt            time.Time         `json:"created_at"`
	UpdatedAt            time.Time         `json:"updated_at"`
	Custom               map[string]any    `json:"custom,omitempty"`

	// Targets is the declared path-glob set the bead is expected to
	// touch (gm-s47n.1.1, work-planning.md §4 Layer 0). Bd adaptors
	// project this from "target:<glob>" labels; non-bd adaptors that
	// have a typed extras column read it directly. Empty means "no
	// targets declared" — the planner's conflict scorer treats that
	// as wildcard-overlap and is appropriately pessimistic.
	Targets []string `json:"targets,omitempty"`

	// Concepts is the controlled-vocabulary tag set for the bead
	// (gm-s47n.1.1). Tags come from internal/concepts (ladders the
	// vocabulary governance pipeline produces). Bd adaptors project
	// from "concept:<tag>" labels; the affinity scorer reads it to
	// match beads against primed sessions.
	Concepts []string `json:"concepts,omitempty"`

	// DispatchStatus is the planner-facing soft-block enum
	// (gm-s47n.1.1). The default DispatchReady is the only kind the
	// planner's "what's next" surface treats as a candidate; the
	// other values are visible in `bd list` but suppressed from
	// auto-dispatch. The conflict scorer (Layer 3) ignores this —
	// it's a selection signal, not a relationship signal.
	DispatchStatus DispatchStatus `json:"dispatch_status,omitempty"`

	// EstimatedSize is the calibration-loop bucket
	// (work-planning.md §7.6, gm-s47n.1.1). Bootstrapped from
	// description-length + DoD-line-count; the retrospective grades
	// it against actual time-to-close so the heuristic gets sharper.
	// Used by Layer 5 to compare bead size against session runway.
	EstimatedSize EstimatedSize `json:"estimated_size,omitempty"`

	// Derived carries UI-facing booleans computed by Derive from this
	// item plus its open escalations (gm-gsh). Servers SHOULD populate
	// it on every read so the UI does not have to rebuild the predicate
	// per-card; it is omitempty because the pure derivation also lets
	// the UI recompute locally if a transport omits it.
	Derived *DerivedSignals `json:"derived,omitempty"`
}

func (WorkItem) BranchFor

func (w WorkItem) BranchFor(id RepositoryID) (string, bool)

BranchFor returns the recorded branch for the given repository, if any. The bool is false when no entry maps to id; the caller derives a default in that case. Provided as a method so call sites in the spawn driver read clearly.

func (*WorkItem) NormalizeRepositories

func (w *WorkItem) NormalizeRepositories()

NormalizeRepositories enforces invariants that the JSON wire shape alone cannot. Called by every adaptor projecting a native record onto WorkItem so callers downstream (spawn, registry, validation) see a coherent state.

Rules:

  • When RepositoryIDs is empty AND PrimaryRepositoryID is set, auto-promote PrimaryRepositoryID into RepositoryIDs as a single-element slice. Preserves back-compat with the gm-26n4 single-repo shape (callers that only set the primary still produce a valid multi-repo bead).
  • Otherwise, fields are left untouched. ValidateRepositories surfaces any remaining inconsistencies as errors.

func (WorkItem) RepositoryID

func (w WorkItem) RepositoryID() RepositoryID

RepositoryID returns the primary repository id this work item is bound to (gm-kdh3). Convenience accessor for callers that don't care about multi-repo plurality — the spawn path's cwd selection always uses the primary, never a non-primary entry. Equivalent to [WorkItem.PrimaryRepositoryID]; provided as a method so call sites read clearly when intent is "the repository", not "the field".

func (WorkItem) ValidateBranches

func (w WorkItem) ValidateBranches() error

ValidateBranches checks the per-repo branch mapping. Caller-safe errors. Rules:

  • Each entry's RepositoryID must be non-empty and present in [WorkItem.RepositoryIDs] (catches typos).
  • Branch must be non-empty.
  • At most one entry per RepositoryID — duplicates are rejected.

Empty Branches is valid (the spawn path falls back to derivation).

func (WorkItem) ValidateRepositories

func (w WorkItem) ValidateRepositories() error

ValidateRepositories checks that the multi-repo fields are internally consistent. Caller-safe error messages — surface directly via HTTP 400 / bd CLI errors. Does NOT check that the referenced repositories exist in any registry; that's the spawn path's job and the right place to fail when a checkout is missing.

Returns nil for the zero case (legacy beads with no repository information yet) — those are valid at the schema level; the spawn path enforces a non-empty repository at consult time.

type WorkItemFilter

type WorkItemFilter struct {
	IDs           []WorkItemID    `json:"ids,omitempty"`
	Kinds         []string        `json:"kinds,omitempty"`
	Statuses      []string        `json:"statuses,omitempty"`
	StateCategory []StateCategory `json:"state_category,omitempty"`
	AssigneeID    *AgentID        `json:"assignee_id,omitempty"`
	SprintID      *string         `json:"sprint_id,omitempty"`
	Labels        []string        `json:"labels,omitempty"`
	UpdatedSince  *time.Time      `json:"updated_since,omitempty"`
	CreatedSince  *time.Time      `json:"created_since,omitempty"`
	// Limit caps the returned set. 0 means "adaptor default".
	Limit int `json:"limit,omitempty"`

	// IncludeTemplates surfaces work items carrying the "template"
	// label — the cooked protomolecules `bd cook` produces. Default
	// false: templates stay off Plan / Backlog / planner. The
	// Workflow Library opts in by setting this true. gm-e12.22.1.
	IncludeTemplates bool `json:"include_templates,omitempty"`

	// IncludeWisps surfaces ephemeral molecules (wisps). Default
	// false: wisps stay off work surfaces. The Workflow Active runs
	// tab opts in by setting this true. gm-e12.22.1.
	IncludeWisps bool `json:"include_wisps,omitempty"`
}

WorkItemFilter narrows a ListWorkItems query. Zero values mean "no filter on that field"; the intersection of non-zero fields applies. Adaptors are expected to push as many filters down to their native store as possible and only fall back to in-process filtering for predicates their backend can't express.

type WorkItemID

type WorkItemID string

WorkItemID is the workspace-qualified identifier for a work item. Format is "<workspace>/<repo>/<native-id>" (e.g. "gemba/gemba/gm-e3.1", "myorg/atl/GEMBA-17"). The workspace/repo prefix disambiguates multi-workspace deployments (gm-root DD-6); two Beads workspaces on the same Gemba instance are distinguishable by this prefix where a bare adaptor-kind prefix would collide.

type WorkItemNotifier

type WorkItemNotifier interface {
	// NotifyExternal re-reads the WorkItem identified by id and
	// publishes a workitem.* event. Returns the re-read WorkItem
	// and the emitted kind ([WorkItemEventUpdated] or
	// [WorkItemEventClosed]). source is an optional hint
	// ("bd-git-hook", "ops-runbook") echoed onto the event payload.
	NotifyExternal(ctx context.Context, id WorkItemID, source string) (WorkItem, string, error)
}

WorkItemNotifier is the optional interface a WorkPlane adaptor implements when it can publish a WorkPlaneEvent for a mutation that landed via an out-of-process writer (gm-e4.3.2). The HTTP handler at POST /api/workitems/notify type-asserts the bound adaptor to this interface; adaptors that don't implement it (the dolt read-only adaptor, the noop adaptor) cause the endpoint to return 409 capability_denied.

Implementations re-read the WorkItem through the same path the in-process UpdateWorkItem uses (no trust of caller-supplied state), derive the kind from the persisted state, and Publish through the same emitter so /events SSE subscribers receive an event indistinguishable from in-process mutations.

type WorkItemPatch

type WorkItemPatch struct {
	Title         *string           `json:"title,omitempty"`
	Description   *string           `json:"description,omitempty"`
	Status        *string           `json:"status,omitempty"`
	StateCategory *StateCategory    `json:"state_category,omitempty"`
	Priority      *int              `json:"priority,omitempty"`
	Owner         *AgentRef         `json:"owner,omitempty"`
	Assignee      *AgentRef         `json:"assignee,omitempty"`
	Labels        []string          `json:"labels,omitempty"`
	DoD           *DefinitionOfDone `json:"dod,omitempty"`
	SprintID      *string           `json:"sprint_id,omitempty"`
	// Parent re-parents the work item via its parent_child edge. Three-
	// state sentinel: nil = no change; pointer to "" = clear (orphan);
	// pointer to a non-empty id = re-parent under that work item.
	// Surfaced for milestone membership management (gm-98sq, gm-gsbj).
	Parent *string        `json:"parent_id,omitempty"`
	Custom map[string]any `json:"custom,omitempty"`
}

WorkItemPatch carries an update intended for an existing WorkItem. Every field is optional; nil pointers and empty slices mean "do not touch". Adaptors translate the patch to their backend's public API (gm-root DD-9) — core never writes private storage.

Status and StateCategory move together at the adaptor boundary: the adaptor may accept either (its own native token or the normalized bucket) and must reject the case where both are set and inconsistent with its StateMap.

type WorkPlane

type WorkPlane interface {
	// Describe returns the adaptor's declared capabilities. MUST be
	// idempotent and side-effect-free; called repeatedly by the UI and
	// the doctor command.
	Describe(ctx context.Context) (CapabilityManifest, error)

	// ListWorkItems returns the work items matching filter, sorted by
	// the adaptor's natural order (typically UpdatedAt desc). Adaptors
	// should respect filter.Limit; 0 means "adaptor default".
	ListWorkItems(ctx context.Context, filter WorkItemFilter) ([]WorkItem, error)

	// GetWorkItem returns a single work item by ID, with relationships
	// and evidence populated when the adaptor supports them.
	// Returns ErrNotFound (or a wrapper thereof) when id is unknown.
	GetWorkItem(ctx context.Context, id WorkItemID) (WorkItem, error)

	// CreateWorkItem creates a new work item through the backend's
	// public API and returns the materialized record (including the
	// backend-assigned id and timestamps). Implementations MUST NOT
	// write private storage directly (gm-root DD-9).
	CreateWorkItem(ctx context.Context, wi WorkItem) (WorkItem, error)

	// UpdateWorkItem applies patch to the work item with the given id
	// and returns the resulting record. Adaptors reject patches that
	// violate their own invariants (e.g. illegal status transitions)
	// with a structured error the UI can surface verbatim.
	UpdateWorkItem(ctx context.Context, id WorkItemID, patch WorkItemPatch) (WorkItem, error)

	// AttachEvidence appends an evidence record to the work item. When
	// the manifest sets EvidenceSynthesisRequired=true the core may
	// call this from its own synthesis pipeline; otherwise the adaptor
	// is expected to manage its own evidence and this method may
	// return an ErrUnsupported.
	AttachEvidence(ctx context.Context, id WorkItemID, ev Evidence) error

	// ListSprints returns the sprints currently declared by the
	// backend. Adaptors with SprintNative=false MAY return an empty
	// slice without error.
	ListSprints(ctx context.Context) ([]Sprint, error)

	// ReadBudgetRollup returns the token consumption aggregated against
	// the named sprint. Adaptors with SprintNative=false or
	// TokenBudgetEnforced=false SHOULD return ErrUnsupported so the UI
	// can hide the widget.
	ReadBudgetRollup(ctx context.Context, sprintID string) (BudgetRollup, error)

	// Subscribe streams WorkPlaneEvents matching f. The adaptor closes
	// the returned channel when ctx is cancelled or the underlying
	// transport disconnects (gm-e4.3.1).
	//
	// Adaptors that cannot emit events (noop, read-only dolt-direct,
	// archival formats) return nil + ErrUnsupported. The server-side
	// pump treats ErrUnsupported as "no events from this plane" and
	// drops silently — callers MUST NOT treat it as a hard failure.
	//
	// Multiple concurrent subscribers are supported; the adaptor fans
	// out each emitted event to all active subscribers. Slow
	// subscribers drop events rather than block the fan-out (the hub
	// handles SSE-client-level backpressure upstream).
	Subscribe(ctx context.Context, f WorkPlaneSubscribeFilter) (<-chan WorkPlaneEvent, error)
}

WorkPlane is the adaptor-agnostic surface every work-tracker implementation must satisfy. One WorkPlane is bound per gemba process (gm-root DD-1) and paired with one OrchestrationPlane (see orchestration.go, gm-e3.3).

Methods are divided into three groups:

  1. Describe — declarative capability advertisement. Called at startup by doctor and on every reconnect by the UI so it always renders against a fresh manifest.
  2. Work-item queries and mutations — the main CRUD surface. Mutations MUST be implemented by calling the backend's public CLI or API; direct writes to private storage are forbidden (gm- root DD-9).
  3. Sprint + budget — optional feature group. Adaptors that set SprintNative=false may return empty slices and zero rollups; the UI hides sprint chrome when the manifest says so.

Contexts carry the request deadline and tracing span (gm-e3.6); implementations should propagate them verbatim to their transport and return a wrapped error on cancellation.

func GuardedWorkPlane

func GuardedWorkPlane(inner WorkPlane, manifest CapabilityManifest) WorkPlane

GuardedWorkPlane wraps inner with a port-level CapabilityManifest guard. Every gated boundary call first consults CheckCapability against the manifest that Describe reports; denied ops return a capability_denied AdaptorError WITHOUT reaching inner.

The wrapper is the canonical core-side enforcement point required by gm-4qf ("every mutation handler in the core calls the guard BEFORE routing to the adaptor"). Adaptors remain required to fail-fast on their own as defense in depth — the guarded wrapper and the adaptor's internal check MUST agree.

The manifest is captured at construction so each call avoids a Describe round-trip; callers that want per-reconnect refresh should rebuild the wrapper. Panics on construction if inner is nil.

type WorkPlaneEmitter

type WorkPlaneEmitter struct {
	// contains filtered or unexported fields
}

WorkPlaneEmitter is the fan-out helper every WorkPlane adaptor that opts into Subscribe embeds. It maintains a set of active subscribers and non-blocking-publishes each WorkPlaneEvent to every matching subscriber; a subscriber whose buffer is full is dropped (its channel closed) per the interface contract. gm-e4.3.1.

Split out of the individual adaptor files so bd, testadaptors, and any future emitting adaptor don't duplicate fan-out / filter / teardown code.

func NewWorkPlaneEmitter

func NewWorkPlaneEmitter() *WorkPlaneEmitter

NewWorkPlaneEmitter returns a ready-to-use emitter.

func (*WorkPlaneEmitter) Close

func (e *WorkPlaneEmitter) Close()

Close drops every subscriber. Idempotent; safe at adaptor shutdown.

func (*WorkPlaneEmitter) Publish

func (e *WorkPlaneEmitter) Publish(ev WorkPlaneEvent)

Publish fans ev out to every matching subscriber with a non-blocking send. Callers MUST call Publish after the underlying mutation has successfully landed — never before, so a failed mutation cannot leak a ghost event.

Holds e.mu for the entire fan-out. The earlier "snapshot then send without the lock" shape raced remove(): Publish could capture a sub pointer, remove() could close its channel before Publish's send reached it, and the send would either panic on a closed channel or trip the race detector. Sends are non-blocking (select+default), so holding the lock is short — never longer than a buffered channel write per subscriber.

func (*WorkPlaneEmitter) Subscribe

Subscribe registers a new subscriber. The returned channel closes when ctx is cancelled, the subscriber lags past its buffer, or the emitter is Closed.

func (*WorkPlaneEmitter) SubscriberCount

func (e *WorkPlaneEmitter) SubscriberCount() int

SubscriberCount exposes the live subscriber count for leak-tests.

type WorkPlaneEvent

type WorkPlaneEvent struct {
	ID         string         `json:"id"`
	Kind       string         `json:"kind"`
	At         time.Time      `json:"at"`
	WorkItemID WorkItemID     `json:"work_item_id,omitempty"`
	Payload    map[string]any `json:"payload,omitempty"`
}

WorkPlaneEvent is the streamed envelope every adaptor mutation surfaces through. Mirrors OrchestrationEvent's shape so the event hub's canonicalisation layer (internal/events/translate.go) treats both planes identically. gm-e4.3.1.

Canonical Kind values:

"workitem_created"           — CreateWorkItem completed
"workitem_updated"           — UpdateWorkItem completed
"workitem_closed"            — state_category transitioned to
                               completed or canceled
"workitem_evidence_attached" — AttachEvidence completed

Adaptors MAY emit additional kinds; the canonicaliser drops unknown kinds under the "workplane." namespace so the SPA's kind-handler registry can ignore them without error.

type WorkPlaneSubscribeFilter

type WorkPlaneSubscribeFilter struct {
	// Kinds filters to a subset of WorkPlaneEvent kinds
	// (workitem_created / workitem_updated / workitem_closed /
	// workitem_evidence_attached). Empty means all kinds.
	Kinds []string
	// WorkItemID narrows to events about a single work item. Empty
	// means all items.
	WorkItemID WorkItemID
	// Since filters out events emitted before this timestamp — useful
	// when a client reconnects and wants to catch up from its last
	// known event time.
	Since *time.Time
}

WorkPlaneSubscribeFilter narrows a Subscribe stream. Zero values mean "no filter on that field"; multiple non-zero fields combine with AND. gm-e4.3.1.

type Workspace

type Workspace struct {
	ID         string        `json:"id"`
	Kind       WorkspaceKind `json:"kind"`
	Repository string        `json:"repository,omitempty"`
	Branch     string        `json:"branch,omitempty"`
	BaseSHA    string        `json:"base_sha,omitempty"`
	// WorktreePath is the absolute filesystem path of the working
	// copy when Kind == WorkspaceWorktree (gm-s47n.2.6). Empty for
	// every other Kind. Promoted to a typed field so the planner
	// can detect workspace collision (two beads needing write
	// access to the same checkout) without parsing per-provider
	// metadata. Adaptors that previously stored this under
	// ProviderMetadata["worktree_path"] MUST set this field too;
	// the metadata key remains as a transitional fallback (see
	// WorkspaceWorktreePath below).
	WorktreePath     string                `json:"worktree_path,omitempty"`
	Status           WorkspaceStatus       `json:"status"`
	Isolation        IsolationCapabilities `json:"isolation"`
	ProviderMetadata map[string]any        `json:"provider_metadata,omitempty"`
	CreatedAt        time.Time             `json:"created_at"`
	ReleasedAt       *time.Time            `json:"released_at,omitempty"`
}

Workspace is Gemba's adaptor-agnostic view of a scoped execution environment — a worktree, a container, a k8s pod. Per gm-root DD-5 the one invariant is that writes within the workspace do not escape to corrupt another concurrent assignment's workspace.

type WorkspaceKind

type WorkspaceKind string

WorkspaceKind enumerates the isolation shapes an OrchestrationPlane can acquire for an assignment. Per gm-root DD-5, every workspace MUST guarantee `fs_scoped`; richer isolation (network, CPU, memory, snapshot/restore) is declared per-kind on the manifest.

const (
	// WorkspaceWorktree — a git worktree on the host (Gas Town polecats).
	WorkspaceWorktree WorkspaceKind = "worktree"
	// WorkspaceContainer — an OCI container (Docker/Podman).
	WorkspaceContainer WorkspaceKind = "container"
	// WorkspaceK8sPod — a Kubernetes pod.
	WorkspaceK8sPod WorkspaceKind = "k8s_pod"
	// WorkspaceVM — a full virtual machine.
	WorkspaceVM WorkspaceKind = "vm"
	// WorkspaceExec — unscoped exec; relies on the agent honouring $PWD
	// (LangGraph default; documents its weakness).
	WorkspaceExec WorkspaceKind = "exec"
	// WorkspaceSubprocess — a spawned child process with a scoped cwd.
	WorkspaceSubprocess WorkspaceKind = "subprocess"
)

type WorkspaceRequest

type WorkspaceRequest struct {
	AssignmentID      string                `json:"assignment_id"`
	Repository        string                `json:"repository"`
	Branch            string                `json:"branch,omitempty"`
	PreferredKind     WorkspaceKind         `json:"preferred_kind,omitempty"`
	RequiredIsolation IsolationCapabilities `json:"required_isolation"`
}

WorkspaceRequest is the input to AcquireWorkspace. The adaptor picks the weakest supported kind that satisfies RequiredIsolation (gm-root DD-5); failing to meet the ask must error rather than silently downgrade.

type WorkspaceStatus

type WorkspaceStatus string

WorkspaceStatus is the lifecycle tag on a Workspace. "provisioning" and "ready" are pre-assignment; "in_use" covers the live session; "released" and "error" are terminal.

const (
	WorkspaceProvisioning WorkspaceStatus = "provisioning"
	WorkspaceReady        WorkspaceStatus = "ready"
	WorkspaceInUse        WorkspaceStatus = "in_use"
	WorkspaceReleased     WorkspaceStatus = "released"
	WorkspaceError        WorkspaceStatus = "error"
)

type WorkspaceTopology

type WorkspaceTopology struct {
	Agents      []AgentRef   `json:"agents,omitempty"`
	Groups      []AgentGroup `json:"groups,omitempty"`
	Workspaces  []Workspace  `json:"workspaces,omitempty"`
	Assignments []Assignment `json:"assignments,omitempty"`
	Sessions    []Session    `json:"sessions,omitempty"`
	CapturedAt  time.Time    `json:"captured_at"`
}

WorkspaceTopology is the payload both DeclaredState and ObservedState return. It generalises Gas City's declared (`city.toml`) vs observed (`.gc/agents/`) split into a single reusable shape (gm-root §Novel §8). The Kanban, Agents dashboard, and capability-negotiation UI all diff these two to surface drift.

type WriteOp

type WriteOp string

WriteOp tells the shader which adaptor entry-point initiated the encode call. The two values map 1:1 to the WorkPlane methods that take a WorkItem (or patch) on the wire.

const (
	WriteCreate WriteOp = "create"
	WriteUpdate WriteOp = "update"
)

Jump to

Keyboard shortcuts

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