Hand off with run 5 completed and the plan change designed

Ledger current through F42. Handoff records the two defects found in the
completion tail, the deployed revisions, and the two links the tail still
cannot reach because test-e2e has no gate and no delivery configuration.

PLAN-SPEC-DESIGN.md is the operator-approved design for the ACE-FCA plan gap,
committed as a note. No implementation yet, by instruction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-28 06:58:48 +04:00
parent dcd9af4806
commit d9a5a61965
3 changed files with 499 additions and 0 deletions
+74
View File
@@ -1716,3 +1716,77 @@ Fixed in the phase brief: the terminal phase now states the marker, when to
write it, and that it is exclusive with asking to go back.
`TestTerminalPhaseNamesTheCompletionSignal` also asserts the negative, so a
phase that can still ask is never told to finish instead.
### F41: harness chrome counted as agent progress
Run 5's review agent produced nothing after 02:12. The 02:46 renewal was
granted anyway, and the lease moved to 03:16. Each renewal window bought
another 30 minutes, so recovery by expiry had no bound.
Reproduced the worker's stored `progress_sha` byte for byte from the live pane,
so the branch taken at `cmd/orchestra-worker/main.go:1020` was
`progress != l.ProgressSHA`. Not `IsBusy`: the 200-line capture held zero
`esc to interrupt` markers, so `AgentStatus` returned `idle`. Not an empty
baseline either: it held `3d93ca89`.
`PaneProgress` dropped prompt lines only, so Claude Code's status footer stayed
in the digest. The mutable fields there are the rolling usage percentage, the
context counter, and the version notice. A 180-second sample showed no churn, so
this leaked intermittently rather than constantly. One tick inside a window was
enough.
Fixed in `2417a39`. `PaneProgress` cuts from the editor's lower rule and then
trims the spinner summary and version notice above it. `AgentStatus` still reads
the raw capture, so the busy markers living in the footer are unaffected.
Proven live in both directions on the first window after deployment: the
03:13:48 renewal took version 28, the 03:16:13 window was refused, the lease
expired at 03:18:43, and review relaunched at 03:20:14 with the F40 brief. The
relaunched pane then showed a new footer field, `5h: 15%`, which the old digest
would have counted as work.
Lease TTL moved to 5 minutes in the same commit, from `domain.LeaseTTL`, with
renewal at half of it. Reclaiming a stalled pane happens only at expiry.
### F42: the result commit refused the completion marker
The relaunched review agent wrote `.orchestra/done` at 03:20:48, 34 seconds
after launch. The worker recognised it, confirmed the agent idle, and then
failed the result commit once every five seconds:
```text
stage result: The following paths are ignored by one of your .gitignore files:
.orchestra/done
```
`internal/herdr/adapter.go:132` writes `.orchestra/.gitignore` containing `*`,
so the marker is ignored. The staging step passed `:!.orchestra/done`, and git
refuses an add whose pathspec names an ignored path. Reproduced in a scratch
repo on git 2.55.0, both the failing and the working form.
Fixed in `dcd9af4`: the exclusion names the directory, `:!.orchestra`. That
holds whether or not the inner `.gitignore` exists.
The failure retried for 22 minutes with the task stuck in review and nothing
observable outside the journal, because each identical error overwrote the
single `last_error` slot. F18, again, and the second time in one night that it
turned a five-second loop into archaeology.
### Run 5 reached TaskCompleted
On the first tick after the `dcd9af4` restart at 06:47:50:
- task state `completed`, lease released, `.orchestra/done` removed
- pane closed, no `orchestra-*` sessions left on the tmux socket
- result branch pushed:
`refs/heads/orchestra/scratch/orchestra-06g4a4f0tfxkzhje48n05xn1hg-73a2e4fd`
at `fb15c61`
Two links of the tail did not run, and neither is a defect. The deployed
`test-e2e` project declares only `id`, `machine_affinity`, `repo` and
`worktree_root`, so no quality gate exists to run. The deployed `config.jsonc`
has no `sources` and no `delivery` keys, so no PR could be opened.
Proven chain: review, done recognised, finalise, result branch pushed,
`TaskCompleted`. Submission, human PR review and merge remain unproven, and they
need registry configuration rather than code.
+176
View File
@@ -0,0 +1,176 @@
# Handoff: run 5 reached TaskCompleted, plan machinery is designed and frozen
Written 2026-08-28, 06:55 local (02:55 UTC). Read with `BURNIN.md` (the ledger,
now current through F42), `PLAN-SPEC-DESIGN.md` (the locked design for the next
change), `HANDOFF-2026-08-28-burnin-4.md` (the session before this one),
`AUDIT.md` and `CLAUDE.md`.
Everything below was observed live unless it says otherwise.
## The headline
Run 5 finished. Task `06G4A4F0TFXKZHJE48N05XN1HG`, issue `kami/test-e2e#5`,
reached `TaskCompleted` at 06:48 after two defects were found and fixed in the
completion tail.
```text
frame -> research -> plan -> implement -> review -> done -> finalise -> pushed -> completed
```
Two defects, both live-proven fixed:
- **F41**: harness chrome counted as agent progress, so a stalled lease renewed
without bound.
- **F42**: the result commit refused the completion marker, so completion could
never happen.
## Deployed state
| Half | Revision | Evidence |
|---|---|---|
| Worker, workpc systemd | `dcd9af4` | PID 3634937, started 06:47:50, sha256 `25b41562…`, journal prints the revision at startup |
| Coordinator, homesrv container | `bcaf0cc` | **stale**, see below |
Branch `webui-and-audit-reconciliation`, HEAD `dcd9af4`.
```text
revision dcd9af4806376caf091e18f669ab52f1ba0dc9df
sha256 25b415629d4a02dca7abe202245ac13f5bdb43b501bc17e371dba75b44f6cebb
```
**Confirm the running worker, not the installed file.** Two restarts silently
did not happen tonight. `sudo install` landed the new binary while the old
process kept serving from the unlinked inode. `/proc/<pid>/exe` showed
`(deleted)` and the old hash both times. Check
`sha256sum /proc/$(systemctl show -p MainPID --value orchestra-worker)/exe`,
never just the path.
The second missed restart cost run 5 its retry budget: the pre-F42 worker kept
failing until the router marked the task `failed`.
## The coordinator is behind, deliberately
`2417a39` moved lease TTL to 5 minutes via `domain.LeaseTTL`, and the
coordinator issues the initial lease at `internal/router/router.go:318`. That
half still runs `bcaf0cc`, so new leases arrive with a 30-minute TTL. Worker
renewals send `ttl=300` explicitly, which is why run 5 recovered anyway.
Rebuild when convenient, from a detached worktree, never with
`docker compose build`:
```text
git worktree add --detach /tmp/orchestra-build-dcd9af4 dcd9af4
docker build --build-arg BUILD_REVISION=dcd9af4… -f $T/Dockerfile.api -t orchestra-web-ui-orchestra-api $T
docker compose up -d --no-deps orchestra-api
```
## What this session found
### F41, harness chrome counted as progress
The 02:46 renewal was granted against a pane idle since 02:12. Reproduced the
worker's stored `progress_sha` byte for byte from the live pane, so the branch
taken was `progress != l.ProgressSHA`, not `IsBusy` and not an empty baseline.
`PaneProgress` hashed Claude Code's status footer, whose usage percentage,
context counter and version notice all move on their own schedule.
Fixed in `2417a39`. Live-proven in both directions on the first window after
deployment: renewal at 03:13:48, refusal at 03:16:13, expiry at 03:18:43,
relaunch at 03:20:14.
### F42, the result commit refused the completion marker
`.orchestra/.gitignore` contains `*`, so `.orchestra/done` is ignored, and git
refuses an add whose pathspec names an ignored path. The staging step named the
marker. Every completion attempt failed, once every five seconds, for 22
minutes.
Fixed in `dcd9af4` by excluding the directory rather than the file. Reproduced
both forms in a scratch repo on git 2.55.0 before changing anything.
### F18 is now the clear top item
Both defects tonight were five-second retry loops whose only trace was the
journal, because each identical error overwrote the single `last_error` slot.
`health` in the worker state file was `{}` throughout. The bounded
recent-observation ring described in the previous handoff is the fix. This
session adds a second concrete instance of the cost.
## Where the completion tail stops
Proven: review, done recognised, agent confirmed idle, finalise, result branch
pushed, `TaskCompleted`.
Not proven, and not defects:
- **No quality gate ran.** The deployed `test-e2e` project declares only `id`,
`machine_affinity`, `repo` and `worktree_root`.
- **No PR was opened.** The deployed `config.jsonc` has no `sources` and no
`delivery` keys. `internal/provider/gitea_pr.go` implements PR creation, but
nothing points this project at a Gitea remote.
Submission, human PR review and merge need registry configuration, not code.
That is the cheapest remaining burn-in, and it is a config edit plus one task.
## The next change is designed and frozen
`PLAN-SPEC-DESIGN.md` holds the operator-approved design for the ACE-FCA plan
gap. No implementation exists yet, by explicit instruction: the design must not
land while the completion tail is still being observed.
Shape, in one paragraph each:
- **`plan.md`**, sealed through the existing path and referenced by the existing
`t.PlanRef`. 128 KiB total bound, structural validation by required headings
and phase blocks, no per-line cap. `DecodePlan` keeps the JSON path for refs
already in the CAS.
- **Research ids.** `Finding{ID, Claim, Evidence, Confidence}`, unique ids,
confidence limited to `fact|inference|assumption`. The schema at
`agentctx.go:167` already promises these and the struct drops them today.
- **Progress that Orchestra establishes.** The implementer may write only
`status: "ready_for_verification"`. The worker reads that phase's automated
verification from the accepted plan, runs it, and records
`PlanPhaseVerified`. An agent cannot write `verified`.
- **Verification allowlist.** Plan commands are argv, executed without a shell,
matched against a per-project policy. Absent policy refuses everything.
- **Mismatch as a typed request.** `PlanMismatchRequest` supersedes or reopens
research and planning. No new agent-askable backward transition.
The live proof ladder is in the note. Steps 1 and 3 are the rungs that matter
for smaller local models. The implement launch contains the plan verbatim, and
a rotated successor still receives it whole with phase state intact.
## Things that will bite
- **Verify the running worker binary, not the installed one.** Twice tonight.
- **The worker state file is a cache, not truth.** It read `state failed` for
run 5 while the coordinator still held the task as leasable. I reported the
cache as fact. The coordinator disagreed and was right.
- **`rm` and `cp` are interactive in this shell.** A plain `rm` prompts, appears
to succeed, and leaves the file. Use `/bin/rm -f` and `install`.
- **Another session owns 20-plus uncommitted paths**, including `AUDIT.md`,
`deploy/build.sh`, `web/` and `internal/authn/`. Commit by path. Never
`git add -A`.
- **No network from this sandbox.** `curl` to raw.githubusercontent.com times
out. `WebFetch` against a `github.com/blob/...` URL works.
- **Secrets are guarded.** Reading `.env` or `printenv` of a token through the
shell is blocked. The working recipe is expanding the token inside the
container in a single remote command, as `HANDOFF-2026-08-28-burnin-4.md`
records.
- **`/tmp` is tmpfs with a 10-day sweep.** Check `/tmp/test-e2e` and
`/tmp/test-e2e-worktrees` exist before every run.
- **Two stale monitors** may still be armed from this session, tailing the
worker journal. They are session-local and die with it.
## What I would do next, in order
1. Rebuild `orchestra-api` at `dcd9af4` so the 5-minute TTL applies at issuance.
2. Fix F18 as a bounded observation ring, since two more instances landed
tonight.
3. Configure `test-e2e` with a Gitea remote and a quality gate, then run one
task to prove submission, human review and merge.
4. Implement `PLAN-SPEC-DESIGN.md` exactly as written, including the allowlist.
Steps 1 through 3 are burn-in and observability. Step 4 is the only new
behaviour, and its design no longer needs an architecture pass.
+249
View File
@@ -0,0 +1,249 @@
# Locked design: plan.md, durable plan state, verified progress
Written 2026-08-28 while run 5's completion tail is still open. **No
implementation until run 5 reaches a terminal outcome.** This file is a note,
not code, and is deliberately uncommitted.
Approved by the operator with three amendments folded in. Markdown plan with
structural validation, stable research finding ids, and progress that Orchestra
establishes rather than the implementer asserting.
The project-level verification allowlist was added on the second pass.
## Ground truth this design starts from
Verified in the tree at `2417a39`, not assumed.
| Concern | Where | Behaviour today |
|---|---|---|
| Research artifact | `internal/workphase/workphase.go` | `Findings{Claim,Evidence}`, 64 items, 500 chars, single line |
| Plan artifact | same | `Changes{Target,Intent}`, `Verification[]`, `Risks[]`, `DecisionsNeeded[]`, same bounds |
| Advertised research schema | `internal/agentctx/agentctx.go:167` | promises `findings[].id` and `findings[].confidence`; struct has neither, both dropped silently |
| Seal path | `cmd/orchestra-worker/main.go:1833` | worker decodes, refuses locally through `answerRefusedPhase` (F39), then `AdvancePhase` |
| Storage | `internal/store/store.go:961`, `cmd/orchestra/main.go:467` | content-addressed CAS, format-agnostic, 4 MiB body cap |
| Refs | `internal/store/store.go:282` | reducer sets `t.ResearchRef` / `t.PlanRef` |
| Implement launch | `internal/agentctx/agentctx.go:418` | `renderSealed` flattens every field through `collapse()` to one bullet line |
| Plan progress | none | no state exists; `workphase` doc points at `continuity.Handoff` |
| Rotation progress | `internal/continuity/continuity.go:113` | `Remaining[]`, `Action`, `Learned[]`, 200 chars per line, unlinked to any plan item |
| Phase graph | `internal/domain/workphase.go:35` | `implement → review` only; no route back |
| Quality gate execution | `cmd/orchestra-worker/main.go:955` | `exec.CommandContext(ctx, "sh", "-c", gateCommand)`, `Dir = worktree`, operator-authored string |
The last row is the security baseline. The gate command is operator-authored,
from the project registry or the task. A plan-authored command is agent-authored,
so it must not inherit that envelope unchanged.
## 1. Sealed specification: `plan.md`
- Markdown, sealed through the existing path, referenced by the existing
`t.PlanRef`. No new storage, no new event type for sealing.
- Bound: 128 KiB total. No per-line cap. The 500-character single-line rule is
what makes a specification impossible today.
- `phaseSealFile[plan]` and `phaseArtifact[plan]` become `plan.md`.
- `DecodePlan` keeps the JSON path for refs already in the CAS. New seals must be
markdown.
- `phaseSealSchema[plan]` states the required outline. Run 5 proved the planner
follows that block (F38), so it is the delivery mechanism for the structure.
Required sections, validated by presence and shape:
```
# <name> implementation plan
## Overview
## Current state
## Desired end state
## Non-goals
## Approach
## Phase <n>: <name> (one or more)
### Files
### Changes
### Verification
#### Automated
- run: ["go", "test", "./internal/foo/..."]
#### Manual
- <human-testable step>
## Testing strategy
## Risks and edge cases
## Migration
## References
```
Validation rules:
- At least one phase block. Phases numbered from 1, contiguous.
- Every phase carries `### Files`, `### Changes`, `### Verification`.
- Every phase carries at least one entry under `#### Automated` or
`#### Manual`. A phase with neither cannot be verified and is refused.
- Automated entries are argv arrays, JSON-parsed from the `run:` line. Not
shell strings. See section 4.
- `## References` entries cite research ids. An id that does not resolve against
the sealed research fails the seal.
## 2. Research ids
- `Finding{ID, Claim, Evidence, Confidence}`.
- `ID` required, unique within the artifact, matching `^r[0-9]+$` or a stable
slug.
- `Confidence` limited to `fact|inference|assumption`.
- The advertised schema at `agentctx.go:167` becomes true rather than
aspirational.
- `renderSealed` prints the id with each finding, so a plan can cite
`research:r12` and a reader can resolve it.
- Plan seal validation resolves every reference. The coordinator holds
`ResearchRef`, so the cross-check costs one CAS read.
## 3. Progress: request, then Orchestra verifies
The implementer may write exactly one status.
```json
{ "phase": "phase-2", "status": "ready_for_verification" }
```
- Any other status value is refused, and the refusal is delivered to the live
session through the F39 path. `verified` is not writable by an agent.
- Worker reads that phase's `#### Automated` entries from the accepted plan, not
from the request.
- Worker runs each argv, in order, in the worktree, capturing exit code and
output.
- All pass, manual steps exist: phase becomes `awaiting_manual_verification`.
- All pass, no manual steps: phase becomes `verified`.
- Any fail: phase stays `in_progress`, and the failing command plus its output
reaches the implementer.
Durable record:
- New event `PlanPhaseVerified{phase, commands, exit_codes, at_sha,
evidence_ref}`.
- Reduced into a task field `PlanProgress`, so state is derived from events
rather than from a worker cache.
- Manual sign-off arrives as a human decision, on the surface decisions already
use. No new approval mechanism.
Rendering, in `agentctx`:
- Plan state sits above continuity and below human decisions.
- It names verified phases, the current phase, and any awaiting-manual gate.
- The plan document itself renders verbatim. `collapse()` stays for research
bullets only.
## 4. Verification-command allowlist
The rule, operator's wording:
```
accepted plan command
∩ existing quality-gate execution envelope
∩ project verification policy
= runnable
```
Properties this must hold:
- No weaker sandbox than the existing quality gate.
- No implicit `sh -c`. Commands execute as argv through `exec.CommandContext`,
with `Dir` set to the worktree, exactly as the gate sets it.
- Shell operators, pipes, and redirections are unavailable by construction,
since there is no shell. A project that genuinely needs one declares it in
policy, explicitly.
- Refusal is observable and delivered to the implementer.
- A rejected verification never becomes `verified`.
- Project policy, not the plan, determines executable reach.
Policy shape, on `registry.Project`:
```json
"verification": {
"allowed": [
["go", "test", "./..."],
["go", "test", "./internal/..."],
["go", "vet", "./..."],
["npm", "test", "--", "*"]
]
}
```
Matcher, argv-aware:
- Match is positional. `argv[0]` must match exactly.
- A literal pattern element matches that element exactly.
- `*` matches exactly one element, any value.
- A pattern element ending in `/...` matches a path argument under that prefix.
- A pattern shorter than the command does not match unless its last element is
`*`. No prefix-match by accident.
- An empty or absent policy means no plan command is runnable. Absence is a
refusal, not a default-allow.
Refusal text names the command and the project, so the planner learns its real
reach. A plan whose commands are all refused fails at seal time, not at
verification time. The failure lands on the planner rather than the
implementer.
## 5. Mismatch as a typed request
```json
{
"plan_phase": "phase-2",
"observed": "...",
"contradicts": "...",
"evidence": ["..."],
"requested_action": "replan | research | human_decision"
}
```
- Written to `.orchestra/plan-mismatch.json`. Bounded lines, same validator
style as the handoff.
- Worker validates and posts. Coordinator records `PlanMismatchRecorded`, then
decides the move.
- Orchestra performs the reopen. The agent never asks for a backward phase, so
`phase-request.json` still refuses one.
- The superseded plan moves to a history list on the task. `PlanRef` always
names the currently accepted plan, so no ref is ever ambiguous.
- A fresh implement session launches against the newly sealed plan. The mismatch
is retained as provenance.
This keeps the phase machine from becoming freely reversible navigation. The
edge exists for Orchestra, not for the agent.
## 6. Tests
Unit:
- Plan with every required section validates. One missing section fails, and the
error names the section.
- Phase with no automated and no manual verification fails.
- `run:` line that is not a JSON argv array fails.
- Reference to an unknown research id fails, with the id named.
- Duplicate research finding id fails. Confidence outside the enum fails.
- `renderSealed` contains the plan document byte-for-byte, fences and multi-line
blocks included.
- Two `Build` calls from the same refs with different handoffs both contain the
complete plan.
- `status: "verified"` written by an agent is refused, and the refusal reaches
the delivery path.
- Allowlist matcher: exact match, `*` single-element, `/...` path prefix,
shorter-pattern rejection, empty policy rejects everything.
- A failed verification leaves the phase unverified and records the exit code.
## 7. Live proof ladder
1. Plan seals three detailed phases. Implement launch contains the plan
verbatim.
2. Phase 1 requests verification. Worker runs the exact plan command. Durable
state says phase 1 verified.
3. Rotation. Successor receives the complete original plan, phase 1 verified,
phase 2 explicitly current.
4. Human correction lands above the plan. Plan and state stay subordinate.
5. Phase 2 hits a contradiction. Mismatch recorded, no silent implementation,
old plan retained, revised plan sealed, fresh launch carries the revised plan
in full.
Step 1 and step 3 are the two that matter for smaller local models. A grep of
the rendered `launch.md` either finds the phase-three specification and its
verification command, or it does not.
## Not in scope
Lifecycle, leases, review, submission, auth, federation, and the burn-in fixes
stay untouched. One adjacent change is needed: the Orchestra-only reopen edge in
section 5. It is reached through a typed request, not through a new
agent-askable transition.