Design Patterns
The reusable patterns orchestra is built on. This is a reference for anyone adopting or extending the system: each section names a pattern, explains how it works today, and states what to do when you build on it.
Single-writer gate
Exclusive write access to auto-maintained paths is enforced at the tool-call level by pre_tool_gate.sh, not by convention. Each protected path has one owner:
- the documenter owns
docs/, - the memoriser owns
CLAUDE.mdandmemory/, agents/*/memory/is writable by the memoriser, the builder, and operators; every other agent is blocked.
Script-name matches in the gate use word boundaries so that merely mentioning a script name in a comment or commit message does not trigger a false-positive block on a Bash call.
Extending: when you add a new auto-maintained directory, add an explicit write block for it in the gate rather than relying on discipline. When you add a new agent, give it explicit blocks for each protected path so it inherits controlled — not permissive — defaults.
Session artifacts
Every scheduled agent writes a per-session markdown record to agents/<agent>/artifacts/<plan_id>.md, surfaced in the TUI Artifacts tab. The filename is the value of the ORCHESTRA_TASK_ID environment variable, so the convention is identical across agents and gives the operator a unified view of agent activity without reading raw session transcripts.
Two content styles are established:
- summary style — free prose describing what the run did,
- structured style — a fixed-format report with stable sections.
Conventions:
- Create
agents/<name>/artifacts/.gitkeepandagents/<name>/sessions/.gitkeepso both directories survive a fresh clone. - Commit the artifact alongside the run's other changes, or standalone on a no-op run; use a
chore:prefix for artifact-only commits. - Document the path, format, and commit rules in the agent's CLAUDE.md and system prompt (
.claude/agents/<name>.md).
Memory as system prompt
There is no separate "memory file" to keep in sync — the system prompt is the evolved memory. Durable knowledge lives in the prompt files themselves (CLAUDE.md for team-wide constraints, .claude/agents/<name>.md for per-agent knowledge).
Promotion is an interactive operator loop, /memory-promote: the memoriser presents each learning candidate and the human approves or rejects it before it is written into a prompt. This keeps prompt evolution human-gated rather than automatic.
Memory scaffolding bootstrap
A new (or retrofitted) agent comes online with working memory infrastructure immediately, before its first memoriser cycle:
- First-actions checklist — a "First actions" section at the top of the agent's CLAUDE.md that runs on session bootstrap (verify git identity, GH auth, locate affected files).
- MEMORY.md with
@importlines — the agent's MEMORY.md boots with@importdirectives pointing at its concept files. - Stub concept files —
CONVENTIONS.mdandPITFALLS.mdexist as near-empty stubs (a single heading) so the@importlines resolve on the first run. The memoriser fills in content later.
When adding an agent, scaffold this layout even if the content is placeholder — the infrastructure itself is the deliverable.
Agent-to-agent handoff
Agents delegate work to other agents through two complementary primitives:
In-process —
scripts/agent-handoff.shis the canonical interface for one agent to hand work to another with no human in the loop. Use it when the decision is algorithmic.bash/opt/orchestra/scripts/agent-handoff.sh <target-agent> <target-repo> '<task-input-json>'The script generates a
hoff-<uuid-short>task id, setssource='handoff', records provenance (origin_task_id,origin_agent) insource_meta_json, and writes a validplan_jsonwith a single worker for the target agent. It validates the target agent name, repo, and task-input JSON before touching the database.Human-mediated — a
handofffield on ahuman_taskoption. When the operator picks a handoff-carrying option, the dispatcher creates a newmas_taskfor the target agent (instead of resuming the delegating agent) and marks the delegating worker done. Use it when the handoff needs judgment: an agent presents findings, the human chooses the path, and the system routes accordingly. The handoff object must containagent,repo, andtask_input; the created plan recordshandoff_from_agentandhandoff_from_task_idfor traceability.
Both paths produce the same hoff--prefixed handoff task, so downstream tooling treats them uniformly.
Shared code between services
A shared/ directory at the repo root holds TypeScript modules consumed by both the dispatcher and the TUI, wired two ways:
- build time — TypeScript project references,
- runtime — Node.js subpath imports (
#shared/*).
Single-source-of-truth logic (for example, the agent routing config) lives here rather than being duplicated across services; the deploy script syncs shared/dist/ to the canonical checkout. When you add logic both services need, put it in shared/ instead of copying it.
routing.yaml as single source of truth for scheduled agents
The scheduled section of routing.yaml is the canonical configuration for scheduled agents. Each entry carries cron, pipeline, prompt, and repo, consumed by both run-scheduled-agent.sh (which uses the prompt as the agent's task prompt) and the TUI (which parses the YAML to populate its manual-trigger feature). Entries without a prompt (e.g. the GC timer) are intentionally excluded from the trigger UI.
To add a scheduled agent, add its entry to routing.yaml — the TUI picks it up automatically and run-scheduled-agent.sh uses its prompt.
Sync hazard: the agent's task prompt lives in
routing.yaml, but the systemd unit'sExecStartis a second source that must agree with it. These two are not automatically reconciled — when you change one, change the other.
Idempotent apply-local and deploy smoke-test parity
apply-local.sh is the single deployment script invoked by both the remote push path (deploy.sh) and operators on the VM. It runs after every deploy and is explicitly idempotent: schema migrations, path creation, ACL assertion, and workspace GC all tolerate no-op runs. New deploy steps follow this pattern.
Post-deploy smoke tests run on both the agent deploy path (apply-local-by-agent.sh) and the operator path (apply-local.sh), and are kept at parity — a check added to one is added to the other. Both share a non-fatal-warning convention: each check writes a warning to stderr and a summary count is printed at the end. Current checks include health-endpoint HTTP 200, /metrics JSON validity, systemd-analyze verify, systemctl is-active for the core services, bash -n syntax checks, and a TUI bundle load check. Add new validations to both scripts together.
ccr proxy resilience
The ccr (Claude Code Relay) proxy can return a synthetic model error ("Content block is not a text block"). This is treated as a first-class reliability concern, defended with layered retries so a transient proxy fault never fails a task:
- in-process worker retry — the fastest path; the worker detects the error pattern in its own output and immediately re-spawns with
--resume, preserving the caller-supplied task input (including any human answers) on the first attempt, - listener dedup-bypass retry — clears the dedup entry for the event and re-spawns,
- stop-hook retry — on session stop, the same pattern is caught and marked for retry rather than failed,
- dispatcher resume — a fast-poll sub-interval picks up retry-marked rows as a fallback.
Error-derived values written to the database are sanitized to prevent injection. The takeaway for extenders: treat known-transient upstream faults as retryable at the layer closest to where they occur, and add outer layers as fallback.
Worktree model
Agents never run in the canonical checkout. Every agent runs in a per-task git worktree under ${ORCHESTRA_WORKSPACES_DIR}/{plan_id}/, on a branch named agent/{name}/{plan_id}. The canonical /opt/orchestra is read-only to agents.
Because each Bash tool call runs in a fresh shell, an agent should verify its location before its first Write or Edit: git rev-parse --show-toplevel must report a path under ${ORCHESTRA_WORKSPACES_DIR}. If it shows /opt/orchestra, the agent must stop and re-check its working directory rather than write to the canonical repo. All agents land changes to main via gh pr create + gh pr merge --squash; only operators push directly to main, enforced by pre_tool_gate.sh.