Files
orchestra/AUDIT.md
T

925 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Orchestra — spec conformance audit & remediation status
Audited 2026-07-27 against `orchestra-spec (1).md` at commit `325c684`.
Method: read every non-test file in `internal/` and `cmd/`, traced each spec
section to its call site, and checked whether the live path (`main.go`
router → coordinator → adapter) actually reaches it. This file is the single
merged record — a prior split between `AUDIT.md` (audit + plan) and
`progress.md` (chronological session log) has been flattened here; treat any
older chronological git history of either file as session notes, not ground
truth.
**Ground truth rule for future sessions:** this repo has a documented history
of code that *looks* wired but isn't (packages with tests that pass in
isolation while the live call path silently no-ops). Before trusting any
"done"/"fixed" claim below, check the actual call site, not just this
document.
---
## Current verdict (as of 2026-07-28)
The substrate (Layer 1) is solid. Layers 24 were originally shaped-but-not-
wired; most of the blocking defects below are now closed, verified by
reading the current code (not just by trusting this file) and by targeted
tests. Two things are not yet proven:
1. **A real end-to-end unattended run.** The first live attempt (2026-07-28)
surfaced **B12** (fixed and confirmed live, see below), and re-verifying
that fix surfaced **B13** (open) — `agent.start` can return success while
silently never starting an agent, under back-to-back leases. One task
(`wD:p1`) did make it all the way to a real attached `claude` session, so
the lease path is provably reachable, but B13 means it's not yet reliable
enough to call proven — occupancy/rotation/handoff/completion still
haven't been exercised end-to-end against a session that's guaranteed to
actually exist.
2. **Cross-machine (federation) correctness.** Deliberately deferred — see
"The federation fork" below.
`go build ./...`, `go vet ./...`, and `go test ./...` all pass.
### Follow-up live verification (2026-07-28)
The OpenCode launch path has now been exercised against workpc herdr through
an isolated `test-e2e` worktree. The verified outcomes are deliberately
narrow:
- Initial prompts use a bounded retry only after an explicit JSON-RPC
rejection (herdr's short post-start readiness window). Transport timeouts
and disconnects are never replayed. A pane is inspected first; blocked or
permission-dialog panes are refused.
- Herdr accepts task-scoped unique names (`oc-<task-id>`); active sessions
are visible by that name in `agent.list`. Prompt routing uses that name,
while pane reads remain pane-scoped.
- The initial launch message carries the task title and description. This is
necessary under Design A because homesrv cannot write `TASK.md` inside a
workpc checkout. A live OpenCode run received the full instruction and,
after explicit operator approval of two file edits, created the requested
marker and report without touching other files.
- The initial launch does **not** wait for the agent to become idle. Waiting
converted an ordinary long-running first turn into a false `TaskBlocked`;
a fresh run now remains `TaskLeased` while OpenCode is paused at its normal
permission boundary.
This does not complete the cross-machine continuity design. `clients/herdr-
bridge.go` is only a byte proxy; it is not a worker. The worker-side process
described below remains required before a remote checkout can author
`TASK.md`, derive a canonical handoff anchor, scratch-commit WIP, or safely
release it. Until then, homesrv must not rotate or clean up a non-local
worktree.
| Spec layer | State |
|---|---|
| L1 substrate (§3, §4) | Built and correct in the main path. |
| L2 harness (§5) | Occupancy, rotation, and completion all wired and reachable; B12's readiness race is fixed and confirmed live, but B13 (`agent.start` silently no-op'ing under back-to-back leases) still blocks reliable live verification of what's downstream. |
| L3 continuity (§6) | Handoff schema, pickup validation, scratch branches, TASK.md are all wired into the live path (Phase 4 complete). |
| L4 surfaces (§7) | Brief/standup/delivery real; quota has a post-hoc producer only (no live push feed yet); authz bypass closed. |
---
## Blocking defect currently open
### B14 — `agent.prompt` can duplicate a task prompt after an ambiguous wait timeout (found live 2026-07-28) — open
Live Claude E2E task `06FTF8CPH3K3DPN6XQA7G3WRQ8` (pane `wM:p1`) received
the same initial `Begin Orchestra task ...` prompt twice. Claude completed a
first turn inspecting the repository and asking for task context, then began
working on the identical prompt again after the turn stopped. This was
observed directly in the pane transcript; it is not merely a UI replay.
**Root cause:** `CLIAdapter.Lease` calls `Client.Prompt` with
`wait.until=idle` and a 60-second wait. `Client.Call`, however, applies the
client's default 10-second connection deadline when the caller context has
no deadline. Once that local read deadline expires, `Prompt` treats the
result as any other transient failure and retries `agent.prompt` with the
same payload for up to `bootRetryWindow`. Herdr may already have accepted
the first request, so this is an ambiguous outcome, not a safe retry.
**Consequence:** an agent can repeat work, waste context/quota, or overwrite
its own in-progress changes. The initial task prompt is especially exposed
because it deliberately waits for the agent to become idle.
**Additional live consequence — OpenCode permission bypass (2026-07-28):**
the OpenCode E2E task `06FTF9Z8RP5FM4F9M2SKAQNFZ4` was leased to
`workpc-opencode` in `/tmp/test-e2e-worktrees/06FTF9Z8RP5FM4F9M2SKAQNFZ4`.
An operator directly observed OpenCode stop at the interactive permission
dialog for its initial `git log --oneline -10 && git status --short` command,
then advance and run that command without operator input. Workpc herdr's
live log records Orchestra's `agent.prompt` calls at 07:37:09 (request IDs 6
and 7), 07:37:10 (ID 8), and 07:37:20 (ID 9); the last two only ended when
Orchestra's 10-second RPC deadline disconnected the client. Herdr labels
each as `changes_ui=true`. The active OpenCode manifest contains only
permission-*detection* rules and no auto-approval rule. The task later
blocked at the next prompt (`~/.claude/RTK.md`) when no further prompt retry
advanced it.
This does not prove which internal herdr action produced the equivalent of
Enter, but it proves the safety boundary is broken: an Orchestra-originated
UI-changing `agent.prompt` operation can advance an OpenCode permission
dialog. Treat this as an authorization bypass, not merely duplicate work.
**Required fix:** make the RPC deadline at least the requested herdr wait
(plus a small transport margin), and do not blindly resend `agent.prompt`
after a timeout or other ambiguous post-write error. Record an observable
failure instead, or use an idempotency/acknowledgement mechanism if herdr
adds one. In addition, never issue `agent.prompt` while `agent_status` is
`blocked` (or a pane read matches a permission dialog); treat the condition
as requiring explicit operator approval. Do not run further unattended
OpenCode E2E tasks until both controls are verified live.
### B15 — a coordinator-side block cannot reconcile a later live completion (found live 2026-07-28) — open
The same OpenCode E2E task `06FTF9Z8RP5FM4F9M2SKAQNFZ4` was marked
`TaskBlocked` when the coordinator's `agent.prompt` RPC timed out, despite
the agent having received the task. The operator later explicitly approved
the one pending `~/.claude/RTK.md` read through herdr; OpenCode completed the
verification and became `idle` in pane `wN:p1`. Orchestra still showed
`state: "blocked"` because `Coordinator.Start` returned through `block()`
and never persisted a session; its monitor therefore has no live pane to
observe or reconcile.
**Consequence:** the event log can permanently claim a task is blocked when
the leased harness has actually completed it. This is not merely stale UI:
blocked tasks have no normal completion hook path and their receipt/quota
record is lost unless an operator manually corrects the lifecycle.
**Required fix:** retain enough launch/session identity after an ambiguous
lease failure to reconcile `agent.get`/completion evidence, or introduce a
versioned `TaskCorrected` terminal-state workflow that records the original
blocker and evidence without pretending it never happened. A live pane must
not be left outside the coordinator's session map simply because prompt
delivery timed out.
### B16 — hardcoded harness name makes each harness globally single-instance (found live 2026-07-28) — open
While starting the real OpenCode healthcheck task
`06FTFDW22833F1CCB8K43Z8808`, Orchestra created worktree/pane `wP:p1` but
herdr never attached an agent. A direct retry exposed the actual cause:
`agent.start` was sent with `name: "opencode"`, which herdr rejected with
`agent_name_taken` because the completed prior E2E agent in `wN:p1` already
owned that name. This is not a herdr limit of one OpenCode process per
machine: retrying the same `kind: "opencode"` in `wP:p1` with the valid,
unique lowercase name `oc-06ftfdw22833f1cc` immediately started a second
attached, idle OpenCode session while `wN:p1` remained intact.
**Root cause:** `Client.StartAgent` passes the harness kind as both
`kind` and `name`; every OpenCode lease therefore competes for the same
global agent-name slot. The same defect applies to Claude and Codex.
**Required fix:** keep `kind` as the configured harness, but derive `name`
from a validated, bounded task/session identifier (lowercase letters,
digits, `-`, `_`, maximum 32 characters) and persist it in `Session` for
subsequent lifecycle operations. Add a contract test that starts two same-
harness sessions in distinct panes and verifies both attach.
### B17 — opaque harnesses must not author canonical handoff anchors (found live 2026-07-28) — open
The real OpenCode healthcheck run showed that prompting an opaque agent to
write the full `continuity.Handoff` schema is the wrong ownership boundary.
The agent wrote JSON that looked plausible but used `anchor.sha` instead of
`anchor.git_sha`, omitted required top-level fields by nesting them under
`knowledge`, and could not correctly represent the uncommitted handoff file
as a dirty-file hash (a file cannot contain the SHA-256 of its own final
contents). Repeated corrective prompts made the handoff less reliable and
turned Orchestra's protocol details into agent prompt lore.
**Required direction:** prompt a harness exactly once for a small semantic
handoff/review report (what changed, validation evidence, remaining work,
review findings). The worker that owns the checkout must then deterministically
collect `HEAD`, branch, dirty paths and SHA-256 values, construct the canonical
handoff artifact, and validate it before publication. Canonical handoff files
are worker-owned protocol state and must be excluded from their own dirty-file
list. Project-level commands and stable expectations (`build`, `test`, file
ownership/invariants, etc.) belong in `AGENTS.md`/`CLAUDE.md`; only task-
specific evidence belongs in the handoff.
This cannot be safely implemented in Design A by homesrv: its coordinator
does not own the workpc checkout. Expand the bridge into a real worker-side
continuity participant (the deferred Design B direction) so all Git-derived
handoff facts are gathered and sealed on the machine that hosts the worktree.
**Live evidence (OpenCode healthcheck/review run, 2026-07-28):** task
`06FTFDW22833F1CCB8K43Z8808` initially hit B13/B16: Orchestra created `wP:p1`
but its hardcoded `name: "opencode"` conflicted with the prior E2E session.
Starting the same `kind: "opencode"` with unique name `oc-06ftfdw22833f1cc`
attached successfully without closing the prior pane. The implementation
agent created executable `scripts/healthcheck.sh`, validated `bash -n`, its
normal mode, and `--help`, then committed `672c123 Add scripts/healthcheck.sh`.
It first authored a lookalike handoff (`anchor.sha`, incomplete `meta`, and
all protocol fields under `knowledge`). A distinct reviewer in a second pane
(`wP:p2`) independently repeated the checks, noticed the nesting error, and
rewrote the document. Its claimed schema validation checked only JSON syntax
and the presence of keys — it did **not** use `continuity.Decode` or the
actual typed contract: `test` remains an array where `Handoff.Test` is a
string, and `last_result` remains a string where `Handoff.LastResult` is a
structured object. This is a concrete proof that a reviewing harness cannot
be trusted to validate canonical protocol state by visual shape alone.
The task is deliberately still `TaskBlocked` from its original failed
automatic attach even though its manually-started implementation/review panes
are live, a second live confirmation of B15. The reviewer pane is left open
for operator inspection; do not close or release it without approval.
**Related live observation — unexpected Claude launch explained:** Claude
pane `wM:p1` did not start spontaneously. At 08:00:41Z the expired lease for
task `06FTF8CPH3K3DPN6XQA7G3WRQ8` emitted `TaskReleased(reason:
"lease_expired")`; the router re-leased it at 08:00:42Z, and workpc herdr
attached Claude at 08:00:43Z. The renewed attempt then hit B14's prompt
timeout and became blocked. This is expected retry behavior, but illustrates
why lifecycle retries must be visible to operators.
### B13 — `agent.start` silently no-ops under back-to-back leases (found live 2026-07-28) — closed (code fix; not yet re-verified live)
Discovered while live-verifying the B12 fix below. B12 itself is confirmed
fixed (see that entry), but re-testing it exposed a second, deeper defect
that B12's retry logic cannot address.
Three fresh tasks were leased in quick succession against `workpc-claude`
(`06FTAKSJZTB73FZQE3QT7XQ1J0`/pane `wD:p1`, `06FTAMFKVEPGPKR0H86C08ZZNG`/pane
`wE:p1`, `06FTANAYZPA55BABR2J4MWQ050`/pane `wF:p1`). All three got a
`worktree.create` success and an `agent.start` call that returned **no
error**. Checked live via `pane.get`/`pane.list` (and independently
confirmed by the user cd'ing into each worktree by hand):
- `wD:p1` (the first, given time alone to settle): a real `claude` process
did eventually attach — `agent: claude`, `agent_status: idle`,
`revision: 2`. This is consistent with B12's model: `agent.start`
acknowledges the request but the actual attach is asynchronous, sometimes
taking well over a minute.
- `wE:p1` and `wF:p1` (started moments after `wD:p1`, while it was presumably
still initializing): **still no agent attached after 2+ minutes of
polling** — `agent_status: unknown`, no `agent` field, `revision` never
incremented past its creation value. Empty shells, confirmed both via
`pane.get` and by the user directly `cd`ing into the worktree and finding
no session running.
So `agent.start`'s "success" is not just delayed for these two — it never
happened at all, and `agent.start` never returned an error to tell Orchestra
that. One plausible cause: something about launching a second/third `claude`
CLI process while a prior one on the same host/account is still mid-startup
(auth handshake, config lock, etc.) silently drops the later request(s)
rather than queuing or erroring them. Not confirmed — would need a
controlled single-at-a-time repro to isolate from herdr's own internals,
which requires spinning up more real panes and wasn't done this pass (see
"three orphaned panes" below).
**Consequence:** unlike B12 (which at least produced an observable
`TaskBlocked`), this failure mode leaves the task leased indefinitely against
a pane that will never produce a session, with no error surfaced anywhere —
worse than B12 was, because nothing currently distinguishes "still booting,
give it more time" from "silently dead, will never start." A client-side
retry on `agent.start` itself (B12's approach) cannot fix this, since the
call that should have started the process already returned success with no
error to retry on.
**Fix:** since `agent.start`'s own return value can't be trusted, `StartAgent`
(`internal/herdr/herdr.go`) no longer treats its success as the end of the
story. After `agent.start` returns without error, it polls `pane.get({pane_id})`
(new `paneAgentAttached` helper) until the pane reports a real attached agent
(`agent` non-empty and `agent_status` present and not `"unknown"`), bounded by
a new `agentAttachWindow` (90s, longer than `bootRetryWindow`'s 15s since a
legitimate attach was observed live taking "well over a minute"). If the
window elapses with no attach, `StartAgent` now returns an explicit error
instead of a false success — this is exactly the "recorded/observable
failure over silent `continue`" pattern this repo's CLAUDE.md calls for, and
should surface as `TaskBlocked` through the same path B12's fix already
proved reachable.
**Not yet done:** re-verified against a live herdr instance (the schema for
`pane.get`'s result — `agent`/`agent_status` fields — was inferred from
prose in this file's own B12/B13 narration of live `pane.get` output, not
re-confirmed by a fresh probe of `192.168.1.105:9245`; `deploy/herdr-schema.json`
has no entry for `pane.get`'s result shape). `go build`/`go vet`/`go test
./...` all pass, but no test exercises `StartAgent`'s new polling loop
directly (existing `internal/herdr` tests don't call `agent.start`/`pane.get`
through the fake TCP listener at all). Next session should confirm the
`pane.get` field names live before trusting this closes B13 operationally,
and ideally fire a fresh back-to-back-lease test against workpc once that's
confirmed.
**Side effect of this investigation — three live orphaned panes on workpc,
left untouched on purpose:** `wD:p1` (has a real but abandoned `claude`
session, task now stuck in `blocked` from before B12's fix), `wE:p1` and
`wF:p1` (empty shells, no agent ever attached). Not cleaned up — same
standing rule as the pre-existing `wA:p1` stuck task: don't call
`pane.close`/`pane.release_agent` against live panes without asking first.
---
## B12 — `StartAgent` races the pane's shell readiness (found live 2026-07-28) — closed (this specific race; see B13 for a second issue it exposed)
Discovered during the first real end-to-end run against a freshly rebuilt/
redeployed binary (the running service had been on stale pre-audit commit
`325c684` the whole time).
Created a fresh task (`06FTAH4MCRCAJYY7V75Z4V2YGG`, project `test-e2e`,
capability `opencode`). It reached `TaskLeased` onto `workpc-opencode`
(worktree created for real at `/tmp/test-e2e-worktrees/<task-id>` on workpc,
confirmed via a live `pane.list` — pane `wB:p1`, correct `cwd`), then
**445ms later** hit `TaskBlocked`:
```
lease: herdr protocol error: agent target pane wB:p1 is not an available shell
```
`internal/herdr/herdr.go:199` (`StartAgent`) calls `agent.start` on the pane
`Worktree` just recorded, with no wait or retry after `worktree.create`
returns. herdr hands the pane back before its shell has actually finished
initializing, and `agent.start` refuses it as not-yet-a-shell. None of B1B11
exercised this — every prior fix assumed a session already existed
(occupancy, rotation, handoff); nothing in the remediation plan tested the
very first `Lease` call against a real herdr pane end-to-end. Phase 0's
probing used `params:{}`/missing-field tricks, never a real
`worktree.create``agent.start` sequence timed against actual pane boot.
**Reproduced a second time, 2026-07-28**, same session: a second fresh task
(`06FTAHW8GZP0S6DCHEQ6PJXWB0`) leased onto a brand-new pane (`wC:p1`) and hit
the identical error on the same sub-second timeline. Two for two — a
systematic race, not a flake.
**Consequence:** every fresh lease that hits this race goes straight to
`TaskBlocked`, and the router **never retries a blocked task**
(`internal/router/router.go` has no reference to `StateBlocked` at all,
confirmed by grep) — so it sits there permanently until a human intervenes
via the approval surface. This is now the single blocking defect for proving
the rest of the remediation plan against a real live task.
**Fix, iterated live across three rounds, 2026-07-28:**
1. First pass: bounded-retry `agent.start` (10×, 500ms apart) specifically on
the `"not an available shell"` string. Redeployed and re-tested against a
real fresh lease — this cleared that exact error, but the very next call
in the sequence (`Lease`'s post-`agent.start` `Prompt`) then hit a
*different* transient error, `"agent ... is not an active named agent"`
same underlying readiness race, one call later, worded differently.
2. Second pass: added the identical string-matched retry to `Prompt` for that
specific message. Redeployed, re-tested — this time hit a *third* distinct
wording, `"agent target ... not found"`, on the same call.
3. Given three different error strings for the same race across three live
attempts, string-matching was abandoned as unwinnable (herdr's wording for
"not ready yet" isn't fixed enough to enumerate). **Final shape:** both
`StartAgent`'s `agent.start` call and `Prompt`'s `agent.prompt` call now
retry on *any* error, bounded by wall-clock time (15s, `bootRetryDelay` of
500ms) rather than attempt count or specific wording — there is no other
legitimate reason a call against a pane/worktree orchestra itself just
created would fail immediately. `StartAgent` still special-cases `"already"`
as a non-error (idempotent re-lease).
Confirmed this closes the original race: task `06FTAKSJZTB73FZQE3QT7XQ1J0`
(pane `wD:p1`) leased successfully, and polling `pane.get` live shows a real
`agent: claude` / `agent_status: idle` attach — independently confirmed by
the user `cd`-ing into that worktree by hand. `go build`/`go vet`/`go test
./...` all pass throughout.
**Not fully clean, however:** re-testing this fix by firing two more tasks
shortly after the first one surfaced a second, separate defect — `agent.start`
returning success while never actually starting an agent, with no error for
the retry logic to even see. That's **B13**, above — this entry only covers
the specific shell/agent-readiness race B12 was originally filed for, which
*is* fixed; B13 is a distinct root cause the same investigation exposed.
---
## Closed defects
Each entry is the flattened final state — design + what's verified — not the
session-by-session narration. Grouped by original defect ID.
### B1 — Occupancy measured from the wrong value (§5.2.1) — closed
`CLIAdapter.Occupancy` was calling `a.Usage(s.PaneID)` (a herdr pane id)
against readers (`ClaudeUsage`/`CodexUsage`/`OpenCodeUsage`) that want a
filesystem path to session state — every call failed and was swallowed by a
bare `continue`. Fixed: `herdr.Session.SessionFile` is resolved at lease time
per harness (Claude: newest-mtime transcript under Claude Code's own project
dir; Codex: existing `CodexActiveUsage` sqlite discovery; opencode: refuses
loudly rather than guess, since it needs a live session id not resolvable
from the worktree alone). A missing/unreadable session file is now a hard
error surfaced via `SessionHealth.Occupancy`/`OccupancyError` on
`GET /v1/tasks/{id}/health`, never a silent zero.
**Still open:** live verification against a real Claude Code session at a
known context fill — not possible from this sandbox, blocked further by B13
now that fresh leases don't reliably survive.
### B2 — Adapter lookup keyed wrong in 3 of 4 call sites (§5.3, §5.4) — closed
`AdapterFactory.Herdrs` is keyed by herdr instance id (`homesrv-claude`);
`Reconcile`/`expire`/`rotate` were looking it up by `session.Harness` (the
harness kind, `claude`) and silently no-op'ing (`continue`) on every miss —
orphaned panes never killed, expired leases never killed their pane,
rotation never began. Fixed with a single `Coordinator.adapterFor(session)`
routed through at all four call sites. Regression test registers an adapter
under a herdr-id key distinct from the harness kind and asserts rotation
still fires.
### B3 — Nothing emitted `TaskCompleted` (§4, §5.2) — closed
Only producer used to be a human calling `POST /v1/tasks/{id}/complete`.
Added `POST /v1/harness/complete`: a Claude Code Stop hook
(`deploy/hooks/orchestra-stop.sh`) fires every turn boundary but only
reports completion once the agent has written a `.orchestra-report.md`
marker at the worktree root (an ordinary turn boundary is a no-op). The
server builds the `receipt` itself from `herdr.ClaudeUsage` against the
transcript (never trusts a self-reported number) and uploads the report
body to CAS for `report_ref`. Gated by optional `ORCHESTRA_HARNESS_TOKEN`;
event appended with `Surface: system` set directly in Go, consistent with
B8. Extended to Codex/opencode via an optional `harness` field in the
request body (`"codex"``CodexUsage`, `"opencode"``OpenCodeUsage`).
**Still open:** no automated test for the HTTP handler (`cmd/orchestra/main.go`
has zero handler test coverage of any kind, pre-existing gap — this follows
the existing pattern rather than introducing a one-off harness).
### B4 — Router counted rotation as a retry (§5.3, §5.4) — closed
Every `TaskReleased` (including rotation, which carries a valid
`handoff_ref`) advanced `MaxAttempts`, and lease also double-counted — a task
healthy enough to rotate twice was killed. Now only a release *without* a
`handoff_ref` (expiry/crash) advances the counter.
### B5 — Herdr protocol methods unverified/invented — closed
Confirmed live against `192.168.1.105:9245` (raw JSON-RPC probing, no local
`herdr` CLI available — schema reconstructed from Rust serde's
"unknown variant"/"missing field" errors). `pane.release`, `pane.kill`,
`pane.rotation_signal`, `pane.status` **do not exist**. Real replacements:
`pane.close({pane_id})` for kill (drop-in); `pane.release_agent({pane_id,
source, agent})` for release — structurally different, does **not** return
a `handoff_ref` (herdr never writes handoffs; the agent does, §6.1).
`RotationSignal` interface/method/call site deleted outright (no replacement
exists; herdr has no concept of Orchestra rotation).
`CLIAdapter.Release` now: reads the agent-authored `.orchestra-handoff.json`,
validates with `continuity.Decode`, cross-checks its anchor SHA against the
worktree's real `HeadSHA`, re-verifies every `Anchor.Dirty` file hash,
scratch-commits any dirty files (`continuity.ScratchCommit`, idempotent
across multiple rotations) and rewrites the handoff's anchor to the new
scratch commit before uploading to CAS via `continuity.Save` (minting the
real `handoff_ref`), and only then calls `pane.release_agent` — sequenced
last so a herdr-side error can't strand an uploaded handoff. Any failure
(missing file, invalid schema, anchor mismatch, stale dirty hash, herdr
error) is a refusal, which `rotate` already treats as "retry next tick."
`CLIAdapter.Lease`'s bootstrap prompt also fixed to pass `time.Minute` (not
`wait=0`) for inline-wait, matching `Bootstrap` and closing the
send-into-a-half-rendered-prompt race §5.1 requires guarding against.
Covered by `internal/herdr/adapter_test.go` against a real git worktree and a
fake in-process herdr TCP listener: upload-and-release on a valid handoff,
refusal with no handoff file, refusal on anchor mismatch, refusal on stale
dirty-file hash, refusal with no CAS configured.
Protocol version confirmed live as a bare JSON number (`17`), not the string
`config.jsonc` declares — `CheckProtocol`'s raw-bytes fallback happens to
compare correctly today; don't "clean up" that code without re-checking
this, or it may start doing a real numeric-vs-string comparison and break.
**Still open:** nothing makes the agent actually write
`.orchestra-handoff.json` unprompted (closed separately, see B6/Phase 4 item
2 below) — that's the piece this entry originally deferred.
### B6 — Layer 3 (continuity) was entirely dead code (§6) — closed
Nothing wrote `TASK.md`, so pickup validation had nothing to check and never
ran. Fixed in full:
- `GitWorktrees.Create` now writes and commits an immutable `TASK.md`
(`continuity.RenderTaskFile`) into every fresh worktree;
`continuity.TaskFileHash` reads it back for `w.TaskFileSHA`.
- `Coordinator.Start` runs `continuity.ValidatePickup` (anchor SHA +
dirty-file hashes + TASK.md hash) before bootstrapping a successor onto a
`handoff_ref` — failure kills the session and emits `TaskBlocked` instead
of trusting an unvalidated ref.
- `ScratchCommit` wired into `Release` (see B5) so pickup collapses to a
single HEAD compare instead of per-file rehashing.
- `CLIAdapter.Bootstrap`'s prompt rewritten to point the agent at
`git log`/the scratch branch (trust already established by the plane's own
`ValidatePickup`) instead of vague "read the handoff" prose, and
deliberately does not claim a `GET /v1/artifacts/<ref>` endpoint since none
exists (`/v1/artifacts` is POST-only — an earlier draft invented this and
was corrected before landing).
- `continuity.MarkdownChanges` (zero callers, zero tests despite being
listed as implemented in an earlier snapshot) was deleted rather than
half-wired. §6.3's actual requirement ("notice to agents whose task is
adjacent") was rebuilt independently: `continuity.ConventionsHash(root)`
hashes `AGENTS.md`/`CLAUDE.md`/`VOCAB.md`; `Coordinator.checkConventions`
runs every `Monitor` tick, recomputes the project base repo's hash for
every leased session, and on drift calls the optional
`herdr.ConventionsNotifier` capability (in-pane prompt) once per drift.
- **Handoff production, the item that most of B6 hinged on:**
`Coordinator.rotate` checks the adapter's optional `herdr.HandoffRequester`
capability; if `.orchestra-handoff.json` is missing, it prompts the agent
once (`CLIAdapter.RequestHandoff`, naming the exact §6.1 JSON shape and
explicitly telling the agent not to fabricate SHA/hashes) and skips
`Release` that tick, retrying every subsequent tick — mirrors the
`.orchestra-report.md`/B3 convention exactly. `Session.HandoffRequested`
avoids re-prompting every tick.
Covered by `TestGitWorktreesCommitsTaskFile`, `TestStartBlocksOnInvalidPickup`,
`TestReleaseScratchCommitsDirtyFilesBeforeUpload`,
`TestReleaseRefusesOnStaleDirtyFile`, `TestConventionsDriftNotifiesActiveSession`,
`TestRotationRequestsHandoffBeforeReleasing` (internal/herdr, internal/orchestrator).
**Caveat still open:** TASK.md hashing is best-effort/untested for the
herdr-hosted (`WorktreeCreator`) worktree path specifically — no adapter or
test exercises that path with a real `handoff_ref`, so pickup validation
there runs with an empty `taskFileSHA` (anchor + dirty-file hashes still
checked). Same cross-host caveat as the federation fork, below.
### B7 — Quota projection had no producer (§7.2) — closed (post-hoc only)
`POST /v1/harness/complete` now appends a `QuotaReported` event
(`consumed` from the same `usage.Numerator()` used for the receipt), so the
router's 5h/weekly filter and the brief's `quota_consumed` stop evaluating
against a permanent zero.
**Design gap recorded, not yet built** — a live push producer, since a
harness that never completes a task cleanly (e.g. the stuck `wA` pane, see
below) currently under-counts its consumption forever:
- **Claude**: the statusline stdin JSON already carries real
`.rate_limits.five_hour.used_percentage` / `.seven_day.used_percentage`
confirmed by reading `~/.claude/statusline.sh` on this machine. Prefer this
over `claude.ai/api/.../usage`, which needs browser session cookies, not
an API key.
- **Codex**: every `token_count` event in the rollout carries a `rate_limits`
object with `used_percent`/`window_minutes`/`resets_at` — confirmed
directly from a real rollout file. No `codex usage` subcommand; the
rollout tail (or `codex app-server`, same events live) is the only
transport.
- **opencode**: no first-class quota surface. `opencode stats` is historical
only. Zen's free-tier daily quota arrives as `x-ratelimit-*` response
headers, not a queryable endpoint — would need an opencode plugin to
intercept.
- Structural consequence if this is ever built: `sumSince`'s *additive* model
is wrong for a used-*percentage* feed (need a second `Availability` reading
the latest report per harness, not summing); static `quota_limit_5h/weekly`
become unnecessary for Claude/Codex once real fractions are reported;
`QuotaWindowLimits{FiveHour,Weekly}` is too narrow for Codex's
self-describing `window_minutes` or opencode's daily Zen window.
### B8 — `Surface: system` unauthenticated bypass (§7.1) — closed
`X-Orchestra-Surface: system` was reachable from any HTTP request header in
both `authz.HTTP` and `main.go`'s `surface` closure — since no deployment
sets `ORCHESTRA_SYSTEM_TOKEN`, this was an unauthenticated full-control
bypass reachable from any LAN caller. Both call sites now downgrade `system`
to `web` before doing anything else with it. `System` is only constructible
in-process (router leases/failures, coordinator releases/blocks, standup
advisory/apply) via the bus-level `authz.AuthorizeEvent` check now enforced
inside `store.Append` itself.
### S1 — duplicate JSON struct tags — closed
`Brief.From`/`To` and `GitSync.Branch`/`Head`/`Status` each shared one JSON
tag (Go only honors the first `json:"..."` tag on a combined field
declaration), making `go vet ./...` fail and the brief's git state
unparseable. Fixed; `go vet ./...` passes clean.
### S2 + S3 — brief git state and completion receipts — closed
`Brief.Git` used to read from `ORCHESTRA_DATA` (the event-log dir, never a
git checkout — always "git unavailable") and completions were counted but
never surfaced with proof. `operations.Brief.Git` is now `map[string]GitSync`
keyed by project ID, built from `registry.Project.Repo` (falling back to a
single `"default"` entry off `ORCHESTRA_REPO`), with `Ahead`/`Behind` vs
upstream added. `Brief.Receipts []CompletionReceipt` pulls `report_ref`/
`receipt` straight out of each `TaskCompleted` event's existing payload.
### S4 — notification goroutine died on first send error — closed
`delivery.Fanout.Run` used to `return` on the first sender error,
permanently killing notifications for the rest of the process's lifetime.
Failed sends now go through an `OnError` hook instead of aborting the loop.
Cursor is persisted (`SaveCursor` → a `delivery-cursor` file next to
`ORCHESTRA_DATA`), so a restart resumes from the last delivered event
instead of re-notifying the entire log from seq 0.
### S5 — colliding event IDs on lease — closed
`Store.Lease`/`ExpireLeases` set `Event.ID` to the task id, so every lease of
a task produced colliding event IDs. Now uses `domain.NewID()`.
### S6 — ingest dedup returned the wrong event with 200 — closed
Dedup path returned `nil` (success) without appending; the HTTP handler then
returned an unrelated event with `201`. Added `domain.ErrDuplicate` and
`Store.TaskBySource`; `POST /v1/tasks` now returns the existing task with
`200` on a duplicate. Every other `Append` caller (Gitea poll/webhook, JSONL
ingest) now treats `ErrDuplicate` as expected.
### S7 — `TaskAmended` dropped most fields — closed
Only `title` was ever applied from an amendment payload; `due`/
`description`/`inherent_priority` were accepted, logged, and silently
dropped by the projection. Also added the missing `Task.Description` field
entirely (it didn't exist, so `TaskCreated` dropped it too). Both
`TaskCreated` and `TaskAmended` now populate all four fields.
### S8 — no compensating-event mechanism (§3.1) — closed
Added `TaskCorrected`: payload requires `corrects` (the id of the event it
repairs) plus at least one change (`state`, or the existing amend-style
fields). `Store.Append` rejects a `corrects` that doesn't name a real prior
event on the same task; `apply` applies the changes and clears `Lease` like
every other terminal-state branch. Covered by a mistaken `TaskFailed`
reverted to `queued`, an unknown-`corrects` rejection, and both events
surviving snapshot+replay.
### S9 — duplicate lease-expiry tickers — closed
`Coordinator.Monitor`'s 30s ticker and `main.go`'s own 1s reclaim ticker both
called `Store.ExpireLeases` independently. Previously filed as "harmless —
CAS rejects the loser," but the real effect was worse: the coordinator's
`expire()` is the *only* place that kills the herdr session/pane for an
expired lease, and the 1s ticker running 30x more often almost always won
the race, leaving the coordinator's own call with nothing left to expire —
silently orphaning herdr panes past TTL. Fixed: `main.go`'s ticker skips
`ExpireLeases` entirely when a coordinator is configured (deferring reclaim
to the coordinator's loop), keeping only `AssignPending` as periodic retry.
### S10 — federation worker registration had no admission control — closed
Any caller could self-declare a worker `id` and self-choose a `token`, and a
second caller could silently re-register an existing worker id with a
*different* token, hijacking its identity/capacity. Added
`Registry.AdmitToken` (pre-shared secret via
`ORCHESTRA_FEDERATION_ADMIT_TOKEN`, checked against the registration
request's bearer header); same-ID re-registration now requires presenting
the existing worker's own token (legitimate restarts still succeed;
different-token same-id is rejected as a hijack).
### S11 — soft threshold, milestone, thrash, agent-initiated ROTATE (§5.3) — closed
All four of §5.3's rotation triggers now land:
- **Soft threshold**: `Coordinator.Soft` (default 0.55,
`ORCHESTRA_OCCUPANCY_SOFT`) — both `rotate()` and `TurnDecision` request a
handoff once occupancy crosses `Soft`, before `Hard` forces one;
`TurnDecision` returns `prepare_handoff` advisory (task stays leased, no
turn boundary required).
- **Agent-initiated ROTATE**: `handoffReason(worktree)` reads a written
`.orchestra-handoff.json`; if `reason == "manual"`, both `rotate()` and
`TurnDecision` skip occupancy and the turn-boundary probe entirely and
release immediately — the agent's own handoff *is* the boundary signal.
Shared release-and-certify tail extracted into `Coordinator.finishRelease`
so this path gets the same anchor-safety guarantee as the threshold path.
- **Milestone + thrash**: needed a transcript/tool-call introspection source
this repo didn't have — built new, `internal/herdr/activity.go`:
- `ToolCall{Name, Kind, Key, Success, IsTest}` normalizes one tool/function
call across harnesses.
- `ClaudeActivity` pairs `tool_use`/`tool_result` blocks by `tool_use_id`
from the transcript already opened for occupancy (an unresolved
tool_use — mid-turn — is dropped, not reported).
- `CodexActivity`**verified against a real live rollout, 2026-07-28**,
after the original guessed `function_call`/`function_call_output` shape
turned out not to exist anywhere in `~/.codex/sessions`. Real shape:
file edits arrive as `event_msg`/`patch_apply_end` (has `changes` +
top-level `success` directly, no pairing needed); shell commands arrive
as a freeform `response_item`/`custom_tool_call` named `"exec"` whose
`input` is a JS snippet — `codexExecCommand` regex-extracts the first
embedded `cmd:"..."`; failure is signaled by the literal output prefix
`"Script error:"` (confirmed for both a JS syntax error and an
`apply_patch` verification failure on this machine's real transcripts).
Manually re-ran the rewritten parser against a real multi-hundred-line
rollout end-to-end and spot-checked the output by eye, in addition to
`TestCodexActivityParsesRealRolloutShape` /
`TestCodexActivityMarksScriptErrorAsFailure`.
- `OpenCodeActivity` **refuses outright** — opencode's on-disk message
storage is only confirmed to carry aggregate token counts, not
per-tool-call records; fabricating a parser against an unconfirmed shape
would repeat the exact mistake this audit exists to catch.
- `DetectThrash(calls, ThrashConfig)` implements all three rules: N
consecutive failed test runs (regex-matched build/test commands), the
same file edited M times (edit tools only, explicitly excluding `Read`
caught by an initially-failing test), and an identical tool call
repeated K times back-to-back (excluding test re-runs and file reads —
also caught by an initially-failing test). Defaults 3/5/4, overridable
via `Coordinator.Thrash`.
- `DetectMilestone(calls)` — deliberately narrow: only "the most recent
call was a successful `git commit`." Fuzzier definitions (passing test
suite, finished subtask) were not guessed at.
- `CLIAdapter.Activity` resolves the session file the same way `Occupancy`
does; `ActivityReader` is an optional Face-B capability like the others.
- New `ReasonedHandoffRequester`/`CLIAdapter.RequestHandoffReason` names
*why* (thrash's dead ends, milestone reasoning) instead of the generic
threshold framing.
- `rotate()`/`TurnDecision` generalize "reason=manual bypasses occupancy"
to `manual`/`milestone`/`thrash` alike: `checkActivityTriggers` runs
before falling through to occupancy/soft/hard (thrash takes priority
over milestone), and a hit calls `requestReasonedHandoff` — request
only, never release, same as the soft-threshold path.
Covered by `internal/herdr/activity_test.go` and
`internal/orchestrator/rotation_test.go`'s
`TestActivityTriggersRequestReasonedHandoffWithoutReleasing`,
`TestTurnDecision` subtests for soft-threshold and manual-reason, and
`TestRotationRequestsHandoffBeforeReleasing`.
**Known remaining limitation:** `DetectMilestone`'s single-command
extraction only sees the *first* `cmd:"..."` in a chained Codex exec script,
so a `git commit` issued later in the same script isn't recognized as the
"last call" even though it executed last. Opencode still has no verified
per-tool-call source at all.
### Also closed this pass (not separately numbered in the original audit)
- **Codex/opencode Stop-hook-equivalent scripts.** Neither harness has a
native Stop hook, so the server-side `/v1/harness/turn` and
`/v1/harness/complete` (harness dispatch) existed with no caller for
either. Added `deploy/hooks/orchestra-codex-poll.sh` and
`orchestra-opencode-poll.sh` — background poll loops (default 60s,
`ORCHESTRA_POLL_INTERVAL`) that find the newest session-state file (codex:
newest `rollout-*.jsonl`; opencode: newest file under
`~/.local/share/opencode/storage/message/`), POST to `/complete` when
`.orchestra-report.md` exists, otherwise POST to `/turn` and log (not act
on) `refuse`/`rotate_now` — advisory-only, since there's no real turn
boundary to refuse *at* from outside either harness process without deeper
app-server/SSE integration.
- **Bus-level authorization.** `authz.AuthorizeEvent` enforced inside
`store.Append` itself (the single choke point every event passes
through), not just at HTTP handlers. Event schema bumped to v2, requiring
every event to declare a `Surface`; schema v1 events on disk still replay
(tolerant reader).
- **Dual quota windows.** `router.QuotaAvailability` tracks a 5-hour rolling
window and a 7-day weekly window independently per harness, applying the
conservative 80% rule to each separately.
- **Turn-boundary detection made observable.** An adapter that fails to
answer `TurnBoundary` now blocks that tick's release rather than treating
the failure as "safe to proceed," and increments
`MonitorHealth.TurnBoundaryDegraded`.
- **Cross-machine lease correctness has a primitive-level test.**
`TestCrossMachineLeaseAnchorAndQuotaArePerHost` exercises a lease claimed
through the federation worker HTTP API, validates the anchor against that
worker's own local checkout, and asserts quota is accounted per-host —
proof for the primitives that exist today; does not yet run against two
real physical machines (see federation fork, below).
- **Fuzz coverage** (`internal/domain/fuzz_test.go`,
`FuzzValidatePayload`/`FuzzValidateEvent`) for all event types including
malformed nested payloads — no panic, always a typed error.
- **Multi-repo Gitea ingestion.** `provider.Gitea` gained a `Project` field
and namespaced `SourceName()`; `provider.MultiGitea` dispatches by
`task.Source`; `LoadGiteaConfigs` loads a JSON array of per-project
sources. Legacy single-repo env vars still work unchanged. Previously zero
tests existed for the Gitea provider at all (an earlier progress.md claim
of coverage was inaccurate) — `internal/provider/gitea_test.go` added.
- **Per-project repos.** `registry.Project` gained optional `repo`/
`worktree_root`; `main.go` builds a `PerProjectGitWorktrees`, falling back
to the global default for any project that omits these fields.
- **Rotation's `TaskReleased` payload validity.** `Coordinator.rotate` used
to build `{"handoff_ref","reason"}`, omitting the `anchor_sha` the spec and
`domain.ValidatePayload` require whenever `handoff_ref` is present —
`store.Append` would reject it, the error was discarded
(`if c.Store.Append(e) == nil`), and rotation silently never happened, no
visible failure. Fixed: `herdr.HeadSHA(worktree)` populates `anchor_sha`
from the real worktree HEAD before appending; if the anchor can't be read,
rotation now skips that tick instead of emitting a payload guaranteed to
fail validation. The federation worker release endpoint
(`/v1/federation/workers/{id}/release`) had the identical gap and now
requires/forwards a 40-hex-char `anchor_sha`, rejecting with 400
otherwise.
---
## The federation fork — decided 2026-07-27, still the deferred half
Two incompatible federation designs coexist in the tree.
**Design A — "drive the remote socket" (currently deployed,
`clients/herdr-bridge.go`)**: homesrv calls `worktree.create`/`agent.start`/
etc. directly on workpc's herdr over TCP as if it were local — meaning
anchor validation (`git rev-parse HEAD`) executes on the *wrong machine*
relative to the actual checkout. Two outcomes if the coordinator's
`session.Worktree` path happens to also exist on homesrv (likely, since
every project shares the same directory layout): either `HeadSHA` errors and
rotation silently skips forever, or — the dangerous case — it returns
**homesrv's HEAD for an unrelated checkout**, passing validation while
certifying a commit the agent never touched. Same class of bug applies to
per-host quota accounting and to `cleanupCompleted`'s `git worktree remove`,
which runs on homesrv for a worktree that lives on workpc.
**Design B — "workers pull tasks" (`/v1/federation/*`)**: fully built
server-side (registration, heartbeat/TTL offline detection, event-cursor
polling/ack, lease claim), zero clients — no worker binary exists anywhere
in this repo. This is what the spec actually describes (§2.1: "everything
crossing a machine boundary is git + a validated artifact, never live state
over the wire"), but every endpoint is currently unreachable in the real
deployment. Latent, never-surfaced defects in this unused half: offline
detection only runs inside `Snapshot()`, called solely from a GET endpoint —
nothing ticks it on its own, so `OnOffline` (the hook that releases leases
held by a vanished worker) only fires if a human hits that endpoint; the
registry is in-memory with no persistence, so a restart forgets all workers
and cursors.
**Decision, unchanged:** keep Design A through Phase 5 (single-host concerns
— occupancy, Face B, rotation, continuity — are provable on homesrv alone
with workpc's herdr as just another pane host); commit to Design B in Phase
6. Two guardrails were meant to land immediately so Design A can't corrupt
state in the meantime: (1) refuse to rotate a lease held by a non-local
herdr rather than validate against the wrong checkout, since the protocol
schema can't run `rev-parse` where the checkout is; (2) same treatment for
`cleanupCompleted`'s worktree removal. **Status of these two guardrails is
unverified in this pass** — re-check `internal/orchestrator` before assuming
they landed; they are not confirmed closed above the way B1B11/S1S11 are.
If the worker binary is ever abandoned, delete `/v1/federation/*` and record
the deviation — leaving both designs in place unmarked is explicitly not
acceptable; the repo would read as though the spec-conformant path is
implemented when nothing can reach it.
---
## Live deployment facts (as of 2026-07-28)
- Runs as `orchestra.service` on **homesrv** via this machine's own systemd.
`homesrv` has no local herdr running (connection refused on 9245) — only
`workpc`'s herdr (`192.168.1.105:9245`) is live and reachable. `main.go`
only logs herdr connection *failures* at startup, never successes, so "no
log line" for a herdr does not mean it's down.
- **Stuck task, deliberately left untouched:** workspace `wA`, task
`06FT6CKD9Y98AZRX6X8K3QXFZG`, opencode harness, pane `wA:p1`. From
Orchestra's own point of view it is no longer "stuck" — it now reads
`state: "failed"` (retries exhausted, `MaxAttempts` hit before B4's fix
landed). But the underlying herdr pane is still live and
`agent_status: "blocked"`, confirmed via a direct `agent.get` probe — the
router gave up and moved on, but nothing ever released or killed the
actual agent, confirming the orphaned-pane prediction from B2/B5. Left
untouched on purpose: user was asked and chose to leave it rather than
have it cleaned up mid-audit. Do not call `pane.close`/`pane.release_agent`
against it without asking again first.
- First live end-to-end task run (2026-07-28, after rebuilding/redeploying —
the running service had been on stale pre-audit commit `325c684`) is what
surfaced **B12** above, since fixed and confirmed live; re-verifying it
surfaced **B13**, still open.
- **Three more live orphaned panes from this pass, left untouched:** `wD:p1`
(task `06FTAKSJZTB73FZQE3QT7XQ1J0`, worktree
`/tmp/test-e2e-worktrees/06FTAKSJZTB73FZQE3QT7XQ1J0` — has a real attached
`claude` session but the task itself is stuck `blocked` from before B12's
fix landed), `wE:p1` and `wF:p1` (tasks `06FTAMFKVEPGPKR0H86C08ZZNG` /
`06FTANAYZPA55BABR2J4MWQ050` — empty shells, no agent ever attached, see
B13). Same rule as `wA:p1`: don't `pane.close`/`pane.release_agent` any of
these without asking first.
---
## Known open gaps (as of 2026-07-28)
- **B13** (above) — `agent.start` can silently no-op under back-to-back
leases, with no error surfaced; blocks reliable live verification of
everything downstream (occupancy, rotation, handoff, completion) against a
real task, even though B12's readiness race is now fixed.
- **Cross-machine lease correctness proven at the primitive level only** —
needs an actual homesrv/workpc pair over the real mesh; this repo cannot
exercise that by itself.
- **Turn-boundary Face B degrades to occupancy-only** for adapters that
don't implement it, by design — degradation is observable
(`MonitorHealth.TurnBoundaryDegraded`) and blocks-on-failure, but whether
Claude/Codex/opencode's native hooks are wired in a live deployment is a
deployment-config fact, not provable from source alone.
- **Quota has no live push producer** — only the post-hoc
`/v1/harness/complete` producer exists; see B7's "design consequences" for
the (unbuilt) Claude statusline / Codex rollout-tail feeds.
- **Federation guardrails' landed-status is unverified** — see federation
fork section above.
- **opencode has no verified per-tool-call activity source** and no
first-class quota surface (Zen headers only, would need a plugin) —
both named, not attempted, in S11/B7.
- **`DetectMilestone`'s single-command extraction** can miss a Codex commit
issued later in a chained exec script.
Everything else audited (provider layer, continuity schema/validation,
router matching, delivery, federation registration primitives) was
spot-checked against the code and its tests and matched described behavior.
---
## Handoff provenance correction (2026-07-28)
The previous rotation path had a **primacy inversion**: the harness wrote a
free-form Markdown “semantic report”, while `canonicalHandoff` fabricated
the canonical fields around it (`goal`, `done_when`, a circular action and a
`cat` command) and placed the entire report in one `remaining` element. That
made the apparent schema a wrapper around prose rather than an authoritative
handoff.
The release path now asks the harness for only six labelled, bounded answers:
`NEXT`, `WHY`, `REMAINING`, `DEAD ENDS`, `OPEN Q`, and `LEARNED`, each with a
`NONE` escape. `canonicalHandoff` parses those answers and derives only
worker-owned facts (Git anchor, dirty state, metadata, and last observed
harness command). It never invents task intent. `goal` and `done_when` were
removed from `continuity.Handoff`; pickup must obtain task scope and success
criteria from immutable `TASK.md`.
`continuity.Handoff.Validate` is the shared semantic gate for both release
and pickup. Strict decoding rejects removed/unknown fields, and validation
rejects circular handoff actions, commands that point at report/handoff
files, oversize or Markdown-smuggled authored list items, and malformed dead
ends. A failed release remains refused and is retried by the existing
rotation loop; a bad artifact that somehow reaches CAS is also refused at
pickup. The fuzzy “empty dead ends after a non-trivial later rotation” signal
is intentionally not yet enforced: no reliable diff-size/rotation-index
evidence is available at this validation boundary, so inventing a hard rule
would create false refusals.
Verified after the change with `go test ./...`, `go build ./...`, `go vet
./...`, and `git diff --check`.