fix(orchestrator): wire TASK.md writing and §6.2 pickup validation (B6, partial)

Fixes AUDIT.md's B6: nothing wrote a TASK.md into a worktree, so
continuity.ValidatePickup had no caller and no file to check.

- continuity.RenderTaskFile/TaskFileHash: render and hash the immutable
  §6.2 TASK.md from a domain.Task.
- GitWorktrees.Create writes and commits TASK.md into every freshly
  created worktree (must be committed, not dirty, for ScratchCommit's
  immutability check and for a stable hash).
- Coordinator.Start now runs continuity.ValidatePickup (anchor SHA,
  dirty-file hashes, TASK.md hash) against the real worktree before
  bootstrapping a successor onto a handoff_ref, and blocks the task
  instead of bootstrapping on a validation failure.

Still open from Phase 4: handoff production (agent writing the real
handoff; Release still refuses per B5), ScratchCommit wiring before
release, and the §6.2 bootstrap-prompt rewrite — see AUDIT.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-27 21:44:00 +04:00
parent a2c3d040d3
commit 7bcad64398
7 changed files with 289 additions and 3 deletions
+59 -2
View File
@@ -558,13 +558,70 @@ handler test coverage of any kind (everything is inline in `main()`), so this
follows the existing gap rather than introducing an isolated test harness for
one handler.
**Fixed 2026-07-27 (later same day):** `CLIAdapter.Lease`'s bootstrap prompt now
passes `time.Minute` instead of `wait=0`, matching `Bootstrap`'s inline-wait
pattern — closes the race B5 named (send-into-a-half-rendered-prompt).
`internal/herdr/adapter.go`. `go build`/`vet`/`test` all still pass; no
existing test asserted the old `wait=0` value.
**Still open from B5** (not attempted this pass — larger, needs design, not
just a method-name swap):
- `agent.prompt` inline `wait` on `CLIAdapter.Lease` (still `wait=0`, per B5's
note that only `Bootstrap` passes a real wait).
- `Release`'s real implementation, which depends on Phase 4 (§6) handoff
production existing at all.
- The stuck live task (`06FT6CKD9Y98AZRX6X8K3QXFZG`) was deliberately **not**
manipulated directly (no `pane.close`/`pane.release_agent` call against it)
— killing or releasing a real running agent from an audit session without
the user present is exactly the kind of action that warrants asking first.
## B6 — partial fix, 2026-07-27 (Phase 4 items 1 and 4)
Two of Phase 4's six items landed; the rest are unchanged (still open, listed
below).
1. **TASK.md is now actually written.** `continuity.RenderTaskFile(t)`
produces the §6.2 immutable spec content from `domain.Task`;
`GitWorktrees.Create` (`internal/orchestrator/orchestrator.go`) writes it
into every freshly created worktree and commits it immediately — it must
be committed, not left dirty, both so `ScratchCommit`'s "TASK.md is
immutable" check (which inspects `git status`) sees it as clean, and so
its hash is stable across whatever the agent does afterward. A worktree
that already has a `TASK.md` (recreation on restart) is left untouched.
New `continuity.TaskFileHash(root)` reads it back and hashes it — this is
what makes `w.TaskFileSHA`/`VerifyTaskFile`, previously dead because
nothing ever set `TaskFileSHA`, actually reachable.
2. **Pickup validation now gates bootstrap.** `Coordinator.Start`
(`internal/orchestrator/orchestrator.go`) computes the new worktree's
`TaskFileHash`, and — whenever the lease carries a `handoff_ref` (i.e.
this is a rotation continuation, not a fresh lease) — loads the handoff
from CAS and runs `continuity.ValidatePickup(worktree, handoff,
taskFileSHA)` **before** calling `Adapter.Bootstrap`. A validation failure
kills the just-started session and emits `TaskBlocked` instead of hand­ing
the successor an unverified anchor. This is exactly the gap B6 named:
`ValidatePickup` had no caller outside its own tests.
`TestStartBlocksOnInvalidPickup` (`rotation_test.go`) drives this against
a handoff whose anchor SHA doesn't exist in the repo and asserts the task
ends `Blocked`, never bootstrapped.
`TestGitWorktreesCommitsTaskFile` (`worktrees_test.go`) asserts the
written/committed content matches `RenderTaskFile` and survives
recreation.
Caveat recorded in code: TASK.md hashing is best-effort — if a worktree came
from the `WorktreeCreator` (herdr-hosted, potentially remote) path rather
than `GitWorktrees`, `TaskFileHash` fails silently and pickup validation runs
with an empty `taskFileSHA` (so it still checks anchor SHA and dirty-file
hashes, just not TASK.md). No current adapter or test exercises
`WorktreeCreator` with a real `handoff_ref`, so this is unverified, not
proven safe — same cross-host caveat as the federation-fork section.
**Still open from B6/Phase 4** (unchanged, larger and needs live-agent
cooperation):
- Item 2: handoff *production* — the rotating agent writing the §6.1
handoff and a stop-hook path uploading it via `POST /v1/artifacts` before
`Coordinator.rotate` calls `Adapter.Release`. `Release` still just refuses
(see B5 above) — there is nothing yet to validate-and-mint a ref from.
- Item 3: `ScratchCommit` before release — not wired into `rotate` at all.
- Item 5: `CLIAdapter.Bootstrap`'s prompt is still ad hoc prose, not the
§6.2 ~200-token procedure (read handoff → validate-handoff → re-read
TASK.md → proceed).
- Item 6: `MarkdownChanges` (§6.3 adjacent-task notice) still uncalled from
anything but its own test.