Documentation
¶
Index ¶
- Constants
- Variables
- func CanonicalBehaviorName(name string) (canonical string, isAlias bool)
- func ClearPendingAnswer(payload json.RawMessage) json.RawMessage
- func ComputeForkPoint(parent *Task) string
- func ComputeHeadBranch(task *Task) string
- func CreateAction(dbtx db.DBTX, a *Action) error
- func CreateProject(dbtx db.DBTX, project *Project) error
- func CreateTask(dbtx db.DBTX, t *Task) error
- func DefaultBuiltinPolicies(role Role, names []string, pctx PolicyContext) map[string]BuiltinPolicy
- func DeleteProject(dbtx db.DBTX, id string) error
- func DeleteTask(dbtx db.DBTX, id string) error
- func ExpandBaseBranch(value, workDir string) (string, error)
- func ExpandTaskBaseBranch(template, remoteID string) (string, error)
- func FilterPayloadByTraits(payload json.RawMessage, consumes []TraitType) json.RawMessage
- func IsBehaviorAliasKey(key string) bool
- func IsInstructionsEditable(status TaskStatus) bool
- func IsProjectScopable(km *KitMeta) error
- func IsReadonly(task *Task) bool
- func MarshalProjectLocalMeta(meta *ProjectLocalMeta) ([]byte, error)
- func MergeKitMetaIntoBehavior(behavior *TaskBehavior, kits []*KitMeta, kitAgents []string) error
- func MergePayload(base, update json.RawMessage) (json.RawMessage, error)
- func MergePayloadPatch(base, patch json.RawMessage, handlerID string, allowedTraits []TraitType) (json.RawMessage, error)
- func ProjectLocalPath(dir string) string
- func RejectPayloadInstructions(payload json.RawMessage) error
- func RejectReservedPayloadKeys(payload json.RawMessage) error
- func RepoRefToCloneURL(repoRef string, useSSH bool) string
- func RepoRefsFromKitRefs(kitRefs []string) []string
- func ResolveHookScript(hooksDir, hookID string) (string, error)
- func ResolveKitAgent(ref KitRef) string
- func SetProjectWorkspace(dbtx db.DBTX, projectID, workspaceID string) error
- func StripAwaitingTrait(payload json.RawMessage) json.RawMessage
- func TraitBool(payload json.RawMessage, trait string) bool
- func TraitExists(payload json.RawMessage, trait string) bool
- func TraitGetString(payload json.RawMessage, trait string) (string, bool)
- func UpdateTask(dbtx db.DBTX, t *Task) error
- func ValidatePayloadPatch(patch json.RawMessage, allowedTraits []TraitType) error
- func WriteProjectLocalMeta(dir string, meta *ProjectLocalMeta) error
- type AbortReason
- type Action
- type AdvanceOutcome
- type AwaitingPayload
- type BaseBranchState
- type BehaviorResolution
- type BehaviorResolveRequest
- type BehaviorSpec
- type BindMount
- type BranchLockManager
- type BuiltinPolicy
- type Capabilities
- type CleanupFunc
- type CommandDef
- type Coordinator
- type DBProjectCatalog
- type DBTaskLookup
- type DispatchPlanner
- type DispatchResult
- type DockerCapability
- type DoneReport
- type Evaluator
- type FailReport
- type FiredEvent
- type GCLoop
- type GCResult
- type GCStore
- type HandlerKind
- type HandlerResult
- type HandlerTraits
- type Hook
- type HookExecutor
- type HookFile
- type HookFireEvent
- type HostCommandSpec
- type HostCommands
- type InMemoryWorktreeLockManager
- type Instruction
- type Instructions
- type JobCompletion
- type JobKind
- type JobSpec
- type JobWaiter
- type KitDetect
- type KitMeta
- type KitMetaInfo
- type KitRef
- type KitRegistry
- func (r *KitRegistry) Install(repoRef string, useSSH bool) error
- func (r *KitRegistry) InstallFromURL(repoRef, url string) error
- func (r *KitRegistry) IsInstalled(repoRef string) bool
- func (r *KitRegistry) List() ([]string, error)
- func (r *KitRegistry) Remove(repoRef string) error
- func (r *KitRegistry) Resolve(ref string) (string, error)
- func (r *KitRegistry) Update(repoRef string) error
- type KitRequires
- type KitResolver
- type KitRuntime
- type Lifecycle
- type LifecycleStore
- type MergeMode
- type MetaCache
- type PlannerWireConfig
- type PolicyContext
- type Project
- type ProjectCatalog
- type ProjectLocalMeta
- type ProjectMeta
- type ProjectRepository
- func (r *ProjectRepository) CreateProject(project *Project) error
- func (r *ProjectRepository) DeleteProject(id string) error
- func (r *ProjectRepository) GetProject(id string) (*Project, error)
- func (r *ProjectRepository) ListProjects() ([]*Project, error)
- func (r *ProjectRepository) ListWorkspaces() ([]*WorkspaceSummary, error)
- func (r *ProjectRepository) SetProjectWorkspace(projectID, workspaceID string) error
- type ProjectStore
- type RawPayload
- type ReplayResult
- type Role
- type RoutedInstruction
- type Rule
- type StateMachine
- type Task
- func FindTaskByRef(dbtx db.DBTX, ref, parentID string) (*Task, error)
- func FindTaskByRemote(dbtx db.DBTX, remoteID string) (*Task, error)
- func GetTask(dbtx db.DBTX, id string) (*Task, error)
- func ListChildren(dbtx db.DBTX, parentID string) ([]*Task, error)
- func ListTasks(dbtx db.DBTX, filter TaskFilter) ([]*Task, error)
- type TaskBehavior
- type TaskFilter
- type TaskGCStore
- type TaskLookup
- type TaskRepository
- func (r *TaskRepository) CreateAction(action *Action) error
- func (r *TaskRepository) CreateTask(task *Task) error
- func (r *TaskRepository) DeleteTask(id string) error
- func (r *TaskRepository) FindDependentTasks(_ string) ([]*Task, error)
- func (r *TaskRepository) FindTaskByRef(ref, parentID string) (*Task, error)
- func (r *TaskRepository) FindTaskByRemote(remoteID string) (*Task, error)
- func (r *TaskRepository) GetTask(id string) (*Task, error)
- func (r *TaskRepository) ListActionsByTask(taskID string) ([]*Action, error)
- func (r *TaskRepository) ListChildren(parentID string) ([]*Task, error)
- func (r *TaskRepository) ListTasks(filter TaskFilter) ([]*Task, error)
- func (r *TaskRepository) UpdateTask(task *Task) error
- type TaskSnapshot
- type TaskStatus
- type TraitType
- type TransitionCondition
- type Visibility
- type WorkspaceSummary
- type WorktreeLocker
Constants ¶
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.
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.
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 ¶
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.
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.
Functions ¶
func CanonicalBehaviorName ¶
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 ¶
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 ¶
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 CreateProject ¶
CreateProject inserts a new project record.
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 DeleteProject ¶
DeleteProject removes a project by ID. All tasks (and their dependent records) belonging to the project are deleted first.
func ExpandBaseBranch ¶
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 ¶
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 IsBehaviorAliasKey ¶
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 ¶
IsProjectScopable reports whether a kit may be placed in the top-level project.yaml kits field. A kit is project-scopable when all its hooks have kind == "agent" (opt-in via instructions, so they cannot fire unexpectedly across behaviors).
func IsReadonly ¶
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 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 RepoRefToCloneURL ¶
RepoRefToCloneURL converts a repo reference to a git clone URL. When useSSH is true, it returns an SSH URL (git@host:owner/repo.git). Otherwise it returns an HTTPS URL (https://host/owner/repo.git).
func RepoRefsFromKitRefs ¶
RepoRefsFromKitRefs extracts unique repo references (host/owner/repo) from a list of kit refs. Only remote refs with 4+ path segments are included. Local refs (< 4 segments) and "local/" prefix refs are skipped.
func ResolveHookScript ¶
func ResolveKitAgent ¶
ResolveKitAgent derives the agent name for a kit reference. If an alias is set via 'as:', that alias is returned. Otherwise the last path segment of the ref is used.
func SetProjectWorkspace ¶
SetProjectWorkspace updates a project's local workspace membership.
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 ValidatePayloadPatch ¶
func ValidatePayloadPatch(patch json.RawMessage, allowedTraits []TraitType) error
func WriteProjectLocalMeta ¶
func WriteProjectLocalMeta(dir string, meta *ProjectLocalMeta) error
Types ¶
type AbortReason ¶
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"`
}
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:
- meta.DefaultTaskBehavior if set
- implicit "supervisor" fallback if that behavior exists in meta (with WARN)
- error if neither is available
- 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.
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 ¶
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"`
AllowStdin bool `json:"allow_stdin,omitempty"`
Env map[string]string `json:"env,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 ¶
func (DBProjectCatalog) GetProject ¶
func (c DBProjectCatalog) GetProject(id string) (*Project, error)
func (DBProjectCatalog) ListProjects ¶
func (c DBProjectCatalog) ListProjects() ([]*Project, error)
type DBTaskLookup ¶
type DispatchPlanner ¶
type DispatchPlanner struct {
Meta MetaCache
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 ¶
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.
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 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 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".
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 HostCommandSpec ¶
type HostCommandSpec struct {
Allow []string `yaml:"allow,omitempty" json:"allow,omitempty"`
Deny []string `yaml:"deny,omitempty" json:"deny,omitempty"`
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"`
}
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.
type Instruction ¶
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
}
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 KitDetect ¶
type KitDetect struct {
// Script is a path (relative to the kit directory) to a POSIX sh
// script. boid init runs it with sh(1) using projectDir as CWD and a
// 5-second timeout. The first trimmed line of stdout is interpreted:
// "required" → kit is auto-selected
// "optional" → kit is shown as a candidate but not auto-selected
// other / empty / non-zero exit → not applicable
Script string `yaml:"script"`
}
KitDetect declares how to determine whether a kit is applicable to a project. The referenced script is executed with POSIX sh in the project directory; its first line of stdout — "required", "optional", or empty — indicates the detection outcome.
type KitMeta ¶
type KitMeta struct {
TaskBehaviors map[string]TaskBehavior `yaml:"task_behaviors"`
Hooks []Hook `yaml:"hooks"`
HostCommands HostCommands `yaml:"host_commands"`
AdditionalBindings []BindMount `yaml:"additional_bindings"`
Env map[string]string `yaml:"env"`
HooksDir string `yaml:"-"`
KitRoot string `yaml:"-"` // directory containing kit.yaml
// Init-time metadata — not merged into runtime spec by MergeKitMeta.
Meta *KitMetaInfo `yaml:"meta,omitempty"`
Detect *KitDetect `yaml:"detect,omitempty"`
Requires *KitRequires `yaml:"requires,omitempty"`
ProvidesAgent string `yaml:"provides_agent,omitempty"`
Deprecated bool `yaml:"deprecated,omitempty"`
}
KitMeta holds the parsed content of a kit.yaml file.
func ReadKitMeta ¶
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 KitRegistry ¶
type KitRegistry struct {
BaseDir string // e.g. ~/.local/share/boid/kits
}
KitRegistry manages installed kit repositories under a base directory.
func NewRegistry ¶
func NewRegistry(baseDir string) *KitRegistry
NewRegistry creates a new kit registry with the given base directory.
func (*KitRegistry) Install ¶
func (r *KitRegistry) Install(repoRef string, useSSH bool) error
Install clones a kit repository from its conventional URL. The repoRef should be like "github.com/user/repo". When useSSH is true, the SSH protocol (git@host:path.git) is used.
func (*KitRegistry) InstallFromURL ¶
func (r *KitRegistry) InstallFromURL(repoRef, url string) error
InstallFromURL clones a git repo from the given URL into BaseDir/repoRef.
func (*KitRegistry) IsInstalled ¶
func (r *KitRegistry) IsInstalled(repoRef string) bool
IsInstalled returns true if the repo directory already exists under BaseDir.
func (*KitRegistry) List ¶
func (r *KitRegistry) List() ([]string, error)
List returns all installed kit repository references. It finds directories containing .git under BaseDir.
func (*KitRegistry) Remove ¶
func (r *KitRegistry) Remove(repoRef string) error
Remove deletes an installed kit repository.
func (*KitRegistry) Resolve ¶
func (r *KitRegistry) Resolve(ref string) (string, error)
Resolve returns the absolute filesystem path for a kit reference. A ref like "github.com/user/repo/go" is split into the repo path (first 3 segments) and the kit subpath (remainder).
func (*KitRegistry) Update ¶
func (r *KitRegistry) Update(repoRef string) error
Update runs git pull in an installed kit repository.
type KitRequires ¶
type KitRequires struct {
Commands []string `yaml:"commands"`
}
KitRequires declares host commands that must be present in PATH.
type KitResolver ¶
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 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 ¶
LifecycleStore is the minimal interface required to derive lifecycle traits from persistent action history.
type MergeMode ¶
type MergeMode string
const ( MergeModeExclusive MergeMode = "exclusive" )
func TraitMergeMode ¶
type MetaCache ¶
type MetaCache interface {
Get(id string) (*ProjectMeta, bool)
}
type PlannerWireConfig ¶
type PlannerWireConfig struct {
Meta MetaCache
Projects ProjectCatalog
Tasks TaskLookup
Adapter adapters.HarnessAdapter
}
type PolicyContext ¶
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 ¶
GetProject retrieves a project by ID.
type ProjectCatalog ¶
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"`
Kits []KitRef `yaml:"kits,omitempty" json:"kits,omitempty"`
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 string `yaml:"secret_namespace,omitempty" json:"secret_namespace,omitempty"`
// Capabilities declares optional sandbox capabilities for jobs in this project.
Capabilities Capabilities `yaml:"capabilities,omitempty" 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 project.local.yaml, resolves kits referenced by each task behavior, and merges kit data into each behavior. Returns a ProjectMeta whose TaskBehaviors have their resolved Hooks/etc. populated and ready for dispatch.
type ProjectRepository ¶
type ProjectRepository struct {
// contains filtered or unexported fields
}
func NewProjectRepository ¶
func NewProjectRepository(db db.DBTX) *ProjectRepository
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) 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.
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.
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 ReplayResult ¶
type ReplayResult struct {
Result HandlerResult
FinalPayload json.RawMessage
NewStatus TaskStatus
FiredEvents []FiredEvent
}
ReplayResult is the result of a single-hook replay operation.
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 StateMachine ¶
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 ¶
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 ¶
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 ListChildren ¶ added in v0.0.6
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).
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"`
Kits []KitRef `yaml:"kits,omitempty" json:"kits,omitempty"`
// Resolved fields populated by ReadProjectMetaWithKits after merging kit data
// and project-level overlays. These are not serialized to YAML.
Hooks []Hook `yaml:"-" 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:
- exact match against the requested name
- if the request is a legacy alias, try the canonical name
- 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 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) 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 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) FindDependentTasks ¶
func (r *TaskRepository) FindDependentTasks(_ string) ([]*Task, 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) 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
func ActiveTraitTypes ¶
func ActiveTraitTypes(raw json.RawMessage) ([]TraitType, error)
func (TraitType) IsOptional ¶
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 WorkspaceSummary ¶
func ListWorkspaces ¶
func ListWorkspaces(dbtx db.DBTX) ([]*WorkspaceSummary, error)
ListWorkspaces returns all configured workspaces with project counts.
Source Files
¶
- awaiting_payload.go
- base_branch.go
- base_branch_classify.go
- behavior_resolve.go
- branch_var.go
- conditions.go
- coordinator.go
- db_adapters.go
- evaluator.go
- gc_loop.go
- gc_sandbox_tmp.go
- head_branch.go
- home.go
- jobspec.go
- kit_registry.go
- lifecycle.go
- machine.go
- model.go
- payload_merge.go
- planner.go
- policy.go
- policy_ops.go
- project_catalog.go
- project_local.go
- project_lock.go
- project_store.go
- readonly.go
- repository.go
- spec_loader.go
- spec_payload.go
- spec_resolve.go
- spec_types.go
- store.go
- types.go
- wire.go
- worktree_lock.go