From 543b0c811a881219dba6b46e85ad1dccdc62d881 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Mon, 24 Aug 2026 14:18:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(tour):=20multi-project=20parallel=20fan-ou?= =?UTF-8?q?t=20=E2=80=94=20one=20runner=20per=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two or more project paths dispatch one general-purpose runner per repo, all in a single message, instead of processing repos one by one. The runner inherits the session model — no pin, it carries tour's reflection (fix decisions, convergence) — and every agent inside keeps its defined tier (security-auditor sonnet, Phase B opus, doc-syncer sonnet). A dead or mute runner becomes an explicit RUNNER FAILED summary row; the gated capitalize offer stays in the main loop, never in a runner. Bounded LRN-083 derogation recorded in BDR-084: the per-project fix loop moves into its runner, but nothing a runner decides touches shared state — independent repos, per-repo chore branches, branches left unmerged for human review exactly as inline. Mechanics proven before building: nested probe, 3 sub-agent windows all overlapping, 9.1s vs ~18s sequential. Census §12: 6 locks, flip-tested. Single-project path unchanged. --- .claude/memory/decisions.md | 8 +++++ .claude/memory/journal.md | 1 + .claude/tasks/TODO.md | 6 ++-- CHANGELOG.md | 10 ++++++ lib/tests/model-routing.test.sh | 11 ++++++ skills/tour/SKILL.md | 61 ++++++++++++++++++++++++++++++--- 6 files changed, 90 insertions(+), 7 deletions(-) diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index 826eb5a..f35ce82 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -93,6 +93,7 @@ rules: | BDR-073 | 2026-07-17 | Scoring: LLM judges findings+severity, engine does the arithmetic (deterministic /20) | accepted | | BDR-080 | 2026-07-21 | Bug routing inverted: /bugfix primary, /investigate explicit-only | accepted | | BDR-083 | 2026-08-24 | Contract gates: deterministic floor (GATE 0) under the fresh verifier | accepted | +| BDR-084 | 2026-08-24 | /tour multi-project: parallel runners (LRN-083 derogation, bounded), runner inherits session model | accepted | --- @@ -1088,3 +1089,10 @@ REFUSED + why: Stop hook `decision:"block"` — contradicts "STOP + human escala Shipped: lib/gates.sh (~250 l bash; `status` never executes and never writes · `run` ALWAYS re-executes every runnable criterion — trusting written evidence is the failure being closed, so there is no incremental mode to get wrong; rc 0 MET / 2 UNMET|malformed / 3 ABANDONED; parse fails closed on partial oracle, duplicate id, unindented attribute, runnable-without-EVIDENCE, and executes nothing when the ledger is malformed). GATE 0 in lib/verify-secure-loop.md (red floor → executor re-dispatch with the NOT-MET rows, NO verifier spawned; own 3-iteration budget, separate from conformity; malformed ledger fixed in the main loop, never dispatched to a dev). Order invariant now GATE 0→1→2 on every re-loop. lib/contract-interview.md: ORACLES section + template + ABANDONMENT lifecycle + per-flow oracle weight. agents/verifier.md: oracle-consumption rules — a red or unrun oracle is NEVER overridden by reading code; a MET oracle proves the ORACLE, not the English sentence ⇒ vacuous oracle = NOT-MET, the one judgement no command can make; verifier may re-run a CHECK but never edits the contract. lib/tests/gates.test.sh 64 assertions (sentinel-proved non-execution, with its own positive control asserted first). Alternatives rejected: Palier 1 doctrine-only (CHECK:/EXPECT: become decorative without an executant); port the Node checker (stack break, shellcheck-blind); fold ABANDONED into ECARTS (would send a dev to fix the impossible and eat the 3-iteration budget); `status` revalidating old evidence (that trust is the failure being closed). Branch feature/contract-gates, UNMERGED (human gate). `make test` rc 0, shellcheck clean, e2e verified on a real contract in the documented template. + +### BDR-084 — /tour multi-project: parallel runners, bounded LRN-083 derogation [accepted] (2026-08-24) +User asked whether agent parallelism on independent tasks is ACTIVE. Measured first (LRN-080): (a) mechanics — nested probe, 1 dispatched orchestrator fanned 3 sub-agents, execution windows all overlap, 9.1s vs ~18s sequential ⇒ nested parallel dispatch WORKS; (b) doctrine — already prescribed at 3 layers (harness "single message" injection; /seo, challenge-plan, /cso, graphify explicit same-message mandates; graphify even anti-sequential wording); remaining serializations all MOTIVATED (audit-delta crash-resilience documented, verify-secure-loop order invariant); (c) behavior — probe orchestrator batched spontaneously without being told "parallel" (N=1), this session fanned 8+7 agents/message during the RED. Conclusion: nothing to add globally — a CLAUDE.md "parallelize" line would duplicate-stack the harness injection (BDR-081 anti-pattern). +ONE real sequential-but-independent candidate: /tour multi-project (independent repos, one by one, no documented reason). User gate: option "tout paralléliser" chosen over report-only-only and no-change, WITH the model invariant "orchestrateur garde le modèle orchestrateur; skills/agents suivent leurs orchestrateurs définis". +Decision: STEP 0 routes (1 project = inline unchanged; ≥2 = STEP 0b fan-out). One general-purpose runner per project, ALL in ONE message, dispatched with NO model override — inherits the session model (model-gate already validated big; a runner carries tour's reflection: fix decisions, convergence). Inside a runner every agent keeps its defined tier (security-auditor sonnet, Phase B opus, doc-syncer sonnet two-mode). Dead/mute runner ⇒ explicit `RUNNER FAILED` summary row (mute is never a pass). Capitalize offer stays MAIN LOOP ONLY (registries = shared state). +LRN-083 derogation, bounded: per-project fix loop + convergence now run INSIDE the dispatched runner. Bounded because nothing a runner decides touches shared state — independent repos, per-repo chore branches, branches stay UNMERGED for human review exactly as inline (report-as-approval-gate design unchanged). Precedent: client-handover-writer already a dispatched orchestrator running parallel audit loops (BDR-077). +Alternatives rejected: report-only-only parallel (my recommendation — user overrode: full parallel wanted); one sub-orchestrator agent .md file (drift risk vs SKILL.md, the runner reads the skill from disk instead — client-handover→/seo precedent); pinning the runner (would put tour reflection on an executor tier — inverts BDR-076); global CLAUDE.md parallelism line (duplicate of harness injection). Census §12: 6 locks (fan-out present, no-pin, single-message, capitalize main-loop, RUNNER FAILED, no pinned runner), flip-tested. Branch feature/tour-parallel, UNMERGED (human gate). diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 17e70db..f7d5cff 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -439,3 +439,4 @@ rules: - Shipped Palier 2 (user-chosen): lib/gates.sh + GATE 0 + oracle-bearing criteria + `ABANDONED(n)` verdict + 4-pass executors. Refused unlazy's Stop hook, approval store, .unlazy/ tree, tree-N arithmetic, Node checker — [[BDR-083]] records each why. - `make test` rc 0, shellcheck clean, 64 new assertions, e2e on a real contract. Branch feature/contract-gates UNMERGED (human gate). - Locks caught a reflow regression (5 red on rewrapped phrases, zero doctrine lost) → [[LRN-142]]. Skill-adoption pattern → [[LRN-141]]. +- Parallelism audit (user ask "est-ce actif ?"): measured, not assumed — nested probe proves concurrent fan-out (9.1s vs 18s), doctrine already prescribed everywhere safe, remaining serializations motivated. One candidate found: /tour multi-project → parallel runners shipped ([[BDR-084]], user gate "tout paralléliser" + model invariant). Branch feature/tour-parallel UNMERGED. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 43863bf..e9784e7 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1209,12 +1209,12 @@ préalable: probe imbriquée 3 sous-agents, fenêtres chevauchantes, 9.1s vs un runner dispatché) → à consigner BDR-084. Repos indépendants, branches chore par repo, report-as-approval-gate ⇒ rien de partagé n'est décidé dans un runner; capitalize reste main-loop. -- [ ] T1 skills/tour/SKILL.md — STEP 0 routé (1 projet = inline inchangé; +- [x] T1 skills/tour/SKILL.md — STEP 0 routé (1 projet = inline inchangé; ≥2 = fan-out) + STEP 0b: un runner general-purpose par projet, TOUS dans UN message, SANS pin modèle (hérite session, model-gate déjà passé); agents internes gardent leurs tiers définis; runner mort = ligne RUNNER FAILED, jamais absent silencieux; capitalize main-loop. -- [ ] T2 locks census §12 dans lib/tests/model-routing.test.sh (fan-out +- [x] T2 locks census §12 dans lib/tests/model-routing.test.sh (fan-out présent, runner non-pinné, single message, capitalize main-loop). -- [ ] T3 BDR-084 + CHANGELOG + journal. +- [x] T3 BDR-084 + CHANGELOG + journal. - [ ] T4 make test + shellcheck; PAS de merge (gate humain). diff --git a/CHANGELOG.md b/CHANGELOG.md index e781ae8..6f27f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] ### Added +- **/tour multi-project parallel fan-out (BDR-084)** — two or more + project paths now dispatch one runner per repo in a single message + (independent working trees, nothing collides) instead of processing + them one by one. The runner inherits the session model (no pin — it + carries tour's reflection); every agent inside keeps its defined tier. + A dead runner surfaces as an explicit `RUNNER FAILED` summary row; the + gated capitalize offer stays in the main loop. Bounded LRN-083 + derogation recorded in BDR-084. Census §12: 6 locks, flip-tested. + Mechanics proven first: nested probe, 3 overlapping agent windows, + 9.1s vs ~18s sequential. - **Contract gates — deterministic floor under the fresh verifier (BDR-083)** — an acceptance criterion can now carry an oracle (`CHECK:` command + `EXPECT:` success-only marker + `EVIDENCE:` slot). `lib/gates.sh run diff --git a/lib/tests/model-routing.test.sh b/lib/tests/model-routing.test.sh index ae5342b..740dac5 100755 --- a/lib/tests/model-routing.test.sh +++ b/lib/tests/model-routing.test.sh @@ -70,6 +70,17 @@ has "agents/validator-analyzer.md" 'model: sonnet' has "agents/plan-challenger.md" 'model: opus' fm_lacks "agents/client-handover-writer.md" 'model:' fm_lacks "agents/interviewer.md" 'model:' +# 12) tour multi-project fan-out (BDR-084) — runner INHERITS the session +# model (no pin: it carries reflection), inner agents keep their tiers, +# dispatch is single-message, capitalize stays in the main loop. +has "skills/tour/SKILL.md" 'STEP 0b — MULTI-PROJECT FAN-OUT' +# shellcheck disable=SC2016 # literal backticks — no expansion intended +has "skills/tour/SKILL.md" 'NO `model` override' +has "skills/tour/SKILL.md" 'ALL in a SINGLE message' +has "skills/tour/SKILL.md" 'MAIN LOOP ONLY, never inside a' +has "skills/tour/SKILL.md" 'RUNNER FAILED' +lacks "skills/tour/SKILL.md" 'description="tour runner", model=' + has "skills/onboard/SKILL.md" 'model="opus"' has "skills/tour/SKILL.md" 'model="opus"' has "lib/challenge-plan.md" 'BDR-076' diff --git a/skills/tour/SKILL.md b/skills/tour/SKILL.md index d7aefaa..9b3535b 100644 --- a/skills/tour/SKILL.md +++ b/skills/tour/SKILL.md @@ -54,11 +54,64 @@ integrate anything (merge/finish/push). ## STEP 0 — ARGS & PROJECT LIST -- Paths in `$ARGUMENTS` → project list, processed **sequentially** in - the given order. No paths → current repo only. -- `--report-only` → run every audit, apply NO fix, write reports only. +- Paths in `$ARGUMENTS` → project list. No paths → current repo only. +- ONE project → run STEP 1 → STEP 3 inline in this loop (nominal path, + unchanged). +- TWO OR MORE projects → parallel fan-out, STEP 0b: the repos are + independent working trees on independent chore branches — nothing the + runners write can collide. +- `--report-only` → run every audit, apply NO fix, write reports only + (the flag is forwarded to every runner). - A failure in one project never aborts the tour: record it in that - project's report section and move to the next. + project's report section — or as its global-summary row when the + runner itself died — and move on. + +## STEP 0b — MULTI-PROJECT FAN-OUT (one runner per project, BDR-084) + +Dispatch ONE runner per project, ALL in a SINGLE message — sequential +dispatch of independent repos defeats the purpose. + +Model discipline (the user-fixed invariant behind this mode): + +- The runner is dispatched with **NO `model` override** — it inherits the + session model, already validated big by the MODEL GATE above. The runner + carries this skill's reflection (fix decisions, convergence calls); it + must never be pinned down to an executor tier. +- Inside a runner, every dispatched agent keeps the tier this skill + already defines: security-auditor (sonnet frontmatter), the Phase B + audit (analyzer opus pin or `model="opus"`), doc-syncer (sonnet + frontmatter, its two-mode contract untouched). + +Runner dispatch, one per project: + +``` +Agent(subagent_type="general-purpose", + description="tour runner — ", + prompt="Read ~/.claude/skills/tour/SKILL.md and execute STEP 1 → STEP 3 + for EXACTLY ONE project: . Flags: <--report-only|none>. + Skip STEP 0/0b (routing) and the global summary — the dispatcher owns + them. Every rule of the skill applies unchanged: max 3 iterations, + never merge/finish/push, scoped commits, report appended to that + project's own .claude/audits/TOUR.md. Return EXACTLY: the project's + one-line global-summary row (STEP 3 format), then BRANCH: , then REPORT: .") +``` + +The main loop then: + +1. Collects each runner's summary row. A dead or mute runner becomes the + row ` : RUNNER FAILED — | no report` — never a silent + absence (a mute runner is NEVER a pass). +2. Prints the global summary (STEP 3 format) once ALL runners returned. +3. Runs the gated capitalize offer — MAIN LOOP ONLY, never inside a + runner: registries are shared state, and gate decisions stay here + (LRN-083). + +LRN-083 derogation, recorded in BDR-084: the per-project iteration loop +(fix decisions, convergence) runs INSIDE its dispatched runner. Bounded +because nothing a runner decides touches shared state — independent +repos, per-repo chore branches, branches left UNMERGED for human review +exactly as in inline mode. ## STEP 1 — PRECONDITIONS (per project)