feat: parallel delegation — natural-language best-of-N + experimental cooperative waves (+ fixes found by live runs)#98
Merged
Conversation
krimvp
enabled auto-merge
July 8, 2026 05:14
…t reason text An LLM approver rewords its veto every round, so the fresh-veto excuse (issue #54) — keyed on the veto reason differing from the prior feedback text — renewed forever: a worker that never changed the tree looped to maxIterations, burning approver spend on every iteration (reproduced: 10 no-diff iterations, 340k approver tokens, on a run that should have aborted after the one excused turn). The excuse is now keyed on the feedback's SOURCE: LoopCtx tracks whether the just-run turn's feedback came from the verifier or a veto, and a no-diff iteration is excused only when that turn was NOT already answering a veto. One-shot by construction, wording-independent. Same scenario now aborts at iteration 2 with the actionable no-diff reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa
…arness seam The LLM provider already refuses the ambient session id (goaly nested under Claude Code adopts and reports it instead of minting a fresh session), but the HARNESS still recorded it as the worker's session — observed live: a claude-harness run logged the OUTER Claude Code session id in AGENT_RAN and printed it as the interactive resume hint. Iteration 2, --resume, or --from-run --inherit-session would then resume the outer conversation into the worker. The guard now lives once in the shared codec core (ambientSessionId in src/agent-cli/codec.ts): runCodecHarness refuses the ambient id as a resume target and scrubs it from classified results (coerced to the codec's unknown-session sentinel, which every consumer already skips); the LLM provider imports the same helper. Verified live: the same run now records claude-unknown and suppresses the resume hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa
The resume guard only knew the codec's OWN unknown-session sentinel, so a run whose log carried a DIFFERENT harness's sentinel (e.g. the fake harness's noop-session) threaded it into 'claude --resume noop-session', crashing every turn/candidate — observed live on a resumed run. The sentinel skip-list now lives in the id domain (src/domain/ids.ts, with the previously missing best-of-error sentinel added) so the harness core can refuse all of them without importing persistence; runlog/session-id re-exports it for its existing consumers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa
…urnament
Say it instead of flagging it: a delegation directive in the goal
('fix the flaky test, work with 4 subagents', '… using 3 parallel
attempts', 'use subagents' => 3) maps onto the existing --candidates
tournament (issue #85). Detection is a small DETERMINISTIC grammar
(src/cli/delegation.ts), never an LLM parse, and deliberately narrow:
only 'subagents' introduced by a delegation verb and 'N parallel
attempts|candidates|tries' trigger it, so app-domain goals ('a queue
with 4 parallel workers') never match. The directive clause is STRIPPED
from the goal — it must never enter the frozen contract, where the
judge/approver would read it as an unverifiable criterion — and the
interpretation is loudly logged; the explicit --candidates/--best-of
always wins; above-cap counts fail closed like the flag.
Mid-run steering rides ADR 0012: the same grammar reads a --resume
note ('try 4 parallel attempts'), lifting the directive out into a new
'candidates' field on the RUN_EXTENDED overlay (an operational knob
like maxIterations — the frozen contract stays structurally
unreachable); remaining note text still steers the worker. --candidates
is now also directly extendable at resume.
Also: a --resume without an explicit --harness now ADOPTS the run's
recorded harness instead of silently switching to the default CLI —
session ids are harness-specific, and live testing showed a resumed
fake-harness run spawning the real claude CLI with the prior harness's
sentinel session.
README, landing page, and CLI usage updated in the same change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa
…rallel-phases) Best-of-N runs K COMPETING attempts and discards K-1; this adds the COOPERATING shape: consecutive plan phases sharing a 'group' value (an optional new SubGoal field, frozen into planHash; groupless plans keep their legacy hash byte-for-byte) execute as one concurrent WAVE, each phase as its own frozen, two-key CHILD goaly run in an isolated git worktree with its own write-ahead log, all children metered by the one shared --budget-tokens. The recombination is the load-bearing piece and it never trusts a merge: DONE children merge in phase order via real 3-way 'git merge-tree --write-tree' plumbing (objects only — a conflicted merge applies nothing), the merged tree is promoted, and each merged child's frozen DETERMINISTIC rungs re-run on the combined tree (two individually-green changes can still break each other). A conflict, a red re-verify, a child that never reaches DONE, a thrown wave runner, or a missing wave seam all DOWNGRADE that phase fail-closed to the classic sequential run on the merged tree — the worst case of the feature is exactly today's --phased — and the cumulative ACCEPTANCE contract still gates the whole run, so no decomposition can green a goal whose parts pass but whole doesn't. Reducer purity is preserved structurally: startPhaseCompile emits ONE RUN_WAVE command (per-phase configs derived exactly as for sequential phases) and folds ONE WAVE_RAN event; PhaseCtx gains optional skip/waved bookkeeping so merged phases are skipped and an attempted group never re-fans-out; children are separate pure folds over separate logs. Replay treats WAVE_RAN's post-merge checkpoint tree like PHASE_ADVANCED. Wave-child spend rides WAVE_RAN outcomes into the parent usage fold (bucketed under harness; totals and the budget cap stay exact). Opt-in and fenced: --parallel-phases requires --phased and --autonomous (children seal concurrently; contracts still frozen + loudly logged); groups come from --plan-file in v1; grouped plans run strictly sequentially without the flag; a crash mid-wave re-runs the whole wave on --resume. Covered by reducer table tests, wave-runner unit tests (worktree-leak regression included), a driver fail-closed test, real-git merge-primitive tests, and two end-to-end pipeline tests (clean wave → DONE; conflicted wave → sequential downgrade → DONE) on a routed fake LLM. README + landing page + usage + ADR 0017 document it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa
krimvp
force-pushed
the
claude/loady-parallel-delegation-89wl1s
branch
from
July 8, 2026 05:15
baaefb4 to
bd4343a
Compare
CI (no 'claude' on PATH) caught an ordering bug in the resume harness
adoption: the preflight validated the DEFAULT harness binary before the
resume branch swapped in the run's recorded harness, so a host without
the default CLI refused to resume a fake/codex run it could perfectly
continue ('the claude CLI was not found on PATH' instead of adopting
'fake'). The --resume validation block (missing/corrupt run, harness
adoption, DONE-extension guard, effective-config fold) now runs before
the preflight, which then checks the harness the resumed run will
actually use. Verified by running the adoption test with the claude
binary hidden from PATH — the exact CI condition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Three commits of parallel-delegation capability plus three real bugs found (and reproduced) by doing live runs.
1. Natural-language delegation → best-of-N (competing)
A delegation directive in the goal —
"fix the flaky test, work with 4 subagents","… using 3 parallel attempts","use subagents"(⇒ 3) — maps onto the existing best-of-N tournament (--candidates, issue #85). Mid-run, the same grammar reads a--resumenote (--note "try 4 parallel attempts") and lifts the directive into a newcandidatesfield on theRUN_EXTENDEDoverlay (ADR 0012).src/cli/delegation.ts); deliberately narrow vocabulary — app-domain goals ("a queue with 4 parallel workers") never match, pinned by tests.2. EXPERIMENTAL: cooperative parallel waves (
--parallel-phases, opt-in)The cooperating shape best-of-N deliberately isn't: consecutive plan phases sharing a
groupvalue (new optionalSubGoalfield, frozen intoplanHash; groupless plans keep their legacy hash byte-for-byte) execute as one concurrent wave, then merge — without weakening a single guarantee (ADR 0017):drive()embedded) in an isolated git worktree: its own compiled + frozen contract, iterations, ladder, veto-only Sign-off (both keys per child), its own write-ahead log — all children on the ONE shared--budget-tokensmeter and the parent's interrupt probe.git merge-tree --write-treeplumbing (objects only, no commits/HEAD/index movement); a textual conflict applies nothing of that child. Compiler-authored (git-excluded) verification files are carried over so frozen commands keep their inputs.--phased. The cumulative ACCEPTANCE contract still gates the whole run.--phased --autonomous(children seal concurrently — still frozen + loudly logged); groups via--plan-fileonly; grouped plans run strictly sequentially without the flag; crash mid-wave re-runs the whole wave on--resume; wave-child spend bucketed under the parentharnesslayer (totals + budget cap exact).3. Fixes from live runs (each reproduced first, each with a regression test)
decide.ts) — keyed on veto reason text, which an LLM rewords every round: a no-op worker burned 10 iterations / 340k approver tokens. Now keyed on the feedback's source (verifier vs veto) inLoopCtx; same scenario aborts at iteration 2.CLAUDE_CODE_SESSION_ID(codec.ts) — a nested run recorded the outer Claude Code session as the worker's session (observed live inAGENT_RAN+ the resume hint). Guard now lives once in the shared codec core, covering surfacing and resuming.--resume+ silent harness switch on resume — observed live asclaude --resume noop-sessioncrashing every candidate of a resumed fake-harness run. Resume now adopts the run's recorded harness (explicit--harnessoverrides, with a printed notice); the sentinel skip-list moved to the id domain (gaining the missingbest-of-error); the codec refuses every sentinel.Type of change
Definition of done
npm run typecheckcleannpm testgreen (139 files, 1764 tests)README.md(new waves section + best-of-N/NL delegation + operator control),docs/index.html(new wave card + best-of-N and operator-control cards), CLI usage text, ADR 0017Invariant impact
Touches
src/orchestrator/and the RUN/EVENT vocabulary, so explicitly:startPhaseCompileemits ONERUN_WAVEcommand (per-phase configs derived by the same purephaseConfigFor) and folds ONEWAVE_RAN;PhaseCtxgains optional pureskip/wavedbookkeeping. Children are separate pure folds over separate logs — they never enter the parent's state machine. The earlier decide.ts change adds a purefeedbackSourcefield.planHash(legacy hashes unchanged); every child contract is frozen at its own Seal; merge/re-verify cannot rewrite anything;RUN_EXTENDEDstill has no field for goal/verifier/rubric.unmergeddowngrade to sequential — never a crash, never a skipped phase, never an unverified merge; NL directives never guess (no match ⇒ classic run); sentinel/ambient session ids refused at the resume seam.WAVE_RANoutcomes andRUN_EXTENDED.candidatesare Zod schemas; plangroupvalidated.WAVE_RANcarries the post-merge checkpoint tree and replay re-points baselines likePHASE_ADVANCED; children keep their own write-ahead logs; thecandidatesoverlay rides the existingextendedRunConfigfold.detectStuckunchanged.Demo
Live run (real
claudeharness, grouped 2-phase plan,--parallel-phases):Nothing crashed, nothing was greened unverified, and the deterministic e2e tests cover the all-merged → acceptance → DONE path.
Notes for reviewers
🤖 Generated with Claude Code
https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa