@sylad/cadence 1.2.0 → 1.3.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.2.0",
11
+ "version": "1.3.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.2.0",
4
+ "version": "1.3.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
@@ -24,7 +24,7 @@ Code prompt that shows the session's context, quota, cost and the orchestrate wa
24
24
 
25
25
  ### Spec step (design, first steps delivered)
26
26
 
27
- For a visible lot that adds a page, a component or changes a journey, `orchestrate` gets a step
27
+ For a visible lot that adds a page (`screen: page`; a component or a journey follows the ordinary path since L209), `orchestrate` gets a step
28
28
  before the code: a static HTML mockup in the project's own style (1440 and 390 px; empty, loading and
29
29
  error states) and a machine-readable screen sheet (fields, numbered rules `R1…` each marked *sourced*
30
30
  or *hypothesis*, actions → API calls). The `ux-reviewer` agent reads the mockup before any code; the
@@ -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.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.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),
@@ -102,10 +102,10 @@ raf gantt # docs/plan/gantt.html
102
102
  | Command | Effect |
103
103
  |---|---|
104
104
  | `raf init [--project name] [--prefix L] [--no-hook]` | create the plan, install the hook, add `.playwright-mcp/` to `.gitignore` |
105
- | `raf add "title" [--estimate d] [--quickwin] [--visible [--screen page\|component\|journey]] [--spec] [--public "title"] [--after L2,L4] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`; `--screen`: the screen a visible lot creates or changes — a new `page`, a new `component` or a modified `journey` — written as `screen:` right after `visible:`, shown by `raf show`; it requires `--visible`, and `raf check` reports a `screen:` without `visible:` or with another value as an error. It is what will trigger the spec step of `orchestrate`, never inferred from the title; `--spec`: the lot changes a behaviour (something a user, an API client or another service can observe), written as `spec: true` right after `screen:`, `visible:`, `quickwin:` or `estimate:` (whichever comes last), shown by `raf show`, refused on a sub-task. Also set by the lead at planning time, never inferred from the title) |
105
+ | `raf add "title" [--estimate d] [--quickwin] [--visible [--screen page\|component\|journey]] [--spec] [--public "title"] [--after L2,L4] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`; `--screen`: the screen a visible lot creates or changes — a new `page`, a new `component` or a modified `journey` — written as `screen:` right after `visible:`, shown by `raf show`; it requires `--visible`, and `raf check` reports a `screen:` without `visible:` or with another value as an error. Only `page` triggers the spec step of `orchestrate` (L209), never inferred from the title; `--spec`: the lot changes a behaviour (something a user, an API client or another service can observe), written as `spec: true` right after `screen:`, `visible:`, `quickwin:` or `estimate:` (whichever comes last), shown by `raf show`, refused on a sub-task. Also set by the lead at planning time, never inferred from the title) |
106
106
  | `raf add "title" --every <days>` · `raf did <id> ["text"]` | recurring lot: `every` (days) and `last` (last time done) fields; the lot stays `todo`, `raf now` and `session start` list it under « Récurrent » with « dû depuis N j » / « prochain dans N j »; `raf did` resets the count (spec: `docs/superpowers/specs/2026-10-04-L11-tache-recurrente.md`) |
107
- | `raf screen <id> page\|component\|journey\|none` | set, replace or remove `screen:` on an existing lot, to adopt the [spec step](#the-spec-step-of-a-lot-with-screen) on a plan already written; refused on a lot without `visible:` (like `raf add --screen`) and on a sub-task; `none` removes the key and leaves `visible:` alone |
108
- | `raf spec <id> on\|off` | set or remove `spec:` on an existing lot (a lot that changes a behaviour, for the back-end spec step of `cadence orchestrate`, see [The spec step of a lot with `spec: true`](#the-spec-step-of-a-lot-with-spec-true)); refused on a sub-task; `off` is refused on a lot that has `screen:` (the screen implies the spec: remove `screen:` first) and `on` there only says so and changes nothing. `raf spec` only takes `on`/`off`: `cadence spec check\|qa <fiche.yaml>` keeps its own subcommands. `raf check` reports a `spec:` that is neither `true` nor `false` (`spec : true ou false attendu`) |
107
+ | `raf screen <id> page\|component\|journey\|none` | set, replace or remove `screen:` on an existing lot, to adopt the [spec step](#the-spec-step-of-a-lot-with-screen) on a plan already written (only `page` adopts the [spec step](#the-spec-step-of-a-lot-with-screen); `component` and `journey` do not); refused on a lot without `visible:` (like `raf add --screen`) and on a sub-task; `none` removes the key and leaves `visible:` alone |
108
+ | `raf spec <id> on\|off` | set or remove `spec:` on an existing lot (a lot that changes a behaviour, for the back-end spec step of `cadence orchestrate`, see [The spec step of a lot with `spec: true`](#the-spec-step-of-a-lot-with-spec-true)); refused on a sub-task; `off` is refused on a lot that has `screen: page` (the page implies the spec: remove `screen:` first) and `on` there only says so and changes nothing; on `component` and `journey` (ordinary path since L209) both work and set or remove the back-end `spec:`. `raf spec` only takes `on`/`off`: `cadence spec check\|qa <fiche.yaml>` keeps its own subcommands. `raf check` reports a `spec:` that is neither `true` nor `false` (`spec : true ou false attendu`) |
109
109
  | `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
110
110
  | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`) |
111
111
  | `raf note <id> "text"` | dated note — keep decisions next to the work |
@@ -207,7 +207,7 @@ plan:
207
207
  fields: # raf field: key in the file (a list = first one present)
208
208
  title: titre
209
209
  public: titre_public # the lot's title for the public; read when it is text, else ignored
210
- screen: ecran # page, component or journey; only on a visible lot
210
+ screen: ecran # page, component or journey; only on a visible lot (only `page` enters the spec step)
211
211
  spec: fiche # true or false: the lot changes a behaviour
212
212
  status: etat
213
213
  estimate: effort
@@ -614,7 +614,7 @@ beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second
614
614
  | `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters |
615
615
  | `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
616
616
 
617
- When an orchestrated wave is live, one more line closes the tour (not part of `--json`): `vagues en cours : 1 · sessions en cours : 0 · créneaux libres : 1 sur 2` — the lead reads the free slots there instead of computing them (L141).
617
+ When an orchestrated wave is live, one more line closes the tour (not part of `--json`): `vagues en cours : 1 · sessions en cours : 0 · créneaux libres : 1 sur 2` — the lead reads the free slots there instead of computing them (L141). The `sur M` is the cap the human set (`CADENCE_MAX_SESSIONS`, 2 by default), not a fixed 2 (L208); an invalid value falls back to 2 in this display.
618
618
 
619
619
  `--json` prints the same content as an array of objects (`project`, `doing`, `drift`, `notes`,
620
620
  `next`, `repo`, and `error` when the project could not be read), plus `progress` — `{ done, doing, todo, added7 }`,
@@ -639,7 +639,7 @@ has to correct itself. From the folder a wave was launched from (default: the cu
639
639
 
640
640
  - the **lots with a correction**: of the lots that went through a successful write session, the share with at least one
641
641
  and at least two `fix` passes, per project and for visible versus non-visible lots (`61 %` and `29 %` of 263 lots in the 10-10 baseline);
642
- - the **passes by cause**, in total and per project. The cause of a pass is read from the findings of the review that
642
+ - the **passes by cause**, in total, per project and **per visibility** (visible lots apart from the others, with the share of `spec-floue`, `regle-inventee` and `test-manquant` among each one's passes — the reference an evolution aimed at screens is compared to is that of the visible lots, not « all lots »: 16 % of 45 passes against 31 % overall in the 10-10 baseline). The cause of a pass is read from the findings of the review that
643
643
  precedes it, among: unclear spec or missing criterion (`spec-floue`), invented business rule (`regle-inventee`),
644
644
  architecture drift (`derive-architecture`), data / SQL (`donnees-sql`), code bug (`bug-code`), missing test
645
645
  (`test-manquant`), false review finding (`faux-constat`). One cause per pass: the root of what forced the fix. A pass with no review finding to read (triggered by red tests right after the write session) is counted apart as `sans-constat` and never sent to the model.
@@ -652,7 +652,7 @@ new passes, and a pass the model could not answer stays out of the cache. `--sam
652
652
  evenly over the sorted keys, with their findings and the model's cause, to be read by hand; a `manual` field written
653
653
  beside a pass in the cache file (one of the seven causes, else the command refuses the file) overrides the model's cause in every table and gives the **agreement rate** of the model
654
654
  on the reviewed sample. `--until date` (ISO, e.g. `2026-10-10T18:31Z`) reads only the waves created up to then (`created` in the wave's `wave.json`) and, in them, only the steps ended by then (`ended`), so that neither later waves nor a wave still running at that time change a published baseline; a wave or a step with no readable date is left out. `--json` prints the measure as data. The command reads the journals and writes nothing but the cache.
655
- The first baseline is in [docs/mesures/corrections-2026-10-10.md](docs/mesures/corrections-2026-10-10.md); `test/corrections-mesure.test.ts` replays its command on the journals and checks the document's results word for word (skipped when the journals are not there).
655
+ The first baseline is in [docs/mesures/corrections-2026-10-10.md](docs/mesures/corrections-2026-10-10.md); `test/corrections-mesure.test.ts` replays its command on the journals and checks the document's results word for word (skipped when the journals are not there). The measure of the screen-sheet step against the visible lots of this baseline (L176: 8 lots with a sheet, verdict per row of the success table, token cost beside the passes avoided) is in [docs/mesures/etape-spec-2026-10-12.md](docs/mesures/etape-spec-2026-10-12.md); the target of its third row is ≤ 8 %, half the 16 % reference.
656
656
 
657
657
  ## spec
658
658
 
@@ -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, 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 (unchanged) sheet (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
@@ -915,6 +915,7 @@ cadence orchestrate … --budget 1.5M # 1500000, 1.5M, 800k; default 2M
915
915
  cadence orchestrate … --max-sessions 3 # sessions running at the same moment, all waves together; default 2
916
916
  cadence orchestrate … --dry-run # preconditions + the plan of the wave; nothing is started
917
917
  cadence orchestrate --continue [--until 18:00] [--priority cadence,maritime] # the wave draws the next ready lot itself (below)
918
+ cadence orchestrate --waits [--since 2026-10-12] # time spent waiting for a session slot, per day and per wave (below)
918
919
  cadence orchestrate --status [<wave>] # the live waves and the repositories they hold, then the table (default: the last wave of this folder)
919
920
  cadence orchestrate --status [<wave>] --watch [--interval 10] # the same, redrawn every 10 s (--interval in seconds); stops by itself when no wave is running (with an explicit `<wave>`: when that wave is no longer running)
920
921
  cadence orchestrate --resume [<wave>] [--budget 1M] [--answer ol-companion:L22 "reply"] [--drop ccc:L29]
@@ -945,7 +946,7 @@ command warns `ne lit pas les demandes de contrôle` on the error output.
945
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
946
947
  session cap — and plays them in the same wave (same budget, same state folder, journal line `continue : tire …`). A lot
947
948
  is drawn when it is `todo`, its `after` is lifted, it is not recurring, neither its title nor a note says « à décider »
948
- (« à 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:` is worth 1.3 M whatever its estimate, 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 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
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
950
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
951
952
  repository** while other repositories have ready lots — the pool plays the lots of one repository one after the other, so a
@@ -1006,7 +1007,8 @@ The Claude Code status band is the `cadence-hud` plugin.
1006
1007
  declared → **code review**, which always comes last (a UX fix changes code) → compliant (no blocking, no
1007
1008
  major finding) → `raf review` is recorded by the orchestrator with the sha the review read, then
1008
1009
  "ready to deliver". Not compliant → a correction in a new session, then a new review (the UX review of a
1009
- visible lot is replayed too, the code having changed), **two passes at most**, then the lot goes back to
1010
+ visible lot is replayed too, the code having changed), **two passes at most** (a third when the review converges: the count of blocking + major findings dropped
1011
+ since the previous review and exactly one is left; the lot budget still applies), then the lot goes back to
1010
1012
  you with the findings. A small lot (`estimate` ≤ 0.5 or `quickwin`) gets one single Opus pass for code and
1011
1013
  usability (code only when the lot is not `visible`). Failing tests (reported red, or red when
1012
1014
  `orchestrate.test` is run) go straight to a correction.
@@ -1014,7 +1016,7 @@ usability (code only when the lot is not `visible`). Failing tests (reported red
1014
1016
  **Review sized to the lot (L108)**: a lot whose `estimate` is ≤ `orchestrate.review.threshold` (0.25 day by default)
1015
1017
  is *light*: one single review (Sonnet by default, `code-reviewer` agent, same criteria), no minors pass — the minors are
1016
1018
  returned to the lead as notes (proposals). A blocking or major finding on a light lot still triggers a correction, and
1017
- the review that follows is the full one (Opus); the cap of two correction passes is unchanged. A bigger lot keeps the
1019
+ the review that follows is the full one (Opus); the cap of two correction passes is unchanged (except the convergence pass above). A bigger lot keeps the
1018
1020
  chain described above (Opus, two corrections, minors pass). `--dry-run` shows « revue légère » on a light lot.
1019
1021
 
1020
1022
  ```yaml
@@ -1051,10 +1053,11 @@ lot that already has commits is never pre-checked (resuming it is legitimate). `
1051
1053
 
1052
1054
  ### The spec step of a lot with `screen:`
1053
1055
 
1054
- A lot that declares `screen:` (`raf add … --visible --screen page|component|journey`, or `raf screen <id> page|component|journey` on a lot that already exists, see [raf](#raf)) gets one more session, `spec`, **after** the pre-check and **before**
1056
+ A lot that declares `screen:` (`raf add … --visible --screen page|component|journey`, or `raf screen <id> page|component|journey` on a lot that already exists, see [raf](#raf)) gets, when it is `page`, one more session, `spec`, **after** the pre-check and **before**
1055
1057
  the first implementation: `precheck → spec → maquette → accept → implement → ux → review` (`maquette` unless `orchestrate.maquette: false`, see [The mockup review](#the-mockup-review-before-any-code); `accept` only when the project declares `orchestrate.accept`, see below). A lot without `screen:` or `spec: true` follows the usual path, unchanged (a lot with `spec: true` and no screen has its own chain, see [The spec step of a lot with `spec: true`](#the-spec-step-of-a-lot-with-spec-true)). `--dry-run` lists the step
1056
1058
  (`spec (sonnet)`), its command line and its brief `templates/orchestrate/spec.md`.
1057
1059
 
1060
+ - **Threshold** (L209): only `screen: page` enters this chain. The measure of 2026-10-12 (no row of the success table reached, about 570 k tokens per lot, 4.5 correction passes per journey lot) narrowed it: `component` and `journey` stay valid values of `screen:` (no existing plan breaks, `raf screen` still sets them) but a lot that carries one follows the ordinary path and gets the ordinary budget, without the 1.3 M flat rate. A lot already in the chain (its sheet, or its `acceptation-<lot>.json`, exists) keeps its sheet and its lock. The `spec: true` step and the back part of a mixed lot do not change. The next measure covers 8 `page` lots.
1058
1061
  - **The session** (L171): Sonnet, the `spec-writer` agent (it writes only the spec folder, never application code), Playwright loaded like for `implement`. It reads the lot,
1059
1062
  the project's CLAUDE.md and its charter (CSS, neighbouring components, `docs/qa/expectations.md`, existing routes and API contracts), then writes in
1060
1063
  `<orchestrate.spec.dir>/<écran>/` (`docs/specs` by default): `maquette.html` (static HTML + CSS to the project's own style; one section `#etat-vide`, `#etat-chargement`,
@@ -1089,7 +1092,7 @@ A lot that changes a behaviour but has no screen (`raf add … --spec`, or `raf
1089
1092
  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 : …`).
1090
1093
  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.
1091
1094
  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.
1092
- - **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:` (`raf spec <id> on` has no effect on a lot with a screen). 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,
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,
1093
1096
  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).
1094
1097
  - **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).
1095
1098
  - **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.
@@ -1131,12 +1134,12 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
1131
1134
 
1132
1135
  **Moving from a string to a list.** The two forms are not interchangeable: a string keeps the repository as its root, a list (even of **one** element) derives each root from the command's leading `cd <folder> &&` and sets `CADENCE_ACCEPTATION_ROOT` to it. The brief of the step and the cycle decide by the same test (a YAML list or not), so they always agree. To switch: (1) write the entries as `cd <folder> && <runner>`; (2) make the runner's config resolve from its own root (its `include` and `rootDir` relative to `<folder>`, since the runner starts there and `acceptationOnly()` returns the paths relative to that root); (3) **copy the runner template again** into the project (`templates/acceptation-runner.mjs` reads `CADENCE_ACCEPTATION_ROOT`; an older copy lists the repository-relative paths and matches nothing from a folder). A project that has not migrated stays on its string, unchanged.
1133
1136
  - **Out of the ordinary test run and the CI while the lot is not green.** Red tests committed would fail `orchestrate.test` (after every `implement`/`fix`) and the project's CI (which `cadence deliver` waits for), for lots that are not theirs. So the
1134
- runner configuration shared by `orchestrate.test` and the CI excludes the files listed in every acceptation file (`acceptation-<lot>.json`) whose `vert` is `false`, and only the dedicated command runs them. `templates/acceptation-runner.mjs` is the helper the session copies
1137
+ runner configuration shared by `orchestrate.test` and the CI excludes the files listed in every acceptation file (`acceptation-<lot>.json`) whose `vert` is `false` — and, always, red or green, those of an acceptation with `"e2e": true` (a browser test needing the build, L203: `acceptationE2e()` lists them for the e2e suite run after the build) — and only the dedicated command runs them. `templates/acceptation-runner.mjs` is the helper the session copies
1135
1138
  into the project: `acceptationIgnored()` (Vitest `exclude`; `acceptationIgnored(root, dir, 'regex')` for Jest `testPathIgnorePatterns`) and `acceptationOnly()` (the `include` of the dedicated config, which reads the environment variable `CADENCE_ACCEPTATION`:
1136
1139
  the program sets it to the absolute path of the lot's acceptation file when it runs `orchestrate.accept`, and also replaces `{acceptation}` in the command with that path — quote it if the path may hold spaces). If the CI or the test command do not use that config
1137
1140
  (paths hard-coded), the session must not commit red tests and puts the question in its report.
1138
1141
  **Run by hand, outside a wave**, the dedicated command fails: `acceptationOnly()` requires `CADENCE_ACCEPTATION`, which only the program sets. Set it yourself, to the path of the lot's acceptation file:
1139
- `CADENCE_ACCEPTATION=<path of the acceptation file> <command>` (with a `{acceptation}` in the command, write the path there too). For a command of an `orchestrate.accept` **list**, also set its root, which `acceptationOnly()` uses to return the files relative to where the runner starts: `CADENCE_ACCEPTATION=<path of the acceptation file> CADENCE_ACCEPTATION_ROOT=<root of the command> <command>` (the folder of its leading `cd <folder> &&`; without the variable the root is the repository `.`, and the paths are relative to it, which no runner started in a folder will match). A single string needs no root. Once the lot's tests are green (`vert: true`), they join the ordinary runner and the CI, so the usual test command runs them without that variable.
1142
+ `CADENCE_ACCEPTATION=<path of the acceptation file> <command>` (with a `{acceptation}` in the command, write the path there too). For a command of an `orchestrate.accept` **list**, also set its root, which `acceptationOnly()` uses to return the files relative to where the runner starts: `CADENCE_ACCEPTATION=<path of the acceptation file> CADENCE_ACCEPTATION_ROOT=<root of the command> <command>` (the folder of its leading `cd <folder> &&`; without the variable the root is the repository `.`, and the paths are relative to it, which no runner started in a folder will match). A single string needs no root. Once the lot's tests are green (`vert: true`), they join the ordinary runner and the CI, so the usual test command runs them without that variable — **except an `"e2e": true` acceptation**, which never joins them: wire its `acceptationE2e()` files into the e2e suite run after the build (`npm run build && npx playwright test`). The dedicated `orchestrate.accept` command still runs those files for the red check and the return to green, so it must build and start what the browser needs.
1140
1143
  - **What the program checks** afterwards, with no model: `acceptation.json` is the lot's (`lot`, `fiche`, listed files committed, `vert: false`), every test name planned in the sheet appears in the listed files, **each listed test file is new** (absent from the head before the first `accept` session: tests written into an existing test file would leave the ordinary run and the CI while the lot is red, so the check refuses `existait avant la session accept`), the other lots' acceptation tests are untouched, the dedicated command
1141
1144
  **fails** (a test green before the code proves nothing), `orchestrate.test` stays **green** with the red tests committed (proof that they are excluded; without `orchestrate.test` nothing proves it, and the step passes with a lot warning only), and the tree is clean. Anything missing goes to **one second `accept` session**; still not valid, the lot is handed back
1142
1145
  (`accept non conforme après 2 passes : …`). A question in the report puts the lot in `question`, like the other steps. The check that each listed test *individually* fails is not made: only the dedicated command's exit code is (it fails if any of them fails).
@@ -1145,9 +1148,9 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
1145
1148
  - **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
1146
1149
  (`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
1147
1150
  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).
1148
- - **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), 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`), 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 not in progress. 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.
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.
1149
1152
  - **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 …`;
1150
- `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; 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.
1153
+ `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.
1151
1154
  - **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).
1152
1155
 
1153
1156
  ### The downstream briefs: sheet, rule → test → green, conformity, Hypothèses
@@ -1251,7 +1254,7 @@ npm run service -- <sha> # [--root dir] [--bin-dir dir] [--repo dir]
1251
1254
  ```
1252
1255
 
1253
1256
  The script refuses a sha that is not an ancestor of HEAD, and a sha whose commits (since the one in service; only the sha itself the first time)
1254
- cite a lot whose status is not `done` (`plan:` commits are ignored). It builds the new copy aside (`npm ci --ignore-scripts`, then `tsc`) and switches
1257
+ cite a lot whose status is not `done` (`plan:` commits are ignored); a lot `dropped` is let through when every one of its commits is undone by a `git revert` present under the sha (L207), and still blocks when one is not. It builds the new copy aside (`npm ci --ignore-scripts`, then `tsc`) and switches
1255
1258
  `service` and the two links only once the build succeeded (atomic renames): a failed install changes nothing. The previous copy is kept for a
1256
1259
  rollback, the older ones are removed only once they are more than 7 days old, counted from the day they left service (a wave's snapshot links its `node_modules` to the copy that launched it). The first run is also what installs the links in `~/.local/bin`. It prints the fingerprint of the copy
1257
1260
  (bin, dist, package.json).
@@ -1327,11 +1330,12 @@ every wave the same value: each wave counts ALL live sessions, whatever their sl
1327
1330
  count has reached ITS OWN cap, so with different caps the highest one can push the total past the lowest
1328
1331
  (which then waits). A session that finds no free slot **waits** (the wave is not refused; the journal says
1329
1332
  `en attente d'un créneau de session depuis …`, repeated every minute, then `créneau de session obtenu après …`).
1333
+ `cadence orchestrate --waits [--since YYYY-MM-DD]` adds those waits up from the journals of `.cadence/runs/` of the launch folder — per day (cumulated wait, number of waits, waves, quota stops — one per "→ suspended — quota atteint" line of the journals, counted on the day of that line, so a wave resumed and stopped again on another day counts once on each day, not once as a wave, tokens) and per wave that waited (the longest wait, a wait that never got its slot) — to decide the cap from figures; each wait is paired with its lot (the journal line carries `project:lot`), so two lots of one wave waiting at the same time (one per repository) are each counted once, and only a wait that never got its slot is reported as such; journals from before this carry no lot and are paired per wave. it reads only, and refuses any other action in the same call. The figures of 2026-10-12 and the cost of a third slot are in `docs/mesures/attente-creneaux-2026-10-12.md`.
1330
1334
  A slot or a registry entry is owned by a pid **and** its start time: a reused pid is a dead owner. Wave
1331
1335
  identifiers are reserved atomically (`-2`, `-3` suffix when two waves start in the same minute; an existing
1332
1336
  `--wave` is refused). The registry of live waves and the slots live under `~/.cadence/orchestrate/` (`CADENCE_HOME`
1333
1337
  to move it): `cadence orchestrate --status` lists, from any folder, the live waves, the repositories each
1334
- still holds (those not yet released), the cap of each wave, the slots in use and the **free slots** (`créneaux libres : N sur 2`: 2 minus, for each live wave, the smaller of its cap and the repositories it still holds — the figure the `lead` skill reads before starting a subagent). Without an identifier it then prints the table of the wave launched most recently from this folder (by launch time, not by alphabetical order of the identifier).
1338
+ still holds (those not yet released), the cap of each wave, the slots in use and the **free slots** (`créneaux libres : N sur M`: M — the cap set with `--max-sessions` or `CADENCE_MAX_SESSIONS`, 2 by default — minus, for each live wave, the smaller of its cap and the repositories it still holds — the figure the `lead` skill reads before starting a subagent; the lead never raises the cap itself, that is the human's decision, L208). Without an identifier it then prints the table of the wave launched most recently from this folder (by launch time, not by alphabetical order of the identifier).
1335
1339
 
1336
1340
  **Guards, imposed by the code**: a global cap of simultaneous sessions, one wave per repository at
1337
1341
  a time (above); `Agent`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions; a
@@ -1342,7 +1346,7 @@ upstream ref and `git ls-remote` are compared with the "before", and a read-only
1342
1346
  `CADENCE_ORCHESTRATED` is set.
1343
1347
 
1344
1348
  **Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
1345
- 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:` gets 1.3 M whatever its estimate (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
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
1346
1350
  `--dry-run` with its basis): it bounds the *writing* passes, never the reviews. A lot that spent it gets no further implementation or
1347
1351
  fix and is handed back to the lead, but its review (and UX review) is played first; a fix pass starts only if
1348
1352
  the remaining budget can also pay the review that follows it (65 k tokens reserved: a fixed amount close to the 90th
@@ -1354,7 +1358,7 @@ the red tests: the pass it names is the one whose tests were red, counted by its
1354
1358
  finish, the wave is *suspended* (exit code 3) and `--resume --budget …` continues. A session that returns
1355
1359
  nothing readable, times out (45 min for work, 25 for a review) or fails is not retried; the lot is handed
1356
1360
  back with the cause. Exit codes: 0 every lot ready · 1 at least one lot handed back (question, failure,
1357
- review still not compliant after two passes) · 2 refused before acting · 3 wave suspended.
1361
+ review still not compliant after two passes, or three when it converges) · 2 refused before acting · 3 wave suspended.
1358
1362
 
1359
1363
  **Stacked lots** (L76): lots of one repository commit on top of each other on `main`. When a ready lot sits under a lot that
1360
1364
  is not ready (handed back, failed…), the final table adds a line `<project>:<lot> — livrable jusqu'à <sha> … : git push origin <sha>:main && cadence deliver --sha <sha>`:
@@ -1534,12 +1538,13 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
1534
1538
  no model: see [lead tour](#lead-tour)) prints one line of facts per project, you choose the priorities, the lots are delegated with
1535
1539
  `cadence orchestrate` (fresh short sessions with a standard brief — test first,
1536
1540
  README and usage documentation updated with the change, commits citing the lot, no push — reviewed by the `code-reviewer` agent, see
1537
- [orchestrate](#orchestrate); a lot with a `screen:` first goes through the [spec, mockup review and accept steps](#the-spec-step-of-a-lot-with-screen)), re-verified by the lead, then delivered
1541
+ [orchestrate](#orchestrate); a lot with `screen: page` first goes through the [spec, mockup review and accept steps](#the-spec-step-of-a-lot-with-screen)), re-verified by the lead, then delivered
1538
1542
  one project at a time; a delivery that changes what a page shows or what it is
1539
1543
  served is then checked in the running app by the `qa-reviewer` agent, whose
1540
- blocking findings come back to you. Two
1541
- subagents at most, never two in the same repository. The whole flow is drawn in
1542
- [docs/lead.md](docs/lead.md).
1544
+ blocking findings come back to you. As many
1545
+ subagents as the session cap at most (2 by default, `CADENCE_MAX_SESSIONS`), never two in the same repository. The whole flow is drawn in
1546
+ [docs/lead.md](docs/lead.md); how a lot, a wave, the lead and the service of cadence itself fit together, in four figures, is in
1547
+ [docs/fonctionnement.md](docs/fonctionnement.md).
1543
1548
  - **ux-reviewer** (agent): captures at 1440 and 390 px, findings grounded in a
1544
1549
  named rule (Nielsen, WCAG 2.2 AA) or a measurement, ranked, turned into
1545
1550
  `raf add --parent` sub-tasks, and a one-line verdict for `raf ux`. It never
@@ -10,7 +10,8 @@ Follow the brief you are given. The method:
10
10
 
11
11
  1. **Read the sheet and the mockup, then the project's way of testing.** `fiche.yaml` (fields, rules, actions), `maquette.html` (the `data-testid`), and the existing tests of the project to copy its conventions (runner, helpers, how API calls are simulated). Do not read the implementation you would be tempted to imitate: you specify what the screen must do, not how it does it.
12
12
  2. **One test per planned name.** Each rule has `tests[]` names (`R3 — …`): write exactly those cases, with exactly those names, selectors = `data-testid` = field ids, API calls simulated from `actions[]`.
13
- 3. **Wire the runner.** Copy the runner template of the brief, make the project's ordinary test config (shared by the project's test command and its CI) exclude the files of the the lot's acceptation file (`acceptation-<lot>.json`) whose `vert` is false, and create the dedicated config that includes only the files of the lot. If the project's CI or test command do not use that config (hard-coded paths), do not commit red tests: stop and put the question in the report.
13
+ 3. **Wire the runner.** Copy the runner template of the brief, make the project's ordinary test config (shared by the project's test command and its CI) exclude the files of the the lot's acceptation file (`acceptation-<lot>.json`) whose `vert` is false, and always those of an acceptation with `"e2e": true` (see below), and create the dedicated config that includes only the files of the lot. If the project's CI or test command do not use that config (hard-coded paths), do not commit red tests: stop and put the question in the report.
14
+ If the tests drive a browser and need the build (Playwright…), set `"e2e": true` in the acceptation file (`{ "lot", "fiche", "fichiers", "vert": false, "e2e": true }`): `acceptationIgnored()` then excludes them even once green, and `acceptationE2e()` lists them for the project's e2e suite, run after the build (`npm run build && npx playwright test`, in the CI after the build step). Wire that suite; the ordinary runner and the unit CI must never run them.
14
15
  4. **Red for the right reason.** Run the dedicated command (by hand, outside the cycle, with `CADENCE_ACCEPTATION=<path of the acceptation file>`; for a command of a list, also `CADENCE_ACCEPTATION_ROOT=<root of the command>`, the folder of its leading `cd <folder> &&`, else the repository `.`): every test must fail because the feature is missing. A test that passes before the code proves nothing; a test that fails on a typo proves nothing either. Run the project's ordinary tests: they must stay green.
15
16
  5. **One commit**, test files + the lot's acceptation file (`acceptation-<lot>.json`) (`lot`, `fiche`, `fichiers`, `vert: false`) + runner config, explicit paths, a message that cites the lot, a clean tree.
16
17
 
package/bin/cadence.js CHANGED
@@ -40,6 +40,7 @@ if (tool === 'raf') {
40
40
  une session claude neuve par étape (implémentation, revues, corrections) ; --status, --resume ;
41
41
  --max-sessions : sessions simultanées, toutes vagues confondues (CADENCE_MAX_SESSIONS)
42
42
  --continue [--until HH:MM] [--priority a,b] : tire seul les lots prêts du plan, jusqu'à une borne
43
+ --waits [--since AAAA-MM-JJ] : l'attente de créneau de session par jour et par vague, lue dans les journaux
43
44
  cadence lead tour [dossier] [--idle 3] [--json]
44
45
  le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier courant
45
46
  s'il est lui-même un projet ; une première ligne « cadence.yaml · ✗ erreur » si priority: est illisible
@@ -50,9 +51,10 @@ if (tool === 'raf') {
50
51
  les invariants d'une fiche d'écran, back ou mixte (étape spec ; gabarits templates/fiche.yaml, fiche-back.yaml) ; code 1 si un écart, 2 si illisible
51
52
  cadence spec qa <fiche.yaml>
52
53
  la section de docs/qa/expectations.md dérivée de la fiche (shows, never, api), sur la sortie standard
53
- cadence spec reseal <lot> <test> --reason "pourquoi"
54
+ cadence spec reseal <lot> <test> --reason "pourquoi" [--porteur <lot en cours>]
54
55
  le chemin du lead quand un test d'acceptation scellé du lot en cours contredit la fiche : le test corrigé dans l'arbre,
55
- l'empreinte de ce seul test refaite, la raison dans acceptation.json et en note du lot ; hors vague orchestrée
56
+ l'empreinte de ce seul test refaite, la raison dans acceptation.json et en note du lot ; hors vague orchestrée.
57
+ Lot livré (vert: true), test instable au contrat inchangé : --porteur nomme le lot en cours qui porte la correction
56
58
  cadence skills install [--dir .claude] [--force]
57
59
  installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
58
60
  process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
package/dist/cli.js CHANGED
@@ -19,11 +19,11 @@ import { dueLine, isRecurring, recurringByDue } from './recurring.js';
19
19
  import { schedule } from './schedule.js';
20
20
  import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
21
21
  import { findProjects, leadTour, readLeadUx, tourLine } from './lead.js';
22
- import { liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
22
+ import { configuredCap, liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
23
23
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
24
24
  import { cadenceHome, openLotsIn } from './orchestrate/registry.js';
25
25
  import { classifyAll, measure, ollamaClassifier, readCache, readRuns, renderReport, sampleKeys, serializeCache } from './corrections.js';
26
- import { findAcceptation, resealAcceptation } from './orchestrate/accept.js';
26
+ import { findAcceptation, parseAcceptation, resealAcceptation } from './orchestrate/accept.js';
27
27
  import { checkFiche, deriveQa, readFiche, summary } from './spec.js';
28
28
  import { readHudContext } from './hud-context.js';
29
29
  import { sessionClose, sessionStart } from './session.js';
@@ -33,7 +33,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
33
33
  raf init [--project nom] [--prefix L] [--no-hook]
34
34
  raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--public "titre public"] [--every jours] [--after L2,L4] [--parent L3]
35
35
  raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
36
- raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (l'étape spec d'orchestrate)
36
+ raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (seule la valeur page adopte l'étape spec d'orchestrate)
37
37
  raf spec <id> on|off pose ou retire spec: d'un lot existant (le lot change un comportement : étape spec back d'orchestrate)
38
38
  raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
39
39
  raf note <id> "texte"
@@ -50,7 +50,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
50
50
  raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot (ou citant un id inconnu) sans réécrire l'historique ; raf check --ignored les liste
51
51
  raf gantt [-o docs/plan/gantt.html]
52
52
  raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
53
- cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague] [--budget 1M] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague]
53
+ cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague] [--budget 1M] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague] ; --waits [--since AAAA-MM-JJ]
54
54
  cadence lead tour [dossier] [--idle 3] [--depth n] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier seul s'il est lui-même un projet (en cours, dérive, notes de clôture, prochain lot prêt, dépôt)
55
55
  cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--json] mesure des passes de correction des journaux dossier/.cadence/runs : lots à ≥1 et ≥2 corrections, par projet, visibles ou non, passes par cause (classées par Ollama avec --classify, hors quota)
56
56
  cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
@@ -123,6 +123,7 @@ function dispatch(argv, io) {
123
123
  parent: { type: 'string' },
124
124
  force: { type: 'boolean' },
125
125
  reason: { type: 'string' },
126
+ porteur: { type: 'string' },
126
127
  status: { type: 'string' },
127
128
  since: { type: 'string' },
128
129
  idle: { type: 'string' },
@@ -179,7 +180,7 @@ function dispatch(argv, io) {
179
180
  for (const r of rows)
180
181
  io.out(tourLine(r));
181
182
  // les créneaux libres viennent du registre, le lead les lit au lieu de les calculer (L141)
182
- const [slots] = liveLines();
183
+ const [slots] = liveLines(configuredCap(io.env));
183
184
  if (slots)
184
185
  io.out(slots);
185
186
  // le réglage lead.ux du cadence.yaml du dossier (L197) : le lead lit ici qui pose le verdict UX, au lieu d'ouvrir le fichier
@@ -338,12 +339,12 @@ function dispatch(argv, io) {
338
339
  }
339
340
  case 'spec': {
340
341
  if (reseal)
341
- return specReseal(rest.slice(1), values.reason, { root, loadPlan, configPath, today }, io);
342
+ return specReseal(rest.slice(1), values.reason, values.porteur, { root, loadPlan, configPath, today }, io);
342
343
  if (rest.length !== 2 || (rest[1] !== 'on' && rest[1] !== 'off'))
343
344
  throw new RafError('usage : raf spec <id> on|off (ou cadence spec check|qa <fiche.yaml>)');
344
345
  const plan = loadPlan();
345
346
  if (!plan.setSpec(rest[0], rest[1] === 'on')) {
346
- io.out(`${rest[0]} a déjà screen: — l'écran implique la spec, rien à changer`);
347
+ io.out(`${rest[0]} a déjà screen: page — l'écran implique la spec, rien à changer`);
347
348
  return 0;
348
349
  }
349
350
  plan.save();
@@ -813,9 +814,9 @@ function waveHolding(root) {
813
814
  * acceptation.json, commite test + acceptation (le nouveau commit rouge), et note la raison au lot. Le verrou garde tout le reste.
814
815
  */
815
816
  const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
816
- function specReseal(args, reason, ctx, io) {
817
+ function specReseal(args, reason, porteur, ctx, io) {
817
818
  if (args.length !== 2 || !reason?.trim())
818
- throw new RafError('usage : cadence spec reseal <lot> <test> --reason "pourquoi le test contredit la fiche"');
819
+ throw new RafError('usage : cadence spec reseal <lot> <test> --reason "pourquoi le test contredit la fiche" [--porteur <lot en cours>, pour un lot livré]');
819
820
  if (io.env.CADENCE_ORCHESTRATED)
820
821
  throw new RafError(`spec reseal refusé pendant une vague orchestrée (${io.env.CADENCE_ORCHESTRATED}) : c'est le travail du lead, hors session`);
821
822
  // Comme `deliver` : la vague tient le verrou du dépôt tant qu'un de ses lots y travaille (état illisible : on refuse) ; un commit du lead en pleine passe ferait signaler au lot suivant un écart qui n'est pas le sien.
@@ -825,17 +826,31 @@ function specReseal(args, reason, ctx, io) {
825
826
  const [lotId, test] = args;
826
827
  const plan = ctx.loadPlan();
827
828
  const lot = plan.lot(lotId);
828
- if (lot.status !== 'doing')
829
- throw new RafError(`${lotId} n'est pas en cours (${lot.status}) : le rescellé ne vaut que pour le lot en cours ; un lot livré évolue avec sa fiche (README, « Exception — a delivered lot »)`);
830
829
  const repo = gitRoot(ctx.root) ?? ctx.root;
831
830
  const dir = readOrchestrateConfig(ctx.configPath).spec.dir;
832
831
  const json = findAcceptation(repo, dir, lotId);
832
+ // Un lot livré (acceptation verte) se rescelle pour un test instable dont le contrat ne change pas : le lot en cours qui porte la correction est nommé (--porteur) et cité dans le commit.
833
+ const livre = lot.status !== 'doing' && !!json && ((a) => typeof a !== 'string' && a.vert)(parseAcceptation(readFileSync(join(repo, json), 'utf8')));
834
+ if (lot.status !== 'doing' && !livre)
835
+ throw new RafError(`${lotId} n'est pas en cours (${lot.status}) : le rescellé vaut pour le lot en cours, ou pour un lot livré dont l'acceptation est verte (vert: true) avec --porteur <lot en cours>`);
836
+ if (porteur && lot.status === 'doing')
837
+ throw new RafError(`--porteur n'a de sens que pour un lot livré : ${lotId} est en cours, son rescellé est cité sous son propre id`);
838
+ if (livre && !porteur?.trim())
839
+ throw new RafError(`${lotId} est livré : --porteur <lot en cours> est obligatoire (le lot dont le commit porte la correction du test)`);
840
+ if (livre && porteur) {
841
+ if (porteur === lotId)
842
+ throw new RafError(`--porteur doit être un autre lot que ${lotId} (le lot livré est clos)`);
843
+ const carrier = plan.lot(porteur);
844
+ if (carrier.status !== 'doing')
845
+ throw new RafError(`--porteur ${porteur} n'est pas en cours (${carrier.status}) : le lot porteur est celui dont le commit porte la correction`);
846
+ }
847
+ const cite = livre ? porteur : lotId;
833
848
  if (!json)
834
849
  throw new RafError(`${lotId} : aucune acceptation sous ${dir} (le lot n'a pas passé l'étape accept)`);
835
850
  const before = { json: readFileSync(join(repo, json), 'utf8'), test: existsSync(join(repo, test)) ? readFileSync(join(repo, test), 'utf8') : '' };
836
851
  let r;
837
852
  try {
838
- r = resealAcceptation(repo, json, test, reason, ctx.today);
853
+ r = resealAcceptation(repo, json, test, reason, ctx.today, livre ? porteur : undefined);
839
854
  }
840
855
  catch (e) {
841
856
  throw new RafError(e.message);
@@ -843,7 +858,7 @@ function specReseal(args, reason, ctx, io) {
843
858
  const norm = posix.normalize(test);
844
859
  try {
845
860
  git(repo, ['add', '--', json, norm]);
846
- git(repo, ['commit', '-q', '-m', `test(${lotId}): acceptation rescellée, ${norm} : ${reason.trim()}`, '--', json, norm]);
861
+ git(repo, ['commit', '-q', '-m', `test(${cite}): acceptation rescellée${livre ? ` du lot livré ${lotId}` : ''}, ${norm} : ${reason.trim()}`, '--', json, norm]);
847
862
  }
848
863
  catch (e) {
849
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.
@@ -861,10 +876,10 @@ function specReseal(args, reason, ctx, io) {
861
876
  const sha = git(repo, ['rev-parse', '--short', 'HEAD']).trim();
862
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.
863
878
  if (plan.readonly) {
864
- 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 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} ; raison dans ${json} et dans le commit — note à ajouter au lot ${cite} avec l'outil du projet (plan en lecture seule)`);
865
880
  return 0;
866
881
  }
867
- plan.note(lotId, `test d'acceptation rescellé (${norm}, fiche inchangée) : ${reason.trim()}`, ctx.today);
882
+ plan.note(cite, `test d'acceptation rescellé${livre ? ` (lot livré ${lotId}, ` : ' ('}${norm}, fiche inchangée) : ${reason.trim()}`, ctx.today);
868
883
  plan.save();
869
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.
870
885
  const planRel = relative(repo, plan.path);
@@ -872,7 +887,7 @@ function specReseal(args, reason, ctx, io) {
872
887
  if (tracked) {
873
888
  try {
874
889
  git(repo, ['add', '--', planRel]);
875
- git(repo, ['commit', '-q', '-m', `plan: ${lotId} test d'acceptation rescellé (${norm})`, '--', planRel]);
890
+ git(repo, ['commit', '-q', '-m', `plan: ${cite} test d'acceptation rescellé (${norm})`, '--', planRel]);
876
891
  }
877
892
  catch (e) {
878
893
  // Le rescellé est fait (commit ${sha}) : on ne le défait pas, on dit où en est la note, au lieu d'une pile d'appels.
@@ -880,7 +895,7 @@ function specReseal(args, reason, ctx, io) {
880
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(' | ')}`);
881
896
  }
882
897
  }
883
- 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${tracked ? '' : ' (plan à commiter)'}`);
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)'}`);
884
899
  return 0;
885
900
  }
886
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. */
@@ -86,7 +86,7 @@ export function readRuns(runsDir, until) {
86
86
  const tally = () => ({ lots: 0, atLeast1: 0, atLeast2: 0, passes: 0 });
87
87
  const causes = () => ({ ...Object.fromEntries([...CAUSES, 'sans-constat', 'non-classe'].map((c) => [c, 0])) });
88
88
  export function measure(data, cache) {
89
- const m = { total: tally(), byProject: {}, byVisibility: { visible: tally(), hidden: tally() }, byCause: causes(), byProjectCause: {}, agreement: { reviewed: 0, agree: 0 } };
89
+ const m = { total: tally(), byProject: {}, byVisibility: { visible: tally(), hidden: tally() }, byCause: causes(), byVisibilityCause: { visible: causes(), hidden: causes() }, byProjectCause: {}, agreement: { reviewed: 0, agree: 0 } };
90
90
  for (const l of data.lots) {
91
91
  const groups = [m.total, (m.byProject[l.project] ??= tally()), l.visible ? m.byVisibility.visible : m.byVisibility.hidden];
92
92
  for (const t of groups) {
@@ -102,6 +102,7 @@ export function measure(data, cache) {
102
102
  const c = cache.get(p.key);
103
103
  const cause = p.findings.length === 0 ? 'sans-constat' : c ? (c.manual ?? c.cause) : 'non-classe';
104
104
  m.byCause[cause]++;
105
+ (p.visible ? m.byVisibilityCause.visible : m.byVisibilityCause.hidden)[cause]++;
105
106
  (m.byProjectCause[p.project] ??= causes())[cause]++;
106
107
  if (c?.manual && cause !== 'sans-constat') {
107
108
  m.agreement.reviewed++;
@@ -208,6 +209,8 @@ export function serializeCache(cache) {
208
209
  return JSON.stringify(Object.fromEntries([...cache].sort(([a], [b]) => a.localeCompare(b))), null, 2) + '\n';
209
210
  }
210
211
  const pct = (a, b) => (b === 0 ? '—' : `${Math.round((100 * a) / b)} %`);
212
+ /** Les causes qu'une meilleure spécification évite (critère de réussite de l'étape spec). */
213
+ const SPEC_CAUSES = ['spec-floue', 'regle-inventee', 'test-manquant'];
211
214
  const row = (name, t) => `| ${name} | ${t.lots} | ${t.atLeast1} (${pct(t.atLeast1, t.lots)}) | ${t.atLeast2} (${pct(t.atLeast2, t.lots)}) | ${t.passes} |`;
212
215
  export function renderReport(m) {
213
216
  const all = [...CAUSES, 'sans-constat', 'non-classe'];
@@ -222,6 +225,12 @@ export function renderReport(m) {
222
225
  out.push(`| ${p} | ${used.map((c) => m.byProjectCause[p]?.[c] ?? 0).join(' | ')} | ${m.byProject[p].passes} |`);
223
226
  out.push('', '### Lots avec correction', '', '| Projet | Lots | ≥ 1 correction | ≥ 2 corrections | Passes |', '|---|---|---|---|---|', ...projects.map((p) => row(p, m.byProject[p])), row('**Total**', m.total), '');
224
227
  out.push('### Lots visibles et non visibles', '', '| Lots | Lots | ≥ 1 correction | ≥ 2 corrections | Passes |', '|---|---|---|---|---|', row('visibles', m.byVisibility.visible), row('non visibles', m.byVisibility.hidden), '');
228
+ out.push('### Par cause et par visibilité (passes)', '', `| Lots | Lots | Passes | ${used.map((c) => LABELS[c]).join(' | ')} | Spec floue, règle inventée ou test manquant |`, `|---|---|---|${used.map(() => '---').join('|')}|---|`);
229
+ for (const [name, t, byC] of [['visibles', m.byVisibility.visible, m.byVisibilityCause.visible], ['non visibles', m.byVisibility.hidden, m.byVisibilityCause.hidden]]) {
230
+ const spec = SPEC_CAUSES.reduce((n, c) => n + byC[c], 0);
231
+ out.push(`| ${name} | ${t.lots} | ${t.passes} | ${used.map((c) => byC[c]).join(' | ')} | ${spec} (${pct(spec, t.passes)}) |`);
232
+ }
233
+ out.push('');
225
234
  if (m.agreement.reviewed > 0)
226
235
  out.push(`Échantillon relu à la main : ${m.agreement.reviewed} passes, le modèle local est d'accord avec la relecture sur ${m.agreement.agree} (${pct(m.agreement.agree, m.agreement.reviewed)}) ; la cause relue prime dans les tableaux.`, '');
227
236
  return out.join('\n');
@@ -50,7 +50,7 @@ export function parseAcceptation(text) {
50
50
  return 'clé « vert » (booléen) attendue';
51
51
  const empreintes = isObj(doc.empreintes) ? Object.fromEntries(Object.entries(doc.empreintes).filter(([, v]) => typeof v === 'string')) : undefined;
52
52
  const resceaux = Array.isArray(doc.resceaux) ? doc.resceaux.filter((r) => isObj(r) && ['test', 'raison', 'date', 'avant', 'apres'].every((k) => typeof r[k] === 'string')) : undefined;
53
- return { lot, fiche, fichiers: fichiers, vert, ...(empreintes ? { empreintes } : {}), ...(resceaux?.length ? { resceaux } : {}) };
53
+ return { lot, fiche, fichiers: fichiers, vert, ...(doc.e2e === true ? { e2e: true } : {}), ...(empreintes ? { empreintes } : {}), ...(resceaux?.length ? { resceaux } : {}) };
54
54
  }
55
55
  /** JSON à clés triées, sans la clé `vert` : la forme que le verrou compare. */
56
56
  const canon = (a) => JSON.stringify(Object.fromEntries(Object.entries(a).filter(([k]) => k !== 'vert').sort(([x], [y]) => x.localeCompare(y))));
@@ -125,13 +125,15 @@ export function sealAcceptation(repo, path) {
125
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
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.
127
127
  */
128
- export function resealAcceptation(repo, path, test, reason, today) {
128
+ export function resealAcceptation(repo, path, test, reason, today, porteur) {
129
129
  const file = join(repo, path);
130
130
  const a = parseAcceptation(readFileSync(file, 'utf8'));
131
131
  if (typeof a === 'string')
132
132
  throw new Error(`${path} : ${a}`);
133
133
  if (!reason.trim())
134
134
  throw new Error('--reason est obligatoire : la raison du rescellé va dans acceptation.json et en note du lot');
135
+ if (porteur && !a.vert)
136
+ throw new Error(`${path} n'est pas verte : un lot porteur (${porteur}) ne vaut que pour le test scellé d'un lot livré (vert: true)`);
135
137
  const norm = posix.normalize(test);
136
138
  const fichiers = a.fichiers.map((f) => posix.normalize(f));
137
139
  if (!fichiers.includes(norm))
@@ -155,7 +157,7 @@ export function resealAcceptation(repo, path, test, reason, today) {
155
157
  const doc = JSON.parse(readFileSync(file, 'utf8'));
156
158
  doc.empreintes[norm] = apres;
157
159
  const journal = Array.isArray(doc.resceaux) ? doc.resceaux : [];
158
- doc.resceaux = [...journal, { test: norm, raison: reason.trim(), date: today, avant, apres }];
160
+ doc.resceaux = [...journal, { test: norm, raison: reason.trim(), date: today, avant, apres, ...(porteur ? { porteur } : {}) }];
159
161
  writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
160
162
  return { avant, apres, lot: a.lot };
161
163
  }
@@ -379,7 +381,7 @@ export function acceptText(o) {
379
381
  isAcceptList(o.command)
380
382
  ? `The project declares several dedicated commands (\`orchestrate.accept\`, one per test runner), each of which runs ONLY the acceptance tests of this lot that sit under its root (the folder of its leading \`cd <folder> &&\`, else the repository): ${commands.map((x) => `\`${x}\` (root \`${acceptRoot(x) || '.'}\`)`).join(', ')}. Each is run from the repository root and gets \`CADENCE_ACCEPTATION\` (the absolute path of the lot's \`acceptation.json\`, also substituted for \`{acceptation}\`) and \`CADENCE_ACCEPTATION_ROOT\` (its root); it takes only the files listed under its root, a command with no listed file under its root is not run, and a listed file under no root is refused. Put each test file where its runner looks, and list every file in \`fichiers\`.${o.test ? ` The project's ordinary test command is \`${o.test}\`: it must stay green with your red tests committed.` : ''}`
381
383
  : `The project declares the dedicated command that runs ONLY the acceptance tests of this lot (\`orchestrate.accept\`): \`${commands[0]}\` (from the repository root; the program sets \`CADENCE_ACCEPTATION\` to the absolute path of the lot's \`acceptation.json\`, and replaces \`{acceptation}\` with it).${o.test ? ` The project's ordinary test command is \`${o.test}\`: it must stay green with your red tests committed.` : ''}`,
382
- `Write \`${o.acceptation}\` (one file per lot, next to the sheet: another lot on the same ${screen ? 'screen' : 'subject'} has its own, which you never touch, nor its tests — give your test files names of your own): \`{ "lot": "<lot>", "fiche": "${o.fiche}", "fichiers": ["<test file>", …], "vert": false }\` (paths relative to the repository). The ordinary test config of the project (shared with its CI) must exclude those files while \`vert\` is false; the dedicated config includes only them. Both read the file with this runner template (copy it into the project, e.g. \`tests/acceptation-runner.mjs\`):`,
384
+ `Write \`${o.acceptation}\` (one file per lot, next to the sheet: another lot on the same ${screen ? 'screen' : 'subject'} has its own, which you never touch, nor its tests — give your test files names of your own): \`{ "lot": "<lot>", "fiche": "${o.fiche}", "fichiers": ["<test file>", …], "vert": false }\` (paths relative to the repository). If the tests need a build or a browser (Playwright, Chromium, \`dist\`), add \`"e2e": true\` to it: those files then stay out of the ordinary runner and the unit CI even once green (they would turn it red without \`dist\` or Chromium) and belong to the project's e2e suite, run after the build (\`acceptationE2e()\` of the runner template lists them). The ordinary test config of the project (shared with its CI) must exclude those files while \`vert\` is false; the dedicated config includes only them. Both read the file with this runner template (copy it into the project, e.g. \`tests/acceptation-runner.mjs\`):`,
383
385
  ];
384
386
  if (o.runner)
385
387
  lines.push('```js', o.runner.trimEnd(), '```');
@@ -12,12 +12,13 @@ import { AGENTS_DIR } from '../skills.js';
12
12
  import { pidAlive, sharedStateDir } from '../state.js';
13
13
  import { acquireSlot, cadenceHome, freeSlots, liveSlots, liveWaves, registerWave, unregisterWave, updateWaveRepos } from './registry.js';
14
14
  import { loadTemplates, newsText, objective, renderBrief } from './briefs.js';
15
+ import { readWaits, renderWaits } from './waits.js';
15
16
  import { acceptationFile, acceptText } from './accept.js';
16
17
  import { ficheReviewText } from './fiche.js';
17
18
  import { maquetteText } from './maquette.js';
18
- import { specText } from './spec.js';
19
+ import { specScreen, specText } from './spec.js';
19
20
  import { candidates, parsePriority, parseUntil, readPriority, stopReason } from './continue.js';
20
- import { Budget, DROPPED, MAX_PASSES, briefName, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
21
+ import { Budget, DROPPED, MAX_PASSES, MAX_PASSES_CONVERGING, briefName, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
21
22
  import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
22
23
  import { buildArgs, buildLocalArgs, killSessions, mcpServersFor, readAgents, realClaude } from './launch.js';
23
24
  import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
@@ -98,7 +99,7 @@ function lotArg(text) {
98
99
  return { project: m[1], lot: m[2], model: m[3] };
99
100
  }
100
101
  export function parseOrchestrateArgs(argv) {
101
- const a = { lots: [], dryRun: false, watch: false, continue: false, answers: [], drop: [], stopAfterCurrent: false };
102
+ const a = { lots: [], dryRun: false, waits: false, watch: false, continue: false, answers: [], drop: [], stopAfterCurrent: false };
102
103
  const optional = (i) => (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-') && !LOT_LIKE.test(argv[i + 1]) ? argv[i + 1] : undefined);
103
104
  for (let i = 0; i < argv.length; i++) {
104
105
  const t = argv[i];
@@ -115,6 +116,13 @@ export function parseOrchestrateArgs(argv) {
115
116
  i++;
116
117
  a[t === '--status' ? 'status' : 'resume'] = v ?? true;
117
118
  }
119
+ else if (t === '--waits')
120
+ a.waits = true;
121
+ else if (t === '--since') {
122
+ a.since = value(t);
123
+ if (!isDay(a.since))
124
+ throw new RafError(`--since invalide : ${a.since} (AAAA-MM-JJ)`);
125
+ }
118
126
  else if (t === '--watch')
119
127
  a.watch = true;
120
128
  else if (t === '--interval')
@@ -271,11 +279,12 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
271
279
  continue;
272
280
  }
273
281
  const small = lot.estimate <= 0.5 || lot.quickwin;
274
- const state = newLot({ project: t.project, repo: t.repo, lot: t.lot, title: lot.title, visible: lot.visible, small, ...(lot.screen ? { screen: lot.screen } : {}), ...(lot.spec && !lot.screen ? { specLot: true } : {}), model: t.model, readOnlyPlan: plan.readonly });
282
+ const screen = specScreen(t.repo, env.config.spec.dir, lot);
283
+ const state = newLot({ project: t.project, repo: t.repo, lot: t.lot, title: lot.title, visible: lot.visible, small, ...(screen ? { screen } : {}), ...(lot.spec && !screen ? { specLot: true } : {}), model: t.model, readOnlyPlan: plan.readonly });
275
284
  if (neighbours.repos.length)
276
285
  state.repos = neighbours.repos;
277
286
  state.light = lot.estimate <= env.config.review.threshold;
278
- state.budget = lotBudget(lot.estimate, lot.screen ? 'screen' : lot.spec ? 'spec' : undefined);
287
+ state.budget = lotBudget(lot.estimate, screen ? 'screen' : lot.spec ? 'spec' : undefined);
279
288
  state.dependsOn = lot.after.filter((d) => earlier.includes(d));
280
289
  if (node.kind === 'ok')
281
290
  state.node = { version: node.version, wanted: node.wanted, bin: node.bin, ...(node.skipped ? { skipped: node.skipped } : {}) };
@@ -342,7 +351,7 @@ function dryRun(lots, io, deps, budget, id) {
342
351
  steps.push({ kind: 'review', model: l.light ? light : full });
343
352
  }
344
353
  io.out(` étapes : ${steps.map((s) => `${s.kind} (${s.model})`).join(' → ')}${l.small ? ' — petit lot' : ''}${l.light ? ` — revue légère (${light}, pas de passe des mineurs ; un bloquant ou un majeur → correction puis revue ${full})` : ''}${l.visible && !env.config.ux ? ' — UX à faire par le lead (orchestrate.ux absent)' : ''}`);
345
- io.out(` corrections : ${MAX_PASSES} passe(s) au plus, en session neuve`);
354
+ io.out(` corrections : ${MAX_PASSES} passe(s) au plus (${MAX_PASSES_CONVERGING} si la revue converge), en session neuve`);
346
355
  io.out(l.light ? ' revue conforme avec mineurs : pas de passe des mineurs, ils sont rendus en notes' : ' revue conforme avec mineurs : une passe de correction des mineurs (session neuve), puis une revue courte ; les mineurs refusés sont rendus en « choix »');
347
356
  const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id, lotSlug(l.project, l.lot), 'playwright');
348
357
  // brief écrit par --dry-run = base d'une délégation à la main : aucun dossier de vague (il n'existe pas)
@@ -377,18 +386,29 @@ function maxSessions(args, io) {
377
386
  const env = io.env.CADENCE_MAX_SESSIONS;
378
387
  return env ? parseMaxSessions(env, 'CADENCE_MAX_SESSIONS') : DEFAULT_MAX_SESSIONS;
379
388
  }
389
+ /** Plafond à AFFICHER (`--status`, `lead tour`) : le réglage de l'humain, jamais 2 en dur. Un réglage invalide ne casse pas l'affichage (le lancement, lui, le refuse) : retour au défaut. */
390
+ export function configuredCap(env, flag) {
391
+ if (flag !== undefined)
392
+ return flag;
393
+ try {
394
+ return env.CADENCE_MAX_SESSIONS ? parseMaxSessions(env.CADENCE_MAX_SESSIONS, 'CADENCE_MAX_SESSIONS') : DEFAULT_MAX_SESSIONS;
395
+ }
396
+ catch {
397
+ return DEFAULT_MAX_SESSIONS;
398
+ }
399
+ }
380
400
  function duration(ms) {
381
401
  const s = Math.round(ms / 1000);
382
402
  return s < 60 ? `${s} s` : `${Math.floor(s / 60)} min ${String(s % 60).padStart(2, '0')} s`;
383
403
  }
384
404
  /** Vagues vivantes (toutes, quel que soit leur dossier de départ), créneaux libres et dépôts qu'elles tiennent. */
385
- export function liveLines() {
405
+ export function liveLines(cap = DEFAULT_MAX_SESSIONS) {
386
406
  const home = cadenceHome();
387
407
  const waves = liveWaves(home);
388
408
  if (waves.length === 0)
389
409
  return [];
390
410
  const slots = liveSlots(home).length;
391
- const lines = [`vagues en cours : ${waves.length} · sessions en cours : ${slots} · créneaux libres : ${freeSlots(waves, DEFAULT_MAX_SESSIONS)} sur ${DEFAULT_MAX_SESSIONS}`];
411
+ const lines = [`vagues en cours : ${waves.length} · sessions en cours : ${slots} · créneaux libres : ${freeSlots(waves, cap)} sur ${cap}`];
392
412
  for (const w of waves)
393
413
  lines.push(` ${w.wave} (pid ${w.pid}, depuis ${w.started}) lancée depuis ${w.cwd} · plafond ${w.cap ?? '?'} · dépôts : ${w.repos.join(', ')}`);
394
414
  return lines;
@@ -397,9 +417,9 @@ export function liveLines() {
397
417
  const CLEAR_SCREEN = '\x1b[H\x1b[2J';
398
418
  const DEFAULT_WATCH_INTERVAL = 10;
399
419
  /** Ce que `--status` montre à un instant : les vagues vivantes, puis le tableau (absent si seules les vagues vivantes comptent). */
400
- function statusView(args, launch) {
420
+ function statusView(args, launch, cap) {
401
421
  const store = typeof args.status === 'string' ? RunStore.find(launch, args.status) : RunStore.last(launch);
402
- const live = liveLines();
422
+ const live = liveLines(cap);
403
423
  if (!store && live.length && args.status === true)
404
424
  return { live, running: true };
405
425
  if (!store)
@@ -433,10 +453,19 @@ export async function orchestrate(argv, io, deps) {
433
453
  throw new RafError('--status ne se combine pas avec --drop / --stop-after-current');
434
454
  if (args.resume !== undefined && args.stopAfterCurrent)
435
455
  throw new RafError('--resume ne se combine pas avec --stop-after-current (il reprend la vague, il ne l\'arrête pas)');
456
+ if (args.since !== undefined && !args.waits)
457
+ throw new RafError('--since s\'utilise avec --waits');
458
+ if (args.waits) {
459
+ if (args.lots.length || args.status !== undefined || args.resume !== undefined || args.watch || args.drop.length || args.stopAfterCurrent || args.continue || args.dryRun)
460
+ throw new RafError('--waits ne se combine avec aucune autre action');
461
+ for (const line of renderWaits(readWaits(launch, args.since)))
462
+ io.out(line);
463
+ return 0;
464
+ }
436
465
  if (args.status !== undefined) {
437
466
  maxSessions(args, io); // une valeur invalide est refusée ici aussi
438
467
  if (!args.watch) {
439
- const { live, table } = statusView(args, launch);
468
+ const { live, table } = statusView(args, launch, configuredCap(io.env, args.maxSessions));
440
469
  for (const line of live)
441
470
  io.out(line);
442
471
  if (!table)
@@ -450,7 +479,7 @@ export async function orchestrate(argv, io, deps) {
450
479
  // --watch : le même tableau, rafraîchi ; il s'arrête seul quand plus aucune vague ne tourne (le dernier rendu reste affiché)
451
480
  const sleep = deps.watchSleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
452
481
  for (;;) {
453
- const { live, table, running } = statusView(args, launch);
482
+ const { live, table, running } = statusView(args, launch, configuredCap(io.env, args.maxSessions));
454
483
  io.out(CLEAR_SCREEN);
455
484
  for (const line of live)
456
485
  io.out(line);
@@ -472,7 +501,7 @@ export async function orchestrate(argv, io, deps) {
472
501
  if (args.drop.length || args.stopAfterCurrent)
473
502
  return control(args, io);
474
503
  if (args.lots.length === 0 && !args.continue)
475
- throw new RafError('usage : cadence orchestrate <projet>:<lot>… [--budget 2M] [--max-sessions 2] [--dry-run] [--continue [--until 18:00] [--priority a,b]] | --status [vague] [--watch [--interval 10]] | --resume [vague] [--answer projet:lot "réponse"] | --drop projet:lot | --stop-after-current');
504
+ throw new RafError('usage : cadence orchestrate <projet>:<lot>… [--budget 2M] [--max-sessions 2] [--dry-run] [--continue [--until 18:00] [--priority a,b]] | --waits [--since AAAA-MM-JJ] | --status [vague] [--watch [--interval 10]] | --resume [vague] [--answer projet:lot "réponse"] | --drop projet:lot | --stop-after-current');
476
505
  const refusals = [];
477
506
  const targets = resolveTargets(args, io, refusals);
478
507
  const pre = await preflight(args, targets, io, deps, launch);
@@ -785,8 +814,8 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
785
814
  const free = await acquireSlot(home, cap, {
786
815
  wave: wave.id,
787
816
  pollMs: deps.slotPollMs,
788
- onWait: (h, ms) => wctx.log(`en attente d'un créneau de session depuis ${duration(ms)} (${h.length}/${cap} en cours : ${[...new Set(h.map((x) => x.wave))].join(', ')})`),
789
- onGot: (ms) => wctx.log(`créneau de session obtenu après ${duration(ms)} d'attente`),
817
+ onWait: (h, ms) => wctx.log(`${o.lot ? `${o.lot} · ` : ''}en attente d'un créneau de session depuis ${duration(ms)} (${h.length}/${cap} en cours : ${[...new Set(h.map((x) => x.wave))].join(', ')})`),
818
+ onGot: (ms) => wctx.log(`${o.lot ? `${o.lot} · ` : ''}créneau de session obtenu après ${duration(ms)} d'attente`),
790
819
  });
791
820
  try {
792
821
  return await deps.claude(args, o);
@@ -6,7 +6,8 @@ import { planLoader } from '../config.js';
6
6
  import { gitRoot } from '../git.js';
7
7
  import { findProjects, isProject } from '../lead.js';
8
8
  import { RafError } from '../plan.js';
9
- import { lotBudget } from './command.js';
9
+ import { lotBudget, projectEnv } from './command.js';
10
+ import { specScreen } from './spec.js';
10
11
  import { STOP_REQUESTED } from './cycle.js';
11
12
  /** `--until 18:00` : l'heure d'aujourd'hui (date de `now`) à partir de laquelle la vague ne tire plus de lot. */
12
13
  export function parseUntil(text, now) {
@@ -71,8 +72,15 @@ export function candidates(launch, o) {
71
72
  catch {
72
73
  continue;
73
74
  }
75
+ let specDir = 'docs/specs';
76
+ try {
77
+ specDir = projectEnv(dir).config.spec.dir;
78
+ }
79
+ catch {
80
+ // configuration illisible : le dossier par défaut
81
+ }
74
82
  for (const lot of ready) {
75
- if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot) || lotBudget(lot.estimate, lot.screen ? 'screen' : lot.spec ? 'spec' : undefined) > o.remaining)
83
+ if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot) || lotBudget(lot.estimate, specScreen(dir, specDir, lot) ? 'screen' : lot.spec ? 'spec' : undefined) > o.remaining)
76
84
  continue;
77
85
  out.push({ project, dir, lot });
78
86
  }
@@ -25,6 +25,8 @@ import { answeredText, checkSpec, costlyHypotheses, ficheHasBack, ficheReady, fi
25
25
  import { lotKey, lotRepoPaths, neighbourDirs } from './state.js';
26
26
  /** Passes de correction au plus, puis la main est rendue au lead. */
27
27
  export const MAX_PASSES = 2;
28
+ /** Plafond quand la revue converge (L205) : le nombre de bloquants+majeurs a baissé d'une revue à l'autre et il n'en reste qu'un. */
29
+ export const MAX_PASSES_CONVERGING = MAX_PASSES + 1;
28
30
  export class Budget {
29
31
  limit;
30
32
  consumed = 0;
@@ -462,6 +464,7 @@ async function session(c, kind, local = false) {
462
464
  agent: kind === 'spec' ? 'spec-writer' : kind === 'accept' ? 'acceptance-writer' : kind === 'ux' || kind === 'maquette' ? 'ux-reviewer' : kind === 'precheck' ? 'precheck-reader' : write ? undefined : 'code-reviewer',
463
465
  cwd: l.repo,
464
466
  wave: w.id,
467
+ lot: lotKey(l.project, l.lot),
465
468
  permissionMode: c.config.permissionMode,
466
469
  prodAccess: c.config.prodAccess,
467
470
  addDirs: [...c.config.addDirs, ...(playwright && write ? [playwrightDir(dirname(mcpConfig))] : []), ...neighbourDirs(l)],
@@ -1422,8 +1425,12 @@ function firstReview(c) {
1422
1425
  function toFix(c, constats, what) {
1423
1426
  const l = c.lot;
1424
1427
  l.constats = constats;
1425
- if (l.pass >= MAX_PASSES) {
1426
- stop(c, 'handed-back', `${what} après ${MAX_PASSES} passe(s) de correction`);
1428
+ // Revue qui converge (L205) : un seul constat bloquant ou majeur reste, après une baisse d'une revue à l'autre → une passe de plus (budget : fixAffordable, à l'entrée de la session).
1429
+ const counts = l.reviewCounts ?? [];
1430
+ const converging = what === 'revue non conforme' && counts.length >= 2 && counts[counts.length - 1] === 1 && counts[counts.length - 1] < counts[counts.length - 2];
1431
+ const cap = converging ? MAX_PASSES_CONVERGING : MAX_PASSES;
1432
+ if (l.pass >= cap) {
1433
+ stop(c, 'handed-back', `${what} après ${l.pass} passe(s) de correction`);
1427
1434
  return;
1428
1435
  }
1429
1436
  l.pass += 1;
@@ -1532,6 +1539,8 @@ async function reviewStep(c, kind) {
1532
1539
  l.minorLines = [];
1533
1540
  }
1534
1541
  addProposals(c, rep, 'code', !minorPass);
1542
+ if (summary.conforme && uxOk)
1543
+ (l.reviewCounts ??= []).push(0); // une revue conforme compte pour 0 dans la comparaison de la convergence (L205)
1535
1544
  if (minorPass) {
1536
1545
  l.minorPass = true;
1537
1546
  l.minorPassAt = l.pass;
@@ -1547,6 +1556,7 @@ async function reviewStep(c, kind) {
1547
1556
  return;
1548
1557
  }
1549
1558
  const constats = [...blockingConstats(rep, 'code'), ...(!l.small && l.ux && !l.ux.conforme ? uxConstatsKept(c) : [])];
1559
+ (l.reviewCounts ??= []).push(constats.length);
1550
1560
  toFix(c, constats, 'revue non conforme');
1551
1561
  }
1552
1562
  /** Constats UX de la dernière revue UX, gardés dans l'état par la revue elle-même (l.constats avant la revue de code). */
@@ -129,7 +129,7 @@ export function buildRetryArgs(spec, sessionId, agents) {
129
129
  */
130
130
  export async function runSession(spec, deps) {
131
131
  const sandbox = sandboxEnv(!!spec.prodAccess);
132
- const launch = (args) => deps.claude(args, { local: spec.local, cwd: spec.cwd, unset: sandbox.unset, env: { CADENCE_ORCHESTRATED: spec.wave, ...sandbox.set, ...(spec.nodeBin || spec.toolBin ? { PATH: [spec.toolBin, spec.nodeBin, process.env.PATH ?? ''].filter(Boolean).join(delimiter) } : {}) }, timeoutMs: spec.timeoutMs, onSpawn: deps.onSpawn });
132
+ const launch = (args) => deps.claude(args, { local: spec.local, cwd: spec.cwd, unset: sandbox.unset, env: { CADENCE_ORCHESTRATED: spec.wave, ...sandbox.set, ...(spec.nodeBin || spec.toolBin ? { PATH: [spec.toolBin, spec.nodeBin, process.env.PATH ?? ''].filter(Boolean).join(delimiter) } : {}) }, timeoutMs: spec.timeoutMs, onSpawn: deps.onSpawn, lot: spec.lot });
133
133
  const out = await launch(spec.local ? buildLocalArgs(spec, deps.agents) : buildArgs(spec, deps.agents));
134
134
  const first = classify(out);
135
135
  if (spec.local)
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { parse } from 'yaml';
5
+ import { acceptationFile } from './accept.js';
5
6
  import { checkFiche, fichesDuDossier, LARGEURS } from '../spec.js';
6
7
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
7
8
  const list = (v) => (Array.isArray(v) ? v.filter(isObj) : []);
@@ -10,6 +11,19 @@ export function findFiche(repo, dir, lot) {
10
11
  const hit = fichesDuDossier(repo, dir).find(({ doc }) => (isObj(doc.ecran) && doc.ecran.lot === lot) || (isObj(doc.sujet) && doc.sujet.lot === lot));
11
12
  return hit ? `${dir}/${hit.name}/fiche.yaml` : null;
12
13
  }
14
+ /**
15
+ * L'écran qui fait entrer un lot dans la chaîne spec → maquette → accept (L209) : `page` seul. `component` et `journey` restent des valeurs
16
+ * valides du plan mais le lot suit le chemin ordinaire, sauf s'il est déjà entré dans la chaîne (une fiche à bloc `ecran` du lot, ou son
17
+ * `acceptation-<lot>.json` à côté d'une fiche à bloc `ecran`, existent dans `dir`) : il garde alors sa fiche et son verrou.
18
+ */
19
+ export function specScreen(repo, dir, lot) {
20
+ if (!lot.screen || lot.screen === 'page')
21
+ return lot.screen;
22
+ // Seul un bloc `ecran` compte : la fiche back (bloc `sujet`) et son acceptation sont ce qu'écrit la spec d'un lot `spec: true`.
23
+ const acceptation = acceptationFile('fiche.yaml', lot.id);
24
+ const entered = fichesDuDossier(repo, dir).some(({ name, doc }) => isObj(doc.ecran) && (doc.ecran.lot === lot.id || existsSync(join(repo, dir, name, acceptation))));
25
+ return entered ? lot.screen : undefined;
26
+ }
13
27
  /** Texte sans accents ni majuscules, pour les mots-clés. */
14
28
  const plain = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
15
29
  /**
@@ -0,0 +1,120 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { isDay, toDay } from '../dates.js';
4
+ import { RunStore } from './state.js';
5
+ /** « 4 min 27 s », « 45 s », « 1 h 02 min 03 s » (la forme de `duration` et des lignes du journal) en millisecondes ; null si illisible. */
6
+ export function parseWaitMs(text) {
7
+ const m = /^(?:(\d+) h )?(?:(\d+) min )?(\d+) s$/.exec(text.trim());
8
+ if (!m)
9
+ return null;
10
+ return ((Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0)) * 60 + Number(m[3])) * 1000;
11
+ }
12
+ /**
13
+ * Les attentes d'un créneau d'un journal : comptées à l'obtention (« obtenu après … »), au jour de cette ligne ; `unfinished` = attentes commencées sans créneau obtenu.
14
+ * Chaque attente est appariée à son lot (« <projet>:<lot> · … ») : deux lots d'une même vague (un par dépôt, runPool) attendent en même temps. Une ligne sans lot (journal d'avant) est celle de la vague entière.
15
+ */
16
+ export function parseWaits(journal) {
17
+ const waits = [];
18
+ let unfinished = 0;
19
+ const state = new Map(); // « depuis » de la dernière ligne d'attente : une ligne dont il ne progresse pas (recule, ou « depuis 0 s » de nouveau) est celle d'une nouvelle attente
20
+ for (const line of journal.split('\n')) {
21
+ const m = /^(\S+) (?:(\S+) · )?(?:en attente d'un créneau de session depuis (.+?) \(|créneau de session obtenu après (.+?) d'attente)/.exec(line);
22
+ if (!m)
23
+ continue;
24
+ const key = m[2] ?? '';
25
+ let s = state.get(key);
26
+ if (!s)
27
+ state.set(key, (s = { open: false, since: 0 }));
28
+ if (m[4] === undefined) {
29
+ const ms = parseWaitMs(m[3]) ?? 0;
30
+ if (s.open && ms <= s.since)
31
+ unfinished++;
32
+ s.open = true;
33
+ s.since = ms;
34
+ continue;
35
+ }
36
+ const ms = parseWaitMs(m[4]);
37
+ const at = new Date(m[1]);
38
+ if (ms !== null && !Number.isNaN(at.getTime()))
39
+ waits.push({ day: toDay(at), ms });
40
+ s.open = false;
41
+ }
42
+ for (const s of state.values())
43
+ if (s.open)
44
+ unfinished++;
45
+ return { waits, unfinished };
46
+ }
47
+ /** Les arrêts par le quota d'un journal (« … → suspended — quota atteint », cycle.ts), au jour de la ligne : le statut final de wave.json ne les garde pas, `--resume` le remet à `running` puis `done`. */
48
+ export function parseQuotaStops(journal) {
49
+ const days = [];
50
+ for (const line of journal.split('\n')) {
51
+ const m = /^(\S+) \S+ → suspended — quota atteint$/.exec(line);
52
+ const at = m ? new Date(m[1]) : null;
53
+ if (at && !Number.isNaN(at.getTime()))
54
+ days.push(toDay(at));
55
+ }
56
+ return days;
57
+ }
58
+ /** Attente de créneau par vague et par jour, lue dans les `journal.log` et `wave.json` de `.cadence/runs/<vague>/` du dossier de lancement ; `since` écarte les vagues d'avant ce jour (début de l'identifiant). */
59
+ export function readWaits(launchDir, since) {
60
+ const base = RunStore.runsDir(launchDir);
61
+ const ids = existsSync(base) ? readdirSync(base).filter((n) => existsSync(join(base, n, 'wave.json'))).sort() : [];
62
+ const waves = [];
63
+ const days = new Map();
64
+ const dayOf = (day) => {
65
+ let d = days.get(day);
66
+ if (!d)
67
+ days.set(day, (d = { day, totalMs: 0, count: 0, waves: 0, quotaHits: 0, consumed: 0 }));
68
+ return d;
69
+ };
70
+ for (const id of ids) {
71
+ const idDay = id.slice(0, 10);
72
+ if (!isDay(idDay) || (since && idDay < since))
73
+ continue; // un dossier qui n'est pas une vague datée (essai à la main) n'a pas de jour
74
+ let meta = {};
75
+ try {
76
+ meta = JSON.parse(readFileSync(join(base, id, 'wave.json'), 'utf8'));
77
+ }
78
+ catch {
79
+ // wave.json illisible : la vague compte sans jetons
80
+ }
81
+ const journalFile = join(base, id, 'journal.log');
82
+ const journal = existsSync(journalFile) ? readFileSync(journalFile, 'utf8') : '';
83
+ const { waits, unfinished } = parseWaits(journal);
84
+ waves.push({ id, totalMs: waits.reduce((s, w) => s + w.ms, 0), count: waits.length, maxMs: Math.max(0, ...waits.map((w) => w.ms)), unfinished, status: meta.status ?? '?', consumed: meta.consumed ?? 0 });
85
+ const d = dayOf(idDay);
86
+ d.waves++;
87
+ d.consumed += meta.consumed ?? 0;
88
+ for (const day of parseQuotaStops(journal))
89
+ dayOf(day).quotaHits++;
90
+ for (const w of waits) {
91
+ const dw = dayOf(w.day);
92
+ dw.totalMs += w.ms;
93
+ dw.count++;
94
+ }
95
+ }
96
+ return { waves, days: [...days.values()].sort((a, b) => a.day.localeCompare(b.day)) };
97
+ }
98
+ function duration(ms) {
99
+ const s = Math.round(ms / 1000);
100
+ if (s < 60)
101
+ return `${s} s`;
102
+ const min = Math.floor(s / 60);
103
+ return min < 60 ? `${min} min ${String(s % 60).padStart(2, '0')} s` : `${Math.floor(min / 60)} h ${String(min % 60).padStart(2, '0')} min ${String(s % 60).padStart(2, '0')} s`;
104
+ }
105
+ /** Deux tableaux : par jour, puis par vague (celles qui ont attendu). */
106
+ export function renderWaits(r) {
107
+ if (r.waves.length === 0)
108
+ return ['aucune vague dans ce dossier'];
109
+ const lines = ['attente de créneau de session, par jour', ' jour attente cumulée attentes vagues quota atteint jetons'];
110
+ for (const d of r.days)
111
+ lines.push(` ${d.day} ${duration(d.totalMs).padEnd(16)} ${String(d.count).padStart(8)} ${String(d.waves).padStart(6)} ${String(d.quotaHits).padStart(13)} ${d.consumed}`);
112
+ const total = r.days.reduce((s, d) => s + d.totalMs, 0);
113
+ lines.push(` total ${duration(total)}`, '', 'par vague (celles qui ont attendu)');
114
+ const waited = r.waves.filter((w) => w.count > 0 || w.unfinished > 0).sort((a, b) => b.totalMs - a.totalMs);
115
+ if (waited.length === 0)
116
+ lines.push(' aucune');
117
+ for (const w of waited)
118
+ lines.push(` ${w.id} ${duration(w.totalMs).padEnd(16)} ${w.count} attente(s), la plus longue ${duration(w.maxMs)}${w.unfinished ? `, ${w.unfinished} sans créneau obtenu` : ''} [${w.status}]`);
119
+ return lines;
120
+ }
package/dist/plan.js CHANGED
@@ -437,15 +437,15 @@ export class Plan {
437
437
  const keys = node.items.map((p) => String(p.key?.value ?? p.key));
438
438
  node.items.splice(keys.indexOf('visible') + 1, 0, this.doc.createPair('screen', screen));
439
439
  }
440
- /** Pose (true) ou retire (false) le `spec:` d'un lot existant ; `screen:` l'implique, donc on ne retire pas la spec d'un lot à écran. Renvoie false quand `screen:` rendait la pose sans objet. */
440
+ /** Pose (true) ou retire (false) le `spec:` d'un lot existant ; `screen: page` l'implique, donc on ne retire pas la spec d'un lot à page (`component` et `journey` suivent le chemin ordinaire depuis L209 : leur `spec: true` est la spec back). Renvoie false quand `screen: page` rendait la pose sans objet. */
441
441
  setSpec(ref, on) {
442
442
  this.writable();
443
443
  if (ref.includes('/'))
444
444
  throw new RafError(`la spec se pose sur un lot, pas une sous-tâche : ${ref}`);
445
445
  const node = this.lotNode(ref);
446
- if (this.lot(ref).screen) {
446
+ if (this.lot(ref).screen === 'page') {
447
447
  if (!on)
448
- throw new RafError(`${ref} a screen: — retirer d'abord screen: (raf screen ${ref} none), l'écran implique la spec`);
448
+ throw new RafError(`${ref} a screen: page — retirer d'abord screen: (raf screen ${ref} none), l'écran implique la spec`);
449
449
  return false;
450
450
  }
451
451
  if (!on) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",
@@ -14,13 +14,16 @@ commands (its CLAUDE.md names them), never with `raf start|done|note|ux|review`.
14
14
 
15
15
  ## Limits that always apply
16
16
 
17
- - **At most two subagents running at once.** Queue the rest. The sessions of an orchestrated wave
18
- (section 2b) count toward this limit: orchestrate sessions plus subagents never exceed two. A wave
19
- already caps itself (`--max-sessions`, 2 by default, shared by every wave); before starting a
20
- subagent, check `cadence orchestrate --status`, and while a wave runs, start none beyond the free
21
- slots. Read the free slots, do not compute them: `cadence orchestrate --status` and `cadence lead tour` print
22
- `créneaux libres : N sur 2` (the registry subtracts, for each live wave, min(cap, repositories it still holds)
23
- from the two slots; no line from `lead tour` means no live wave, so both are free). Never raise `--max-sessions` above two to speed a wave up.
17
+ - **At most as many subagents running at once as the session cap, 2 unless the human set another.** Queue
18
+ the rest. The cap is the human's setting (`CADENCE_MAX_SESSIONS`, or `--max-sessions` on a wave; 2 by
19
+ default, shared by every wave). The sessions of an orchestrated wave (section 2b) count toward this limit:
20
+ orchestrate sessions plus subagents never exceed the cap. A wave already caps itself; before starting a
21
+ subagent, check `cadence orchestrate --status`, and while a wave runs, start none beyond the free slots. Read the cap and the
22
+ free slots, do not compute them: `cadence orchestrate --status` and `cadence lead tour` print
23
+ `créneaux libres : N sur M` (M is the configured cap; the registry subtracts, for each live wave, min(cap,
24
+ repositories it still holds) from the M slots; no line from `lead tour` means no live wave, so all M are free).
25
+ Never raise `--max-sessions` or `CADENCE_MAX_SESSIONS` yourself to speed a wave up: raising the cap is a decision of
26
+ the human (a trial at 3 was decided on 2026-10-12); if the slots are the bottleneck, say so and let them decide.
24
27
  - **Never two subagents in the same repository at the same time**, and the lead does not commit in a
25
28
  repository where a subagent is working.
26
29
  - **Deliveries and `raf ux` / `raf review` verdicts are done by the lead, one project at a time** —
@@ -92,7 +95,7 @@ and the project declares how to see its app (`orchestrate.ux` in `cadence.yaml`)
92
95
  (Opus, the `code-reviewer` agent; Sonnet, with no pass on the minor findings, for a lot whose estimate is
93
96
  ≤ 0.25 d — `orchestrate.review` in `cadence.yaml`; a blocker or major finding still brings a correction and an
94
97
  Opus review), a correction in a new session if the review is not compliant (two
95
- passes at most) — and records `raf review` itself when the code review is compliant. A lot with `spec: true` (no screen) goes `spec → fiche → accept` before the code: the `code-reviewer` reviews its back-end sheet, and the hypotheses of the final sheet come back in the table as « hypothèse de la fiche » lines, which the lead relays to the human. The state is in
98
+ passes at most, a third when the review converges: blocking + major findings down from one review to the next and only one left) — and records `raf review` itself when the code review is compliant. A lot with `spec: true` (no screen) goes `spec → fiche → accept` before the code: the `code-reviewer` reviews its back-end sheet, and the hypotheses of the final sheet come back in the table as « hypothèse de la fiche » lines, which the lead relays to the human. The state is in
96
99
  `.cadence/runs/<wave>/`, not in this conversation. `@haiku` only when the human writes it, for a
97
100
  mechanical lot. Exit code: 0 all ready · 1 some lots handed back · 2 refused before acting · 3 wave
98
101
  suspended (budget or usage limit; `--resume --budget …` continues).
@@ -100,7 +103,7 @@ suspended (budget or usage limit; `--resume --budget …` continues).
100
103
  What the orchestrator does **not** do, and stays with the lead: choose the lots (with the human), bring the
101
104
  questions back (`--resume --answer <project>:<lot> "…"`), look at the UX reviewer's captures and record
102
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),
103
- `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. It refuses a lot that is not in progress, 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 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
104
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
105
108
  and do not deliver it (`cadence deliver` refuses).
106
109
 
@@ -6,8 +6,11 @@
6
6
  // vitest.acceptation.config.ts (commande dédiée, orchestrate.accept)
7
7
  // test: { include: acceptationOnly() } (une LISTE de lanceurs : chacun ne voit que les fichiers sous sa racine, cf. acceptationOnly)
8
8
  //
9
+ // playwright.config.ts testMatch: acceptationE2e() (suite e2e, lancée APRÈS le build : une acceptation `"e2e": true`, navigateur et dist requis)
10
+ //
9
11
  // Un acceptation.json à `vert: false` (tests rouges du lot, pas encore implémenté) exclut ses fichiers du lanceur du projet et de la CI ;
10
12
  // le cycle pose `vert: true` quand la commande dédiée passe, et les fichiers rejoignent alors le lanceur ordinaire.
13
+ // Exception (L203) : une acceptation `"e2e": true` ne le rejoint JAMAIS (sans dist ni Chromium elle le rendrait rouge) : elle reste exclue, verte ou non, et va dans la suite e2e.
11
14
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
12
15
  import { join, posix } from 'node:path';
13
16
 
@@ -22,8 +25,7 @@ const read = (file) => {
22
25
 
23
26
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
24
27
 
25
- /** Fichiers des acceptation-<lot>.json (ou acceptation.json) à `vert: false` sous `<root>/<dir>/*` (un fichier par lot : plusieurs par écran), relatifs à `root` ; `format: 'regex'` pour testPathIgnorePatterns. */
26
- export function acceptationIgnored(root = process.cwd(), dir = 'docs/specs', format = 'glob') {
28
+ function collect(root, dir, format, keep) {
27
29
  const base = join(root, dir);
28
30
  if (!existsSync(base)) return [];
29
31
  const out = [];
@@ -37,13 +39,23 @@ export function acceptationIgnored(root = process.cwd(), dir = 'docs/specs', for
37
39
  }
38
40
  for (const file of files) {
39
41
  const doc = read(join(folder, file));
40
- if (!doc || doc.vert === true) continue;
42
+ if (!doc || !keep(doc)) continue;
41
43
  for (const f of doc.fichiers) if (typeof f === 'string') out.push(format === 'regex' ? `${escape(f)}$` : f);
42
44
  }
43
45
  }
44
46
  return out;
45
47
  }
46
48
 
49
+ /** Fichiers des acceptation-<lot>.json (ou acceptation.json) à `vert: false`, ou à `e2e: true` (verts ou non), sous `<root>/<dir>/*` (un fichier par lot : plusieurs par écran), relatifs à `root` ; `format: 'regex'` pour testPathIgnorePatterns. */
50
+ export function acceptationIgnored(root = process.cwd(), dir = 'docs/specs', format = 'glob') {
51
+ return collect(root, dir, format, (doc) => doc.vert !== true || doc.e2e === true);
52
+ }
53
+
54
+ /** Fichiers des acceptations `e2e: true` (vertes ou rouges) sous `<root>/<dir>/*`, relatifs à `root` : le `testMatch` de la suite e2e, lancée après le build. */
55
+ export function acceptationE2e(root = process.cwd(), dir = 'docs/specs', format = 'glob') {
56
+ return collect(root, dir, format, (doc) => doc.e2e === true);
57
+ }
58
+
47
59
  /**
48
60
  * Fichiers de l'acceptation.json donné (par défaut celui de `CADENCE_ACCEPTATION`, posé par le cycle) : l'`include` de la config dédiée.
49
61
  * Avec une liste de lanceurs (`orchestrate.accept` en liste), chaque lanceur ne prend que les fichiers sous SA racine, le dossier de son
@@ -14,7 +14,7 @@ Sheet rules:
14
14
  - Check every date in the sample data of the mockup before keeping it: compute its weekday (a command such as `date -d 2026-09-03 +%A`, or a script), never write it from memory — a « Wed 03/09 » that is a Thursday sends the mockup back for nothing. The same goes for any figure that must add up (a score and its result, a total and its parts).
15
15
  - Expose the messages exactly as the screen shows them; the message of a rule is visible in the state its `etat` names.
16
16
  - A rule you invent is not a free choice: **a hypothesis whose `si_faux` involves an expensive-to-undo choice is a question, not a hypothesis.** The four expensive categories: a data model or storage (key, migration, user data created once shipped); a public or shared API contract, or an already published route / URL; a destructive or irreversible action (deletion, sending, payment); a rule that contradicts an existing sourced rule. In those cases do not write the hypothesis: stop and put the question under "questions" (the lead answers and the step is resumed). Everything else (wording, order of the fields, error state, spacing) is a written hypothesis, cheap to correct later.
17
- - If, after reading the lot and the code, you find that the lot changes NO screen (no new page, component or modified journey), write one line with the reason in `sousSeuil`, create no mockup and no sheet, and finish: the lot then follows the usual path.
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
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".