api

package
v0.0.8 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 38 Imported by: 0

Documentation

Index

Constants

View Source
const (
	AttachmentMaxFileBytes  int64 = 10 * 1024 * 1024 // 10 MB per file
	AttachmentMaxTotalBytes int64 = 30 * 1024 * 1024 // 30 MB per task dir
	AttachmentMaxNameBytes        = 255
)

Attachment-related constants. Values are also enforced client-side in web/static/boid-paste-attach.js; keep the two in sync.

Variables

View Source
var AttachmentAllowedExts = map[string]bool{
	".png":  true,
	".jpg":  true,
	".jpeg": true,
	".webp": true,
	".txt":  true,
	".md":   true,
	".json": true,
	".log":  true,
}

AttachmentAllowedExts is the file-extension allowlist for uploads. Both images (for screenshot paste) and a few text formats (for logs / json snippets) are accepted.

View Source
var ErrAskPending = errors.New("task_ask: another question is pending")

ErrAskPending is returned by BlockingAskRegistry.Register when the task already has an outstanding blocking ask. It implements decision B1: a second concurrent `boid task ask` for the same task fails immediately rather than queueing behind the first.

Functions

func AttachmentsRootForTask added in v0.0.7

func AttachmentsRootForTask(dataHome, taskID string) string

AttachmentsRootForTask resolves the on-disk attachments directory for the given task. dataHome is the data root (e.g. filepath.Dir(cfg.DBPath)), the same convention as runtimesDirFor.

func BuildFlatItems

func BuildFlatItems(tasks []*orchestrator.Task, projectNames map[string]string) []components.TreeItem

BuildFlatItems returns tasks as a flat list (Depth=0, HasChildren=false, ParentID=""). Used for the "closed" status view where tree structure is irrelevant.

func BuildTreeItems

func BuildTreeItems(tasks []*orchestrator.Task, projectNames map[string]string) []components.TreeItem

BuildTreeItems takes a flat task list and returns items in DFS tree order with depth and visual-parent info. Siblings appear in input order. Tasks whose ParentID is absent from the list are treated as roots. Cycles in ParentID references are detected via a visited set and skipped.

projectNames maps project ID to display name; pass nil to skip name resolution.

func EnsureAttachmentsDir added in v0.0.7

func EnsureAttachmentsDir(dataHome, taskID string) (string, error)

EnsureAttachmentsDir creates the per-task attachments directory if it does not already exist. Used by handlers right after task creation / before accepting answer attachments so the bind mount has a non-empty source.

func ResolveTaskField

func ResolveTaskField(task *orchestrator.Task, actions orchestrator.LifecycleStore, path string) (string, error)

ResolveTaskField returns the value at the given dotted path against the task.

Path resolution:

  • Each dot-separated segment is a JSON key.
  • The first segment is matched against top-level Task fields (via JSON tags). When it doesn't match, the path is implicitly resolved inside `payload`, so traits like `awaiting.question`, `artifact.report`, and `lifecycle.abort.message` work without an explicit `payload.` prefix.
  • `lifecycle` is a computed trait derived from action history. When lifecycle is referenced, ResolveTaskField calls DeriveLifecycle (when actions is non-nil) and injects the result under `payload.lifecycle` before traversal.

Output format:

  • Strings: returned unquoted.
  • Numbers and booleans: stringified.
  • Objects and arrays: compact JSON.
  • Missing path: returns "" with no error.
  • Traversing into a scalar with remaining path segments: error.

func SanitizeAttachmentName added in v0.0.7

func SanitizeAttachmentName(raw string) (string, error)

SanitizeAttachmentName validates a user-supplied filename and returns the cleaned basename. It rejects anything that doesn't match attachmentNameRe, exceeds the byte cap, has a disallowed extension, or contains only a dot prefix (".env" etc., which the regex would otherwise allow).

func SaveMultipartAttachments added in v0.0.7

func SaveMultipartAttachments(dataHome, taskID string, files []*multipart.FileHeader) ([]string, error)

SaveMultipartAttachments persists every file under the multipart form field "attachments" into the task-scoped attachments directory. It enforces:

  • per-file size cap (AttachmentMaxFileBytes)
  • aggregate task-dir size cap (AttachmentMaxTotalBytes)
  • filename sanitization (SanitizeAttachmentName)

On success it returns the list of saved basenames. On any error it removes the files it had partially written during this call so the caller can surface the error without leaving orphans behind.

func ValidateAttachmentHeaders added in v0.0.7

func ValidateAttachmentHeaders(files []*multipart.FileHeader) error

ValidateAttachmentHeaders runs the size + name checks that SaveMultipartAttachments would apply, without touching the filesystem. Web handlers call this *before* creating the task so a bad upload (oversized, bad extension, disallowed name) doesn't leave a half-created task behind.

Types

type ActionApplication

type ActionApplication struct {
	Task         *orchestrator.Task   `json:"task"`
	Action       *orchestrator.Action `json:"action"`
	MatchedHooks []string             `json:"matched_hooks,omitempty"`
}

type ActionHandler

type ActionHandler struct {
	Service WorkflowService
}

func (*ActionHandler) Apply

func (h *ActionHandler) Apply(w http.ResponseWriter, r *http.Request)

func (*ActionHandler) Routes

func (h *ActionHandler) Routes() chi.Router

type ActionStore

type ActionStore interface {
	CreateAction(action *orchestrator.Action) error
	ListActionsByTask(taskID string) ([]*orchestrator.Action, error)
}

type AnswerTaskRequest

type AnswerTaskRequest struct {
	QuestionID string `json:"question_id"`
	Answer     string `json:"answer"`
}

type ApplyActionRequest

type ApplyActionRequest struct {
	Type    string          `json:"type"`
	Payload json.RawMessage `json:"payload,omitempty"`
}

type BlockingAskRegistry added in v0.0.6

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

BlockingAskRegistry coordinates harness-independent blocking Q&A. When an agent calls `boid task ask`, the broker holds the RPC connection open and the server-side handler blocks in Wait until the user/supervisor answers (via TaskHandler.Answer → AnswerTask → Notify) or the context is cancelled (daemon shutdown / agent disconnect).

The registry is purely in-memory: a daemon restart drops every pending ask. That is consistent — the sandbox process backing the blocked RPC dies with the daemon too, so there is nothing to resume.

func NewBlockingAskRegistry added in v0.0.6

func NewBlockingAskRegistry() *BlockingAskRegistry

NewBlockingAskRegistry returns an initialised registry.

func (*BlockingAskRegistry) Cancel added in v0.0.6

func (r *BlockingAskRegistry) Cancel(qid string)

Cancel removes the registration for qid (and the task it belongs to). It does not unblock Wait by itself — a blocked Wait observes ctx cancellation — but it frees the B1 slot so the task can ask again. Idempotent and safe to defer.

func (*BlockingAskRegistry) Has added in v0.0.6

func (r *BlockingAskRegistry) Has(qid string) bool

Has reports whether qid currently has a registration. Intended for tests and diagnostics.

func (*BlockingAskRegistry) Notify added in v0.0.6

func (r *BlockingAskRegistry) Notify(qid, answer string) bool

Notify delivers answer to the waiter registered for qid. It returns true when a registration exists and the answer was accepted, false when no waiter is registered (e.g. the agent already disconnected and Cancel ran) or an answer was already delivered. The send is non-blocking thanks to the buffered channel, so Notify never blocks even if Wait has not yet been scheduled.

func (*BlockingAskRegistry) Register added in v0.0.6

func (r *BlockingAskRegistry) Register(taskID, qid string) error

Register reserves the answer channel for (taskID, qid). It MUST be called before the task transitions to awaiting so an answer that arrives immediately afterwards is never dropped. The check-and-insert is atomic under the registry lock, so two concurrent asks for the same task can never both succeed.

B1 (relaxed): a second pending ask for the same task fails with ErrAskPending ONLY when it carries a DIFFERENT question id. Re-registering the SAME qid is a re-attach — an agent whose `boid task ask` was killed by a harness command-timeout retrying the identical question — and is allowed. The re-attach installs a fresh channel; any prior waiter is already tearing down on its cancelled context. (Even if its deferred Cancel later clobbers this channel, the durable PendingAnswer path backstops delivery, so at worst the fast path degrades to one extra ask round-trip.)

func (*BlockingAskRegistry) Wait added in v0.0.6

func (r *BlockingAskRegistry) Wait(ctx context.Context, qid string) (string, error)

Wait blocks until an answer is delivered for qid via Notify or ctx is cancelled. qid must have been registered; an unknown qid is a programming error and returns immediately. The caller is responsible for Cancel(qid) cleanup (typically via defer) on every exit path.

type BrokerHandler

type BrokerHandler struct {
	Registry BrokerRegistry
}

func (*BrokerHandler) Register

func (h *BrokerHandler) Register(w http.ResponseWriter, r *http.Request)

func (*BrokerHandler) Routes

func (h *BrokerHandler) Routes() chi.Router

type BrokerRegisterRequest

type BrokerRegisterRequest struct {
	Commands        map[string]orchestrator.HostCommandSpec `json:"commands"`
	BuiltinPolicies map[string]sandbox.BuiltinPolicy        `json:"builtin_policies,omitempty"`
	ProjectID       string                                  `json:"project_id,omitempty"`
}

type BrokerRegisterResponse

type BrokerRegisterResponse struct {
	Token  string `json:"token"`
	Socket string `json:"socket"`
	// ResolvedHostCommands echoes back the absolute-path-keyed map produced by
	// dispatcher.ResolveHostCommands. The caller (boid exec) feeds this into
	// SandboxRuntimeInfo so shim bind-mount targets line up with the broker's
	// policy keys without re-resolving on the client side.
	ResolvedHostCommands map[string]orchestrator.CommandDef `json:"resolved_host_commands,omitempty"`
}

type BrokerRegistry

type BrokerRegistry interface {
	RegisterBrokerCommands(commands map[string]orchestrator.HostCommandSpec, builtinPolicies map[string]sandbox.BuiltinPolicy, projectID string) (*BrokerRegisterResponse, error)
}

type CreateProjectRequest

type CreateProjectRequest struct {
	WorkDir string `json:"work_dir"`
}

type CreateTaskRequest

type CreateTaskRequest struct {
	ID           string                     `json:"id,omitempty"`
	ProjectID    string                     `json:"project_id"`
	Title        string                     `json:"title"`
	Description  string                     `json:"description,omitempty"`
	Behavior     string                     `json:"behavior,omitempty"`
	BehaviorSpec *orchestrator.BehaviorSpec `json:"behavior_spec,omitempty"`
	RemoteID     string                     `json:"remote_id,omitempty"`
	Payload      json.RawMessage            `json:"payload,omitempty"`
	Instructions json.RawMessage            `json:"instructions,omitempty"`
	AutoStart    bool                       `json:"auto_start,omitempty"`
	Traits       []string                   `json:"traits,omitempty"`
	Ref          string                     `json:"ref,omitempty"`
	ParentID     string                     `json:"parent_id,omitempty"`
	Readonly     *bool                      `json:"readonly,omitempty"`
}

type DeviceGCStore

type DeviceGCStore interface {
	DeleteRevokedDevices(ctx context.Context, dryRun bool) (int64, error)
}

type DispatchCoordinator

type DispatchCoordinator interface {
	DispatchAndAdvance(ctx context.Context, task *orchestrator.Task, meta *orchestrator.ProjectMeta, sm *orchestrator.StateMachine) (*orchestrator.DispatchResult, error)
	ReplayHook(ctx context.Context, task *orchestrator.Task, meta *orchestrator.ProjectMeta, sm *orchestrator.StateMachine, hookID string) (*orchestrator.ReplayResult, error)
}

type DuplicateTaskRequest

type DuplicateTaskRequest struct {
	AutoStart bool `json:"auto_start"`
}

type GCAppService

type GCAppService struct {
	Store       GCStore
	DeviceStore DeviceGCStore // optional; deletes revoked devices on GC
}

func (*GCAppService) GC

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

GC implements orchestrator.GCStore so GCAppService can be passed to GCLoop.

func (*GCAppService) Run

func (s *GCAppService) Run(olderThan time.Duration, dryRun bool) (*orchestrator.GCResult, error)

type GCHandler

type GCHandler struct {
	Service GCService
}

func (*GCHandler) Routes

func (h *GCHandler) Routes() chi.Router

func (*GCHandler) Run

func (h *GCHandler) Run(w http.ResponseWriter, r *http.Request)

type GCService

type GCService interface {
	Run(olderThan time.Duration, dryRun bool) (*orchestrator.GCResult, error)
}

type GCStore

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

type GlobalJobStore

type GlobalJobStore interface {
	ListJobsWithContext(filter JobListFilter) ([]JobWithContext, error)
}

GlobalJobStore supports cross-task job listing with context (task title, project name).

type HookService

type HookService interface {
	ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
	ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
}

HookService provides hook replay and hook listing operations.

type ImportError

type ImportError struct {
	Line     int    `json:"line"`
	RemoteID string `json:"remote_id"`
	Error    string `json:"error"`
}

type ImportResult

type ImportResult struct {
	Created int           `json:"created"`
	Skipped int           `json:"skipped"`
	Errors  []ImportError `json:"errors"`
}

type Job

type Job struct {
	ID                    string     `json:"id"`
	TaskID                string     `json:"task_id"`
	ProjectID             string     `json:"project_id"`
	HandlerID             string     `json:"handler_id"`
	DisplayName           string     `json:"display_name,omitempty"`
	Role                  string     `json:"role"`
	RuntimeID             string     `json:"runtime_id,omitempty"`
	WorkspacePath         string     `json:"workspace_path,omitempty"`
	Interactive           bool       `json:"interactive"`
	TTY                   bool       `json:"tty"`
	Status                JobStatus  `json:"status"`
	ExitCode              int        `json:"exit_code,omitempty"`
	Output                string     `json:"output,omitempty"`
	ExecutionState        string     `json:"execution_state,omitempty"`
	CreatedAt             time.Time  `json:"created_at"`
	UpdatedAt             time.Time  `json:"updated_at"`
	TranscriptSize        int64      `json:"transcript_size,omitempty"`
	TranscriptMtime       *time.Time `json:"transcript_mtime,omitempty"`
	TranscriptIdleSeconds int64      `json:"transcript_idle_seconds,omitempty"`
}

type JobCompletion

type JobCompletion struct {
	Output   string
	ExitCode int
}

type JobDoneRequest

type JobDoneRequest struct {
	ExitCode int    `json:"exit_code"`
	Output   string `json:"output,omitempty"`
}

type JobHandler

type JobHandler struct {
	Jobs       JobStore
	Global     GlobalJobStore // optional: enables cross-task listing when task_id is absent
	Service    WorkflowService
	LogReader  JobLogReader // optional: enables static GET /{id}/log
	SSEHandler http.Handler // optional: enables SSE streaming for GET /{id}/log?follow=true
}

func (*JobHandler) AgentStop

func (h *JobHandler) AgentStop(w http.ResponseWriter, r *http.Request)

AgentStop asks the daemon to deliver SIGUSR1 to the runtime pgrp. claude.Adapter.Run() catches the signal and forwards SIGTERM to the claude child while the surrounding runner-inner-child keeps running and posts `boid job done --output-file payload_patch.json` through the broker — that callback remains the canonical CompleteJob caller, preserving the agent's session id without racing against UnregisterJob. See WorkflowService.StopAgent for the lifecycle rationale.

func (*JobHandler) Done

func (h *JobHandler) Done(w http.ResponseWriter, r *http.Request)

func (*JobHandler) Get

func (h *JobHandler) Get(w http.ResponseWriter, r *http.Request)

func (*JobHandler) List

func (h *JobHandler) List(w http.ResponseWriter, r *http.Request)

func (*JobHandler) Log

func (h *JobHandler) Log(w http.ResponseWriter, r *http.Request)

func (*JobHandler) Patch added in v0.0.4

func (h *JobHandler) Patch(w http.ResponseWriter, r *http.Request)

func (*JobHandler) Routes

func (h *JobHandler) Routes() chi.Router

type JobLifecycle

type JobLifecycle interface {
	CompleteJob(jobID string, result JobCompletion)
	UnregisterJob(jobID string)
	CleanupTaskWindow(taskID string)
	StopJobRuntime(runtimeID string)
	// SignalJobRuntime delivers a single Unix signal to the runtime's process
	// group. Phase 3-b uses it to graceful-stop the agent (SIGUSR1) without
	// tearing down the surrounding sandbox runtime: claude.Adapter.Run() has a
	// signal.Notify(SIGUSR1) handler that translates the group signal into a
	// SIGTERM toward the claude child, then normalises the resulting exit
	// status into Result.StoppedByDaemon=true.
	SignalJobRuntime(runtimeID string, sig syscall.Signal)
}

type JobListFilter

type JobListFilter struct {
	Status       string
	Interactive  *bool // nil = no filter
	TasklessOnly bool  // true = only jobs where task_id IS NULL
}

JobListFilter specifies optional filters for global job listing.

type JobLogReader

type JobLogReader interface {
	ReadJobLog(runtimeID string) ([]byte, error)
	StatJobLog(runtimeID string) (size int64, mtime time.Time, err error)
}

JobLogReader reads the transcript log for a given runtime.

type JobLogSSEHandler

type JobLogSSEHandler struct {
	Subscriber dispatcher.RuntimeSubscriber
	Registry   *auth.ConnectionRegistry
}

JobLogSSEHandler streams live job output as Server-Sent Events on GET /{id}/log?follow=true.

func (*JobLogSSEHandler) ServeHTTP

func (h *JobLogSSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type JobStatus

type JobStatus string
const (
	JobStatusRunning   JobStatus = "running"
	JobStatusCompleted JobStatus = "completed"
	JobStatusFailed    JobStatus = "failed"
)

type JobStore

type JobStore interface {
	GetJob(id string) (*Job, error)
	ListJobsByTask(taskID string) ([]*Job, error)
	UpdateJob(job *Job) error
}

type JobWithContext

type JobWithContext struct {
	Job
	TaskTitle   string `json:"task_title"`
	ProjectName string `json:"project_name"`
}

JobWithContext extends Job with task and project metadata for the TUI global view.

type LoginHandler

type LoginHandler struct {
	Pairing loginPairing
	Signer  loginSigner
	Store   loginDeviceStore
	Limiter loginRateLimiter
}

LoginHandler handles /login and /auth.

func (*LoginHandler) GetAuth

func (h *LoginHandler) GetAuth(w http.ResponseWriter, r *http.Request)

func (*LoginHandler) GetLogin

func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request)

func (*LoginHandler) PostLogin

func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request)

type MetaStore

type MetaStore interface {
	Get(id string) (*orchestrator.ProjectMeta, bool)
	// GetWithWorkspace returns the project meta with workspace.yaml (kits,
	// env, capabilities) hydrated in. Use this whenever the caller dispatches
	// hooks or otherwise needs the resolved runtime view.
	GetWithWorkspace(ctx context.Context, projectID string) (*orchestrator.ProjectMeta, error)
}

type Notifier

type Notifier interface {
	Notify(ctx context.Context, ev notify.Event) error
}

Notifier sends an agent-driven notification for a task. Implementations typically exec a user-configured command. nil-safe at the call site: TaskAppService.NotifyTask returns an error when Notify is unset.

type NotifyTaskRequest

type NotifyTaskRequest struct {
	Message    string `json:"message"`
	Ask        string `json:"ask,omitempty"`
	QuestionID string `json:"question_id,omitempty"`
	Progress   string `json:"progress,omitempty"`
	Done       string `json:"done,omitempty"`
	Fail       string `json:"fail,omitempty"`
}

type Pairer

type Pairer interface {
	Issue(ctx context.Context, label string) (string, error)
}

WebManagementHandler serves the CLI management API at /api/web/*. All routes are accessible only via UNIX socket (CLI control plane). Pairer issues pairing codes.

type ProjectAppService

type ProjectAppService struct {
	Projects ProjectRepository
	Meta     interface {
		Load(workDir string) (*orchestrator.ProjectMeta, error)
		Get(id string) (*orchestrator.ProjectMeta, bool)
		Remove(id string)
		LoadAll(projects []*orchestrator.Project) []error
		SetWorkspaceID(projectID, workspaceID string)
	}
	// Hydrator is optional. When set, callers that need fully-resolved meta
	// (workspace-level capabilities / kits / env merged in + SecretNamespace
	// injected) go through GetWithWorkspace instead of the bare Meta.Get path.
	// Wired in internal/server/wire.go alongside the workspace store.
	Hydrator orchestrator.MetaHydrator
}

func (*ProjectAppService) CreateProject

func (s *ProjectAppService) CreateProject(workDir string) (*orchestrator.Project, error)

func (*ProjectAppService) DeleteProject

func (s *ProjectAppService) DeleteProject(id string) error

func (*ProjectAppService) GetProject

func (s *ProjectAppService) GetProject(id string) (*orchestrator.Project, error)

func (*ProjectAppService) ListProjects

func (s *ProjectAppService) ListProjects(workspaceID string) ([]*orchestrator.Project, error)

func (*ProjectAppService) ListWorkspaces

func (s *ProjectAppService) ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)

func (*ProjectAppService) ReloadProjects

func (s *ProjectAppService) ReloadProjects() (*ProjectReloadResult, error)

func (*ProjectAppService) ResolveProjectRef

func (s *ProjectAppService) ResolveProjectRef(ref string) ([]*orchestrator.Project, error)

ResolveProjectRef resolves ref to matching projects with the following priority:

  1. id exact match (returns immediately on first hit)
  2. name exact match (all projects with that name)
  3. name substring match, case-insensitive

Returns a single-element slice on unambiguous match, a multi-element slice on ambiguous match, or StatusError{404} when nothing matches.

func (*ProjectAppService) SetProjectWorkspace

func (s *ProjectAppService) SetProjectWorkspace(id, workspaceID string) (*orchestrator.Project, error)

type ProjectHandler

type ProjectHandler struct {
	Service           ProjectService
	SessionDispatcher SessionDispatcher // optional; nil disables the start-session endpoint
}

func (*ProjectHandler) Create

func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request)

func (*ProjectHandler) Delete

func (h *ProjectHandler) Delete(w http.ResponseWriter, r *http.Request)

func (*ProjectHandler) Get

func (*ProjectHandler) List

func (*ProjectHandler) Reload

func (h *ProjectHandler) Reload(w http.ResponseWriter, r *http.Request)

func (*ProjectHandler) Routes

func (h *ProjectHandler) Routes() chi.Router

func (*ProjectHandler) SetWorkspace

func (h *ProjectHandler) SetWorkspace(w http.ResponseWriter, r *http.Request)

func (*ProjectHandler) StartSession added in v0.0.6

func (h *ProjectHandler) StartSession(w http.ResponseWriter, r *http.Request)

StartSession handles POST /api/projects/{id}/sessions. The project is resolved from the URL ref (so refs like a name or short id work the same way other project routes do); the body specifies harness_type and the optional knobs (instruction / readonly / model / session_id / display_name).

type ProjectReloadResult

type ProjectReloadResult struct {
	Status string   `json:"status"`
	Errors []string `json:"errors,omitempty"`
}

type ProjectRepository

type ProjectRepository interface {
	CreateProject(project *orchestrator.Project) error
	GetProject(id string) (*orchestrator.Project, error)
	ListProjects() ([]*orchestrator.Project, error)
	SetProjectWorkspace(projectID, workspaceID string) error
	ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
	DeleteProject(id string) error
}

type ProjectService

type ProjectService interface {
	CreateProject(workDir string) (*orchestrator.Project, error)
	ListProjects(workspaceID string) ([]*orchestrator.Project, error)
	ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
	GetProject(id string) (*orchestrator.Project, error)
	SetProjectWorkspace(id, workspaceID string) (*orchestrator.Project, error)
	DeleteProject(id string) error
	ReloadProjects() (*ProjectReloadResult, error)
	// ResolveProjectRef resolves a ref string to one or more matching projects.
	// Priority: id exact match > name exact match > name substring match (case-insensitive).
	// Returns 1 project on unambiguous match, multiple on ambiguous match, StatusError{404} on no match.
	ResolveProjectRef(ref string) ([]*orchestrator.Project, error)
}

type ProjectWorkDirLookup

type ProjectWorkDirLookup interface {
	GetProject(id string) (*orchestrator.Project, error)
}

ProjectWorkDirLookup provides read access to a project's working directory.

type ReopenTaskRequest

type ReopenTaskRequest struct {
	Message string `json:"message,omitempty"`
}

type ReplayHookRequest

type ReplayHookRequest struct {
	HookID string
	Status string // optional: override task.Status before replay
}

ReplayHookRequest is the input for hook replay.

type ReplayHookResult

type ReplayHookResult struct {
	Task        *orchestrator.Task        `json:"task"`
	FiredEvents []orchestrator.FiredEvent `json:"fired_events,omitempty"`
}

ReplayHookResult is the output of a hook replay.

type RerunTaskRequest

type RerunTaskRequest struct {
	AutoStart            bool            `json:"auto_start,omitempty"`
	InstructionsOverride json.RawMessage `json:"instructions_override,omitempty"`
}

type SecretHandler

type SecretHandler struct {
	Store SecretStore
}

func (*SecretHandler) Delete

func (h *SecretHandler) Delete(w http.ResponseWriter, r *http.Request)

func (*SecretHandler) GetValue

func (h *SecretHandler) GetValue(w http.ResponseWriter, r *http.Request)

func (*SecretHandler) List

func (h *SecretHandler) List(w http.ResponseWriter, r *http.Request)

func (*SecretHandler) Routes

func (h *SecretHandler) Routes() chi.Router

func (*SecretHandler) Set

type SecretStore

type SecretStore interface {
	List(namespace string) ([]string, error)
	Set(namespace, key, value string) error
	Delete(namespace, key string) error
	Get(namespace, key string) (string, error)
}

type SessionDispatcher added in v0.0.6

type SessionDispatcher interface {
	StartSession(ctx context.Context, req StartSessionRequest) (*StartSessionResult, error)
}

SessionDispatcher launches a session job (claude / codex / opencode under a HarnessAdapter) and returns the runtime job id.

type SessionHandler added in v0.0.6

type SessionHandler struct {
	Service    ProjectService
	Dispatcher SessionDispatcher
}

SessionHandler exposes the POST /api/sessions surface (and the project-scoped /api/projects/{id}/sessions variant mounted by ProjectHandler). Phase 3-d (PR1) introduced sessions as a first-class JobKind alongside hook and exec so user-initiated agent runs (WebUI [New Session] / `boid agent`) no longer have to piggyback on the project command path.

func (*SessionHandler) Routes added in v0.0.6

func (h *SessionHandler) Routes() chi.Router

func (*SessionHandler) Start added in v0.0.6

func (h *SessionHandler) Start(w http.ResponseWriter, r *http.Request)

Start handles POST /api/sessions. The request body must specify project_id. Use the project-scoped variant when the project is implied by the URL.

Session-id resume was removed: every Start launches a fresh agent process, so there is no `/api/sessions/{id}/resume` companion route anymore.

type SetProjectWorkspaceRequest

type SetProjectWorkspaceRequest struct {
	WorkspaceID string `json:"workspace_id"`
}

type StartSessionRequest added in v0.0.6

type StartSessionRequest struct {
	// ProjectID names the project whose traits the session inherits. For the
	// project-scoped route (`/api/projects/{id}/sessions`) it is taken from
	// the URL and the body field is ignored.
	ProjectID string `json:"project_id"`

	// HarnessType selects the agent adapter. Must be one of "claude",
	// "codex", "opencode", or "shell" — the shell harness drops the user
	// into an interactive bash inside the project sandbox (`boid agent shell`).
	HarnessType string `json:"harness_type"`

	// Instruction is the optional bootstrap prompt for the first turn. Empty
	// leaves the harness to pick its default (no positional for session mode
	// on claude, since /boid-task is meaningless without a task.yaml).
	Instruction string `json:"instruction,omitempty"`

	// Readonly, when true, mounts the project workspace read-only. Sessions
	// default to writable (developer ergonomics > fail-safety).
	Readonly bool `json:"readonly,omitempty"`

	// Model overrides the harness binary's default model selection.
	Model string `json:"model,omitempty"`

	// DisplayName is the human-readable session label persisted to
	// jobs.display_name. Empty falls back to "<harness> session".
	DisplayName string `json:"display_name,omitempty"`
}

StartSessionRequest is the body of POST /api/sessions and POST /api/projects/{id}/sessions. Phase 3-d (PR1) introduced the session concept as a first-class JobKind so the WebUI [New Session] dialog and the `boid agent` CLI share one daemon entry point.

type StartSessionResult added in v0.0.6

type StartSessionResult struct {
	JobID     string `json:"job_id"`
	AttachURL string `json:"attach_url"`
}

StartSessionResult is the response shape for POST /api/sessions and the project-scoped variant.

type StatusError

type StatusError struct {
	Code    int
	Message string
}

func (*StatusError) Error

func (e *StatusError) Error() string

type TaskAnswerService

type TaskAnswerService interface {
	AnswerTask(ctx context.Context, taskID, questionID, answer string) error
}

TaskAnswerService records a user reply to a pending Q&A question and transitions the task back to executing.

type TaskAppService

type TaskAppService struct {
	Tasks       TaskStore
	Actions     ActionStore
	Jobs        JobStore
	Meta        MetaStore
	Workflow    WorkflowService
	Projects    ProjectWorkDirLookup
	RuntimesDir string
	Notify      Notifier
	// BlockingAsk coordinates harness-independent blocking Q&A (boid task ask).
	// Shared with the sandbox boid builtin executor (which calls AskTaskBlocking)
	// and the answer path (AnswerTask), so both halves of a blocking ask use the
	// same in-memory registry. Nil disables blocking ask (notify --ask still works).
	BlockingAsk *BlockingAskRegistry
	// AskDisconnectGrace is how long an awaiting task may sit with no live agent
	// parked before the daemon reclaims it (a blocking ask whose foreground
	// command was killed by a harness command-timeout). Zero falls back to
	// defaultAskDisconnectGrace.
	AskDisconnectGrace time.Duration
}

func (*TaskAppService) AnswerTask

func (s *TaskAppService) AnswerTask(ctx context.Context, taskID, questionID, answer string) error

AnswerTask saves the user's reply and transitions the task awaiting → executing.

func (*TaskAppService) AskTaskBlocking added in v0.0.6

func (s *TaskAppService) AskTaskBlocking(ctx context.Context, taskID, question string) (string, error)

AskTaskBlocking implements the harness-independent blocking Q&A RPC behind `boid task ask <question>`. The agent stays alive and blocks inside this call until the user/supervisor answers, so the round-trip works for every harness uniformly — there is no session-resume path anywhere in boid (the legacy `notify --ask` → `claude --resume` flow was removed).

The call is disconnect-resilient and idempotent across re-asks. Harnesses kill long-running shell commands on a command-timeout (claude-code / opencode at ~120s), which severs the held connection. When that happens the model retries the identical `boid task ask`; this handler recognises the retry and recovers instead of treating it as a fresh question:

  • executing → fresh ask: register the answer channel, transition executing → awaiting via ApplyAction("ask"), fire the user notification (root tasks only; child tasks are seen by their supervisor's monitor loop), then block in Wait.
  • awaiting → re-ask: if an answer was parked durably while the agent was disconnected (PendingAnswer), consume it immediately and flip back to executing. Otherwise re-attach to the SAME question id and block again — no second "ask" transition, no new notification.

Register happens before the awaiting transition so an answer that races in immediately afterwards is never dropped. Decision B1 (relaxed): a concurrent ask carrying a DIFFERENT question id still fails with ErrAskPending; a re-ask of the same question is a re-attach and is allowed.

func (*TaskAppService) CreateTask

func (s *TaskAppService) CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)

func (*TaskAppService) DeleteTask

func (s *TaskAppService) DeleteTask(id string, force bool) error

func (*TaskAppService) DuplicateTask

func (s *TaskAppService) DuplicateTask(sourceID string, autoStart bool) (*orchestrator.Task, error)

func (*TaskAppService) GetTask

func (s *TaskAppService) GetTask(id string) (*orchestrator.Task, error)

func (*TaskAppService) GetTaskDetail

func (s *TaskAppService) GetTaskDetail(id string) (*TaskDetailView, error)

func (*TaskAppService) GetTaskField

func (s *TaskAppService) GetTaskField(id, path string) (string, error)

GetTaskField resolves a dotted field path against the task. See ResolveTaskField for the path syntax (top-level fields, payload traits, computed lifecycle).

func (*TaskAppService) ImportTasks

func (s *TaskAppService) ImportTasks(reqs []CreateTaskRequest) (*ImportResult, error)

func (*TaskAppService) ListTasks

func (s *TaskAppService) ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)

func (*TaskAppService) NotifyTask

func (s *TaskAppService) NotifyTask(ctx context.Context, taskID, message, ask, questionID, progress, done, fail string) error

NotifyTask invokes the configured notify command for the given task. Returns 501 when no notifier is wired and ask is empty (notifications disabled in config). When ask is non-empty the task is transitioned to awaiting; the notification is best-effort and skipped if no notifier is configured. questionID identifies the Q&A turn (generated when empty). When progress is non-empty (progress mode), no hook fires and no state transition occurs — only a progress Action is written to the timeline. ask and progress are mutually exclusive.

Note: ask transitions the task to awaiting but does NOT spawn a resume dispatch when the user replies. The session-id resume path was removed (every dispatch is a fresh agent process); only `boid task ask` (the blocking RPC) can deliver an answer back to a live agent.

func (*TaskAppService) RerunTask

func (s *TaskAppService) RerunTask(id string, req RerunTaskRequest) (*orchestrator.Task, error)

func (*TaskAppService) UpdateTask

func (s *TaskAppService) UpdateTask(id string, req UpdateTaskRequest) (*orchestrator.Task, error)

type TaskDetailView

type TaskDetailView struct {
	Task             *orchestrator.Task
	Actions          []*orchestrator.Action
	Jobs             []*Job
	AvailableActions []string             `json:"available_actions"`
	Dependents       []*orchestrator.Task `json:"dependents,omitempty"`
}

type TaskEvent

type TaskEvent struct {
	Kind    string // "action" / "job" / "fired_event" etc.
	Payload any
}

TaskEvent はタスクに関連するイベントを表す。

type TaskEventHub

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

TaskEventHub は taskID ごとに複数の subscriber へイベントを配送する pub-sub ハブ。

func NewTaskEventHub

func NewTaskEventHub() *TaskEventHub

NewTaskEventHub は新しい TaskEventHub を返す。

func (*TaskEventHub) Broadcast

func (h *TaskEventHub) Broadcast(taskID string, ev TaskEvent)

Broadcast は taskID の全 subscriber に ev を非ブロッキングで配送する。 チャネルが満杯の subscriber はドロップする(hub 全体を止めない)。

func (*TaskEventHub) Subscribe

func (h *TaskEventHub) Subscribe(ctx context.Context, taskID string) <-chan TaskEvent

Subscribe は taskID のイベントを受け取るチャネルを返す。 ctx がキャンセルされると、チャネルはクローズされ内部からも除去される。

type TaskHandler

type TaskHandler struct {
	Service  TaskService
	Hooks    HookService       // optional: enables hook replay/list when set
	Notifier TaskNotifyService // optional: enables POST /{id}/notify when set
	Answerer TaskAnswerService // optional: enables POST /{id}/answer when set
}

func (*TaskHandler) Answer

func (h *TaskHandler) Answer(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Create

func (h *TaskHandler) Create(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Delete

func (h *TaskHandler) Delete(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Detail

func (h *TaskHandler) Detail(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Duplicate

func (h *TaskHandler) Duplicate(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Field

func (h *TaskHandler) Field(w http.ResponseWriter, r *http.Request)

Field returns a single dotted-path value extracted from the task. The response is text/plain so it can be consumed directly by shell scripts (e.g. `STATUS=$(boid task show $ID --field status)`).

func (*TaskHandler) Get

func (h *TaskHandler) Get(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Import

func (h *TaskHandler) Import(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) List

func (h *TaskHandler) List(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) ListHooks

func (h *TaskHandler) ListHooks(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Notify

func (h *TaskHandler) Notify(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Patch

func (h *TaskHandler) Patch(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) ReplayHook

func (h *TaskHandler) ReplayHook(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Rerun

func (h *TaskHandler) Rerun(w http.ResponseWriter, r *http.Request)

func (*TaskHandler) Routes

func (h *TaskHandler) Routes() chi.Router

type TaskNotifyService

type TaskNotifyService interface {
	NotifyTask(ctx context.Context, taskID, message, ask, questionID, progress, done, fail string) error
}

TaskNotifyService dispatches an agent-driven notification for a task. Wired to *TaskAppService at runtime; left optional on TaskHandler so existing tests do not need to satisfy this interface. ask/questionID are optional Q&A fields: when ask is non-empty the task is transitioned to awaiting after the notification is sent. The session-id resume path was removed, so an answer can only reach an agent that is still alive inside a blocking `boid task ask` RPC. progress is a no-state-change progress note (timeline action only). done / fail are agent self-completion / self-failure: they transition the task to done / aborted respectively and SIGTERM running hook runtimes — the parent supervisor's polling drives the response (verify + optional reopen). ask/progress/done/fail are mutually exclusive.

type TaskService

type TaskService interface {
	CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
	ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
	GetTask(id string) (*orchestrator.Task, error)
	GetTaskDetail(id string) (*TaskDetailView, error)
	GetTaskField(id, path string) (string, error)
	UpdateTask(id string, req UpdateTaskRequest) (*orchestrator.Task, error)
	DeleteTask(id string, force bool) error
	ImportTasks(reqs []CreateTaskRequest) (*ImportResult, error)
	DuplicateTask(sourceID string, autoStart bool) (*orchestrator.Task, error)
	RerunTask(id string, req RerunTaskRequest) (*orchestrator.Task, error)
}

type TaskStore

type TaskStore interface {
	CreateTask(task *orchestrator.Task) error
	GetTask(id string) (*orchestrator.Task, error)
	ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
	UpdateTask(task *orchestrator.Task) error
	DeleteTask(id string) error
	FindTaskByRemote(remoteID string) (*orchestrator.Task, error)
	FindTaskByRef(ref, parentID string) (*orchestrator.Task, error)
	FindDependentTasks(taskID string) ([]*orchestrator.Task, error)
	// ListChildren returns direct children (one level only) of the given parent
	// task, ordered by created_at ASC. Returns an empty slice (not nil) when the
	// task has no children. Used by finalizeTerminal to sweep boid/<id8> branches
	// once a supervisor reaches a terminal state.
	ListChildren(parentID string) ([]*orchestrator.Task, error)
}

type TaskWorkflowService

type TaskWorkflowService struct {
	Tasks       TaskStore
	Jobs        JobStore
	Projects    ProjectRepository
	Tx          Transactor
	Meta        MetaStore
	Coordinator DispatchCoordinator
	Lifecycle   JobLifecycle
	Worktrees   WorktreeCleaner
	Hub         *TaskEventHub
	// Locks pins the branch lock to the executing lifetime of each task.
	// Optional: when nil, no branch locking is performed (matches pre-P0-2
	// behaviour for tests that don't exercise concurrency).
	Locks *orchestrator.BranchLockManager
	// Adapter is the harness adapter used to query post-run usage. Phase 3-b
	// dropped the StopAgent role: graceful stop is delivered as a SIGUSR1
	// directly via Lifecycle.SignalJobRuntime, which claude.Adapter.Run()'s
	// signal.Notify handler intercepts. Adapter remains optional; when nil,
	// usage / future per-harness queries become no-ops.
	Adapter adapters.HarnessAdapter
	// contains filtered or unexported fields
}

func (*TaskWorkflowService) ApplyAction

func (*TaskWorkflowService) CompleteJob

func (s *TaskWorkflowService) CompleteJob(_ context.Context, jobID string, req JobDoneRequest) (*Job, error)

func (*TaskWorkflowService) InitDispatch

func (s *TaskWorkflowService) InitDispatch(ctx context.Context)

InitDispatch initialises the lifecycle context used by dispatch-loop goroutines. Must be called before the first action is applied. The returned cancel is stored internally; call Shutdown to invoke it.

func (*TaskWorkflowService) ListHooksForStatus

func (s *TaskWorkflowService) ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)

ListHooksForStatus returns hooks that match the given status for the task. If status is empty, the task's current status is used.

func (*TaskWorkflowService) ReplayHook

ReplayHook replays a single hook for the given task. If req.Status is non-empty the task's status is overwritten before dispatch. Running jobs on the same task cause a 409 Conflict.

func (*TaskWorkflowService) Shutdown

func (s *TaskWorkflowService) Shutdown()

Shutdown cancels the dispatch context and blocks until all in-flight dispatch loops have returned. Call this before closing the database.

func (*TaskWorkflowService) StopAgent

func (s *TaskWorkflowService) StopAgent(runtimeID string)

StopAgent gracefully stops the agent backing runtimeID by delivering SIGUSR1 to its process group. claude.Adapter.Run()'s signal.Notify(SIGUSR1) handler forwards a SIGTERM to the claude child and returns Result.StoppedByDaemon=true, so the surrounding sandbox runtime survives long enough to post `boid job done` through the broker normally. No-op when runtimeID is empty or no JobLifecycle has been configured.

func (*TaskWorkflowService) TriggerDependents

func (s *TaskWorkflowService) TriggerDependents(ctx context.Context, taskID string)

TriggerDependents は taskID に依存する pending タスクを評価し、 auto_start=true かつ依存条件が満たされた場合に自動 start する。 auto_start=false のタスクは依存解決しても pending のまま残り、 ユーザが手動で start するまで待機する。

type Transactor

type Transactor interface {
	WithinTx(func(TxStore) error) error
}

type TxStore

type TxStore interface {
	TaskStore
	ActionStore
	JobStore
}

type UpdateJobRequest added in v0.0.4

type UpdateJobRequest struct {
	DisplayName *string `json:"display_name"`
}

type UpdateTaskRequest

type UpdateTaskRequest struct {
	Title        string          `json:"title"`
	Description  string          `json:"description"`
	ProjectID    string          `json:"project_id,omitempty"`
	RemoteID     *string         `json:"remote_id,omitempty"`
	Payload      json.RawMessage `json:"payload,omitempty"`
	Instructions json.RawMessage `json:"instructions,omitempty"`
	ParentID     *string         `json:"parent_id,omitempty"`
	AutoStart    *bool           `json:"auto_start,omitempty"`
}

type WSAttachHandler

type WSAttachHandler struct {
	Subscriber dispatcher.RuntimeSubscriber
	Writer     dispatcher.RuntimeInputWriter
	PublicURL  string
	Registry   *auth.ConnectionRegistry
}

WSAttachHandler handles WebSocket connections for interactive PTY attach. Route: GET /api/jobs/{id}/attach/ws

func (*WSAttachHandler) ServeHTTP

func (h *WSAttachHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type WebAppService

type WebAppService struct {
	Tasks      TaskStore
	Actions    ActionStore
	Jobs       JobStore
	GlobalJobs GlobalJobStore
	Projects   ProjectRepository
	Meta       MetaStore
	Workflow   WorkflowService
	TaskSvc    TaskService
	Hooks      HookService
	Answerer   TaskAnswerService // optional: enables POST /tasks/{id}/answer
}

func (*WebAppService) AnswerTask

func (s *WebAppService) AnswerTask(ctx context.Context, taskID, questionID, answer string) error

func (*WebAppService) ApplyAction

func (s *WebAppService) ApplyAction(taskID string, actionType string) error

func (*WebAppService) CreateTask

func (s *WebAppService) CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)

func (*WebAppService) DeleteTask

func (s *WebAppService) DeleteTask(id string, force bool) error

DeleteTask delegates to the shared TaskService so the web UI uses the same delete semantics as the JSON API and TUI.

func (*WebAppService) DuplicateTask

func (s *WebAppService) DuplicateTask(id string) (string, error)

DuplicateTask delegates to the shared TaskService so the Web UI uses the same duplication semantics as the JSON API: a fresh task is created via CreateTask + resolveBehavior so that Instructions and Payload come from the behavior's DefaultInstruction / DefaultPayload, not from the source task's runtime state. Without this delegation the duplicate inherited the source's runtime payload (awaiting trait and harness session state) and missing Instructions caused the hook evaluator to skip the agent hook, so no hook fired on Start.

The Web UI button does not auto-start the duplicate; the user clicks Start separately.

func (*WebAppService) GetJob

func (s *WebAppService) GetJob(id string) (*JobWithContext, error)

func (*WebAppService) GetProjectByID

func (s *WebAppService) GetProjectByID(id string) (*orchestrator.Project, error)

func (*WebAppService) GetTaskDetail

func (s *WebAppService) GetTaskDetail(id string) (*TaskDetailView, error)

func (*WebAppService) ListBehaviors

func (s *WebAppService) ListBehaviors() ([]string, error)

func (*WebAppService) ListHooksForStatus

func (s *WebAppService) ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)

func (*WebAppService) ListJobs

func (s *WebAppService) ListJobs(status string) ([]JobWithContext, error)

func (*WebAppService) ListProjects

func (s *WebAppService) ListProjects() ([]*orchestrator.Project, error)

func (*WebAppService) ListSessions added in v0.0.2

func (s *WebAppService) ListSessions() ([]JobWithContext, error)

func (*WebAppService) ListTasks

func (s *WebAppService) ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)

func (*WebAppService) ListWorkspaces

func (s *WebAppService) ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)

func (*WebAppService) ReopenTask

func (s *WebAppService) ReopenTask(id string, req ReopenTaskRequest) error

func (*WebAppService) ReplayHook

func (s *WebAppService) ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)

func (*WebAppService) RerunTask

func (s *WebAppService) RerunTask(id string, req RerunTaskRequest) error

func (*WebAppService) UpdateTask

func (s *WebAppService) UpdateTask(id string, req UpdateTaskRequest) error

type WebHandler

type WebHandler struct {
	Service           WebService
	Hub               *TaskEventHub
	SessionDispatcher SessionDispatcher
	Registry          *auth.ConnectionRegistry

	// AttachmentsRoot is the data-home directory under which per-task
	// attachments (`tasks/<id>/attachments`) are persisted. When empty (e.g.
	// :memory: DB during tests) the multipart code path falls back to
	// rejecting attachments while still accepting plain form-urlencoded
	// submissions.
	AttachmentsRoot string
}

func (*WebHandler) GetTaskEdit

func (h *WebHandler) GetTaskEdit(w http.ResponseWriter, r *http.Request)

func (*WebHandler) HookReplayList

func (h *WebHandler) HookReplayList(w http.ResponseWriter, r *http.Request)

func (*WebHandler) JobDetail

func (h *WebHandler) JobDetail(w http.ResponseWriter, r *http.Request)

func (*WebHandler) JobTerminal

func (h *WebHandler) JobTerminal(w http.ResponseWriter, r *http.Request)

JobTerminal redirects legacy deep-links (/jobs/{id}/terminal) to the job detail page (/jobs/{id}), which now renders the terminal inline.

func (*WebHandler) PostAction

func (h *WebHandler) PostAction(w http.ResponseWriter, r *http.Request)

func (*WebHandler) PostAnswer

func (h *WebHandler) PostAnswer(w http.ResponseWriter, r *http.Request)

func (*WebHandler) PostDelete

func (h *WebHandler) PostDelete(w http.ResponseWriter, r *http.Request)

PostDelete deletes the task and redirects to the task list. Errors are surfaced via ?error= on the same task page so the user sees the reason (e.g. dependents exist).

func (*WebHandler) PostDuplicate

func (h *WebHandler) PostDuplicate(w http.ResponseWriter, r *http.Request)

func (*WebHandler) PostEdit

func (h *WebHandler) PostEdit(w http.ResponseWriter, r *http.Request)

func (*WebHandler) PostHookReplay

func (h *WebHandler) PostHookReplay(w http.ResponseWriter, r *http.Request)

func (*WebHandler) PostReopen

func (h *WebHandler) PostReopen(w http.ResponseWriter, r *http.Request)

func (*WebHandler) PostRerun

func (h *WebHandler) PostRerun(w http.ResponseWriter, r *http.Request)

func (*WebHandler) PostStartSession added in v0.0.6

func (h *WebHandler) PostStartSession(w http.ResponseWriter, r *http.Request)

PostStartSession launches a HarnessAdapter-backed session for the project from the Web UI's [New Session] dialog. Phase 3-d (PR1) introduced this alongside PostProjectExecuteCommand so users can start a claude / codex / opencode session without going through the legacy commands path.

func (*WebHandler) PostTaskCreate

func (h *WebHandler) PostTaskCreate(w http.ResponseWriter, r *http.Request)

func (*WebHandler) QuestionPage

func (h *WebHandler) QuestionPage(w http.ResponseWriter, r *http.Request)

QuestionPage renders the dedicated Q&A turn page at `/tasks/{id}/questions/{question_id}`. The notification deep-link from `boid task notify --ask` lands here. The page shows the question and either an answer form (when this is the active awaiting turn) or the recorded answer (when an answer action exists for the same question_id).

func (*WebHandler) ReopenForm

func (h *WebHandler) ReopenForm(w http.ResponseWriter, r *http.Request)

func (*WebHandler) Routes

func (h *WebHandler) Routes() chi.Router

func (*WebHandler) SessionList added in v0.0.2

func (h *WebHandler) SessionList(w http.ResponseWriter, r *http.Request)

func (*WebHandler) SessionNew added in v0.0.2

func (h *WebHandler) SessionNew(w http.ResponseWriter, r *http.Request)

func (*WebHandler) TaskDetail

func (h *WebHandler) TaskDetail(w http.ResponseWriter, r *http.Request)

func (*WebHandler) TaskDetailFragment

func (h *WebHandler) TaskDetailFragment(w http.ResponseWriter, r *http.Request)

TaskDetailFragment returns a partial HTML fragment for the task detail page. The `kind` query parameter selects which section to render:

  • "timeline": action history section
  • "status": status card + available actions
  • "jobs": jobs section

func (*WebHandler) TaskEvents

func (h *WebHandler) TaskEvents(w http.ResponseWriter, r *http.Request)

TaskEvents streams Server-Sent Events for a specific task. Subscribes to the TaskEventHub and forwards events until the client disconnects or the device is revoked.

func (*WebHandler) TaskList

func (h *WebHandler) TaskList(w http.ResponseWriter, r *http.Request)

func (*WebHandler) TaskNew

func (h *WebHandler) TaskNew(w http.ResponseWriter, r *http.Request)

type WebManagementHandler

type WebManagementHandler struct {
	Pairing   Pairer
	Store     *auth.Store
	PublicURL string
	Registry  *auth.ConnectionRegistry
}

func (*WebManagementHandler) DeleteAllDevices

func (h *WebManagementHandler) DeleteAllDevices(w http.ResponseWriter, r *http.Request)

func (*WebManagementHandler) DeleteDevice

func (h *WebManagementHandler) DeleteDevice(w http.ResponseWriter, r *http.Request)

func (*WebManagementHandler) GetDevices

func (h *WebManagementHandler) GetDevices(w http.ResponseWriter, r *http.Request)

func (*WebManagementHandler) PostPair

func (*WebManagementHandler) Routes

func (h *WebManagementHandler) Routes() chi.Router

type WebService

type WebService interface {
	ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
	GetTaskDetail(id string) (*TaskDetailView, error)
	ListProjects() ([]*orchestrator.Project, error)
	ListBehaviors() ([]string, error)
	ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
	ApplyAction(taskID string, actionType string) error
	DuplicateTask(id string) (string, error)
	DeleteTask(id string, force bool) error
	ListJobs(status string) ([]JobWithContext, error)
	ListSessions() ([]JobWithContext, error)
	GetJob(id string) (*JobWithContext, error)
	CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
	UpdateTask(id string, req UpdateTaskRequest) error
	RerunTask(id string, req RerunTaskRequest) error
	ReopenTask(id string, req ReopenTaskRequest) error
	AnswerTask(ctx context.Context, taskID, questionID, answer string) error
	ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
	ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
	GetProjectByID(id string) (*orchestrator.Project, error)
}

type WorkflowService

type WorkflowService interface {
	ApplyAction(ctx context.Context, taskID string, req ApplyActionRequest) (*ActionApplication, error)
	CompleteJob(ctx context.Context, jobID string, req JobDoneRequest) (*Job, error)
	TriggerDependents(ctx context.Context, taskID string)
	// StopAgent asks the agent backing runtimeID to terminate gracefully,
	// without tearing down the surrounding runner-inner-child. The broker's
	// `boid job done` call still fires normally, preserving any payload
	// patch the agent wrote up to that point. NotifyTask calls this after
	// `ApplyAction("ask")` so the awaiting transition does not race with
	// payload_patch capture. No-op when runtimeID is empty.
	StopAgent(runtimeID string)
}

type WorkspaceHandler

type WorkspaceHandler struct {
	Service ProjectService
}

func (*WorkspaceHandler) List

func (*WorkspaceHandler) Routes

func (h *WorkspaceHandler) Routes() chi.Router

type WorktreeCleaner

type WorktreeCleaner interface {
	CleanupForTask(taskID, projectDir, newStatus string) error
	// SweepChildBranches deletes the boid/<id8> branches associated with the
	// given child task IDs. Called by finalizeTerminal once a supervisor task
	// reaches a terminal state — its children's branches were retained through
	// CleanupForTask so the supervisor could merge them into the base branch;
	// now that the supervisor is done, the merged refs are safe to drop.
	// Branches that don't exist (already deleted, never created) are silently
	// skipped. Non-boid/* branches (root-task base branches) are never deleted.
	SweepChildBranches(projectDir string, taskIDs []string) error
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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