orchestrator

package
v0.0.9 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const (
	OpBoidJobDone    = "job_done"
	OpBoidJobList    = "job_list"
	OpBoidJobShow    = "job_show"
	OpBoidJobLog     = "job_log"
	OpBoidActionSend = "action_send"
	OpBoidAgentStop  = "agent_stop"
	OpBoidTaskCreate = "task_create"
	OpBoidTaskGet    = "task_get"
	OpBoidTaskUpdate = "task_update"
	OpBoidTaskImport = "task_import"
	OpBoidTaskReopen = "task.reopen"
	OpBoidTaskList   = "task_list"
	OpBoidTaskNotify = "task_notify"
	OpBoidTaskAnswer = "task_answer"
	OpBoidTaskAsk    = "task_ask"
	OpBoidTaskDelete = "task_delete"

	OpGitFetch      = "fetch"
	OpGitPush       = "push"
	OpGitPushDelete = "push_delete"
	OpGitCloneLocal = "clone_local"

	OpFetchGet = "get"
)

Mirror of sandbox layer's op constants. orchestrator cannot import sandbox (that would reverse the layer direction), so these are kept as string literals that must stay in lock-step with internal/sandbox/protocol.go. Drift is detected by internal/dispatcher/policy_translate_test.go — the only layer allowed to see both sides.

View Source
const DefaultBehavior = "supervisor"

DefaultBehavior is the hardcoded fallback behavior name used when a CreateTaskRequest omits both behavior and behavior_spec, the project meta is nil, and no default_task_behavior is configured. When meta is non-nil, the default resolution consults meta.DefaultTaskBehavior first, then falls back to "supervisor" with a deprecation warning if that behavior exists.

View Source
const DefaultWorkspaceSlug = "default"

DefaultWorkspaceSlug is the reserved slug for the implicit "default" workspace that every project belongs to when no explicit assignment is chosen. It is auto-created at daemon startup, used as the fallback target for `project add` / `project migrate` when --workspace is omitted, and guarded against deletion by WorkspaceStore.Remove.

View Source
const ParentIDSentinelRoot = "-"

ParentIDSentinelRoot is a sentinel value for CreateTaskRequest.ParentID that explicitly requests root-task creation. When this value is detected at an entry point (sandbox, CLI, HTTP API), auto-populate is skipped and the stored parent_id is left empty. Use this when a root task must be created from inside a child context where auto-populate would otherwise attach the current task as parent.

Variables

View Source
var BehaviorAliases = map[string]string{
	"plan": "supervisor",
	"dev":  "executor",
}

BehaviorAliases maps legacy behavior names to their canonical counterparts. This is the alias table used during the "task_behavior simplification" rename: project.yaml files written before the rename keep using "plan" / "dev"; on load they are normalized to "supervisor" / "executor" and a deprecation warning is emitted. The map is intentionally not exported as mutable state — callers should go through CanonicalBehaviorName.

View Source
var ErrDetachedHead = fmt.Errorf("project is in detached HEAD state")

ErrDetachedHead is returned by ClassifyBaseBranch when the project working directory is in detached-HEAD state. Wrapped so callers can use errors.Is.

View Source
var ErrTaskNotFound = errors.New("task not found")

ErrTaskNotFound is returned by scanTask (and propagated by GetTask, FindTaskByRemote, FindTaskByRef) when no matching task row exists. Callers should check for it with errors.Is rather than matching on error strings.

Functions

func AssignDefaultWorkspaceToUnlinked added in v0.0.8

func AssignDefaultWorkspaceToUnlinked(dbtx db.DBTX, workspaceID string) (int, error)

AssignDefaultWorkspaceToUnlinked inserts a project_workspaces row pointing at workspaceID for every project that does not yet have one. Used at daemon startup to migrate legacy unlinked projects to the default workspace. The INSERT ... SELECT pattern keeps the operation idempotent and atomic in a single statement.

Returns (number of rows inserted, error). Pass the DefaultWorkspaceSlug to land projects in the implicit default workspace.

func CanonicalBehaviorName

func CanonicalBehaviorName(name string) (canonical string, isAlias bool)

CanonicalBehaviorName returns the canonical behavior name for the given (possibly aliased) name. If the input is a deprecated alias, the returned canonical name and isAlias=true are returned. Otherwise the input is returned unchanged with isAlias=false.

func ClearPendingAnswer

func ClearPendingAnswer(payload json.RawMessage) json.RawMessage

ClearPendingAnswer removes the pending_answer field from the awaiting trait while preserving session_id, question, and question_id. This is called after a hook is spawned so the answer is not consumed again on the next resume. Returns payload unchanged when the awaiting trait is absent or has no answer.

func ComputeForkPoint

func ComputeForkPoint(parent *Task) string

ComputeForkPoint returns the branch a child task should fork its worktree from, given the parent task. It differs from ComputeHeadBranch in that worktree-less parents (Worktree == false) return their BaseBranch rather than the synthetic "boid/<id8>" branch — because a worktree=false parent runs in the host project dir on its base_branch and never creates a boid/<id8> branch. Falling back to base_branch is required for the Phase 2-2 supervisor case 1 path (host HEAD already on base_branch); otherwise child worktree creation fails with "fork point boid/<id8> not found locally (parent task worktree missing?)".

func ComputeHeadBranch

func ComputeHeadBranch(task *Task) string

ComputeHeadBranch returns the HEAD branch that the given task will occupy when its worktree is created.

Root tasks (ParentID == "") occupy the project's base_branch directly. Child tasks (ParentID != "") get an isolated "boid/<id8>" branch derived from the first 8 characters of their task ID.

Used by BranchLockManager to derive the compound lock key so that tasks on the same HEAD branch are serialized while tasks on different branches run in parallel. For the worktree resolver's fork-point computation (which has different semantics for worktree-less parents), use ComputeForkPoint.

func CreateAction

func CreateAction(dbtx db.DBTX, a *Action) error

func CreateProject

func CreateProject(dbtx db.DBTX, project *Project) error

CreateProject inserts a new project record.

func CreateTask

func CreateTask(dbtx db.DBTX, t *Task) error

func DefaultBuiltinPolicies

func DefaultBuiltinPolicies(role Role, names []string, pctx PolicyContext) map[string]BuiltinPolicy

DefaultBuiltinPolicies creates per-command BuiltinPolicy values for the given role and command names. "boid" and "git" are always available as builtins; pass them explicitly via names.

func DefaultWorkspaceDir added in v0.0.8

func DefaultWorkspaceDir() (string, error)

DefaultWorkspaceDir returns the default directory for workspace YAML files: $XDG_CONFIG_HOME/boid/workspaces, or ~/.config/boid/workspaces when XDG_CONFIG_HOME is unset (matching the behaviour of os.UserConfigDir on Linux).

func DeleteProject

func DeleteProject(dbtx db.DBTX, id string) error

DeleteProject removes a project by ID. All tasks (and their dependent records) belonging to the project are deleted first. Standalone jobs (task_id NULL session / hook) are also swept by project_id so the jobs.project_id FK constraint does not refuse the project delete.

func DeleteTask

func DeleteTask(dbtx db.DBTX, id string) error

func ExpandBaseBranch

func ExpandBaseBranch(value, workDir string) (string, error)

ExpandBaseBranch expands ${current_branch} (or $current_branch) in value by reading the HEAD branch of the git repo at workDir. Unknown variables are left as-is for backward compatibility. Returns an error if the git command fails or the repo is in detached HEAD state.

func ExpandTaskBaseBranch

func ExpandTaskBaseBranch(template, remoteID string) (string, error)

ExpandTaskBaseBranch expands the ${TASK_REMOTE_ID} template variable in template against the supplied remoteID. Other template variables are rejected with one specific carve-out: ${current_branch} (the variable owned by ExpandBaseBranch) is preserved as-is so callers can compose the two expanders without ordering surprises.

Contract:

  • Static values (no ${...} or $... markers, or only ${current_branch}) pass through unchanged. An empty remoteID is fine in that case.
  • If the template references ${TASK_REMOTE_ID} but remoteID is empty, expansion fails: task creation must not silently produce a broken branch name. Callers are expected to surface this as a 400.
  • Any other ${VAR} or $VAR reference is rejected. This intentionally restricts the surface; the longer-term plan is to keep template variables to an explicit allow-list rather than letting arbitrary env vars leak into task state.

func FilterPayloadByTraits

func FilterPayloadByTraits(payload json.RawMessage, consumes []TraitType) json.RawMessage

FilterPayloadByTraits returns a payload containing only the top-level keys matching the listed traits. If consumes is empty, an empty payload is returned.

func FormatMigrationIssue added in v0.0.8

func FormatMigrationIssue(p ProjectMigrationIssue) string

FormatMigrationIssue renders a single ProjectMigrationIssue using the canonical multi-line format. When ProjectID is non-empty the output is prefixed with `project "<ID>": ` to mirror the wrapping previously done by project_store.LoadAll via fmt.Errorf("project %q: %w", id, err).

Exported so that callers building aggregate messages (server/wire.go, boid start parent) can reuse the same canonical format for each issue.

func IsBehaviorAliasKey

func IsBehaviorAliasKey(key string) bool

IsBehaviorAliasKey reports whether the given key is a deprecated alias key. Display code that needs to suppress mirror entries for the migration period (so user-facing output does not show the same behavior twice) can skip keys where IsBehaviorAliasKey returns true and the canonical counterpart is also present in the same map.

func IsInstructionsEditable

func IsInstructionsEditable(status TaskStatus) bool

IsInstructionsEditable reports whether a task's instructions can be edited in the given status. Editing is only allowed while the task is pending to avoid racing with in-flight handlers and to prevent post-execution mutations.

func IsProjectScopable

func IsProjectScopable(km *KitMeta) error

IsProjectScopable reports whether a kit may be placed in the top-level project.yaml kits field. Kits no longer provide hooks or task_behaviors, so all kits are project-scopable by definition.

func IsReadonly

func IsReadonly(task *Task) bool

IsReadonly returns true if the task's working directory should be mounted read-only. Driven solely by the task.readonly flag (e.g. plan tasks).

func MarshalProjectLocalMeta

func MarshalProjectLocalMeta(meta *ProjectLocalMeta) ([]byte, error)

func MergeKitMetaIntoBehavior

func MergeKitMetaIntoBehavior(behavior *TaskBehavior, kits []*KitMeta, kitAgents []string) error

MergeKitMetaIntoBehavior merges kit-provided hooks, env, bindings, and host_commands into the given TaskBehavior. Kit hook IDs are prefixed with the agent name. The behavior is modified in place.

func MergePayload

func MergePayload(base, update json.RawMessage) (json.RawMessage, error)

func MergePayloadPatch

func MergePayloadPatch(base, patch json.RawMessage, handlerID string, allowedTraits []TraitType) (json.RawMessage, error)

func ProjectLocalPath

func ProjectLocalPath(dir string) string

func RejectPayloadInstructions

func RejectPayloadInstructions(payload json.RawMessage) error

RejectPayloadInstructions returns an error if payload contains an "instructions" top-level key. instructions moved out of payload into Task.Instructions; accepting it here would silently drop it.

func RejectReservedPayloadKeys

func RejectReservedPayloadKeys(payload json.RawMessage) error

RejectReservedPayloadKeys returns an error if the payload contains writes to the artifact.children.* namespace (which is managed by virtual evaluation only).

func ResolveAllowedDomains added in v0.0.8

func ResolveAllowedDomains(globalFloor []string, workspace *WorkspaceMeta) []string

ResolveAllowedDomains returns the effective proxy egress allowlist for a sandbox launched under workspace. The result is the additive union of the daemon-wide floor (config.yaml sandbox.allowed_domains, plus boid built-in defaults) and the workspace's AllowedDomains. The workspace cannot remove entries from the floor: that guarantee keeps tool-install endpoints (pypi.org, github.com, …) reachable across every workspace.

Duplicate entries are de-duplicated (case-insensitive) while preserving first-seen order. The function is a free function (rather than a method on WorkspaceMeta) so that callers may pass a nil workspace to mean "no workspace overrides" without having to construct an empty struct.

Future extension point: a third parameter for kit-supplied domains is expected here (see [[project-workspace-allowed-domains]]); when added it will slot in between the floor and the workspace overrides with the same additive semantics.

func ResolveHookScript

func ResolveHookScript(hooksDir, hookID string) (string, error)

func ResolveKitAgent

func ResolveKitAgent(ref KitRef) string

ResolveKitAgent derives the agent name for a kit reference. The last path segment of the ref is used.

func SetPendingAnswer added in v0.0.7

func SetPendingAnswer(payload json.RawMessage, answer string) json.RawMessage

SetPendingAnswer writes answer into the awaiting trait's pending_answer field while preserving question/question_id and any sibling traits. It is the durable counterpart to the in-memory BlockingAskRegistry delivery: when the answering agent has disconnected (its `boid task ask` foreground command was killed by a harness command-timeout), the answer is parked here so the agent picks it up on its next ask. If the awaiting trait is absent, one is created holding just the answer so it is never silently dropped.

func SetProjectWorkspace

func SetProjectWorkspace(dbtx db.DBTX, projectID, workspaceID string) error

SetProjectWorkspace updates a project's local workspace membership.

Domain-layer validation per plan (3-layer defense, last line before DB INSERT). Empty workspaceID clears the membership and bypasses slug validation; any non-empty slug must satisfy ValidWorkspaceSlug so we never persist a malformed identifier even if an upstream layer forgets to check.

func StripAwaitingTrait

func StripAwaitingTrait(payload json.RawMessage) json.RawMessage

StripAwaitingTrait removes the entire awaiting trait from a payload. The awaiting trait is owned exclusively by ApplyAction("ask"/"answer") and the lifecycle, so any value carried in a coordinator's FinalPayload (which snapshots task.Payload before the hook runs) is necessarily stale and must not be merged back over the freshly-persisted DB state on hook completion. Returns the payload unchanged when the awaiting trait is absent.

func TraitBool

func TraitBool(payload json.RawMessage, trait string) bool

TraitBool returns true if the given trait key exists and its value is the JSON literal true. The key may be a dot-separated path (e.g. "lifecycle.executed") for nested object access.

func TraitExists

func TraitExists(payload json.RawMessage, trait string) bool

TraitExists reports whether the dotted-path key resolves to a present, non-null JSON value in payload. Unlike TraitBool it does not require the value to be the JSON literal true; any concrete value (object, array, string, number, true) counts as "exists". Used by state-machine condition rules that gate on the presence of a sibling trait (e.g. `lifecycle.done` set by `done_request`) regardless of its body shape.

func TraitGetString

func TraitGetString(payload json.RawMessage, trait string) (string, bool)

TraitGetString reads the string value at a dotted path. Returns ("", false) if the path is missing or the value is not a JSON string. Used by auto-rule ActionPayloadFns to extract user-facing message text from lifecycle traits.

func UpdateTask

func UpdateTask(dbtx db.DBTX, t *Task) error

func ValidKitName added in v0.0.8

func ValidKitName(s string) error

ValidKitName checks that s is a valid kit name. A valid name consists of lowercase ASCII letters, digits, and hyphens, is between 1 and 64 characters long, and contains no other characters.

func ValidWorkspaceSlug added in v0.0.8

func ValidWorkspaceSlug(s string) error

ValidWorkspaceSlug checks that s is a valid workspace slug. A valid slug consists of lowercase ASCII letters, digits, and hyphens, is between 1 and 64 characters long, and contains no other characters.

func ValidatePayloadPatch

func ValidatePayloadPatch(patch json.RawMessage, allowedTraits []TraitType) error

func WriteProjectLocalMeta

func WriteProjectLocalMeta(dir string, meta *ProjectLocalMeta) error

Types

type AbortReason

type AbortReason struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

AbortReason holds metadata derived from the aborted-state transition action.

type Action

type Action struct {
	ID         string          `json:"id"`
	TaskID     string          `json:"task_id"`
	Type       string          `json:"type"`
	Payload    json.RawMessage `json:"payload"`
	FromStatus TaskStatus      `json:"from_status,omitempty"`
	ToStatus   TaskStatus      `json:"to_status,omitempty"`
	CreatedAt  time.Time       `json:"created_at"`
}

func ListActionsByTask

func ListActionsByTask(dbtx db.DBTX, taskID string) ([]*Action, error)

type AdvanceOutcome

type AdvanceOutcome struct {
	Task          *Task
	ActionPayload json.RawMessage // nil unless the fired rule has ActionPayloadFn
}

AdvanceOutcome carries the result of a successful condition-based transition.

type AwaitingPayload

type AwaitingPayload struct {
	Question      string `json:"question,omitempty"`
	QuestionID    string `json:"question_id,omitempty"`
	PendingAnswer string `json:"pending_answer,omitempty"`
}

AwaitingPayload holds the fields of the "awaiting" trait in task.Payload.

The Q&A flow is now uniformly the blocking RPC `boid task ask`: the agent stays alive inside a broker connection while the daemon waits for a user answer, which is then handed back over the same socket via the in-memory BlockingAskRegistry. There is no session-resume path: the legacy `notify --ask` → `reopen -m` round trip is no longer wired to claude `--resume`, and every dispatch starts a fresh agent process.

Fields written when an `ask` action lands:

  • Question: human-readable question text shown to the user
  • QuestionID: UUID identifying this Q&A turn (for multi-turn tracking)

Fields written by boid (set when the user submits an answer):

  • PendingAnswer: the user's reply (legacy field; the blocking RPC delivers answers in-memory and never sets this, but legacy `notify --ask` paths still surface it as $BOID_USER_ANSWER on the next hook invocation)

SessionID/Mode/Source have been removed: the harness-resume mode they described is gone, and persisted records with those fields deserialize cleanly (encoding/json ignores unknown keys).

func GetAwaitingPayload

func GetAwaitingPayload(payload json.RawMessage) AwaitingPayload

GetAwaitingPayload reads the awaiting trait from raw payload. Returns an empty struct if the trait is absent or malformed.

type BaseBranchState

type BaseBranchState int

BaseBranchState describes the relationship between the project's working directory HEAD and a task's resolved base_branch. The three cases drive Phase 2-2 supervisor execution location routing:

  • Case1HeadMatches: project dir is already on baseBranch → supervisor runs in project dir directly (worktree=false).
  • Case2ExistsButNotCheckedOut: baseBranch exists locally or on origin but project HEAD is on a different branch → supervisor needs a worktree that checks baseBranch out.
  • Case3NotFound: baseBranch is unknown to both local and origin → supervisor needs a worktree whose base branch must be created from the current project HEAD before the worktree can be allocated.

Detached-HEAD projects cannot serve as a sensible default for any of these cases (case 3 in particular needs a real branch to derive from), so classification reports the dedicated detached-HEAD error rather than guessing.

const (
	// BaseBranchStateUnknown is the zero value. It only appears when an error
	// is returned; callers should never act on it directly.
	BaseBranchStateUnknown BaseBranchState = iota
	// Case1HeadMatches: project HEAD == baseBranch. Worktree is unnecessary;
	// the supervisor can run directly in the project dir.
	Case1HeadMatches
	// Case2ExistsButNotCheckedOut: baseBranch resolves to a ref (local or
	// origin/<baseBranch>) but is not what the project HEAD points at.
	Case2ExistsButNotCheckedOut
	// Case3NotFound: baseBranch does not exist locally and origin/<baseBranch>
	// is unknown to the project repo. Callers that own the project dir create
	// the branch from HEAD before allocating a worktree.
	Case3NotFound
)

func ClassifyBaseBranch

func ClassifyBaseBranch(projectDir, baseBranch string) (BaseBranchState, error)

ClassifyBaseBranch inspects the git repository at projectDir to decide how a task whose resolved base_branch is baseBranch should be scheduled. See BaseBranchState for the three case definitions.

baseBranch must already be expanded (no ${...} templates) and non-empty. P1: empty baseBranch is rejected — the service layer must resolve ${current_branch} before calling ClassifyBaseBranch.

The classify call is read-only: no branches are created, no fetches are issued, and the project HEAD is not mutated. Case 3 mitigation (creating the branch) is the caller's responsibility; ClassifyBaseBranch only reports the state.

func (BaseBranchState) String

func (s BaseBranchState) String() string

String makes BaseBranchState printable for log lines / test failure messages. Not part of the public contract — callers should switch on the enum value, not on the rendered string.

type BehaviorResolution

type BehaviorResolution struct {
	BehaviorName string
	Traits       []string
	Readonly     bool
	Worktree     bool
	BranchPrefix string
	BaseBranch   string
	Payload      json.RawMessage
	Instructions Instructions
}

BehaviorResolution holds the resolved behavior fields after processing either a named behavior or an inline behavior_spec.

func ResolveBehavior

func ResolveBehavior(meta *ProjectMeta, req BehaviorResolveRequest) (*BehaviorResolution, error)

ResolveBehavior validates and resolves behavior fields from a task creation request. It handles both the named behavior path (meta lookup) and the inline behavior_spec path. When both behavior and behavior_spec are empty, the default is resolved via:

  1. meta.DefaultTaskBehavior if set
  2. implicit "supervisor" fallback if that behavior exists in meta (with WARN)
  3. error if neither is available
  4. hardcoded DefaultBehavior when meta is nil (nil-meta paths, e.g. test wiring)

type BehaviorResolveRequest

type BehaviorResolveRequest struct {
	Behavior     string
	BehaviorSpec *BehaviorSpec
	Payload      json.RawMessage
	Instructions json.RawMessage
}

BehaviorResolveRequest carries the behavior-relevant fields from a task creation request.

type BehaviorSpec

type BehaviorSpec struct {
	Name               string       `yaml:"name" json:"name"`
	Traits             []string     `yaml:"traits,omitempty" json:"traits,omitempty"`
	DefaultInstruction *Instruction `yaml:"default_instruction,omitempty" json:"default_instruction,omitempty"`
}

BehaviorSpec is an inline behavior specification that can be used instead of referencing a named behavior from project.yaml task_behaviors. This allows kits to self-describe the behavior they need without depending on project config.

type BindMount

type BindMount struct {
	Source   string `yaml:"source" json:"source"`
	Target   string `yaml:"target,omitempty" json:"target,omitempty"` // if empty, defaults to Source
	Mode     string `yaml:"mode" json:"mode"`                         // "rw" | "" (ro default)
	IsFile   bool   `yaml:"is_file,omitempty" json:"is_file,omitempty"`
	Optional bool   `yaml:"optional,omitempty" json:"optional,omitempty"` // if true, skip mount when Source does not exist on the host
}

BindMount is a plain shared DTO across orchestration and sandbox planning. It carries mount source/target/mode data and does not encode provider behavior.

func (*BindMount) UnmarshalYAML added in v0.0.8

func (b *BindMount) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts two equivalent forms so handwritten and generated kit.yaml can use whichever is more convenient:

additional_bindings:
  - /host/path              # short form: equivalent to {source: "/host/path"}
  - source: /host/path      # struct form: required when mode/target/is_file/etc. are set
    mode: rw

Without this, yaml.v3 rejects the short form with "cannot unmarshal !!str into orchestrator.BindMount" and the single kit's parse error cascades into project meta hydration falling back to raw meta, silently dropping host_commands from *unrelated* kits.

type BranchLockManager

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

BranchLockManager keeps exclusive access to a branch (identified by the compound key "<projectID>:<headBranch>") for the entire executing lifetime of a single task.

Unlike the underlying WorktreeLocker (which returns an opaque release closure per acquire), BranchLockManager keys the held lock by task id so that Acquire and Release can be called from different goroutines and at different points in the task lifecycle (Acquire when the task enters executing, Release when it leaves).

Calls are idempotent for the same task id: re-Acquire on a task that already holds the lock for the same branch is a no-op, and Release on an unheld task is a no-op. This matches the realistic dispatch flow where a task may go through multiple dispatch cycles inside a single executing window.

BranchLockManager is safe for concurrent use.

func NewBranchLockManager

func NewBranchLockManager(underlying WorktreeLocker) *BranchLockManager

NewBranchLockManager wraps the given WorktreeLocker so the lock can be pinned to a task id rather than a single Acquire call. The underlying locker is responsible for the actual mutual exclusion semantics (FIFO queueing, context cancellation, etc.).

func (*BranchLockManager) AcquireForTask

func (m *BranchLockManager) AcquireForTask(ctx context.Context, projectID, headBranch, taskID string) error

AcquireForTask acquires the branch lock keyed by "<projectID>:<headBranch>" on behalf of the given task. If the task already holds the lock for the same key, this is a no-op (no double-acquire). Otherwise it blocks on the underlying locker until either the lock is acquired or ctx is cancelled.

func (*BranchLockManager) IsHeldForTask

func (m *BranchLockManager) IsHeldForTask(taskID string) bool

IsHeldForTask reports whether the given task currently holds a branch lock. Intended for tests and diagnostics.

func (*BranchLockManager) ReleaseForTask

func (m *BranchLockManager) ReleaseForTask(taskID string)

ReleaseForTask releases the branch lock held on behalf of the task. No-op when the task does not currently hold a lock — the goal is to make "release on every executing-leaving path" cheap to wire up.

type BuiltinPolicy

type BuiltinPolicy struct {
	AllowedOps      []string
	AllowedCwdRoots []string
}

BuiltinPolicy is the orchestrator-owned, sandbox-agnostic policy type. dispatcher is responsible for translating this into sandbox.BuiltinPolicy before it reaches the broker.

AllowedOps is a sorted slice (rather than a set) so the value is trivially comparable and serialisable across the orchestrator/dispatcher boundary.

func (BuiltinPolicy) Allows

func (p BuiltinPolicy) Allows(op string) bool

Allows reports whether op is in the allowed set.

func (BuiltinPolicy) AllowsCwd

func (p BuiltinPolicy) AllowsCwd(cwd string) bool

AllowsCwd reports whether cwd is within any of the policy's additional cwd roots.

type Capabilities added in v0.0.2

type Capabilities struct {
	// Docker, when non-nil, enables the per-sandbox native docker proxy.
	// Declared as capabilities.docker: {} in project.yaml.
	Docker *DockerCapability `yaml:"docker,omitempty" json:"docker,omitempty"`
}

Capabilities declares optional sandbox capabilities declared in project.yaml.

type CleanupFunc

type CleanupFunc func()

CleanupFunc releases transient resources created while planning a JobSpec (e.g. staging directories for hook scripts). dispatcher invokes it after the sandbox process has exited. A nil CleanupFunc means nothing to release.

type CommandDef

type CommandDef struct {
	Name               string            `json:"name,omitempty"`
	Path               string            `json:"path,omitempty"`
	AllowedPatterns    []string          `json:"allowed_patterns,omitempty"`
	DeniedPatterns     []string          `json:"denied_patterns,omitempty"`
	AllowedSubcommands []string          `json:"allowed_subcommands,omitempty"`
	Env                map[string]string `json:"env,omitempty"`
	RejectRules        []RejectRule      `json:"reject_rules,omitempty"`
}

CommandDef is the orchestrator-side transport shape for sandbox command policy input. Dispatcher and sandbox mirror this shape; sandbox owns the enforcement semantics.

type Coordinator

type Coordinator struct {
	Evaluator      *Evaluator
	HookExecutor   HookExecutor
	Waiter         JobWaiter
	MaxDepth       int
	LifecycleStore LifecycleStore // optional; nil skips rework_count/abort derivation
}

Coordinator orchestrates the hook → advance flow.

Locking is owned by the workflow service for the entire executing lifetime of each task (see internal/api/service.go). The coordinator dispatches hooks under the assumption that the branch lock is already held by the caller.

func (*Coordinator) DispatchAndAdvance

func (d *Coordinator) DispatchAndAdvance(
	ctx context.Context,
	task *Task,
	meta *ProjectMeta,
	sm *StateMachine,
) (*DispatchResult, error)

DispatchAndAdvance runs the full dispatch cycle: 1. Evaluate and execute hooks (parallel if readonly, sequential otherwise) 2. Merge hook payload patches 3. Evaluate condition-based auto-advance via state machine

func (*Coordinator) ReplayHook

func (d *Coordinator) ReplayHook(
	ctx context.Context,
	task *Task,
	meta *ProjectMeta,
	sm *StateMachine,
	hookID string,
) (*ReplayResult, error)

ReplayHook executes a single named hook in isolation against the task's current state. After the hook completes, lifecycle is derived and sm.Advance is applied — identical to the post-hook phase of DispatchAndAdvance.

Returns an error if the hook ID is not found in the behavior or if the hook does not match the current status (e.g. task not in executing state).

type DBProjectCatalog

type DBProjectCatalog struct {
	DB db.DBTX
}

func (DBProjectCatalog) GetProject

func (c DBProjectCatalog) GetProject(id string) (*Project, error)

func (DBProjectCatalog) ListProjects

func (c DBProjectCatalog) ListProjects() ([]*Project, error)

type DBTaskLookup

type DBTaskLookup struct {
	DB db.DBTX
}

func (DBTaskLookup) GetTask

func (l DBTaskLookup) GetTask(id string) (*Task, error)

type DispatchPlanner

type DispatchPlanner struct {
	Meta     MetaCache
	Hydrator MetaHydrator // optional; when set, loadContext uses GetWithWorkspace instead of Get
	Projects ProjectCatalog
	Tasks    TaskLookup
	Adapter  adapters.HarnessAdapter
}

DispatchPlanner turns state-machine-driven hook fire events into a sandbox-agnostic JobSpec. All sandbox construction concerns (mounts, env, proxy wiring, exit scripts, worktree recreation) live in dispatcher.

func WireDispatchPlanner

func WireDispatchPlanner(cfg PlannerWireConfig) *DispatchPlanner

func (*DispatchPlanner) PlanHook

func (p *DispatchPlanner) PlanHook(event *HookFireEvent) (*JobSpec, CleanupFunc, error)

PlanHook renders a hook fire event into a JobSpec.

Agent-kind hooks (Hook.Kind == HandlerKindAgent) may omit ScriptPath — the HarnessAdapter builds its own argv from CLI conventions, so an empty Argv flows through fine. Non-agent hooks (shell-bound) still require a resolved script path. The Evaluator may synthesize a script-less agent hook when the behavior declares none of its own (Phase 3-e kit-retirement fallback); the relaxed validation here is what makes those virtual hooks dispatch-ready.

type DispatchResult

type DispatchResult struct {
	Results       []HandlerResult
	FiredEvents   []FiredEvent
	FinalPayload  json.RawMessage
	NewStatus     TaskStatus      // set if orchestrator advanced the state
	ActionPayload json.RawMessage // optional payload to attach to the auto_advance action
}

DispatchResult is the accumulated result of a full dispatch cycle.

type DockerCapability added in v0.0.2

type DockerCapability struct{}

DockerCapability is the opt-in marker for the native docker proxy. Presence (non-nil pointer in Capabilities) enables the proxy; the empty struct is a placeholder for future per-project policy fields.

type DoneReport

type DoneReport struct {
	Message string `json:"message"`
}

DoneReport carries the message the agent reported via `notify --done`.

type Evaluator

type Evaluator struct{}

func (*Evaluator) Evaluate

func (e *Evaluator) Evaluate(task *Task, hooks []Hook) []Hook

Evaluate returns hooks that should fire for the given task. Hooks fire only during executing state. Hooks with Kind == HandlerKindAgent additionally require an instruction in task.Instructions addressed to that hook's Agent.

Phase 3-e fallback: when the behavior declares no agent-kind hook at all (typical after the boid-kits claude-code/codex retirement landed in PR #604), the evaluator synthesizes a virtual agent-kind hook for the active instruction's agent. The runner-inner-child hands every agent-kind job to its HarnessAdapter directly, so a hook with no ScriptPath is dispatch-ready — see planner.PlanHook and adapters.HarnessAdapter.Run. The synthesis is gated to known harness agents (claude-code / codex / opencode) so unknown agent names do not collide with the shell adapter's Argv requirement.

type FailReport

type FailReport struct {
	Message string `json:"message"`
}

FailReport carries the message the agent reported via `notify --fail`.

type FiredEvent

type FiredEvent struct {
	KitID       string // kit that owns this handler; empty for project-local
	HandlerID   string // hook ID
	JobID       string // ID of the job that executed this handler
	Kind        string // "hook" or "hook_replay"
	SourceState string // task status at the time of dispatch
	Success     bool
	Error       string
}

FiredEvent records a single hook execution for action logging.

type GCLoop

type GCLoop struct {
	Store        GCStore
	Interval     time.Duration
	OlderThan    time.Duration
	InitialDelay time.Duration
}

GCLoop periodically calls GC on a GCStore.

func (*GCLoop) Run

func (l *GCLoop) Run(ctx context.Context)

Run blocks until ctx is done. It waits InitialDelay before the first GC run, then calls Store.GC every Interval. Errors are logged as warnings; the loop always continues.

type GCResult

type GCResult struct {
	Tasks      int64
	Jobs       int64
	Actions    int64
	Worktrees  int64
	Runtimes   int64
	SandboxTmp int64 // leaked /tmp/boid-* sandbox artifacts removed
	Devices    int64 // revoked web devices deleted
}

GCResult holds the count of records affected by GC.

func GCTasks

func GCTasks(dbtx db.DBTX, statuses []string, olderThan time.Duration, dryRun bool) (*GCResult, error)

GCTasks deletes terminal tasks older than olderThan and their related data (actions, jobs, worktrees). If dryRun is true, counts only without deleting. olderThan=0 disables the time filter (all matching tasks are affected). Must be called within a transaction for atomicity.

type GCStore

type GCStore interface {
	GC(olderThan time.Duration, dryRun bool) (*GCResult, error)
}

GCStore is the interface required by GCLoop to run garbage collection.

type HandlerKind

type HandlerKind string

HandlerKind distinguishes the role a hook plays. An empty kind means a generic hook (no instructions routing). Only agent-kind hooks participate in instructions routing.

const (
	HandlerKindAgent HandlerKind = "agent"
)

func (HandlerKind) IsValid

func (k HandlerKind) IsValid() bool

IsValid reports whether the kind value is recognized.

type HandlerResult

type HandlerResult struct {
	ID           string // hook ID
	Role         Role
	JobID        string // ID of the job that executed this handler
	ExitCode     int
	PayloadPatch json.RawMessage
}

HandlerResult is the result of a single hook execution.

type HandlerTraits

type HandlerTraits struct {
	Consumes []TraitType `json:"consumes,omitempty" yaml:"consumes,omitempty"`
	Produces []TraitType `json:"produces,omitempty" yaml:"produces,omitempty"`
}

type Hook

type Hook struct {
	ID         string        `yaml:"id" json:"id"`
	Name       string        `yaml:"name,omitempty" json:"name,omitempty"`
	Kind       HandlerKind   `yaml:"kind,omitempty" json:"kind,omitempty"`
	Traits     HandlerTraits `yaml:"traits" json:"traits"`
	Requires   []string      `yaml:"requires" json:"requires"`
	Agent      string        `yaml:"agent,omitempty" json:"agent,omitempty"`
	Kit        string        `yaml:"-" json:"kit,omitempty"`
	ScriptPath string        `yaml:"-" json:"-"`
}

func ListHooksForStatus

func ListHooksForStatus(meta *ProjectMeta, task *Task, status TaskStatus) []Hook

ListHooksForStatus returns hooks from the behavior that would match the given status. Since hooks only fire during executing, only executing status yields results. Used by "boid task hook list".

func (*Hook) UnmarshalYAML

func (h *Hook) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML rejects legacy `on:` entries to surface migration breakage clearly.

type HookExecutor

type HookExecutor interface {
	ExecuteHook(ctx context.Context, event *HookFireEvent) (jobID string, err error)
}

HookExecutor launches a hook and returns the job ID.

type HookFile

type HookFile struct {
	Source     string // host-side absolute path
	TargetName string // filename inside sandbox .boid/hooks/
}

HookFile describes a single hook file to bind-mount into the sandbox.

type HookFireEvent

type HookFireEvent struct {
	EventID   string
	TaskID    string
	ProjectID string
	Hook      Hook
}

type HostCommandSpec

type HostCommandSpec struct {
	Allow []string `yaml:"allow,omitempty" json:"allow,omitempty"`
	Deny  []string `yaml:"deny,omitempty" json:"deny,omitempty"`
	// Stdin is deprecated: it is still parsed for backward compatibility but
	// will be ignored in a future release (loading a spec with stdin: true
	// emits a deprecation warning).
	Stdin  bool              `yaml:"stdin,omitempty" json:"stdin,omitempty"`
	Path   string            `yaml:"path,omitempty" json:"path,omitempty"`
	Env    map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
	Reject []RejectRule      `yaml:"reject,omitempty" json:"reject,omitempty"`
}

HostCommandSpec is the simplified YAML DSL for declaring host commands.

func (HostCommandSpec) ToCommandDef

func (s HostCommandSpec) ToCommandDef(name string) CommandDef

ToCommandDef converts a HostCommandSpec into a CommandDef for internal use.

type HostCommands

type HostCommands map[string]HostCommandSpec

HostCommands supports both list and map YAML forms:

host_commands: [gh, aws]
host_commands:
  gh:
    allow: [pr, issue]
  aws:

func (HostCommands) ToCommandDefs

func (h HostCommands) ToCommandDefs() map[string]CommandDef

ToCommandDefs converts the DSL specs to internal CommandDef map.

func (*HostCommands) UnmarshalYAML

func (h *HostCommands) UnmarshalYAML(value *yaml.Node) error

type InMemoryWorktreeLockManager

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

InMemoryWorktreeLockManager implements WorktreeLocker with per-key FIFO queueing.

func NewInMemoryWorktreeLockManager

func NewInMemoryWorktreeLockManager() *InMemoryWorktreeLockManager

NewInMemoryWorktreeLockManager creates a new InMemoryWorktreeLockManager.

func (*InMemoryWorktreeLockManager) Acquire

func (m *InMemoryWorktreeLockManager) Acquire(ctx context.Context, key string) (func(), error)

type Instruction

type Instruction struct {
	Agent   string `json:"agent" yaml:"agent"`
	Name    string `json:"name,omitempty" yaml:"name,omitempty"`
	Message string `json:"message,omitempty" yaml:"message,omitempty"`
	Model   string `json:"model,omitempty" yaml:"model,omitempty"`
}

type Instructions

type Instructions []Instruction

Instructions is the persisted instruction history for a task. The most recent entry is the "active" instruction passed to the agent on dispatch; earlier entries are kept as history (e.g. for reopen tracking).

JSON shape on the wire is an array. For backward compatibility, the legacy single-instruction map form ({"main": {...}}) is also accepted on unmarshal and converted to a single-element array.

func AppendInstruction

func AppendInstruction(existing Instructions, inst Instruction) Instructions

AppendInstruction returns a new instruction list with `inst` appended. The caller is responsible for filling in fields not derived from the previous active entry. Used by `boid task reopen` to record a new context message while preserving history.

func MergeDefaultInstructions

func MergeDefaultInstructions(defaultInstruction *Instruction, requestInstructions json.RawMessage) (Instructions, error)

MergeDefaultInstructions builds the initial instruction list for a new task.

Merge semantics:

  • defaultInstruction == nil: use requestInstructions as-is (no base to inherit from).
  • defaultInstruction != nil:
  • override empty/null/[]/{} → return the default as a single-entry list.
  • override has exactly 1 entry → per-field merge: non-empty fields from the override win; empty fields inherit from defaultInstruction.
  • override has 2+ entries → complete replacement (caller is building an explicit history and partial merge would be ambiguous).

requestInstructions accepts the array form `[{...}, ...]` and the legacy single-map form `{"main": {...}}` (handled by Instructions.UnmarshalJSON).

func (Instructions) Active

func (is Instructions) Active() *Instruction

Active returns the currently-active instruction (the last entry), or nil if the list is empty.

func (*Instructions) UnmarshalJSON

func (is *Instructions) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts both the new array form and the legacy {"main": {...}, "verify": {...}} map form. For the map form, only the "main" entry is preserved (verifying/reworking variants were removed).

type JobCompletion

type JobCompletion struct {
	JobID    string
	Output   string // stdout capture or payload_patch.json content
	ExitCode int
}

JobCompletion represents the result of a completed job.

type JobKind

type JobKind string

JobKind is a DB / observability label for a job. It is NOT a sandbox construction signal — dispatcher's mount/env logic must derive everything from Visibility / HostCommands / Instruction, never from Kind. Kind exists purely so the Job row / TUI can tell apart "agent hook" and "user-initiated exec" without re-deriving the category from loosely-related primitives like HandlerID.

const (
	JobKindHook JobKind = "hook"
	JobKindExec JobKind = "exec"
	// JobKindSession is the Phase 3-d label for a user-initiated agent
	// session not tied to a task lifecycle (WebUI [New Session] / `boid
	// agent` CLI). It is distinct from JobKindHook (state-machine driven)
	// and JobKindExec (sandboxed argv with no harness), so the TUI / Web UI
	// can present it as a separate top-level concept.
	JobKindSession JobKind = "session"
)

type JobSpec

type JobSpec struct {
	// Identity used by dispatcher for Job DB persistence and state-machine
	// notification. TaskID and HandlerID are empty for boid-exec jobs.
	TaskID      string
	ProjectID   string
	HandlerID   string
	DisplayName string // human-readable label (hook name or command-session name); persisted to jobs.display_name

	// Kind is a DB-label / TUI-display category. Sandbox construction details
	// MUST NOT branch on this value.
	Kind JobKind

	// Argv is the command to execute. Argv[0] is either a host absolute path
	// (hook scripts) or a bare command name resolved via broker shim
	// (boid exec). Everything after Argv[0] is passed as-is.
	Argv []string

	// Instruction, when non-nil, materializes at $HOME/.boid/context/instructions.yaml
	// and identifies the agent that should pick up the job. Agent-less jobs
	// (boid exec) leave this nil.
	Instruction *RoutedInstruction

	// Task, when non-nil, materializes at $HOME/.boid/context/task.yaml.
	// boid-exec jobs always leave it nil.
	Task *TaskSnapshot

	// PrimaryInput is the payload the script expects to read on stdin (or,
	// for agent jobs, at $HOME/.boid/context/payload.json).
	// nil means the job reads nothing from boid (e.g. boid-exec with a user TTY).
	PrimaryInput json.RawMessage

	// Visibility describes which host directories the sandbox sees and whether
	// they are writable.
	Visibility Visibility

	// BuiltinPolicies authorises broker-mediated builtin operations (boid, git).
	BuiltinPolicies map[string]BuiltinPolicy

	// HostCommands authorises broker-mediated host command invocations.
	// Hook jobs leave this empty; exec jobs populate it from behavior.
	HostCommands map[string]CommandDef

	// SecretNamespace scopes the broker's secret resolver.
	SecretNamespace string

	// Env carries extra environment variables the orchestrator wants to export
	// (e.g. behavior-level overrides). dispatcher merges these with its own
	// HOME/PATH/proxy/broker settings.
	Env map[string]string

	// ExecutionState records the task.Status at the time this job was dispatched.
	// Stored in the job DB row so TUI can reconstruct replay context.
	ExecutionState string

	// Interactive, when true, forces TTY allocation regardless of whether an
	// Instruction is attached. Used by daemon-side command execution (Web UI)
	// where the caller always expects a PTY-backed terminal.
	Interactive bool

	// HarnessType identifies which HarnessAdapter implementation the runner
	// hands the agent process off to. Phase 3-d made this invariant
	// non-empty for every dispatched job:
	//   - "shell" for hooks without an `agent:` declaration, every `boid
	//     exec`, and the fall-through for unknown agents
	//   - "claude" / "codex" / "opencode" for the corresponding agent hooks
	//     and user-initiated sessions
	// dispatcher bridges this into sandbox.Spec.HarnessType and the
	// runner-inner-child resolves the adapter via the registry.
	HarnessType string

	// SandboxProfile selects the filesystem layout strategy for the sandbox.
	// Zero value (sandbox.ProfileDefault) preserves existing behaviour.
	// Set to sandbox.ProfileInit for kit-init / workspace-configure generation
	// scripts that need read access to the full host filesystem.
	// When ProfileInit is set, broker registration and the broker socket mount
	// are both skipped.
	SandboxProfile int // sandbox.Profile — kept as int to avoid a circular import
}

JobSpec is the orchestrator-owned, sandbox-agnostic execution request. It is written purely in business vocabulary: "what to run with what visibility and permissions". All sandbox construction details (mounts, env, exit scripts, proxy wiring, stdin routing) are left to dispatcher.

dispatcher is the only layer that bridges JobSpec and sandbox.Spec.

type JobWaiter

type JobWaiter interface {
	WaitForJob(ctx context.Context, jobID string) (JobCompletion, error)
}

JobWaiter waits for a job to complete.

type KitMeta

type KitMeta struct {
	HostCommands       HostCommands      `yaml:"host_commands"`
	AdditionalBindings []BindMount       `yaml:"additional_bindings"`
	Env                map[string]string `yaml:"env"`
	KitRoot            string            `yaml:"-"` // directory containing kit.yaml

	// Human-readable metadata — not merged into runtime spec.
	Meta *KitMetaInfo `yaml:"meta,omitempty"`
}

KitMeta holds the parsed content of a kit.yaml file. A kit provides tooling only: host_commands, env, and additional_bindings. Kits do not provide hooks or task_behaviors (those are project/workspace concerns).

func ReadKitMeta

func ReadKitMeta(dir string) (*KitMeta, error)

type KitMetaInfo

type KitMetaInfo struct {
	Name        string `yaml:"name"`
	Description string `yaml:"description"`
	Category    string `yaml:"category"` // language / vcs / ci / agent / workflow / utility
}

KitMetaInfo holds human-readable metadata for a kit.

type KitRef

type KitRef struct {
	Ref string `yaml:"ref" json:"ref"`
}

func (*KitRef) UnmarshalYAML

func (k *KitRef) UnmarshalYAML(value *yaml.Node) error

type KitRegistry

type KitRegistry struct {
	BaseDir string // e.g. ~/.local/share/boid/kits
}

KitRegistry manages installed kits under a base directory. Each kit is a directory ~/.local/share/boid/kits/<name>/ containing kit.yaml.

func NewRegistry

func NewRegistry(baseDir string) *KitRegistry

NewRegistry creates a new kit registry with the given base directory.

func (*KitRegistry) IsInstalled

func (r *KitRegistry) IsInstalled(name string) bool

IsInstalled returns true if the kit directory exists under BaseDir.

func (*KitRegistry) List

func (r *KitRegistry) List() ([]string, error)

List returns all installed kit names. It finds directories that contain kit.yaml directly under BaseDir. If BaseDir does not exist, an empty slice and nil error are returned.

func (*KitRegistry) Remove

func (r *KitRegistry) Remove(name string) error

Remove deletes an installed kit directory.

func (*KitRegistry) Resolve

func (r *KitRegistry) Resolve(name string) (string, error)

Resolve returns the absolute filesystem path for a kit directory. The name must be a valid kit name (see ValidKitName).

nil receiver は「 KitsDir 未設定 (typed-nil pointer が KitResolver interface に boxed されて呼び出し側の `resolver == nil` を素通りした) 」 状態を意味する。 呼び出し側 (resolveKitRef) の nil check を信頼すべきだが、 caller 全てに untyped nil interface 渡しを徹底するのは現実的でないので defensive guard で 戻り値の error に倒す。 #635 (KitRegistry 簡素化) で typed nil 罠を踏み、 internal/api / internal/server の test panic を起こしていた退行のガード。

type KitResolver

type KitResolver interface {
	Resolve(ref string) (string, error)
}

type KitRuntime

type KitRuntime struct {
	AdditionalBindings []BindMount
	HostCommands       HostCommands
	Env                map[string]string
}

KitRuntime holds the merged runtime fields derived from a set of kits. It covers env, host_commands, and additional_bindings. Hooks and directory metadata are excluded — those are TaskBehavior-specific and handled by MergeKitMetaIntoBehavior.

func MergeKitRuntime

func MergeKitRuntime(kits []*KitMeta, kitAgents []string) (KitRuntime, error)

MergeKitRuntime merges env, host_commands, and additional_bindings from the given kits into a KitRuntime value. kitAgents provides display names for error messages (one per kit).

type LegacyProjectMeta added in v0.0.8

type LegacyProjectMeta struct {
	ID                  string                        `yaml:"id"`
	Name                string                        `yaml:"name"`
	Kits                []KitRef                      `yaml:"kits,omitempty"`
	TaskBehaviors       map[string]LegacyTaskBehavior `yaml:"task_behaviors,omitempty"`
	Worktree            bool                          `yaml:"worktree,omitempty"`
	BaseBranch          string                        `yaml:"base_branch,omitempty"`
	ForkPoint           string                        `yaml:"fork_point,omitempty"`
	HostCommands        HostCommands                  `yaml:"host_commands,omitempty"`
	AdditionalBindings  []BindMount                   `yaml:"additional_bindings,omitempty"`
	Env                 map[string]string             `yaml:"env,omitempty"`
	SecretNamespace     string                        `yaml:"secret_namespace,omitempty"`
	Capabilities        Capabilities                  `yaml:"capabilities,omitempty"`
	DefaultTaskBehavior string                        `yaml:"default_task_behavior,omitempty"`
}

LegacyProjectMeta holds the raw contents of a pre-migration project.yaml, including fields that have been moved to workspace.yaml and kit.yaml in the new schema. This type is used only by the migrate command; normal loading goes through ReadProjectMeta.

func ReadProjectMetaLegacy added in v0.0.8

func ReadProjectMetaLegacy(dir string) (*LegacyProjectMeta, error)

ReadProjectMetaLegacy reads .boid/project.yaml (and .boid/project.local.yaml if present) using a raw-map first pass that does NOT reject unknown or removed fields. This is intentionally separate from ReadProjectMeta so that the migrate command can load old-schema files that would otherwise fail validation.

The returned LegacyProjectMeta captures all fields that are subject to migration, including kits/env/host_commands/additional_bindings/secret_namespace/ capabilities at both the project level and the task_behaviors level.

func (*LegacyProjectMeta) CollectAllKitRefs added in v0.0.8

func (m *LegacyProjectMeta) CollectAllKitRefs() []KitRef

CollectAllKitRefs returns a deduplicated, ordered list of all kit refs referenced at the project level and across all task behaviors.

func (*LegacyProjectMeta) HasLegacyFields added in v0.0.8

func (m *LegacyProjectMeta) HasLegacyFields() bool

HasLegacyFields reports whether the LegacyProjectMeta contains any fields that should be migrated to workspace.yaml or a legacy kit. This is used by the migrate command to detect whether migration is needed at all.

type LegacyTaskBehavior added in v0.0.8

type LegacyTaskBehavior struct {
	Readonly           *bool        `yaml:"readonly,omitempty"`
	Traits             []string     `yaml:"traits,omitempty"`
	DefaultInstruction *Instruction `yaml:"default_instruction,omitempty"`
	Kits               []KitRef     `yaml:"kits,omitempty"`
}

LegacyTaskBehavior holds a task behavior including the legacy kits field that appears at the behavior level in old project.yaml files.

type Lifecycle

type Lifecycle struct {
	Executed bool         `json:"executed"`
	Done     *DoneReport  `json:"done,omitempty"`
	Fail     *FailReport  `json:"fail,omitempty"`
	Abort    *AbortReason `json:"abort,omitempty"`
}

Lifecycle holds computed lifecycle traits derived from action history and the current dispatch state. It is never persisted to the payload; it is injected transiently before state-machine evaluation.

Done / Fail carry the agent's intent reported via `notify --done` / `notify --fail` (recorded as `done_request` / `fail_request` actions). They gate the executing→done / executing→aborted auto-advance rules. By keeping these as derived traits (rather than firing an immediate state transition inside NotifyTask), the runtime is allowed to exit cleanly via SIGUSR1 → bash EXIT trap → `boid job done` before the dispatch loop advances the state. See docs/plans/lifecycle-accountability.md (Phase 2.c follow-up).

func DeriveLifecycle

func DeriveLifecycle(_ context.Context, taskID string, store LifecycleStore, hookExecuted bool) (Lifecycle, error)

DeriveLifecycle computes lifecycle traits from action history. hookExecuted indicates whether a hook job completed successfully in the current dispatch cycle (used to set lifecycle.executed). If store is nil, only Executed is set from hookExecuted.

Done / Fail are scoped to the current executing cycle: every time the task (re-)enters executing (start / answer / reopen) the prior intent is cleared, so a `done_request` recorded before a `reopen` does not bleed into the new cycle. done_request / fail_request are mutually exclusive — recording one clears the other.

type LifecycleStore

type LifecycleStore interface {
	ListActionsByTask(taskID string) ([]*Action, error)
}

LifecycleStore is the minimal interface required to derive lifecycle traits from persistent action history.

type MergeMode

type MergeMode string
const (
	MergeModeExclusive MergeMode = "exclusive"
	MergeModeShared    MergeMode = "shared"
)

func TraitMergeMode

func TraitMergeMode(trait TraitType) MergeMode

type MetaCache

type MetaCache interface {
	Get(id string) (*ProjectMeta, bool)
}

type MetaHydrator added in v0.0.8

type MetaHydrator interface {
	GetWithWorkspace(ctx context.Context, projectID string) (*ProjectMeta, error)
}

MetaHydrator extends MetaCache with workspace-aware hydration. When a ProjectStore has a WorkspaceStore configured, it implements this interface and returns a ProjectMeta enriched with workspace capabilities/kits/env.

type PlannerWireConfig

type PlannerWireConfig struct {
	Meta     MetaCache
	Hydrator MetaHydrator // optional; when set, dispatch uses GetWithWorkspace
	Projects ProjectCatalog
	Tasks    TaskLookup
	Adapter  adapters.HarnessAdapter
}

type PolicyContext

type PolicyContext struct {
	ProjectDir string
	HomeDir    string
}

PolicyContext carries non-role data needed to compute role-derived policies. ProjectDir lets boid policy accept the host project dir as cwd. HomeDir accepts the sandbox HOME, which is the default WorkDir for hook jobs.

type Project

type Project struct {
	ID          string      `json:"id"`
	WorkspaceID string      `json:"workspace_id"`
	WorkDir     string      `json:"work_dir"`
	Meta        ProjectMeta `json:"meta"`
	CreatedAt   time.Time   `json:"created_at"`
	UpdatedAt   time.Time   `json:"updated_at"`
}

func GetProject

func GetProject(dbtx db.DBTX, id string) (*Project, error)

GetProject retrieves a project by ID.

func ListProjects

func ListProjects(dbtx db.DBTX) ([]*Project, error)

ListProjects returns all projects ordered by creation time.

type ProjectCatalog

type ProjectCatalog interface {
	GetProject(id string) (*Project, error)
}

type ProjectLocalMeta

type ProjectLocalMeta struct {
	Version            int               `yaml:"version"`
	HostCommands       HostCommands      `yaml:"host_commands,omitempty"`
	AdditionalBindings []BindMount       `yaml:"additional_bindings,omitempty"`
	Env                map[string]string `yaml:"env,omitempty"`
	SecretNamespace    string            `yaml:"secret_namespace,omitempty"`
}

func NewProjectLocalMeta

func NewProjectLocalMeta() *ProjectLocalMeta

func ReadProjectLocalMeta

func ReadProjectLocalMeta(dir string) (*ProjectLocalMeta, error)

type ProjectMeta

type ProjectMeta struct {
	ID            string                  `yaml:"id" json:"id"`
	Name          string                  `yaml:"name" json:"name"`
	TaskBehaviors map[string]TaskBehavior `yaml:"task_behaviors" json:"task_behaviors"`
	// Worktree controls whether tasks in this project run in a per-task git
	// worktree by default. For the canonical "executor" behavior the value
	// is used as-is; for "supervisor" the worktree decision is governed by
	// the base_branch state classification (see ClassifyBaseBranch). For
	// non-canonical behaviors, this is the worktree flag verbatim.
	Worktree bool `yaml:"worktree,omitempty" json:"worktree,omitempty"`
	// BaseBranch is the default git base branch for worktrees created by
	// tasks in this project. It is resolved at task creation time (with
	// ${TASK_REMOTE_ID} / ${current_branch} expansion) and persisted on
	// each task row.
	BaseBranch string `yaml:"base_branch,omitempty" json:"base_branch,omitempty"`
	// ForkPoint is the git ref used as the start point when creating a
	// base branch that does not yet exist (ClassifyBaseBranch case 3).
	// Accepts any ref that `git rev-parse --verify` resolves (e.g. "main",
	// "origin/main", a tag, or a commit SHA). When empty, the dispatcher
	// falls back to "refs/remotes/origin/HEAD"; if that is also unset the
	// case-3 worktree creation fails. The project root's working-tree HEAD
	// is intentionally never consulted, since it can drift to an
	// unexpected branch between task creation and dispatch.
	ForkPoint          string            `yaml:"fork_point,omitempty" json:"fork_point,omitempty"`
	HostCommands       HostCommands      `yaml:"host_commands" json:"host_commands"`
	AdditionalBindings []BindMount       `yaml:"additional_bindings" json:"additional_bindings"`
	Env                map[string]string `yaml:"env" json:"env"`
	// SecretNamespace is a runtime-only field injected at hydration time from the
	// linked workspace ID. It is intentionally not read from project.yaml (yaml:"-").
	SecretNamespace string `yaml:"-" json:"secret_namespace,omitempty"`
	// Capabilities declares optional sandbox capabilities. This is a runtime-only
	// field injected from workspace.yaml at hydration time (yaml:"-").
	Capabilities Capabilities `yaml:"-" json:"capabilities,omitempty"`
	// DefaultTaskBehavior names the behavior to use when a CreateTaskRequest
	// omits both behavior and behavior_spec. When empty, the daemon falls back
	// to "supervisor" if that behavior exists (with a deprecation warning);
	// if neither is set, CreateTask returns an error.
	DefaultTaskBehavior string `yaml:"default_task_behavior,omitempty" json:"default_task_behavior,omitempty"`
}

func ReadProjectMeta

func ReadProjectMeta(dir string) (*ProjectMeta, error)

func ReadProjectMetaWithKits

func ReadProjectMetaWithKits(dir string, resolver KitResolver) (*ProjectMeta, error)

ReadProjectMetaWithKits reads project.yaml and merges project-level overlays into each behavior. Returns a ProjectMeta whose TaskBehaviors have their resolved Hooks/etc. populated and ready for dispatch.

Note: kits are no longer supported in project.yaml (removed in the new schema). Workspace kits are resolved at GetWithWorkspace time via WorkspaceStore. project.local.yaml is deprecated; use workspace.yaml instead.

type ProjectMigrationError added in v0.0.8

type ProjectMigrationError struct {
	Projects []ProjectMigrationIssue
}

ProjectMigrationError is a typed error carrying one or more migration issues. Callers (e.g. boid start) use errors.As to extract the issues and drive auto-migration; Error() preserves the legacy multi-line format so log output (boid.log) is byte-identical to the pre-typed-error version.

func (*ProjectMigrationError) Error added in v0.0.8

func (e *ProjectMigrationError) Error() string

Error returns the multi-line, human-readable error message. The format for a single issue is byte-identical to the legacy rejectRemovedProjectFields output so existing spec_loader tests pass unchanged. Multiple issues are separated by a single newline.

type ProjectMigrationIssue added in v0.0.8

type ProjectMigrationIssue struct {
	// ProjectID is the registered project ID, when known. Empty when the
	// issue surfaces from a loader path that does not yet have a DB lookup
	// (e.g. ReadProjectMeta called directly with a path).
	ProjectID string
	// Dir is the absolute path to the project root (parent of .boid/).
	Dir string
	// Messages are the per-field violation messages in the same order as
	// the old single-string error produced by rejectRemovedProjectFields.
	Messages []string
}

ProjectMigrationIssue describes one project whose project.yaml uses the pre-Phase-3-e schema (top-level kits / host_commands / additional_bindings / secret_namespace / capabilities, or task_behaviors.<name>.kits) and must be migrated to the new workspace+kit layout via `boid project migrate`.

type ProjectMissingError added in v0.0.8

type ProjectMissingError struct {
	ProjectID string // registered project ID
	Dir       string // expected project root (where .boid/project.yaml should live)
	Err       error  // underlying os.ReadFile error (preserved for diagnostics / errors.Is)
}

ProjectMissingError is a typed error indicating that a project registered in the DB no longer has a project.yaml on disk (the project directory was removed, the path moved, etc.). It is distinct from *ProjectMigrationError, which signals schema migration is required.

Callers (boid start parent / server wire) extract this via errors.As and auto-prune the stale row from the project DB so the daemon can boot instead of refusing because of a dangling registration. The decision is data-safe: the project.yaml is the source of truth, so a DB row that points at a vanished directory is unambiguously stale.

func (*ProjectMissingError) Error added in v0.0.8

func (e *ProjectMissingError) Error() string

Error matches the legacy `project "<id>": <inner>` shape used by wrapPerProjectLoadErr so existing log output is byte-identical.

func (*ProjectMissingError) Unwrap added in v0.0.8

func (e *ProjectMissingError) Unwrap() error

Unwrap exposes the underlying os.ReadFile error so callers can errors.Is(err, fs.ErrNotExist) without knowing about ProjectMissingError.

type ProjectRepository

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

func NewProjectRepository

func NewProjectRepository(db db.DBTX) *ProjectRepository

func (*ProjectRepository) AssignDefaultWorkspaceToUnlinked added in v0.0.8

func (r *ProjectRepository) AssignDefaultWorkspaceToUnlinked(workspaceID string) (int, error)

AssignDefaultWorkspaceToUnlinked inserts a project_workspaces row pointing at workspaceID for every project that does not yet have one. Returns the number of rows inserted. See the package-level function for the underlying statement.

func (*ProjectRepository) CreateProject

func (r *ProjectRepository) CreateProject(project *Project) error

func (*ProjectRepository) DeleteProject

func (r *ProjectRepository) DeleteProject(id string) error

func (*ProjectRepository) GetProject

func (r *ProjectRepository) GetProject(id string) (*Project, error)

func (*ProjectRepository) ListProjects

func (r *ProjectRepository) ListProjects() ([]*Project, error)

func (*ProjectRepository) ListWorkspaces

func (r *ProjectRepository) ListWorkspaces() ([]*WorkspaceSummary, error)

func (*ProjectRepository) SetProjectWorkspace

func (r *ProjectRepository) SetProjectWorkspace(projectID, workspaceID string) error

type ProjectStore

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

ProjectStore holds project metadata in memory, loaded from project.yaml files.

func NewProjectStore

func NewProjectStore(resolver KitResolver) *ProjectStore

NewProjectStore creates a new store. If resolver is non-nil, kit references in project.yaml files will be resolved and merged at load time.

func (*ProjectStore) Get

func (s *ProjectStore) Get(id string) (*ProjectMeta, bool)

Get returns the cached meta for a project.

func (*ProjectStore) GetWithWorkspace added in v0.0.8

func (s *ProjectStore) GetWithWorkspace(_ context.Context, projectID string) (*ProjectMeta, error)

GetWithWorkspace returns a ProjectMeta hydrated with workspace-level capabilities, kits, env, and SecretNamespace injection.

Hydration rules:

  • If the project has no linked workspace, returns the cached meta unchanged.
  • If linked: always injects meta.SecretNamespace = workspaceID.
  • On workspace.yaml load success: merges Capabilities, kits, and Env.
  • On os.ErrNotExist (degraded window): logs a warning, returns meta with only SecretNamespace injected (no error).
  • On other errors: returns nil and the error.

Workspace kit merging rules:

  • All workspace kits always merge into the top-level meta.HostCommands / AdditionalBindings / Env so session jobs (which bypass behaviors) see the resolved tools.
  • For per-behavior merging (env/host_commands/bindings/KitRoots), every behavior receives every workspace kit. Kits do not provide hooks.

The returned *ProjectMeta is a fresh copy when hydration occurs; callers must not mutate the value returned when workspaceID is empty (it is the cached pointer).

func (*ProjectStore) Load

func (s *ProjectStore) Load(workDir string) (*ProjectMeta, error)

Load reads project.yaml from the work_dir and stores the meta in memory.

func (*ProjectStore) LoadAll

func (s *ProjectStore) LoadAll(projects []*Project) []error

LoadAll reads project.yaml for each registered project and records each project's workspaceID so that GetWithWorkspace can hydrate at call time.

Per-project errors are returned in the original order. When the inner error is a *ProjectMigrationError, the candidate's project ID is stamped onto every Issue in the returned error so downstream callers (e.g. the boid start parent picking issues out via errors.As) can drive auto-migration without parsing strings. Non-migration errors retain the legacy `project "<id>": <wrapped>` form.

func (*ProjectStore) Remove

func (s *ProjectStore) Remove(id string)

Remove deletes a project's meta from the store.

func (*ProjectStore) Set

func (s *ProjectStore) Set(id string, meta *ProjectMeta)

Set stores meta directly.

func (*ProjectStore) SetWorkspaceID added in v0.0.8

func (s *ProjectStore) SetWorkspaceID(projectID, workspaceID string)

SetWorkspaceID updates the cached workspace association for a project. Empty workspaceID clears the association. Subsequent GetWithWorkspace calls will hydrate using the new value (or return the cached meta unchanged when cleared).

func (*ProjectStore) SetWorkspaceStore added in v0.0.8

func (s *ProjectStore) SetWorkspaceStore(ws *WorkspaceStore)

SetWorkspaceStore configures the workspace store used by GetWithWorkspace. Call this before LoadAll when workspace hydration is desired.

func (*ProjectStore) WorkspaceStore added in v0.0.8

func (s *ProjectStore) WorkspaceStore() *WorkspaceStore

WorkspaceStore returns the configured workspace store (or nil when none has been wired). Exposed so server-side wiring can hand the same store to dispatcher.Runner for workspace-scoped proxy allowlist resolution at dispatch time, without having to construct a second one.

Note: callers that pass the result into an interface-typed field should guard against the typed-nil trap by checking the concrete return for nil before assigning — see internal/server/wire.go (resolveDispatcherWorkspaceLookup).

type RawPayload

type RawPayload json.RawMessage

func (RawPayload) MarshalJSON

func (p RawPayload) MarshalJSON() ([]byte, error)

func (RawPayload) RawMessage

func (p RawPayload) RawMessage() json.RawMessage

func (*RawPayload) UnmarshalJSON

func (p *RawPayload) UnmarshalJSON(data []byte) error

UnmarshalJSON / MarshalJSON は json.RawMessage と同じ振る舞いを named type に 改めて再実装する。 named type はメソッドを継承しないため、 これが無いと encoding/json は underlying []byte 扱いで base64 文字列を要求してしまい、 JSON object/array 形式の default_payload を弾いてしまう。

func (*RawPayload) UnmarshalYAML

func (p *RawPayload) UnmarshalYAML(node *yaml.Node) error

type RejectRule added in v0.0.9

type RejectRule struct {
	Match  string `yaml:"match" json:"match"`
	Reason string `yaml:"reason" json:"reason"`
}

RejectRule declares a pattern that rejects an invocation with a human-readable reason. Match is a glob over the joined args string with the same semantics as allow/deny patterns (globMatch in internal/sandbox/policy.go). Reason is surfaced to the agent so it can self-correct. This type is vocabulary/transport only for now; enforcement is wired up separately.

type ReplayResult

type ReplayResult struct {
	Result       HandlerResult
	FinalPayload json.RawMessage
	NewStatus    TaskStatus
	FiredEvents  []FiredEvent
}

ReplayResult is the result of a single-hook replay operation.

type Role

type Role string
const (
	RoleHook Role = "hook"
)

type RoutedInstruction

type RoutedInstruction struct {
	Role    string `json:"role"`
	Agent   string `json:"agent"`
	Name    string `json:"name,omitempty"`
	Message string `json:"message"`
	Model   string `json:"model,omitempty"`
}

func FilterInstructions

func FilterInstructions(instructions Instructions, agent string) []RoutedInstruction

FilterInstructions returns the active routed instruction for the given agent. Only the most recent entry in the history is considered (older entries are kept for audit but do not drive dispatch). Returns nil when agent is empty or the active entry does not address it. Callers gate on status==executing before routing (see selectInstruction).

type Rule

type Rule struct {
	Action          string // manual transition trigger (mutually exclusive with Condition)
	FromStatus      string // "*" matches any
	ToStatus        string
	Condition       TransitionCondition                   // auto transition trigger (mutually exclusive with Action)
	Manual          bool                                  // true if the action is user-initiated (shown in available_actions)
	ActionPayloadFn func(json.RawMessage) json.RawMessage // optional; generates action.Payload when the rule fires
}

type SecretFinding added in v0.0.8

type SecretFinding struct {
	// Path is the source file path used for attribution.
	Path string
	// Line / Column are 1-based, from the YAML node position.
	Line   int
	Column int
	// Kind is "high-entropy" or "literal-marker".
	Kind string
	// Pattern names the matching marker (only set for literal-marker findings,
	// e.g. "password=").
	Pattern string
	// Snippet is a redacted fingerprint of the offending text suitable for
	// display in error messages: short values are summarised by length only,
	// longer values are reduced to "<first4>...<last4>" so the report never
	// echoes the raw value back to logs.
	Snippet string
}

SecretFinding describes a YAML scalar value that looks like a raw secret (a high-entropy token, or text containing a literal marker such as "password=").

The scanner is intentionally conservative. The goal is to catch obvious leaks of credentials written into kit.yaml / workspace.yaml during `boid kit init` / `boid workspace configure` generation, not to be a comprehensive DLP system. False positives on long opaque identifiers (digest strings, UUIDs without dashes, image references) are accepted as the cost of a simple implementation.

func ScanSecretsBytes added in v0.0.8

func ScanSecretsBytes(path string, data []byte) ([]SecretFinding, error)

ScanSecretsBytes scans an in-memory YAML document.

path is used only for finding attribution (Finding.Path). Pass the expected on-disk path when scanning generated content before persisting it to disk, or any descriptive label for tests.

func ScanSecretsFile added in v0.0.8

func ScanSecretsFile(path string) ([]SecretFinding, error)

ScanSecretsFile reads path as YAML and returns any suspicious-looking scalar values.

Returns nil + nil if the file parses cleanly and contains no findings. Returns nil + non-nil for I/O failures or YAML parse errors so callers can distinguish "could not check" from "checked and clean".

func (SecretFinding) String added in v0.0.8

func (f SecretFinding) String() string

String returns a human-readable single-line description that never includes the raw value.

type StateMachine

type StateMachine struct {
	Name  string
	Rules []Rule
}

func DefaultMachine

func DefaultMachine() *StateMachine

DefaultMachine returns the unified state machine used by all tasks.

func NewMachine

func NewMachine() *StateMachine

NewMachine returns the unified state machine.

Manual transitions:

start  : pending → executing
done   : executing → done     (UI button; agent path goes through done_request + auto)
done   : awaiting → done      (parent confirms child's done_request)
fail   : executing → aborted  (UI button; agent path goes through fail_request + auto)
reopen : done → executing
reopen : aborted → executing  (recover from failure via fix)
ask    : executing → awaiting
answer : awaiting → executing
abort  : * → aborted

Event-driven transitions:

job_failed : * → aborted

Non-transitioning records (created directly by NotifyTask, bypassing ApplyAction):

progress      : * → *   (FYI timeline note)
done_request  : * → *   (agent's `notify --done` intent; consumed by DeriveLifecycle)
fail_request  : * → *   (agent's `notify --fail` intent; consumed by DeriveLifecycle)

Auto transitions (condition-based, evaluated after dispatch). Order matters — first match wins:

executing → aborted when lifecycle.executed && lifecycle.fail
executing → done    when lifecycle.executed && lifecycle.done
executing → done    when lifecycle.executed                     (legacy bare; non-agent hooks)

`lifecycle.{executed,done,fail}` are transient traits injected by the coordinator; they are never persisted to the payload. The state machine treats them as input signals derived from the action history (done_request / fail_request) plus the just-finished hook outcome.

The split between `done_request` (intent recorded immediately) and the auto-advance (state transition after `lifecycle.executed` confirms the runtime exited cleanly) preserves the bash EXIT trap → `boid job done` path. Without this split NotifyTask had to SIGTERM the runtime to apply the state transition synchronously, which raced against the SIGUSR1 graceful-stop path and left jobs marked failed.

Hook failures surface as job_failed via the dispatcher path, which routes the task to aborted.

func (*StateMachine) Advance

func (sm *StateMachine) Advance(task *Task) (*Task, bool)

Advance evaluates condition-based rules for the task's current status and payload. Returns the transitioned task and true if a condition was met, or (nil, false) otherwise. Use AdvanceFull when the action payload is also needed.

func (*StateMachine) AdvanceFull

func (sm *StateMachine) AdvanceFull(task *Task) *AdvanceOutcome

AdvanceFull evaluates condition-based rules for the task's current status and payload. Returns an AdvanceOutcome (including optional action payload) if a condition was met, or nil otherwise.

func (*StateMachine) Apply

func (sm *StateMachine) Apply(task *Task, action *Action) (*Task, error)

Apply finds an action-based rule matching the action type and current status. Condition-based rules are ignored by Apply. When a matching rule has an empty ToStatus the task status is left unchanged (non-transitioning action, e.g. "progress").

func (*StateMachine) AvailableActions

func (sm *StateMachine) AvailableActions(status TaskStatus) []string

AvailableActions returns the list of manual actions that can be applied to a task in the given status. Condition-based (automatic) rules and non-manual rules are excluded. Terminal statuses (done, aborted) return an empty list.

type Task

type Task struct {
	ID           string          `json:"id"`
	ProjectID    string          `json:"project_id"`
	RemoteID     string          `json:"remote_id,omitempty"`
	Title        string          `json:"title"`
	Description  string          `json:"description,omitempty"`
	Status       TaskStatus      `json:"status"`
	Behavior     string          `json:"behavior"`
	Traits       []string        `json:"traits,omitempty"`
	Readonly     bool            `json:"readonly,omitempty"`
	Worktree     bool            `json:"worktree,omitempty"`
	BranchPrefix string          `json:"branch_prefix,omitempty"`
	BaseBranch   string          `json:"base_branch,omitempty"`
	Payload      json.RawMessage `json:"payload"`
	Instructions Instructions    `json:"instructions,omitempty"`
	AutoStart    bool            `json:"auto_start,omitempty"`
	Ref          string          `json:"ref,omitempty"`
	ParentID     string          `json:"parent_id,omitempty"`
	CreatedAt    time.Time       `json:"created_at"`
	UpdatedAt    time.Time       `json:"updated_at"`
	// 以下はDBに保存しない派生フィールド(list/get クエリ時にサブクエリで集計)
	TotalChildCount   int `json:"total_child_count,omitempty"`
	DoneChildCount    int `json:"done_child_count,omitempty"`
	AbortedChildCount int `json:"aborted_child_count,omitempty"`
	OpenChildCount    int `json:"open_child_count,omitempty"`
	// Blocked は表示用フィールド(DBには保存しない)
	Blocked bool `json:"blocked,omitempty"`
}

func FindTaskByRef

func FindTaskByRef(dbtx db.DBTX, ref, parentID string) (*Task, error)

FindTaskByRef returns the task matching the given ref within the given parent scope, or nil if no matching task is found. If ref is a UUID, the task is looked up by id directly (backward compatibility).

func FindTaskByRemote

func FindTaskByRemote(dbtx db.DBTX, remoteID string) (*Task, error)

FindTaskByRemote returns the most recently created task (by created_at DESC, id DESC) matching the given remote_id, or nil if no matching task is found.

func GetTask

func GetTask(dbtx db.DBTX, id string) (*Task, error)

func ListChildren added in v0.0.6

func ListChildren(dbtx db.DBTX, parentID string) ([]*Task, error)

ListChildren returns all direct children of the given parent task, ordered by created_at ASC (oldest first). Returns an empty slice if the task has no children — never nil. parentID must be non-empty; passing "" returns an empty result (root tasks have no parent record, so they can't be queried as children either).

func ListTasks

func ListTasks(dbtx db.DBTX, filter TaskFilter) ([]*Task, error)

type TaskBehavior

type TaskBehavior struct {
	// Readonly controls whether the sandbox working directory is mounted read-only
	// for tasks using this behavior. When nil (unset), the daemon applies a
	// fail-safe default: readonly=true for all behaviors except the canonical
	// "executor" (which retains readonly=false during the compat period).
	// Set explicitly to override: readonly: false in project.yaml.
	Readonly           *bool        `yaml:"readonly,omitempty" json:"readonly,omitempty"`
	Traits             []string     `yaml:"traits" json:"traits"`
	DefaultInstruction *Instruction `yaml:"default_instruction,omitempty" json:"default_instruction,omitempty"`

	// Hooks is parsed from project.yaml task_behaviors.<name>.hooks at load time.
	// ScriptPath on each Hook is resolved separately by ReadProjectMeta (not stored
	// in YAML). Env, HostCommands, AdditionalBindings, and KitRoots are
	// runtime-overlay fields populated by ReadProjectMetaWithKits after merging
	// kit data and project-level overlays. These are not serialized to YAML.
	Hooks              []Hook            `yaml:"hooks,omitempty" json:"-"`
	Env                map[string]string `yaml:"-" json:"-"`
	HostCommands       HostCommands      `yaml:"-" json:"-"`
	AdditionalBindings []BindMount       `yaml:"-" json:"-"`
	// KitRoots holds the deduplicated list of kit root directories to bind-mount
	// in the sandbox at their original host paths. Populated by MergeKitMetaIntoBehavior.
	KitRoots []string `yaml:"-" json:"-"`
}

func LookupBehaviorWithAlias

func LookupBehaviorWithAlias(meta *ProjectMeta, name string) (TaskBehavior, string, bool)

LookupBehaviorWithAlias finds a TaskBehavior in meta.TaskBehaviors by name, being tolerant of the plan / dev → supervisor / executor rename. Lookup is tried in this order:

  1. exact match against the requested name
  2. if the request is a legacy alias, try the canonical name
  3. if the request is a canonical name, try the legacy alias (handles unnormalized in-memory ProjectMeta values that may exist in tests or transitional code paths)

When (2) or (3) hits, a deprecation warning is logged. The returned key is the map key that actually matched; callers may use it for further logging or store the canonical form on the task.

type TaskFilter

type TaskFilter struct {
	Status      string
	ProjectID   string
	Behavior    string
	WorkspaceID string
	Title       string
	ParentID    *string
}

type TaskGCStore

type TaskGCStore struct {

	// RuntimeReaper, when set, is called with each runtime directory path
	// before os.RemoveAll removes it. Use this to Reap docker resources that
	// may still be alive in the upstream daemon (safety net for jobs whose
	// cleanupSandboxAfterWait did not complete, e.g. daemon restart).
	RuntimeReaper func(runtimeDir string) error
	// contains filtered or unexported fields
}

TaskGCStore handles GC of tasks and their related data.

func NewTaskGCStore

func NewTaskGCStore(conn *sql.DB) *TaskGCStore

func NewTaskGCStoreWithWorktree

func NewTaskGCStoreWithWorktree(conn *sql.DB, resolveProjectDir func(projectID string) (string, error), gitBin string, runtimesDir string) *TaskGCStore

NewTaskGCStoreWithWorktree creates a TaskGCStore that also cleans up worktree directories on disk before deleting DB records. resolveProjectDir returns the project's work directory given its ID. gitBin is the path to the git binary; empty string defaults to "git". runtimesDir is the path to the runtimes root directory; empty string disables runtime cleanup.

func (*TaskGCStore) GC

func (s *TaskGCStore) GC(olderThan time.Duration, dryRun bool) (*GCResult, error)

func (*TaskGCStore) WithAttachmentsRoot added in v0.0.7

func (s *TaskGCStore) WithAttachmentsRoot(dir string) *TaskGCStore

WithAttachmentsRoot enables disk-level cleanup of the per-task attachments directory tree rooted at `<dir>/tasks/<id>/attachments`. dir is the data-home (matches dataHomeFor in wire.go). Empty disables the cleanup.

func (*TaskGCStore) WithRuntimeReaper added in v0.0.2

func (s *TaskGCStore) WithRuntimeReaper(fn func(runtimeDir string) error) *TaskGCStore

WithRuntimeReaper sets a callback that is invoked with each runtime directory path before it is deleted. This allows the caller to Reap docker resources created by sandbox jobs (safety net when cleanupSandboxAfterWait didn't run, e.g. after a daemon restart).

func (*TaskGCStore) WithSandboxTmpDir

func (s *TaskGCStore) WithSandboxTmpDir(dir string) *TaskGCStore

WithSandboxTmpDir enables safety-net cleanup of leaked /tmp/boid-* sandbox artifacts during GC. Pass the directory to scan (typically "/tmp"); empty string disables this cleanup.

type TaskLookup

type TaskLookup interface {
	GetTask(id string) (*Task, error)
}

type TaskRepository

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

func NewTaskRepository

func NewTaskRepository(db db.DBTX) *TaskRepository

func (*TaskRepository) CreateAction

func (r *TaskRepository) CreateAction(action *Action) error

func (*TaskRepository) CreateTask

func (r *TaskRepository) CreateTask(task *Task) error

func (*TaskRepository) DeleteTask

func (r *TaskRepository) DeleteTask(id string) error

func (*TaskRepository) FindTaskByRef

func (r *TaskRepository) FindTaskByRef(ref, parentID string) (*Task, error)

func (*TaskRepository) FindTaskByRemote

func (r *TaskRepository) FindTaskByRemote(remoteID string) (*Task, error)

func (*TaskRepository) GetTask

func (r *TaskRepository) GetTask(id string) (*Task, error)

func (*TaskRepository) ListActionsByTask

func (r *TaskRepository) ListActionsByTask(taskID string) ([]*Action, error)

func (*TaskRepository) ListChildren added in v0.0.6

func (r *TaskRepository) ListChildren(parentID string) ([]*Task, error)

func (*TaskRepository) ListTasks

func (r *TaskRepository) ListTasks(filter TaskFilter) ([]*Task, error)

func (*TaskRepository) UpdateTask

func (r *TaskRepository) UpdateTask(task *Task) error

type TaskSnapshot

type TaskSnapshot struct {
	ID          string
	Title       string
	Status      string
	Behavior    string
	Description string
}

TaskSnapshot is the business metadata that materializes at $HOME/.boid/context/task.yaml. Fields mirror the subset historically produced by planner's buildTaskYAML helper.

type TaskStatus

type TaskStatus string
const (
	TaskStatusPending   TaskStatus = "pending"
	TaskStatusExecuting TaskStatus = "executing"
	TaskStatusAwaiting  TaskStatus = "awaiting"
	TaskStatusDone      TaskStatus = "done"
	TaskStatusAborted   TaskStatus = "aborted"
)

type TraitType

type TraitType string
const (
	TraitArtifact     TraitType = "artifact"
	TraitVerification TraitType = "verification"
	TraitAwaiting     TraitType = "awaiting"
)

func ActiveTraitTypes

func ActiveTraitTypes(raw json.RawMessage) ([]TraitType, error)

func (TraitType) Base

func (t TraitType) Base() TraitType

Base returns the trait name without the optional "?" suffix.

func (TraitType) IsOptional

func (t TraitType) IsOptional() bool

IsOptional reports whether the trait is declared with a trailing "?".

type TransitionCondition

type TransitionCondition func(payload json.RawMessage) bool

TransitionCondition evaluates whether a condition-based transition should fire.

type Visibility

type Visibility struct {
	// ProjectDir is the host path to the project working directory.
	// Empty means the project filesystem is not visible.
	ProjectDir string

	// UseWorktree asks dispatcher to replace ProjectDir with a per-task git
	// worktree obtained from its WorktreeManager.
	UseWorktree bool

	// AdditionalBindings lists extra host bind-mounts (e.g. kit-provided CLIs
	// like the claude binary) that must be visible inside the sandbox.
	AdditionalBindings []BindMount

	// Writable permits writes to ProjectDir / the resolved worktree. When
	// ProjectDir is empty, this field has no effect.
	Writable bool

	// KitRoots lists the kit root directories to bind-mount at their original
	// host paths inside the sandbox. This lets scripts source sibling helpers
	// via relative paths (e.g. ${SCRIPT_DIR}/../scripts/lib.sh).
	KitRoots []string

	// ForkPoint is ProjectMeta.ForkPoint passed through to the dispatcher.
	// Used as the start point when a worktree's base_branch does not exist
	// (ClassifyBaseBranch case 3). Empty means dispatcher falls back to
	// refs/remotes/origin/HEAD.
	ForkPoint string

	// DockerEnabled, when true, indicates capabilities.docker was declared in
	// project.yaml. Dispatcher uses this to start a per-sandbox docker proxy.
	DockerEnabled bool
}

Visibility captures which host paths the sandbox sees and whether they are writable. orchestrator sets this once per JobSpec; dispatcher turns it into mount entries with no further role-aware logic.

type WorkspaceMeta added in v0.0.8

type WorkspaceMeta struct {
	// Kits is the ordered list of kit slugs active in this workspace.
	// Kit slugs are resolved by KitRegistry at load time. Kits supply
	// host_commands / env / additional_bindings and are merged into every
	// behavior (kits do not provide hooks under the current schema).
	Kits []string `yaml:"kits,omitempty" json:"kits,omitempty"`

	// Env holds environment variable overrides applied to every sandbox
	// launched under this workspace. Values here take precedence over
	// kit-supplied env but are overridden by project.yaml env.
	Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`

	// Capabilities declares optional sandbox capability flags for this
	// workspace. Uses the same Capabilities type as ProjectMeta.
	Capabilities Capabilities `yaml:"capabilities,omitempty" json:"capabilities,omitempty"`

	// AllowedDomains is the workspace-scoped HTTP(S) proxy egress allowlist.
	// Domains listed here are ADDED to the daemon-wide allowlist
	// (config.yaml sandbox.allowed_domains); the workspace cannot remove
	// entries from the global floor — that floor exists to keep
	// pypi/github/etc reachable for tool installation.
	//
	// Same matching rules as the global list (see sandbox.Proxy):
	//   - "registry-1.docker.io"  exact match
	//   - ".cosmos.azure.com"     suffix match (matches "<sub>.cosmos.azure.com")
	AllowedDomains []string `yaml:"allowed_domains,omitempty" json:"allowed_domains,omitempty"`
}

WorkspaceMeta holds the machine-local workspace configuration that is stored in ~/.config/boid/workspaces/<slug>.yaml.

A workspace defines which kits are active, environment variable overrides, and optional capability flags for sandboxes. Fields that are project-specific (secret_namespace, host_commands, additional_bindings, name, description, version) are intentionally absent; they remain in project.yaml.

type WorkspaceStore added in v0.0.8

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

WorkspaceStore provides persistence for WorkspaceMeta values. Each workspace is stored as a YAML file at <dir>/<slug>.yaml.

func NewWorkspaceStore added in v0.0.8

func NewWorkspaceStore(dir string) *WorkspaceStore

NewWorkspaceStore returns a WorkspaceStore backed by dir. If dir is empty, DefaultWorkspaceDir is called to determine the directory. The directory is not created eagerly; it is created on the first Save call.

func (*WorkspaceStore) EnsureDefault added in v0.0.8

func (s *WorkspaceStore) EnsureDefault() error

EnsureDefault writes an empty WorkspaceMeta to the DefaultWorkspaceSlug path if no file exists yet. It is safe to call repeatedly and a no-op once the file exists (the existing content — including user edits — is left untouched). Returns nil when the workspace dir cannot be determined yet (DefaultWorkspaceDir failure): callers are expected to surface that via the first Load/Save attempt rather than at boot.

func (*WorkspaceStore) List added in v0.0.8

func (s *WorkspaceStore) List() ([]string, error)

List returns the slug of every workspace stored in the directory, sorted alphabetically. If the directory does not exist, an empty slice and nil error are returned (degraded window).

func (*WorkspaceStore) Load added in v0.0.8

func (s *WorkspaceStore) Load(slug string) (*WorkspaceMeta, error)

Load reads and parses the WorkspaceMeta for the given slug. Returns an error wrapping os.ErrNotExist when the file does not exist.

func (*WorkspaceStore) Remove added in v0.0.8

func (s *WorkspaceStore) Remove(slug string) error

Remove deletes the YAML file for the given slug. Returns an error wrapping os.ErrNotExist when the file does not exist. The reserved DefaultWorkspaceSlug cannot be removed. It does not check whether any project references this workspace.

func (*WorkspaceStore) Save added in v0.0.8

func (s *WorkspaceStore) Save(slug string, meta *WorkspaceMeta) error

Save atomically writes meta as YAML to <dir>/<slug>.yaml. The directory is created with mode 0755 if it does not exist. The write is atomic: the data is first written to a temporary file in the same directory and then renamed into place.

type WorkspaceSummary

type WorkspaceSummary struct {
	ID           string `json:"id"`
	ProjectCount int    `json:"project_count"`
}

func ListWorkspaces

func ListWorkspaces(dbtx db.DBTX) ([]*WorkspaceSummary, error)

ListWorkspaces returns all configured workspaces with project counts.

type WorktreeLocker

type WorktreeLocker interface {
	Acquire(ctx context.Context, key string) (release func(), err error)
}

WorktreeLocker manages exclusive write access to a shared worktree.

Directories

Path Synopsis
Package refname generates random "adjective_noun" style names for task refs.
Package refname generates random "adjective_noun" style names for task refs.

Jump to

Keyboard shortcuts

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