Agents
orchestra runs a fleet of specialised agents. Each agent has a fixed trigger, a defined write scope, and a set of constraints enforced by hooks. This document describes what each agent is and does.
Roles
| Agent | Trigger | Writes to | Key constraint |
|---|---|---|---|
delegator | every inbound event | mas_tasks | exits immediately after writing the dispatch plan; never spawns workers; banned from calling human_task_open.sh — ambiguous events write a failed mas_task instead. Resolves the target repo from agent scope first, falling back to explicit directives, GitHub-sync links, or ticket content. |
maintainer | twice daily (06:00, 22:00 UTC) | orchestra (proposals via human_task) | scans the harness and drafts evolutionary proposals; routes each to documenter/memoriser/builder by affected_paths; single-repo — locked to orchestra. |
builder | dispatcher (approved maintainer proposals) | orchestra (PR + squash-merge) | applies operator-approved changes to the harness; self-deploys via apply-local-by-agent.sh with health-check auto-rollback; single-repo — locked to orchestra. |
documenter | daily (21:00 UTC) + maintainer proposals affecting docs/ | docs/ in orchestra | sole writer to docs/; single-repo — locked to orchestra. |
memoriser | daily (20:00 UTC) + maintainer proposals affecting memory//CLAUDE.md | CLAUDE.md and memory/ in orchestra | only agent allowed to modify CLAUDE.md and memory/; single-repo — locked to orchestra. |
monitor | 00:00, 08:00, 16:00 UTC | observes only (no code writes) | proactive health checks; silent when healthy, opens human_tasks (optionally with a handoff to builder) when attention is needed; single-repo — locked to orchestra. |
reviewer | pull_request.review_requested and @-mentions | GitHub PR review | validates PRs; never merges; repo-agnostic across example-* (also reviews builder PRs on the orchestra harness). |
auditor | weekly (Fri 17:00 UTC) | orchestra (PR) | infrastructure security scan of the harness repo; opens a PR for findings; single-repo — locked to orchestra. |
developer (optional example) | Linear/GitHub/Slack events via dispatcher | product repo PR | general feature/bug/refactor agent; repo-agnostic across example-*. See note below. |
Delegator
The delegator is the router. It runs on every inbound event, decides which agent(s) handle the task, and writes a dispatch plan to mas_tasks — then exits. It never spawns workers and never opens a human_task; if an event is ambiguous it writes a failed mas_task row rather than escalating directly.
The listener pre-creates a placeholder mas_tasks row before spawning the delegator, so the delegator issues an UPDATE rather than an INSERT — every event has a visible task row even if the delegator crashes. The listener also pre-resolves the Linear issue creator via the Linear API before forwarding, so the delegator does no LLM-based extraction of that field.
Repo resolution follows a fixed order: agent scope first (single-repo agents are always dispatched to their locked repo), then explicit directives, then GitHub-sync links, then ticket content. When envelope.task_id is present the delegator uses it exactly, never generating a different id. After writing, it re-reads the row (SELECT after UPDATE) to confirm the write took effect and to repair status if a dispatcher race left the row in an unexpected state.
The delegator handles @orchestra mentions in GitHub comments (issue_comment.created, pull_request_review.submitted, pull_request_review_comment.created), routing to developer or reviewer based on comment content and acknowledging with a 👀 emoji reaction. A Slack app_mention is answered directly for non-codebase questions or routed to developer for action requests.
Maintainer
The maintainer is a background agent triggered by orchestra-maintainer.timer twice daily at 06:00 and 22:00 UTC via the dispatcher lifecycle. It performs autonomous roadmap review and proposal generation.
Two-source context:
- The current codebase as-is.
- Commits since the last run — an SHA-based window persisted to
${ORCHESTRA_STATE_DIR}/maintainer-last-sha.txt.
Roadmap bootstrapping: If ROADMAP.md is absent, the maintainer creates it and seeds it with an operator-defined autonomy definition and short/long-term priorities via a conditional human_task, adding a concrete "Create ROADMAP.md" action to its first human_task so the operator does not overlook the bootstrap step. Roadmap questions are asked only when bootstrapping or when genuinely blocked on priority ambiguity.
Proposal domains: new agent capabilities, architecture evolution, autonomy maturity tracking, and operator experience.
Routing: proposals are routed by the dispatcher based on affected_paths:
docs/only → documenterCLAUDE.mdor paths entirely underagents/(includingmemory/) → memoriser- everything else (or mixed paths across domains) → builder
Low-risk proposals (docs-only changes, minor memory updates) are auto-approved by the dispatcher without operator review. The TUI Maintenance tab mirrors this routing logic to display the correct target agent for each proposal.
Feedback loop: before drafting new proposals the maintainer examines past-proposal outcomes — builder/deploy logs, human_task resolutions, and commit history — to identify patterns in previously approved changes. It also persists cumulative acceptance-rate metrics to ${ORCHESTRA_STATE_DIR}/maintainer-acceptance.txt, tracking approved/declined counts by target agent (builder, documenter, memoriser) and priority band (high, medium, low), excluding proposals still status='open'. Low acceptance rates for a category act as a calibration signal — a hint to reframe, not a hard gate. The state-file format and update rules are documented in agents/maintainer/memory/ACCEPTANCE_RATE.md.
Session artifacts: the maintainer writes per-run markdown summaries to agents/maintainer/artifacts/<plan_id>.md, committed with the chore: prefix for artifact-only commits and surfaced in the TUI Artifacts tab.
Builder
The builder applies operator-approved maintainer proposals to the orchestra repo via PR + squash-merge. It is spawned by the dispatcher for proposals affecting non-documentation paths.
Deploy flow:
- Builds from the worktree and runs
apply-local-by-agent.shunder the deploy lock — this is the only deploy script the builder may run. - It cannot run
apply-local.shordeploy.shdirectly; those are blocked bypre_tool_gate.sh. - Lock acquisition, canonical repo sync, build, and release are collapsed into a single compound command so the EXIT trap does not release the lock prematurely across separate Bash tool invocations.
Merge conflict handling: the builder checks mergeStateStatus before enabling auto-merge, handles BLOCKED/DIRTY/BEHIND states with rebase + force-with-lease on its own branch, and hands off to a human if rebase conflicts cannot be resolved mechanically.
Post-deploy smoke tests: after deploying, the builder verifies the listener health endpoint returns HTTP 200 and confirms the dispatcher process is alive, catching deploy regressions before the session closes.
Health-check verification with auto-rollback: the builder polls the listener health endpoint for up to 30s after deploy. If the listener returns 4xx/5xx, fails to start, or the service enters a failed state, the builder rolls back to the last known-good commit recorded during the deploy sequence.
Sudo access: the builder has an explicit sudo exception for apply-local-by-agent.sh in pre_tool_gate.sh, so the deploy script can restart systemd services despite the defense-in-depth sudo block that applies to all other agents.
Documenter
The documenter is the sole writer to docs/. It has two activation modes:
- Mode 1 (daily routine): triggered by a systemd timer every day at 21:00 UTC via the dispatcher lifecycle. It runs in a per-invocation git worktree under
${ORCHESTRA_WORKSPACES_DIR}/{plan_id}/, branches offorigin/mainasagent/documenter/{plan_id}, scans non-memoriser commits, updates thedocs/directory, and squash-merges back via PR +gh pr merge --squash. It writes a session artifact toagents/documenter/artifacts/<plan_id>.mdsummarising what was processed. - Mode 2 (maintainer proposal): spawned by the dispatcher when a maintainer proposal affects
docs/only. This is a single-scope, already-approved change — the documenter applies the proposal per its implementation plan, commits, and squash-merges.
Memoriser
The memoriser is the sole writer to the orchestra repo's CLAUDE.md and memory/. It uses the same PR + squash-merge flow as the documenter and builder, and follows a read-before-write convention when editing memory files — running Read first to verify current on-disk content before any Edit or Write. It has two activation modes:
- Mode 1 (daily routine): memory promotion via the
/memory-promoteinteractive operator review loop at 20:00 UTC, unified into the dispatcher lifecycle. It writes a session artifact toagents/memoriser/artifacts/<plan_id>.mdsummarising detected patterns and generated proposals; if the run produces zero changes, the artifact is still committed standalone. - Mode 2 (maintainer proposal): dispatcher-spawned for proposals affecting
memory/orCLAUDE.mdonly.
Monitor
The monitor is a stateless health agent triggered by orchestra-monitor.timer three times daily at 00:00, 08:00, and 16:00 UTC via the dispatcher lifecycle. Every run starts fresh.
Checks: queue depth (pending/failed tasks), worker liveness (running workers vs. PIDs), systemd service health, disk space, and SQLite DB size.
Alerting: silent when healthy — no output, no commits, no PRs. It opens a human_task only when something needs operator attention and never pushes code. Automatable findings (stuck workers, disk reclamation, workspace GC, DB vacuum) include a "Fix automatically" option carrying a handoff definition targeting the builder; the operator picks it to hand the work off.
Anomaly detection:
- Tracks consecutive skipped scheduled runs and escalates if health checks fail to complete repeatedly, preventing silent degradation.
- Detects orphaned
human_tasks— tasks stuck instatus='open'whose correspondingrunning_workersrow is alreadydoneorfailed. - Flags workers stuck in
retry_requestedfor more than 1 hour and detects retry storms (plans with more than 5 total worker rows where at least one isretry_requested); these are non-automatable alerts for the operator. - Does not flag
waiting_for_humanworkers as stuck — humans may take hours to answer, which is normal operation. - Scrapes the listener
/metricsendpoint (latency percentiles p50/p95/p99, error rates by source) with--max-time 5to avoid hanging on a degraded listener. Delta-based alerting fires when error rate exceeds 5% per source or p95 latency stays above 2000ms; snapshots are persisted tomonitor-metrics-prev.jsonfor run-over-run comparison.
Session artifacts: the monitor writes per-run health-check history to agents/monitor/artifacts/<plan_id>.md. The TUI Scheduled panel shows the monitor's health at the top — "Healthy" when there are zero open monitor human_tasks, or a red alert count otherwise — with each timer's last-run status displayed inline.
Reviewer
The reviewer validates pull requests against team and repo conventions. It is triggered by pull_request.review_requested and by @orchestra mentions on PRs. It never merges. It is repo-agnostic across example-* and also reviews builder PRs on the orchestra harness itself.
Its CLAUDE.md includes a first-actions checklist for session bootstrap — check for an existing branch, verify GitHub auth, and locate affected files. It writes per-session review summaries to agents/reviewer/artifacts/<plan_id>.md following the shared artifact convention.
Auditor
The auditor runs weekly on orchestra-auditor.timer, Fridays at 17:00 UTC via the dispatcher lifecycle. It performs an infrastructure security scan of the harness repo orchestra only and opens a PR for any findings.
Security checklist (from agents/auditor/memory/CONVENTIONS.md): command injection in Bash scripts, hardcoded credentials, unsafe file operations (rm -rf, chmod 777), path traversal in script arguments, sudo usage patterns, and network-exposed service hardening. It uses a dedicated state file (auditor-last-sha.txt) for commit-window tracking. The repo can be overridden via the repo field in the routing.yaml scheduled section.
Session artifacts: the auditor writes per-session summaries to agents/auditor/artifacts/<plan_id>.md recording the commit window examined, any findings, and whether a PR was opened. If no findings were detected, the artifact states that explicitly and is committed standalone on a zero-change run.
Developer (optional example)
The developer is an optional example agent shipped as a starting point. It is a general feature/bug/refactor agent for product repos, repo-agnostic across example-*, and triggered by Linear/GitHub/Slack events via the dispatcher. Adopters can remove it, use it as-is, or treat it as a template for their own product-facing agents.
It uses conventional commits, adds no agent-name tags or Claude attribution, and transitions the Linear issue to "In Review" on PR open. Linear comments are summary-only — the PR body is the canonical context; Linear gets a one-line link with the PR headline.
Additional conventions for agent-maintained repos:
- Never run
npx tsc— use the repo'spackage.jsonbuildscript instead.npx tsccan pick up stale global config files (e.g.~/.tsconfig.json) and fail in confusing ways. - Use
NODE_ENV=development npm ci(ornpm install --include=dev) before anynpm run buildortscinvocation. WhenNODE_ENV=productionis set,npm cisilently skips devDependencies, so include dev deps before building. - Prisma 7 /
prisma.config.ts: useNODE_ENV=developmentwhen runningnpx prisma generate(production skips the required TypeScript devDependency); ensureprisma-json-types-generatorhas TypeScript installed explicitly; and use the fullpostgresql://connection string format when settingDATABASE_URLfor Prisma-backed test runs.
Routing
routing.yaml holds the routing configuration for the delegator. The integrations section contains hint pipelines the delegator uses as context but can override. The agents section is binding: repo_scope defines which repo(s) each agent may work in. The scheduled section carries prompt and repo fields for each entry, serving as the canonical reference for what each scheduled agent runs — the TUI reads these for manual triggers and run-scheduled-agent.sh passes them to the dispatcher. The pipeline: [] entry for the gc timer is intentional: gc is infrastructure-only with no agent dispatched. The delegator MUST NOT dispatch an agent to a repo outside its scope.
Agent repo scopes (binding):
| Agent | repo_scope | Type |
|---|---|---|
builder | orchestra | single-repo |
maintainer | orchestra | single-repo |
documenter | orchestra | single-repo |
memoriser | orchestra | single-repo |
monitor | orchestra | single-repo |
auditor | orchestra | single-repo |
reviewer | example-* | repo-agnostic (wildcard) |
developer | example-* | repo-agnostic (wildcard) |
The delegator (the router) has no repo_scope entry. It determines the target repo from agent scope first (single-repo agents are always dispatched to their locked repo), then falls back to explicit directives, GitHub-sync links, or ticket content. This prevents harness-internal tasks from being dispatched to the wrong repo when a Linear project's GitHub sync points elsewhere.
Event routing:
| Event | Default hint | Delegator override cases |
|---|---|---|
| Linear issue assigned to bot | developer | multi-repo → one worker per repo |
pull_request.review_requested | reviewer | — |
pull_request.merged_to_main | (none; documenter handles via schedule) | — |
issue_comment.created (@orchestra mention) | developer or reviewer | routes to developer for feature/bug requests, reviewer for review requests; acknowledges with 👀 reaction |
pull_request_review.submitted (@orchestra mention) | developer or reviewer | routes based on review comment content |
pull_request_review_comment.created (@orchestra mention) | developer or reviewer | routes based on inline comment content |
Slack app_mention | direct answer or developer | direct answer for non-codebase questions, developer for action requests |
| Maintainer proposal (approved) | routed by dispatcher, not delegator | docs/ → documenter, CLAUDE.md or paths under agents/ → memoriser, all others → builder; low-risk proposals auto-approved without operator review |
| Ambiguous / no clear repo | (open human_task, no plan written) | — |
Human handoff protocol — controlled autonomy
The system operates under controlled autonomy: the human operator does all thinking and judgment; the system handles all execution.
- Decisions belong to humans. Any choice involving trade-offs, preferences, risk assessment, or ambiguity MUST be escalated as a structured question.
- Execution belongs to agents. Once a human has made a decision, the agent carries it out precisely and completely.
A worker that encounters ambiguity calls human_task_open.sh with a structured JSON payload, then ends its turn.
Payload shape (1–4 questions, each 2–4 options; the 2–4 option constraint is enforced at runtime by human_task_open.sh):
{
"questions": [{
"question": "...",
"header": "...",
"multiSelect": false,
"options": [{"label": "...", "description": "..."}]
}]
}Options can optionally carry a handoff field. When the operator picks a handoff-carrying option, the dispatcher creates a new mas_task for the target agent instead of resuming the delegating agent. The delegating agent's worker is marked done, and the handoff target picks up the work on the next tick:
{"label": "Fix automatically", "description": "...",
"handoff": {"agent": "builder", "repo": "orchestra",
"task_input": {"summary": "...", "details": "..."}}}The human_task_open.sh hook validates that the handoff object, if present, contains non-empty agent and repo strings and a task_input object.
The dispatcher detects human_tasks.status = answered and then:
- If the answer maps to a handoff: creates a new
mas_task(source=handoff) for the target agent and marks the delegating agent's workerdone. - Otherwise: resumes the worker via
claude --resume <session_id>with the answer injected as the next user turn.
The answer key used by the dispatcher and the TUI must agree: both use the header field (short label) as the primary answer key, with question (full text) as a fallback for backward compatibility. When writing answer-consumption logic, use the header field as the lookup key.
Human task lifecycle:
- Agent opens →
status='open'(dispatcher notifies Slack). - Human answers via TUI →
status='answered'(TUI writes theanswercolumn). - Dispatcher sees the answered task →
- Handoff path: if the chosen option carries a
handoffdefinition, the dispatcher creates a newmas_taskfor the target agent (source=handoff), marks the delegating agent's workerdone, and the handoff target picks up on the next tick. - Resume path: otherwise the dispatcher resumes the agent with a
[dispatcher] Human answered human_task <uuid>: ...message containing the Q&A pairs.
- Handoff path: if the chosen option carries a
- Agent processes the answer and continues toward the original task (resume path only).
- On the next tick, the dispatcher marks
status='consumed'.
Human task hooks:
| Hook | Path | Purpose |
|---|---|---|
| Open | .claude/hooks/human_task_open.sh | Create a structured questionnaire; pipe AskUserQuestion JSON on stdin |
| Check | .claude/hooks/human_task_check.sh | Read-only lookups: by UUID or by plan id (with optional status filter). Outputs JSONL. |
Agents must NOT run ad-hoc sqlite3 queries against $ORCHESTRA_DB to inspect human_task state — the schema may evolve. Use the check script instead. The dispatcher's resume message is the source of truth for the human's answer; agents process it directly rather than independently re-querying the DB.
Slack rendering:
- Multi-question tasks render as
static_selectmenus plus a Submit button, collecting all answers atomically. Single-question tasks keep per-option buttons. - Each select menu includes an "Other… (type below)" option with a free-text input; the listener prefers free-text input over the select choice when both are present.
- After answering, the original question text is preserved, interactive blocks are removed, and a Q&A summary is shown inline with an "Answered!" status marker.
- Option descriptions render as markdown bullet points above each select menu rather than in Slack's 75-char-limited description field.
- The task creator is @-mentioned in a threaded reply so Slack sends a notification (Block Kit context blocks don't trigger pings).
- For Linear-origin tasks, notifications are sent as direct messages to the issue creator when their Slack user ID is resolved; non-Linear tasks and unresolved lookups fall back to the team channel (
SLACK_CHANNEL).
Memory model
The memory system separates concerns across layers:
- System prompts in
.claude/agents/{name}.md— loaded on every session, containing the agent's role, constraints, and handoff protocol. - Agent-level memory in
agents/{name}/memory/— durable conventions and promoted patterns for a specific agent. - Team memory — team-wide patterns promoted by the memoriser.
- CLAUDE.md at the repo root — team-wide rules, auto-loaded every session.
Key files:
.claude/agents/{name}.md ← agent system prompt (loaded every session)
agents/{name}/CLAUDE.md ← per-agent conventions (tracked)
agents/{name}/memory/MEMORY.md ← per-agent auto-memory (tracked)
agents/{name}/memory/CONVENTIONS.md ← per-agent conventions (tracked)
team/memory/ ← team-wide promoted patterns (tracked)
CLAUDE.md ← team-wide rules, auto-loaded every sessionPromotion path: memoriser detects 3+ recurrences of a pattern → runs /memory-promote as an interactive operator review loop → the human approves/rejects each proposal → approved entries are promoted to the appropriate system prompt or memory file.
Each agent's memory/ directory carries the same scaffolding: MEMORY.md with @import lines for CONVENTIONS.md and PITFALLS.md, plus stub concept files for the memoriser to populate.
Branch convention
agent/{agent-name}/{plan_id} — e.g. agent/developer/STU-806, agent/documenter/cron-documenter-20260604T210000Z-669281. All agents run in per-invocation git worktrees under ${ORCHESTRA_WORKSPACES_DIR}/{plan_id}/, checked out on a branch named agent/{name}/{plan_id} off origin/main. The canonical checkout is read-only for agents.
Agent onboarding template
agents/builder/memory/NEW_AGENT_PATTERN.md is the canonical checklist for adding a new agent to orchestra. It covers:
- System prompt (
.claude/agents/<name>.md). - Agent directory scaffolding (
agents/<name>/). CLAUDE.mdconventions file.- Dispatcher integration —
routing.yamlentry,planSchema.ts, TUI agent list. - Session artifacts (scheduled agents only) — write per-run markdown summaries to
agents/<name>/artifacts/<plan_id>.md; commit alongside other changes (or standalone on no-op runs) using thechore:prefix for artifact-only commits; two styles exist (summary prose and structured fixed-format report), documented inagents/<name>/CLAUDE.md. - Systemd units +
bootstrap-vm.shintegration — new.serviceand.timerfiles are auto-discovered by glob-basedinstall -m 0644 systemd/orchestra-*.{service,timer}commands inapply-local.shandbootstrap-vm.sh; the only manual integration point is thesystemctl enable --nowlist for the new timer. - Hook gate (if the agent writes to protected paths).
- TUI integration (typically automatic) and the validation checklist.
Shared agent references
docs/agent-handoff-protocol.md is a shared document referenced by multiple agent system prompts (builder, developer, monitor, reviewer) via their .claude/agents/{name}.md files. It contains the canonical controlled-autonomy handoff protocol, the human_task payload shape, the handoff field spec, and the lifecycle stages — extracted from previously duplicated inline descriptions to keep the prompts in sync. The pre_tool_gate.sh hook references this file for docs-gate consistency.
Hooks
| Hook | When | Does |
|---|---|---|
pre_tool_gate.sh | PreToolUse: Bash, Write, Edit | blocks rm -rf /, force-push to main, non-memoriser writes to CLAUDE.md, non-documenter writes to docs/, and non-memoriser/non-builder writes to agents/*/memory/; blocks agents from running apply-local.sh/deploy.sh (builder exception for apply-local-by-agent.sh); only operators may push directly to main; script-name regexes require word boundaries so mentioning a script in comments or commit messages does not trip the block; blocks all sudo usage by agents for defense in depth, with an explicit builder exception for apply-local-by-agent.sh. |
on_stop_auto_retry.sh | Stop (agent session ends non-zero) | detects ccr proxy synthetic model errors and marks the worker for retry instead of failure; includes SQL sanitization to prevent injection through error-message-derived values; also detects synthetic errors in cumulative (resumed) transcripts via a parallel model='<synthetic>' + isApiErrorMessage=true path, since resumed transcripts omit the result event; matches worker status IN ('failed', 'running') because the hook runs before the dispatcher marks the worker failed. |
human_task_open.sh | on demand (agent hits ambiguity) | creates a structured questionnaire from AskUserQuestion JSON piped on stdin; validates per-question option counts (2–4) and any handoff object. |
human_task_check.sh | on demand (agent needs human_task state) | read-only lookups against the human_tasks table by UUID or plan id (with optional status filter); outputs JSONL — agents use this instead of ad-hoc sqlite3 queries. |