Files
orchestra/orchestra-spec (1).md
T
2026-07-30 14:34:29 +04:00

648 lines
26 KiB
Markdown

# Orchestra — autonomous handoff protocol
Version: 2 (binding target)
Revised: 2026-07-30
Implementation language: Go
`MUST`, `MUST NOT`, `SHOULD`, and `MAY` are normative. Defaults marked `[D]`
are tunable. `AUDIT.md` records implementation gaps; it does not weaken this
contract.
## 0. Objective
Maximize useful unattended runtime across opaque CLI harnesses: Claude Code,
Codex, OpenCode, and future adapters. Orchestra drives processes through
herdr; it does not call model APIs.
Optimize, in order:
1. No lost or duplicated work.
2. One valid owner per task.
3. Recovery without operator reconstruction.
4. Useful autonomous progress.
5. Throughput and resource efficiency.
Cost-per-turn is secondary. UI breadth, provider breadth, and delegation are
secondary until the handoff protocol passes §13.
## 1. Invariants
1. **Append-only truth.** The coordinator event log is canonical. Events are
never edited or deleted; corrections append compensation events.
2. **Durable before visible.** An event is appended and fsynced before any
projection changes or success is acknowledged.
3. **Plane-owned lifecycle.** Agents may write code and bounded intent
artifacts. Only Orchestra emits lifecycle events.
4. **Artifact references.** Events carry content hashes, never embedded
reports, task specifications, diffs, or handoffs.
5. **Strict input.** Event and artifact schemas reject unknown fields,
invalid transitions, oversized values, and unbounded instruction text.
6. **Ground-truth pickup.** A successor validates Git, the immutable task
artifact, and the handoff artifact before starting work.
7. **One owner.** Every lease has an unguessable `lease_id` and monotonically
increasing `lease_epoch`. Stale owners cannot renew, release, complete, or
mutate task lifecycle.
8. **Self-fencing workers.** A worker that cannot renew before its local lease
deadline parks its agent before the lease can be reassigned.
9. **Git crosses machines.** Remote workers exchange repository state only
through verified Git refs plus CAS artifacts. The coordinator never
validates a remote checkout.
10. **Split, validate, then close.** A predecessor remains recoverable until
the successor has validated pickup and started the replacement lease.
11. **Idempotent control.** Every cross-process mutation has an operation ID.
Retries return the original result; ambiguous UI-changing herdr calls are
reconciled, not blindly repeated.
12. **Observable refusal.** No lifecycle-critical error is a bare `continue`.
Phase, error class, retry time, owner, pane, and anchor are queryable.
13. **Liveness is live.** Pane/agent status comes from herdr or process exit,
not event replay.
14. **No preemption.** Importance orders queued work only; it never interrupts
a valid running lease.
## 2. Ownership and topology
| Component | Owner | Authority |
|---|---|---|
| Event log, CAS, projections, router | coordinator on homesrv | canonical |
| Git checkout, worktree, herdr socket, pane | worker on that machine | local execution |
| Git remote | homesrv | cross-machine repository transport |
| Browser/TUI/notifications | clients | projections and authorized commands |
### 2.1 Federation
- Remote execution is worker-pull only. A coordinator MUST NOT call a remote
herdr socket or inspect a remote filesystem.
- One worker represents one configured herdr/harness capacity pool.
- Worker identity and herdr registry identity MUST match.
- Registration declares protocol version, immutable build revision,
capabilities, concurrency, supported projects, and local health.
- Registration does not imply eligibility. Eligibility requires a recent
heartbeat and recent local-herdr protocol check.
- Coordinator and worker protocol versions MUST be compatible. Production
release gates require the same build revision unless a rolling-upgrade
compatibility test exists.
- Heartbeat loss marks a worker ineligible; it MUST NOT release a lease early.
Lease expiry or explicit relinquishment is authoritative.
Direct coordinator-to-remote-herdr operation (“Design A”) is forbidden.
### 2.2 Project configuration
Each project declares:
```text
id
machine_affinity[] # hard by default
worker-local repo
worker-local worktree_root
git remote
quality_gate
safe_action_policy
capabilities
```
Paths are local to the worker that advertises the project. A worker MUST NOT
accept a project lacking a complete local configuration.
Machine affinity is hard `[D]`: an unavailable preferred machine leaves the
task queued instead of running it elsewhere.
## 3. Durable substrate
### 3.1 Event envelope
```json
{
"schema_version": 3,
"seq": 123,
"id": "event-ulid",
"type": "TaskLeased",
"task_id": "task-ulid",
"task_version": 7,
"at": "RFC3339Nano",
"surface": "system|web|tui|mcp|maven",
"actor": "stable-actor-id",
"operation_id": "idempotency-id",
"payload": {}
}
```
- The append boundary assigns `seq`, `task_version`, and `at`.
- `seq` is globally monotonic; `task_version` is monotonic per task.
- A producer MAY supply `expected_task_version` for optimistic concurrency.
- Worker ownership is checked with `lease_id` + `lease_epoch`, not task
version. Unrelated approvals cannot invalidate a lease.
- Reusing `operation_id` with the same request returns the original event.
Reusing it with different input is rejected.
- Global projections use a named aggregate ID; they do not fake task
versions.
### 3.2 Append transaction
For each accepted event:
1. Authenticate surface and actor.
2. Strict-decode payload.
3. Validate artifact references.
4. Validate legal state transition.
5. Validate lease owner/epoch when required.
6. Allocate sequence and task version.
7. Append and fsync the event.
8. Apply in-memory projections.
9. Acknowledge the caller.
10. Write snapshots asynchronously and atomically.
Snapshot failure MUST NOT make a durable append appear failed. Restart rebuilds
from the last valid snapshot plus the complete event tail.
### 3.3 CAS
- Address: lowercase SHA-256 of exact bytes.
- Write: temporary file → fsync → atomic rename → directory fsync.
- Existing objects are re-hashed before reuse.
- Read always re-hashes.
- Corruption is a hard, observable error; a corrupt object is never trusted.
### 3.4 Worker state
Worker state is a durable local journal containing:
```text
event cursor
leases and local monotonic deadlines
sessions and exact pane/agent identities
handoff transaction phases
pending coordinator operations
last health/error observations
```
Writes are temporary file → fsync → atomic rename. Missing state starts empty;
corrupt state fails closed and requires reconciliation. It MUST NOT silently
become an empty map.
## 4. Task and lease model
### 4.1 Immutable task
`TaskCreated` references a strict, immutable task artifact rendered as
`TASK.md`:
```text
task_id, source, external_id, project
title, instructions, acceptance[]
capabilities[], quality_gate
```
The artifact hash is `task_ref`. Scheduling metadata—priority, due date,
estimate, parent—may be amended. Instructions, acceptance, and gate are not
amended in place; a scope change supersedes the task with a new task artifact.
### 4.2 Projected states
| State | Meaning | Lease |
|---|---|---|
| `queued` | eligible now or at `next_retry_at` | none |
| `leased` | launching, running, or preparing handoff | required |
| `needs_attention` | recoverable pane awaits approval/operator action | retained when live |
| `completed` | gate, commit, push, and report verified | none |
| `failed` | retry policy exhausted or non-recoverable failure | none |
`needs_attention` is not terminal. The same fenced owner may renew, resume, or
complete it. A task with no recoverable session returns to `queued` through an
explicit retry/release event or becomes `failed`.
Terminal state changes require an explicit compensation event naming the event
being corrected.
A transition to `failed` must include evidence that any live pane was parked or
retired. Terminal state is never used as a substitute for cleanup.
### 4.3 Lease
```json
{
"lease_id": "unguessable-id",
"lease_epoch": 4,
"worker_id": "workpc-opencode",
"expires_at": "RFC3339Nano",
"attempt": 2
}
```
- Epoch increases on every new lease.
- Renewal preserves ID and epoch and advances expiry.
- Every worker release, handoff, attention, approval execution, and completion
includes ID and epoch.
- A worker derives a conservative local monotonic deadline from each lease
response. It parks the agent before deadline if renewal cannot complete.
- Self-fencing means no process may keep editing after expiry. If an agent is
still busy inside the safety margin and releasing its binding does not stop
execution, the worker closes the exact pane while retaining the worktree
and local recovery journal. Before closing, it best-effort commits current
local state to an immutable orphan ref.
- A resumed worker reconciles ownership before reading, prompting, or starting
any persisted session.
- If ownership was lost, the worker never resumes or auto-merges. It pushes
the orphan ref when connectivity returns and publishes observation evidence
for operator or explicit recovery.
- Capturing a pane is not sufficient proof for renewal; pane identity, agent
identity, lease ownership, and local-herdr health must all match.
### 4.4 Retry
Failures are typed:
```text
transient_transport
worker_unhealthy
launch_failed
lease_expired
handoff_invalid
pickup_invalid
quality_gate_failed
git_push_failed
protocol_incompatible
operator_required
```
`attempt`, `failure_class`, `last_failure_ref`, and `next_retry_at` are durable
projection fields. Healthy rotations do not increment attempts. Retry policy
is exponential with bounded jitter `[D]`; the default maximum is three failed
attempts.
## 5. Event contract
| Event | Required payload | Transition / reaction |
|---|---|---|
| `TaskCreated` | `task_ref`, project, source, external ID, scheduling fields | new → queued; dedup `(source, external_id)` |
| `TaskAmended` | scheduling fields only | reproject queued ordering |
| `TaskLeased` | lease, `task_ref`, `handoff_ref?` | queued → leased; worker validates offer |
| `TaskLeaseStarted` | lease token, pane/session evidence | leased → leased; launch ACK |
| `TaskLeaseRenewed` | lease token, new expiry | leased/needs_attention → same |
| `TaskLeaseFailed` | lease token, class, evidence ref, retry time | leased → queued/failed |
| `TaskLeaseExpired` | lease token, attempt, retry time | leased/needs_attention → queued/failed |
| `TaskOrphanCheckpointed` | old lease token, evidence ref, Git ref/SHA | observation only; never restores ownership |
| `TaskNeedsAttention` | lease token, class, evidence ref, pane evidence | leased → needs_attention |
| `TaskResumed` | lease token, resolution event | needs_attention → leased |
| `ApprovalRequested` | lease token, subject ref, bounded options | leased → needs_attention |
| `ApprovalGranted/Denied` | subject ref, actor, capture revision | command result; may resume |
| `TaskHandoffPrepared` | transaction ID, lease token, handoff ref, Git anchor/ref, reason | leased → leased |
| `TaskReleased` | transaction ID, lease token, handoff ref, anchor | leased → queued; predecessor parked |
| `TaskPickupValidated` | new lease token, transaction ID, handoff ref, anchor | leased → leased |
| `TaskHandoffRetired` | transaction ID, predecessor pane result | observation after successor start |
| `TaskCompleted` | lease token, report ref, result SHA/ref, receipt | leased/needs_attention → completed |
| `TaskFailed` | class, attempts, evidence ref | queued/leased/needs_attention → failed |
| `TaskCorrected` | corrected event ID, bounded compensation fields | explicit compensation only |
| `QuotaReported` | worker/harness, window, consumed, observed at | quota projection |
Events not listed here are invalid until this contract is versioned.
Per-turn transcripts, pane liveness, and raw token events are not lifecycle
events.
## 6. Worker launch and pickup
### 6.1 Initial launch
1. Verify current lease token and worker/project eligibility.
2. Fetch/verify `task_ref`; write exact immutable `TASK.md`.
3. Sync the configured base checkout by fast-forward only.
4. Create/open the task worktree and branch.
5. Ask local herdr to open the worktree and start the configured harness.
6. Confirm the exact pane contains the exact attached agent.
7. Persist session identity.
8. Deliver one bounded launch prompt.
9. Emit `TaskLeaseStarted`.
Before step 7, a partial pane creation failure is still reconciled and cleaned
or retained as evidence. A launch failure emits `TaskLeaseFailed`; it does not
hold capacity until TTL by default.
### 6.2 Handoff pickup
Before starting a successor agent:
1. Strict-decode and validate the handoff artifact.
2. Fetch the exact remote handoff ref.
3. Create a worktree at `anchor.git_sha`.
4. Verify `HEAD == anchor.git_sha`.
5. Verify the remote ref resolves to the same SHA.
6. Verify exact `TASK.md` hash equals `task_ref`.
7. Verify transaction ID, task ID, handoff ref, and new lease token.
8. Emit `TaskPickupValidated`.
9. Start/confirm the successor agent.
10. Deliver the bounded bootstrap prompt.
11. Emit `TaskLeaseStarted`.
Any failure emits `TaskLeaseFailed(class=pickup_invalid)` with evidence. The
predecessor remains parked and recoverable.
## 7. Rotation and handoff
Rotation runs on the worker that owns the checkout. Coordinator-local rotation
logic MUST NOT be a separate implementation.
### 7.1 Triggers
| Trigger | Default | Action |
|---|---:|---|
| soft occupancy | 55% | request handoff at a natural boundary |
| hard occupancy | 75% | prepare and rotate at the next verified boundary |
| milestone | successful coherent checkpoint | rotate |
| thrash | repeated failures/edits/calls | rotate with dead ends |
| manual | authorized operator/agent intent | rotate |
Occupancy is current-turn context input, not cumulative token cost. Each
harness adapter binds usage data to the exact session/pane; “newest file
globally” is invalid when identity is ambiguous.
| Harness | Exact source | Occupancy numerator |
|---|---|---|
| Claude Code | attached session transcript, last assistant usage | input + cache read + cache creation |
| Codex | rollout path bound to the attached thread/session | last input + last cached input |
| OpenCode | message/status record bound to the attached session | input + cache read |
Cumulative session totals are receipts, never occupancy.
Unknown occupancy is observable. After two missed samples `[D]`, request a
conservative handoff at the next verified boundary; never interrupt a busy
turn.
### 7.2 Canonical handoff artifact
The agent writes only bounded labelled answers. The worker derives protocol
facts and seals strict JSON:
```json
{
"schema_version": 2,
"handoff_id": "id",
"transaction_id": "id",
"task_id": "id",
"lease_id": "id",
"reason": "threshold|milestone|thrash|manual",
"rotation_index": 1,
"anchor": {
"git_sha": "40-hex",
"remote_ref": "refs/orchestra/tasks/<task>/<rotation>",
"task_sha256": "64-hex"
},
"next": {
"action": "one bounded line",
"why": "one bounded line",
"command": "optional literal command",
"files": ["relative/path"]
},
"remaining": ["bounded item"],
"dead_ends": [{"tried": "bounded", "why_failed": "bounded"}],
"open_questions": ["bounded item"],
"learned": ["bounded invariant"]
}
```
Goal and done criteria live only in immutable `TASK.md`. Completed work is
derived from Git. The agent does not supply SHA, branch/ref, task hash, lease
identity, completion claims, or a prose report.
Authored handoff fields are treated as bounded data. Orchestra never executes
`next.command` automatically or promotes handoff text into control input.
### 7.3 Checkpoint algorithm
1. Verify lease and exact turn boundary.
2. Obtain and strict-parse bounded handoff answers.
3. Verify immutable `TASK.md`.
4. Remove/exclude transient protocol markers.
5. `git add -A`; staged, unstaged, untracked, renamed, and deleted paths count.
6. Commit to an immutable per-rotation ref if the index differs from `HEAD`.
7. Select current `HEAD` even when the work was already clean and committed.
8. Push exact SHA to `refs/orchestra/tasks/<task>/<rotation>`.
9. Verify `ls-remote` returns that exact SHA.
10. Seal/upload the handoff artifact.
11. Persist local transaction phase `prepared`.
12. Emit idempotent `TaskHandoffPrepared`.
No handoff may reference an object that is only present in a worker-local
object database.
### 7.4 Release transaction
1. After `TaskHandoffPrepared` is durable, persist phase `prepared_acked`.
2. Ask herdr to `pane.release_agent`; retain pane, worktree, report, artifact,
and transaction state.
3. Reconcile ambiguous herdr results by inspecting the exact pane/agent.
4. Persist phase `predecessor_parked`.
5. Emit idempotent `TaskReleased`.
6. On lost response, pull events by transaction ID and resume the known phase.
7. Successor completes §6.2 and emits `TaskLeaseStarted`.
8. Coordinator queues an idempotent retire command to the predecessor worker.
9. Predecessor closes the exact old pane, cleans protocol markers, persists
`retired`, and emits `TaskHandoffRetired`.
The predecessor MUST NOT be closed at step 5. It MUST NOT resume work after
release without receiving a new lease epoch.
## 8. Completion
The agent may create an empty, bounded completion-intent marker. The worker
does not trust a narrative completion report.
At a verified idle boundary:
1. Verify lease token and immutable `TASK.md`.
2. If no completion, handoff, or blocker intent exists, issue one bounded
continuation prompt requiring exactly one of: continue work, completion
intent, handoff answers, or blocker data.
3. On completion intent, run the configured worker-owned quality gate.
4. Reject a gate that modifies files unless policy explicitly permits it and
the gate is rerun on the resulting tree.
5. Stage all intended changes, commit if required, and obtain result SHA.
6. Push an immutable result ref.
7. Verify remote ref → exact result SHA.
8. Build a worker-derived report artifact containing gate command/exit,
result ref/SHA, task hash, timestamps, and pane/session evidence.
9. Record per-lease usage delta.
10. Emit idempotent `TaskCompleted` with the lease token.
11. Only after durable acknowledgement, close the pane and clean the worktree
according to retention policy.
A late completion from `needs_attention` is valid only for its retained lease
token. A stale or superseded lease can never complete.
## 9. Herdr contract
- Transport is raw JSON-RPC over TCP or Unix socket, not HTTP.
- Every request includes `params`; use `{}` for parameterless methods.
- Protocol is checked with `ping`; protocol 17 is a JSON number.
- Relevant real methods include `worktree.create/open`, `agent.start/get`,
`agent.prompt`, `pane.get/read`, `pane.send_text/keys`,
`pane.release_agent`, and `pane.close`.
- `pane.release`, `pane.kill`, `pane.rotation_signal`, and `pane.status` do
not exist.
- Herdr never creates a handoff or returns a handoff reference.
- `pane.release_agent({pane_id,source,agent})` drops a binding;
`pane.close({pane_id})` retires the pane.
- UI-changing calls are not retried after an ambiguous post-write transport
failure. The worker reconciles state first.
- Orchestra never prompts a blocked pane or a visible permission dialog.
- Approval input is bound to an exact pane and capture revision and uses only
a recognized explicit control.
## 10. Routing, quota, and performance
### 10.1 Scheduling
Router evaluation occurs when:
- a task becomes queued;
- retry time arrives;
- a worker becomes eligible;
- capacity or quota changes.
Candidate order:
1. hard project affinity;
2. supported local project;
3. fresh worker and local-herdr health;
4. capability match;
5. free concurrency;
6. quota headroom;
7. derived importance;
8. creation sequence, then task ID for deterministic ties.
The scheduler uses one immutable task/worker/health snapshot per pass. It MUST
NOT perform network probes or full event-log scans inside the task-candidate
loop.
### 10.2 Quota
Quota and context occupancy are separate:
- occupancy: current session window; drives rotation;
- quota: subscription usage across sessions; drives eligibility.
Maintain indexed 5-hour and 7-day projections per worker/harness. Receipts are
per-lease deltas and sum across rotations. Unknown or stale quota makes a
bounded pool unavailable `[D]`; 80% reported usage is treated as full `[D]`.
### 10.3 Performance requirements
- Active leases, external task keys, worker eligibility, and quota windows
have indexed projections.
- Event APIs are cursor-paginated and bounded.
- Slow notification/provider consumers cannot block append or scheduling.
- Snapshots are periodic/checkpointed, not rewritten synchronously per event.
- Benchmarks cover 1k and 10k queued tasks and publish append/assignment p50,
p95, allocation count, and projection rebuild time.
- Default target `[D]`: schedule 10k tasks against 100 worker slots in
≤250 ms p95 on the deployment-class coordinator, excluding Git/herdr work.
## 11. Authorization and approvals
Authorization is enforced at the append/command bus:
| Surface | Rights |
|---|---|
| system worker/coordinator | scoped protocol operations |
| web/TUI | full operator control |
| MCP/Maven | read plus approval-gated writes |
| ntfy/telegram | notification only |
Workers are authorized only for their own lease tokens, captures, and command
queue. “System” cannot be selected by an HTTP header.
Approval policy is project-scoped and audited:
- MAY pre-authorize safe reads, edits, tests, formatting, and Git operations
inside the isolated worktree;
- MUST gate destructive operations, secret access, arbitrary network access,
privilege changes, and paths outside the worktree;
- has no automatic timeout;
- records requested option, capture revision, actor/policy, worker ACK, and
resulting lifecycle event.
## 12. Providers, reflection, and operations
- Providers ingest by stable `(source, external_id)` and reflect terminal or
attention state without becoming task truth.
- Ingestion is idempotent; write-back cannot create an ingestion loop.
- The JSONL provider is the baseline. Gitea/Vikunja adapters use the same port.
- Notifications are asynchronous subscribers with durable cursors.
- The brief is a read-only windowed projection of completions, failures,
attention, approvals, quota, worker health, and Git refs.
- Mid-day handoff and overnight batch are the same lifecycle. The morning
brief is the overnight window; workpc pulls verified result refs on wake.
- Parent links project task → epic → roadmap. Sprint is a queued-task
selection; estimates retain author and confidence.
- Standups are advisory projections and require the same approval path as
other control changes.
- Shared `AGENTS.md`, `CLAUDE.md`, and vocabulary files are agent-editable
repository state. Workers hash them at lease time and notify adjacent live
sessions when the hash changes; pickup always rereads them.
- Deterministic quality-gate residue that is outside the current task creates
a deduplicated follow-up task instead of relying on an agent promise.
- Health exposes coordinator revision/protocol, worker revision/protocol,
local-herdr status, active lease/epoch, pane, phase, last error, retry time,
occupancy source, quota freshness, and event lag.
- Investigative tooling uses read-only herdr calls. Destructive pane operations
require explicit operator intent outside the protocol owner.
## 13. Release gate
Every production revision MUST pass:
```text
go build ./...
go vet ./...
go test ./...
go test -race ./...
git diff --check
```
Required automated coverage:
1. Strict schema, transition, authorization, and lease-fencing tests.
2. Fuzz/property tests for replay and invalid event/artifact input.
3. Crash injection before/after every append, CAS, worker-state, Git push,
herdr release, release event, pickup validation, and retire phase.
4. Lost request and lost response tests for every worker mutation.
5. Worker partition, lease self-fence, expiry, reassignment, and stale
completion tests.
6. Staged, unstaged, untracked, renamed, deleted, and clean-committed handoff
tests across distinct repositories/machines.
7. Invalid/missing remote ref, task hash, handoff, and pickup tests.
8. Coordinator and worker restart at every handoff phase.
9. Soft, hard, milestone, thrash, manual, completion, attention, approval,
and late-completion paths.
10. Performance benchmarks from §10.3.
Required controlled live proof on each harness:
```text
lease → start → work → prepare → release → pickup validate
→ successor start → predecessor retire → quality gate → push → complete
```
The proof records event IDs, lease epochs, transaction ID, pane IDs, task hash,
handoff hash, Git refs/SHAs, usage receipt, build revisions, and checksums.
Safe repository work completes without manual approvals under the configured
project policy.
## 14. Migration and defaults
- Existing schema v1/v2 events remain immutable and replay through tolerant
legacy readers.
- New protocol behavior emits schema v3 events only.
- Migration rebuilds projections; it never rewrites the log.
- Ambiguous legacy blocked/session state is `needs_attention`, never guessed
active or completed.
- Raw events are retained indefinitely `[D]`.
- No approval timeout `[D]`.
- Hard affinity `[D]`.
- Three failed attempts `[D]`.
- Unknown bounded quota is unavailable `[D]`.
- Unknown occupancy requests a conservative boundary handoff `[D]`.
This specification defines the target. Passing isolated package tests does not
establish conformance; only the live owner path and §13 do.