Commit Graph

193 Commits

Author SHA1 Message Date
kami cbd6b11c49 Record run 16: the rotation rung, half proven
A successor inherits the whole sealed plan. Phase progress is withheld
from a review successor on purpose, so the rung still needs an
implement-phase successor. F62: a requested handoff nobody answers is
invisible, and its only consequence is an expiry indistinguishable from
an idle one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 14:14:37 +04:00
kami 34f3c2888f Rebuild the operator console on the ethos shell
Nine screens against orchestra-ui-spec.md and the accepted mockups, on
the ethos design system: signal violet #8F7AE5, the routing fork motif,
the 64px rail and 56px top bar, mono for every machine value and sans
for every human one.

Each screen was built by its own agent against a fixed foundation, so
the shell, tokens and primitives have one author and the screens cannot
drift into nine dialects.

Real data only. Where no endpoint exists the screen says which one it
needs instead of inventing a value. That is most of what was learned
here: Steer / Correct is disabled because nothing records a human
decision from the web, take-control is disabled because nothing
forwards keystrokes to a pane, context occupancy is missing from three
screens, and projects can show no repo, remote, quality gate or
verification policy because those live only in config.jsonc.

Three bugs the render caught that no computed value would have:

The previous stylesheet fought every shared class name and leaked
properties the new rules never mention, which is how position:fixed
survived on .topbar. It is now scoped under .legacy and applies only to
the login route, which also stops its green accent and its
backdrop-filter from reaching the console.

Go marshals a zero time.Time as "0001-01-01T00:00:00Z" and omitempty
does not omit a struct, so absent timestamps arrived populated-looking
and rendered as "739855d ago". Stripped once at the API boundary every
screen reads through, with a test.

Long machine ids overflowed their cards and painted under the next one.

Verified by rendering: chromium screenshots of the dashboard, tasks,
task detail, terminal, workers and review at 1440px, and the dashboard
at 390px. Geist is still not on disk, so both stacks fall back to the
system faces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
orchestra-release-v1
2026-08-29 02:44:43 +04:00
kami 118ac9fbcb Commit the concurrent session's pending web and docs work
Not my work. These nine files sat uncommitted in the shared checkout
while another session worked on them, and the UI redesign that follows
rewrites web/src/main.tsx and web/src/style.css. Committing first means
that work is recoverable rather than overwritten.

Contents, by inspection rather than by authorship: whitespace
normalisation and edits across main.tsx, 568 added lines of style.css,
client and client test changes, the orchestra-user line in build.sh, and
docs updates to AGENTS.md, AUDIT.md, DEPLOYMENT.md and the env example.

Committed at the operator's explicit instruction.
2026-08-29 02:17:32 +04:00
kami 2d28f7b462 Record slice one's first run against 881 real events
Seven eligible items, three gaps, and four defects in the model that a
read-only projection surfaced before any schema was committed to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 02:02:34 +04:00
kami a757cffc78 Project a debt ledger from canonical history, read-only
Slice one of DEBT-DESIGN.md, with the four amendments applied. It writes
nothing: no new event types, no scheduling, no clustering, no
maintenance tasks. The point is to find out whether the model can
represent debt this project already knows about, before committing to a
durable schema.

The projected type lives in domain and the fold lives in store, so the
first implementation does not bake a read model into the command layer.
operations owns the one action that exists, CheckDebtEligibility, which
is a pure function returning explicit reasons like CheckSubmission.

Signatures carry their version in the string. Normalization rules will
change, and without a version that silently regroups history and moves
the recurrence counts eligibility was already decided on.

Observations require exactly one of event_id and legacy_ref. Imported
Fxx history predates the events that would justify it, and a fabricated
event id would break the provenance rule the ledger exists to enforce.

Incompleteness is reported, not hidden. Manual interventions and worker
observations are carried by no event type, so the ledger names both as
non-durable gaps rather than reading as "no operational cost". The
operational refusal reason says the intervention count is structurally
zero on every current log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 02:00:58 +04:00
kami 2f61a99986 Design the debt ledger from what the event log already carries
Nine questions answered against the tree at 3c7cf95, no implementation.
The ledger is two event types, one projection beside Task, one pure
eligibility function and one read-only endpoint. Nothing in the task
lifecycle changes.

The first slice writes nothing: a projection over the existing log that
must reproduce the release loop, the opencode adapter gap and the
retry-idleness dynamic from runs 10 to 14. A model that cannot represent
debt already known is wrong before any schema is committed to.

Also corrects run 14: the OpenCode Zen free tier is not blocked, the
selected model is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 01:51:53 +04:00
kami 595a1d3533 Record run 14: plan-phase execution proven, opencode diagnosed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 01:11:57 +04:00
kami 0797432d6f Record run 13: F60 settled, and F18 populated under a real failure
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 23:55:20 +04:00
kami 3c7cf95d8c Settle a release transaction deterministically in every case
F60, and the general rule F58 and F59 were reaching for one case at a
time: a transaction must settle or be abandoned deterministically, and
must never spin on an answer that cannot change.

Terminal now means failed or completed. Both drop the transaction and
free the session; nothing will ever lease either task again.

Blocked keeps the transaction, because a reopen returns the task to the
queue and that exact owner can still commit. TaskBlocked therefore
retains the ending epoch the way TaskReleased already did, or the
late-handoff path would have nothing to fence against after the reopen.

A refusal parks the commit instead of retrying every five seconds. It
is the coordinator's answer about who owns the task, so it stays true
until an event about that task arrives, and any such event un-parks it.
A reopen arrives as TaskCorrected, so the rule cannot be a list of
event types. Backoff runs 30s to a 5 minute cap.

A transport failure is not an answer and keeps retrying at once. That
distinction is the whole reason the park keys on a 4xx StatusError
rather than on any error at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 23:52:18 +04:00
kami 0f83559ecc Record F18, the merge, and what run 12 leaves open
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 23:45:03 +04:00
kami d6ee10f028 Report a bounded ring of worker failures, not one slot
F18. A single last_error slot destroyed causal evidence twice. Run 7
kept only the last of four failures. In run 11 a five-second retry loop
on a dead task pinned the slot for twenty-six minutes, so the live
task's own expiry was never visible at all, and run 12 lost diagnosis
time to the same thing before F58 removed the flood.

WorkerHealth now carries up to sixteen distinct observations, each with
its repeat count and first/last times. Collapsing is by message rather
than by position, because a loop interleaved with other failures would
otherwise still flush the ring. Eviction drops the least recently seen.
last_error and error_at keep their wire names and still report only the
newest failure, so nothing reading them has to change.

The ring lives in memory beside last_error and is not persisted, which
is the behaviour last_error already had across a restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
orchestra-f18-baseline
2026-08-28 23:43:26 +04:00
kami b317ecb1bd Record run 12, the expired release proven both ways
F57 accepted, F58 and F59 abandoned. The rig, the two live traces, and
the correction to the operator-lifecycle entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 23:16:56 +04:00
kami 8e37989526 Drop a release transaction when its task fails
The supersession rule fires on TaskLeased, and a failed task is never
leased again. Run 12's rig task reached retry_limit still holding a
transaction whose commit the coordinator refuses permanently, so it kept
asking every five seconds with nothing that could ever change.

Terminal means terminal: TaskFailed now drops the transaction and
quarantines the session even when the anchor was pushed. Blocked keeps
the old rule, because a reopen still produces a successor that can pick
the anchor up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 23:14:09 +04:00
kami 03663f413b Abandon a release transaction another lease has superseded
The late-handoff path fences on the epoch that expired. Once the task is
leased again under a different transaction, that epoch is two owners old
and the commit can never be accepted. The worker kept asking anyway,
every five seconds, holding the pane and pinning both ActiveTask and the
single last_error slot. Run 10's task did that for seven hours.

Proven live at 19:01:30Z: the lease expired while the anchor was
pushing, a successor took the task 13ms later, and the coordinator
refused the late commit with 409 lease not owned. The refusal is right.
The retry loop behind it was not.

TaskLeased now abandons a release transaction whose id the lease does
not carry, and quarantines its session. A successor pickup carries the
predecessor's own transaction id, so the recoverable predecessor F30
protects is left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 23:05:09 +04:00
kami 6565b9fce2 Let an expired lease still commit the anchor it pushed
A release pushes the anchor first and commits second. When the lease
expired in between, the commit could never land: the worker sent the
epoch from w.leases, which the expiry replay had already deleted, and
the coordinator refused a /handoff without a live owned lease. Run 10
lost a finished task this way, its work sitting in the worktree until
retry_limit.

The epoch now belongs to the release transaction, so it survives the
lease. TaskReleased retains the ending epoch as Task.LastLeaseEpoch,
and lateHandoffAccepted lets exactly that owner commit while the task
is queued, unleased, and carrying no handoff of its own. A successor
that has already re-leased the task holds the lease, so the late
handoff loses. Store.Append's version fence settles the race, which is
why the late path skips the version check the worker cannot satisfy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 20:57:19 +04:00
kami 8850de3782 Hand off with the plan machinery proven and tagged
Thirteen rungs, ten defects found and fixed, both halves deployed at
orchestra-plan-v1. The handoff records what the defects had in common, why the
mismatch rung took four attempts, and the two open items that should be fixed
before anything runs unattended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 18:34:51 +04:00
kami 1e8d55f933 Record run 11's completion, the capture loop, and the deployed state
Ten defects now, all fixed. The tenth was a five-second retry loop against a
blocked task that ran for twenty-six minutes and pinned the single last_error
slot to a dead task, which is F18 turning a small leak into a blind spot.

Run 11 went on through review, submission and merge without intervention, so
the frozen lifecycle is re-proven above every fix in this session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 17:58:20 +04:00
kami 44ff35aeb3 Stop publishing captures for a task the worker no longer holds
publishCaptures iterated every session the worker had, with no reference to
whether it still owned the lease. Run 10's task was blocked and unleased for
twenty-six minutes while this called the coordinator every five seconds and
logged "409 Conflict: lease not owned" each time.

The cost was not only the traffic. The single last_error slot stayed pinned to
that dead task, so run 11's own expiry reason was never visible, which is F18
turning a small leak into a blind spot.

A capture is lease-scoped. holdsLease names that invariant so the guard is
testable, rather than inline where a stub backend makes the test vacuous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 17:17:52 +04:00
kami 936fa74cf0 Record the typed mismatch and the reopen, proven in run 11
The last rung. A plan sealed through the phase endpoint asserted a helper the
script does not define, and the implementer reported it rather than improvising
or working around it.

Every authority property held. The report carried an observation and no
replacement plan. Orchestra chose the transition. The old plan stayed accepted
until the replacement sealed, then moved to PlanHistory, and progress did not
carry across the ref change. The replacement rendered verbatim into a fresh
implement session.

The three failed attempts are kept, because each was the agent being right: a
file it can restore is reversible, a human correction is authoritative, and a
competent planner does not author a false premise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 17:06:18 +04:00
kami c4306dba28 Record runs 7 to 10 and the nine defects the plan burn-in found
Twelve rungs proven, one not. The typed mismatch never fired, and both
attempts to force it tested the wrong thing: a file the agent can restore is
reversible, and a human correction is authoritative. Neither is a case where
the repository contradicts the plan.

Nine defects, all fixed. Three of them are one shape, a constraint the code
enforces and no brief states. The fourth is what made them expensive: a
refused handoff had no feedback loop, so the same rejected bytes were re-read
at every boundary until the task hit retry_limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 16:50:25 +04:00
kami 38aa0738a6 Tell the agent its handoff was rejected instead of looping on the file
A refused handoff had no feedback loop. PrepareRelease read the report, the
parser refused it, the worker recorded the error in health, and the next
boundary read the same bytes and refused them again. Run 10 spent four leases
that way and the agent was never told anything.

The plan-progress path already had the answer: answerRefusedProgress says why,
drops the file, and lets the agent write a corrected one. The release path now
does the same, gated on a typed ErrInvalidHandoffAnswer so a transport or Git
failure keeps its retry.

This is the silent-loop shape CLAUDE.md names, in a path nobody had checked.
The three format fixes above it each removed one trigger; this removes the
loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 16:28:30 +04:00
kami 015764ec7b State the handoff length limit on every field it bounds
The prompt gave "at most 200 characters" for NEXT, WHY and REMAINING. The
validator applies it to OPEN Q and LEARNED as well, and run 10 lost a lease to
a 219-character OPEN Q against a limit nobody had stated for that field.

Third instance of one shape: a constraint the code enforces and no brief
mentions. The other two were the research finding id and the dead-end
separator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 16:08:10 +04:00
kami d7e75e9e31 Render a pending manual gate as stale when the tree has moved
f4dbcf7 taught the verified branch about staleness and left the pending one.
Run 10 showed a successor "automated checks passed at 94bd45c3b5d6" while HEAD
was 7d04aef: the automated half of that phase was established against code
that had since changed, and nothing said so.

A human about to confirm the manual steps of a phase deserves to know the
automated half no longer describes the tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 15:55:29 +04:00
kami fda78cf6e6 Stop refusing a handoff over words the arrow already says
98f1b2d fixed the arrow and left the other half. "tried" and "failed because"
restate the field names either side of a separator that already says what the
line means, and the parser required both.

Run 9 lost three leases to this. Every refused line carried exactly the data
the fields want:

    tried run line ["bash", "scripts/test_healthcheck.sh"] for phase-3
      -> refused, policy allows only bash -n on one file
    tried finding ids "F1".."F8" in research.json
      -> schema requires lowercase letters, digits, dash or underscore

Both prefixes are now optional. A line with no cause and effect is still
refused, because the split is the thing being validated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 15:38:34 +04:00
kami fb7135e1d9 Refuse a plan command outside project policy when the plan seals
The brief tells the planner "a command outside its policy is refused when you
seal, not later". It was not. The only caller of VerificationPolicy.Allows was
PlanPhaseCommands, which runs when the implementer asks to verify: one phase,
one session and one rotation after the planner could have fixed it.

Run 9 sealed ["bash", "scripts/test_healthcheck.sh"] against a policy that
allows neither shape, and the phase request was accepted.

The check now runs beside citation resolution, on the coordinator, where the
project is already in scope. A project with no verification policy can still
seal a plan; it cannot seal one that declares run: lines, which matches what
an absent policy already meant at verification time.

Test fixtures gained a policy for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 15:27:39 +04:00
kami 98f1b2dccc Stop losing a lease to an arrow character
Run 9's planner wrote a dead end as "tried X -> failed because Y". The content
was exactly what the protocol asks for. The parser split on "→" only, refused
the handoff, and a refused handoff fails the release rather than the turn, so
the lease expired with the work intact and unrecorded.

Accept either arrow. A separator is not the thing being validated.

The prefix stripping was also wrong on the right-hand side: it trimmed
"failed because " from an untrimmed string that started with a space, so every
parsed dead end kept the literal prefix in why_failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 15:11:06 +04:00
kami 4712c7dc0e State the seal rules a JSON shape cannot show
Run 9's research seal was refused for writing "F1" as a finding id. The rule
is real and the message was precise, but no brief had ever stated it: the
shape block shows keys and types, and a format constraint is neither.

The agent recovered in fifteen seconds, so this cost one boundary rather than
a run. It is still a refusal nobody had to earn, and the same shape hid F38 a
few runs ago.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 15:06:54 +04:00
kami f4dbcf70f6 Stop telling a successor the tree is settled when every phase is stale
Run 8 rendered three phases as "verified at 14654d6cab63, stale because the
tree is now at 56d9b9acdd59" and then printed "Every phase is verified" under
them. The staleness was honest and the conclusion was not.

Stale still counts as done. The usual reason a phase goes stale is the next
phase's own commit, and treating that as unverified would send the implementer
back to phase 1 after every commit. What the successor needed was the names:
which phases were verified against code that has since changed, and an
instruction to reverify what its work touches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 14:41:59 +04:00
kami 49409c9dd3 Give the manual plan-phase gate a producer
The gate had two live consumers and no producer. store.go resolves a pending
sign-off by subject, and manuallySignedOff checks for a prior one, but every
comment Orchestra imported was hardcoded to operator_instruction. A phase
carrying a manual step could reach awaiting_manual_verification and never
leave it.

A comment whose first line reads "orchestra verify <phase-id>" now approves
that phase. Orchestra supplies the plan ref from the task's own accepted plan,
so the approval binds to the plan that was current when the human wrote it.
Everything else still lands under operator_instruction, which is what keeps a
generic "looks good" from satisfying a gate nobody was discussing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 14:15:24 +04:00
kami 4b320809bd Stop a delivered TASK.md from poisoning every later task
Delivery merges the task branch, and that branch carries the "orchestra:
TASK.md" commit. Master therefore ends up holding the previous task's TASK.md,
and the next worktree branches from it.

writeTaskFile returned early on os.Stat, so it left that inherited file in
place. The worker then hashed the current task and every immutability check
failed with "TASK.md changed" against a hash for a task nobody was running.
Releases failed, rotation never relaunched, phase requests were never read, and
the task died on retry_limit without leaving research.

Presence is not identity. Compare content, and rewrite when it differs.

Found live in run 7, the first task to start after run 6's pull request merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 14:15:23 +04:00
kami 7700dd60c6 Hand off with the lifecycle frozen and the plan machinery unproven
The tag orchestra-conformance-v1 marks the full original lifecycle, live-proven
by run 6. Both deployed halves sit there deliberately.

The four plan-machinery commits above it pass build, vet and test, and have
never run against a live agent. The handoff records what they need before they
can: a verification policy on test-e2e, both halves rebuilt together, and F18
fixed first, since the burn-in will generate the retry loops it exists to make
visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 12:25:30 +04:00
kami c76112a309 Make a contradicted plan a typed report, and the reopen Orchestra's
An implementer that finds the plan contradicted by the code had two options,
both bad: work around it silently, or improvise a different plan inside the
phase meant to execute one. PlanMismatch is the third.

The report carries an observation and nothing else. It may not propose a
replacement plan, because writing the next plan is the planning phase's work.
requested_action stays advisory: replan, research, or human_decision is a
recommendation, and Orchestra decides.

Staleness is checked before anything is recorded. A report names the plan ref
and the commit it was written against, both filled by the worker from what it
can verify rather than from what the agent asserted. A report against an older
plan says nothing about the current one, and one against an older tree may
already be fixed. Neither is replayed.

The reducer keeps two things apart that are easy to conflate:

    mismatch recorded  !=  plan superseded

A plan stops being accepted only when a replacement is actually sealed, so an
abandoned replan leaves the accepted plan and its verified progress intact. On
a real re-seal the old ref moves to PlanHistory and its progress stops counting,
while the verification events stay in the log as provenance.

human_decision never reopens. It blocks with a packet stating what was observed
and what it contradicts, and a human answer can resolve the contradiction
without resealing anything: the plan, its progress and the phase all survive,
and the answer outranks the plan where they differ. Turning every ambiguity
into a replan would put the planner above the person who set the goal.

The backward edge is Orchestra's alone. CanReopenPhase is separate from
CanTransitionPhase, which every path validating an agent's request uses, so
phase-request.json still refuses a move back. An agent asks by reporting a
mismatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 12:19:17 +04:00
kami a221502356 Let Orchestra establish plan progress instead of the implementer asserting it
A detailed plan that nothing enforces is a document. This makes the phases
executable: the implementer may write exactly one status, and every other
status is a conclusion Orchestra reaches by running the plan's own commands.

    agent may request:  ready_for_verification
    agent may not assert: verified, awaiting_manual_verification, failed, skipped

The worker resolves commands from the coordinator, never from the request, so a
request cannot smuggle in a command the planner did not write. They run as argv
through exec with Dir set to the worktree, which is the quality gate's existing
envelope and not a weaker one. There is no shell, so a pipe is a literal
argument.

Project policy decides executable reach. registry.Project.Verification matches
argv positionally, and an absent policy refuses everything: a plan command is
agent-authored, so inheriting the operator-authored gate's reach by default
would be the wrong direction to fail in. A refused command is refused before
anything runs, and the refusal names the project and the command so the planner
learns its real reach.

Two bindings make the record mean something later. PlanRef, so progress earned
under plan A cannot survive into plan B. AtSHA, so "verified" does not outlive
the code that made it true: a record whose commit has moved is retained as
provenance and rendered as stale, never as a claim about the current tree.
Both are the same failure this codebase already fixed for reviews, which bind
to the commit they examined.

Manual steps hold a phase at awaiting_manual_verification. The sign-off is an
ordinary human decision whose subject carries the plan ref and the phase id, so
a later "looks good" on an unrelated thread cannot satisfy a gate nobody was
discussing.

A plan sealed before plan.md declares no executable unit, and says so: the
implement context states that phase progress is unavailable and the work
continues under the old semantics. Inventing phases it never had would be worse
than admitting it has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 11:59:39 +04:00
kami 57c028f94f Seal the plan as a specification instead of four bullet lists
The plan artifact was Changes{Target,Intent} plus three string lists, every
entry capped at 500 single-line characters. That bound makes a specification
impossible: a phase cannot carry a code block, a paragraph of reasoning, or a
verification command with its own argument list. renderSealed then flattened
what little survived through collapse(), so an implement session received a
summary of a summary.

plan.md replaces it. Markdown, 128 KiB, no per-line cap, sealed through the
existing path under the existing PlanRef. The parser enforces the structure the
brief states: required sections, phases numbered from 1 with no gaps, Files,
Changes and Verification per phase, and at least one automated or manual check,
because a phase nobody can verify can never be established as done. Automated
entries are JSON argv arrays, so a pipe is a literal argument rather than an
operator. Headings inside fenced blocks are content, so a plan may show
markdown without parsing its own example.

Citations resolve at seal time against the accepted research, on the
coordinator, which is the only party holding ResearchRef. A plan resting on a
finding nobody recorded fails on the planner while its session is still alive
to be told.

The plan now renders byte for byte into the implement launch, and a rotated
successor receives the same complete document. That is the property the whole
change exists for. collapse() stays for research findings, which really are
short claims.

DecodeStoredPlan reads pre-markdown refs and renders them into the same type,
labelled, so nothing downstream branches on which era a plan came from. A
legacy plan carries no phases, which is honest: the old artifact never named an
executable unit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 11:36:45 +04:00
kami 822f086451 Make research findings citable
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
2026-08-28 11:21:29 +04:00
kami 4e244d8104 Record run 6, the four missing links, and the baseline conformance run
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 11:07:41 +04:00
kami 063a3ab9ad Clear the previous cycle's findings when review is entered again
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
2026-08-28 11:03:42 +04:00
kami 4af9880b86 Start the submission reflection loop after something can fill its map
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
2026-08-28 10:53:47 +04:00
kami 6ccc755999 Stop the Gitea source ingesting Orchestra's own pull requests
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
2026-08-28 10:51:22 +04:00
kami c11bf0eff2 Tell the reviewer when its findings file is missing or malformed
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
2026-08-28 10:43:00 +04:00
kami e8d04d719d Give the reviewed change a path to the human
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
2026-08-28 10:28:50 +04:00
kami d9a5a61965 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
2026-08-28 06:58:48 +04:00
kami dcd9af4806 Exclude the control directory, not the ignored marker inside it
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
2026-08-28 03:45:40 +04:00
kami 2417a39a1e Stop counting harness chrome as agent progress, and shorten the lease
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
2026-08-28 03:08:45 +04:00
kami 61442d801c Record the worker install and run 5's renewed lease
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
2026-08-28 02:33:28 +04:00
kami 49fc9d9df1 Hand off with the chain proven and run 5 parked in review
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
2026-08-28 02:32:47 +04:00
kami bcaf0cc285 Tell the terminal phase how to finish
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
2026-08-28 02:16:36 +04:00
kami 6eef1096bf Record run 5 proving the whole chain
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
2026-08-28 02:06:13 +04:00
kami cd1675a90c Record F38 and F39
The agent guessed an undocumented schema and was never told it guessed wrong.
The operator noticed the stall before the system reported anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
2026-08-28 01:59:12 +04:00
kami 0bd86e28c6 Tell the agent the artifact shape, and tell it when the shape is wrong
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
2026-08-28 01:58:36 +04:00