Documentation
¶
Index ¶
- Constants
- Variables
- func AssignDefaultWorkspaceToUnlinked(dbtx db.DBTX, workspaceID string) (int, error)
- 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 DefaultWorkspaceDir() (string, error)
- 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 FormatMigrationIssue(p ProjectMigrationIssue) string
- 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 ResolveAllowedDomains(globalFloor []string, workspace *WorkspaceMeta) []string
- func ResolveHookScript(hooksDir, hookID string) (string, error)
- func ResolveKitAgent(ref KitRef) string
- func SetPendingAnswer(payload json.RawMessage, answer string) json.RawMessage
- 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 ValidKitName(s string) error
- func ValidWorkspaceSlug(s string) 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 KitMeta
- type KitMetaInfo
- type KitRef
- type KitRegistry
- type KitResolver
- type KitRuntime
- type LegacyProjectMeta
- type LegacyTaskBehavior
- type Lifecycle
- type LifecycleStore
- type MergeMode
- type MetaCache
- type MetaHydrator
- type PlannerWireConfig
- type PolicyContext
- type Project
- type ProjectCatalog
- type ProjectLocalMeta
- type ProjectMeta
- type ProjectMigrationError
- type ProjectMigrationIssue
- type ProjectMissingError
- type ProjectRepository
- func (r *ProjectRepository) AssignDefaultWorkspaceToUnlinked(workspaceID string) (int, error)
- 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
- func (s *ProjectStore) Get(id string) (*ProjectMeta, bool)
- func (s *ProjectStore) GetWithWorkspace(_ context.Context, projectID string) (*ProjectMeta, error)
- func (s *ProjectStore) Load(workDir string) (*ProjectMeta, error)
- func (s *ProjectStore) LoadAll(projects []*Project) []error
- func (s *ProjectStore) Remove(id string)
- func (s *ProjectStore) Set(id string, meta *ProjectMeta)
- func (s *ProjectStore) SetWorkspaceID(projectID, workspaceID string)
- func (s *ProjectStore) SetWorkspaceStore(ws *WorkspaceStore)
- func (s *ProjectStore) WorkspaceStore() *WorkspaceStore
- type RawPayload
- type ReplayResult
- type Role
- type RoutedInstruction
- type Rule
- type SecretFinding
- 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
- func (s *TaskGCStore) GC(olderThan time.Duration, dryRun bool) (*GCResult, error)
- func (s *TaskGCStore) WithAttachmentsRoot(dir string) *TaskGCStore
- func (s *TaskGCStore) WithRuntimeReaper(fn func(runtimeDir string) error) *TaskGCStore
- func (s *TaskGCStore) WithSandboxTmpDir(dir string) *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 WorkspaceMeta
- type WorkspaceStore
- 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 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.
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 AssignDefaultWorkspaceToUnlinked ¶ added in v0.0.8
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 ¶
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 DefaultWorkspaceDir ¶ added in v0.0.8
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 ¶
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 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 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 ¶
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. Kits no longer provide hooks or task_behaviors, so all kits are project-scopable by definition.
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 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 ResolveKitAgent ¶
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 ¶
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 ValidKitName ¶ added in v0.0.8
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
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 ¶
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.
func (*BindMount) UnmarshalYAML ¶ added in v0.0.8
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 ¶
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
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 ¶
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
// 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 ¶
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 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 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 ¶
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 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 ¶
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"`
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 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 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 ¶
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"`
// 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:
- 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) 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 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 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 ¶
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_name.go
- kit_registry.go
- lifecycle.go
- machine.go
- migration_error.go
- missing_error.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
- secret_scan.go
- spec_loader.go
- spec_loader_legacy.go
- spec_payload.go
- spec_resolve.go
- spec_types.go
- store.go
- types.go
- wire.go
- workspace_meta.go
- workspace_slug.go
- workspace_store.go
- worktree_lock.go