feat(tour): multi-project parallel fan-out — one runner per repo

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.
This commit is contained in:
Bastien Chanot
2026-08-24 14:18:59 +02:00
parent 4ededc75ab
commit 543b0c811a
6 changed files with 90 additions and 7 deletions
+8
View File
@@ -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).
+1
View File
@@ -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.
+3 -3
View File
@@ -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).
+10
View File
@@ -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
+11
View File
@@ -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'
+57 -4
View File
@@ -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 — <project basename>",
prompt="Read ~/.claude/skills/tour/SKILL.md and execute STEP 1 → STEP 3
for EXACTLY ONE project: <absolute path>. 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: <name|no
branch>, then REPORT: <path>.")
```
The main loop then:
1. Collects each runner's summary row. A dead or mute runner becomes the
row `<path> : RUNNER FAILED — <reason> | 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)