@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +32 -27
- package/agents/acceptance-writer.md +2 -1
- package/bin/cadence.js +4 -2
- package/dist/cli.js +32 -17
- package/dist/corrections.js +10 -1
- package/dist/orchestrate/accept.js +6 -4
- package/dist/orchestrate/command.js +44 -15
- package/dist/orchestrate/continue.js +10 -2
- package/dist/orchestrate/cycle.js +12 -2
- package/dist/orchestrate/launch.js +1 -1
- package/dist/orchestrate/spec.js +14 -0
- package/dist/orchestrate/waits.js +120 -0
- package/dist/plan.js +3 -3
- package/package.json +1 -1
- package/skills/lead/SKILL.md +12 -9
- package/templates/acceptation-runner.mjs +15 -3
- package/templates/orchestrate/spec.md +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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(${
|
|
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(
|
|
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: ${
|
|
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. */
|
package/dist/corrections.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
1426
|
-
|
|
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)
|
package/dist/orchestrate/spec.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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".
|