@sylad/cadence 1.12.1 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +3 -0
  4. package/agents/reference-planner.md +26 -0
  5. package/bin/cadence.js +8 -1
  6. package/dist/cli.js +197 -22
  7. package/dist/config-set.js +252 -0
  8. package/dist/config.js +49 -9
  9. package/dist/corrections.js +2 -0
  10. package/dist/git.js +42 -1
  11. package/dist/lead.js +5 -1
  12. package/dist/orchestrate/accept.js +63 -1
  13. package/dist/orchestrate/baseline.js +39 -3
  14. package/dist/orchestrate/briefs.js +1 -1
  15. package/dist/orchestrate/changelog-merge.js +40 -1
  16. package/dist/orchestrate/command.js +120 -19
  17. package/dist/orchestrate/continue.js +5 -2
  18. package/dist/orchestrate/cycle.js +318 -74
  19. package/dist/orchestrate/decisions.js +33 -0
  20. package/dist/orchestrate/guard.js +16 -0
  21. package/dist/orchestrate/integrate.js +26 -3
  22. package/dist/orchestrate/launch.js +4 -1
  23. package/dist/orchestrate/state.js +1 -1
  24. package/dist/orchestrate/workers.js +71 -0
  25. package/dist/orchestrate/worktree.js +15 -4
  26. package/dist/perf.js +12 -2
  27. package/dist/plan-commit.js +88 -0
  28. package/dist/plan.js +41 -3
  29. package/dist/ref/analyse.js +34 -4
  30. package/dist/ref/command.js +2 -1
  31. package/dist/ref/coverage.js +136 -0
  32. package/dist/ref/diff.js +99 -15
  33. package/dist/release.js +249 -0
  34. package/dist/session.js +7 -1
  35. package/dist/stale-notes.js +22 -0
  36. package/dist/verify-gate.js +138 -0
  37. package/docs/deliver.md +37 -0
  38. package/docs/fonctionnement.md +3 -1
  39. package/docs/lead.md +18 -1
  40. package/docs/orchestrate.md +71 -13
  41. package/docs/overview.md +5 -2
  42. package/docs/perf.md +4 -1
  43. package/docs/raf.md +49 -1
  44. package/docs/ref.md +61 -3
  45. package/docs/releasing.md +43 -2
  46. package/docs/session.md +8 -0
  47. package/docs/skills.md +1 -0
  48. package/docs/spec.md +2 -0
  49. package/docs/stabilite.md +2 -0
  50. package/docs/whats-new.md +9 -0
  51. package/package.json +3 -3
  52. package/skills/lead/SKILL.md +6 -4
  53. package/skills/orchestrate/SKILL.md +1 -1
  54. package/skills/session-close/SKILL.md +2 -0
  55. package/skills/session-start/SKILL.md +2 -1
  56. package/templates/orchestrate/fix-docs.md +1 -0
  57. package/templates/orchestrate/fix-minors.md +1 -0
  58. package/templates/orchestrate/fix.md +2 -0
  59. package/templates/orchestrate/implement.md +4 -0
@@ -0,0 +1,138 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { lstatSync, mkdtempSync, rmSync, unlinkSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { onTermination } from './proc.js';
6
+ import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
7
+ import { git, gitFailure, linkDeclared, linkIgnored } from './orchestrate/worktree.js';
8
+ /** Délai de chaque commande de la porte : celui du rejeu de la ligne de base de la vague. */
9
+ const GATE_TIMEOUT_MS = 30 * 60_000;
10
+ /** Lignes de la fin de la sortie montrées quand aucun test n'est identifiable dans une sortie rouge. */
11
+ const TAIL_LINES = 10;
12
+ function killGroup(pgid) {
13
+ try {
14
+ process.kill(-pgid, 'SIGKILL');
15
+ }
16
+ catch {
17
+ // groupe déjà vide
18
+ }
19
+ }
20
+ /**
21
+ * Une commande `sh -c` dans `cwd`, sortie standard et d'erreur réunies ; au délai, tout son groupe de processus est tué.
22
+ * `group` reçoit le groupe en cours : le nettoyage d'un signal reçu par cadence le tue avant de retirer le worktree.
23
+ */
24
+ function sh(cmd, cwd, timeoutMs, group) {
25
+ return new Promise((resolve) => {
26
+ const child = spawn('sh', ['-c', cmd], { cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
27
+ const pgid = child.pid;
28
+ if (pgid === undefined) {
29
+ child.once('error', (e) => resolve({ code: 127, output: e.message }));
30
+ return;
31
+ }
32
+ group.pgid = pgid;
33
+ let output = '';
34
+ for (const stream of [child.stdout, child.stderr])
35
+ stream.on('data', (d) => (output += d.toString()));
36
+ let timedOut = false;
37
+ const timer = setTimeout(() => {
38
+ timedOut = true;
39
+ killGroup(pgid);
40
+ }, timeoutMs);
41
+ child.once('close', (code) => {
42
+ clearTimeout(timer);
43
+ killGroup(pgid); // ce que la commande a laissé en arrière-plan ne survit pas à elle
44
+ group.pgid = undefined;
45
+ resolve({ code: timedOut ? 124 : (code ?? 1), output: timedOut ? `${output}\ndélai dépassé (${Math.round(timeoutMs / 60_000)} min)` : output });
46
+ });
47
+ });
48
+ }
49
+ /**
50
+ * `cadence verify --gate` (L348) : la porte du projet (`orchestrate.build`, puis `orchestrate.test`, dans l'ordre de la vague : les tests lisent ce que le build produit)
51
+ * rejouée au `sha` dans un worktree jetable, hors du dépôt, dont les dépendances non suivies sont reliées depuis le dépôt vivant (`orchestrate.worktreeDeps`,
52
+ * sinon les dossiers ignorés de tout niveau, comme les worktrees de vague). Les deux commandes sont rejouées même si la première est rouge : le compte rendu est entier.
53
+ * Le worktree est retiré dans tous les cas. 0 vert, 1 rouge (avec les tests en échec), 2 rien à rejouer ou worktree impossible.
54
+ */
55
+ export async function verifyGate(ctx) {
56
+ const { repo, sha, config, out } = ctx;
57
+ const steps = [
58
+ ...(config.build ? [{ name: 'build', cmd: config.build }] : []),
59
+ ...(config.test ? [{ name: 'tests', cmd: config.test }] : []),
60
+ ];
61
+ const short = sha.slice(0, 7);
62
+ if (steps.length === 0) {
63
+ out('verify --gate : ni orchestrate.test ni orchestrate.build déclarés dans cadence.yaml — rien à rejouer');
64
+ return 2;
65
+ }
66
+ const dir = mkdtempSync(join(tmpdir(), 'cadence-verify-'));
67
+ const links = [];
68
+ const group = {};
69
+ const cleanup = () => {
70
+ if (group.pgid !== undefined)
71
+ killGroup(group.pgid);
72
+ for (const k of links)
73
+ if (lstatSync(k, { throwIfNoEntry: false })?.isSymbolicLink())
74
+ unlinkSync(k);
75
+ try {
76
+ git(repo, 'worktree', 'remove', '--force', dir);
77
+ }
78
+ catch {
79
+ rmSync(dir, { recursive: true, force: true });
80
+ }
81
+ try {
82
+ git(repo, 'worktree', 'prune');
83
+ }
84
+ catch {
85
+ // rien à élaguer
86
+ }
87
+ };
88
+ // Ctrl-C, SIGTERM ou raccrochage reçu par cadence : la commande est tuée et le worktree retiré avant de mourir du signal.
89
+ const forget = onTermination(cleanup);
90
+ try {
91
+ try {
92
+ git(repo, 'worktree', 'add', '--detach', '-f', dir, sha);
93
+ }
94
+ catch (e) {
95
+ out(`verify --gate : worktree de ${short} impossible — ${gitFailure(e)}`);
96
+ return 2;
97
+ }
98
+ if (config.worktreeDeps) {
99
+ const linked = linkDeclared(repo, dir, config.worktreeDeps);
100
+ links.push(...linked.links);
101
+ if (linked.missing.length)
102
+ out(`dépendances déclarées absentes de l'arbre principal, non liées : ${linked.missing.join(', ')}`);
103
+ }
104
+ else {
105
+ links.push(...linkIgnored(repo, dir));
106
+ }
107
+ let red = 0;
108
+ for (const step of steps) {
109
+ const r = await sh(step.cmd, dir, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group);
110
+ if (r.code === 0) {
111
+ out(`✓ ${step.name} (${step.cmd})`);
112
+ continue;
113
+ }
114
+ red++;
115
+ out(`✗ ${step.name} (${step.cmd}) — code ${r.code}`);
116
+ const tests = step.name === 'tests';
117
+ const names = tests ? failedTests(r.output) : [];
118
+ const unloaded = tests ? unloadedSuites(r.output) : [];
119
+ for (const n of names)
120
+ out(` - ${n}`);
121
+ for (const n of unloaded)
122
+ out(` - ${n} (ne se charge pas)`);
123
+ if (names.length + unloaded.length === 0 || hasUnnamedFailure(r.output, true)) {
124
+ if (names.length + unloaded.length)
125
+ out(' (au moins un échec que la sortie ne rattache à aucun test)');
126
+ if (r.output.trim())
127
+ for (const l of r.output.trimEnd().split('\n').slice(-TAIL_LINES))
128
+ out(` | ${l}`);
129
+ }
130
+ }
131
+ out(red === 0 ? `verify --gate ${short} : vert` : `verify --gate ${short} : rouge (${red} étape${red > 1 ? 's' : ''} sur ${steps.length})`);
132
+ return red === 0 ? 0 : 1;
133
+ }
134
+ finally {
135
+ forget();
136
+ cleanup();
137
+ }
138
+ }
package/docs/deliver.md CHANGED
@@ -273,3 +273,40 @@ everything is green, the red effects and the summary otherwise. It is a fact lik
273
273
  the others: it never changes the exit code, and an unreachable network shows up
274
274
  as red lines ("erreur réseau") without blocking the session. No section for a
275
275
  project without `deliver.verify`.
276
+
277
+ ## verify --gate: replay the project's gate at a sha (L348)
278
+
279
+ `cadence verify` above replays the *effect* of a delivery. `cadence verify --gate --sha <sha>`
280
+ replays the *gate* of the project, `orchestrate.build` then `orchestrate.test` of `cadence.yaml`
281
+ (the order of the wave's gate: the tests read what the build produces), at one commit, for the
282
+ lead's re-verification (« the lead re-verifies itself », skill `lead`). It never touches the shared
283
+ tree, which holds the lots still running.
284
+
285
+ ```sh
286
+ cadence verify --gate --sha 6b0d9aa # in the project
287
+ cadence verify --gate --sha 6b0d9aa --project app # from the parent folder, the project `app` (its own git repository)
288
+ ```
289
+
290
+ It adds a throwaway worktree of the sha under the OS temporary folder (`cadence-verify-…`), links
291
+ the **untracked dependencies** of the main tree into it, runs the commands there, and removes the worktree
292
+ whatever the result. A bare worktree has no `node_modules` or `.venv`: on 26-10 the re-verification of a lot
293
+ in one gave nine red suites that were only missing dependencies. Which dependencies:
294
+
295
+ - `orchestrate.worktreeDeps: [node_modules, .venv, front/node_modules]` in `cadence.yaml`: those paths, relative to the repository
296
+ (no absolute path, no `..`); one that does not exist in the main tree is reported and skipped;
297
+ - absent: the same folders as the worktrees of a wave (the ignored folders of every level, `node_modules`, `.venv`…; build outputs such as `dist` excluded). The
298
+ workspace links of a `node_modules` are made again towards the worktree, so the code run is the sha's.
299
+
300
+ ```
301
+ ✓ build (npx tsc --noEmit)
302
+ ✗ tests (npx vitest run) — code 1
303
+ - test/a.test.ts > R3 — …
304
+ verify --gate 6b0d9aa : rouge (1 étape sur 2)
305
+ ```
306
+
307
+ Both commands always run, even when the first is red, so the report is whole. A red step lists the failing
308
+ tests the output names (the same reading as the wave's baseline) and the test files that do not load (`- test/a.test.ts (ne se charge pas)`, the typical sign of a missing dependency); with none identifiable, the last 10 lines of the output.
309
+ Each command has 30 minutes (then its whole process group is killed, code 124). Ctrl-C, SIGTERM or a hangup received by cadence kills the running command and removes the worktree before cadence dies.
310
+ `--sha` is required (the moving head of a wave is not a sha to re-verify) and `--retry` does not apply. Exit code:
311
+ **0** green · **1** red · **2** nothing to replay (neither `orchestrate.build` nor `orchestrate.test`), unknown sha or a worktree that cannot be made.
312
+ The Node of the `.nvmrc` is not selected here, and `CADENCE_TEST_WORKERS` is not set: the commands run with the environment of the caller (`PATH` included), so a re-verification run during a wave should export the session's `CADENCE_TEST_WORKERS` itself, or vitest takes every core.
@@ -87,6 +87,7 @@ The budget a wave gives a lot is derived from its estimate, measured on the runs
87
87
  | `screen: page` | 1.3 M at least, or the ordinary budget + 450 k if that is more |
88
88
  | `spec: true` | ordinary budget + 450 k |
89
89
  | fix pass and short review | 155 k reserved |
90
+ | escalation (`orchestrate.escalation` declared, L349) | + 175 k reserved (estimate: one correction and one fresh review on the escalation model) |
90
91
 
91
92
  The final review is always played; the budget bounds the writing passes. A `screen: component` or
92
93
  `screen: journey` lot takes the ordinary budget, unless its sheet already exists: it then gets the
@@ -99,7 +100,8 @@ The final review is always played; the budget bounds the writing passes. A `scre
99
100
  - Never two sessions in the same repository at the same time. (The worktree mode, `orchestrate.worktrees: true` — on in cadence's own `cadence.yaml`, off where the key is absent — lets two independent lots of one repository play at once, each in its own worktree — [docs/orchestrate.md](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279).)
100
101
  - A session cannot ask the human anything: it stops and reports, and the lead brings the question back.
101
102
  - A sealed acceptance test that contradicts its sheet is corrected by the lead with
102
- `cadence spec reseal <lot> <test> --reason "…"`, never by editing `acceptation.json`.
103
+ `cadence spec reseal <lot> <test> --reason "…"`, never by editing `acceptation.json`; a lot closed by hand
104
+ and left at `vert: false` is put back to green with `cadence spec green <lot>` (L366).
103
105
  - Deliveries and the `raf ux` / `raf review` verdicts belong to the lead, one project at a time.
104
106
 
105
107
  See also the [orchestrate](orchestrate.md#orchestrate) page and the
package/docs/lead.md CHANGED
@@ -85,6 +85,7 @@ picture, not its source.
85
85
  | At most **as many subagents as the session cap** running at once (2 by default; the human sets it with `CADENCE_MAX_SESSIONS`), orchestrate sessions included | `cadence orchestrate --status` and `cadence lead tour` print `créneaux libres : N sur M`, M being the configured cap and the sessions the lead started by hand (`cadence lead hold`, L255) being subtracted; the lead never raises `--max-sessions` or `CADENCE_MAX_SESSIONS` itself |
86
86
  | Never two sessions in the same repository at the same time; the lead does not commit where a wave runs (with `orchestrate.worktrees: true` two lots of one repository play at once, each in its own worktree: not a rule for the lead) | a wave's lock refuses `cadence deliver` while a lot still works there |
87
87
  | A locked acceptance test that contradicts the sheet is corrected by the lead with `cadence spec reseal <lot> <test> --reason "…"` (lot in progress, outside a wave and with no wave that is not `done` keeping open a lot of the repository still `doing` in its plan, the resealed lot or another, e.g. a question waiting: `--resume --wave <wave> --answer` or `--drop` first, the one error lists every wave concerned, the live one that holds the lock (to wait for) and the stopped ones, and gives for each stopped wave the single `cd <launch folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> [--drop …]` to run, a `--drop` per open lot of the repository in that wave, the queued ones that are not `doing` in the plan included, since `--resume` would relaunch them and they would take the lock back, L238 L239; an amended sheet is accepted once committed, every commit that touched it citing the lot, L217, never put it back to the red state; a delivered lot with an unstable test: add `--porteur <lot in progress>`, L212), then a new `cadence orchestrate <project>:<lot>` for a lot handed back (`--resume` skips it) | never by a session, never by editing `acceptation.json` |
88
+ | A lot closed by hand (`raf done`) and left at `vert: false` in its `acceptation-<lot>.json` is put back to green by the lead with `cadence spec green <lot>` (L366, GitHub issue #18): the program runs the lot's acceptance command and, green, sets `vert: true`, reseals the listed tests refined since the red commit and commits the file alone, citing the lot; red, nothing changes. `spec reseal` does not cover it (a lot `doing`, or `vert: true` with `--porteur`). `raf done` refuses such a lot without `--force`, and `cadence orchestrate --dry-run` prints `acceptation rouge d'un lot clos` before the wave starts | closed like that it keeps the lock total on its tests, and every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them; never by editing `acceptation.json` |
88
89
  | Deliveries and the `raf ux` / `raf review` verdicts belong to the lead, one project at a time | never to a subagent, never two deliveries in parallel |
89
90
  | The UX verdict is the human's unless `lead.ux: lead` (parent `cadence.yaml`, default `human`) | then the lead decides from the report and the captures, and brings any doubt to the human |
90
91
  | A session cannot ask the human anything | it stops and reports; the lead brings the question back |
@@ -125,6 +126,22 @@ only wants the list of the documents to change (L306). A document outside the re
125
126
  of the project's `cadence.yaml`, never guessed: the lead reads that key itself and puts its entries in the brief with the absolute path of each repository (the subagent is given read access to it), because the agent examines no outside document the brief does not name. The agent proposes a lot for the plan of that repository, in the `resume` of its
126
127
  report, and the lead decides whether to add it (`raf add` in that repository). The agent never writes to that plan.
127
128
 
129
+ ## config set --all
130
+
131
+ `cadence config set <key> <value> --all [--dry-run] [--depth n]` (L342) applies the same change to the `cadence.yaml` of every project the lead knows: it is run from the parent folder and takes the projects `lead tour` lists (same `--depth` / `depth:` rule; run inside a project with no sub-project, it takes that project, as `lead tour` does). It replaces the chain the lead used to type by hand in 8 or 9 projects (a test gate, a duplicate build, the Fable escalation).
132
+
133
+ ```
134
+ cadence config set orchestrate.escalation '{ model: fable }' --all --dry-run
135
+ cadence config set orchestrate.build 'npx tsc --noEmit' --all
136
+ ```
137
+
138
+ - `<key>` is a dotted path (`orchestrate.escalation`); `<value>` is read as YAML, so `true`, a command, or a flow mapping like `{ model: fable }` all work. A key already at that value is left alone.
139
+ - The rest of the file is kept as it is, byte for byte — comments, key order, blank lines, inline lists (`[a, b]`) and mappings: only the lines of the key change (a new key goes at the end of its parent; a value replaced in place keeps its end-of-line comment). The only exception is a file whose rewrite by the YAML library moves lines other than the key's (its line count changes, a comment lands on another line, or the lines carried over would not read back as the same document): then the whole file is rewritten in the library's style. A project with no `cadence.yaml` gets one.
140
+ - In each project it creates a **config lot** that quotes the key (`config cadence.yaml : <key> = <value>`, estimate 0.05), as `raf add` then `raf start` would, and commits `cadence.yaml`, the plan file and, when the project names `plan.onWrite`, the files that command produces (it runs when the plan is written; see `docs/raf.md`) with explicit paths (`chore(L12): cadence.yaml — <key> = <value>`): nothing else in the index or the working tree goes in. The lot is left `doing`: closing it (`raf done`) stays the lead's step. **Nothing is pushed.**
141
+ - The new text is read back with cadence's own config loaders (plan, news, hook, perf, session, orchestrate, docs, references, migrate, lead.ux, depth, priority, deliver) before anything is written. A `cadence.yaml` that does not read back — before the change or after it — **stops the run** (exit code 1) at that project, with nothing written there; the projects after it are named `non traités`. A commit the repository refuses (a hook) or a `plan.onWrite` command that fails stops it the same way (`lot ou commit impossible`), `cadence.yaml`, the plan file and the produced files put back as they were (a produced file HEAD does not know is deleted).
142
+ - A project is **skipped**, with the reason, when: it is not its own git repository; the key is already at that value; the key cannot be set in its file (a plain value in the way); `cadence.yaml`, the plan file or a file named by `plan.onWrite.files` has uncommitted changes (the explicit-path commit would take them); or its plan is read-only (kept by another tool) and `cadence.yaml` names no creation command. A read-only plan that names one with `plan.add` (a shell command containing `{title}`, replaced by the quoted title of the config lot, e.g. `plan: { path: taches.yaml, lots: taches, add: 'mytool add {title}' }`) gets its config lot from that command, run in the project folder; the lot id is the first id-like word of its output (the commit subject has none when there is none), and `cadence.yaml` and the plan file are committed together. The command alone decides the lot's status.
143
+ - `--dry-run` prints the same lines, prefixed `+ … serait fait`, and writes and commits nothing. The last line counts what was done and skipped. Without `--all` the command refuses (run `raf`/an editor inside the project for a single one).
144
+
128
145
  ## lead tour
129
146
 
130
147
  `cadence lead tour [folder] [--idle 3] [--depth n] [--json]` is the lead's morning table, with no model in
@@ -142,7 +159,7 @@ beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second
142
159
  |---|---|
143
160
  | `en cours` | ids of the lots in progress, each with its number of commits (`0 commit` included: a close note on a lot without commit is not « done, `raf done` forgotten »); then `aucune activité` when it has no activity at all, `silencieux Nj` when the lot's last activity (commit, note or start) is more than `--idle` days old (default 3) |
144
161
  | `dérive` | the gaps `raf check` reports (a lot whose commits leave a document behind, [`docs.sync`](news.md#documentation-that-follows-the-code), included), counted and the first two shown |
145
- | `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters |
162
+ | `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters; when one quotes a lot already `done` or `dropped` (L347), ` ⚠ note périmée : L217 done le 24-10` follows (not cut) — relay it as stale, never as work to do (`--json`: `staleNotes`, absent when none is stale) |
146
163
  | `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters; a lot whose [`gate`](raf.md#plan-file) is not met is not ready |
147
164
  | `attend` | only when a lot waits on its `gate`: `attend L190` |
148
165
  | `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
@@ -74,7 +74,7 @@ The review and UX briefs carry it too, on a line `Goal of the lot (it takes prec
74
74
  `correction non jouée (budget)` or `budget du lot atteint` kept its findings but not its budget, and a plain `--resume` does not replay a
75
75
  finished lot. `--budget maritime:B26.4=+300k` (the project may be left out when only one project carries the lot; the option is
76
76
  repeatable for several lots, and `--budget 1M` still raises the wave's budget — the `=` tells them apart) puts the lot back in play:
77
- its budget becomes **what its sessions already counted plus N** (849 k spent of 750 k, `=+300k`: 1 149 000), its outcome is cleared,
77
+ its budget becomes **what its sessions already counted plus N** (849 k spent of 750 k, `=+300k`: 1 149 000; when the lot still holds the share of the escalation (L349) it is added on top, so N stays the margin of the ordinary passes), its outcome is cleared,
78
78
  and the step that was not played (`fix`, `implement`, `spec` or `accept` — the only ones a lot budget bounds; a review is never handed
79
79
  back for it) runs in a **new session** (never `claude --resume` on the earlier one) with the findings the lot kept, then the normal cycle
80
80
  goes on: review, verdict recorded with the sha re-read and the plan committed. Nothing is recorded twice, since a lot handed back for
@@ -139,8 +139,8 @@ keeps its `/` (`maritime-atlas:Q4/accueil-4-ux12@haiku`); this holds for `--dry-
139
139
  same for the lot folder and the dry-run briefs); ids without `/` are unchanged.
140
140
 
141
141
  You choose the lots; the order is the order given (one queue per repository, as many repositories at the
142
- same time as the session cap allows — 2 by default; the `orchestrate.worktrees: true` mode (on in cadence's own `cadence.yaml`, off where the key is absent) plays the lots of one repository in parallel, each in its own worktree — « One worktree per lot »). `@haiku|@sonnet|@opus` sets the model of the implementation and corrections of that
143
- lot (default Sonnet; reviews are Opus, except the light review below; Haiku only when you write it, for a mechanical lot). Run it
142
+ same time as the session cap allows — 2 by default; the `orchestrate.worktrees: true` mode (on in cadence's own `cadence.yaml`, off where the key is absent) plays the lots of one repository in parallel, each in its own worktree — « One worktree per lot »). `@<model>` sets the model of the implementation and corrections of that
143
+ lot (`@haiku`, `@sonnet`, `@opus`, or any other name, passed as it stands to `claude --model` like `orchestrate.escalation.model`: `@fable`, `@claude-fable-5-1` — L345; the dry-run warns, without refusing, for a name outside the three aliases; default Sonnet; reviews are Opus, except the light review below; Haiku only when you write it, for a mechanical lot). Run it
144
144
  in the background and read `--status`: it prints one line per transition and the final table.
145
145
 
146
146
  **Following a wave (L49)**: `--status --watch` clears the screen and redraws the live waves and the table every
@@ -166,10 +166,14 @@ usability (code only when the lot is not `visible`). Failing tests (reported red
166
166
 
167
167
  **A project without `orchestrate.test` is flagged (L327)**: without the key the wave never runs the suite itself — only the review does, if it decides to (it ran 12 test files of a project on 2026-10-25 and a red test reached « ready »). `cadence orchestrate --dry-run` writes one line per repository, under the first lot of that repository: `<repo> · porte de tests absente : orchestrate.test n'est pas déclaré, la vague ne lancera pas la suite (seule la revue le fera si elle le décide) · commande probable : npm test (à déclarer en orchestrate.test)`, and [`cadence lead tour`](lead.md#lead-tour) writes the same line for every project of the folder. The probable command is read from the repository, never run: `npm test` when `package.json` has a `scripts.test` (the `npm init` placeholder « no test specified » does not count), `pytest` when `pyproject.toml` names pytest, `mvn test` when there is a `pom.xml`; with none of them the line carries no command. A `cadence.yaml` that cannot be read gives no such line (it has its own error). Declaring `test:` (and `build:`) in the project's `cadence.yaml`, once the command is known to be green, removes the line.
168
168
 
169
- **Tests already red at the start (L213)**: a lot is no longer handed back « tests rouges » for failures that were there before it. When `orchestrate.test` fails (or the session reports red tests), the program reads the failing tests from the runner's output (vitest/jest `FAIL`/`●`, pytest `FAILED`, go `--- FAIL:`, TAP `not ok`), replays the same command at the sha the lot started from (the parent of the lot's oldest work commit, plan-only commits left out, else the `HEAD` before its first session) in a throwaway `git worktree` outside the repository, and compares: if every failing test was already red, it is not a defect of the lot — no correction pass, the code review takes place, the reviewer is told, and the final table lists them apart (`déjà rouge au départ (<sha>) : <test>`, 30 at most then a count). The worktree gets links to the repository's ignored folders at any depth, such as `.venv` and, in a monorepo, `packages/*/node_modules`, when their parent exists at that sha; build outputs such as `dist` or `coverage` are left out. Each `node_modules` is rebuilt entry by entry, every entry linked to the live one except the workspace links of npm, pnpm and yarn (`node_modules/@x/shared -> ../../packages/shared`, a link to code of the repository outside any `node_modules`), which are remade towards the worktree's own copy so that the start sha is tested against its own sources, not the lot's (a workspace package that does not exist at the start sha gets no link). Known limit: only `node_modules` links are remade; whatever points into the live repository by an absolute path, such as an editable Python install (`.venv` with a `.pth` file naming the live `src`, `pip install -e`) or an absolute symlink in another ignored folder, still runs the lot's code in the baseline. Likewise, a workspace package consumed through its build output (`main: dist/...`) cannot load in the baseline worktree: output dirs are not linked, so such a monorepo keeps the pre-L213 « tests rouges » outcome unless `orchestrate.build` is declared (the build is then played first in the baseline worktree, see below). A lot resumed in a later wave keeps the same baseline: the sha is the parent of its oldest work commit, not the `HEAD` of the new wave, so a failure the lot itself introduced in an earlier wave is never excused. A test red now and not at the start stays a blocking finding, and only that one is given to the correction (the others are counted as « already red »). When the output names no test, or the baseline cannot be run, the tests stay red as before (no comparison is guessed). A failure the output does not tie to a test (unhandled error, a spec file that fails to load such as jest « Test suite failed to run » or vitest `FAIL a.test.ts [ a.test.ts ]`, coverage threshold) keeps the tests red. Without `orchestrate.test` the program cannot replay anything: a session that reports green tests and lists under `echecsDeBase` the failures it saw at the start sha gets them listed apart, marked « non vérifié » in the table, and the review takes place; a session that reports the tests red stays red, the brief asking it not to report red for base failures alone — unless (L215) the report says so in a structured way: with `vert: false`, a non-empty `echecsDeBase` and an empty `nouveauxEchecs` (the list of tests red now that were not red at the start sha), `vert: false` is not a reason to correct, the lot carries a « non vérifié » warning, the reviewer is told that the baseline is unverified and the review takes place. A non-empty `nouveauxEchecs` with `vert: false` triggers a correction pass (the new failures are in the finding); an absent `nouveauxEchecs`, or an empty `echecsDeBase`, keeps the correction. The program never guesses from the text of the result (« 24 failed »). The `implement` and `fix` briefs ask for `echecsDeBase` and `nouveauxEchecs`.
169
+ **Tests already red at the start (L213)**: a lot is no longer handed back « tests rouges » for failures that were there before it. When `orchestrate.test` fails (or the session reports red tests), the program reads the failing tests from the runner's output (vitest/jest `FAIL`/`●`, pytest `FAILED`, go `--- FAIL:`, TAP `not ok`), replays the same command at the sha the lot started from (the parent of the lot's oldest work commit, plan-only commits left out, else the `HEAD` before its first session) in a throwaway `git worktree` outside the repository, and compares: if every failing test was already red, it is not a defect of the lot — no correction pass, the code review takes place, the reviewer is told, and the final table lists them apart (`déjà rouge au départ (<sha>) : <test>`, 30 at most then a count). The worktree gets links to the repository's ignored folders at any depth, such as `.venv` and, in a monorepo, `packages/*/node_modules`, when their parent exists at that sha; build outputs such as `dist` or `coverage` are left out. Each `node_modules` is rebuilt entry by entry, every entry linked to the live one except the workspace links of npm, pnpm and yarn (`node_modules/@x/shared -> ../../packages/shared`, a link to code of the repository outside any `node_modules`), which are remade towards the worktree's own copy so that the start sha is tested against its own sources, not the lot's (a workspace package that does not exist at the start sha gets no link). Known limit: only `node_modules` links are remade; whatever points into the live repository by an absolute path, such as an editable Python install (`.venv` with a `.pth` file naming the live `src`, `pip install -e`) or an absolute symlink in another ignored folder, still runs the lot's code in the baseline. Likewise, a workspace package consumed through its build output (`main: dist/...`) cannot load in the baseline worktree: output dirs are not linked, so such a monorepo keeps the pre-L213 « tests rouges » outcome unless `orchestrate.build` is declared (the build is then played first in the baseline worktree, see below). A lot resumed in a later wave keeps the same baseline: the sha is the parent of its oldest work commit, not the `HEAD` of the new wave, so a failure the lot itself introduced in an earlier wave is never excused. A test red now and not at the start stays a blocking finding, and only that one is given to the correction (the others are counted as « already red »). When the output names no test, or the baseline cannot be run, the tests stay red as before (no comparison is guessed). A failure the output does not tie to a test (unhandled error, a spec file that fails to load such as jest « Test suite failed to run » or vitest `FAIL a.test.ts [ a.test.ts ]`, coverage threshold) keeps the tests red. Without `orchestrate.test` the program cannot replay anything: a session that reports green tests and lists under `echecsDeBase` the failures it saw at the start sha gets them listed apart, marked « non vérifié » in the table, and the review takes place; a session that reports the tests red stays red, the brief asking it not to report red for base failures alone — unless (L215) the report says so in a structured way: with `vert: false`, a non-empty `echecsDeBase` and an empty `nouveauxEchecs` (the list of tests red now that were not red at the start sha), `vert: false` is not a reason to correct, the lot carries a « non vérifié » warning, the reviewer is told that the baseline is unverified and the review takes place. A non-empty `nouveauxEchecs` with `vert: false` triggers a correction pass (the new failures are in the finding); an absent `nouveauxEchecs`, or an empty `echecsDeBase`, keeps the correction. With `orchestrate.test` declared (L361), the same structured report is honoured when the program's own gate is green (`orchestrate.test` exits 0): `vert: false` with a non-empty `echecsDeBase` and an empty `nouveauxEchecs` goes straight to the review, with a warning, without a correction pass and without the « tests rouges » escalation; without those two fields the claim of red tests still triggers the correction. The program never guesses from the text of the result (« 24 failed »). The `implement` and `fix` briefs ask for `echecsDeBase` and `nouveauxEchecs`.
170
170
 
171
171
  **The build is played before the tests (L333)**: the gate of the wave runs `orchestrate.build` first, then `orchestrate.test`, because tests that read the build output (end-to-end tests over `dist`) are red without it — on 2026-10-25 three projects had to declare `test: astro build && npm test` (13 tests out of 15 red without `dist`), which played the build twice on every pass. A red build hands the lot back as a single « build » finding and the tests are not run. The L213 baseline plays the build first in its throwaway worktree too, then the tests: without it the start sha, with no `dist`, would excuse a test wrongly or turn everything red. A build that fails at the start sha gives no baseline (the build says nothing about the tests there): the tests stay red as before, no comparison is guessed. Without `orchestrate.build` nothing changes. A project that wrote `build && test` into its `orchestrate.test` can drop the duplicate build.
172
172
 
173
+ **A red test is replayed alone before it counts, only on a declared command (L354)**: on 2026-10-26, with 32 cores and a cap of 4 sessions, every suite ran vitest at 31 workers, and four correction passes (L344 11 min, L342 9, L349 7, B28.12 4) found nothing to fix — each time « a test that passes alone ». cadence does not guess how a file is replayed on your runner (a first version appended the files to `orchestrate.test` and, on a composed gate, excused a real regression), so the replay is **off unless the project declares its command**: `orchestrate.testReplay`, where `{files}` is replaced by the files of the red tests, e.g. `testReplay: "npx vitest run --maxWorkers=$CADENCE_TEST_WORKERS --minWorkers=1 {files}"` (a value without `{files}` is refused when the file is read). **Without it nothing is replayed and nothing is held for unstable**: a red test is counted as before. With it, a red test of the gate (`orchestrate.test`, played after every writing session and at the rebased tip) that is absent from the baseline is **replayed alone, twice at most**, by that command, in the lot's worktree (vitest names `file > suite > test`, pytest `file::test`, jest `FAIL file` and `● Suite › test`, the latter tied to the `FAIL` line that precedes it in the same output and replayed with that file; the file is the part before the first `::` or ` > `, so either of them inside a test title is not part of the path). Green at a replay, the test is **« instable sous charge »**: it is not counted, it triggers no correction pass, even when the session reported its tests red (`vert: false`: a session that plays the suite under the same load sees the same red; the announcement is not held against the lot when every red of the gate is unstable, as it is not when every red was already red at the start), the lot goes on to its review, and the trace is a lot warning (so a line of the final table), a journal line `<project>:<lot> · N test(s) instable(s) sous charge …`, the lot's `instables` list in its journal, and the `cadence perf` line `tests instables sous charge` (`instables` in `--json`). Red at both replays it is counted as before. A red name that designates no file (a plain `FAIL name`) cannot be replayed alone and is counted at once. **A replay in which the test did not run counts red**: only a replay that exits 0 after running tests proves a test green, and only for the tests whose file appears in the replay's output, as a whole path on a line of the runner — never in the launcher's echo of the command: npm 12 writes `npm notice run 'vitest' run … <files>` on stderr for `npx …` as for `npm test -- …`, npm and pnpm write `> vitest run … <files>`, yarn `$ …`, and those lines name every file asked for, played or not (a runner that silently skips a file it cannot find, such as vitest given a path of another workspace, exits 0 without playing it: that test stays red; with a reporter that does not list files, nothing is ever excused; `a.test.ts` is not found in `src/a.test.ts`); a replay that fails, or that runs no test (the runner's closing line `No test files found, exiting with code`, `No tests found, exiting with code` or `no tests ran in`, even with exit code 0; the same words inside a test title or a test's output do not count), leaves every test it was asked about red. In a vitest workspace the names read `|package| src/a.test.ts > case`: the `|package|` prefix is dropped to find the file. The reviewer is told which tests were red under load (`instables` of the check, also when the baseline holds red tests of its own), so it does not report them, and the correction finding lists only the tests still red after the replays. The baseline run is included: with `testReplay`, a red test of the L213 baseline (the gate played at the start sha, in its throwaway worktree) is replayed alone there, twice at most, before it is retained as « already red at the start »; green at a replay it is dropped from the baseline (a journal line `… · ligne de base <sha> : N test(s) rouge(s) sous charge, vert(s) rejoué(s) seul(s), non retenu(s) …`), so a test the lot really breaks is not excused by a load failure of the start sha. The baseline replays are not the lot's « instables »: they say nothing about the lot's code. **A composed gate is not supported (decision of 26-10)**: `orchestrate.testReplay` is refused when `cadence.yaml` is read if `orchestrate.test` has `&&` or `||` outside quotes, or a parenthesis (sub-shell `( … )`, `$( … )`, function): a first runner that goes red cuts the next one, so a green replay of its tests says nothing of the rest, and a replay command written for one runner of the gate cannot replay the tests of the other. Write the replay for a gate that launches a single runner; without `testReplay` a composed gate is played as before, nothing is replayed. A jest `FAIL` line may carry a duration (`FAIL src/a.test.ts (6.21 s)`) or a project name (`FAIL backend src/a.test.ts`): the file is its last word, and any other `PASS`/`FAIL` line ties no file to the `●` lines that follow. The `FAIL project file` line is itself a red name (`project file`) and is replayed with that file, like the `●` tests under it.
174
+
175
+ **Each session gets its share of the machine (L350)**: the wave computes `CADENCE_TEST_WORKERS = max(1, cores ÷ session cap)` (`--max-sessions` or `CADENCE_MAX_SESSIONS`; 32 cores and a cap of 4 give 8; a `CADENCE_TEST_WORKERS` already set in the environment that launches the wave wins when it is a positive integer). It is put in the environment of every session and of every command of the gate (tests, build, baseline, replays), recorded as `testWorkers` in the wave state, and the `implement`, `fix`, minors and librarian briefs tell the session to respect it: `vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS` (vitest 2 refuses `--maxWorkers` alone, its default minimum being the core count: « options.minThreads and options.maxThreads must not conflict »), `jest --maxWorkers=$CADENCE_TEST_WORKERS`, `pytest -n $CADENCE_TEST_WORKERS`. The gate command itself must honour it for the share to bind: cadence's own `vitest.config.ts` reads it (`poolOptions.forks.maxForks`, `threads.maxThreads`); in another project write `orchestrate.test: npx vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS` (or the equivalent of its runner).
176
+
173
177
  **Review sized to the lot (L108)**: a lot whose `estimate` is ≤ `orchestrate.review.threshold` (0.25 day by default)
174
178
  is *light*: one single review (Sonnet by default, `code-reviewer` agent, same criteria), no minors pass — the minors are
175
179
  returned to the lead as notes (proposals). A blocking or major finding on a light lot still triggers a correction, and
@@ -178,7 +182,7 @@ chain described above (Opus, two corrections, minors pass). `--dry-run` shows «
178
182
 
179
183
  ```yaml
180
184
  orchestrate:
181
- review: { threshold: 0.25, light: sonnet, full: opus } # defaults; threshold in days (0 = no light lot), models haiku|sonnet|opus
185
+ review: { threshold: 0.25, light: sonnet, full: opus } # defaults; threshold in days (0 = no light lot), models: haiku|sonnet|opus or any name passed as it stands to claude --model (L345)
182
186
  ```
183
187
 
184
188
  `full` is also the model of the UX review and of the single pass of a small lot that is not light.
@@ -190,9 +194,9 @@ orchestrate:
190
194
  escalation: { model: fable } # no default: absent = nothing changes
191
195
  ```
192
196
 
193
- The name is passed as it stands to `claude --model` — no closed list `haiku|sonnet|opus`, and no default value, because the account may not have the model. When the review is still not compliant after the planned passes (`MAX_PASSES`, or `MAX_PASSES_CONVERGING` when it converges), or when the lot would be handed back « tests rouges » after them, the wave plays **one** correction and then **one** fresh code review, both on that model, before handing the lot back. The lot is handed back only if that escalated review is not compliant (`… ; rendu malgré l'escalade <model>`); the escalated correction that leaves the tests red hands it back without a review. A compliant escalated review ends the lot as any compliant review does (minors pass, UX review, `ready`). The UX review keeps its own rule: a lot handed back for a non-compliant UX review is not escalated. The escalation counts as a correction pass (`passes` of the table) and is played at most once per lot, a resume included (`escalation` in the lot's journal).
197
+ The name is passed as it stands to `claude --model` — no closed list `haiku|sonnet|opus` (the same holds for `@<model>` of a lot and for `orchestrate.review.light` / `full`, L345: the three aliases keep their meaning, any other name goes through unchanged, and `--dry-run` prints `⚠ modèle « <name> » hors des alias …` for each such name, without refusing), and no default value, because the account may not have the model. When the review is still not compliant after the planned passes (`MAX_PASSES`, or `MAX_PASSES_CONVERGING` when it converges), or when the lot would be handed back « tests rouges » after them, the wave plays **one** correction and then **one** fresh code review, both on that model, before handing the lot back. The lot is handed back only if that escalated review is not compliant (`… ; rendu malgré l'escalade <model>`); the escalated correction that leaves the tests red hands it back without a review. A compliant escalated review ends the lot as any compliant review does (minors pass, UX review, `ready`). The UX review keeps its own rule: a lot handed back for a non-compliant UX review is not escalated. The escalation counts as a correction pass (`passes` of the table) and is played at most once per lot, a resume included (`escalation` in the lot's journal).
194
198
 
195
- The lot's budget is respected: the escalation needs what a correction and its review need (`REVIEW_RESERVE`, as for any correction pass). When the budget does not cover it, it is **not played** and the outcome says so (`escalade <model> non jouée (budget du lot …)`); the lot is handed back to the lead with its findings. The final table prints one line per escalated lot, `<project>:<lot> — escalade <model> : <outcome>` (`en cours` only while the lot still runs — the table also serves `--status` and a suspended wave —; a lot withdrawn (`--drop`), failed by an error or stopped by an incident of the repository before the model's verdict says `engagée, lot rendu avant le verdict du modèle`: the model did not judge it and it is not counted against the model —, compliant review, lot handed back despite the escalation — for the human or an arbitration of the lead —, not played, or an escalated session that returned nothing (empty output, launch error, cut, timeout; the only case said `session escaladée sans résultat`) — then the model never judged the lot and the lead takes it up, `rendu malgré l'escalade` is said only for a lot handed back on a verdict). A lot whose escalated review was compliant but is handed back later (non-compliant UX review, refused integration) keeps `revue neuve conforme` and says its lot line is the reason, the wave log traces the three moments, and `cadence perf` counts the escalated lots of each window and their outcome (`docs/perf.md`). The price grid of `cadence perf` knows `sonnet`, `opus` and `haiku`: the cost of an escalated step on another model reads `n/d`.
199
+ The lot's budget is respected: the escalation needs what a correction and its review need (`REVIEW_RESERVE`, as for any correction pass), and the budget of a lot in a project that declares `escalation` reserves its share for it (`ESCALATION_RESERVE`, see **Budget**). When the budget does not cover it, it is **not played** and the outcome says so (`escalade <model> non jouée (budget du lot …)`); the lot is handed back to the lead with its findings. The final table prints one line per escalated lot, `<project>:<lot> — escalade <model> : <outcome>` (`en cours` only while the lot still runs — the table also serves `--status` and a suspended wave —; a lot withdrawn (`--drop`), failed by an error or stopped by an incident of the repository before the model's verdict says `engagée, lot rendu avant le verdict du modèle`: the model did not judge it and it is not counted against the model —, compliant review, lot handed back despite the escalation — for the human or an arbitration of the lead —, not played, or an escalated session that returned nothing (empty output, launch error, cut, timeout; the only case said `session escaladée sans résultat`) — then the model never judged the lot and the lead takes it up, `rendu malgré l'escalade` is said only for a lot handed back on a verdict). A lot whose escalated review was compliant but is handed back later (non-compliant UX review, refused integration) keeps `revue neuve conforme` and says its lot line is the reason, the wave log traces the three moments, and `cadence perf` counts the escalated lots of each window and their outcome (`docs/perf.md`). The price grid of `cadence perf` knows `sonnet`, `opus` and `haiku`: the cost of an escalated step on another model reads `n/d`.
196
200
 
197
201
  **Code changed without a test (L287)**: before any review session, the program checks the diff of the lot (every commit that
198
202
  cites it, all sessions included). If it touches code (`testGate.code`, minus `testGate.except`) without touching a single test
@@ -271,7 +275,7 @@ the first implementation: `precheck → spec → maquette → accept → impleme
271
275
  `#etat-<état>` section per state and a `data-testid` per field; every state has its capture at both widths; everything is committed and the tree is clean. Anything missing is listed back to a fresh `spec` session whose brief names the points, within the lot's session budget: `orchestrate.maquetteReviews` sessions (3 by default) when the mockup review is on (see [The mockup review](#the-mockup-review-before-any-code)), 2 otherwise; if the spec is still not valid on the last one the lot is handed back (`spec non conforme après 3 passes : …`, `après 2 passes` with `orchestrate.maquette: false`).
272
276
  - **A costly choice is a question, not a hypothesis.** A rule marked `hypothese` whose `si_faux` involves a data model or storage, a public or shared API contract or an already published route,
273
277
  a destructive or irreversible action, or a contradiction with a sourced rule, puts the lot in `question` (the program recognises these by keywords as a safety net behind the agent's own
274
- judgement; a keyword right after a negation — « rien d'irréversible », « sans suppression », « pas de paiement » — does not count); `--resume --wave <wave> --answer <lot> "…"` gives the answer to the resumed `spec` session, **and the program itself turns the rules it asked about into `statut: sourcee`** (L297) — `source: réponse du lead, <date>, vague <id>`, no `si_faux`, the sheet committed alone as `spec(<lot>): R3 sourcée par la réponse du lead (orchestrate <wave>)` — before that session starts, so a session that is lost or writes nothing no longer leaves the rule an `hypothese` for the sheet review (seen on 24-10: L279 handed back « fiche non conforme » for R27 although the lead had answered). Only the `statut`, `source` and `si_faux` lines of the rule are edited, the rest of the sheet is kept byte for byte (L302: `[R2]` stays `[R2]`, comments and folded lines stay; a rule written in flow style `{id: R1, …}` hands the lot back instead of being edited). An answer that deletes or changes the rule (« non, supprime R3 », « change en X »: a word of refusal or correction such as `non`, `pas`, `supprime`, `change`, `plutôt`, `remove`, `instead`) sources nothing: the rule stays an `hypothese` for the resumed `spec` session, which applies the answer, and if it does not, the question goes back to the lead. The session only rewrites a rule the lead corrected; only the rules named by the program's question are touched, and an answer to a question of the session itself leaves the sheet alone. If the commit is refused (a hook), the lot is handed back saying so, with no `spec` session played. Every other hypothesis (wording, order of fields, error state, spacing) stays written in the sheet.
278
+ judgement; a keyword right after a negation — « rien d'irréversible », « sans suppression », « pas de paiement » — does not count); `--resume --wave <wave> --answer <lot> "…"` gives the answer to the resumed `spec` session, **and the program itself turns the rules it asked about into `statut: sourcee`** (L297) — `source: réponse du lead, <date>, vague <id>`, no `si_faux`, the sheet committed alone as `docs(<lot>): règle R3 sourcée par la réponse du lead (orchestrate <wave>)` (the `plan.commit` template when set, L343) — before that session starts, so a session that is lost or writes nothing no longer leaves the rule an `hypothese` for the sheet review (seen on 24-10: L279 handed back « fiche non conforme » for R27 although the lead had answered). Only the `statut`, `source` and `si_faux` lines of the rule are edited, the rest of the sheet is kept byte for byte (L302: `[R2]` stays `[R2]`, comments and folded lines stay; a rule written in flow style `{id: R1, …}` hands the lot back instead of being edited). An answer that deletes or changes the rule (« non, supprime R3 », « change en X »: a word of refusal or correction such as `non`, `pas`, `supprime`, `change`, `plutôt`, `remove`, `instead`) sources nothing: the rule stays an `hypothese` for the resumed `spec` session, which applies the answer, and if it does not, the question goes back to the lead. The session only rewrites a rule the lead corrected; only the rules named by the program's question are touched, and an answer to a question of the session itself leaves the sheet alone. If the commit is refused (a hook), the lot is handed back saying so, with no `spec` session played. Every other hypothesis (wording, order of fields, error state, spacing) stays written in the sheet.
275
279
  The agent can also ask a question itself in its report.
276
280
  - **Under the threshold**: if the session finds that the lot changes no screen, it gives the reason in one line (`sousSeuil`), writes no mockup, and the lot goes on to `implement` as before; the reason is
277
281
  kept as a warning of the lot. On resume the spec step is skipped only when a sheet of the lot (`ecran.lot` is the lot) already passes the program's checks (`cadence spec check`, mockup, captures) and carries no costly hypothesis; otherwise the spec is played again. The sheet path in the report is relative to the repository (an absolute or `./` path is normalised, a path outside the repository is a finding).
@@ -352,10 +356,18 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
352
356
  (`git diff <red commit> -- <files>`, empty expected, working tree included, and the fingerprints), and each `acceptation.json` with its version before the session. The only difference tolerated on `acceptation.json` is the `vert` key, and only **the cycle** sets it: a session that changes it, rewrites the
353
357
  file, deletes it, creates one outside the accept step, or modifies a locked test hands the lot back (`verrou des tests d'acceptation : …`) — for a modified test the message ends with `; premier commit fautif : <sha7> <subject>`, the oldest commit after the reference (red commit, or the head before the lot) that touched the sealed path (a deletion or a rename included, renames not followed), and names no commit when the deviation is only in the working tree (L231) — a useful change is a question to the lead or a review finding, never a silent gesture. The lock does not depend on `orchestrate.accept` being declared. **Exception — a delivered lot (`vert: true`):** a later lot that makes the rule evolve may modify the listed tests of that acceptation when **a commit that cites the lot in progress amends the sheet of the screen and comes after the red commit** (a strict descendant of it; the commit may have been made before the wave, and once one exists the exception covers the whole acceptation: the tests may change in the same commit as the sheet, in a later one or in a following session, a `fix` after the `implement` for instance, L231): the sheet is what evolves the rules, and tests and sheet change together. A sheet amended by another lot, by a commit that cites no lot, by a commit that is not after the red commit, or only in the working tree (not committed) opens nothing. The exception never covers the acceptation of the lot in progress (its `vert: true` is set right after the implement): that one keeps the whole lock. Without a sheet change the difference is still a deviation — except one narrow case (L184): a listed file whose content is identical to what it was at the head before the lot's first **work** commit (commits found by the plan's refs) is not a deviation, the lot having taken its own addition back (a lot handed back for writing in a locked file and resumed in a new wave, which then removes that addition). This applies to every acceptation, `vert: false` included, but only to a file that **exists** at that head and now: a file absent at that head (e.g. created by the lot's own red commit) or deleted since is never exempted, so deleting a locked test stays a deviation; nor when the sheet of the screen has changed since that head (an evolution admitted for another lot: going back to the old content would cancel it, not the lot's addition); apart from that, with `vert: false` the whole lock applies, sheet change or not; `acceptation.json` itself stays locked (only its `vert` key moves).
354
358
  - **A sealed test that contradicts the sheet: `cadence spec reseal` (L200).** The lock is total for the lot in progress (the delivered-lot exception above never covers it; for a delivered lot, see the end of this item), so a test that is wrong against its sheet — e.g. the regex `/\d{1,2} /` of an acceptance test rejecting « T3 2027 annoncée », which the sheet allows — would leave the lot handed back with no way out. The way out is the lead's, outside any wave: correct the test in the working tree (the sheet is left as it is: unchanged since the red commit, or amended by commits of the lot and committed, see « A sheet amended in the same lot » below), then `cadence spec reseal <lot> <test> --reason "why the test contradicts the sheet"`. The program checks that the lot is in progress (`doing`; for a delivered lot, see the end of this item), that the test is listed in the lot's sealed `acceptation-<lot>.json`, that the sheet is unchanged since the red commit or amended by commits of the lot (below), that **no other listed test** changed (the lock keeps everything else) and that the test did change; it then redoes the fingerprint of that one test, appends `{ test, raison, date, avant, apres }` (plus `ficheAmendee` for an amended sheet, below) to the `resceaux` key of `acceptation.json`, commits the test and `acceptation.json` (`test(L35): acceptation rescellée, <test> : <reason>`, the new red commit) and the lot's note with the reason (`plan: …`). It is refused during an orchestrated wave (`CADENCE_ORCHESTRATED`, or the repository's wave lock while one of its lots still works, like `cadence deliver`), **while a wave that is not `done` (interrupted, suspended, or still alive with a lot waiting on a question) keeps open any lot of this repository that is still `doing` in this repository's plan (L238)** — the resealed lot, the `--porteur` or any other lot of the repository: the lock hands back every open lot of the repository, so a reseal committed in the meantime was handed back to any of them as the « first offending commit » of the lock (ol-companion L172, then L173). Open means any non-final state in the wave (a question waiting for its `--answer`, a lot suspended by the quota or the budget, a queued lot, a session running): a stopped wave holds no lock, but its `--resume` replays those lots. The filter is by repository (the repository of the lot's project, not its neighbour repositories: lot ids belong to each plan, so a finance-tracker L172 never blocks an ol-companion L172) and by plan: a lot that is `done` or absent in the plan never refuses, so an old interrupted wave whose lots are long delivered blocks nothing, and neither does a `done` wave. The error is a single message that names **every** wave concerned, with its launch folder and its open lots, the wave that holds the repository lock included (alive: while it has an open lot the lock refuses first, so the same message lists it together with the stopped waves of the repository, not the bare « orchestration en cours » line `cadence deliver` gives), and gives for each stopped wave ONE command, runnable as it stands, with a `--drop` per open lot of the repository in that wave, the queued ones that are not `doing` in the plan included (L239: `--resume` would relaunch them, and the lot would take the lock back and find the tree dirty with the corrected test): `cd <launch folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> --drop <project>:<lot>` (`--resume` replays the whole wave except the dropped lots, so one command per lot would start sessions on the others; it looks for the wave in the folder it is run from only); a wave that is still alive is not called suspended, the error says it is running and is to be waited for. A repository reached by another path (a worktree, a symbolic link) is the same repository: the `.git` directories are compared by their real path. The waves read are those of `.cadence/runs/` of the repository root, of the plan's folder and of every folder above them (a wave keeps its state where `cadence orchestrate` was launched, usually the parent folder of the projects), with no `--reason`, and on a lot that is neither in progress nor delivered with a green acceptation. Then, depending on how the lot stands: a lot waiting on a `question` sits in a wave that is not `done`, which refuses the reseal (L238): answer it first (`--resume --wave <wave> --answer`, the lot then goes on or is handed back) or close it (`--resume --wave <wave> --drop`), reseal afterwards if the lot is handed back (a rewrite of `acceptation.json` without a `resceaux` entry stays a violation of the lock); a lot handed back with `verrou des tests d'acceptation` is finished for `--resume`, so it is replayed by a new wave `cadence orchestrate <project>:<lot>`, which starts again from the red commit. A session never does it, three guards: (1) `cadence spec reseal` is in the sessions' `--disallowedTools` (like `raf done`/`review`/`ux`); (2) a resealed commit is adopted as the new red commit only if it belongs to **no session of the wave** (commits listed in each `steps[].commits`, or in `headBefore..headAfter` of any session of any lot of the wave) — otherwise it stays a violation of the lock; (3) the lock is checked at the end of **every** work session, a session that ends with a question included, so a session that replaces the test, forges the fingerprint, commits and asks a question is handed back with `verrou des tests d'acceptation` without waiting for the lead's `--answer`. The reseal is atomic: if the commit fails (a `pre-commit` hook exiting 1, say) `acceptation.json` and the test are put back as they were before the command (the lead's correction in the working tree is kept, nothing staged) and a readable error says nothing was resealed. With a plan kept read-only by another tool, the reseal itself succeeds but the plan is not touched: the note of the lot is left to the lead (the reason stays in `acceptation.json` and in the commit message; the command only says a note is to be added). With a plan outside the repository or untracked, the note **is** written in the plan file but not committed (the command says `plan à commiter`); do not add it a second time. If the commit of the note is refused (a hook), the reseal stays done: the error gives its commit and says the note, already written, is to be committed by hand. **A sheet amended in the same lot (L217):** a sheet amended since the red commit is accepted, for a lot in progress as for a delivered one (the sheet is then not put back to its red state to be restored afterwards). When its net difference with the red commit (`git diff <red> -- <sheet>`) is not empty, the command reads `git log <red>..HEAD -- <sheet>`: every commit that touched it must cite the lot (read like `raf commits`, i.e. `citedRefs`: the scope of the subject first, the body otherwise; for a delivered lot, the lot, the `--porteur` or any delivered lot of the plan, status `done`, L229: the living sheet is the reference; one cited delivered lot among several is enough), and the sheet must be committed. The fingerprint of the test is then redone against the current sheet and the `resceaux` entry gains the optional key `ficheAmendee` (short sha of the last commit that touched the sheet); the lot's note and the commit message say `fiche amendée en <sha>` instead of `fiche inchangée`. Still refused, with nothing written: a sheet modified by a commit that cites neither the lot nor (delivered lot) the `--porteur` or a delivered lot (`la fiche … a été modifiée depuis le commit rouge … par un commit qui ne cite pas <lot> : <sha>`; for a delivered lot `… ne cite ni <lot>, ni le porteur <porteur>, ni un lot livré : <sha> (cite : <lots ou aucun lot>) — faire citer le porteur ou un lot livré par ce commit (le réécrire avant qu'il soit poussé) ou, sinon, rescelle à la main avec le lead`), a sheet deleted or renamed since the red commit, a sheet with uncommitted changes (working tree or index: `a des modifications non commitées : la commiter (en citant <lot>) avant le rescellé`), as before any other listed test touched, a test left unchanged, an orchestrated wave and a missing `--reason`; the atomicity is unchanged. A sheet amended then brought back to the identical content is treated as unchanged, whoever committed in between. After the reseal the new red commit is the reseal commit, so a second reseal sees the sheet as unchanged. **A delivered lot (L212):** a lot whose acceptation is green (`vert: true`) and whose sealed test turns out unstable while the contract does not change (seen: a tab read as « blank » in CI) had no way out but the « sheet modified in the same lot » exception. `cadence spec reseal <lot> <test> --reason "why" --porteur <lot in progress>` now covers it, with the same guards (the sheet unchanged since the red commit, or amended as above by commits that cite the delivered lot, the carrier or another delivered lot; no other listed test touched, no wave, `--reason`) and three differences: `--porteur` is required and names the lot in progress that carries the fix (refused if it is the delivered lot itself, unknown or not `doing`); the commit cites the **carrier** (`test(L113): acceptation rescellée du lot livré L47, <test> : <reason>`) and so does the plan note and its commit, the delivered lot being closed; the dated `resceaux` entry also records `porteur`, and `vert` stays `true`. `--porteur` on a lot in progress, or `spec reseal` on a delivered lot without `--porteur`, is refused.
359
+ - **A closed lot left at `vert: false`: `cadence spec green` (L366).** The cycle sets `vert: true` itself; a lot driven and closed by hand (`raf done`) never gets it, and its `acceptation-<lot>.json` then keeps the lock total on its tests: every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them. Three guards:
360
+ - `cadence spec green <lot>` runs the dedicated command (`orchestrate.accept`, same rules as the cycle: the commands whose root holds a listed file, `{acceptation}` and `CADENCE_ACCEPTATION` set; a listed file under no command's root is refused) and, when it is green, sets `vert: true` and commits the file alone, citing the lot (`chore(L12): acceptation verte, vert: true posé par cadence spec green`, or the `plan.commit` template of the project when it has one). The subject stays short and fixed: the resealed tests are listed in the commit body, one `- <path>` line each, so a `commit-msg` hook bounding the header (commitlint `header-max-length` 100) accepts the commit. Tests refined by the lot after the red commit (the case of issue #18: its own fix commits changed them) would still be reported by the lock, whose reference is the red commit: the command therefore reseals every listed test whose fingerprint changed (the `empreintes` are redone, one `resceaux` entry each) in that same commit, which becomes the red commit; a listed test changed but not committed is refused before anything runs. Red: the tail of the output on stderr, exit 1, nothing changed. Already green: nothing to do. Refused for a `todo` lot, with `CADENCE_ORCHESTRATED` set, while a wave holds the repository lock and while an unfinished wave keeps a lot of the repository open (the same refusals as `spec reseal`, worded for `spec green`: « spec green : refusé … puis relancer spec green »).
361
+ - `raf done <lot>` refuses a lot whose acceptance is `vert: false` (the message names the file and the command above); `--force` closes it anyway and warns on stderr.
362
+ - `cadence orchestrate … --dry-run` prints once per repository `⚠ <repo> · acceptation rouge d'un lot clos : L12 (<file>, vert: false)` for each `done` lot left that way, before the wave starts.
355
363
  - **Back to green, set by the cycle.** At the end of each `implement`/`fix`, once the session reports green, the cycle runs the dedicated command. Red: the findings go to a `fix` pass (the tests are made green without editing the locked files). Green: the cycle sets `vert: true` and commits it (`chore(L7): acceptation verte …`;
356
364
  `commitPlan` may commit the `acceptation.json` files, otherwise the dirty tree would stop the lot). The files then join `orchestrate.test` and the CI, which keep the lot green from then on (an `"e2e": true` acceptation excepted: its files stay in the e2e suite after the build); the following lot, running with `vert: true`, is compared with the head before its own work, not with the red commit, only once the sheet has changed between the red commit and that head: after an admitted evolution (tests changed along with the sheet) it sees no difference either; only a test changed *during* that lot without the sheet is a deviation.
357
365
  - **When the lot is handed back** (question, budget, passes exhausted) its tests stay red but excluded from the project's runner and CI: the next lots and `cadence deliver` do not see them; `--resume` continues with the same lock. On resume, the step is skipped when the lot already has a valid committed **and sealed** `acceptation.json` (its `empreintes` set by the program); a conforming file without them proves nothing and `accept` runs again. A sealed green one (`vert: true`) also skips the step when its seal no longer holds (L233); a green one without `empreintes`, with `empreintes: {}`, or whose `empreintes` lack one of the listed files (copied from another file or lot) proves nothing and `accept` runs again (the contents are not compared here, only the presence of one fingerprint per listed file). The fingerprints are keyed by normalised path, so a listed `./test/x.ts` is recognised like `test/x.ts`, on resume and by the lock (which compares it to its fingerprint).
358
366
 
367
+ ## Elements of a reference document in the briefs (L367)
368
+
369
+ When the title, the goal or a note of a lot quotes ids of a reference inventory (`<slug>#<type>-<n>`, see [ref](ref.md#cadence-ref-coverage-inventairejson)), the `spec` brief lists them and asks the spec writer to take each into the sheet (its `source` quotes the id) or to justify it (`<id> : <reason>` under « choix » and in `perimetre.hors_perimetre`), and the `ux` brief asks the ux-reviewer to check each on the finished screen (a small visible lot has no `ux` pass: its single `review-small` pass gets the same instruction and checks the ids on the finished screen). The ids are read from the plan at launch (`lotIds`, `src/ref/coverage.ts`); a lot without id has unchanged briefs.
370
+
359
371
  ## The downstream briefs: sheet, rule → test → green, conformity, Hypothèses
360
372
 
361
373
  Once the sheet exists, the steps after it all read it (L174, `src/orchestrate/aval.ts`, the `{{fiche}}` marker of the templates `implement.md`, `fix.md`, `fix-minors.md`, `review.md`, `review-small.md` and `ux.md`). A lot without a sheet (no `screen:`, or under the threshold) keeps exactly the briefs it had.
@@ -363,8 +375,10 @@ Once the sheet exists, the steps after it all read it (L174, `src/orchestrate/av
363
375
  - **`implement` and `fix`** get the path of the sheet and of the mockup, and **one line per rule** (`- R3 [erreur, hypothese] statement. Message: « … ». Tests: « R3 — … »`). The session makes the acceptance tests green **without modifying them** (they are named when the `accept` step ran; the lock above hands the lot back otherwise), **the tests it writes in addition cite the rule** in their name (`R2 — …`), and it **does not invent a rule**: a case the sheet does not cover is written in the report (`nonVerifie` or `choix`), not coded. In a `fix` the sheet stays the reference: a finding that contradicts it is contested under `choix`. Both briefs also say that a conflict with the sealed test of **another** lot is a question to the lead naming both tests (file and `Rn — …` name), never a modification or a choice (L231). The `accept` and `accept-back` briefs and the `acceptance-writer` agent require each assertion to cite, in a comment, the rule and the sentence of its statement that it verifies, to assert nothing beyond that sentence, and to go through a self-check before the commit (an assertion with no sentence behind it is removed or listed under `choix`); the `review` and `review-small` briefs and the `code-reviewer` agent re-read each sealed assertion against the sentence it cites, and one that asserts more than the sheet is a *major* finding (« sealed test stronger than the sheet », worded « than its rule » in the briefs), corrected by the lead with `cadence spec reseal` (L231).
364
376
  - **A self-check list before handing back (L222).** Both `implement.md` and `fix.md` carry the same fixed list (plain text, no marker, no new variable), taken from the recurring findings of the reviews of 2026-10-15: the usage documentation (README and architecture documentation) updated in the same commits as the behaviour; one test named `Rn — …` per rule of the sheet (one per business rule of the lot when there is no sheet); no dead code and no test that cannot fail (an assertion always true, or a test that does not exercise its rule); for a modified screen, the capture due of each state at 1440 and 390 px with a file name (where it goes is the Playwright paragraph's rule) (a lot with no modified screen owes none and says so); no false comment, docstring or message. The session goes through it point by point on its own commits before it reports, and a point it cannot hold is written under `choix` or `nonVerifie` of its report: the report schema has no new field. In `fix` the list covers only what the session changed, and "fix them, and nothing else" stays. The other briefs (`fix-minors`, `spec`, `spec-back`, `accept`, `review-small`…) are unchanged. The before/after measure — correction passes per lot, replayed with `cadence corrections` (`--until <bound>` for the "before", `--since <bound>` for the "after", the bound being the time of the `npm run service` that puts the list in service) — is in [docs/mesures/autocontrole-avant-revue-2026-10-15.md](mesures/autocontrole-avant-revue-2026-10-15.md).
365
377
  - **Auto-verification once the tests are green (L236).** After the self-check list, `implement.md` and `fix.md` carry a second fixed block: when the full suite and the build pass, the session re-reads the complete diff of its lot (of its fix commits, in `fix`) against the grid of the `code-reviewer` agent — code bug, architecture drift, `docs/architecture.md` (or the project's architecture documentation) up to date, edge cases, one test per changed behaviour — fixes what it finds at once (test first, a `fix(<lot>): …` commit, suite and build replayed), then starts `resume` with an « auto-vérification » paragraph: what it re-read, found and fixed, could not hold. No new report field. The cost bound is written in the brief: one reading of the diff, not a second review (no subagent, no files the diff does not touch). Measure before the block (`cadence corrections --classify`, 10 to 17-10): 356 passes, 53 % code bug, 19 % architecture drift, 14 % unclear spec; `cadence perf` of 17-10: 37 % right the first time. Target: above 50 % on the next window with tokens per lot stable (`cadence perf`).
378
+ - **Real walkthrough before the auto-verification (L344).** Reading a diff did not show three major review findings of 26-10 (a command impossible in its own documented path, a « champs exclus » read against a decision of 22-10, a failure banner out of view). `implement.md` therefore asks, once the tests are green, to play what was delivered as a user would: a command in a throwaway repository (`git init` in a fresh temporary folder), following the documentation the lot wrote, command by command; a screen in each of its states, failure included and shown where the action takes place, with a capture of each. What breaks is fixed in the same session (test first) and played again; the report says it in a paragraph of `"resume"` that starts with « parcours réel » (no new field; a lot with nothing to play says so). `fix.md` does not carry it: a correction pass fixes the findings it is given.
379
+ - **Decisions to respect (L344).** The program (not the pre-check session, which many lots skip) reads the notes of the lot and of its parent lot (`parent:`) and keeps those that name who decided (Sylvain, the human, the lead, the user) and say that they decided, validated, asked or refused (`src/orchestrate/decisions.ts`). The notes the program writes itself are never taken for one, whatever they quote: a hypothesis of a fiche (« Hypothèses (fiche …) — », a choice nobody validated), a minor of a review (« Revue de … — mineur : »), a replaced title (« titre remplacé : », the stale one), « remis en attente » and « abandonné : ». Each is given with its date and its text as written (cut at 1500 characters) under « Decisions to respect » in the `implement` and `fix` briefs (and in the `--dry-run` briefs), with the rule: an interpretation that departs from one of them is a question under `"questions"`, never a choice under `"choix"`. Nothing is written when no note qualifies. The effect is measured by the share right the first time of `cadence perf` over two windows.
366
380
  - **The code review (`review`, `review-small`) checks rule → test → green** (`règle → test → vert`). For each `Rn` of the sheet the reviewer checks that its test exists (named after the rule), that it is green, and that the behaviour is in the diff; when an acceptance file is named, that each listed test is unchanged since the red commit (`git diff <red commit> -- <file>` empty; the brief gives the red commit). A rule without a test, an acceptance test modified since the red commit, a rule coded that is not in the sheet, and a test that is green without exercising its rule are **major** findings. The reviewer no longer rebuilds the cases from the code.
367
- - **The UX pass becomes a conformity pass.** For a lot with a sheet, the `ux` session (the `ux-reviewer` agent, **conformity mode**) no longer audits the whole screen: it compares the running app to the mockup and to its captures `maquette-<état>-<largeur>.png`, state by state, at 1440 and 390 px, and reports the differences of rendering and ergonomics the tests do not cover, each tied to a rule or a measure. The rules of the sheet are the tests' job. Its verdict feeds `raf ux`, which stays the lead's, decided on the captures. A small visible lot (single `review-small` pass) gets the conformity instruction in that pass.
381
+ - **The UX pass becomes a conformity pass.** For a lot with a sheet, the `ux` session (the `ux-reviewer` agent, **conformity mode**) no longer audits the whole screen: it compares the running app to the mockup and to its captures `maquette-<état>-<largeur>.png`, state by state, at 1440 and 390 px, and reports the differences of rendering and ergonomics the tests do not cover, each tied to a rule or a measure. The rules of the sheet are the tests' job. Its verdict feeds `raf ux`, which stays the lead's, decided on the captures. A small visible lot (single `review-small` pass) gets the conformity instruction in that pass. It gets the verification of the reference ids the lot quotes there too (L367, see above).
368
382
  - **The `## Hypothèses` section of the news entry.** The `implement` brief asks for an entry that ends with `## Hypothèses`: one line `- Rn — …` per rule whose status is `hypothese`, with what changes if it is wrong (its `si_faux`) — it is what the lead shows for the `raf ux` decision, the choices nobody validated.
369
383
 
370
384
  ```markdown
@@ -486,6 +500,21 @@ except a copy that is still needed, which stays and is said: a copy that a live
486
500
  The waves that count are those of the launch folders the script knows (`<folder>/.cadence/runs/<wave>/wave.json`): the repository itself and the working directory of every wave the orchestrator's registry still lists (`~/.cadence/orchestrate/waves/<pid>.json`; the registry drops a dead wave when another registers, so it does not know suspended waves by itself). There is no search over the whole disk: a suspended wave launched from another folder is not protected here, and `--resume` repairs its snapshot instead (above). A wave never marked `done` keeps its copy until it is, or until its folder is removed. A failure of the purge (unreadable copy, busy file) once `service` and the links have switched is a warning on the error output (`avertissement : purge des anciennes copies interrompue …`): the new version is in service, the fingerprint is printed, the exit code is 0, the next put in service tries again. The first run is also what installs the links in `~/.local/bin`. It prints the fingerprint of the copy
487
501
  (bin, dist, package.json).
488
502
 
503
+ ## The subject of the plan commits (`plan.commit`, L343)
504
+
505
+ The commits the program writes for the plan in a wave (`plan: <lot> démarré (orchestrate <wave>)`, `… revue de code enregistrée`, `… revue de code notée`, `… hypothèses de la fiche <sujet> notées`, `… mineurs de la revue de <revue> notés`) take the template of the `plan.commit` key of `cadence.yaml`
506
+ (`{lot}`, `{action}`, `{vague}`; the same key sets the plan commit of `cadence spec reseal`; absent, the subject is unchanged — keys and rules in [docs/raf.md](raf.md#the-subject-of-the-plan-commits-plancommit-l343)).
507
+ A project whose `commit-msg` hook enforces Conventional Commits sets for instance `plan: { commit: 'chore({lot}): {action} (orchestrate {vague})' }`.
508
+
509
+ `--dry-run` plays the subjects the lot would write, for each lot, through the repository's **real** `commit-msg` hook, as git would run it (`git rev-parse --git-path hooks/commit-msg`: `core.hooksPath` and husky included; a hook that is not executable is skipped by git and here). Every lot writes `démarré`, `revue de code enregistrée` and `revue de code notée`; a lot with a spec stage (`spec: true` or `screen:`) also writes `mineurs de la revue de la fiche notés`, the sheet sourced by the lead's answer (action `règles R1, R2 sourcées par la réponse du lead`, through the same `plan.commit` template; absent, `docs(<lot>): règles R1, R2 sourcées par la réponse du lead (orchestrate <wave>)`, lower-case after the colon because `subject-case` refuses a capital) and, for a `spec: true` lot without a screen, `hypothèses de la fiche <sujet> notées`. Those sheet subjects are measured with a subject of **real length**, since `header-max-length` (100) depends on it: the lot's sheet when it is already written (its `sujet.id` for the hypotheses, its **costly** hypotheses for the lead's answer, since the answer only sources the rules the wave asked about — with none, no answer is committed and none is played), otherwise the longest subject of the sheets folder and as many rules as the sheet of the folder that has the most costly hypotheses (one at least); with no sheet in the repository at all, `<sujet>` and one rule, with a warning that the measure is short. Measuring the answer with every hypothesis of a sheet (13) declared refused an answer that cites five. It prints under the lot:
510
+
511
+ - `messages de plan : 3 message(s) acceptés par le hook commit-msg du dépôt (<hook>)` (5 or 6 for a lot with a spec stage: the hypotheses subject only exists for a `spec: true` lot without a screen) when none is refused;
512
+ - `✗ message de plan refusé par le hook commit-msg : « <subject> » — <what the hook said>` for each one refused, then the advice to set `plan.commit` (it governs every one of them, the lead's answer included): without it the lot would fail on its first refused plan commit, before or during its sessions (the hypotheses are noted after the spec session). The dry-run still exits 0 (it simulates, it does not gate);
513
+ - for a lot with a spec stage, what the measure used: `fiche du lot : docs/specs/<sujet>/fiche.yaml (hypothèses R1, R2)`, or `fiche du lot pas encore écrite : mesure avec le sujet le plus long de docs/specs (<sujet>) et <n> règle(s)`;
514
+ - nothing when the repository has no `commit-msg` hook and no `plan.commit` is set.
515
+
516
+ When a plan commit is refused all the same (a hook of the project, a header too long), the plan files are unstaged and what cadence wrote is undone before the lot fails, so the next lot of the wave does not find a dirty repository and is not handed back. What the lead had changed in the plan without committing before cadence wrote (a `raf add` or `raf note` in the project repository during a session: the plan files, the files maintained with them, the `acceptation.json`) is put back as it was, not erased: those files stay modified, as before the commit, and only the content cadence added (the hypotheses notes, the review record) is gone.
517
+
489
518
  ## One worktree per lot (`orchestrate.worktrees`, L279)
490
519
 
491
520
  > **On by default in cadence (L320, 2026-10-25).** After the trials L310 and L316 (−45 % and −22 % of wall time, three automatic integrations without incident) cadence's own
@@ -518,7 +547,7 @@ nothing below applies and the wave behaves as before; any other value is refused
518
547
  read-only plan (`plan:` in `cadence.yaml`) gets `orchestrate.start` and `orchestrate.verdict` run in that checkout, never in the worktree; a plan
519
548
  outside the repository is read and written in place, without a commit. The lots' branches carry work commits only (the acceptance seal and
520
549
  `vert: true` are work of the lot), so a plan conflict at the rebase cannot happen. A session that modifies the plan file, a file generated with it
521
- (`plan.files`, the Gantt page) in its worktree — committed or not — gets its lot handed back, naming the file and the commit.
550
+ (`plan.files`, the files of `plan.onWrite`, the Gantt page) in its worktree — committed or not — gets its lot handed back, naming the file and the commit.
522
551
  Before each of these writes the program checks that the repository is still on the branch the lots started from, that no tracked file other than the
523
552
  plan is modified in the project's checkout, and takes the lock; a refusal hands the lot back with the cause
524
553
  (`écriture du plan refusée : arbre du dépôt modifié (<files>)`, `dépôt sur <branch> et non <start branch>`, `verrou tenu par <wave> (pid <pid>)`), writes
@@ -531,6 +560,11 @@ nothing below applies and the wave behaves as before; any other value is refused
531
560
  (`arbre du dépôt modifié (<files>)`): the program never changes the lead's branch or tree. The check is made again right before the
532
561
  fast-forward (the replayed checks and the short review can last) and right before the plan is written: a lead moved to another branch, or whose
533
562
  tree got dirty, in the meantime gets neither the fast-forward nor a plan commit, and the lot is handed back with `next: integrate` (L299);
563
+ **When the repository's branch itself moved** during that time (a plan commit of the lead, another lot closed by hand), the fast-forward is no longer
564
+ possible: instead of handing the lot back, the program replays steps 2 to 5 at the new tip (rebase, replayed checks, verdict carry-over or short
565
+ review, fast-forward), at most **twice**, and logs `<project>:<lot> main a avancé pendant l'intégration, rebase rejoué (n/2)`. A branch that moves again
566
+ after the second replay hands the lot back with `intégration refusée : main a encore avancé après 2 rebases rejoués`, `next: integrate`, nothing of
567
+ the lot in the repository's branch (L364);
534
568
  2. the lot's branch is rebased on the tip of the repository's branch (`git rebase`); a conflict is **aborted** (`git rebase --abort`), the lot
535
569
  is handed back with the files in conflict, its worktree and branch untouched, and no session tries to resolve it. **One exception** (L314): two
536
570
  parallel lots that each add an entry under `## [Unreleased]` of `CHANGELOG.md` (what `docs.sync` imposes on every lot) always collide, so a commit
@@ -541,6 +575,11 @@ nothing below applies and the wave behaves as before; any other value is refused
541
575
  branch) was added by an earlier commit of the same lot, so the replayed commit may reword or remove it — its version wins, the earlier one is dropped
542
576
  from the repository's side — while an entry that was already there at the starting point, reworded, stays a conflict by hand. The
543
577
  journal says `<project>:<lot> CHANGELOG.md : conflit d'ajout pur résolu par union sous [Unreleased] (<n> commit(s))`. Anything else — an entry that
578
+ **A release during the branch's life** (L362): when the repository's branch was released after the lot's branch was cut, its `## [Unreleased]` became
579
+ `## [x.y.z]` and git, merging without conflict, files the lot's entries under that published version. After the rebase the program moves the entries of
580
+ the lot (those in its `[Unreleased]` and not in the `CHANGELOG.md` of the starting point) that landed in the published part, and were not already
581
+ there on the repository's branch, back under the repository's `## [Unreleased]`, in a commit of its own (`docs: CHANGELOG — les entrées du lot replacées
582
+ sous [Unreleased] …`); the journal says `<project>:<lot> CHANGELOG.md : <n> entrée(s) du lot replacée(s) sous [Unreleased], …`.
544
583
  was already there removed or reworded, free text under `[Unreleased]`, the rest of the
545
584
  file changed on both sides, another file in conflict with it — aborts the rebase and hands the lot back as above. The rebased commits differ, so
546
585
  steps 3 and 4 apply: tests replayed, and a short review since the patch-id changed;
@@ -698,7 +737,7 @@ a time (above); `Agent`, `Monitor`, `git push`, `cadence deliver`, `raf done|rev
698
737
  temporary `pre-push` hook, installed for the duration of the wave and removed at its end (or, for a repository, as soon as all the lots that touch it are finished), refuses any push
699
738
  from a session (`CADENCE_ORCHESTRATED` is in their environment; a repository that already has another
700
739
  `pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
701
- upstream ref and `git ls-remote` are compared with the "before" (a `git checkout <sha>` or `git switch` that only moves `HEAD` — detached `HEAD`, another branch, no upstream any more — is not a push: the upstream ref is compared only when it is the same branch on both sides, `git ls-remote` stays the judge; the write briefs forbid `git checkout` and `git switch` in the repository and point to a throwaway `git worktree add --detach <tmp> <sha>`; should a write session still end off its starting branch, a clean tree with nothing outside the branch gets the branch put back (`git checkout <branch>`, a warning in the log, the lot carries on from the right `HEAD`), anything else — including a put-back that fails (`index.lock`, a conflicting file), cause « HEAD déplacé — remise de la branche <b> impossible : <git message> » — hands the lot back with the cause « HEAD déplacé » and holds the queue of that repository, the other repositories carrying on; a push or a removed guard hook in ANY watched repository, project or neighbour, is checked first and wins over a moved `HEAD`, L214), and a read-only step that left only UNTRACKED files (screenshots written at the root by a script with a relative path) is not an incident: the files are moved to `.cadence/runs/<wave>/<project>--<lot>/stray/` (kept in their tree; for a neighbour repository, under `stray/<its relative path, `/` replaced by `_`>/`, e.g. `stray/.._gitops/`), a warning naming them goes to the lot's warnings and the wave log, the verdict is kept and the lot continues (L162; a move that is impossible, or an untracked file that vanished during the step, stays an incident); a review that changed `HEAD` or a tracked file (a commit, a modified file) is an incident of that lot only: the lot is handed back to the lead, the lots of the other repositories carry on while the lots still queued behind it in the same repository are suspended (resumable, nothing started on a dirty tree), and a verdict the review had already produced is reported in the outcome and the lot's warnings (never recorded in the plan). A push or a removed guard hook still stops the wave. `raf done|ux|review` and `cadence deliver` refuse when
740
+ upstream ref and `git ls-remote` are compared with the "before" (a `git checkout <sha>` or `git switch` that only moves `HEAD` — detached `HEAD`, another branch, no upstream any more — is not a push: the upstream ref is compared only when it is the same branch on both sides, `git ls-remote` stays the judge; the write briefs forbid `git checkout` and `git switch` in the repository and point to a throwaway `git worktree add --detach <tmp> <sha>`; should a write session still end off its starting branch, a clean tree with nothing outside the branch gets the branch put back (`git checkout <branch>`, a warning in the log, the lot carries on from the right `HEAD`), anything else — including a put-back that fails (`index.lock`, a conflicting file), cause « HEAD déplacé — remise de la branche <b> impossible : <git message> » — hands the lot back with the cause « HEAD déplacé » and holds the queue of that repository, the other repositories carrying on; a push or a removed guard hook in ANY watched repository, project or neighbour, is checked first and wins over a moved `HEAD`, L214), and a read-only step that left only UNTRACKED files (screenshots written at the root by a script with a relative path) is not an incident: the files are moved to `.cadence/runs/<wave>/<project>--<lot>/stray/` (kept in their tree; for a neighbour repository, under `stray/<its relative path, `/` replaced by `_`>/`, e.g. `stray/.._gitops/`), a warning naming them goes to the lot's warnings and the wave log, the verdict is kept and the lot continues (L162; a move that is impossible, or an untracked file that vanished during the step, stays an incident); a review that changed `HEAD` or a tracked file (a commit, a modified file) is an incident of that lot only: the lot is handed back to the lead, the lots of the other repositories carry on while the lots still queued behind it in the same repository are suspended (resumable, nothing started on a dirty tree), and a verdict the review had already produced is reported in the outcome and the lot's warnings (never recorded in the plan). **The lead's own push is the one exception (L352)**: `git push origin <sha>:main` to deliver a ready lot while the wave runs (the « livrable jusqu'à <sha> » path of the lead skill) is tolerated, because the pushed state is one the lead has re-verified. The check is the guard of `cadence deliver --sha`: every remote branch that moved advanced fast-forward (or was created) to a sha that is an ancestor of the repository's `HEAD` and holds no commit of a lot of the wave that is not ready (running, queued, in question, handed back, failed, or whose commits cannot be read), so no commit of a lot of the wave that is not ready (a commit that cites no lot, such as one of the plan, passes); the journal says `push du lead (livraison de <lot>) toléré` (`livraison du plan` when no ready lot is in the range) and the session carries on. Any other push — a deleted branch, a forced push, a sha outside the history of `HEAD`, a commit of a lot still running — and a removed guard hook still stop the wave. `raf done|ux|review` and `cadence deliver` refuse when
702
741
  `CADENCE_ORCHESTRATED` is set.
703
742
 
704
743
  **Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
@@ -709,6 +748,14 @@ the remaining budget can also pay the fix and the short review that follows it (
709
748
  pass, 65 k over 497 passes, plus the p90 of a short Opus review, 88 k over 272; measured on 2026-10-15 in the wave journals
710
749
  `.cadence/runs`). A lot refused that fix is handed back with the cause `correction non jouée (budget) :` first, so the lead
711
750
  takes the correction by hand (L218).
751
+ When `orchestrate.escalation` is declared, the lot's budget also carries the share of the escalation (L349): `ESCALATION_RESERVE`, 175 k tokens counted, added to the
752
+ budget derived from the estimate (600 k becomes 775 k, 750 k becomes 925 k, 1.3 M becomes 1.475 M), `--dry-run` printing `plus 175000 réservés à l'escalade <model>` in the basis. It is an
753
+ **estimate**: no escalation had been played when it was written (the only journal carrying one, L342, left it not played), so it is the p90 of a fix pass (67 k over 823 passes) plus the p90 of a full
754
+ review (109 k over 1 009), 176 k rounded down; to be remeasured on the first escalations played (`cadence perf`). Until the escalation engages, the ordinary passes
755
+ do not spend that share: an implementation or a fix stops 175 k short of the budget, and the escalation itself needs only what a correction and its review need (`REVIEW_RESERVE`) from the full budget. Once engaged
756
+ (or ruled out), the share is released. The refusals and warnings that quote the budget (« correction non jouée (budget) », « budget du lot atteint », the « code sans test » gate, the
757
+ librarian) count the lot against the budget of its ordinary passes and name the share: `6000 / 160999 tokens comptés, 175000 retenus pour l'escalade sur 335999`, never
758
+ `6000 / 335999` with 155 k said to be missing. A lot of a wave started before L349, or in a project without `escalation`, has no share.
712
759
  Exception: when the tests are red after a write pass, the lot goes to a fix pass without a review; if that fix pass is
713
760
  refused for lack of budget the lot is handed back to the lead without a review, with its findings, and the cause names
714
761
  the red tests: the pass it names is the one whose tests were red, counted by its number (« la passe fix 2 », « l'implémentation »), and « la passe des mineurs » appears only when that pass was the one played. The wave budget stays for the others. When the budget (or the usage limit) is reached no new session starts, the running ones
@@ -781,7 +828,9 @@ News instruction (`cadence news new <lot>`, factual user-side text, a screenshot
781
828
  ```yaml
782
829
  orchestrate:
783
830
  test: npm test # run by the orchestrator after a work step (optional)
831
+ testReplay: npx vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS {files} # replay alone of a red test before it counts (L354, see « A red test is replayed alone »); `{files}` required; absent: no replay, nothing held for unstable (optional)
784
832
  build: npm run build # run BEFORE the tests (L333); both results go to the reviewer, who does not redo them (optional)
833
+ worktreeDeps: [node_modules, .venv] # untracked dependencies that `cadence verify --gate` links into its throwaway worktree (L348, `docs/deliver.md`); absent: the ignored folders of every level, as the worktrees of a wave (optional)
785
834
  precheck: true # default: before the first implementation of a lot with no commit whose title cites a finding or an earlier lot, and whose title or `goal:` cites a finding (`constat`, `revue`, `review`, `audit`) or a plan id (an earlier `L<number>`, also followed by a hyphen as in `L12-L15` or `L12-bis`; any other id such as `Q-CAN5`; `project:L<number>`), a read-only Sonnet session checks whether the deliverable is already in the repository (see below); false skips it, local runs it on claude-local (Ollama) with Sonnet as fallback, for every lot with no commit whatever its title (L296); a lot created after the last code commit of the repository is not pre-checked, local or Sonnet, unless it cites a lot that already has commits (L322)
786
835
  accept: npx vitest run -c vitest.acceptation.config.ts # accept step (see « The accept step »): the dedicated command that runs ONLY the acceptance tests of the lot (optional; without it the step is skipped with a warning)
787
836
  maquette: true # default: UX review of the mockup between spec and accept (see « The mockup review »); false skips it
@@ -848,7 +897,12 @@ the `.nvmrc` again for every live lot and refuses it like at start if it can no
848
897
  The file is read trimmed (`22`, `v22`, `22.22`, `v22.22.3`); the highest matching version installed under
849
898
  `$NVM_DIR/versions/node` (default `~/.nvm/versions/node`) **that has an executable `bin/node`** is used: a higher
850
899
  version with an empty or broken `bin` is skipped for a lower valid one (and `--dry-run` says so); if none is valid
851
- the lot is refused. Only the sessions' environment changes,
900
+ the lot is refused. The same `PATH` goes to every command the program
901
+ launches itself — `orchestrate.test`, `orchestrate.build` (the baseline run at the start sha included), `start`, the acceptance
902
+ runs and `verdict` — so the gate judges the lot under the Node the session wrote it for, not under the one the wave was
903
+ started with. `--dry-run` adds `⚠ .nvmrc 24 ≠ node du PATH v20.20.2 : …` under the lot when `orchestrate.test` or
904
+ `orchestrate.build` is declared and the `node` the `PATH` resolves does not match the `.nvmrc` (nothing is printed without a
905
+ gate, without a `node` on the `PATH`, or when they match). Only the environment of the sessions and of these commands changes,
852
906
  never the orchestrator's own. No `.nvmrc` → nothing changes. A `.nvmrc` that cannot be resolved (`lts/*`, an
853
907
  alias, a version not installed) refuses the lot before anything is started (exit 2), naming the requested
854
908
  version and the folder searched — there is no silent fallback to the default Node. `--dry-run` prints
@@ -874,7 +928,11 @@ folder whenever Playwright was loaded for the lot, even if no `ux` step ran
874
928
 
875
929
  Without `start`, a read-only plan's `todo` lot is refused (start it with the project's tool); without
876
930
  `verdict`, the review verdict stays in the wave's state and you report it. Only the plan's files
877
- (`plan.path`, `plan.files`, the QA expectations file) are committed from those commands; anything else dirty stops the lot (`.cadence/runs/`, `.cadence/corrections.json` and `.playwright-mcp/` excepted, as above).
931
+ (`plan.path`, `plan.files`, the files of `plan.onWrite`, the QA expectations file) are committed from those commands; anything else dirty stops the lot (`.cadence/runs/`, `.cadence/corrections.json` and `.playwright-mcp/` excepted, as above).
932
+
933
+ ### `plan.onWrite`: the published plan follows every write (L346)
934
+
935
+ When `cadence.yaml` declares `plan.onWrite: { command, files }` ([raf](raf.md#regenerate-the-published-copy-on-every-write-planonwrite-l346)), the wave runs `command` at the repository root after each write of the plan it makes (start of the lot, notes of the sheet and of the review, review recorded), and the produced `files` go into the same `plan: …` commit as the plan, even when they were not tracked yet. If the command fails the write is refused: the plan is put back, nothing is committed, and the lot is handed back naming `plan.onWrite en échec`.
878
936
 
879
937
  ### Time limit of a write step (L298)
880
938