@sylad/cadence 1.3.0 → 1.4.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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "cadence",
10
10
  "description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
11
- "version": "1.3.0",
11
+ "version": "1.4.0",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
3
  "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -40,7 +40,7 @@ the mockup (the `ux-reviewer` agent in mockup mode), see [The mockup review](#th
40
40
 
41
41
  ## What's new
42
42
 
43
- **1.3.0**: the spec step's threshold is narrowed to `screen: page` after the measure of 2026-10-12 (no row of the success table reached, about 570 k tokens per lot): `component` and `journey` lots follow the ordinary path again (L176, L209, L210); `cadence spec reseal … --porteur <lot>` re-seals the unstable sealed test of a delivered lot whose contract does not change (L212); the session cap shown and followed is the human's (`CADENCE_MAX_SESSIONS`, L208); `cadence orchestrate --waits` measures slot waits (L206); a third correction pass when the review converges (L205); `"e2e": true` acceptations stay out of the unit runner (L203); a dropped lot fully reverted no longer blocks `npm run service` (L207). **1.2.0**: the spec step reaches lots without a screen: a lot that declares `spec: true` (`raf add --spec`, `raf spec`) gets a back-end sheet of cases, rules, contracts and data (`cadence spec check`), reviewed before any code, then acceptance tests locked before the implementation, and `raf screen` lets a plan already written adopt the step; `orchestrate.accept` may be a list of runners (front and back), each run under its own root; `cadence spec reseal <lot> <test> --reason` is the lead's way out when a sealed test contradicts its unchanged sheet; `lead.ux: lead` lets the lead form the UX verdict itself; `cadence lead tour --depth n` looks for projects deeper in the folder; the budgets of a wave are recalibrated on the measured runs (750 k for an ordinary lot of 0.5 to 1 day, 1.3 M for a lot with a screen); the code in service on the cadence repository is a separate worktree, updated only by `npm run service`, so a wave can no longer put unreviewed code in service (L185–L189, L191, L192, L194–L198, L200, L201). **1.1.0**: the spec step of `cadence orchestrate`: a lot that declares `screen:` gets a machine-readable screen sheet (`cadence spec check|qa`), a mockup reviewed against it, acceptance tests committed red and locked before the implementation, and briefs that carry the sheet and rule → test → green to `implement`, `fix` and the code review; the news entry gains a *Hypothèses* section (L166, L169–L174, L177–L184). **1.0.0**: the first major release, a stable contract: the commands and options, the keys of `cadence.yaml`, the plan file and the names of the skills and agents follow semantic versioning, and the README's *Stability* section lists what is guaranteed and what is not (L164). **0.28.0**: `orchestrate.precheck: local` runs the « deliverable already present? » pre-check of `cadence orchestrate` on the local model (`claude-local`, Ollama), off the Anthropic quota, Sonnet staying the default and the fallback (L146); the `qa-reviewer` agent walks only the pages a lot touched, with a **Scope** line in its report (L123); the sub-tasks a review closes are also read in a lot's neighbouring repositories (L156) and a commit's sub-task list is read with the plan's left guard (L155); an unstable test is fixed (L90). **0.27.1**: `cadence orchestrate` no longer leaves a session's child processes alive: a dev server started in its own process group, or orphaned by a shell that exits (`nohup srv &`), is killed with the session through its process tree and its `CADENCE_SESSION` mark, a tmux server, `screen`, `gpg-agent` or `dirmngr` started on demand being spared; a session that exits with code 0 while a descendant holds its output open is no longer reported as « délai dépassé » (L83); the `ux-reviewer` and `qa-reviewer` agents carry the briefs' Playwright rule outside `cadence orchestrate` (L84). **0.27.0**: a compliant review of `cadence orchestrate` settles the open sub-tasks that commits of the lot cite (`feat(L3/t2): …`, comma lists included), closed in the same plan commit as the verdict or, on a read-only plan, returned as `[sous-tâche clore]` proposals, so `raf done` no longer refuses a lot over sub-tasks whose work is done; on a foreign-format plan the ids are read as the plan reads them (L82); the test suite stays green with two full suites running in parallel (L150). **0.26.0**: `cadence orchestrate` raises the floor of a lot's budget from 200 k to 450 k tokens, enough to pay a write pass, its review, one fix pass and the short review that follows, so a lot of 0.5 or 1 day is no longer handed back at its second or third pass; a `--continue` wave draws fewer lots as a result (L128). **0.25.0**: the project line of the plans' progress in the `cadence-hud` band names the lots in progress after their count, three at most then `…` (L153, `cadence-hud` 0.4.2); the progress follows the folder the session was launched from, so a `cd` into a sub-project no longer shrinks the band, and the session context keeps the `session.start` folder (L154). **0.24.0**: `cadence orchestrate --drop <project:lot>` and `--stop-after-current` steer a live wave from another terminal, `--resume --drop` removes a lot before replaying a stopped wave (L79); the final table of a wave says « livrable jusqu'à <sha> » for a ready lot stacked under a lot handed back, with `git push origin <sha>:main && cadence deliver --sha <sha>` (L76); a version commit (version fields, `CHANGELOG.md`, the README's « What's new ») no longer asks for a new code review (L72). **0.23.0**: the progress bar of each project in the `cadence-hud` band is coloured by the share of lots done, red below 33 %, orange below 66 %, green beyond (L152, `cadence-hud` 0.4.1); `cadence deliver` under a wave lock refuses only when a lot of the wave still works in that very repository, a linked worktree included (L127). **0.22.0**: `cadence orchestrate --continue` draws the next ready lot of the plan itself, in the declared priority, until the budget, the time window or a question stops it (L147); `cadence session context` prints the context of the session, which the `cadence-hud` band publishes, and the `lead` skill chains lots without the human while it stays under 60 % (L148); the band shows the progress of each project's plan, and `cadence lead tour --json` gains `progress` (L149, `cadence-hud` 0.4.0); the final review of a lot is always played, the lot's own budget bounding the writing passes only, with 65 k tokens reserved for it (L145); the cause « tests rouges après … » names the pass whose tests were red (L151). **0.21.0**: `cadence orchestrate --status` and `cadence lead tour` print the free session slots, which the `lead` skill reads (L141); `orchestrate.effort` sets the effort level of each pass of a wave (L137); `docs.sync` makes `raf check`, `lead tour` and the review brief report a document that does not follow the code (L143); `docs.articles` opens, after a green `cadence deliver`, a lot `Article <project> à rafraîchir` in the neighbouring repository (L144). **0.20.0**: `cadence-hud` no longer misleads once the work is over: `/hud cmd` lists the background commands it counts (id, tool, origin, age, name), any other argument answers `argument inconnu` instead of hiding the band, commands with no end seen for an hour or no record go to a grey `N cmd sans fin vue`, a finished subagent takes its commands with it, two races that left a `1 cmd` counted forever are closed, and a finished wave shows grey without its budget then disappears after 30 minutes (L142); the `qa-reviewer` and `ux-reviewer` agents are read-only for real, their frontmatter listing Read, Grep, Glob, Bash and the Playwright tools (L140). **0.19.1**: `cadence-hud`'s `N cmd` counter goes back down when a Monitor expires, when the background commands a subagent started end with it, and at session start or plugin reload (L135); its `tool.call` and `prompt.submit` guard hooks carry a `.catch` on registration, which clears the `claude plugin validate` warning (L136). **0.19.0**: `cadence orchestrate` lifts a repository's lock and `pre-push` guard as soon as every lot of the wave that touches it is finished, so `cadence deliver` works there while the wave continues elsewhere (L132); a review that leaves a repository dirty hands back that lot only, suspends the lots still queued in that repository and keeps the other repositories running, its verdict reported instead of lost (L133); `cadence session close` re-run without a `session start` keeps the window since the last opening (L131); `cadence-hud` draws the lot rows of a wave in aligned columns on every surface (L134). **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
43
+ **1.4.0**: `cadence spec reseal` accepts a sheet amended by commits of the lot, in progress or delivered, and records `ficheAmendee` (L217); the budget of a wave keeps 155 k for a fix pass plus a short review, the floor of an ordinary lot goes to 540 k and the spec allocation to 450 k, and a refused fix says `correction non jouée (budget)` in the final table (L218); a session that runs `git checkout` no longer stops the wave with a false « push détecté » (L214, L216); tests already red at the start sha no longer hand a lot back (L213, L215). **1.3.0**: the spec step's threshold is narrowed to `screen: page` after the measure of 2026-10-12 (no row of the success table reached, about 570 k tokens per lot): `component` and `journey` lots follow the ordinary path again (L176, L209, L210); `cadence spec reseal … --porteur <lot>` re-seals the unstable sealed test of a delivered lot whose contract does not change (L212); the session cap shown and followed is the human's (`CADENCE_MAX_SESSIONS`, L208); `cadence orchestrate --waits` measures slot waits (L206); a third correction pass when the review converges (L205); `"e2e": true` acceptations stay out of the unit runner (L203); a dropped lot fully reverted no longer blocks `npm run service` (L207). **1.2.0**: the spec step reaches lots without a screen: a lot that declares `spec: true` (`raf add --spec`, `raf spec`) gets a back-end sheet of cases, rules, contracts and data (`cadence spec check`), reviewed before any code, then acceptance tests locked before the implementation, and `raf screen` lets a plan already written adopt the step; `orchestrate.accept` may be a list of runners (front and back), each run under its own root; `cadence spec reseal <lot> <test> --reason` is the lead's way out when a sealed test contradicts its unchanged sheet; `lead.ux: lead` lets the lead form the UX verdict itself; `cadence lead tour --depth n` looks for projects deeper in the folder; the budgets of a wave are recalibrated on the measured runs (750 k for an ordinary lot of 0.5 to 1 day, 1.3 M for a lot with a screen); the code in service on the cadence repository is a separate worktree, updated only by `npm run service`, so a wave can no longer put unreviewed code in service (L185–L189, L191, L192, L194–L198, L200, L201). **1.1.0**: the spec step of `cadence orchestrate`: a lot that declares `screen:` gets a machine-readable screen sheet (`cadence spec check|qa`), a mockup reviewed against it, acceptance tests committed red and locked before the implementation, and briefs that carry the sheet and rule → test → green to `implement`, `fix` and the code review; the news entry gains a *Hypothèses* section (L166, L169–L174, L177–L184). **1.0.0**: the first major release, a stable contract: the commands and options, the keys of `cadence.yaml`, the plan file and the names of the skills and agents follow semantic versioning, and the README's *Stability* section lists what is guaranteed and what is not (L164). **0.28.0**: `orchestrate.precheck: local` runs the « deliverable already present? » pre-check of `cadence orchestrate` on the local model (`claude-local`, Ollama), off the Anthropic quota, Sonnet staying the default and the fallback (L146); the `qa-reviewer` agent walks only the pages a lot touched, with a **Scope** line in its report (L123); the sub-tasks a review closes are also read in a lot's neighbouring repositories (L156) and a commit's sub-task list is read with the plan's left guard (L155); an unstable test is fixed (L90). **0.27.1**: `cadence orchestrate` no longer leaves a session's child processes alive: a dev server started in its own process group, or orphaned by a shell that exits (`nohup srv &`), is killed with the session through its process tree and its `CADENCE_SESSION` mark, a tmux server, `screen`, `gpg-agent` or `dirmngr` started on demand being spared; a session that exits with code 0 while a descendant holds its output open is no longer reported as « délai dépassé » (L83); the `ux-reviewer` and `qa-reviewer` agents carry the briefs' Playwright rule outside `cadence orchestrate` (L84). **0.27.0**: a compliant review of `cadence orchestrate` settles the open sub-tasks that commits of the lot cite (`feat(L3/t2): …`, comma lists included), closed in the same plan commit as the verdict or, on a read-only plan, returned as `[sous-tâche clore]` proposals, so `raf done` no longer refuses a lot over sub-tasks whose work is done; on a foreign-format plan the ids are read as the plan reads them (L82); the test suite stays green with two full suites running in parallel (L150). **0.26.0**: `cadence orchestrate` raises the floor of a lot's budget from 200 k to 450 k tokens, enough to pay a write pass, its review, one fix pass and the short review that follows, so a lot of 0.5 or 1 day is no longer handed back at its second or third pass; a `--continue` wave draws fewer lots as a result (L128). **0.25.0**: the project line of the plans' progress in the `cadence-hud` band names the lots in progress after their count, three at most then `…` (L153, `cadence-hud` 0.4.2); the progress follows the folder the session was launched from, so a `cd` into a sub-project no longer shrinks the band, and the session context keeps the `session.start` folder (L154). **0.24.0**: `cadence orchestrate --drop <project:lot>` and `--stop-after-current` steer a live wave from another terminal, `--resume --drop` removes a lot before replaying a stopped wave (L79); the final table of a wave says « livrable jusqu'à <sha> » for a ready lot stacked under a lot handed back, with `git push origin <sha>:main && cadence deliver --sha <sha>` (L76); a version commit (version fields, `CHANGELOG.md`, the README's « What's new ») no longer asks for a new code review (L72). **0.23.0**: the progress bar of each project in the `cadence-hud` band is coloured by the share of lots done, red below 33 %, orange below 66 %, green beyond (L152, `cadence-hud` 0.4.1); `cadence deliver` under a wave lock refuses only when a lot of the wave still works in that very repository, a linked worktree included (L127). **0.22.0**: `cadence orchestrate --continue` draws the next ready lot of the plan itself, in the declared priority, until the budget, the time window or a question stops it (L147); `cadence session context` prints the context of the session, which the `cadence-hud` band publishes, and the `lead` skill chains lots without the human while it stays under 60 % (L148); the band shows the progress of each project's plan, and `cadence lead tour --json` gains `progress` (L149, `cadence-hud` 0.4.0); the final review of a lot is always played, the lot's own budget bounding the writing passes only, with 65 k tokens reserved for it (L145); the cause « tests rouges après … » names the pass whose tests were red (L151). **0.21.0**: `cadence orchestrate --status` and `cadence lead tour` print the free session slots, which the `lead` skill reads (L141); `orchestrate.effort` sets the effort level of each pass of a wave (L137); `docs.sync` makes `raf check`, `lead tour` and the review brief report a document that does not follow the code (L143); `docs.articles` opens, after a green `cadence deliver`, a lot `Article <project> à rafraîchir` in the neighbouring repository (L144). **0.20.0**: `cadence-hud` no longer misleads once the work is over: `/hud cmd` lists the background commands it counts (id, tool, origin, age, name), any other argument answers `argument inconnu` instead of hiding the band, commands with no end seen for an hour or no record go to a grey `N cmd sans fin vue`, a finished subagent takes its commands with it, two races that left a `1 cmd` counted forever are closed, and a finished wave shows grey without its budget then disappears after 30 minutes (L142); the `qa-reviewer` and `ux-reviewer` agents are read-only for real, their frontmatter listing Read, Grep, Glob, Bash and the Playwright tools (L140). **0.19.1**: `cadence-hud`'s `N cmd` counter goes back down when a Monitor expires, when the background commands a subagent started end with it, and at session start or plugin reload (L135); its `tool.call` and `prompt.submit` guard hooks carry a `.catch` on registration, which clears the `claude plugin validate` warning (L136). **0.19.0**: `cadence orchestrate` lifts a repository's lock and `pre-push` guard as soon as every lot of the wave that touches it is finished, so `cadence deliver` works there while the wave continues elsewhere (L132); a review that leaves a repository dirty hands back that lot only, suspends the lots still queued in that repository and keeps the other repositories running, its verdict reported instead of lost (L133); `cadence session close` re-run without a `session start` keeps the window since the last opening (L131); `cadence-hud` draws the lot rows of a wave in aligned columns on every surface (L134). **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
44
44
  stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
45
45
  (sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
46
46
  terminal cells and kept to ASCII; `publish.yml` on the v5 actions. **0.12.0**: the `cadence-hud` plugin (a status band above the Claude Code prompt),
@@ -680,7 +680,7 @@ The first baseline is in [docs/mesures/corrections-2026-10-10.md](docs/mesures/c
680
680
 
681
681
  `cadence spec check` prints the same summary line for each kind (`N cas, N règles (N hypothèses), N contrats, N données` for a back-end sheet) and keeps its exit codes: 0 valid, 1 violations, 2 usage.
682
682
 
683
- `cadence spec reseal <lot> <test> --reason texte` is the lead's way to correct one sealed acceptance test of the lot in progress that contradicts its (unchanged) sheet (of a delivered lot: add `--porteur <lot in progress>`), see [The accept step](#the-accept-step-red-acceptance-tests-and-their-lock).
683
+ `cadence spec reseal <lot> <test> --reason texte` is the lead's way to correct one sealed acceptance test of the lot in progress that contradicts its sheet (unchanged, or amended by commits of the lot, L217) (of a delivered lot: add `--porteur <lot in progress>`), see [The accept step](#the-accept-step-red-acceptance-tests-and-their-lock).
684
684
 
685
685
  `cadence spec qa <fiche.yaml>` derives from a valid sheet the section of `docs/qa/expectations.md` for its screen and prints it on the standard output
686
686
  (it never writes the file: you paste the section, and the lead commits it like the rest of the plan files). A sheet with violations prints them on the
@@ -946,7 +946,7 @@ command warns `ne lit pas les demandes de contrôle` on the error output.
946
946
  plan too); then, each time the lots in play are finished, the wave **draws the next ready lots of the plan** — up to the
947
947
  session cap — and plays them in the same wave (same budget, same state folder, journal line `continue : tire …`). A lot
948
948
  is drawn when it is `todo`, its `after` is lifted, it is not recurring, neither its title nor a note says « à décider »
949
- (« à décider avec … », « (à décider) »: a lot waiting for a human decision), it is not already in the wave, and its own budget (derived from its estimate; a lot with `screen: page` is worth 1.3 M whatever its estimate (`component` and `journey` have their ordinary budget), or its ordinary budget plus the 350 k spec-stage allocation if that is more, and a lot with `spec: true` gets that allocation on top of its ordinary budget)
949
+ (« à décider avec … », « (à décider) »: a lot waiting for a human decision), it is not already in the wave, and its own budget (derived from its estimate; a lot with `screen: page` is worth 1.3 M whatever its estimate (`component` and `journey` have their ordinary budget), or its ordinary budget plus the 450 k spec-stage allocation if that is more, and a lot with `spec: true` gets that allocation on top of its ordinary budget)
950
950
  fits in the budget left — otherwise the next one that fits is taken (a lot that does not fit in what is left of the current
951
951
  round is not dropped: it is tried again at the next round, once the budget allows it). A round takes at most **one lot per
952
952
  repository** while other repositories have ready lots — the pool plays the lots of one repository one after the other, so a
@@ -1013,6 +1013,8 @@ you with the findings. A small lot (`estimate` ≤ 0.5 or `quickwin`) gets one s
1013
1013
  usability (code only when the lot is not `visible`). Failing tests (reported red, or red when
1014
1014
  `orchestrate.test` is run) go straight to a correction.
1015
1015
 
1016
+ **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`.
1017
+
1016
1018
  **Review sized to the lot (L108)**: a lot whose `estimate` is ≤ `orchestrate.review.threshold` (0.25 day by default)
1017
1019
  is *light*: one single review (Sonnet by default, `code-reviewer` agent, same criteria), no minors pass — the minors are
1018
1020
  returned to the lead as notes (proposals). A blocking or major finding on a light lot still triggers a correction, and
@@ -1079,7 +1081,7 @@ the first implementation: `precheck → spec → maquette → accept → impleme
1079
1081
 
1080
1082
  A lot that changes a behaviour but has no screen (`raf add … --spec`, or `raf spec <id> on`, see [raf](#raf); L188, design: [docs/superpowers/specs/2026-10-11-etape-spec-back-design.md](docs/superpowers/specs/2026-10-11-etape-spec-back-design.md)) is specified **before** it is coded, on a sheet, like a screen:
1081
1083
  `precheck → spec → fiche → accept → implement → review` (`fiche` unless `orchestrate.fiche: false`; `accept` only when the project declares `orchestrate.accept`). A lot with neither `screen:` nor `spec: true` keeps its path. `--dry-run` lists
1082
- `spec (sonnet)`, `fiche (sonnet)` and `accept (sonnet)` with their command lines and briefs `templates/orchestrate/spec-back.md`, `fiche.md` and `accept-back.md`; the lot's budget gets a 350 k allocation on top of its estimate-derived budget (the worst case measured on a screen lot's steps: the back chain has not been measured yet).
1084
+ `spec (sonnet)`, `fiche (sonnet)` and `accept (sonnet)` with their command lines and briefs `templates/orchestrate/spec-back.md`, `fiche.md` and `accept-back.md`; the lot's budget gets a 450 k allocation on top of its estimate-derived budget (the back chain measured on L120: 388 k).
1083
1085
 
1084
1086
  - **The spec session**: Sonnet, the `spec-writer` agent, no Playwright. It reads the lot, the project's CLAUDE.md and the code and tests of the modules the lot touches (a rule already coded is a source, cited `file:line`), then writes only
1085
1087
  `<orchestrate.spec.dir>/<sujet>/fiche.yaml` in the format of [`templates/fiche-back.yaml`](templates/fiche-back.yaml) (`sujet`, `cas`, `regles`, `contrats`, `donnees`; no mockup, no capture, no state) and commits it with a message that cites the lot.
@@ -1092,7 +1094,7 @@ A lot that changes a behaviour but has no screen (`raf add … --spec`, or `raf
1092
1094
  Findings are ranked blocking / major / minor, each with its measurement. A blocking or major one sends the sheet back for a **second `spec` session** (its brief carries the findings), then the sheet is reviewed again; still not conforming, the lot is handed back with what remains (`fiche non conforme après 2 revues : …`).
1093
1095
  The `spec` session has **one budget of two sessions**, shared by the program's check and the reviews: if the check already used the second one, a non-conforming review hands the lot back at once (`fiche non conforme et la spec a déjà joué 2 sessions`), with no second review. A question to the lead (the session's own, or a costly hypothesis) and its answer open a new budget.
1094
1096
  Minor findings become notes of the lot (`Revue de la fiche — mineur : …`, one per finding, committed with the plan; on a read-only plan they become `[note à poser]` proposals), not lines of the final table. This verdict is **not** `raf review`: the review of the code keeps that role. `orchestrate.fiche: false` skips the step.
1095
- - **A lot with a screen and a behaviour**: it is a lot with `screen:` whose **sheet** has a back part (a `sujet` block next to `ecran`, same folder), never a `spec: true` beside `screen: page` (`raf spec <id> on` only says so on such a lot and changes nothing; on `component` or `journey`, which follow the ordinary path since L209, `spec: true` makes a back-end spec lot, ordinary budget plus 350 k). The program reads the sheet, not the plan: as soon as the sheet has a `sujet` block (the contracts), the `code-reviewer` also reviews it,
1097
+ - **A lot with a screen and a behaviour**: it is a lot with `screen:` whose **sheet** has a back part (a `sujet` block next to `ecran`, same folder), never a `spec: true` beside `screen: page` (`raf spec <id> on` only says so on such a lot and changes nothing; on `component` or `journey`, which follow the ordinary path since L209, `spec: true` makes a back-end spec lot, ordinary budget plus 450 k). The program reads the sheet, not the plan: as soon as the sheet has a `sujet` block (the contracts), the `code-reviewer` also reviews it,
1096
1098
  the chain is `spec → fiche → maquette → accept`, and the findings of the two reviews go together to a single second `spec` session. The brief of the spec of a lot with a screen offers the back part (and joins its template) without making it compulsory; a lot whose sheet has no `sujet` keeps its chain, without `fiche`. `--dry-run` cannot list `fiche` for a lot with a screen (the sheet does not exist yet).
1097
1099
  - **Acceptance and code**: `accept` is the screen's step, with a back brief (`accept-back.md`: cases call the contracts of `contrats[]` as close as possible, external sources simulated from `entree`; no `data-testid`). `implement`, `fix` and `review` receive the sheet (cases, rules, contracts) as for a screen, without mockup; the review also checks that each contract is held (output, exact codes and messages of the errors).
1098
1100
  - **Hypotheses**: a lot without a screen has no News entry. Once the sheet is final — after its review and the second `spec` session if there was one, just before `accept` or `implement`, and also for a sheet already present on a resume — the program writes **one note of the lot per hypothesis** (`Hypothèses (fiche <sujet>) — R3 : … ; si faux : …`, not duplicated on a resume) and commits it with the plan; on a plan kept by another tool the lines become proposals (`[note à poser] …`). The end-of-wave table lists them under each lot (`hypothèse de la fiche : …`) for the lead to relay.
@@ -1148,7 +1150,7 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
1148
1150
  - **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
1149
1151
  (`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
1150
1152
  file, deletes it, creates one outside the accept step, or modifies a locked test hands the lot back (`verrou des tests d'acceptation : …`) — 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 **the sheet of the screen changes in the commits of the same lot** (compared with the head before the lot's first session, working tree included, or in a commit that cites the lot, even made before the wave): the sheet is what evolves the rules, and tests and sheet change together. 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).
1151
- - **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 an unchanged 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 unchanged**), 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, 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 }` 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 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 (sheet unchanged since the red commit, 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.
1153
+ - **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 or the `--porteur`), 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` (`la fiche … a été modifiée depuis le commit rouge … par un commit qui ne cite pas <lot> : <sha>`), 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 or the carrier; 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.
1152
1154
  - **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 …`;
1153
1155
  `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.
1154
1156
  - **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. 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).
@@ -1342,16 +1344,17 @@ a time (above); `Agent`, `git push`, `cadence deliver`, `raf done|review|ux` are
1342
1344
  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
1343
1345
  from a session (`CADENCE_ORCHESTRATED` is in their environment; a repository that already has another
1344
1346
  `pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
1345
- upstream ref and `git ls-remote` are compared with the "before", and a read-only step that left only UNTRACKED files (screenshots written at the root by a script with a relative path) is not an incident: the files are moved to `.cadence/runs/<wave>/<project>--<lot>/stray/` (kept in their tree; for a neighbour repository, under `stray/<its relative path, `/` replaced by `_`>/`, e.g. `stray/.._gitops/`), a warning naming them goes to the lot's warnings and the wave log, the verdict is kept and the lot continues (L162; a move that is impossible, or an untracked file that vanished during the step, stays an incident); a review that changed `HEAD` or a tracked file (a commit, a modified file) is an incident of that lot only: the lot is handed back to the lead, the lots of the other repositories carry on while the lots still queued behind it in the same repository are suspended (resumable, nothing started on a dirty tree), and a verdict the review had already produced is reported in the outcome and the lot's warnings (never recorded in the plan). A push or a removed guard hook still stops the wave. `raf done|ux|review` and `cadence deliver` refuse when
1347
+ upstream ref and `git ls-remote` are compared with the "before" (a `git checkout <sha>` or `git switch` that only moves `HEAD` — detached `HEAD`, another branch, no upstream any more — is not a push: the upstream ref is compared only when it is the same branch on both sides, `git ls-remote` stays the judge; the write briefs forbid `git checkout` and `git switch` in the repository and point to a throwaway `git worktree add --detach <tmp> <sha>`; should a write session still end off its starting branch, a clean tree with nothing outside the branch gets the branch put back (`git checkout <branch>`, a warning in the log, the lot carries on from the right `HEAD`), anything else — including a put-back that fails (`index.lock`, a conflicting file), cause « HEAD déplacé — remise de la branche <b> impossible : <git message> » — hands the lot back with the cause « HEAD déplacé » and holds the queue of that repository, the other repositories carrying on; a push or a removed guard hook in ANY watched repository, project or neighbour, is checked first and wins over a moved `HEAD`, L214), and a read-only step that left only UNTRACKED files (screenshots written at the root by a script with a relative path) is not an incident: the files are moved to `.cadence/runs/<wave>/<project>--<lot>/stray/` (kept in their tree; for a neighbour repository, under `stray/<its relative path, `/` replaced by `_`>/`, e.g. `stray/.._gitops/`), a warning naming them goes to the lot's warnings and the wave log, the verdict is kept and the lot continues (L162; a move that is impossible, or an untracked file that vanished during the step, stays an incident); a review that changed `HEAD` or a tracked file (a commit, a modified file) is an incident of that lot only: the lot is handed back to the lead, the lots of the other repositories carry on while the lots still queued behind it in the same repository are suspended (resumable, nothing started on a dirty tree), and a verdict the review had already produced is reported in the outcome and the lot's warnings (never recorded in the plan). A push or a removed guard hook still stops the wave. `raf done|ux|review` and `cadence deliver` refuse when
1346
1348
  `CADENCE_ORCHESTRATED` is set.
1347
1349
 
1348
1350
  **Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
1349
- 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 450 k (floor: a write pass, its review, one fix pass and the short review after it; 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 350 k beyond that; a lot with `spec: true` gets its ordinary budget plus 350 k (no such lot measured yet); shown by
1351
+ 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
1350
1352
  `--dry-run` with its basis): it bounds the *writing* passes, never the reviews. A lot that spent it gets no further implementation or
1351
1353
  fix and is handed back to the lead, but its review (and UX review) is played first; a fix pass starts only if
1352
- the remaining budget can also pay the review that follows it (65 k tokens reserved: a fixed amount close to the 90th
1353
- percentile of the full reviews, 62.7 k over 187 reviews; the short review after the minors pass has a higher p90, 67.6 k
1354
- over 114; measured on 2026-10-09 in the wave journals `.cadence/runs`, `tokens.counted`, 67 waves from 10-04 to 10-09).
1354
+ the remaining budget can also pay the fix and the short review that follows it (155 k tokens reserved: the p90 of a fix
1355
+ pass, 65 k over 497 passes, plus the p90 of a short Opus review, 88 k over 272; measured on 2026-10-15 in the wave journals
1356
+ `.cadence/runs`). A lot refused that fix is handed back with the cause `correction non jouée (budget) :` first, so the lead
1357
+ takes the correction by hand (L218).
1355
1358
  Exception: when the tests are red after a write pass, the lot goes to a fix pass without a review; if that fix pass is
1356
1359
  refused for lack of budget the lot is handed back to the lead without a review, with its findings, and the cause names
1357
1360
  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
package/dist/cli.js CHANGED
@@ -810,7 +810,7 @@ function waveHolding(root) {
810
810
  }
811
811
  /**
812
812
  * `cadence spec reseal <lot> <test> --reason texte` (L200) : le chemin du lead quand un test d'acceptation scellé du lot EN COURS contredit la fiche.
813
- * Le test est corrigé dans l'arbre de travail (fiche inchangée) ; la commande refait l'empreinte de ce seul test, journalise la raison dans
813
+ * Le test est corrigé dans l'arbre de travail (fiche inchangée, ou amendée et commitée par des commits du lot, L217) ; la commande refait l'empreinte de ce seul test, journalise la raison dans
814
814
  * acceptation.json, commite test + acceptation (le nouveau commit rouge), et note la raison au lot. Le verrou garde tout le reste.
815
815
  */
816
816
  const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
@@ -850,7 +850,7 @@ function specReseal(args, reason, porteur, ctx, io) {
850
850
  const before = { json: readFileSync(join(repo, json), 'utf8'), test: existsSync(join(repo, test)) ? readFileSync(join(repo, test), 'utf8') : '' };
851
851
  let r;
852
852
  try {
853
- r = resealAcceptation(repo, json, test, reason, ctx.today, livre ? porteur : undefined);
853
+ r = resealAcceptation(repo, json, test, reason, ctx.today, livre ? porteur : undefined, plan.refs);
854
854
  }
855
855
  catch (e) {
856
856
  throw new RafError(e.message);
@@ -858,7 +858,7 @@ function specReseal(args, reason, porteur, ctx, io) {
858
858
  const norm = posix.normalize(test);
859
859
  try {
860
860
  git(repo, ['add', '--', json, norm]);
861
- git(repo, ['commit', '-q', '-m', `test(${cite}): acceptation rescellée${livre ? ` du lot livré ${lotId}` : ''}, ${norm} : ${reason.trim()}`, '--', json, norm]);
861
+ git(repo, ['commit', '-q', '-m', `test(${cite}): acceptation rescellée${livre ? ` du lot livré ${lotId}` : ''}, ${norm} : ${reason.trim()}${r.ficheAmendee ? ` (fiche amendée en ${r.ficheAmendee})` : ''}`, '--', json, norm]);
862
862
  }
863
863
  catch (e) {
864
864
  // Atomique : un commit refusé (hook pre-commit, ...) ne laisse ni empreinte refaite ni index à moitié prêt ; le test retrouve le texte du lead.
@@ -876,10 +876,10 @@ function specReseal(args, reason, porteur, ctx, io) {
876
876
  const sha = git(repo, ['rev-parse', '--short', 'HEAD']).trim();
877
877
  // Un plan tenu par un autre outil ne se modifie pas d'ici : la raison est déjà dans acceptation.json et dans le commit, la note reste à la main du lead.
878
878
  if (plan.readonly) {
879
- io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha} ; raison dans ${json} et dans le commit — note à ajouter au lot ${cite} avec l'outil du projet (plan en lecture seule)`);
879
+ io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha}${r.ficheAmendee ? `, fiche amendée en ${r.ficheAmendee}` : ''} ; raison dans ${json} et dans le commit — note à ajouter au lot ${cite} avec l'outil du projet (plan en lecture seule)`);
880
880
  return 0;
881
881
  }
882
- plan.note(cite, `test d'acceptation rescellé${livre ? ` (lot livré ${lotId}, ` : ' ('}${norm}, fiche inchangée) : ${reason.trim()}`, ctx.today);
882
+ plan.note(cite, `test d'acceptation rescellé${livre ? ` (lot livré ${lotId}, ` : ' ('}${norm}, ${r.ficheAmendee ? `fiche amendée en ${r.ficheAmendee}` : 'fiche inchangée'}) : ${reason.trim()}`, ctx.today);
883
883
  plan.save();
884
884
  // La note du lot rejoint le dépôt dans la foulée (un dépôt sale arrêterait la vague suivante) ; un plan hors du dépôt ou non suivi reste à la main du lead.
885
885
  const planRel = relative(repo, plan.path);
@@ -895,7 +895,7 @@ function specReseal(args, reason, porteur, ctx, io) {
895
895
  throw new RafError(`spec reseal : le rescellé a eu lieu (commit ${sha}) mais le commit de la note du lot a échoué — la note est écrite dans ${planRel}, à commiter à la main (un plan sale arrêterait la vague suivante) — ${(out.stderr || out.stdout || out.message).trim().split('\n').slice(-5).join(' | ')}`);
896
896
  }
897
897
  }
898
- io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha} ; raison dans ${json} et en note du lot ${cite}${tracked ? '' : ' (plan à commiter)'}`);
898
+ io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha}${r.ficheAmendee ? `, fiche amendée en ${r.ficheAmendee}` : ''} ; raison dans ${json} et en note du lot ${cite}${tracked ? '' : ' (plan à commiter)'}`);
899
899
  return 0;
900
900
  }
901
901
  /** `cadence spec check|qa <fiche>` (L170) : code 0 valide, 1 écarts, 2 usage ou fichier illisible. `qa` imprime la section expectations.md de l'écran. */
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { isAbsolute, join, posix } from 'node:path';
4
4
  import { parse } from 'yaml';
5
+ import { citedRefs } from '../link.js';
5
6
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
6
7
  function git(repo, ...args) {
7
8
  return execFileSync('git', args, { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 });
@@ -122,10 +123,12 @@ export function sealAcceptation(repo, path) {
122
123
  }
123
124
  /**
124
125
  * Rescelle UN test (L200, `cadence spec reseal`, le lead — jamais une session) : le test d'un lot en cours contredit sa fiche et le verrou total
125
- * ne laisse aucun chemin de correction. La fiche reste inchangée depuis le commit rouge, les autres fichiers listés aussi : seule l'empreinte
126
- * du test nommé est refaite, la raison et les deux empreintes vont dans `resceaux`. Le commit (test + acceptation) est à faire : il devient le commit rouge.
126
+ * ne laisse aucun chemin de correction. Seule l'empreinte du test nommé est refaite (les autres fichiers listés restent inchangés), la raison et les deux
127
+ * empreintes vont dans `resceaux`. Le commit (test + acceptation) est à faire : il devient le commit rouge.
128
+ * La fiche est celle du commit rouge, ou (L217) une fiche amendée par des commits qui citent tous le lot (`refsOf` : `plan.refs`, lu comme `citedRefs` ;
129
+ * lot livré : le lot ou son `porteur`) et commitée : l'entrée porte alors `ficheAmendee`, le sha court du dernier commit qui l'a touchée.
127
130
  */
128
- export function resealAcceptation(repo, path, test, reason, today, porteur) {
131
+ export function resealAcceptation(repo, path, test, reason, today, porteur, refsOf = () => []) {
129
132
  const file = join(repo, path);
130
133
  const a = parseAcceptation(readFileSync(file, 'utf8'));
131
134
  if (typeof a === 'string')
@@ -146,8 +149,20 @@ export function resealAcceptation(repo, path, test, reason, today, porteur) {
146
149
  throw new Error(`${path} : pas de commit rouge lisible`);
147
150
  if (!existsSync(join(repo, norm)))
148
151
  throw new Error(`${norm} : fichier absent (un test supprimé ne se rescelle pas)`);
149
- if (!gitOk(repo, 'diff', '--quiet', red, '--', a.fiche))
150
- throw new Error(`la fiche ${a.fiche} a changé depuis le commit rouge ${red.slice(0, 7)} : le rescellé ne vaut que fiche inchangée (une fiche qui évolue passe par le lot et sa revue)`);
152
+ // Une fiche modifiée dans l'arbre de travail ou l'index n'est pas celle d'un commit du lot : elle se commite d'abord.
153
+ if (git(repo, 'status', '--porcelain', '--', a.fiche).trim() !== '')
154
+ throw new Error(`la fiche ${a.fiche} a des modifications non commitées : la commiter (en citant ${a.lot}) avant le rescellé`);
155
+ let ficheAmendee;
156
+ if (!gitOk(repo, 'diff', '--quiet', red, '--', a.fiche)) {
157
+ if (!existsSync(join(repo, a.fiche)))
158
+ throw new Error(`la fiche ${a.fiche} a été supprimée ou renommée depuis le commit rouge ${red.slice(0, 7)} : le rescellé ne vaut que pour une fiche présente`);
159
+ const citant = new Set([a.lot, ...(porteur ? [porteur] : [])]);
160
+ const commits = git(repo, 'log', '--format=%H%x1f%s%x1f%b%x1e', `${red}..HEAD`, '--', a.fiche).split('\x1e').map((r) => r.replace(/^\n/, '')).filter(Boolean).map((r) => { const [sha = '', subject = '', body = ''] = r.split('\x1f'); return { sha, day: '', subject, body: body.trim() }; });
161
+ const fautif = commits.find((c) => !citedRefs(c, refsOf).some((r) => citant.has(r.lot)));
162
+ if (fautif)
163
+ throw new Error(`la fiche ${a.fiche} a été modifiée depuis le commit rouge ${red.slice(0, 7)} par un commit qui ne cite pas ${a.lot} : ${fautif.sha.slice(0, 7)}`);
164
+ ficheAmendee = commits[0]?.sha.slice(0, 7);
165
+ }
151
166
  const touches = fichiers.filter((f) => f !== norm && (!gitOk(repo, 'diff', '--quiet', red, '--', f) || !existsSync(join(repo, f)) || a.empreintes?.[f] !== git(repo, 'hash-object', '--', f).trim()));
152
167
  if (touches.length)
153
168
  throw new Error(`autre(s) test(s) listé(s) modifié(s) depuis le commit rouge : ${touches.join(', ')} (le rescellé ne couvre que ${norm}, le verrou garde le reste)`);
@@ -157,9 +172,9 @@ export function resealAcceptation(repo, path, test, reason, today, porteur) {
157
172
  const doc = JSON.parse(readFileSync(file, 'utf8'));
158
173
  doc.empreintes[norm] = apres;
159
174
  const journal = Array.isArray(doc.resceaux) ? doc.resceaux : [];
160
- doc.resceaux = [...journal, { test: norm, raison: reason.trim(), date: today, avant, apres, ...(porteur ? { porteur } : {}) }];
175
+ doc.resceaux = [...journal, { test: norm, raison: reason.trim(), date: today, avant, apres, ...(porteur ? { porteur } : {}), ...(ficheAmendee ? { ficheAmendee } : {}) }];
161
176
  writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
162
- return { avant, apres, lot: a.lot };
177
+ return { avant, apres, lot: a.lot, ...(ficheAmendee ? { ficheAmendee } : {}) };
163
178
  }
164
179
  /** Pose `vert: true` dans le fichier (le cycle, jamais une session) ; le commit est à faire. */
165
180
  export function setVert(repo, path) {
@@ -0,0 +1,66 @@
1
+ // Ligne de base des tests (L213) : un lot n'est pas rendu « tests rouges » pour des échecs déjà présents au sha de départ.
2
+ /* eslint-disable-next-line no-control-regex */
3
+ const ANSI = /\u001b\[[0-9;]*m/g;
4
+ /** Marqueurs d'échec des lanceurs courants : vitest/jest (FAIL, ●), pytest (FAILED, ERROR), go (--- FAIL:), TAP (not ok). */
5
+ const MARKERS = [/^\s*FAIL(?:ED)?\s+(.+)$/, /^\s*ERROR\s+(\S+::.+)$/, /^\s*--- FAIL:\s+(\S+)/, /^\s*not ok\s+\d*\s*-?\s*(.+)$/, /^\s*●\s+(.+)$/];
6
+ /** Identifiant pytest « chemin::test[param] » : coupe le message (« - assert … ») au premier « - » hors crochets. */
7
+ function pytestId(s) {
8
+ if (!s.includes('::'))
9
+ return s;
10
+ let depth = 0;
11
+ for (let i = 0; i < s.length; i++) {
12
+ if (s[i] === '[')
13
+ depth++;
14
+ else if (s[i] === ']' && depth > 0)
15
+ depth--;
16
+ else if (depth === 0 && /\s/.test(s[i]) && /^\s+-(?:\s|$)/.test(s.slice(i)))
17
+ return s.slice(0, i);
18
+ }
19
+ return s;
20
+ }
21
+ /**
22
+ * vitest, fichier de test qui ne se charge pas : « FAIL a.test.ts [ a.test.ts ] ». Seule une entrée de fichier porte le suffixe « [ chemin ] » (un test ou une suite contient toujours « > »).
23
+ * En espace de travail, le fichier est relatif à la racine du projet (précédé de « |projet| ») et les crochets à la racine du lancement : « FAIL |pa| src/a.test.ts [ packages/a/src/a.test.ts ] », donc pas de comparaison des deux.
24
+ * Le crochet doit finir par une extension : « FAIL\tgithub.com/x/y [build failed] » (go) n'est pas un fichier.
25
+ */
26
+ const FILE_LOAD_FAIL = /^\s*FAIL\s+(?:\|[^|]*\|\s+)?[^>]+?\s+\[\s*[^\]]*\.\w+\s*\]\s*$/;
27
+ const clean = (s) => pytestId(s)
28
+ .replace(/\s+-\s+(?:Assertion|\w*Error)\b.*$/, '') // pytest : « - AssertionError: … »
29
+ .replace(/\s*\(?\d+(?:\.\d+)?\s*m?s\)?\s*$/, '') // durée finale
30
+ .trim();
31
+ /**
32
+ * Identifiants des tests en échec lus dans la sortie d'un lanceur (triés, sans doublon). Vide quand rien n'est identifiable :
33
+ * l'appelant ne peut alors rien comparer et garde les tests pour rouges.
34
+ */
35
+ export function failedTests(output) {
36
+ const found = new Set();
37
+ for (const raw of output.replace(ANSI, '').split('\n')) {
38
+ if (FILE_LOAD_FAIL.test(raw))
39
+ continue; // échec sans nom (cf. hasUnnamedFailure), pas un test
40
+ for (const re of MARKERS) {
41
+ const m = re.exec(raw);
42
+ if (!m)
43
+ continue;
44
+ const name = clean(m[1]);
45
+ if (name && !/^(?:Test Files?|Tests?)\b/.test(name))
46
+ found.add(name);
47
+ break;
48
+ }
49
+ }
50
+ return [...found].sort();
51
+ }
52
+ /** Échecs d'après le lot qui n'étaient pas rouges au départ (`nouveaux`) et ceux qui l'étaient déjà (`existants`). */
53
+ export function compareBaseline(now, base) {
54
+ const was = new Set(base);
55
+ return { nouveaux: now.filter((t) => !was.has(t)), existants: now.filter((t) => was.has(t)) };
56
+ }
57
+ /**
58
+ * Un échec que la sortie ne rattache à aucun test : erreur non gérée (vitest « Unhandled Errors », promesse rejetée), fichier de spec qui ne se charge plus (jest « Test suite failed to run », vitest « FAIL a.test.ts [ a.test.ts ] » : le FAIL de fichier existait peut-être au départ), seuil de
59
+ * couverture non atteint. Comparer les seuls noms lus le masquerait derrière d'anciens échecs : l'appelant garde les tests pour rouges.
60
+ */
61
+ export function hasUnnamedFailure(output) {
62
+ const text = output.replace(ANSI, '');
63
+ if (text.split('\n').some((l) => FILE_LOAD_FAIL.test(l)))
64
+ return true;
65
+ return /Unhandled (?:Errors?|Rejections?)|Test suite failed to run|caught \d+ unhandled|coverage threshold|Coverage for \w+ \([\d.]+%\) does not meet/i.test(text);
66
+ }
@@ -35,8 +35,9 @@ export const DEFAULT_BUDGET = 2_000_000;
35
35
  * où il est passé, 250 lots, 11-10), rangé par estimate : jusqu'à 0,5 j p90 374 k (152 lots), de 0,5 à 1 j p90 732 k (88 lots, max
36
36
  * 1,09 M), au-delà d'un jour p90 1,14 M pour les 10 lots de 1,25 à 2 j (570 k par jour d'estimate), max 1,7 M (L188). Le plafond
37
37
  * de L78 (400 k par jour) rendait au lead, au deuxième tour, un lot de 1 j (L188 : 483 k puis 495 k).
38
+ * Plancher relevé de 450 k à 540 k (L218) avec la réserve de la passe fix (155 k) : 14 k + 282 k + 80 k + 155 k = 531 k arrondis, la marge avant refus du fix reste 385 k.
38
39
  */
39
- const LOT_BUDGET_FLOOR = 450_000;
40
+ const LOT_BUDGET_FLOOR = 540_000;
40
41
  /** Lot de plus de 0,5 j et d'au plus 1 j : p90 mesuré du total, 732 k arrondis à 750 k. */
41
42
  const LOT_BUDGET_ONE_DAY = 750_000;
42
43
  /** Tokens par jour d'estimate au-delà d'un jour : p90 1,14 M pour 2 j. */
@@ -49,9 +50,10 @@ const LOT_BUDGET_PER_DAY = 570_000;
49
50
  const SCREEN_LOT_BUDGET = 1_300_000;
50
51
  /**
51
52
  * Allocation d'un lot à fiche `spec: true` (L188/L191), en plus du budget dérivé de l'estimate : sessions spec, fiche et accept hors de
52
- * l'estimate d'écriture. Aucun lot `spec:` seul n'a encore été mesuré ; 350 k est le pire cas mesuré des étapes d'un lot à écran (ol L113 : 349 k).
53
+ * l'estimate d'écriture. Relevée de 350 k à 450 k (L218) : la chaîne avant implement du lot spec warhammer L120 a coûté 388 k (350 k : pire cas
54
+ * mesuré des étapes d'un lot à écran, ol L113 : 349 k) ; cadence L217 n'en a pris que 176 k.
53
55
  */
54
- export const SPEC_STAGE_ALLOCATION = 350_000;
56
+ export const SPEC_STAGE_ALLOCATION = 450_000;
55
57
  /** Base mesurée d'un budget, pour `--dry-run` : l'estimate (et l'étape spec) d'où il sort. */
56
58
  export function lotBudgetBasis(estimate, stage) {
57
59
  if (stage === 'screen' && lotBudget(estimate, stage) === SCREEN_LOT_BUDGET)
@@ -2,8 +2,9 @@ import { execFileSync, spawn } from 'node:child_process';
2
2
  import { parse } from 'yaml';
3
3
  import { toolBinOf, withoutLaunchVars } from './snapshot.js';
4
4
  import { randomUUID } from 'node:crypto';
5
- import { dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
6
- import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
6
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
7
8
  import { coveredTasks, lotCommits, lotWork } from '../audit.js';
8
9
  import { readDocsConfig } from '../config.js';
9
10
  import { docSyncBrief, docSyncGaps, filesOf } from '../docsync.js';
@@ -12,6 +13,7 @@ import { citedRefs } from '../link.js';
12
13
  import { repoShas, repoWork } from '../repos.js';
13
14
  import { isOpen } from '../plan.js';
14
15
  import { isPlanOnly } from '../audit.js';
16
+ import { compareBaseline, failedTests, hasUnnamedFailure } from './baseline.js';
15
17
  import { APP_TIMEOUT_S, startApp } from './app.js';
16
18
  import { capturesText, newsText, objective, renderBrief, reposText, TEMPLATES_DIR } from './briefs.js';
17
19
  import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig } from './launch.js';
@@ -62,6 +64,12 @@ const short = (c) => `${c.sha.slice(0, 7)} ${c.subject}`;
62
64
  function git(repo, ...args) {
63
65
  return execFileSync('git', args, { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
64
66
  }
67
+ /** Le message d'une commande git qui a échoué : sa sortie d'erreur (sur une ligne), à défaut le message de l'exception. */
68
+ function gitFailure(e) {
69
+ const err = e.stderr;
70
+ const text = typeof err === 'string' && err.trim() ? err : e instanceof Error ? e.message : String(e);
71
+ return text.trim().replace(/\s*\n\s*/g, ' ');
72
+ }
65
73
  function save(c) {
66
74
  c.wave.store.writeLot(c.lot);
67
75
  }
@@ -99,11 +107,136 @@ async function commitPlan(c, message) {
99
107
  git(repo, 'commit', '-q', '-m', message, '--', ...dirty);
100
108
  return null;
101
109
  }
110
+ /**
111
+ * Sha d'où le lot est parti : le parent de son plus ancien commit de travail (un lot relancé, ou déjà en partie commité, a ses propres
112
+ * commits avant la vague), à défaut le HEAD d'avant la première session de la vague.
113
+ */
114
+ const startSha = (c) => lotStartHead(c.lot.repo, lotWork(c.loadPlan(), c.lot.repo, c.lot.lot).map((k) => k.sha)) ?? c.lot.steps.find((s) => s.headBefore)?.headBefore;
115
+ /** Dossiers ignorés de sortie de build : jamais liés dans le worktree jetable, une commande de tests qui y écrit toucherait le build du lot. */
116
+ const OUTPUT_DIRS = new Set(['dist', 'build', 'out', 'coverage', 'target', '.next', '.nuxt', '.output', '.angular', '.turbo', '.svelte-kit', '.parcel-cache']);
117
+ /**
118
+ * Un `node_modules` dans le worktree de base (L213) : un vrai dossier dont chaque entrée est un lien vers celle du dépôt vivant, SAUF les liens
119
+ * de workspace (npm, pnpm, yarn : `node_modules/@x/shared -> ../../packages/shared`) qui pointent vers du code du dépôt hors de tout
120
+ * `node_modules`. Lier le dossier d'un bloc les ferait résoudre dans le dépôt vivant, donc exécuter le code du lot au lieu de celui du sha de
121
+ * départ ; ils sont refaits vers la copie du worktree (absente au sha de départ : pas de lien). Les dossiers de portée `@x` sont parcourus,
122
+ * pas les paquets. Ajoute chaque lien créé à `links`.
123
+ */
124
+ function mirrorNodeModules(repo, dir, name, links) {
125
+ const repoReal = realpathSync(repo);
126
+ const workspaceCopy = (live) => {
127
+ let real;
128
+ try {
129
+ real = realpathSync(live);
130
+ }
131
+ catch {
132
+ return null; // lien mort dans le dépôt vivant : rien à refaire
133
+ }
134
+ const rel = relative(repoReal, real);
135
+ if (!rel || rel.startsWith('..') || isAbsolute(rel) || rel.split(sep).includes('node_modules'))
136
+ return null;
137
+ return join(dir, rel);
138
+ };
139
+ const link = (target, at) => {
140
+ symlinkSync(target, at);
141
+ links.push(at);
142
+ };
143
+ const mirror = (liveDir, to, scope) => {
144
+ mkdirSync(to);
145
+ for (const e of readdirSync(liveDir, { withFileTypes: true })) {
146
+ const live = join(liveDir, e.name);
147
+ const at = join(to, e.name);
148
+ if (e.isSymbolicLink()) {
149
+ const copy = workspaceCopy(live);
150
+ if (copy) {
151
+ if (existsSync(copy))
152
+ link(copy, at);
153
+ continue;
154
+ }
155
+ }
156
+ else if (!scope && e.isDirectory() && e.name.startsWith('@')) {
157
+ mirror(live, at, true);
158
+ continue;
159
+ }
160
+ link(live, at);
161
+ }
162
+ };
163
+ mirror(join(repo, name), join(dir, name), false);
164
+ }
165
+ /**
166
+ * Échecs des tests au sha de départ du lot (L213) : la commande est rejouée dans un worktree jetable de ce sha, hors du dépôt,
167
+ * avec des liens vers les dossiers ignorés de tout niveau (node_modules, .venv… ; `dist` et les autres sorties de build exclus, une commande
168
+ * de tests qui y écrit toucherait le build du lot) pour que les dépendances s'y trouvent. Les `node_modules` sont recréés entrée par entrée
169
+ * pour que les paquets de workspace résolvent dans le worktree (`mirrorNodeModules`). Null quand le worktree ne peut être monté :
170
+ * l'appelant garde alors les tests pour rouges.
171
+ */
172
+ async function baselineFailures(c, command) {
173
+ const l = c.lot;
174
+ const sha = startSha(c);
175
+ if (!sha)
176
+ return null;
177
+ if (l.baseline?.verified && l.baseline.sha === sha && l.baseline.command === command)
178
+ return l.baseline.failures;
179
+ const dir = mkdtempSync(join(tmpdir(), 'cadence-baseline-'));
180
+ const links = [];
181
+ try {
182
+ git(l.repo, 'worktree', 'add', '--detach', '-f', dir, sha);
183
+ // Tous niveaux : en monorepo les dépendances vivent dans packages/*/node_modules. `--directory` ne descend pas dans un dossier ignoré, donc
184
+ // chaque entrée est le dossier ou fichier ignoré le plus haut ; son parent doit exister au sha de départ (sinon rien où le relier).
185
+ const ignored = git(l.repo, 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory').split('\n').map((f) => f.replace(/\/$/, '')).filter((f) => f && !f.startsWith('.cadence') && !f.split('/').some((p) => OUTPUT_DIRS.has(p)));
186
+ for (const name of ignored) {
187
+ const to = join(dir, name);
188
+ if (existsSync(to) || !existsSync(dirname(to)))
189
+ continue;
190
+ if (basename(name) === 'node_modules' && lstatSync(join(l.repo, name)).isDirectory()) {
191
+ mirrorNodeModules(l.repo, dir, name, links);
192
+ continue;
193
+ }
194
+ symlinkSync(join(l.repo, name), to);
195
+ links.push(to);
196
+ }
197
+ const r = await sh(c, command, 30 * 60_000, {}, dir);
198
+ const failures = r.code === 0 ? [] : failedTests(r.output);
199
+ l.baseline = { sha, command, failures, verified: true };
200
+ save(c);
201
+ return failures;
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ finally {
207
+ for (const k of links)
208
+ if (lstatSync(k, { throwIfNoEntry: false })?.isSymbolicLink())
209
+ unlinkSync(k);
210
+ try {
211
+ git(l.repo, 'worktree', 'remove', '--force', dir);
212
+ }
213
+ catch {
214
+ rmSync(dir, { recursive: true, force: true });
215
+ }
216
+ try {
217
+ git(l.repo, 'worktree', 'prune');
218
+ }
219
+ catch {
220
+ // rien à élaguer
221
+ }
222
+ }
223
+ }
224
+ /** Des tests rouges comparés à la ligne de base (L213) : tous déjà rouges au départ, ou au moins un nouveau, ou rien de comparable. */
225
+ async function againstBaseline(c, command, output) {
226
+ const now = failedTests(output);
227
+ if (!now.length || hasUnnamedFailure(output))
228
+ return { kind: 'unknown' };
229
+ const base = await baselineFailures(c, command);
230
+ if (!base?.length)
231
+ return { kind: 'unknown' };
232
+ const { nouveaux, existants } = compareBaseline(now, base);
233
+ return nouveaux.length ? { kind: 'new', nouveaux, existants: existants.length } : { kind: 'existing', n: existants.length };
234
+ }
102
235
  const shEscape = (s) => s.replace(/\s+/g, ' ').replace(/[\\"$`]/g, '\\$&');
103
236
  /** Commande du projet, asynchrone : la boucle d'événements reste libre (délai de l'autre créneau, signaux). Son groupe est tué au délai et au signal. */
104
- function sh(c, command, timeoutMs = 30 * 60_000, env = {}) {
237
+ function sh(c, command, timeoutMs = 30 * 60_000, env = {}, cwd = c.lot.repo) {
105
238
  return new Promise((resolve) => {
106
- const child = spawn('sh', ['-c', command], { cwd: c.lot.repo, env: { ...withoutLaunchVars(process.env), ...env }, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
239
+ const child = spawn('sh', ['-c', command], { cwd, env: { ...withoutLaunchVars(process.env), ...env }, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
107
240
  const pid = child.pid;
108
241
  if (pid === undefined) {
109
242
  child.once('error', (e) => resolve({ code: 127, output: String(e.message) }));
@@ -205,11 +338,15 @@ function choixText(choix) {
205
338
  }
206
339
  /** Tests et build déjà lancés par le programme au HEAD relu : le relecteur ne les refait pas (L75). Vide si HEAD a bougé depuis ou si rien n'a tourné. */
207
340
  function checksText(c) {
341
+ const b = c.lot.baseline;
342
+ // Sans orchestrate.test aucun rejeu : la base annoncée par la session (non vérifiée) est la seule trace des échecs de départ ; elle s'ajoute aux runs du programme (le build), elle ne les remplace pas.
343
+ const declared = b && !b.verified && b.failures.length ? `The author reports ${b.failures.length} test(s) already red at the start sha${b.sha ? ` ${b.sha.slice(0, 7)}` : ''}, before this lot (not verified by the program):\n${b.failures.map((t) => `- ${t}`).join('\n')}\nThey are not defects of this lot, do not report them as findings; a failing test outside this list is still a finding.` : '';
208
344
  const k = c.lot.checks;
209
345
  if (!k?.runs.length || k.head !== git(c.lot.repo, 'rev-parse', 'HEAD'))
210
- return '';
211
- const runs = k.runs.map((r) => `- \`${r.command}\` (${r.label}): ${r.code === 0 ? 'green' : `RED, exit code ${r.code}`}`);
212
- return [`The program already ran these checks at HEAD ${k.head.slice(0, 7)}, just before this review:`, ...runs, 'Take these results as given: do not rebuild, do not rerun the whole suite, and do not wait on them. Run only a check they do not cover (a targeted test of a case you doubt), and list under "nonVerifie" what neither they nor you verified.'].join('\n');
346
+ return declared;
347
+ const runs = k.runs.map((r) => `- \`${r.command}\` (${r.label}): ${r.code === 0 ? 'green' : r.dejaRouges ? `RED, exit code ${r.code}, but every failing test (${r.dejaRouges}) was already red at the start sha${c.lot.baseline?.sha ? ` ${c.lot.baseline.sha.slice(0, 7)}` : ''}: not a defect of this lot, do not report them as findings` : `RED, exit code ${r.code}`}`);
348
+ const ran = [`The program already ran these checks at HEAD ${k.head.slice(0, 7)}, just before this review:`, ...runs, 'Take these results as given: do not rebuild, do not rerun the whole suite, and do not wait on them. Run only a check they do not cover (a targeted test of a case you doubt), and list under "nonVerifie" what neither they nor you verified.'].join('\n');
349
+ return declared ? `${ran}\n${declared}` : ran;
213
350
  }
214
351
  /** Consigne docs.sync du brief de revue (L143) : calculée sur les commits du lot au moment de la revue, pas écrite d'avance. */
215
352
  function docsText(c) {
@@ -370,18 +507,17 @@ export const lotSpent = (l) => l.steps.reduce((n, s) => n + (s.tokens?.counted ?
370
507
  /** Le lot a dépensé son propre budget (L78) : il ne compte plus que sur le lead, la vague garde le sien pour les autres. */
371
508
  const lotOver = (l) => l.budget !== undefined && lotSpent(l) >= l.budget;
372
509
  /**
373
- * Coût réservé à la revue qui suit une passe de correction (L145). Réserve fixe de 65 k tokens comptés, proche du p90 des
374
- * revues complètes. Mesure faite le 2026-10-09 sur les journaux `.cadence/runs/*` (67 dossiers de vague du 04 au 09-10,
375
- * `tokens.counted` des `N-review.json`, `N-review-small.json`, `N-ux.json` ; aucun journal `ux` conservé, 2 `review-small`
376
- * sans `tokens.counted` écartés ; p90 par interpolation linéaire) : 187 revues, médiane 43,4 k, p90 62,7 k, max 105,9 k ;
377
- * 114 revues courtes (review-small, après la passe des mineurs), médiane 37,4 k, p90 67,6 k, max 82,7 k ; ensemble des
378
- * 301 revues, médiane 42,3 k, p90 67,0 k, max 105,9 k. La revue courte a donc un p90 plus haut que la revue complète : 65 k
379
- * couvre 170 revues sur 187 (90,9 %) mais 99 revues courtes sur 114 (86,8 %). Une passe fix ne part que s'il reste de quoi payer cette revue.
510
+ * Coût réservé à une passe de correction et à la revue courte qui la suit (L145, étendu L218). Réserve fixe de 155 k tokens comptés :
511
+ * p90 d'une passe fix (65 k) plus p90 d'une revue courte Opus (88 k), arrondi. Mesure faite le 2026-10-15 sur les journaux
512
+ * `.cadence/runs` (523 fichiers de lot) : 497 passes fix Sonnet, médiane 45 k, p90 65 k, max 124 k ; 272 revues courtes Opus,
513
+ * médiane 45 k, p90 88 k ; la somme des p90 majore le p90 de la somme, elle n'a pas été recalculée par lot. La réserve de L145 (65 k, revue
514
+ * seule) laissait un lot rendu avant la correction à 740 k / 800 k (warhammer L120) ou 406 k / 450 k (maritime B26.1). Une passe fix ne part
515
+ * que s'il reste de quoi payer la correction ET sa revue ; sinon le lot est rendu avec « correction non jouée (budget) ».
380
516
  * Seule exception : des tests rouges après une écriture (work() appelle toFix() sans revue) — la passe fix suivante peut
381
517
  * alors être refusée et le lot revient au lead sans revue, avec ses constats.
382
518
  */
383
- export const REVIEW_RESERVE = 65_000;
384
- /** Une passe de correction laisse-t-elle de quoi rejouer la revue dans le budget du lot ? Sans budget de lot : toujours. */
519
+ export const REVIEW_RESERVE = 155_000;
520
+ /** Le budget du lot paie-t-il la passe de correction et la revue courte qui la suit ? Sans budget de lot : toujours. */
385
521
  const fixAffordable = (l) => l.budget === undefined || lotSpent(l) + REVIEW_RESERVE < l.budget;
386
522
  /** Cause d'une suspension demandée par `--stop-after-current` (L79). */
387
523
  export const STOP_REQUESTED = 'arrêt demandé (--stop-after-current)';
@@ -397,6 +533,8 @@ function halted(w, lot) {
397
533
  if (lot && w.dirtyRepos) {
398
534
  for (const r of lotRepoPaths(lot)) {
399
535
  const by = w.dirtyRepos.get(r);
536
+ if (by && w.movedRepos?.has(r))
537
+ return `dépôt ${r} laissé en HEAD déplacé par ${by} : à remettre sur sa branche par le lead avant de reprendre`;
400
538
  if (by)
401
539
  return `dépôt ${r} sali par la revue de ${by} : à nettoyer par le lead avant de reprendre`;
402
540
  }
@@ -554,7 +692,8 @@ async function session(c, kind, local = false) {
554
692
  step.headAfter = after.head ?? undefined;
555
693
  // Le dépôt du projet d'abord, puis chaque voisin : mêmes contrôles, l'incident nomme le dépôt voisin.
556
694
  const watched = [{ where: '', path: l.repo, before, after }, ...others.map((o) => ({ where: ` dans ${o.repo.rel}`, path: o.repo.path, before: o.before, after: o.after }))];
557
- for (const { where, path, before: b, after: a } of watched) {
695
+ // Les incidents d'abord, dans TOUS les dépôts surveillés (projet et voisins) : un push ou un hook de garde supprimé où que ce soit arrête la vague, avant tout traitement d'un HEAD déplacé ou d'un lot rendu (L214).
696
+ for (const { where, before: b, after: a } of watched) {
558
697
  if (pushed(b, a)) {
559
698
  step.status = 'failed';
560
699
  step.cause = `push détecté${where}`;
@@ -569,6 +708,43 @@ async function session(c, kind, local = false) {
569
708
  w.saveWave();
570
709
  return stop(c, 'failed', `incident : ${w.incident}`);
571
710
  }
711
+ }
712
+ for (const { where, path, before: b, after: a } of watched) {
713
+ // Session d'écriture finie sur une autre branche ou en HEAD détaché (L214) : pas un push, mais les contrôles et le lot suivant partiraient d'un HEAD qui n'est plus la branche du lot.
714
+ if (write && b.branch !== a.branch) {
715
+ const was = `${a.branch ? `sur la branche ${a.branch}` : `en HEAD détaché (${(a.head ?? '').slice(0, 7)})`} au lieu de ${b.branch ?? 'HEAD détaché'}`;
716
+ const why = b.branch ? headStranded(path, b, a) : 'le dépôt n\'était pas sur une branche au départ';
717
+ // La remise peut échouer (index.lock, fichier en conflit) : le lot est rendu, l'orchestrateur ne s'arrête pas (L214).
718
+ let failed = null;
719
+ if (b.branch && why === null) {
720
+ try {
721
+ git(path, 'checkout', '-q', b.branch);
722
+ }
723
+ catch (e) {
724
+ failed = `HEAD déplacé — remise de la branche ${b.branch} impossible : ${gitFailure(e)}`;
725
+ }
726
+ }
727
+ if (b.branch && why === null && failed === null) {
728
+ Object.assign(a, await snapshot(path));
729
+ if (path === l.repo)
730
+ step.headAfter = a.head ?? undefined;
731
+ const text = `${kind} : la session a fini${where} ${was} (arbre propre, rien hors de la branche) — branche ${b.branch} remise`;
732
+ l.warnings.push(text);
733
+ w.log(`${lotKey(l.project, l.lot)} · ${text}`);
734
+ }
735
+ else if (!b.branch) {
736
+ const text = `${kind} : la session a fini${where} ${was} — ${why}`;
737
+ l.warnings.push(text);
738
+ w.log(`${lotKey(l.project, l.lot)} · ${text}`);
739
+ }
740
+ else {
741
+ step.status = 'failed';
742
+ step.cause = failed ? `${failed}${where}` : `HEAD déplacé${where}`;
743
+ (w.dirtyRepos ??= new Map()).set(path, lotKey(l.project, l.lot));
744
+ (w.movedRepos ??= new Set()).add(path);
745
+ return stop(c, 'handed-back', `incident : HEAD déplacé${where} pendant ${lotKey(l.project, l.lot)} (${kind}) : la session a fini ${was} et ${failed ?? why}`);
746
+ }
747
+ }
572
748
  // Étape en lecture seule qui n'a laissé que des fichiers NON SUIVIS (captures d'un script en chemin relatif, L162) : déplacés dans le dossier du lot, avertissement, le verdict est gardé.
573
749
  if (!write && b.head === a.head && b.tracked.join() === a.tracked.join() && b.untracked.join() !== a.untracked.join() && b.untracked.every((f) => a.untracked.includes(f))) {
574
750
  const fresh = a.untracked.filter((f) => !b.untracked.includes(f)).map(unquotePath);
@@ -593,6 +769,28 @@ async function session(c, kind, local = false) {
593
769
  save(c);
594
770
  return { step, report: res.structured, before, after, others };
595
771
  }
772
+ /** Pourquoi la branche de départ ne peut pas être remise sans risque après un HEAD déplacé (L214), null si elle le peut : arbre propre et rien hors de la branche. */
773
+ function headStranded(path, b, a) {
774
+ if (a.tracked.length || a.untracked.join() !== b.untracked.join())
775
+ return 'l\'arbre n\'est pas propre';
776
+ // Le HEAD final, et chaque position prise par HEAD pendant la session (reflog) : un commit fait en chemin puis quitté n'est plus que dans le reflog.
777
+ let seen = [];
778
+ try {
779
+ seen = git(path, 'reflog', '--format=%H', `-n${Math.max(0, a.reflog - b.reflog)}`, 'HEAD').split('\n').filter(Boolean);
780
+ }
781
+ catch {
782
+ // reflog illisible : le HEAD final seul juge
783
+ }
784
+ for (const sha of [a.head ?? 'HEAD', ...seen]) {
785
+ try {
786
+ git(path, 'merge-base', '--is-ancestor', sha, `refs/heads/${b.branch}`);
787
+ }
788
+ catch {
789
+ return `des commits ne sont pas dans la branche ${b.branch}`;
790
+ }
791
+ }
792
+ return null;
793
+ }
596
794
  /** Incident du LOT (L133) : la revue a laissé des traces (captures, commit), ni push ni garde supprimée — les lots des autres dépôts continuent. */
597
795
  function handBack(c, kind, report, step, where, path, b, a, left) {
598
796
  const l = c.lot;
@@ -679,13 +877,14 @@ function overBudget(c, reserve = false) {
679
877
  // Une réponse du lead encore en attente n'a pas été consommée : elle ne doit pas se perdre sans qu'on le dise.
680
878
  const answer = l.pendingAnswer ? ` ; la réponse du lead (« ${l.pendingAnswer} ») n'a pas été jouée` : '';
681
879
  const basis = hasSpecStage(l) ? 'estimate et étape spec' : "dérivé de l'estimate";
682
- const why = reserve ? `budget du lot : ${lotSpent(l)} / ${l.budget} tokens comptés (${basis}), il ne reste pas de quoi payer la revue qui suivrait la correction (${REVIEW_RESERVE} réservés)` : `budget du lot atteint (${lotSpent(l)} / ${l.budget} tokens comptés, ${basis})`;
880
+ const why = reserve ? `budget du lot : ${lotSpent(l)} / ${l.budget} tokens comptés (${basis}), il ne reste pas de quoi payer la correction et la revue courte qui la suivrait (${REVIEW_RESERVE} réservés)` : `budget du lot atteint (${lotSpent(l)} / ${l.budget} tokens comptés, ${basis})`;
683
881
  // Tests rouges après une écriture : la passe fix refusée aurait suivi, le lot revient sans revue (L145) — la cause le dit.
684
882
  const red = l.next === 'fix' && l.constats.some((k) => k.source === 'tests');
685
883
  // Passe d'écriture qui vient d'être jouée, nommée d'après le compteur : l.pass - 1 passes de correction l'ont précédée (minorPass reste vrai après la passe des mineurs).
686
884
  const played = l.pass - 1;
687
885
  const after = l.minorPass && played === (l.minorPassAt ?? 0) ? 'la passe des mineurs' : played > 0 ? `la passe fix ${played}` : "l'implémentation";
688
- const prefix = red ? `tests rouges après ${after}, correction suivante non abordable : ` : '';
886
+ const notPlayed = l.next === 'fix' ? 'correction non jouée (budget) : ' : '';
887
+ const prefix = notPlayed + (red ? `tests rouges après ${after}, correction suivante non abordable : ` : '');
689
888
  const unreviewed = red ? ' ; le lot revient sans revue, avec ses constats' : '';
690
889
  return stop(c, 'handed-back', `${prefix}${why} : étape « ${l.next ?? '?'} » non jouée${answer}${unreviewed}, à décider par le lead`);
691
890
  }
@@ -1371,8 +1570,28 @@ async function work(c, kind) {
1371
1570
  else if (made === 0)
1372
1571
  l.warnings.push(kind === 'fix' ? 'fix sans nouveau commit : revue lancée sur les commits du lot' : `${kind} sans nouveau commit : revue lancée sur les commits déjà faits du lot`);
1373
1572
  const red = [];
1374
- if (!rep.tests.vert)
1375
- red.push({ source: 'tests', gravite: 'bloquant', texte: `tests annoncés rouges par la session : ${rep.tests.commande} — ${rep.tests.resultat}` });
1573
+ // Tests annoncés rouges (L213) : avec orchestrate.test, le programme rejoue et compare à la ligne de base, la session n'est crue que si les tests y sont rouges autrement que par des échecs de départ ; sans, la liste des échecs de base de la session, non vérifiée.
1574
+ const claimed = { source: 'tests', gravite: 'bloquant', texte: `tests annoncés rouges par la session : ${rep.tests.commande} — ${rep.tests.resultat}` };
1575
+ let deferred = false;
1576
+ const declared = (rep.echecsDeBase ?? []).map((t) => t.trim()).filter(Boolean);
1577
+ const newFailures = rep.nouveauxEchecs?.map((t) => t.trim()).filter(Boolean);
1578
+ if (!c.config.test && declared.length) {
1579
+ l.baseline = { sha: startSha(c) ?? '', failures: declared, verified: false };
1580
+ l.warnings.push(`échecs déjà présents au sha de départ selon la session (${declared.length}) : non vérifié par le programme (orchestrate.test absent)`);
1581
+ }
1582
+ if (!rep.tests.vert) {
1583
+ if (c.config.test)
1584
+ deferred = true;
1585
+ // Sans orchestrate.test (L215) : champ structuré, pas de lecture de la prose. vert:false + echecsDeBase + nouveauxEchecs présent et vide → pas de correction (non vérifié, signalé à la revue) ; nouveauxEchecs non vide → correction.
1586
+ else if (declared.length && newFailures && !newFailures.length)
1587
+ l.warnings.push(`tests annoncés rouges par la session, aucun nouvel échec déclaré (nouveauxEchecs vide) : les échecs sont ceux de la base qu'elle déclare (${declared.length}) ; pas de correction, non vérifié par le programme (orchestrate.test absent)`);
1588
+ else if (newFailures?.length)
1589
+ red.push({ ...claimed, texte: `${claimed.texte} — nouveaux échecs déclarés par la session : ${newFailures.join(' ; ')}` });
1590
+ else
1591
+ red.push(claimed);
1592
+ }
1593
+ if (rep.tests.vert && !c.config.test && newFailures?.length)
1594
+ l.warnings.push(`rapport incohérent : tests annoncés verts mais nouveaux échecs déclarés (${newFailures.join(' ; ')}) ; pas de correction, non vérifié par le programme (orchestrate.test absent)`);
1376
1595
  if (!rep.build.vert)
1377
1596
  red.push({ source: 'tests', gravite: 'bloquant', texte: `build annoncé rouge par la session : ${rep.build.commande} — ${rep.build.resultat}` });
1378
1597
  const runs = [];
@@ -1386,13 +1605,29 @@ async function work(c, kind) {
1386
1605
  if (!command)
1387
1606
  continue;
1388
1607
  const r = await sh(c, command);
1389
- runs.push({ label, command, code: r.code });
1390
- if (r.code !== 0) {
1391
- red.push({ source: 'tests', gravite: 'bloquant', texte: `${command} en échec (code ${r.code}) :\n${r.output.trim().split('\n').slice(-40).join('\n')}` });
1608
+ const run = { label, command, code: r.code };
1609
+ runs.push(run);
1610
+ if (r.code === 0)
1611
+ continue;
1612
+ const vs = label === 'tests' ? await againstBaseline(c, command, r.output) : { kind: 'unknown' };
1613
+ if (vs.kind === 'existing') {
1614
+ run.dejaRouges = vs.n;
1615
+ const note = `${vs.n} test(s) déjà rouge(s) au sha de départ ${(l.baseline?.sha ?? '').slice(0, 7)}, pas un défaut du lot : listés à part, la revue a lieu`;
1616
+ if (!l.warnings.includes(note))
1617
+ l.warnings.push(note);
1618
+ continue;
1619
+ }
1620
+ if (vs.kind === 'new') {
1621
+ red.push({ source: 'tests', gravite: 'bloquant', texte: `${command} en échec (code ${r.code}) : ${vs.nouveaux.length} test(s) rouge(s) qui ne l'étaient pas au sha de départ :\n${vs.nouveaux.map((t) => `- ${t}`).join('\n')}${vs.existants ? `\n(${vs.existants} autre(s) test(s) déjà rouge(s) au sha de départ : à ne pas corriger)` : ''}` });
1392
1622
  break;
1393
1623
  }
1624
+ red.push({ source: 'tests', gravite: 'bloquant', texte: `${command} en échec (code ${r.code}) :\n${r.output.trim().split('\n').slice(-40).join('\n')}` });
1625
+ break;
1394
1626
  }
1395
1627
  }
1628
+ // La session annonçait des tests rouges et le programme ne les voit pas rouges au départ : l'annonce vaut.
1629
+ if (deferred && red.length === 0 && !runs.some((r) => r.label === 'tests' && r.dejaRouges))
1630
+ red.push(claimed);
1396
1631
  if (red.length) {
1397
1632
  toFix(c, red, 'tests rouges');
1398
1633
  return;
@@ -1519,7 +1754,7 @@ async function reviewStep(c, kind) {
1519
1754
  const noBudget = wanted && (w.budget.exhausted || !fixAffordable(l)) && !w.incident && !w.quota.hit;
1520
1755
  const minorPass = wanted && !noBudget;
1521
1756
  if (noBudget)
1522
- l.warnings.push(`${w.budget.exhausted ? 'budget atteint' : 'budget du lot trop juste pour payer la revue qui suivrait'} : la passe des mineurs n'a pas eu lieu, mineurs rendus en propositions`);
1757
+ l.warnings.push(`${w.budget.exhausted ? 'budget atteint' : 'budget du lot trop juste pour payer la correction et la revue courte qui la suivrait'} : la passe des mineurs n'a pas eu lieu, mineurs rendus en propositions`);
1523
1758
  // Les mineurs confiés à la passe ont pu être proposés par une revue non conforme antérieure : si la revue qui la suit est
1524
1759
  // conforme, ils sont traités et ne restent pas en propositions (ceux que cette revue signale encore sont ajoutés juste après).
1525
1760
  if (l.minorPass && !minorPass && summary.conforme && uxOk && l.minorLines?.length) {
@@ -250,10 +250,13 @@ export function guardPresent(repo) {
250
250
  return existsSync(file) && readFileSync(file, 'utf8').includes(MARK);
251
251
  }
252
252
  export async function snapshot(repo, opts = {}) {
253
- const [status, head, upstream, remote] = await Promise.all([
253
+ const [status, head, upstream, upstreamRef, branch, reflog, remote] = await Promise.all([
254
254
  out(repo, ['status', '--porcelain', '--untracked-files=all']),
255
255
  out(repo, ['rev-parse', '--verify', '-q', 'HEAD']),
256
256
  out(repo, ['rev-parse', '-q', '--verify', '@{u}']),
257
+ out(repo, ['rev-parse', '-q', '--symbolic-full-name', '@{u}']),
258
+ out(repo, ['symbolic-ref', '-q', '--short', 'HEAD']),
259
+ out(repo, ['reflog', '--format=%H', 'HEAD']),
257
260
  opts.remote === false ? Promise.resolve(null) : out(repo, ['ls-remote', '--heads']),
258
261
  ]);
259
262
  const lines = (status ?? '').split('\n').filter(Boolean);
@@ -264,13 +267,20 @@ export async function snapshot(repo, opts = {}) {
264
267
  tracked: lines.filter((l) => !l.startsWith('??') && keep(l)),
265
268
  untracked: lines.filter((l) => l.startsWith('??') && keep(l)).map((l) => l.slice(3)),
266
269
  upstream,
270
+ upstreamRef: upstreamRef?.trim() || null,
271
+ branch: branch?.trim() || null,
272
+ reflog: (reflog ?? '').split('\n').filter(Boolean).length,
267
273
  guard: guardPresent(repo),
268
274
  remote,
269
275
  };
270
276
  }
271
- /** Écart de la référence amont ou du dépôt distant depuis `before` : un push est parti. */
277
+ /**
278
+ * Écart de la référence amont ou du dépôt distant depuis `before` : un push est parti.
279
+ * La référence amont n'est comparée que si c'est la même branche amont des deux côtés : un `git checkout <sha>` (HEAD
280
+ * détaché, plus d'amont) ou un changement de branche déplace le HEAD, pas origin (L214) ; le dépôt distant reste juge.
281
+ */
272
282
  export function pushed(before, after) {
273
- if (before.upstream !== after.upstream)
283
+ if (before.upstreamRef !== null && before.upstreamRef === after.upstreamRef && before.upstream !== after.upstream)
274
284
  return true;
275
285
  return before.remote !== null && after.remote !== null && before.remote !== after.remote;
276
286
  }
@@ -12,6 +12,8 @@ export const WORK_SCHEMA = {
12
12
  nonVerifie: strings,
13
13
  questions: strings,
14
14
  choix: strings,
15
+ echecsDeBase: strings,
16
+ nouveauxEchecs: strings,
15
17
  resume: str,
16
18
  },
17
19
  required: ['commits', 'tests', 'build', 'nonVerifie', 'questions', 'resume'],
@@ -93,6 +93,8 @@ function deliverable(lots, ancestor, fromGit) {
93
93
  }
94
94
  return out;
95
95
  }
96
+ /** Tests déjà rouges au départ listés un par un dans le tableau ; au-delà, un compte. */
97
+ const BASELINE_LISTED = 30;
96
98
  /** Le tableau de fin de vague (ou de `--status`), une ligne par lot puis le total et les questions. */
97
99
  export function renderTable(wave, lots, deps = {}) {
98
100
  const rows = lots.map((l) => [`${l.project}:${l.lot}`, LABEL[l.status], String(l.pass), review(l), ux(l), String(commits(l)), k(counted(l)), `${minutes(l)} min`]);
@@ -121,6 +123,15 @@ export function renderTable(wave, lots, deps = {}) {
121
123
  out.push(`${l.project}:${l.lot} — captures Playwright : ${l.uxCaptures}`);
122
124
  for (const w of l.warnings)
123
125
  out.push(`${l.project}:${l.lot} — ⚠ ${w}`);
126
+ // Ligne de base des tests (L213) : les échecs déjà présents au sha de départ, à part de ceux du lot.
127
+ const base = l.baseline;
128
+ if (base?.failures.length) {
129
+ const where = `${base.sha ? ` (${base.sha.slice(0, 7)}${base.verified ? '' : ', annoncé par la session, non vérifié'})` : ''}`;
130
+ for (const t of base.failures.slice(0, BASELINE_LISTED))
131
+ out.push(`${l.project}:${l.lot} — déjà rouge au départ${where} : ${t}`);
132
+ if (base.failures.length > BASELINE_LISTED)
133
+ out.push(`${l.project}:${l.lot} — déjà rouge au départ${where} : … et ${base.failures.length - BASELINE_LISTED} autre(s)`);
134
+ }
124
135
  for (const x of l.choix ?? [])
125
136
  out.push(`${l.project}:${l.lot} — choix fait : ${x}`);
126
137
  for (const h of l.hypotheses ?? [])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvain Ladoire",
@@ -103,7 +103,7 @@ suspended (budget or usage limit; `--resume --budget …` continues).
103
103
  What the orchestrator does **not** do, and stays with the lead: choose the lots (with the human), bring the
104
104
  questions back (`--resume --answer <project>:<lot> "…"`), look at the UX reviewer's captures and record
105
105
  `raf ux` (the orchestrator reports its verdict, it never records it; who decides it is the `lead.ux` setting, section 2c), re-verify (section 3, point 2),
106
- `raf done`, push and deliver. **A locked acceptance test that contradicts the sheet** (the lot is handed back with `verrou des tests d'acceptation`, or the session's question says its own test is wrong, e.g. a regex that rejects a sentence the sheet allows) has a path, and it is the lead's, never a session's: correct the test in the working tree, **the sheet unchanged**, then `cadence spec reseal <lot> <test> --reason "why it contradicts the sheet"` (outside any wave): the program redoes the fingerprint of that one test, writes the reason in `acceptation.json` (`resceaux`) and as a note of the lot, commits both, and the lock keeps everything else. For a **delivered** lot (`vert: true`) whose contract does not change but whose test is unstable, add `--porteur <the lot in progress that carries the fix>`: the commit and the note cite that lot, `vert` stays true (L212). It refuses a lot that is neither in progress nor delivered, a changed sheet and any other listed test touched, and during a wave of that repository. It is atomic (a refused commit restores `acceptation.json` and the test), with a plan kept read-only by another tool it leaves the plan untouched and says a note is to be added by hand, with an untracked plan or one outside the repository it writes the note but does not commit it (`plan à commiter`: do not add it again), and if the commit of the note is refused the reseal stays done and the error says to commit the plan by hand. Do it **between two sessions**, never inside one: a resealed commit that belongs to a session of the wave is not adopted (still a lock violation), and sessions are forbidden the command. Then continue the lot: `--resume --answer` when it waits on a `question`; a new wave `cadence orchestrate <project>:<lot>` when it was handed back (`--resume` skips finished lots). Minor findings and proposed sub-tasks come back in the table: adding them to
106
+ `raf done`, push and deliver. **A locked acceptance test that contradicts the sheet** (the lot is handed back with `verrou des tests d'acceptation`, or the session's question says its own test is wrong, e.g. a regex that rejects a sentence the sheet allows) has a path, and it is the lead's, never a session's: correct the test in the working tree (the sheet is left as it is; if it was amended, **commit it first, citing the lot**: an amended sheet whose every commit cites the lot is accepted, L217, never put it back to the red state), then `cadence spec reseal <lot> <test> --reason "why it contradicts the sheet"` (outside any wave): the program redoes the fingerprint of that one test, writes the reason in `acceptation.json` (`resceaux`) and as a note of the lot, commits both, and the lock keeps everything else. For a **delivered** lot (`vert: true`) whose contract does not change but whose test is unstable, add `--porteur <the lot in progress that carries the fix>`: the commit and the note cite that lot, `vert` stays true (L212). It refuses a lot that is neither in progress nor delivered, a sheet with uncommitted changes or touched by a commit that does not cite the lot, and any other listed test touched, and during a wave of that repository. It is atomic (a refused commit restores `acceptation.json` and the test), with a plan kept read-only by another tool it leaves the plan untouched and says a note is to be added by hand, with an untracked plan or one outside the repository it writes the note but does not commit it (`plan à commiter`: do not add it again), and if the commit of the note is refused the reseal stays done and the error says to commit the plan by hand. Do it **between two sessions**, never inside one: a resealed commit that belongs to a session of the wave is not adopted (still a lock violation), and sessions are forbidden the command. Then continue the lot: `--resume --answer` when it waits on a `question`; a new wave `cadence orchestrate <project>:<lot>` when it was handed back (`--resume` skips finished lots). Minor findings and proposed sub-tasks come back in the table: adding them to
107
107
  the plan is the lead's decision. A minor finding becomes a lot of its own only if it describes an observable bug (a wrong output, a crash, a measured regression); otherwise it stays a note of the originating lot, so that a review never feeds the next one. If an orchestrated wave is running in a repository, do not commit there
108
108
  and do not deliver it (`cadence deliver` refuses).
109
109
 
@@ -7,6 +7,7 @@ Rules:
7
7
  - Every test must FAIL for now, because the feature is missing (not because of a typo or a broken setup): the program runs the dedicated command and refuses tests that pass.
8
8
  - Do not touch the sheet, the plan or any application code. Do not push, deliver, run `raf done`, `raf ux` or `raf review`.
9
9
  - Commit with explicit paths (never `git add -A` or `commit -a`), ONE commit citing the lot (`test({{lot}}): acceptation R1–Rn (rouge)`) with the test files, the acceptation file and the runner configuration, and leave the working tree clean. The program seals the acceptation file afterwards (it adds the fingerprint of each test file): never write `empreintes` or `vert: true` yourself.
10
+ Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
10
11
  Decide minor interpretation questions yourself and list each one under "choix" with the alternative you did not take. Leave "questions" empty rather than writing a placeholder such as "no question".
11
12
  {{commits}}
12
13
  {{reponse}}
@@ -7,6 +7,7 @@ Rules:
7
7
  - Every test must FAIL for now, because the feature is missing (not because of a typo or a broken setup): the program runs the dedicated command and refuses tests that pass.
8
8
  - Do not touch the sheet, the mockup, the plan or any application code. Do not push, deliver, run `raf done`, `raf ux` or `raf review`.
9
9
  - Commit with explicit paths (never `git add -A` or `commit -a`), ONE commit citing the lot (`test({{lot}}): acceptation R1–Rn (rouge)`) with the test files, the acceptation file and the runner configuration, and leave the working tree clean. The program seals the acceptation file afterwards (it adds the fingerprint of each test file): never write `empreintes` or `vert: true` yourself.
10
+ Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
10
11
  Decide minor interpretation questions yourself and list each one under "choix" with the alternative you did not take. Leave "questions" empty rather than writing a placeholder such as "no question".
11
12
  {{commits}}
12
13
  {{reponse}}
@@ -12,6 +12,7 @@ Fix each minor finding that is right, and nothing else. Rules: test first; commi
12
12
  its tests pass, with explicit paths (never `git add -A` or `commit -a`), and a message that cites the
13
13
  lot (`fix({{lot}}): …`); run the project's full test suite and build before reporting; do not push,
14
14
  deliver, run `raf done`, `raf ux` or `raf review`. Read the project's CLAUDE.md first.
15
+ Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
15
16
  Do not stop to ask. A finding that is wrong, or not worth its change, is not fixed: list it under
16
17
  "choix" in your report with the reason — the reviewer re-reads it and the lead sees it. Leave
17
18
  "questions" empty. Making no commit at all is a valid outcome when every finding is rejected.
@@ -12,6 +12,7 @@ Rules: test first; commit each fix as soon as its tests pass, with explicit path
12
12
  `git add -A` or `commit -a`), and a message that cites the lot (`fix({{lot}}): …`); run the project's
13
13
  full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
14
14
  `raf review`. Read the project's CLAUDE.md first.
15
+ Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
15
16
  If a finding is wrong, say so under "choix" with the reason. Ask (under "questions") only when a fix needs a decision that passes the rule below.
16
17
  A question is legitimate only if it names what it would change: the scope of the lot, the architecture, or a costly rollback (data, production, public API);
17
18
  otherwise it is a choice: decide, write the alternative you did not take, go on. Minor choices to settle yourself:
@@ -19,6 +20,7 @@ a spacing, colour or label value when a measurement or a rule justifies it; the
19
20
  a News entry: only if a finding asks for it; a written rule of the project's CLAUDE.md (apply it); who launches the review or UX pass (never the session: the program and the lead do).
20
21
  "not my job to touch the plan" is not a question: say it in the report.
21
22
  Never stop to ask about how to organise your own work (split or squash a commit, order of the sub-parts, whether to continue, "I have no question"): decide, list it under "choix", go on. Leave "questions" empty rather than writing a placeholder such as "no question".
23
+ Tests already red before the lot: if the suite is red, check whether the same tests are red at the sha the lot started from (the parent of the oldest work commit that cites {{lot}}, plan-only commits left out, or the `HEAD` before this session if the lot has none: a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards). Failures already there are not a defect of this lot: do not try to fix them and do not report your tests as red for them; list their names under "echecsDeBase" in your report (one test per entry, as the runner prints it), list under "nouveauxEchecs" the tests that are red now and were NOT red at that sha (an empty list when there are none: with `vert: false`, a non-empty "echecsDeBase" and an empty "nouveauxEchecs" the lot goes to review without a correction pass; any entry in "nouveauxEchecs" with `vert: false` triggers one), and say under "nonVerifie" if you could not check. The program replays the suite itself and compares to the same baseline.
22
24
  A finding you contest (it describes a state older than the code, or you think it wrong) is not a question and not a reason to stop: do not ask the lead to arbitrate. Write it under "choix" with the evidence (the command, the line, the commit), fix what is right, go on; the next fresh review re-reads it.
23
25
  You are one short session of an orchestrated wave: do not launch subagents (the Agent tool is disabled).
24
26
  Give your final report as the structured output (commits, tests, build, what you could not verify, choices, questions).
@@ -7,6 +7,7 @@ Rules: test first; commit each sub-part as soon as its tests pass, with explicit
7
7
  `git add -A` or `commit -a`), and a message that cites the lot (`feat({{lot}}): …`); run the project's
8
8
  full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
9
9
  `raf review`.
10
+ Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
10
11
  Documentation: the project's README (and its usage documentation) describes the behaviour you deliver, updated in the same commits, like the CHANGELOG — a README that does not follow your change is a major finding of the review.
11
12
  Decide minor interpretation questions yourself (the wording of a message, a name, a default, the
12
13
  reading of an ambiguous line of the lot) and list each one under "choix" in your report, with the
@@ -19,6 +20,7 @@ a written rule of the project's CLAUDE.md (apply it); who launches the review or
19
20
  Never stop to ask about how to organise your own work (split or squash a commit, order of the sub-parts, whether to continue, "I have no question"): decide, list it under "choix", go on. Leave "questions" empty rather than writing a placeholder such as "no question".
20
21
  If the lot's commits already cover its open sub-tasks, do not ask whether to continue: make no commit and say so
21
22
  in your report (the review that follows re-reads those commits). If they do not cover them all, implement the rest.
23
+ Tests already red before the lot: if the suite is red, check whether the same tests are red at the sha the lot started from (the parent of the oldest work commit that cites {{lot}}, plan-only commits left out, or the `HEAD` before this session if the lot has none: a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards). Failures already there are not a defect of this lot: do not try to fix them and do not report your tests as red for them; list their names under "echecsDeBase" in your report (one test per entry, as the runner prints it), list under "nouveauxEchecs" the tests that are red now and were NOT red at that sha (an empty list when there are none: with `vert: false`, a non-empty "echecsDeBase" and an empty "nouveauxEchecs" the lot goes to review without a correction pass; any entry in "nouveauxEchecs" with `vert: false` triggers one), and say under "nonVerifie" if you could not check. The program replays the suite itself and compares to the same baseline.
22
24
  Report: commits (sha + subject), tests and build results with their numbers, what you could not
23
25
  verify, choices made, open questions.
24
26
 
@@ -16,6 +16,7 @@ Sheet rules:
16
16
  - If, after reading the lot and the code, you find that the lot changes NO behaviour (nothing a user, an API client or another service can observe: a lint, a rename, a move, a doc, a CI or cache setting without observable effect), write one line with the reason in `sousSeuil`, create no sheet, and finish: the lot then follows the usual path.
17
17
 
18
18
  Commit the sheet with explicit paths (never `git add -A` or `commit -a`), a message that cites the lot (`spec({{lot}}): …`), and leave the working tree clean. Do not implement the behaviour, do not write application code or tests, do not push, deliver, run `raf done`, `raf ux` or `raf review`.
19
+ Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
19
20
  Decide minor interpretation questions yourself and list each one under "choix" with the alternative you did not take. Leave "questions" empty rather than writing a placeholder such as "no question".
20
21
  {{commits}}
21
22
  {{reponse}}
@@ -17,6 +17,7 @@ Sheet rules:
17
17
  - If, after reading the lot and the code, you find that the lot changes NO screen (no new page), write one line with the reason in `sousSeuil`, create no mockup and no sheet, and finish: the lot then follows the usual path.
18
18
 
19
19
  Commit the spec with explicit paths (never `git add -A` or `commit -a`), a message that cites the lot (`spec({{lot}}): …`), the sheet, the mockup and the captures together, and leave the working tree clean. Do not implement the screen, do not write application code or tests, do not push, deliver, run `raf done`, `raf ux` or `raf review`.
20
+ Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
20
21
  Decide minor interpretation questions yourself and list each one under "choix" with the alternative you did not take. Leave "questions" empty rather than writing a placeholder such as "no question".
21
22
  {{commits}}
22
23
  {{reponse}}