@sylad/cadence 1.8.0 → 1.10.0

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 (105) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +27 -54
  4. package/agents/code-reviewer.md +2 -2
  5. package/agents/doc-keeper.md +19 -0
  6. package/agents/precheck-reader.md +1 -1
  7. package/agents/qa-reviewer.md +2 -2
  8. package/agents/reference-analyst.md +25 -0
  9. package/agents/reference-verifier.md +19 -0
  10. package/agents/spec-writer.md +1 -1
  11. package/agents/ux-reviewer.md +1 -1
  12. package/bin/cadence.js +9 -1
  13. package/dist/audit.js +16 -4
  14. package/dist/check.js +24 -2
  15. package/dist/cli.js +258 -30
  16. package/dist/config.js +101 -5
  17. package/dist/corrections.js +24 -2
  18. package/dist/deliver.js +125 -37
  19. package/dist/docsync.js +19 -3
  20. package/dist/git.js +16 -3
  21. package/dist/lead.js +45 -12
  22. package/dist/migrate.js +166 -0
  23. package/dist/orchestrate/briefs.js +46 -6
  24. package/dist/orchestrate/changelog-merge.js +94 -0
  25. package/dist/orchestrate/command.js +358 -41
  26. package/dist/orchestrate/cycle.js +922 -285
  27. package/dist/orchestrate/guard.js +2 -2
  28. package/dist/orchestrate/integrate.js +129 -0
  29. package/dist/orchestrate/launch.js +48 -6
  30. package/dist/orchestrate/mutex.js +30 -0
  31. package/dist/orchestrate/no-test.js +41 -0
  32. package/dist/orchestrate/pool.js +74 -36
  33. package/dist/orchestrate/registry.js +146 -14
  34. package/dist/orchestrate/result.js +1 -1
  35. package/dist/orchestrate/snapshot.js +54 -1
  36. package/dist/orchestrate/spec.js +89 -5
  37. package/dist/orchestrate/state.js +56 -15
  38. package/dist/orchestrate/table.js +74 -3
  39. package/dist/orchestrate/timeouts.js +40 -0
  40. package/dist/orchestrate/worktree.js +368 -0
  41. package/dist/perf.js +188 -25
  42. package/dist/plan.js +112 -7
  43. package/dist/proc.js +6 -0
  44. package/dist/ready-free.js +109 -0
  45. package/dist/ref/add.js +129 -0
  46. package/dist/ref/analyse.js +220 -0
  47. package/dist/ref/check.js +338 -0
  48. package/dist/ref/command.js +92 -0
  49. package/dist/ref/corpus-render.js +83 -0
  50. package/dist/ref/corpus.js +201 -0
  51. package/dist/ref/diff.js +262 -0
  52. package/dist/ref/evaluate.js +241 -0
  53. package/dist/ref/figures.js +252 -0
  54. package/dist/ref/measure.js +330 -0
  55. package/dist/ref/pdf.js +116 -0
  56. package/dist/ref/png.js +125 -0
  57. package/dist/ref/regard.js +102 -0
  58. package/dist/ref/session.js +34 -0
  59. package/dist/ref/store.js +40 -0
  60. package/dist/ref/tools.js +51 -0
  61. package/dist/resources.js +51 -0
  62. package/dist/session.js +5 -2
  63. package/dist/state.js +75 -7
  64. package/dist/testgate.js +15 -0
  65. package/docs/cadence-hud.md +12 -0
  66. package/docs/cycle-vague-dark.svg +1 -1
  67. package/docs/cycle-vague-light.svg +1 -1
  68. package/docs/deliver.md +76 -5
  69. package/docs/fonctionnement-wave-dark.svg +1 -1
  70. package/docs/fonctionnement-wave-light.svg +1 -1
  71. package/docs/fonctionnement.md +6 -6
  72. package/docs/lead.md +40 -13
  73. package/docs/news.md +30 -1
  74. package/docs/orchestrate.md +275 -33
  75. package/docs/overview.md +65 -1
  76. package/docs/perf.md +14 -8
  77. package/docs/raf.md +90 -11
  78. package/docs/ref.md +415 -0
  79. package/docs/releasing.md +3 -3
  80. package/docs/session.md +2 -2
  81. package/docs/skills.md +21 -2
  82. package/docs/spec.md +1 -1
  83. package/docs/stabilite.md +4 -4
  84. package/docs/whats-new.md +30 -1
  85. package/package.json +2 -1
  86. package/skills/deliver/SKILL.md +6 -1
  87. package/skills/lead/SKILL.md +28 -10
  88. package/skills/session-close/SKILL.md +1 -1
  89. package/skills/session-start/SKILL.md +2 -2
  90. package/templates/inventaire.json +96 -0
  91. package/templates/orchestrate/accept-back.md +1 -0
  92. package/templates/orchestrate/accept.md +1 -0
  93. package/templates/orchestrate/fiche.md +1 -0
  94. package/templates/orchestrate/fix-docs.md +18 -0
  95. package/templates/orchestrate/fix-minors.md +2 -1
  96. package/templates/orchestrate/fix.md +5 -2
  97. package/templates/orchestrate/implement.md +7 -4
  98. package/templates/orchestrate/maquette.md +2 -1
  99. package/templates/orchestrate/precheck.md +2 -1
  100. package/templates/orchestrate/review-recheck.md +2 -0
  101. package/templates/orchestrate/review-small.md +5 -1
  102. package/templates/orchestrate/review.md +2 -0
  103. package/templates/orchestrate/spec-back.md +2 -1
  104. package/templates/orchestrate/spec.md +4 -3
  105. package/templates/orchestrate/ux.md +3 -1
@@ -12,6 +12,7 @@ exception is a **formatting retry** (below).
12
12
  cadence orchestrate finance-tracker:L41 ol-companion:L22 cadence:L18@haiku
13
13
  cadence orchestrate L18 # from inside a project
14
14
  cadence orchestrate maritime-atlas:Q4/accueil-4-ux12@haiku # a lot id may contain "/" (sub-tasks of read-only plans)
15
+ cadence orchestrate maritime:B28.4 --goal "…" # the goal replaces the stale title as the objective of the briefs (below)
15
16
  cadence orchestrate … --budget 1.5M # 1500000, 1.5M, 800k; default 2M
16
17
  cadence orchestrate … --max-sessions 3 # sessions running at the same moment, all waves together; default 2
17
18
  cadence orchestrate … --dry-run # preconditions + the plan of the wave; nothing is started
@@ -19,9 +20,17 @@ cadence orchestrate --continue [--until 18:00] [--priority cadence,maritime] #
19
20
  cadence orchestrate --waits [--since 2026-10-12] # time spent waiting for a session slot, per day and per wave (below)
20
21
  cadence orchestrate --status [<wave>] # the live waves and the repositories they hold, then the table (default: the last wave of this folder)
21
22
  cadence orchestrate --status [<wave>] --watch [--interval 10] # the same, redrawn every 10 s (--interval in seconds); stops by itself when no wave is running (with an explicit `<wave>`: when that wave is no longer running)
22
- cadence orchestrate --resume [<wave>] [--budget 1M] [--answer ol-companion:L22 "reply"] [--drop ccc:L29]
23
- cadence orchestrate --resume <wave> --budget maritime:B26.4=+300k # replay a lot handed back for its budget, with the budget raised (below)
23
+ cadence orchestrate --resume [--wave <wave> | <wave>] [--budget 1M] [--answer ol-companion:L22 "reply"] [--drop ccc:L29]
24
+ cadence orchestrate --resume --wave <wave> --budget maritime:B26.4=+300k # replay a lot handed back for its budget, with the budget raised (below)
25
+ cadence orchestrate --resume --wave <wave> --integrate ccc:L29 # replay a lot handed back at the integration step of a worktree wave (« One worktree per lot »)
26
+ cadence orchestrate --resume --wave <wave> --replay ccc:L29 # same, and also a lot handed back on a refused plan write (fiche notes) at the step it had reached
27
+ cadence orchestrate --remove-worktree ccc:L29 [--abandon-commits] # remove the kept worktree and branch of a lot (« One worktree per lot »)
28
+ cadence orchestrate --resume … a:L1 # refused: a resume replays the lots of the wave, it launches none
24
29
  cadence orchestrate --resume … --dry-run # refused: a resume cannot be simulated, it writes the wave state and relaunches sessions
30
+ # `--resume` without a wave resumes the only unfinished wave of the folder; with several, it lists them and is refused (exit 2): name one with `--resume --wave <wave>`
31
+ # every resume command the program prints (under a question, after `--stop-after-current`, on a lot handed back at the integration step, in the `spec reseal` refusal, in the several-waves refusal) has the form `cadence orchestrate --resume --wave <wave> …`; `--resume <wave>` with an id that looks like a lot (`suite2`) is read as a lot to launch and refused, and the refusal prints the `--wave` form
32
+ # the table of a wave (end of wave and `--status`) prints, under each question, the command that answers it with the wave named: `cadence orchestrate --resume --wave <wave> --answer <project>:<lot> "…"` (`--wave` rather than `--resume <wave>` so that an id that looks like a lot, `suite2`, is read as a wave too)
33
+ # a resume running on the wave's snapshot receives `--resume <wave>` in place of `--wave <wave>`, so an older snapshot cannot pick another wave; a wave whose id looks like a lot (`suite2`) cannot be named that way, so it is resumed with the current `dist/` and templates
25
34
  cadence orchestrate --drop ccc:L29 [--wave <wave>] # from another terminal: take a lot out of the running wave
26
35
  cadence orchestrate --stop-after-current [--wave <wave>] # from another terminal: let the running sessions finish, start nothing else
27
36
  ```
@@ -32,16 +41,34 @@ from (`--wave <id>` when several waves run from it), and write a request in the
32
41
  `journal.log`. `--drop <project:lot>` (repeatable) takes a lot out of the wave: a lot not started yet never starts (no
33
42
  `raf start`); a lot with a session running **lets that session finish** (it is not killed), then plays no other: it is
34
43
  *handed back* (`retiré de la vague (--drop)`), its commits stay, and a lot that depended on it is handed back too. A lot
35
- waiting for an answer (`question`) is handed back as well, at the end of the wave or on `--resume --drop`, instead of staying in
44
+ waiting for an answer (`question`) is handed back as well, at the end of the wave or on `--resume --wave <wave> --drop`, instead of staying in
36
45
  question forever. A lot that is unknown or already finished is refused (exit 2). `--stop-after-current` lets the sessions running finish, starts no
37
46
  other session nor lot (`arrêt demandé (--stop-after-current)`), and leaves the wave **interrupted** and resumable
38
- (`--resume`, which forgets the stop; `--continue` draws nothing more). Neither combines with lots to launch,
47
+ (`--resume --wave <wave>`, which forgets the stop; `--continue` draws nothing more). Neither combines with lots to launch,
39
48
  `--continue`, `--dry-run`, `--budget` or `--answer`, nor `--status`; without a live wave from that folder they are refused
40
- (exit 2). A stopped wave is no longer live, so to take a lot out of it use **`--resume --drop <project:lot>`**: the lot is
49
+ (exit 2). A stopped wave is no longer live, so to take a lot out of it use **`--resume --wave <wave> --drop <project:lot>`**: the lot is
41
50
  handed back instead of being replayed (an unknown or finished lot is refused, exit 2, before anything runs). A lot taken out
42
51
  this way is left out of the resume checks (repository lock, `.nvmrc` Node, dirty tree): only the lots that will be replayed can
43
52
  refuse the resume, and a refusal writes nothing to `control.log`.
44
- `--resume --stop-after-current` is refused (exit 2): resuming contradicts stopping.
53
+ `--resume --wave <wave> --stop-after-current` is refused (exit 2): resuming contradicts stopping.
54
+
55
+ **`--goal "…"` and `goal:` — the objective of the briefs when the title is stale (L258)**: the first line of the `{{objectif}}` of every
56
+ brief of a lot (and the title quoted in their first sentence) is the lot's title; when a change of scope made it stale, the lead no longer
57
+ has to delegate by hand. Three ways to give the goal, in this order of precedence (`--goal`, then the plan's key, then the title):
58
+ - `--goal "…"` on the command line: `cadence orchestrate maritime:B28.4 --goal "…"`. It takes **exactly one launched lot** (two lots,
59
+ `--continue`, `--resume`, `--status` are refused, exit 2, with nothing written); an empty text, or one starting with `--` (a forgotten
60
+ text: `--goal --dry-run` would otherwise swallow the option and start a real wave), is refused.
61
+ - the `goal:` key of the lot in a **native plan** (`.cadence/raf.yaml`): `goal: Dedup on merge, bank line kept`.
62
+ - on a plan **kept by another tool** (read-only, mapped by `plan.fields` in `cadence.yaml`, e.g. maritime's `docs/plan/taches.yaml`): map the
63
+ key that holds the goal, `plan.fields: { goal: objectif }` (a list takes the first key present, like the other fields); without that
64
+ mapping only a key literally named `goal` is read (a field left out is read under its own name), any other key is ignored without a message.
65
+ The key is read for every lot of a launch, so it is the way to give a goal to several lots at once, on both plan kinds (cadence only reads the plan,
66
+ it never writes a goal into it). The notes and sub-tasks of the lot stay in the objective. The goal read at launch is kept in the state
67
+ of the lot and is the only source afterwards (briefs, `--dry-run`, table): a `--resume` replays the sessions with the goal of the launch, and
68
+ a `goal:` added or changed in the plan since is not read (to change it: `--resume --wave <wave> --drop` the lot and launch it again). `--dry-run` prints
69
+ `objectif donné : …` under the lot, and the table of the wave repeats it (`<project>:<lot> — objectif donné : …`) so the lead sees which
70
+ objective the sessions worked from.
71
+ The review and UX briefs carry it too, on a line `Goal of the lot (it takes precedence over the title in the plan): …`, and `code-reviewer` / `qa-reviewer` measure the commits against it, not against the stale title still in a read-only plan.
45
72
 
46
73
  **`--resume --budget <project>:<lot>=+N` — replaying a lot handed back for its budget (L223)**: a lot handed back with
47
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
@@ -77,10 +104,10 @@ second one would leave a session idle — and is completed with the same reposit
77
104
  alphabetical order. Launched from inside a project, only that project is drawn. A `priority:` key that cannot be read is refused before anything
78
105
  starts, lots given or not. A candidate that preflight refuses (dirty tree, Node missing…) is skipped with its cause in the
79
106
  journal (`continue : lot sauté — <cause>`; for the first draw also on the error output). Drawing stops — `continue : arrêt — <reason>` — at: the budget, the
80
- time window (`--until HH:MM`, today's clock: nothing new is drawn from then on, a running lot is not cut), the usage limit,
107
+ time window (`--until HH:MM`, today's clock: nothing new is drawn from then on, a running lot is not cut), the usage limit (« usage limit reached », « rate limit », « You've hit your weekly limit »),
81
108
  a question asked (the wave stays resumable with `--answer`), two lots handed back in a row, an interrupted wave, a `--stop-after-current` request (`arrêt demandé`, even when the last running
82
109
  lots finished cleanly), or no ready
83
- lot whose estimate fits the budget left. `--resume … --continue` draws again after the resumed lots. `--dry-run --continue` replays the
110
+ lot whose estimate fits the budget left. `--resume … --continue` draws again after the resumed lots. When the wave starts, `--continue` (and its `--dry-run`) prints the line `prêts, ressources libres : <project>:<lot> <title> (<resources>) · …` that `cadence lead tour` prints (L252): the ready lots, from every project of the folder, whose `uses:` resources (`raf uses`, `resources:` of the parent `cadence.yaml`) are held neither by a lot `doing` (of the folder's projects, and of every project that shares their resources: those under the folder whose `cadence.yaml` declares `resources:`, walked with its `depth:`, one level by default, so launched from a group or from inside one project it still sees the others) nor by an unfinished lot of a live wave nor by the lots of the first draw of the wave that starts (a lot of a wave is recognised by its repository and its id, not by the project name; those lots, and the ready lots that compete with them for a resource, are not listed); at most 3 lots per project, then `<project> +K`; nothing is held back by it, the draw below is unchanged. `--dry-run --continue` replays the
84
111
  draws round by round with the same function as the wave (one lot per repository per round, pre-check, budget counted on the estimates) and
85
112
  names the candidates skipped with their cause: `tour 1 : a:L2, b:L2 · tour 2 : a:L3, a:L4`. Drawing happens between rounds: a round of up to `--max-sessions` lots must finish before the next draw.
86
113
 
@@ -112,7 +139,7 @@ keeps its `/` (`maritime-atlas:Q4/accueil-4-ux12@haiku`); this holds for `--dry-
112
139
  same for the lot folder and the dry-run briefs); ids without `/` are unchanged.
113
140
 
114
141
  You choose the lots; the order is the order given (one queue per repository, as many repositories at the
115
- same time as the session cap allows — 2 by default). `@haiku|@sonnet|@opus` sets the model of the implementation and corrections of that
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
116
143
  lot (default Sonnet; reviews are Opus, except the light review below; Haiku only when you write it, for a mechanical lot). Run it
117
144
  in the background and read `--status`: it prints one line per transition and the final table.
118
145
 
@@ -137,7 +164,11 @@ usability (code only when the lot is not `visible`). Failing tests (reported red
137
164
 
138
165
  **One UX review per lot (L221)**: the UX review of a `visible` lot used to run before the code review and again after every correction (finance L75 on 2026-10-15: three Opus UX sessions for one useful verdict). It now runs once, as the last step, on the code the review found compliant: after the single pass on minor findings and its short review when there is one. Without `orchestrate.ux`, or when the app does not start (port taken, silent app), no UX session is played: the lot concludes on the compliant code review and keeps its note (« UX à faire par le lead », « UX non vérifiée : … »). The lot budget bounds writing, never this review; an exhausted wave budget suspends it like any session. Every write step (implementation, correction) clears the lot's UX state (`ux`, `uxVerdict`, UX findings), so a stale UX verdict never sends a compliant code review back to a correction. A resumed wave whose state says `next = ux` (written before this change) replays the code review first unless the last compliant one read the current `HEAD`. The recorded `raf review` verdict and sha are the code review's; a commit made by someone else since that review hands the lot back, as before. A small lot and a lot with no screen keep their single review. The measure before/after is in [`docs/mesures/ux-une-fois-2026-10-15.md`](mesures/ux-une-fois-2026-10-15.md).
139
166
 
140
- **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 and nothing is built there, so such a monorepo keeps the pre-L213 « tests rouges » outcome. 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`.
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
+
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`.
170
+
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.
141
172
 
142
173
  **Review sized to the lot (L108)**: a lot whose `estimate` is ≤ `orchestrate.review.threshold` (0.25 day by default)
143
174
  is *light*: one single review (Sonnet by default, `code-reviewer` agent, same criteria), no minors pass — the minors are
@@ -152,6 +183,40 @@ orchestrate:
152
183
 
153
184
  `full` is also the model of the UX review and of the single pass of a small lot that is not light.
154
185
 
186
+ **Code changed without a test (L287)**: before any review session, the program checks the diff of the lot (every commit that
187
+ cites it, all sessions included). If it touches code (`testGate.code`, minus `testGate.except`) without touching a single test
188
+ file (`testGate.tests`), the lot goes straight to a correction pass — no review session is spent finding what a file list
189
+ already shows (a missing test was 32 % of the major findings of the 24-10 audit, one pass ≈ 190 k tokens). The pass counts
190
+ as one of the two correction passes, its brief names the files, and the gate plays once per lot: if the correction adds no
191
+ test, the review goes ahead and judges it. It does not play during the minors pass, nor when a correction pass is already used (the gate's pass would take the last one and the review would arrive with none left to correct its majors),
192
+ nor when the lot budget no longer pays the correction and the review after it (a warning says so each time; the review goes ahead). Absent key: no gate.
193
+
194
+ The diff read is that of the lot's repository **and** of its neighbour repositories (`repos:`, same as the « work already present » check; patterns apply to each repository's own paths, so a test in a neighbour lifts the gate). Files a commit **deletes** do not count as code without a test (`git diff-tree --diff-filter=d`): a lot that removes dead code used to trigger a pass of about 65 k tokens for nothing. `docs.sync` still sees deleted files.
195
+
196
+ On a light lot (see « Review sized to the lot »), the gate's correction pass is pass 1: the review that follows is therefore the full one (Opus), not the light one (Sonnet). A light lot that touches code without a test pays the correction **and** an Opus review.
197
+
198
+ ```yaml
199
+ orchestrate:
200
+ testGate:
201
+ code: [src/**, bin/*.js] # required: patterns of the code files (same patterns as docs.sync, `!…` excludes)
202
+ tests: ['**/*.test.*', '**/*.spec.*', 'test/**', 'tests/**', '**/__tests__/**'] # default: one touched test file lifts the gate
203
+ except: [src/**/*.d.ts] # code files that need no test (types, configuration, migrations…); default none
204
+ threshold: 1 # default 1: how many code files (after exceptions) before the gate plays
205
+ ```
206
+
207
+ `testGate: false` is the same as no key.
208
+
209
+ **Librarian before the review** (L306). When `docs.sync` (see [news](news.md#documentation-that-follows-the-code)) reports that
210
+ the commits of the lot touch code without touching the document that describes it, the lot goes to a documentation pass
211
+ before its review: a `fix` session launched with the `doc-keeper` agent (`agents/doc-keeper.md`) and its own brief
212
+ (`fix-docs`), which lists the lines the program computed and asks for the documents to be brought up to date, nothing else. The
213
+ pass counts as one of the two correction passes and plays once per lot: if the librarian leaves the gap open — it commits no document, and has not acknowledged the gap with `raf docs-ok <lot> --reason "…"` (L317) — the lot is **handed back** (cause « documentation en retard (docs.sync) ») and the review does not play: a lot never ends ready while `raf check` reports a documentation gap. The same check runs at the end of the conforming review, for a lot whose librarian gate could not play (passes or budget used up). An acknowledged gap (`docs_ok:` on the lot, committed with the plan) is no gap: no pass, no finding in the review brief. In a lot's worktree (`orchestrate.worktrees`) only the program writes the plan: the librarian's brief there does not ask it to run `raf docs-ok` — it makes no commit and says why under "choix", the lot is handed back and the lead acknowledges in the main checkout; an acknowledgement the lead committed before the wave is read from the lead's plan (L320). It does not play during the minors pass, nor when a
214
+ correction pass is already used (its pass would take the last one), nor when the lot budget no longer pays it and the review after
215
+ it (a warning `bibliothécaire non appelé` says so each time; the review goes ahead). Without `docs.sync`, or with no gap: no pass.
216
+ When the project's `cadence.yaml` declares `docs.articles` (the same key as the article refreshed after a delivery, see [news](news.md#an-article-elsewhere-that-follows-the-delivery)), the brief names those
217
+ documents with the absolute path of their repository (`repo` relative to the project, or absolute), opens that repository to the pass for reading (`--add-dir`), forbids editing them and asks for a proposed lot in the report; the proposal (the `resume` of the librarian's report, when it starts with « lot proposé ») becomes a proposal of the lot, printed in the
218
+ table at the end of the wave, and is the lead's to take into the plan of the other repository.
219
+
155
220
  **Effort level per pass** (L137). Each session is launched with `claude -p --effort <level>` (Claude Code 2.1.284; an
156
221
  older `claude` is refused at launch unless every pass is `default`). Defaults: `precheck` low (it only reads),
157
222
  `implement` and `fix` medium, `review` high (`review-small` follows `review`), `ux` high. A project overrides any of them
@@ -175,7 +240,7 @@ what the lot asks for (another lot, or a correction, may have done it already).
175
240
  with proofs), `partiel` or `non`: on `oui` no implementation session is opened and the lot is handed back
176
241
  (`livrable déjà présent : <résumé> (<preuves>)`) for the lead to drop or close it; on `partiel` the finding goes into the
177
242
  implementation brief and a warning; on `non` — or an unreadable report, which only adds a warning — the wave goes on. A
178
- lot that already has commits is never pre-checked (resuming it is legitimate). `orchestrate.precheck: false` turns it off.
243
+ lot that already has commits is never pre-checked (resuming it is legitimate), and neither is a lot **whose title and `goal:` cite neither a finding nor another lot** (L288, L292): the pre-check runs only when the title, the `goal:` of the lot (or the goal given at launch) contains the word `constat` (any ending), `revue`, `review` or `audit` (any case, plural allowed; whole words, so `qa-reviewer` and `review-small` do not count), or a whole plan lot id: an `L<number>` earlier than the lot (`L12` in the title of `L30`; `L30` itself, a later lot or `L120` for `L12` do not count), any other id of the plan, whatever the lot (`Q-CAN5`, `B28.1`, `Q4/qa-expectations`; `B28.1` is not `B28.10`), or a lot of another project written `project:L<number>` (`cadence:L68`). Measured on 24-10: 13.4 M tokens (7.1 %) of pre-check for 10 `oui` and 20 `partiel` out of the sessions read, hence the restriction. The restriction costs real `oui`: 7 of the 17 measured (L171, L68, Q-LBL3, Q-NOW5, `Q4/dashboard-21-mesure-prod`, `Q4/qa-expectations`, `Q4/qa-expectations-nouveautes`) have a title that cites nothing, against about a third of the pre-checks avoided (about 20 k tokens each). With `orchestrate.precheck: local` (below) the criterion is therefore dropped (L296): every lot with no commit is pre-checked, on the local model, off the quota. The local/Sonnet agreement on the 37 `oui` and `partiel` verdicts could not be replayed (the repositories now hold every deliverable, so any replay answers `oui`): the safety nets of `local` stay (a local `oui` is confirmed by Sonnet) and the key stays opt-in. The verdict the pre-check reached (`oui`, `partiel`, `non`; an unproven `oui` is recorded `partiel`) is kept in the lot's journal (`precheckVerdict`; for a journal older than L288, `cadence perf` reads it back from the journal: `oui` for a lot handed back `livrable déjà présent`, `partiel` when the lot carries the pre-check finding, `non` for any other lot whose `precheck` step succeeded), and `cadence perf` prints the share of `oui` per window ([perf](perf.md)): change the key or the criterion, then compare the current window with the previous one. **A lot created after the last code commit is not pre-checked unless it cites a lot that has commits** (L322, local or Sonnet): such a lot has nothing to find, everything the repository holds is older than the lot, so no other lot can have delivered it. The skip is narrowed to lots whose title and `goal:` (and the goal given at launch) cite no other lot that already has commits of its own, with the same recognition of lot ids as the criterion above (an earlier `L<number>`, any other plan id) and a cited lot « has commits » when at least one commit of work (not a plan-only one) cites it. A lot of another project cited as `project:L<number>` keeps the pre-check too (its commits are not read). Why: a « suite de revue L<n> » written right after the last fix commit of `L<n>` lists findings that the correction passes of `L<n>` may already have settled before the new lot existed; replayed on the journals since 24-10, the skip would have fired on 5 such lots (L292, L302, L304, L305, L308), two of them (L304, L305) with a real `partiel` verdict. The program dates the lot by the commit that added its id to the plan (`git log -G`, following renames), bounded by the lot's `created:` day (a plan move or migration does not make an old lot young); the last code commit is the last one that does not touch only the plan files (the same test as `raf check`: a plan-only commit is not code). The pre-check session is skipped, the lot goes straight to its first write step, and the lot's journal says it (`contrôle préalable sauté : lot créé après le dernier commit de code du dépôt (<sha>) : rien à retrouver`; `--dry-run` prints the same line and no `precheck` step). The check stays when in doubt: a lot that cites a lot with commits (or a lot of another project), a lot that works in neighbouring repositories (`repos:`), an id not found in the plan history, a repository with no code commit, equal instants. Older lots keep the pre-check. `orchestrate.precheck: false` turns it off.
179
244
 
180
245
  ## The spec step of a lot with `screen:`
181
246
 
@@ -195,7 +260,7 @@ the first implementation: `precheck → spec → maquette → accept → impleme
195
260
  `#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`).
196
261
  - **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,
197
262
  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
198
- judgement; a keyword right after a negation — « rien d'irréversible », « sans suppression », « pas de paiement » — does not count); `--resume --answer <lot> "…"` gives the answer to the resumed `spec` session. Every other hypothesis (wording, order of fields, error state, spacing) stays written in the sheet.
263
+ 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.
199
264
  The agent can also ask a question itself in its report.
200
265
  - **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
201
266
  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).
@@ -275,7 +340,7 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
275
340
  - **The lock, for every session of the repository.** At the end of each `implement` and `fix` the cycle compares, for the acceptation files of **all** lots found under `orchestrate.spec.dir` (a repository has one queue: the next lot runs on the same branch), the listed test files with the red commit
276
341
  (`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
277
342
  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).
278
- - **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`), 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` is continued by `--resume --answer` (the cycle moves its red-commit anchor to the resealed commit, a rewrite of `acceptation.json` without a `resceaux` entry still being a violation); 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.
343
+ - **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.
279
344
  - **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 …`;
280
345
  `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.
281
346
  - **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).
@@ -285,7 +350,8 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
285
350
  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.
286
351
 
287
352
  - **`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).
288
- - **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 relative file name (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).
353
+ - **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).
354
+ - **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`).
289
355
  - **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.
290
356
  - **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.
291
357
  - **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.
@@ -312,6 +378,10 @@ stops the accidental access, not a deliberate workaround; making the file unread
312
378
  the project's own `test`, `build` and `ux.command`, which the program launches. A lot that really needs the cluster is run in a
313
379
  project with `orchestrate.prodAccess: true`: the restriction is lifted for all its sessions (default `false`).
314
380
 
381
+ **Cadence's own pre-check runs on Sonnet (L328, 2026-10-25).** `cadence.yaml` sets `orchestrate.precheck: true`, no longer `local`. Measured on 25 October: the local
382
+ model failed on about one lot in three since 23 October and, when it answered, said `oui` wrongly on L275, L326 and L327 (Sonnet: `non` / `partiel`), 2 to 3 minutes each;
383
+ Sonnet decides in about 16 s for about 20 k tokens.
384
+
315
385
  `orchestrate.precheck: local` (L146) runs the pre-check on the local model of the dev machine instead of Sonnet: the
316
386
  orchestrator launches `claude-local` with the same brief, the `precheck-reader` prompt and tools, no quota used and nothing counted in the
317
387
  lot's budget (the step shows `local` as its model). `claude-local` is **not shipped with cadence**: it is a personal wrapper (a script on the `PATH`, `qwen3-coder:30b` by default) around
@@ -322,7 +392,7 @@ Without such a binary every local check fails and is replayed on Sonnet: keep `t
322
392
  down, timeout, no structured report) or an unreadable report is replayed on Sonnet with a warning, and a local `oui` — the
323
393
  only answer that hands the lot back without an implementation — is confirmed by Sonnet before it counts. A local `partiel`
324
394
  or `non` is taken as is. The default stays `true` (Sonnet): switch to `local` only after comparing verdicts and durations
325
- against Sonnet on a few lots, and go back to `true` the first time the local model gets a `partiel` wrong. `--dry-run`
395
+ against Sonnet on a few lots, and go back to `true` the first time the local model gets a `partiel` wrong. Measured on 23–25-10 over 32 lots: the local pre-check failed on 10 (7 « Autocompact is thrashing », the 64 k window of `qwen3-coder:30b` filled by a plan of several hundred kB; 3 tool calls written as text, `<function=Bash>`, with no structured output), each time replayed in full on Sonnet. L322 therefore (1) gives the local session the lot itself in its brief (title, goal, last five notes, sub-tasks) with the order not to open the plan file and to keep every read small, where Sonnet still reads the plan; (2) takes a session that ends without a structured output (a tool call written as text included) as a failure at once — a local session is never relaunched to format its answer — and (3) **sets the local model aside for the rest of the wave** after the first failure of a local session or the first unreadable local report: the next lots go straight to Sonnet, with `contrôle préalable : modèle local écarté pour la vague (<cause>), Sonnet directement` in their journal. A local `oui` is still confirmed by Sonnet. `cadence perf` measures the pre-check per lot ([perf](perf.md)). `--dry-run`
326
396
  prints the step as `precheck (local)` with its `claude-local` arguments. Those arguments carry neither `--model` nor `--effort` nor `--agents`: `effort.precheck` applies only to the Sonnet
327
397
  session (the fallback, or the confirmation of a local `oui`).
328
398
 
@@ -369,11 +439,20 @@ resumed wave, not from the current `dist/`; a wave without a snapshot (started b
369
439
  package and a warning. One more exception (L223): a wave whose snapshot predates L223 (its `dist/orchestrate/command.js`
370
440
  has no `lotBudgets`) resumed with `--budget <project>:<lot>=+N` runs on the current `dist/` and templates, with a warning
371
441
  on the error output, because that snapshot would refuse the option as an invalid budget; the sessions of such a resume still
372
- get `<wave>/tool/bin` first in their `PATH` (see below), so the `raf` and `cadence` they call are the snapshot's. The copy is removed with the wave folder. The wave's pid in `--status` is the relaunched process.
442
+ get `<wave>/tool/bin` first in their `PATH` (see below), so the `raf` and `cadence` they call are the snapshot's. Before relaunching the snapshot, `--resume` checks that `tool/node_modules` still resolves (L330): that link points to the `node_modules` of the copy that launched the wave, and `npm run service` may have purged that copy since (a suspended wave outlives the few most recent versions put in service). A dangling link is relinked to the `node_modules` of the current package (the copy in service), the repair is written to the wave's `journal.log` and to the error output, and the resume goes on; when the current package cannot resolve its dependencies either, the resume is refused with a message (exit code 2), no stack trace. The copy is removed with the wave folder. The wave's pid in `--status` is the relaunched process.
373
443
  The sessions of the wave also get `.cadence/runs/<wave>/tool/bin` (the `raf` and `cadence` entries of the snapshot) **first in their
374
444
  `PATH`**, ahead of the per-project Node below and of the usual `PATH`: the `raf` a reviewer runs (`raf commits`…) is the
375
445
  snapshot's, not the installed one. A wave without a snapshot leaves the `PATH` as it is.
376
446
 
447
+ **A short temporary folder per lot (L242, L243)**: each session gets a `TMPDIR` of its own lot, `<os.tmpdir()>/cadence-<8 hex of the sha1 of the lot's state folder>`
448
+ (created before the session, exported in the environment of `claude -p` and given to it as an `--add-dir`, so that a write there is not refused outside the working directory; outside the repository, hence never tracked by git),
449
+ and every brief names it: test logs, dumps and exports go there, not in the repository's `tmp/` (sessions left `tmp/suite-l149.txt`, `tmp/vt-*.json`,
450
+ `tmp/*.log` four times on 2026-10-18, one lot handed back for a « dirty repository »). **It is short on purpose**: Chrome (the Playwright MCP) puts its Unix
451
+ sockets under `TMPDIR`, and a socket path is limited to 107 characters. L242 put it under `.cadence/runs/<wave>/<project>--<lot>/tmp` (about 95 characters), Playwright
452
+ refused to start (« Socket path too long ») and the UX reviews of the finance wave 2026-10-19-fin-prev had no capture, 4 lots out of 4. The path leaves 60 characters free
453
+ (a test checks `TMPDIR` + 60 < 108); when `os.tmpdir()` is too long for that, `/tmp` is used. The wave's folder of the lot keeps a `tmp` **symbolic link** to it, to find it from there.
454
+ The folder is shared by the sessions of the lot (implement, review, fix…) and **removed, link included, at the end of each pass of the lot, and when a signal interrupts the wave** (lot finished, suspended or waiting for an answer: nothing is left in `/tmp`; the next session recreates it, empty). A `--dry-run` brief or one delegated by hand has no wave folder, hence no such line.
455
+
377
456
  **The code in service (L195)**: on cadence's own repository, `bin/raf.js` and `bin/cadence.js` load `dist/`, so the `raf` and `cadence` of your
378
457
  `PATH` used to be whatever the working repository last built. The snapshot protects the wave running, not that installed tool, and a wave's
379
458
  sessions run `npm run build`, `npm install`… The code in service now lives in a **separate copy**: a detached git worktree built there,
@@ -389,10 +468,142 @@ npm run service -- <sha> # [--root dir] [--bin-dir dir] [--repo dir]
389
468
 
390
469
  The script refuses a sha that is not an ancestor of HEAD, and a sha whose commits (since the one in service; only the sha itself the first time)
391
470
  cite a lot whose status is not `done` (`plan:` commits are ignored); a lot `dropped` is let through when every one of its commits is undone by a `git revert` present under the sha (L207), and still blocks when one is not. It builds the new copy aside (`npm ci --ignore-scripts`, then `tsc`) and switches
392
- `service` and the two links only once the build succeeded (atomic renames): a failed install changes nothing. The previous copy is kept for a
393
- rollback, the older ones are removed only once they are more than 7 days old, counted from the day they left service (a wave's snapshot links its `node_modules` to the copy that launched it). The first run is also what installs the links in `~/.local/bin`. It prints the fingerprint of the copy
471
+ `service` and the two links only once the build succeeded (atomic renames): a failed install changes nothing. It then purges the old copies (L330): it keeps the copy in service and the **3 put in service before it**
472
+ (a rollback is `npm run service -- <old sha>`; the order is the order of the puts in service, not the install date or the age), and removes the others (`git worktree remove`, then `prune`) —
473
+ except a copy that is still needed, which stays and is said: a copy that a live process uses (its working directory, its binary or an argument of its command line, read in `/proc` and resolved with `realpath`; and the copy a `node_modules` link points to, found on the folders above those paths), and a copy that the snapshot of a wave **not finished** points to (`tool/node_modules` of a suspended, interrupted or running wave, status other than `done`), even with no process alive. The output ends with the purge:
474
+ `purge : 37 copies retirées, 2,3 Go libérés` (`aucune copie retirée` when there was nothing to remove, `; 1 gardée, un processus ou une vague non terminée l'utilise encore` for a copy spared). The copies grew without bound before (41 copies, 2.5 GB on 2026-10-25).
475
+ 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
394
476
  (bin, dist, package.json).
395
477
 
478
+ ## One worktree per lot (`orchestrate.worktrees`, L279)
479
+
480
+ > **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
481
+ > `cadence.yaml` sets `orchestrate.worktrees: true`. The key is the whole switch: the guard that refused it without `CADENCE_EXPERIMENTAL_WORKTREES=1` is gone and the variable is no longer read,
482
+ > so a resumed wave (`--resume`, `--integrate`, `--replay`, `--continue`) needs nothing more than its configuration or its `cadence/<wave>/…` branch. The other projects keep the default (key absent
483
+ > or `false`: one lot at a time, in the repository) until the lead decides otherwise.
484
+
485
+ By default a wave plays the lots of one repository one after the other, in the repository itself: a lot with four correction passes
486
+ holds the whole queue. With `orchestrate: { worktrees: true }` in the project's `cadence.yaml`, two independent lots of the same repository
487
+ play **at the same time**, each in its own `git worktree` on its own branch, within the session cap (`--max-sessions`). Absent or `false`,
488
+ nothing below applies and the wave behaves as before; any other value is refused when the configuration is read
489
+ (`orchestrate.worktrees doit être true ou false`).
490
+
491
+ - **Which lots.** The lots of one repository (as project; two lots that share a neighbour `repos:` are one group) go to worktrees when the
492
+ group has at least two lots, the repository is on a branch (not a detached `HEAD`) and no lot of the group declares `repos:`. Otherwise they
493
+ stay one after the other in the repository, with `orchestrate : <repository> : worktrees demandés mais <reason> : lots joués à la suite` in the
494
+ journal. A lot alone in its repository plays in it. A lot that already has its branch (a resume) keeps its worktree whatever the group.
495
+ - **When.** At the moment the lot takes its place under the cap, after its `raf start` — not at launch: a lot waiting for a place has neither a
496
+ worktree nor a branch. A lot that depends on another lot of the repository (`after`) waits until that one is `ready`, which in this mode only
497
+ happens **after** its integration, then starts from the tip of the repository's branch, which contains it. If the worktree or the branch cannot be
498
+ created (a branch of that name that is not cadence's, a full disk, an `index.lock`) only that lot is handed back, with git's cause
499
+ (`orchestrate : <project>:<lot> : worktree impossible : <cause>`); the other lots go on. Dependency folders ignored by git (`node_modules`…)
500
+ are linked into the worktree like in the baseline worktree.
501
+ - **Names, nothing stored.** The worktree is `.cadence/runs/<wave>/worktrees/<project>--<lot>` — or, when the wave was launched from the repository's own root (its run folder then lies inside the repository), `<CADENCE_HOME>/worktrees/<wave>-<hash of the run folder>/<project>--<lot>`, outside the repository (L301); the branch `cadence/<wave>/<project>--<lot>`; the
502
+ branch the lots started from is noted in the git config of the lot's branch (`branch.<name>.cadence-base`), with the lot and the creation time.
503
+ A resume finds them by their names. `LotState` gains no field: the integration step is the value `integrate` of the existing `next`.
504
+ - **The plan is never written in a worktree.** Everything the program writes to the plan for a lot — `raf start`, the notes of the sheet's
505
+ hypotheses and of the review's minors, the closing of the covered sub-tasks, the review verdict — is written **by the program, in the
506
+ project's own checkout**, one after the other under the repository's integration lock, each as a plan-only commit with explicit paths. A
507
+ read-only plan (`plan:` in `cadence.yaml`) gets `orchestrate.start` and `orchestrate.verdict` run in that checkout, never in the worktree; a plan
508
+ outside the repository is read and written in place, without a commit. The lots' branches carry work commits only (the acceptance seal and
509
+ `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
510
+ (`plan.files`, the Gantt page) in its worktree — committed or not — gets its lot handed back, naming the file and the commit.
511
+ 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
512
+ plan is modified in the project's checkout, and takes the lock; a refusal hands the lot back with the cause
513
+ (`é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
514
+ and commits nothing, and keeps the step the lot reached.
515
+ - **Integration, before `ready`.** At the end of its cycle (compliant review, UX included) the lot is integrated into the repository's branch, and only
516
+ then does it become `ready`; the repository's branch gets the lots one by one, on a linear history. Under the repository's **integration lock**
517
+ (a file next to the wave lock, same format, pid and start time, stale when the holder is dead, plus an in-memory queue because the lots of a wave share
518
+ a pid; the wait is bounded to 10 minutes and holds no session slot):
519
+ 1. the repository must still be on its start branch (`le dépôt est sur <branch> et non sur <start branch>`) and have no modified tracked file
520
+ (`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
521
+ 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
522
+ 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);
523
+ 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
524
+ is handed back with the files in conflict, its worktree and branch untouched, and no session tries to resolve it. **One exception** (L314): two
525
+ parallel lots that each add an entry under `## [Unreleased]` of `CHANGELOG.md` (what `docs.sync` imposes on every lot) always collide, so a commit
526
+ whose only conflicting file is `CHANGELOG.md` and whose conflict is a **pure addition** is resolved by the program: union per section, one title per
527
+ section (duplicated titles are merged), sections in the Keep a Changelog order (`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`, any
528
+ other title after), the entries of the repository's branch first, then those of the lot; the rest of the file follows the side that changed it. **A lot may rewrite its own entry** (L323): an
529
+ entry of the replayed commit's parent that was not in the `CHANGELOG.md` at the branch's starting point (the merge-base with the repository's
530
+ 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
531
+ from the repository's side — while an entry that was already there at the starting point, reworded, stays a conflict by hand. The
532
+ journal says `<project>:<lot> CHANGELOG.md : conflit d'ajout pur résolu par union sous [Unreleased] (<n> commit(s))`. Anything else — an entry that
533
+ was already there removed or reworded, free text under `[Unreleased]`, the rest of the
534
+ 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
535
+ steps 3 and 4 apply: tests replayed, and a short review since the patch-id changed;
536
+ 3. if the rebase changed the commits, the build and the tests (`orchestrate.build`, `orchestrate.test`, in that order, L333) are run again at the rebased tip, a red that
537
+ was not already red at the lot's base sha (L213) hands the lot back and leaves the repository's branch alone. **A red is played once more before the lot is
538
+ handed back** (L328): on 2026-10-25 L326 was handed back for a load-induced red while L325 was running, and the same tip replayed alone was green (3559
539
+ tests). Green the second time: the lot goes on, the journal says `<project>:<lot> tests rouges au sommet intégré, relancés une fois (…)` and the lot
540
+ gets a warning `rouge au sommet intégré, vert à la relance`. Red twice: handed back with the result of the second run. **The refusal names the red
541
+ tests** (up to ten, then `… (+n)`), or the end of the output when the runner's output names none; it used to quote only `npx vitest run en échec
542
+ (code 1) :`;
543
+ 4. the patch-id of the lot's diff (`git patch-id --stable`, work commits only) is compared before and after the rebase. **Same patch-id**: the
544
+ reviewed diff is the integrated diff, the verdict is **carried over** to the integrated sha, with a `reporte: { de, vers, patchId }` trace in the plan
545
+ (and in the journal: `<project>:<lot> verdict reporté : <old sha> → <new sha> (patch-id identique)`); no new review. **Different patch-id** (context
546
+ changed): a short review (`review-small`) of the rebased diff is played before the branch moves, a blocking or major finding hands the lot back. A lot handed back after that short review (late refusal) and resumed with
547
+ `--resume --integrate` plays it again, and the new verdict replaces the old trace in the lot's verdict instead of being added to it (L303);
548
+ 5. the repository's branch advances by `git merge --ff-only` (`<project>:<lot> intégré : <from>..<to>`), then the program writes the verdict and closes the
549
+ covered sub-tasks in a plan-only commit, on the tip. The verdict's `commit` is the last work commit integrated (`vers`), so the `raf done` gate,
550
+ **unchanged**, finds no work commit after the reviewed one; the `reporte` trace is information, the gate does not read it. A read-only plan gets the
551
+ trace in the text passed to `orchestrate.verdict`;
552
+ 6. the worktree and the branch, clean and merged, are removed; the lot is `ready`.
553
+ A stop between the advance and the plan write is resumed at the `integrate` step: the branch being already contained in the repository's, the rebase
554
+ and the advance are skipped and the verdict is written once.
555
+ - **A lot handed back at the integration step** (lock held for more than 10 minutes, repository's branch changed, tracked file modified, rebase conflict,
556
+ red tests, blocking finding of the short review, plan write refused) keeps `next: integrate`, its worktree and its branch. **Only
557
+ `--resume --wave <wave> --integrate <project:lot>` replays it** (repeatable): a plain `--resume`, `--drop`, `--answer` or `--budget` never does. The lot
558
+ restarts at `suspended`, step `integrate` (`<project>:<lot> → suspended — intégration reprise (--resume --integrate)`), and the rebase and the checks run
559
+ again — after a conflict the lead resolves it by hand in the worktree and commits, otherwise the lot is handed back again.
560
+ **A lot handed back on a refused plan write outside the integration** (the fiche's hypothesis notes, or the minor findings of the fiche review: the repository
561
+ is not on its start branch, holds a modified tracked file or the lock stayed held) keeps the step it had reached (`next`, e.g. `implement` or `fiche`) and its
562
+ worktree, and is replayed the same way by the general option `--resume --wave <wave> --replay <project:lot>` (lead's decision of 24 October: the integration option,
563
+ generalised and renamed; `--integrate` keeps meaning the integration step only). The lot restarts at `suspended` on that step
564
+ (`<project>:<lot> → suspended — écriture du plan reprise à l'étape <step> (--resume --replay)`): the notes are written again without duplicates (a review of the
565
+ fiche is replayed and counted once). `--replay` is refused for a lot handed back in any other way, with the same exit code and rules as `--integrate`. Refused (exit 2, before
566
+ anything is written, like the other resume refusals): a lot unknown or named twice, also named by `--drop`, `--answer` or `--budget`, a lot that is not
567
+ handed back at the integration step (`le lot n'est pas rendu à l'étape d'intégration (<status>, étape <step or aucune> : <cause>)`: a lot handed back for
568
+ review, tests or budget has `next` empty and takes the recovery path below), and, like any resume, a repository with a modified tracked file.
569
+ - **A lot handed back outside the integration step** (non-compliant review, red tests, `HEAD` moved, plan touched, acceptance lock, quota…) keeps its worktree and its
570
+ branch `cadence/<wave>/<project>--<lot>` but is not replayed by `--resume`. Its recovery path (L301) is **a new wave of the same lot** (`cadence orchestrate <project>:<lot>`):
571
+ with the worktree mode on (`orchestrate.worktrees: true`), the new wave looks for the **most recent** (creation time noted by cadence) recognised branch `cadence/<other wave>/<project>--<lot>`
572
+ of a past wave that carries unintegrated commits and started from the repository's current branch, renames it `cadence/<this wave>/<project>--<lot>` (git keeps the cadence config
573
+ with it), moves its worktree to this wave's folder and plays the lot there, so the sessions start from the work already done. The journal says it
574
+ (`orchestrate : <project>:<lot> : branche cadence/<wave>/… d'une vague précédente reprise (N commit(s) non intégré(s))`). Nothing is recovered when the branch is integrated,
575
+ started from another branch, is not cadence's, or belongs to a live wave; the lot then starts afresh as before. A lot that recovers its branch **never goes through the pre-check** (L311: its own commits would make the pre-check answer « livrable déjà présent » and hand the lot back with the major of its last review uncorrected): it takes over the findings of the last review recorded by the wave that handed it back and starts at the `fix` step with them, or at the review when that review left none. That starting correction **counts as a pass** (like any `fix` step: the verdict and the table say « 1 passe(s) de correction » after it, and « Two passes at most » bounds the total, recovery included). A lot handed back before its first review (no review step, no finding, or an unreadable state) skips the pre-check too, but starts at the writing step (spec, acceptance or implementation, as a new lot would). If such a lot is handed back for budget with red tests before any correction pass was played (L315), the cause the lead reads is « tests rouges après la dernière revue de la branche reprise » for a recovered lot already reviewed, and « tests rouges après l'implémentation » for a lot never reviewed, recovered or not. A lot counts as reviewed only through a `review*` step or a finding from a review (`code`): the `tests` findings of a lot handed back for red tests are not a review (L318). A branch whose worktree holds a modified tracked file (the same
576
+ guard as `--resume`), or one of whose commits modifies the plan or a file generated with it, is not recovered either (it would end in an integration conflict): the journal says why
577
+ (`… d'une vague précédente non reprise : <cause>`), the lot starts afresh and the kept branch stays untouched for the lead to clean or integrate by hand. The same refusal, with git's own message as the cause,
578
+ answers a `git worktree move` or `git branch -m` that fails (a locked worktree for instance): the move is undone and nothing is renamed. A lot handed back **at the integration step** (`next: integrate`) never takes this path: its
579
+ branch keeps its own wave's name and is replayed by `--resume --integrate`, so a kept branch of another wave is not renamed in its place. The recovery path needs the worktree mode on (`orchestrate.worktrees: true`): without the
580
+ key a new wave leaves every kept branch where it is. The branch of a lot of a wave started at the repository root (`.` as project) is `cadence/<wave>/_--<lot>` (the leading dot of the project becomes `_`).
581
+ **Integrating by hand** (a refused branch, or a lead who prefers to): in the kept worktree (`cadence orchestrate --status` lists it), (1) `git rebase <repository's branch>` and settle the conflicts; (2) run the project's tests and
582
+ build there; (3) from the repository, `git merge --ff-only <branch>` (the branch of the repository must not have moved since the rebase: otherwise rebase again); (4) write the verdict of the lot in the plan (by
583
+ the lead, never by a session); (5) `cadence orchestrate --remove-worktree <project>:<lot> [--wave <wave>]`, which removes the worktree and the branch now that all its commits are integrated.
584
+ - **Nothing is deleted with unintegrated commits.** A worktree whose commits are all in the repository's branch is removed after the integration, by a guarded
585
+ branch deletion (never `-D`). A lot handed back, dropped (`--drop`), failed or in conflict keeps its worktree and branch, and the journal gives the path
586
+ (`orchestrate : <project>:<lot> : worktree conservé : <path> (branche <branch>)`). The final table, `cadence orchestrate --status` and `cadence lead tour`
587
+ list each kept worktree: repository, lot, age, number of unintegrated commits, path (the worktree of a lot a live wave still plays is not « kept »: it
588
+ is in use and not listed). Only the lead removes one:
589
+ `cadence orchestrate --remove-worktree <project>:<lot> [--wave <wave>]` refuses (exit 2, nothing removed) while a commit is neither integrated nor
590
+ declared abandoned with `--abandon-commits`, while the worktree holds a modified tracked file or an untracked file that is not ignored (the removal would delete it),
591
+ and, with or without `--abandon-commits`, while a live wave (the waves registry) still plays the lot; a fully integrated, clean worktree is removed without the option
592
+ (`orchestrate : <project>:<lot> : worktree retiré (<n> commit(s) abandonné(s))`).
593
+ - **Resume of a lot whose branch is gone.** A lot handed back at the integration step whose branch was removed with `--remove-worktree --abandon-commits` has no
594
+ commit left to integrate: `--resume --integrate` creates no worktree for it at the repository's top (also when other lots of its group are resumed with it) and hands
595
+ it back again, `next: integrate`, with the cause « commits abandonnés ». The review verdict is never written against a null `commit`: an integration that finds
596
+ no commit of the lot in the repository's branch is handed back, nothing written in the plan.
597
+ A branch that is still there but lost its `branch.<name>.cadence-base` key (deleted by hand) is not a removed branch: the lot is handed back with a cause that
598
+ names the branch and the missing key, never « commits abandonnés », and nothing is integrated or removed (L303).
599
+ - **Resume.** The worktree of each resumed lot is found again by its name (a worktree git still lists whose folder is gone is pruned and recreated on the
600
+ existing branch) and is subject to the guards of a repository: a modified tracked file refuses the resume (exit 2, nothing written). The wave's lock
601
+ stays one per repository, taken once and released when all the lots of the repository are finished and integrated or handed back; a second wave aiming at
602
+ that repository is refused as before, without creating a worktree.
603
+ - **The application of a UX step.** A step that starts the application (`orchestrate.ux`) waits, in the program, for the application another lot of the
604
+ same repository started, without holding a session slot; steps that do not start the application never wait for it.
605
+ - **Slots.** `cadence orchestrate --status` counts a wave's parallel lots in its held slots, so the free slots are not overestimated.
606
+
396
607
  ## A lot whose work is in a neighbouring repository
397
608
 
398
609
  Some lots are done in another repository that is not a cadence project (B64 of `maritime-atlas` lives in
@@ -436,7 +647,7 @@ the name `repos`). The effect, for `cadence orchestrate`:
436
647
  - **Lock and guard**: the neighbour gets the repository lock (`orchestrate.lock`, so `cadence deliver` refuses it too, and no
437
648
  other wave can hold it), the temporary `pre-push` hook, and the same after-session checks (push, tampered hook, a review
438
649
  that changed it). Two lots that share a repository, as project or as neighbour, run one after the other, never two
439
- sessions in one repository; `--status` lists the neighbours among the repositories a wave still holds (a repository released once all its lots are finished no longer appears).
650
+ sessions in one repository (a lot that declares `repos:` is never put in a worktree, so the rule holds for it even with `orchestrate.worktrees: true`); `--status` lists the neighbours among the repositories a wave still holds (a repository released once all its lots are finished no longer appears).
440
651
  - **Sessions**: every session gets each neighbour as `--add-dir`, and the briefs name them: the implementation and the
441
652
  corrections must commit there too, with a message that cites the lot (`feat(B64): …`), and never push; the reviewers read
442
653
  the commits that cite the lot in the project **and** in each neighbour (`raf commits <lot>` lists them all; in a neighbour only the commits that cite the lot and are dated within the lot's period count, and also contain the `cite:` string when the lot declares one for it — a `fix(B64): …` outside the period, or one without the `cite:` string, is not listed.) A lot whose
@@ -447,7 +658,7 @@ the name `repos`). The effect, for `cadence orchestrate`:
447
658
  review hands the lot back, like one in the project.
448
659
  - **Out of scope**: `raf check` does not audit the neighbours, and their delivery (push, CI, deploy) stays with the lead.
449
660
 
450
- **What stays with you**: choosing the lots, the questions raised (`--resume --answer`), re-verifying
661
+ **What stays with you**: choosing the lots, the questions raised (`--resume --wave <wave> --answer`, the command printed under the question), re-verifying
451
662
  after the wave (`git log`, tests, `raf check`), `raf done`, **`raf ux`** (the orchestrator reports the UX
452
663
  verdict and screenshots, it does not record it), the push and the deliveries, one project at a time.
453
664
 
@@ -463,16 +674,16 @@ across all live waves: 2 by default, `--max-sessions N` (or `CADENCE_MAX_SESSION
463
674
  every wave the same value: each wave counts ALL live sessions, whatever their slot, and waits while that
464
675
  count has reached ITS OWN cap, so with different caps the highest one can push the total past the lowest
465
676
  (which then waits). A session that finds no free slot **waits** (the wave is not refused; the journal says
466
- `en attente d'un créneau de session depuis …`, repeated every minute, then `créneau de session obtenu après …`).
677
+ `en attente d'un créneau de session depuis …`, repeated every minute, then `créneau de session obtenu après …`). A free slot is always taken, even when a request is already written (relaunching a cut review is not a wait; the request applies afterwards). Once the wait has really started (a first try found no free slot), it reads the wave's requests at every turn (L332): a `--drop` aimed at that lot ends it (the slot is never taken, no session starts, the lot is *handed back* `retiré de la vague (--drop)` like a lot not started) and a `--stop-after-current` ends it too (the lot is *suspended* `arrêt demandé (--stop-after-current)`, the wave **interrupted** and resumable); a `--drop` aimed at another lot does not. The step announced but never started is removed from the state and the lead's answer it was carrying (`pendingAnswer`, taken out of the lot when the brief was built) is put back: `--resume` replays the step with the answer in its brief, and a lot handed back by `--drop` says « la réponse du lead (« … ») n'a pas été jouée » in its outcome. When it is the relaunch of a session already run in the step (a cut review relaunched in a fresh session, L256; the formatting retry) that was waiting, the step ends `failed` with its cause (« relance en session neuve non faite : … ») and keeps what the first session left: its id, its tokens (step and wave budget), its stderr in `<n>-<kind>.first.err` (and its output in `.first.json` for the formatting retry).
467
678
  `cadence orchestrate --waits [--since YYYY-MM-DD]` adds those waits up from the journals of `.cadence/runs/` of the launch folder — per day (cumulated wait, number of waits, waves, quota stops — one per "→ suspended — quota atteint" line of the journals, counted on the day of that line, so a wave resumed and stopped again on another day counts once on each day, not once as a wave, tokens) and per wave that waited (the longest wait, a wait that never got its slot) — to decide the cap from figures; each wait is paired with its lot (the journal line carries `project:lot`), so two lots of one wave waiting at the same time (one per repository) are each counted once, and only a wait that never got its slot is reported as such; journals from before this carry no lot and are paired per wave. it reads only, and refuses any other action in the same call. The figures of 2026-10-12 and the cost of a third slot are in `docs/mesures/attente-creneaux-2026-10-12.md`.
468
679
  A slot or a registry entry is owned by a pid **and** its start time: a reused pid is a dead owner. Wave
469
680
  identifiers are reserved atomically (`-2`, `-3` suffix when two waves start in the same minute; an existing
470
681
  `--wave` is refused). The registry of live waves and the slots live under `~/.cadence/orchestrate/` (`CADENCE_HOME`
471
682
  to move it): `cadence orchestrate --status` lists, from any folder, the live waves, the repositories each
472
- still holds (those not yet released), the cap of each wave, the slots in use and the **free slots** (`créneaux libres : N sur M`: M — the cap set with `--max-sessions` or `CADENCE_MAX_SESSIONS`, 2 by default — minus, for each live wave, the smaller of its cap and the repositories it still holds — the figure the `lead` skill reads before starting a subagent; the lead never raises the cap itself, that is the human's decision, L208). Without an identifier it then prints the table of the wave launched most recently from this folder (by launch time, not by alphabetical order of the identifier).
683
+ still holds (those not yet released), the cap of each wave, the slots in use and the **free slots** (`créneaux libres : N sur M`: M — the cap set with `--max-sessions` or `CADENCE_MAX_SESSIONS`, 2 by default — minus, for each live wave, the smaller of its cap and the repositories it still holds (default mode: a lot that declares neighbouring repositories holds each of them; with `orchestrate.worktrees` and lots playing in worktrees, the registry carries `lanes` instead: one per lot playing in a worktree, one per other group of lots) — minus the sessions the lead started by hand and declared with `cadence lead hold <label>` (L255, [lead.md](lead.md#lead-hold-and-release): the line then reads `… · sessions du lead : N (labels) · créneaux libres : …`, and prints even with no live wave) — the figure the `lead` skill reads before starting a subagent; the lead never raises the cap itself, that is the human's decision, L208). Without an identifier it then prints the table of the wave launched most recently from this folder (by launch time, not by alphabetical order of the identifier).
473
684
 
474
685
  **Guards, imposed by the code**: a global cap of simultaneous sessions, one wave per repository at
475
- a time (above); `Agent`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions; a
686
+ a time (above); `Agent`, `Monitor`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions, and they run with `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` (no `run_in_background` on Bash or sub-agents: `claude -p` waits for any background task before exiting, up to 10 min of inactivity, so a finished session kept its slot; L285 — the session is **not** killed at its `StructuredOutput`, the real cost is only in the final `result` message); a
476
687
  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
477
688
  from a session (`CADENCE_ORCHESTRATED` is in their environment; a repository that already has another
478
689
  `pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
@@ -480,7 +691,7 @@ upstream ref and `git ls-remote` are compared with the "before" (a `git checkout
480
691
  `CADENCE_ORCHESTRATED` is set.
481
692
 
482
693
  **Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
483
- shown apart. Each lot also has its own budget, the 90th percentile of the *total* measured per lot on `.cadence/runs` (all the waves a lot went through, 250 lots) for its category (L194): an ordinary lot of up to 0.5 day gets 540 k (floor: a write pass, its review, one fix pass and the short review after it, 155 k being reserved for those last two; measured p90 374 k), 0.5 to 1 day 750 k (p90 732 k), then 570 k per day of estimate, 750 k staying the floor until that exceeds it, i.e. up to 1.3 day (p90 1.14 M for 2 days); a lot with `screen: page` gets 1.3 M whatever its estimate (`component` and `journey` keep the ordinary budget; p90 1.28 M: spec, mockup, accept, conformity UX and passes), or its ordinary budget plus 450 k beyond that; a lot with `spec: true` gets its ordinary budget plus 450 k (the back chain measured on L120: 388 k); shown by
694
+ shown apart. Each lot also has its own budget, the 90th percentile of the *total* measured per lot on `.cadence/runs` (all the waves a lot went through, 250 lots) for its category (L194): an ordinary lot of up to 0.5 day gets 600 k (floor: a write pass, its review, one fix pass and the short review after it, 155 k being reserved for those last two; measured p90 374 k on 11-10, 451 k on 16–22-10: 3 of 145 lots over 540 k, all maritime-atlas, hence 600 k, L247; remeasured on 17–23-10: 0 of 309 lots over budget, p90 452 k, so unchanged, L280; and on 18–24-10: 0 of 322 over, p90 460 k at 600 k, 557 k at 750 k, 744 k at 1.3 M, unchanged again although the tokens per lot rose by a third at estimates 0.25 and 0.5: the rise is the write pass, which the budget bounds with room, L312), 0.5 to 1 day 750 k (p90 732 k), then 570 k per day of estimate, 750 k staying the floor until that exceeds it, i.e. up to 1.3 day (p90 1.14 M for 2 days); a lot with `screen: page` gets 1.3 M whatever its estimate (`component` and `journey` keep the ordinary budget; p90 1.28 M: spec, mockup, accept, conformity UX and passes), or its ordinary budget plus 450 k beyond that; a lot with `spec: true` gets its ordinary budget plus 450 k (the back chain measured on L120: 388 k); shown by
484
695
  `--dry-run` with its basis): it bounds the *writing* passes, never the reviews. A lot that spent it gets no further implementation or
485
696
  fix and is handed back to the lead, but its review (and its single UX review, once the code review is compliant) is played first; a fix pass starts only if
486
697
  the remaining budget can also pay the fix and the short review that follows it (155 k tokens reserved: the p90 of a fix
@@ -491,7 +702,7 @@ Exception: when the tests are red after a write pass, the lot goes to a fix pass
491
702
  refused for lack of budget the lot is handed back to the lead without a review, with its findings, and the cause names
492
703
  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
493
704
  finish, the wave is *suspended* (exit code 3) and `--resume --budget …` continues. A session that returns
494
- nothing readable, times out (45 min for work, 25 for a review) or fails is not retried; the lot is handed
705
+ nothing readable, times out (a work step: see « Time limit of a write step »; 25 min for a review) or fails is not retried; the lot is handed
495
706
  back with the cause. Exit codes: 0 every lot ready · 1 at least one lot handed back (question, failure,
496
707
  review still not compliant after two passes, or three when it converges) · 2 refused before acting · 3 wave suspended.
497
708
 
@@ -504,6 +715,10 @@ on `main`). A lot's commits are read from git (the commits that cite it, whateve
504
715
  version commits do not count), not only from the sessions' reports, so a failed or timed-out session that committed still
505
716
  bounds the line. Nothing is printed when every lot is ready or when the ready lots sit above the unfinished one.
506
717
 
718
+ **Delivering while the wave runs** (L277): the same sha can be delivered before the wave ends. `cadence deliver --sha <sha>` is accepted
719
+ during a wave of the same repository when no commit in the history of `<sha>` belongs to a lot of the wave that is not ready; the
720
+ refusal names the lots that do. A lot's commits are read from git (in the repository and in the neighbour repositories of its `repos:` key) as well as from the sessions' reports, so a session still running or cut by the quota counts; a lot whose commits cannot be read refuses the sha. Without `--sha`, `cadence deliver` is still refused until every lot of the repository is ready or handed back.
721
+
507
722
  **Formatting retry** (the only `--resume` of a session): when a session ends successfully but in plain text,
508
723
  without the `structured_output` the schema asks for (the verdict is there, not in the required shape), the
509
724
  orchestrator resumes **that same session once** (`claude -p --resume <session-id> --json-schema <same schema>`,
@@ -512,9 +727,20 @@ format. It applies to every step that has a schema (implementation, correction,
512
727
  retry count in the wave budget and in the step (`formatRetry: true`); if the output is still missing after it,
513
728
  the step fails as usual — never a second retry.
514
729
 
730
+ **Cut review, relaunched once (L256)**: a read-only step (review, small review, UX, precheck, mockup review, sheet) whose session is
731
+ killed by a signal without writing anything (exit code above 128, such as 143 for SIGTERM, and an empty output) is *cut*. The
732
+ orchestrator launches the same step **once more in a NEW session** (a fresh `--session-id`, the same brief; never `--resume`: a cut
733
+ session has nothing to resume and the formatting retry above does not apply to it) before handing the lot back. The cut session's
734
+ tokens are read from its journal and counted in the wave budget **and in the step** (so the lot's own budget and the table's token column see them), its `stderr` is kept in `<n>-<kind>.first.err`, and the table shows
735
+ `<kind> relancée en session neuve après une coupure (code 143, sortie vide)` among the warnings. Cut again, the lot fails with `coupure : …`.
736
+ A session killed by the orchestrator itself (Ctrl-C or SIGTERM of the wave) is never relaunched: the new one would outlive it. A signal that arrives WHILE the new session runs kills it like any other: it is recorded as the step's session (`sessionId`) the moment it is launched, so the wave's stop kills it and counts its tokens, and so does the resume of a dead orchestrator (`countInterrupted`). Write steps (implementation, correction, spec, acceptance) are never relaunched, nor are the local model, a timeout, a non-empty output or
737
+ an ordinary exit code. The table tells the three stops apart in the line under it: `quota atteint` (the lot is suspended, the wave
738
+ resumes with `--resume`), `coupure : <kind> coupée (…), relancée une fois en session neuve, coupée de nouveau` (the session itself was
739
+ killed twice: look at the machine, not the lot), `échec : <kind> sans résultat : <cause>` (the session ended without a usable report).
740
+
515
741
  State is in `.cadence/runs/<wave>/` of the folder where the command is run (added to `.git/info/exclude`
516
- when that folder is in a repository; `.cadence/` and `.playwright-mcp/`, where the Playwright MCP writes its
517
- output, never count as a dirty repository, even when `.gitignore` does not list them): `wave.json`, one `<project>--<lot>.json` per lot (steps, tokens
742
+ when that folder is in a repository, together with `.cadence/corrections.json`; the rest of `.cadence/`, the plan and its notes, is versioned. `.cadence/runs/`, `.cadence/corrections.json` and `.playwright-mcp/`, where the Playwright MCP writes its
743
+ output, never count as a dirty repository, even when `.gitignore` does not list them, while a change to `.cadence/raf.yaml` does): `wave.json`, one `<project>--<lot>.json` per lot (steps, tokens
518
744
  kept apart, session ids, commits, verdicts), the JSON output of every session and a `journal.log`. After a
519
745
  cut (Ctrl-C, WSL closed) `--resume` replays an interrupted step entirely in a new session whose brief
520
746
  lists the commits already present; finished steps are never replayed.
@@ -544,19 +770,21 @@ News instruction (`cadence news new <lot>`, factual user-side text, a screenshot
544
770
  ```yaml
545
771
  orchestrate:
546
772
  test: npm test # run by the orchestrator after a work step (optional)
547
- build: npm run build # run after the tests; both results go to the reviewer, who does not redo them (optional)
548
- precheck: true # default: before the first implementation of a lot with no commit, 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
773
+ build: npm run build # run BEFORE the tests (L333); both results go to the reviewer, who does not redo them (optional)
774
+ 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)
549
775
  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)
550
776
  maquette: true # default: UX review of the mockup between spec and accept (see « The mockup review »); false skips it
551
777
  maquetteReviews: 3 # default 3: how many reviews of the mockup per lot (integer ≥ 1); also the `spec` session budget of a lot with a screen
552
778
  fiche: true # default: review of the back-end sheet of a lot with spec: true between spec and accept (see « The spec step of a lot with spec: true »); false skips it
553
779
  spec: { dir: docs/specs } # spec step (see « The spec step of a lot with screen: »): folder of the screen specs, relative to the repository (default docs/specs); render: the command the spec session runs to render the mockups at 1440 and 390 px (optional, Playwright otherwise)
780
+ testGate: { code: [src/**] } # code changed without a test goes to a correction pass before the review (see « Code changed without a test »); absent: no gate
554
781
  prodAccess: false # default: wave sessions get no access to production (see « Production access of the sessions »); true lifts it for this project
782
+ worktrees: false # on in cadence's own cadence.yaml since L320; default: one lot at a time per repository, in the repository itself; true plays independent lots of one repository in parallel, each in its own git worktree (see « One worktree per lot »)
555
783
  effort: { review: xhigh } # effort level per pass (see « Effort level per pass »): precheck, spec, maquette, fiche, accept, implement, fix, review, ux
556
784
  ux: http://localhost:4200 # a URL, a launch command, or { command, url, timeout? } — for the UX review (see below)
557
785
  permissionMode: auto # default
558
786
  addDirs: [/home/me/projects/tmp] # extra directories the sessions may use
559
- timeouts: { implement: 45, review: 25 } # minutes
787
+ timeouts: { implement: 45, review: 25 } # minutes; `implement` is the base for a 0.25-day lot (see « Time limit of a write step »)
560
788
  # a plan kept by the project's own tool is read-only for raf: the orchestrator calls these instead
561
789
  start: python3 scripts/raf.py start {lot}
562
790
  verdict: python3 scripts/raf.py note {lot} "revue de code : {verdict}"
@@ -629,11 +857,25 @@ the review stay with the wave, outside the repository, where the lead can look a
629
857
  folder whenever Playwright was loaded for the lot, even if no `ux` step ran
630
858
  (`<project>:<lot> — captures Playwright : <dir>`; `uxCaptures` in the wave state). For `maquette` and `spec` the server also gets
631
859
  `--allow-unrestricted-file-access`: the mockup is a local file, and by default the server blocks every `file://` URL. The `implement`, `fix`,
632
- `fix-minors`, `review-small` and `ux` briefs tell the session to give relative file names if a Playwright server is available, and
633
- otherwise to keep any Playwright CLI output outside the repository.
860
+ `fix-minors`, `review-small`, `ux`, `maquette` and `spec` briefs carry a `{{shots}}` paragraph (L249) when the step loads the server: the absolute path of the wave's capture directory (`<lot folder>/playwright`), to put in front of every screenshot, snapshot and trace name. Measured on `@playwright/mcp` 0.0.83, `--output-dir` only covers the files the server names itself; a name given by the session is resolved against the server's workspace root, the repository, which is why « a relative file name only » left `savings-1440px.png`… at the root of 5 lots out of 5 on 2026-10-23 (finance L119 L120 L122 L124, warhammer L175). The server allows the absolute path (it lies in its output directory). A brief delegated by hand (`--dry-run`) has no wave folder, so no `{{shots}}` (a test reads the briefs `--dry-run` writes to check it); the `ux-reviewer` and `qa-reviewer` agents, which can also be run by hand with the Playwright plugin, give a path under `.playwright-mcp/` there (`.playwright-mcp/page-home.png`), never a bare name, which the server would put at the repository root. The brief otherwise tells the session to keep any Playwright CLI output outside the repository; `stray/` stays the safety net. The `review-small` brief also forbids a capture at the root of the reviewed repository (`romans-1440px.png`…, four times on 2026-10-17: finance L82 and L84, warhammer L138 and L139): it says that a capture reaches the wave's run directory only through the absolute path of `{{shots}}`, and that a bare file name is resolved against the repository (L237, L249).
634
861
  `--dry-run` prints, under each step, `mcp : aucun` or `mcp : playwright (…)`. There is no per-project override in
635
862
  `cadence.yaml` yet.
636
863
 
637
864
  Without `start`, a read-only plan's `todo` lot is refused (start it with the project's tool); without
638
865
  `verdict`, the review verdict stays in the wave's state and you report it. Only the plan's files
639
- (`plan.path`, `plan.files`, the QA expectations file) are committed from those commands; anything else dirty stops the lot (`.cadence/` and `.playwright-mcp/` excepted, as above).
866
+ (`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).
867
+
868
+ ### Time limit of a write step (L298)
869
+
870
+ `timeouts.implement` (45 min by default) is the limit of a lot estimated at 0.25 day. The limit of a write step
871
+ (implementation, fix, spec, acceptance) is **proportioned to the lot**: multiplied by `estimate / 0.25` (never below 1: a
872
+ 0.5-day lot gets 90 min), by 1.5 more when the lot has a sheet (`screen:` or `spec: true`), and never above 4 times the
873
+ base. The estimate is kept in the lot's state; a state written before L298 has none and keeps the base. A review keeps
874
+ `timeouts.review`.
875
+
876
+ The limit is also **extended while the session commits**: at each deadline, if HEAD moved since the previous deadline (or since
877
+ the start), the session gets 15 more minutes, 8 times at most (2 h at the most after the proportioned limit); with no commit since,
878
+ it is cut. A cut write step is no longer a bare « failure »: the table gives the commits kept, the files left **uncommitted**
879
+ (with those that are tests, and the lines added) and says the work stays in the repository. A cut that left commits or files
880
+ hands the lot back to the lead; a cut that left nothing is a failure. For a lot with neighbouring repositories (`repos:`, L303),
881
+ the report counts the commits and the uncommitted files of each neighbour too, prefixed with its name (`../gitops: chart.yaml`).