Burn-in run 3 failed three times with prompt_not_submitted: the launch text
reached the editor every attempt and the following Enter never took effect.
Six isolated probes of the same code path, environment and worktree all
submitted on the first Enter, so the submit is not deterministic and waiting
for it to land is not enough.
F17 first. pendingInput scanned every line beginning with the prompt marker,
but a queued or already-accepted message renders with the same prefix. Only
the editor that owns the pane cursor is unsubmitted input, so inputState asks
tmux for the cursor row and reads the editor around it, joining soft-wrapped
rows.
On that footing ConfirmLaunch becomes an active submit protocol: resend Enter
while the live editor still holds exactly what was submitted, at most three
times and no closer together than two poll intervals, then observe until the
deadline. Queued input confirms rather than fails. The evidence records
confirmation kind, submit_attempts and both timestamps, so a harness that
needs a second Enter is distinguishable from one that needs none.
Verified live against a real Claude Code pane: confirmation=editor_cleared
submit_attempts=1, no spurious resend.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Burn-in run 2 recorded TaskLaunchAcknowledged, opened a pane, and ran nothing
for fifteen minutes. The launch instruction sat in Claude Code's input editor
as "[Pasted text #1 +66 lines]" at zero tokens and zero elapsed. Two separate
bugs produced that.
The transport was wrong for the harness. TmuxBackend.Prompt writes the whole
instruction with send-keys -l and then sends Enter, and the TUI coalesces the
fast multi-line write into a paste that absorbs the following Enter. Launch
transport is now a backend property rather than one universal prompt format:
claude on tmux submits a single line pointing at .orchestra/launch.md, every
other harness keeps the inline path it was verified on. agentctx is unchanged
and the file still holds the exact bytes Orchestra rendered, so what the agent
receives is identical either way. Under the file transport a failed write is
now a failed launch, because there the file is the instruction.
The acknowledgement was also wrong. It meant "Prompt returned nil", not "the
harness accepted the prompt". Backends may now implement ConfirmLaunch, and
the tmux one polls until the input editor clears and the agent is observably
busy, blocked on approval, or at least no longer holding the text. An editor
that still holds the prompt at the deadline is a definite failure. The worker
kills the pane, drops the session so the retry starts clean, and returns
ErrPromptNotSubmitted, which classifies as prompt_not_submitted rather than
launch_uncertain. That class already falls through to TaskReleased, so the
existing retry path takes it and no lease is held on a launch that never
happened.
The confirmation bound is tunable because how fast a terminal harness reacts
is a property of the host. It is not a sleep before the submit: the submit is
deterministic, and this waits for the harness to visibly react to it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The .orchestra directory showed up as untracked work in the very worktree whose
own launch context said "uncommitted changes: false". It would have polluted
the quality gate, the review diff, and the agent's reading of git status. A
.gitignore of "*" inside the directory ignores it including itself. The
worker's done marker lives there too and had the same problem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The v3 stack, previously an uncommitted working tree, plus this session's two
units and the burn-in instrument. This commit is the burn-in build identity:
coordinator and worker must both report this revision before a task is created.
Workflow (earlier sessions, uncommitted until now): human decision events and
reduction, source cursors and reconcile-before-launch, turn-boundary
reconciliation, internal/agentctx as the single renderer, ace-fca phases with
sealed artifacts, the trajectory gate, bounded grilling, independent review,
task pr enforcement, and human review reflection.
Capability restrictions at the agent boundary: an authz.Agent surface at
GatedWrite may ask and may not act. It also fixes two bugs the unit exposed --
gated surfaces could not reach the two endpoints written for them, and
RequestHumanDecision would block an unowned task while rejecting a question
from the session that did own it.
Turn-boundary reconcile-failure escalation: a streak of consecutive failures
asks the session to hand off, fenced on the lease epoch, with reconcile_failure
as a real handoff reason. The worker was dropping the coordinator's verdict on
the floor; it now acts on it.
Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to
<worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md
is the runbook. deploy/build.sh stamps both binaries from one commit.
go build, go vet and go test ./... pass, 20 packages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Introduces the browser-facing surface and the worker-side protocol that
backs it:
- internal/ui: joined read model plus per-task lifecycle and approval
controls, kept separate from the raw endpoints workers and harnesses
depend on.
- internal/webui + web/: Vite/React app, build output embedded via
go:embed and served as an SPA fallback.
- federation: per-(worker, task) captures with a monotonic revision that
advances only when pane text actually changes, and a command queue
restricted to grant_approval / deny_approval, each bound to the capture
revision the operator acted on.
- orchestra-worker: publishes captures and executes commands only after
re-reading the pane and confirming the revision still matches. Sends
keystrokes only for a visible y/n prompt or OpenCode's fully labelled
selector, and refuses to deny through that selector rather than guess
at unobservable navigation.
This is the ownership boundary AUDIT.md's B14 and B17 call for: approval
becomes an explicit, revision-bound operation executed by the worker that
owns the pane, instead of a side effect of prompting over a
coordinator-driven remote socket.
Also ignores the web build inputs and outputs. node_modules ships vendored
Go packages, so go build and go test walk into it if it is merely
untracked; both node_modules and .node_modules are excluded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01535A3Y8RtkAi8wYuWhtkEd
herdr hands a freshly created pane/agent back before it's actually ready,
and rejects the very next call with a range of different transient errors
("not an available shell", "not an active named agent", "target ... not
found") depending on timing. String-matching each wording as it turned up
live proved unwinnable across three live redeploy-and-test rounds, so
StartAgent and Prompt now retry any error for up to 15s (bounded by
wall-clock time, not attempt count) rather than pattern-matching herdr's
error text.
Confirmed live against workpc: a fresh lease (wD:p1) now reaches a real
attached claude session instead of failing before the agent starts.
Live testing also exposed a second, separate defect (B13, documented in
AUDIT.md, not fixed here): agent.start can return success while never
actually starting an agent when two leases land close together, with no
error for a retry to catch. Left three test panes on workpc untouched
(wD:p1, wE:p1, wF:p1) pending manual cleanup, per the standing rule against
destructive herdr calls without asking first.
Also folds in the already-flattened AUDIT.md/progress.md merge that was
staged ahead of this session's changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
The previous function_call/function_call_output shape was never verified
and doesn't exist in any real Codex rollout. Confirmed the real shape
against this machine's own ~/.codex/sessions files: file edits arrive as
event_msg/patch_apply_end (changes+success, no pairing needed), and shell
commands arrive as a freeform custom_tool_call named "exec" whose input is
a JS snippet embedding cmd:"..." rather than a flat arguments object, with
failure signaled by a literal "Script error:" prefix in the output text.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
Closes the last two S11 triggers. internal/herdr/activity.go normalizes
tool/function calls per harness (ClaudeActivity verified against the
existing transcript format, CodexActivity best-effort/unverified,
OpenCodeActivity refuses — no confirmed per-tool-call source exists) and
implements the three thrash rules plus a narrow milestone check
(successful git commit as the last call).
CLIAdapter.RequestHandoffReason asks the agent to write a handoff with
meta.reason set, same "ask, don't invent" pattern as the existing handoff/
report requests. rotate() and TurnDecision generalize the manual-bypass
shortcut to manual/milestone/thrash and request (never directly release)
on a detected trigger.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
MarkdownChanges was deleted as dead code, but the underlying spec
requirement wasn't abandoned — rebuilt it independently. Adds
continuity.ConventionsHash for AGENTS.md/CLAUDE.md/VOCAB.md, tracks a
per-session snapshot on herdr.Session, and adds
Coordinator.checkConventions (run every Monitor tick) which compares
each active session's snapshot against its project's base repo and
pushes an in-pane notice via a new herdr.ConventionsNotifier
capability when they drift.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
Release already validated and uploaded a §6.1 handoff, but nothing ever
told the agent the .orchestra-handoff.json convention existed, so the
file it waited on never got written. rotate() now prompts the agent
once via a new optional herdr.HandoffRequester capability
(CLIAdapter.RequestHandoff) when the file is missing, and defers
Release until it appears, mirroring the .orchestra-report.md/B3 ask
pattern rather than inventing a handoff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
Release now re-verifies every handoff Anchor.Dirty file hash (previously
unchecked after the top-level anchor SHA compare), snapshots dirty state
onto a per-task scratch branch before uploading, and rewrites the anchor
to the new commit so successor pickup collapses to a single HEAD compare.
ScratchCommit made idempotent for repeated rotations of the same task.
Bootstrap's prompt now points the agent at the scratch-branch commit
history instead of vague "read the handoff" prose, and does not claim a
GET /v1/artifacts/<ref> endpoint that doesn't exist.
MarkdownChanges had zero callers and zero tests; deleted per AUDIT.md's
explicit deletion option rather than half-wiring an undesigned feature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
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
CLIAdapter.Lease's initial prompt used wait=0, skipping the inline wait
Bootstrap already used. The spec (§5.1) requires inline wait on
agent.prompt for bootstrap injection so the prompt isn't sent into a
half-rendered pane.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
Verified against a live herdr instance (192.168.1.105:9245) that pane.kill,
pane.release, and pane.rotation_signal never existed in the protocol, as
AUDIT.md's B5 suspected. Real method list captured in deploy/herdr-schema.json.
- Kill now calls the real pane.close({pane_id}).
- RotationSignal interface/method/call-site deleted; no real equivalent exists.
- Release now refuses loudly instead of calling a nonexistent method — the
real pane.release_agent can't return a handoff_ref either way (herdr
doesn't write handoffs, the agent does), so a real fix needs Phase 4
handoff production first.
Also documents Phase 0 findings in AUDIT.md/progress.md, and adds
CLAUDE.md/AGENTS.md with project-specific knowledge (herdr protocol facts,
deployment topology, a currently-stuck live task, the federation fork) for
future sessions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
CLIAdapter.Occupancy called a.Usage(s.PaneID), but ClaudeUsage/CodexUsage/
OpenCodeUsage all take a filesystem path to session state, not a herdr pane
id. Every call failed with "open <pane-id>: no such file", and
Coordinator.rotate silently `continue`d past every failure, so occupancy
always looked unmeasurable and rotation never fired.
Add herdr.Session.SessionFile and CLIAdapter.resolveSessionFile:
- claude: ClaudeSessionFile resolves the transcript by newest-mtime under
Claude Code's own encoded project directory
(~/.claude/projects/<abs-worktree-with-/-as-minus>/*.jsonl). This is the
Phase-1 fallback; the Stop hook's transcript_path (Phase 2) is the
authoritative source once wired.
- codex: routes through the existing CodexActiveUsage sqlite/rollout
discovery instead of the pane id.
- opencode: resolution needs a live session id from the SSE/status API,
not derivable from the worktree alone — refuses loudly with a pointer
to AUDIT.md Phase 1 rather than guessing a path, per the spec's "verify
against a live session before wiring any trigger" (§5.2.1).
A missing/unreadable session file is now a hard error, not a silent
zero-usage Usage{}. SessionHealth gained Occupancy/OccupancyError fields,
populated every refreshSessionHealth tick, so GET /v1/tasks/{id}/health
makes the number rotation decides on observable before trusting it.
Tests: TestClaudeUsageIsLastTurnNotCumulative guards the exact trap named
in §5.2.1 (large early-turn total, small last-turn usage -> low occupancy).
TestClaudeSessionFileNewestByMtime and TestClaudeSessionFileMissingIsHardError
cover the resolver.
AUDIT.md B1. Live verification against a real Claude Code session (the
spec's own acceptance bar for this phase) still needs to happen on a host
with an actual session — not possible from this sandbox.
Pre-existing uncommitted work found at session start: rotation now emits
anchor_sha on TaskReleased (previously silently dropped by store.Append
validation), multi-repo Gitea provider support, per-project git worktree
roots, and associated test coverage. Committing as a checkpoint before
starting remediation work tracked in AUDIT.md.