The brief at agentctx.go:167 advertised findings[].id and findings[].confidence
to every research session. The struct carried neither, so encoding/json dropped
both on every seal, silently, for as long as the schema has existed. A plan
phase had nothing stable to cite and no way to tell an observation from an
assumption.
Finding gains ID and Confidence. Ids are unique within an artifact and shaped
so "research:<id>" is unambiguous in plan prose. Confidence is fact, inference,
or assumption, matching the labels the output style already uses.
DecodeStoredResearch reads what is already in the CAS and backfills both.
Refusing an artifact sealed before this change would block every task whose
research predates it, including at rotation, where the agent that could fix it
is already gone. A backfilled finding is labelled inference rather than fact:
the old schema required evidence and made no verification claim, so upgrading
it on the way in would be the same class of lie this commit removes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
The worktree survives a changes-requested round trip, so .orchestra/review.json
from the first review is still there when the second one starts. A reviewer
that writes .orchestra/done without rewriting it would have the earlier
findings sealed against the new commit, and submit binds whatever it reads to
the commit being submitted, so a stale pass is indistinguishable from a fresh
one.
Observed on the 2026-08-28 baseline run: the file from 10:55:45 was still
present when the second review session launched at 10:57:48. That reviewer did
rewrite it, so the run is sound, but nothing enforced it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
The loop that reads merged pull requests was guarded by len(pullRequests) > 0
at a point 600 lines before the Gitea wiring that writes to that map. The
length was always zero, so the goroutine never started and a merged pull
request could never complete its task. Live on the first submission this
deployment made: PR #8 took a trusted comment and nothing moved.
Moving the block below the wiring fixes the ordering and avoids the race that
reading the map inside the tick would have introduced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
The issues endpoint returns pull requests alongside issues, and nothing
filtered them. The first submission this deployment ever made, kami/test-e2e#8,
came straight back as task 06G4E83E4KRXM8DS90M2648MGM with the submission
packet as its description. That task would have implemented, reviewed and
submitted again, opening a pull request per cycle.
The webhook had the same hole from the other side: a pull_request delivery
leaves the issue key empty, so it would have appended a task numbered 0 with
no title. Both routes now refuse a pull request, and the poll count reports
what was ingested rather than what was listed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
Submission needs a sealed review, so a reviewing session that writes
.orchestra/done without .orchestra/review.json refuses every five seconds
with the failure recorded only in worker health. That is the silent loop this
codebase keeps producing: F39 at a phase boundary, F42 at the result commit,
and now the completion tail.
A review-artifact refusal is separated from every other submission failure,
delivered to the pane that can fix it, and clears the done marker so the
corrected file is what finishes the phase.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
The completion tail ended at TaskCompleted with no pull request. Nothing in
the running system ever called the review or submission endpoints: the whole
event log holds zero ReviewRecorded and zero TaskSubmitted, so the merge
reflection, the publisher and the human trust boundary had no entry point.
Four links, in the order the tail needs them:
- finalize commits first and runs the quality gate against the committed
tree, so the gate result is bound to the commit being submitted.
CheckSubmission requires gate sha, review sha and head sha to be one
commit, which a gate run on the pre-commit tree can never satisfy.
- The worker seals the reviewer's findings and submits, through a new
/v1/federation/workers/<id>/submit. A blocking review returns the task to
implementation instead; a project with no forge still completes directly.
- The reviewing session is told where findings go. The brief asked for
findings and named no file, and it described a diff nobody supplied.
- GiteaPublisher.Push asks the forge what the branch holds before reaching
for a local checkout. A worker-owned worktree is on another machine and
has already pushed the commit; the coordinator has no such directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
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
F42, live on run 5's completion tail. The review agent wrote .orchestra/done,
the worker recognised it, confirmed the agent idle, and then failed the result
commit on every attempt:
stage result: The following paths are ignored by one of your .gitignore
files: .orchestra/done
.orchestra carries a .gitignore of "*" (internal/herdr/adapter.go:132), so the
marker is ignored, and git refuses an add whose pathspec names an ignored path.
The exclusion now names the directory. Reproduced against git 2.55.0 in a
scratch repo both ways, and the regression test uses the same constant the
worker passes to git.
The failure retried every five seconds 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. That is F18, still open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
F41, live on run 5: the review agent produced nothing after 02:12 and the
02:46 renewal was granted anyway. The progress digest covered Claude Code's
status footer, and one of its fields ticked inside the window. Reproduced the
worker's stored progress_sha byte for byte from the live pane, so the branch
taken was progress != ProgressSHA, not IsBusy and not an empty baseline.
PaneProgress now cuts from the editor's lower rule and 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.
Lease TTL moves to 5 minutes, from domain.LeaseTTL, with renewal at half of
it. Reclaiming a stalled pane happens only at expiry, and 30 minutes per
window made run 5's stall unbounded in practice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
Both halves are on bcaf0cc. The review lease renewed once to 22:56:10Z, since
the agent's 02:12 output was real progress against its launch baseline, so the
recovery lands twenty minutes later than the handoff first said.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Run 5 proved frame through review with four autonomous rotations, sealed
artifacts, and an operator correction rendered above both accepted artifacts and
carried into shipped code.
It is parked on F40, which is fixed but needs the worker installed. Recovery is
the lease expiring at 22:36:06Z and review relaunching with the corrected brief.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
The worker finalises a task when .orchestra/done appears. No brief ever named
that file: grepping a rendered launch.md for it returned nothing, in any phase.
review is terminal. Its only legal move is backwards to implement, so a review
that passes has nothing to ask for and, until now, nothing to write either. Run
5 halted exactly there after four clean rotations, with no error anywhere,
because stopping was the correct reading of its instructions.
The brief now names the marker in the terminal phase, says when to write it,
and says it is exclusive with asking to go back. The test asserts the negative
too, so a phase that can still ask is never told to finish instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Frame to implement, four phase transitions, two artifact refusals the agent
corrected itself, and an operator correction delivered to a live plan session
in three seconds and rendered above both accepted artifacts.
Durable human authority, phase isolation, disposable sessions and reliable
handoff are now observed rather than argued.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Two halves of the same failure, live on run 5.
F38: the phase brief named .orchestra/research.json and described its contents
in prose, never its schema. The agent guessed dead_ends as strings where the
decoder wants {tried, why_failed} objects. The brief now carries the shape, and
a test decodes each documented shape with the same function the worker uses, so
a struct change that is not mirrored fails the build.
F39: the local artifact check refused the request through recordError alone.
answerRefusedPhase only ran on a coordinator 409, so a decode failure told the
agent nothing. The session sat at a boundary rewriting nothing, which is the
silent-loop shape the comment above that block warns about, reached by the one
path with no delivery. Both local refusals now reach the agent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Handoff produced, validated, anchored, committed, predecessor slot freed,
successor leased and started with pickup validated. Autonomous.
F33 and F34 proved themselves incidentally in the same event.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Handoff.Command is the last command observed in the pane. The handoff prompt
tells the agent to write .orchestra-handoff-report.md and stop, so that write
is almost always the last command there. Validate then rejects it as circular:
"must not point to a handoff or report".
Every phase rotation therefore failed on Orchestra's own instruction. Live on
run 5, one stage past F36.
lastObservedCommand now skips commands the validator would call circular, so
the successor gets the last command that was real work. The rule stays in
continuity and is asked, not restated, which is how F36 happened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Run 5 reached the phase transition, further than any previous run, then stopped
on F36: the adapter emits a rotation reason the validator refuses.
Each defect tonight was reachable only after the previous fix. Tests passed
throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
handoffReason has emitted "phase_changed" since phase rotations landed. The
continuity validator's reason list was never extended, so every phase rotation
built a handoff it then refused as "invalid handoff meta".
Live on run 5: with F31 clearing the parse ahead of it, the release reached
this and stopped at phase "prepared" with
"adapter: upload handoff: invalid handoff meta".
The test asserts the property rather than the constant: every reason the
adapter can produce must survive Validate, including its fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Two gaps F33 left behind.
F34: the renewal gate exempts a lease with no baseline, so a pane that opened
and never started got a full free renewal period. That is the exact case the
gate exists to catch, and it happened live at 01:33:37: the stuck run 5 lease
renewed to 22:03 on a pane that had not moved since 01:13. Baseline the
progress hash at launch, where the pane is already being read.
F35: nothing could re-poke a live pane. Orchestra can put text in an editor and
be wrong about whether it landed, and the only recovery was to destroy the
lease and wait out expiry, roughly an hour. The new "resubmit" action presses
Enter on text Orchestra itself submitted. It changes no lifecycle state so it
appends no event, and it is fenced like an approval: a live worker-owned pane
at the capture revision the operator was looking at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Run 5's first attempt never started: ConfirmInput's first poll beat the TUI's
render, saw an empty editor and called it editor_cleared. The prompt sat unsent
while the lease renewed against a pane doing nothing.
Recovery is left to lease expiry rather than a keystroke, so the relaunch stays
autonomous evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
ConfirmInput's first poll can run before the TUI renders the pasted text. It
then finds an empty editor, falls through to the idle branch, and reports
confirmation=editor_cleared. The launch text sits unsent for the whole lease
while the coordinator believes the agent is working.
Run 5 died on exactly that: first_submit_at 21:13:19.570, confirmed_at
21:13:19.577. Seven milliseconds. The two launches that worked took ~500ms and
a second Enter, so the difference was scheduling luck.
An empty editor is only proof once it has held the text, or once it has stayed
empty past a settle window. Text seen and then gone still confirms at once, and
so do busy, blocked and queued. Waiting only happens in the never-observed
case, which is the one that cannot be told apart from a slow render.
With this, the failing case reaches the existing resubmit path instead: the
text appears, is recognised as unsubmitted, and Enter is resent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
F29 and F30 are both live-proven on the new build. F30's session cleanup then
exposed F32: a running but empty tmux server answers "no current target", which
hasSession did not recognise, so an already-dead pane could never be reaped.
F32 is a side effect of dea56e4. The reply only exists because the runtime now
outlives its last pane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
A running but empty server answers "no current target". hasSession only knew
"can't find session", "no server running" and "no sessions", so it returned
that as a real error. Kill then failed for a pane that was already gone, the
quarantine never cleared, and the worker's only session slot stayed pinned.
This is a side effect of dea56e4. Before the runtime became its own unit the
server exited with its last session and answered "no server running", which
hasSession already handled, so this reply had never been produced. AgentStatus
shares the same helper and now reports "exited" instead of erroring.
Found live: the F28 disposable task's session survived its own block on
93338b7, quarantined and unreapable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Runtime death is live-proven distinct from worker deployment: no renewal in the
20:45 window, expiry and requeue at 20:55:50.
F31 explains run 4 and the F28 disposable task both dying at rotation. The
handoff action is two agent-written lines joined by the code, held to one
line's budget, and the error named a branch the answer cannot reach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
parseHandoffAnswer joins the agent's NEXT and WHY answers with " — ", and the
prompt asks for a sentence each without naming any budget. Validate then held
that join to one authored line's 200 characters. Two ordinary sentences do not
fit, so every rotation failed.
The failure was invisible twice over. The message said "prose smuggled into
list", which named a branch the answer cannot reach: parseHandoffAnswer splits
on newlines and trims, so no authored field ever contains "\n#". The only
reachable cause was length, and the agent was never told what to shorten.
Seen live on two tasks, and it left the release transaction stuck at "prepared"
that pinned workpc-claude's only session slot (F30).
Give Action the budget of both lines, name the length in the error, and put the
limit in the prompt the agent actually reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
F28's worker-restart direction is live-proven: three restarts, one pane pid,
one lease epoch, no lifecycle events appended. The runtime-death direction is
still running and will be appended when it lands.
F29 and F30 are both fixed and both unbuilt. Run 5 needs a deployment carrying
them, or it hits F30 the first time a handoff artifact is rejected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
A release transaction that never reached anchor_pushed has no artifact: tx.Ref
is empty and no successor can pick anything up. The event handler still kept
its session mapping alive on TaskReleased/TaskBlocked/TaskFailed, so once the
pane was gone the mapping was immortal. health() reports ActiveTask straight
out of w.sessions, so the coordinator saw the harness as permanently busy and
never leased to it again. It produced no log line at all.
Found live on workpc-claude, stuck at phase "prepared" behind a rejected
handoff artifact. Freeing it needed hand surgery on the worker's state file.
Keep the mapping only while an anchor actually exists. Drop the transaction
with it, since nothing can advance it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
The store fences TaskReleased, TaskBlocked and TaskCompleted on a leased task
against the live harness_id and lease_epoch. The UI action handler sent
neither, so all three returned 409 on exactly the tasks the UI listed them as
enabled for. Found live: eight block attempts against a stuck run at a stable
version, all 409 "task version conflict".
The fence is there to reject a stale writer, not the operator. Carry the lease
read at the top of the handler. The version CAS on the append still rejects a
racing write.
Also initialise body when the request carries none. The block path wrote
block_reason into a nil map.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Type=forking with no PIDFile left MainPID=0, so systemd read a crashed tmux
server as a clean exit and Restart=on-failure never fired. tmux -D runs the
server in the foreground, which gives systemd the real pid.
-D also turns exit-empty off, so the synthetic orchestra-runtime session is no
longer needed to hold the server open past its last agent pane. Nothing else
in the tree referenced that session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
The socket is /tmp/tmux-$UID, so orchestra-tmux.service and
orchestra-worker.service must run as the same user or the worker talks to a
different server. The installed unit has run as kami since before the split;
the repo copy still said orchestra.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
F28. The worker spawns the tmux server on its first command, so the server and
every agent pane sit in the worker unit's cgroup. Restarting the worker
destroyed the sessions it was restarting to manage, and F16's missing-pane
branch has been firing on deployment rather than on real execution loss.
KillMode is not the fix. Under mixed systemd still SIGKILLs the cgroup
remainder once the main process exits, and process only encodes accidental
orphaning. The runtime becomes its own service instead.
The worker gains After= and Wants= on it, ordering only: a worker that finds
the runtime missing must report that rather than be stopped by it. The unit
holds an idle session so the server outlives its last agent pane.
User must match between the units, since the socket lives under /tmp/tmux-$UID.
The installed worker on workpc runs as kami while this file still says
orchestra; the staged copy is set to kami to match.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
F25. rotationTick returned early for claude before reaching federatedTurn,
which has one call site below that return. On the harness both burn-in runs
used, no phase request could ever be read and every human decision recorded
against a live session went undelivered. Claude still skips the occupancy
state machine below, because it owns its context rollover through the
installed hook. A turn boundary is not a rotation.
F26. The phase brief listed every domain-legal target, so run 4's frame
session read "research, implement" and asked for implement, which the
project's path refuses. The path is Orchestra's to know: the brief now names
one step and says a wrong target comes back with the right one.
F27. A refused request only reached recordError, leaving the agent to rewrite
the same rejected file forever with nothing telling it why. federation.
StatusError makes a 409 classifiable, and the refusal is delivered through
sendPrompt under the F20 guarantee. A transport failure is not an answer: the
request survives and the agent is told nothing.
The F25 regression test fails against the unfixed rotationTick.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
F24 is the last Orchestra-owned editor write outside the delivery guarantee,
in the coordinator-local CLIAdapter.LeasePrompt. It cannot execute on the
deployed topology, so it is logged rather than fixed: run 4 exercises the
federated worker path, whose launch delivery already confirms.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
The brief told the agent to ask for a phase change and never carried the
asking. The agent asked in prose, no code represented the request, and the
session idled until its lease expired. That is what failed run 3.
F21. The agent asks with .orchestra/phase-request.json, and seals
research.json or plan.json where the phase it is leaving produces one. At a
verified turn boundary the worker checks the phase belief, the transition and
the artifact, then calls the coordinator with its lease epoch and a derived
operation id. AdvanceWorkPhase is unchanged, so a request cannot reach a move
the operator surface could not also make. Redelivery is idempotent.
F22. A session now records the phase it was launched to run. One that no
longer matches its task rotates with reason phase_changed, whether this worker
asked for the change or an operator made it.
F20. CLIAdapter.prompt sent handoff and rotation prompts without confirming
them, which is the failure F20 exists to catch. Fixed at the shared call site.
F23 needed no change. Issue comments already become decisions with no
submission, through Reconciler.Reconcile at PreLease and at every turn
boundary. The earlier finding searched internal/operations alone and was
wrong. Tests now cover the boundary it turns on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
Run 3 is waiting on an autonomous expiry and relaunch, and the next session
has to judge it without repeating this one mistake: F16 was reported as
passing live on the strength of renewal events, while its predicate was
inert. The hierarchy the operator set is at the top of the file, and the two
hard failure predicates are stated before anything else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F20. Only the launch confirmed its submit. A decision notice at a turn
boundary, and /clear or @HANDOFF.md during a context reset, were
fire-and-forget through the same transport that loses an Enter often enough
that the launch needed three resubmits. A lost Enter on the context-reset path
is the worst of them: it strands the session mid-rollover and nothing retries
it. LaunchConfirmer is therefore InputConfirmer, ConfirmLaunch is ConfirmInput,
and sendPrompt and sendLine both go through it.
Orchestra does not try to guarantee delivery of input it did not originate.
But it must never read that input as work, which is the F16 half. Burn-in run
3 stalled with an unexplained "go ahead and implement it" in the editor, and
the renewal check hashed the whole capture, so those keystrokes read as
progress and the lease kept renewing around an idle agent. PaneProgress drops
input lines from the capture, which the -J join makes exact: a wrapped input
block is one line beginning with the prompt marker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F16 shipped inert. renewLeases refuses to renew when the agent is idle and the
pane is unchanged since the hash recorded at the previous renewal, but
reconcileLeases rebuilt every lease from the coordinator's task list on each
tick and constructed a fresh struct, dropping ProgressSHA. The check therefore
took its no-baseline branch forever and renewed unconditionally.
Observed live: task 06G44JZB80MZBEY97196EZN8EC renewed at 09:31, 09:52 and
10:12 while its agent had been idle since 09:13, with a byte-identical pane
capture and no busy marker anywhere in it.
UsageBaseline and PickupAcknowledged were being wiped the same way, so a usage
receipt lost its baseline and a pickup could be re-acknowledged. The
coordinator is authoritative for the lease, not for what this worker observed
under it, so those three carry across a reconcile while the epoch is unchanged
and reset when it is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F12 through F19, each against the revision it landed in, with what was
verified live and what has tests only. AUDIT.md is uncommitted and owned by
another session, so the ledger goes here rather than colliding with it.
The entry worth keeping: run 3 failed three times with prompt_not_submitted
while six isolated probes of the same code path submitted on the first Enter.
F17's evidence line settles it, submit_attempts=2 in production against 1 in
every probe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A task that reached the router's MaxAttempts was permanently terminal.
TaskReleased only ever increments Attempt, TaskCorrected could not touch it,
and no HTTP route emitted a correction at all. The only way to work an
exhausted issue again was to invent a second task for it, which defeats
(source, external_id) dedupe and abandons the task's own history.
POST /v1/tasks/{id}/retry, full-control surfaces only. It requires the task
to be failed, unleased, and failed with reason retry_limit: restoring a retry
budget is not an answer to a failure that was not the budget running out. The
effect is one TaskCorrected naming that failure, setting state queued and
attempt 0 and clearing next_retry_at, failure_class and last_error. Task id,
source pair, goal, acceptance, decisions, work phase and artifact refs all
stay, and the original failure events stay in the log.
operation_id is required and makes the call idempotent, so a repeated request
cannot reset an attempt that has since started running.
This is RetryTask, not a generic correction endpoint: arbitrary task mutation
over HTTP is a different and much larger authority. It also does not address
F9, which is an operator releasing a lease someone else owns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
renewLeases renewed whenever a session existed and PaneCapture succeeded,
so a pane that opened and never accepted a prompt held its lease forever.
That is the mechanism behind the July stuck task: the launch failed and
nothing ever let go.
Renewal now needs the agent to be busy, or the pane capture to differ from
the one recorded at the previous renewal. The first renewal has no baseline,
so it records one and passes; the next must show movement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 2 never reached conformance. It found F12, then F15, then F16, each
hidden behind the one before it. F12, F13, F14 and F15 are fixed and
committed; F16 is diagnosed and open. Neither deployed half is at 1fd82f8
yet, and the worker install needs root, so the next session starts with an
operator step.
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>
Gitea ingest set title, description and capability but never acceptance, so
every Gitea-sourced task rendered "Acceptance: Not stated." however carefully
its body was written. F2 restored the body; it did not wire the field.
The convention is deliberately tiny. A markdown heading spelled "acceptance"
or "acceptance criteria" opens the section, bullet and checklist items until
the next heading become the criteria in order, checkbox markers are stripped,
empty items are dropped, and the whole section leaves the description so a
criterion is never also read as instruction prose. Nothing else is recognized:
inferring acceptance from arbitrary prose would eventually invent a
requirement, and a fabricated criterion outranks every human decision beneath
it in the authority order.
An absent section yields no acceptance, which is not an ingestion failure. The
task renders "Not stated." and the frame phase resolves it through the
decision-request path, where a human answers instead of the parser guessing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Burn-in run 2 ingested its task and then sat queued forever. Every herdr in
the live config declares quota_limit_5h and quota_limit_weekly, the event log
holds zero QuotaReported events, and QuotaSince reported an empty window as
unknown. QuotaAvailability fails closed on unknown, so no harness could ever
be leased, and the only producer of a receipt is a completed lease.
The event log is Orchestra's whole accounting source, so a window holding no
receipts is observable zero consumption. QuotaSince now reports known for an
empty window and for a harness that has never reported. A receipt that
declares its own consumption unknown still fails closed.
The refusal also lied about its cause. federatedAvailability collapsed a base
gate refusal into the federation health string, so router health said "stale
heartbeat or unhealthy local backend" while the heartbeat was one second old.
Availability gates now name themselves through an optional
ReasonedAvailability contract: quota refusals say whether usage is unknown or
the window is exhausted and by how much, and worker refusals distinguish an
unregistered worker, a never-probed backend, a stale heartbeat, a stale
health check, and an unreachable backend.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>