Commit Graph

143 Commits

Author SHA1 Message Date
kami 87a3a6e2e4 Record the first successful phase rotation
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
2026-08-28 01:49:39 +04:00
kami 10f3cd2ea5 Record F37
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
2026-08-28 01:45:28 +04:00
kami cb7782db01 Stop the handoff carrying the write Orchestra asked for
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
2026-08-28 01:45:11 +04:00
kami 294cbcb078 Record F34, F35 and F36 from run 5's second attempt
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
2026-08-28 01:41:21 +04:00
kami b9ca365b9a Let the validator accept the reason the adapter produces
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
2026-08-28 01:38:58 +04:00
kami 92f32d6fea Give the operator a way to resubmit, and baseline progress at launch
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
2026-08-28 01:34:26 +04:00
kami 48326d4301 Record F33, the launch that confirmed itself in seven milliseconds
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
2026-08-28 01:24:22 +04:00
kami 32220d929b Stop an unrendered editor from confirming a launch
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
2026-08-28 01:23:48 +04:00
kami f860de4370 Record the 93338b7 deployment and F32
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
2026-08-28 01:09:30 +04:00
kami 92adf0477d Recognise an empty tmux server's answer for a missing session
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
2026-08-28 01:09:00 +04:00
kami 93338b72ce Close F28 and record F31, the reason every rotation failed
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
2026-08-28 01:00:33 +04:00
kami 65230020b9 Budget the handoff action for the two lines it is made of
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
2026-08-28 01:00:04 +04:00
kami c8db659b45 Record the F28 proof and the two defects setting it up exposed
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
2026-08-28 00:42:38 +04:00
kami 2dc90bd277 Stop an unpushed release from pinning the worker's only slot
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
2026-08-28 00:21:06 +04:00
kami efd0a5e3cd Let the operator act on a leased task from the browser
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
2026-08-27 18:22:53 +04:00
kami dea56e4bcd Let systemd track the tmux server it restarts
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
2026-08-27 18:05:57 +04:00
kami fc4c29ff3f Match the worker unit's User to the installed one
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
2026-08-27 17:55:17 +04:00
kami fc63dda3b9 Hand off with F28 uninstalled and run 5 unstarted
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
2026-08-27 17:53:42 +04:00
kami daa5d20d9b Own the tmux execution runtime as its own service
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
2026-08-27 17:48:28 +04:00
kami fbaaf79bb1 Let the claude harness reach the turn boundary at all
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
2026-08-27 17:29:43 +04:00
kami f27cc4879f Record the run 4 deployment boundary and F24
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
2026-08-27 16:43:31 +04:00
kami 1f5bf7e66e Give the phase brief a protocol, and end the session it advances
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
2026-08-27 16:35:37 +04:00
kami 15408a5463 Hand off mid-observation, with the evidence standard
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>
2026-08-27 14:59:49 +04:00
kami edbe98fc5e Confirm every write Orchestra sends, and count none of them as progress
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>
2026-08-27 14:44:04 +04:00
kami 770cc6a74b Stop reconcile from wiping what the worker observed
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>
2026-08-27 14:15:10 +04:00
kami 801ec1be24 Record runs 2 and 3 in the burn-in ledger
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>
2026-08-27 13:21:31 +04:00
kami edff021265 Give an operator one way to retry a terminal task
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>
2026-08-27 13:07:48 +04:00
kami f54fb0036d Confirm a launch from the editor that owns the cursor
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>
2026-08-27 12:53:56 +04:00
kami 1888d4280e Renew a lease only when the agent shows progress
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>
2026-08-27 12:00:32 +04:00
kami 214212c9e2 Hand off the burn-in: run 2 found three bugs, run 3 needs an install
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>
2026-08-27 01:21:37 +04:00
kami 1fd82f863c Acknowledge a launch only when the harness accepted it
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>
2026-08-27 00:46:56 +04:00
kami a5d361b59f Read acceptance criteria from one recognized issue section
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>
2026-08-27 00:35:37 +04:00
kami 0d67af9976 Admit a first lease when a quota limit has no receipt history
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>
2026-08-27 00:28:57 +04:00
kami c833e0eb62 Hand off the burn-in: run 1 closed, run 2 ready to start
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 00:00:43 +04:00
kami fb7fd234ca Record the fix pass and the run 2 preconditions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:50:42 +04:00
kami 2f7b209b62 Build stamped binaries in a throwaway worktree of HEAD
This checkout is shared with another session. Its uncommitted Go changes must
neither be compiled into a binary stamped with a commit revision nor block a
deploy, and a dirty-tree refusal does both jobs badly. deploy/build.sh now
builds in a detached worktree of the revision it stamps, and the container image
is built the same way rather than from the live checkout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:50:02 +04:00
kami 77a2b323fa Stop lease validation from depending on the current clock
The API was in a restart loop, exiting with `invalid event: until_ns required`.
ValidateEvent compared until_ns against time.Now() for TaskLeased and
TaskLeaseRenewed, so a lease event that was valid when written failed validation
once it expired. store.Open replays the log tail after the snapshot and
log.Fatal's on the first invalid event, so the coordinator refused its own
history and could not start.

Validation of a durable event must be time-independent. Well-formedness is this
function's question; freshness belongs to Store.Lease and Store.ExpireLeases,
which compute until_ns themselves.

Latent since the field was introduced. It needed a renewal in the post-snapshot
tail plus a restart after that renewal expired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:48:04 +04:00
kami 4fbf3ac966 Report queued tasks the scheduling pass never considers
A task in retry backoff was filtered out before the candidate loop, so it
recorded no rejection at all: queued, apparently assignable, and silent. That is
the exact shape that made F5 take a live session to diagnose. It now reports
"retry backoff until <time>".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:42:26 +04:00
kami 0ead6d2d02 Close F7, F5 and F8 before resuming burn-in
F7, security. An unset surface token makes the middleware skip its check, so a
full-control surface with no credential is an open control plane rather than a
closed one. With ORCHESTRA_TUI_TOKEN unset, any LAN caller could lease, release,
complete or block any task by declaring one header, which is how this session's
manual leases were issued. authz.RequireCredentials now refuses startup instead
of logging. Web is exempt: Sessions makes its login mandatory.

F5, lifecycle. router.go's silent `continue` was the first bug, not the
predicate behind it. Every eligibility gate now records a router.Rejection with
task, herdr and reason, exposed at GET /v1/router/health, reset per pass. No
gate was weakened: a direct Store.Lease succeeding proves the lease path, not
that eligibility should have selected that worker.

F8, correctness. Reconcile iterated every configured source for every task, so a
task's external id was looked up in whatever repository each source pointed at.
Once two repositories share an issue number, an unrelated human comment becomes
an authoritative decision for the wrong task. Reconciliation is now bound to
task.Source, the provider:project identity the ingest stamped, and a source that
cannot prove it owns the task is skipped. A task with no matching source
reconciles to nothing and still launches, because nothing to import is not a
failure to read.

The integration fixture ingested from "jsonl" while reconciling from "gitea",
which is exactly the shape F8 makes impossible; it now ingests from the source
it reconciles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:36:10 +04:00
kami 7e49348096 Record run 1: three bugs fixed, five open and classified
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:58:29 +04:00
kami a0209a2951 Keep the launch dump out of the session's git status
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>
2026-08-26 22:57:39 +04:00
kami 09e572f11e Carry the Gitea issue body into the task
Found by reading .orchestra/launch.md on the first burn-in task. The rendered
context said `Acceptance: Not stated.` and carried the issue title as the whole
goal, because Gitea.event parsed the issue body and then dropped it from the
TaskCreated payload.

The body is the task's own statement of what it wants, which every rendered
context ranks above continuity and below only a human decision. The store
already reads `description` from TaskCreated; only the provider was silent.

Every Gitea-sourced task so far has therefore run on its title alone. Classify
as an authority bug, not a model-following failure: no agent could have known
what it was not told.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:53:36 +04:00
kami 2753a8df62 Let a federated worker read the intent endpoint
Found live on the first burn-in task. Every federated launch failed with
`effective intent: federation: 401 Unauthorized: unauthorized surface`, and the
task nacked back to queued.

GET /v1/tasks/<id>/intent was added so a worker renders its own launch
instruction from the reduced intent, but authz.HTTPWithSessions never got the
matching worker-path exemption. An unlabelled request defaults to the Web
surface, which is session-gated, so the one caller the endpoint exists for could
never reach it.

Twenty test packages passed throughout, because the authz tests covered the
surface capability table and not the worker path list. Only the live owner path
established this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:49:47 +04:00
kami 41532a3efc Correct the pane push finding and record the test-e2e flow 1 setup
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 21:42:04 +04:00
kami 16f09106b9 Record live burn-in findings: no workpc repo, pane credential reality, unset surface tokens
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 21:34:37 +04:00
kami 7e1140d248 Record the pane-environment and herdr findings that block flow 1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:41:36 +04:00
kami 69f630a5f1 Point the burn-in identity at HEAD
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:37:10 +04:00
kami 6f9300b549 Record the burn-in build identity and deployment state
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:35:47 +04:00
kami 86b67d9fbf Log build revision at startup in both binaries
The burn-in pairs a coordinator with a worker, and step one of every run is
checking that both report the burn-in commit. That was only visible behind the
operator login, at /v1/admin/diagnostics and /v1/federation/workers, and the
worker never logged its own revision at all. Both now print it on startup, so
`docker logs orchestra-api` and `journalctl -u orchestra-worker` are enough.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:34:27 +04:00
kami 7f12c7fc37 v3 workflow: intent, phases, review, submission, enforcement, burn-in
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>
2026-08-26 18:31:20 +04:00