Skip to content

feat: parallel delegation — natural-language best-of-N + experimental cooperative waves (+ fixes found by live runs)#98

Merged
krimvp merged 6 commits into
mainfrom
claude/loady-parallel-delegation-89wl1s
Jul 8, 2026
Merged

feat: parallel delegation — natural-language best-of-N + experimental cooperative waves (+ fixes found by live runs)#98
krimvp merged 6 commits into
mainfrom
claude/loady-parallel-delegation-89wl1s

Conversation

@krimvp

@krimvp krimvp commented Jul 6, 2026

Copy link
Copy Markdown
Owner

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 --resume note (--note "try 4 parallel attempts") and lifts the directive into a new candidates field on the RUN_EXTENDED overlay (ADR 0012).

  • Deterministic grammar, never an LLM parse (src/cli/delegation.ts); deliberately narrow vocabulary — app-domain goals ("a queue with 4 parallel workers") never match, pinned by tests.
  • The directive is stripped from the goal before compile (a leftover "use 4 subagents" would become an unverifiable criterion the approver vetoes on — observed live). Loudly logged; explicit flags always win; above-16 fails closed.

2. EXPERIMENTAL: cooperative parallel waves (--parallel-phases, opt-in)

The cooperating shape best-of-N deliberately isn't: consecutive plan phases sharing a group value (new optional SubGoal field, frozen into planHash; groupless plans keep their legacy hash byte-for-byte) execute as one concurrent wave, then merge — without weakening a single guarantee (ADR 0017):

  • Fork — each wave member is a FULL goaly child run (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-tokens meter and the parent's interrupt probe.
  • Merge — DONE children merge in phase order via real 3-way git merge-tree --write-tree plumbing (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.
  • Re-verify — a merge is never trusted: each merged child's frozen DETERMINISTIC rungs re-run on the combined tree (two individually-green changes can still break each other).
  • Fail-closed to sequential — 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 to the classic sequential run on the merged tree (fresh frozen contract, same sub-goal — the bar never moves, only the starting tree). The worst case of the feature is byte-for-byte today's --phased. The cumulative ACCEPTANCE contract still gates the whole run.
  • Fenced (v1): requires --phased --autonomous (children seal concurrently — still frozen + loudly logged); groups via --plan-file only; grouped plans run strictly sequentially without the flag; crash mid-wave re-runs the whole wave on --resume; wave-child spend bucketed under the parent harness layer (totals + budget cap exact).

3. Fixes from live runs (each reproduced first, each with a regression test)

  1. No-diff veto excuse never expired with a real LLM approver (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) in LoopCtx; same scenario aborts at iteration 2.
  2. Harness seam trusted the ambient CLAUDE_CODE_SESSION_ID (codec.ts) — a nested run recorded the outer Claude Code session as the worker's session (observed live in AGENT_RAN + the resume hint). Guard now lives once in the shared codec core, covering surfacing and resuming.
  3. Foreign sentinels threaded into --resume + silent harness switch on resume — observed live as claude --resume noop-session crashing every candidate of a resumed fake-harness run. Resume now adopts the run's recorded harness (explicit --harness overrides, with a printed notice); the sentinel skip-list moved to the id domain (gaining the missing best-of-error); the codec refuses every sentinel.

Type of change

  • Bug fix
  • Feature (new capability)
  • Enhancement (improve existing behavior)
  • Harness adapter
  • Docs / chore / refactor

Definition of done

  • npm run typecheck clean
  • npm test green (139 files, 1764 tests)
  • New behavior covered by tests — delegation grammar (20 cases incl. false-positive guards) + args wiring; wave reducer table tests (fan-out, opt-in, partial/total downgrade, skip bookkeeping, never-re-fan-out, acceptance advance); wave-runner unit tests (incl. a worktree-leak regression it caught); driver fail-closed missing-seam test; real-git merge-primitive tests (clean disjoint / typed conflict); two end-to-end pipeline tests on real git with a content-routed fake LLM (clean wave → DONE; conflicted wave → sequential downgrade → DONE); regression tests for all three bug fixes
  • None of the eight invariants weakened
  • Docs synced in this change: 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 0017

Invariant impact

Touches src/orchestrator/ and the RUN/EVENT vocabulary, so explicitly:

Demo

Live run (real claude harness, grouped 2-phase plan, --parallel-phases):

INFO  wave: forking children phases=0,1 base=5b69e3c2…
INFO  wave child starting phase=0 root=/tmp/goaly-wt-L2kJF1
INFO  wave child starting phase=1 root=/tmp/goaly-wt-CsHkRM     ← concurrent, isolated
…child p1: run finished status=DONE iterations=1                ← both keys, in its worktree
…child p0: ABORTED (approver veto → excused turn → no-diff)     ← the earlier fix, working live
INFO  wave: merged + re-verified merged=1 total=2 tree=cb3a4637…
# WAVE_RAN outcomes: [{unmerged, index 0, reason "child run ABORTED…"}, {merged, index 1}]
→ phase 0 downgraded to the classic sequential run on the merged tree; zero stray worktrees after.

Nothing crashed, nothing was greened unverified, and the deterministic e2e tests cover the all-merged → acceptance → DONE path.

Notes for reviewers

  • The biggest design question was conflict resolution: there is deliberately no merge agent. A conflicted/red phase becomes ordinary verified work (sequential re-run under a frozen contract) — LLM-free recombination keeps unverified writes off the DONE path.
  • Judge rungs are not re-run post-merge (each child already turned both keys in isolation; acceptance still gates with LLM keys) — the merged-tree guard is the ungameable deterministic bar in between. Trade-off documented in ADR 0017.
  • Commit order tells the story: harness/session fixes → NL delegation → waves.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GQyZKAfKCeAkQZHvKv8EEa

@krimvp krimvp changed the title feat(cli): natural-language parallel delegation + fixes found by live runs feat: parallel delegation — natural-language best-of-N + experimental cooperative waves (+ fixes found by live runs) Jul 7, 2026
@krimvp
krimvp enabled auto-merge July 8, 2026 05:14
claude added 5 commits July 8, 2026 05:15
…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
krimvp force-pushed the claude/loady-parallel-delegation-89wl1s branch from baaefb4 to bd4343a Compare July 8, 2026 05:15
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
@krimvp
krimvp merged commit f258bdc into main Jul 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants