@sylad/cadence 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +59 -24
- package/bin/cadence.js +1 -1
- package/dist/audit.js +21 -1
- package/dist/cli.js +25 -7
- package/dist/corrections.js +8 -4
- package/dist/lead.js +6 -3
- package/dist/orchestrate/command.js +89 -12
- package/dist/orchestrate/cycle.js +66 -31
- package/dist/orchestrate/snapshot.js +14 -0
- package/dist/plan.js +46 -1
- package/dist/session.js +3 -2
- package/package.json +1 -1
- package/skills/lead/SKILL.md +2 -3
- package/templates/orchestrate/fix.md +8 -0
- package/templates/orchestrate/implement.md +8 -0
|
@@ -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.5.1",
|
|
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.5.1",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ the mockup (the `ux-reviewer` agent in mockup mode), see [The mockup review](#th
|
|
|
40
40
|
|
|
41
41
|
## What's new
|
|
42
42
|
|
|
43
|
-
**1.4.0**: `cadence spec reseal` accepts a sheet amended by commits of the lot, in progress or delivered, and records `ficheAmendee` (L217); the budget of a wave keeps 155 k for a fix pass plus a short review, the floor of an ordinary lot goes to 540 k and the spec allocation to 450 k, and a refused fix says `correction non jouée (budget)` in the final table (L218); a session that runs `git checkout` no longer stops the wave with a false « push détecté » (L214, L216); tests already red at the start sha no longer hand a lot back (L213, L215). **1.3.0**: the spec step's threshold is narrowed to `screen: page` after the measure of 2026-10-12 (no row of the success table reached, about 570 k tokens per lot): `component` and `journey` lots follow the ordinary path again (L176, L209, L210); `cadence spec reseal … --porteur <lot>` re-seals the unstable sealed test of a delivered lot whose contract does not change (L212); the session cap shown and followed is the human's (`CADENCE_MAX_SESSIONS`, L208); `cadence orchestrate --waits` measures slot waits (L206); a third correction pass when the review converges (L205); `"e2e": true` acceptations stay out of the unit runner (L203); a dropped lot fully reverted no longer blocks `npm run service` (L207). **1.2.0**: the spec step reaches lots without a screen: a lot that declares `spec: true` (`raf add --spec`, `raf spec`) gets a back-end sheet of cases, rules, contracts and data (`cadence spec check`), reviewed before any code, then acceptance tests locked before the implementation, and `raf screen` lets a plan already written adopt the step; `orchestrate.accept` may be a list of runners (front and back), each run under its own root; `cadence spec reseal <lot> <test> --reason` is the lead's way out when a sealed test contradicts its unchanged sheet; `lead.ux: lead` lets the lead form the UX verdict itself; `cadence lead tour --depth n` looks for projects deeper in the folder; the budgets of a wave are recalibrated on the measured runs (750 k for an ordinary lot of 0.5 to 1 day, 1.3 M for a lot with a screen); the code in service on the cadence repository is a separate worktree, updated only by `npm run service`, so a wave can no longer put unreviewed code in service (L185–L189, L191, L192, L194–L198, L200, L201). **1.1.0**: the spec step of `cadence orchestrate`: a lot that declares `screen:` gets a machine-readable screen sheet (`cadence spec check|qa`), a mockup reviewed against it, acceptance tests committed red and locked before the implementation, and briefs that carry the sheet and rule → test → green to `implement`, `fix` and the code review; the news entry gains a *Hypothèses* section (L166, L169–L174, L177–L184). **1.0.0**: the first major release, a stable contract: the commands and options, the keys of `cadence.yaml`, the plan file and the names of the skills and agents follow semantic versioning, and the README's *Stability* section lists what is guaranteed and what is not (L164). **0.28.0**: `orchestrate.precheck: local` runs the « deliverable already present? » pre-check of `cadence orchestrate` on the local model (`claude-local`, Ollama), off the Anthropic quota, Sonnet staying the default and the fallback (L146); the `qa-reviewer` agent walks only the pages a lot touched, with a **Scope** line in its report (L123); the sub-tasks a review closes are also read in a lot's neighbouring repositories (L156) and a commit's sub-task list is read with the plan's left guard (L155); an unstable test is fixed (L90). **0.27.1**: `cadence orchestrate` no longer leaves a session's child processes alive: a dev server started in its own process group, or orphaned by a shell that exits (`nohup srv &`), is killed with the session through its process tree and its `CADENCE_SESSION` mark, a tmux server, `screen`, `gpg-agent` or `dirmngr` started on demand being spared; a session that exits with code 0 while a descendant holds its output open is no longer reported as « délai dépassé » (L83); the `ux-reviewer` and `qa-reviewer` agents carry the briefs' Playwright rule outside `cadence orchestrate` (L84). **0.27.0**: a compliant review of `cadence orchestrate` settles the open sub-tasks that commits of the lot cite (`feat(L3/t2): …`, comma lists included), closed in the same plan commit as the verdict or, on a read-only plan, returned as `[sous-tâche clore]` proposals, so `raf done` no longer refuses a lot over sub-tasks whose work is done; on a foreign-format plan the ids are read as the plan reads them (L82); the test suite stays green with two full suites running in parallel (L150). **0.26.0**: `cadence orchestrate` raises the floor of a lot's budget from 200 k to 450 k tokens, enough to pay a write pass, its review, one fix pass and the short review that follows, so a lot of 0.5 or 1 day is no longer handed back at its second or third pass; a `--continue` wave draws fewer lots as a result (L128). **0.25.0**: the project line of the plans' progress in the `cadence-hud` band names the lots in progress after their count, three at most then `…` (L153, `cadence-hud` 0.4.2); the progress follows the folder the session was launched from, so a `cd` into a sub-project no longer shrinks the band, and the session context keeps the `session.start` folder (L154). **0.24.0**: `cadence orchestrate --drop <project:lot>` and `--stop-after-current` steer a live wave from another terminal, `--resume --drop` removes a lot before replaying a stopped wave (L79); the final table of a wave says « livrable jusqu'à <sha> » for a ready lot stacked under a lot handed back, with `git push origin <sha>:main && cadence deliver --sha <sha>` (L76); a version commit (version fields, `CHANGELOG.md`, the README's « What's new ») no longer asks for a new code review (L72). **0.23.0**: the progress bar of each project in the `cadence-hud` band is coloured by the share of lots done, red below 33 %, orange below 66 %, green beyond (L152, `cadence-hud` 0.4.1); `cadence deliver` under a wave lock refuses only when a lot of the wave still works in that very repository, a linked worktree included (L127). **0.22.0**: `cadence orchestrate --continue` draws the next ready lot of the plan itself, in the declared priority, until the budget, the time window or a question stops it (L147); `cadence session context` prints the context of the session, which the `cadence-hud` band publishes, and the `lead` skill chains lots without the human while it stays under 60 % (L148); the band shows the progress of each project's plan, and `cadence lead tour --json` gains `progress` (L149, `cadence-hud` 0.4.0); the final review of a lot is always played, the lot's own budget bounding the writing passes only, with 65 k tokens reserved for it (L145); the cause « tests rouges après … » names the pass whose tests were red (L151). **0.21.0**: `cadence orchestrate --status` and `cadence lead tour` print the free session slots, which the `lead` skill reads (L141); `orchestrate.effort` sets the effort level of each pass of a wave (L137); `docs.sync` makes `raf check`, `lead tour` and the review brief report a document that does not follow the code (L143); `docs.articles` opens, after a green `cadence deliver`, a lot `Article <project> à rafraîchir` in the neighbouring repository (L144). **0.20.0**: `cadence-hud` no longer misleads once the work is over: `/hud cmd` lists the background commands it counts (id, tool, origin, age, name), any other argument answers `argument inconnu` instead of hiding the band, commands with no end seen for an hour or no record go to a grey `N cmd sans fin vue`, a finished subagent takes its commands with it, two races that left a `1 cmd` counted forever are closed, and a finished wave shows grey without its budget then disappears after 30 minutes (L142); the `qa-reviewer` and `ux-reviewer` agents are read-only for real, their frontmatter listing Read, Grep, Glob, Bash and the Playwright tools (L140). **0.19.1**: `cadence-hud`'s `N cmd` counter goes back down when a Monitor expires, when the background commands a subagent started end with it, and at session start or plugin reload (L135); its `tool.call` and `prompt.submit` guard hooks carry a `.catch` on registration, which clears the `claude plugin validate` warning (L136). **0.19.0**: `cadence orchestrate` lifts a repository's lock and `pre-push` guard as soon as every lot of the wave that touches it is finished, so `cadence deliver` works there while the wave continues elsewhere (L132); a review that leaves a repository dirty hands back that lot only, suspends the lots still queued in that repository and keeps the other repositories running, its verdict reported instead of lost (L133); `cadence session close` re-run without a `session start` keeps the window since the last opening (L131); `cadence-hud` draws the lot rows of a wave in aligned columns on every surface (L134). **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
43
|
+
**1.5.1**: 1.5.0 was tagged but never published (a test of `session --all` was red on the GitHub runner, a fault of the test and not of the tool, L224); 1.5.1 publishes it with what followed: one UX review per lot (L221), the replay of a lot handed back for its budget (L223), a self-check list in the briefs (L222), and a `tests.yml` workflow that plays the suite on the publish job's environment. **1.5.0**: `gate: { count: N, spec: true }` or `{ count: N, screen: page }` on a lot is a drawing condition added to `after`: until N `done` lots carry the criterion, `raf now`, `session start` and `lead tour` show the lot as waiting with its progress, `cadence orchestrate` refuses it and `--continue` never draws it (L219); `raf todo <id> [--reason]` puts a started lot without work commits back to `todo` with a dated note (L220). **1.4.0**: `cadence spec reseal` accepts a sheet amended by commits of the lot, in progress or delivered, and records `ficheAmendee` (L217); the budget of a wave keeps 155 k for a fix pass plus a short review, the floor of an ordinary lot goes to 540 k and the spec allocation to 450 k, and a refused fix says `correction non jouée (budget)` in the final table (L218); a session that runs `git checkout` no longer stops the wave with a false « push détecté » (L214, L216); tests already red at the start sha no longer hand a lot back (L213, L215). **1.3.0**: the spec step's threshold is narrowed to `screen: page` after the measure of 2026-10-12 (no row of the success table reached, about 570 k tokens per lot): `component` and `journey` lots follow the ordinary path again (L176, L209, L210); `cadence spec reseal … --porteur <lot>` re-seals the unstable sealed test of a delivered lot whose contract does not change (L212); the session cap shown and followed is the human's (`CADENCE_MAX_SESSIONS`, L208); `cadence orchestrate --waits` measures slot waits (L206); a third correction pass when the review converges (L205); `"e2e": true` acceptations stay out of the unit runner (L203); a dropped lot fully reverted no longer blocks `npm run service` (L207). **1.2.0**: the spec step reaches lots without a screen: a lot that declares `spec: true` (`raf add --spec`, `raf spec`) gets a back-end sheet of cases, rules, contracts and data (`cadence spec check`), reviewed before any code, then acceptance tests locked before the implementation, and `raf screen` lets a plan already written adopt the step; `orchestrate.accept` may be a list of runners (front and back), each run under its own root; `cadence spec reseal <lot> <test> --reason` is the lead's way out when a sealed test contradicts its unchanged sheet; `lead.ux: lead` lets the lead form the UX verdict itself; `cadence lead tour --depth n` looks for projects deeper in the folder; the budgets of a wave are recalibrated on the measured runs (750 k for an ordinary lot of 0.5 to 1 day, 1.3 M for a lot with a screen); the code in service on the cadence repository is a separate worktree, updated only by `npm run service`, so a wave can no longer put unreviewed code in service (L185–L189, L191, L192, L194–L198, L200, L201). **1.1.0**: the spec step of `cadence orchestrate`: a lot that declares `screen:` gets a machine-readable screen sheet (`cadence spec check|qa`), a mockup reviewed against it, acceptance tests committed red and locked before the implementation, and briefs that carry the sheet and rule → test → green to `implement`, `fix` and the code review; the news entry gains a *Hypothèses* section (L166, L169–L174, L177–L184). **1.0.0**: the first major release, a stable contract: the commands and options, the keys of `cadence.yaml`, the plan file and the names of the skills and agents follow semantic versioning, and the README's *Stability* section lists what is guaranteed and what is not (L164). **0.28.0**: `orchestrate.precheck: local` runs the « deliverable already present? » pre-check of `cadence orchestrate` on the local model (`claude-local`, Ollama), off the Anthropic quota, Sonnet staying the default and the fallback (L146); the `qa-reviewer` agent walks only the pages a lot touched, with a **Scope** line in its report (L123); the sub-tasks a review closes are also read in a lot's neighbouring repositories (L156) and a commit's sub-task list is read with the plan's left guard (L155); an unstable test is fixed (L90). **0.27.1**: `cadence orchestrate` no longer leaves a session's child processes alive: a dev server started in its own process group, or orphaned by a shell that exits (`nohup srv &`), is killed with the session through its process tree and its `CADENCE_SESSION` mark, a tmux server, `screen`, `gpg-agent` or `dirmngr` started on demand being spared; a session that exits with code 0 while a descendant holds its output open is no longer reported as « délai dépassé » (L83); the `ux-reviewer` and `qa-reviewer` agents carry the briefs' Playwright rule outside `cadence orchestrate` (L84). **0.27.0**: a compliant review of `cadence orchestrate` settles the open sub-tasks that commits of the lot cite (`feat(L3/t2): …`, comma lists included), closed in the same plan commit as the verdict or, on a read-only plan, returned as `[sous-tâche clore]` proposals, so `raf done` no longer refuses a lot over sub-tasks whose work is done; on a foreign-format plan the ids are read as the plan reads them (L82); the test suite stays green with two full suites running in parallel (L150). **0.26.0**: `cadence orchestrate` raises the floor of a lot's budget from 200 k to 450 k tokens, enough to pay a write pass, its review, one fix pass and the short review that follows, so a lot of 0.5 or 1 day is no longer handed back at its second or third pass; a `--continue` wave draws fewer lots as a result (L128). **0.25.0**: the project line of the plans' progress in the `cadence-hud` band names the lots in progress after their count, three at most then `…` (L153, `cadence-hud` 0.4.2); the progress follows the folder the session was launched from, so a `cd` into a sub-project no longer shrinks the band, and the session context keeps the `session.start` folder (L154). **0.24.0**: `cadence orchestrate --drop <project:lot>` and `--stop-after-current` steer a live wave from another terminal, `--resume --drop` removes a lot before replaying a stopped wave (L79); the final table of a wave says « livrable jusqu'à <sha> » for a ready lot stacked under a lot handed back, with `git push origin <sha>:main && cadence deliver --sha <sha>` (L76); a version commit (version fields, `CHANGELOG.md`, the README's « What's new ») no longer asks for a new code review (L72). **0.23.0**: the progress bar of each project in the `cadence-hud` band is coloured by the share of lots done, red below 33 %, orange below 66 %, green beyond (L152, `cadence-hud` 0.4.1); `cadence deliver` under a wave lock refuses only when a lot of the wave still works in that very repository, a linked worktree included (L127). **0.22.0**: `cadence orchestrate --continue` draws the next ready lot of the plan itself, in the declared priority, until the budget, the time window or a question stops it (L147); `cadence session context` prints the context of the session, which the `cadence-hud` band publishes, and the `lead` skill chains lots without the human while it stays under 60 % (L148); the band shows the progress of each project's plan, and `cadence lead tour --json` gains `progress` (L149, `cadence-hud` 0.4.0); the final review of a lot is always played, the lot's own budget bounding the writing passes only, with 65 k tokens reserved for it (L145); the cause « tests rouges après … » names the pass whose tests were red (L151). **0.21.0**: `cadence orchestrate --status` and `cadence lead tour` print the free session slots, which the `lead` skill reads (L141); `orchestrate.effort` sets the effort level of each pass of a wave (L137); `docs.sync` makes `raf check`, `lead tour` and the review brief report a document that does not follow the code (L143); `docs.articles` opens, after a green `cadence deliver`, a lot `Article <project> à rafraîchir` in the neighbouring repository (L144). **0.20.0**: `cadence-hud` no longer misleads once the work is over: `/hud cmd` lists the background commands it counts (id, tool, origin, age, name), any other argument answers `argument inconnu` instead of hiding the band, commands with no end seen for an hour or no record go to a grey `N cmd sans fin vue`, a finished subagent takes its commands with it, two races that left a `1 cmd` counted forever are closed, and a finished wave shows grey without its budget then disappears after 30 minutes (L142); the `qa-reviewer` and `ux-reviewer` agents are read-only for real, their frontmatter listing Read, Grep, Glob, Bash and the Playwright tools (L140). **0.19.1**: `cadence-hud`'s `N cmd` counter goes back down when a Monitor expires, when the background commands a subagent started end with it, and at session start or plugin reload (L135); its `tool.call` and `prompt.submit` guard hooks carry a `.catch` on registration, which clears the `claude plugin validate` warning (L136). **0.19.0**: `cadence orchestrate` lifts a repository's lock and `pre-push` guard as soon as every lot of the wave that touches it is finished, so `cadence deliver` works there while the wave continues elsewhere (L132); a review that leaves a repository dirty hands back that lot only, suspends the lots still queued in that repository and keeps the other repositories running, its verdict reported instead of lost (L133); `cadence session close` re-run without a `session start` keeps the window since the last opening (L131); `cadence-hud` draws the lot rows of a wave in aligned columns on every surface (L134). **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
44
44
|
stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
|
|
45
45
|
(sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
|
|
46
46
|
terminal cells and kept to ASCII; `publish.yml` on the v5 actions. **0.12.0**: the `cadence-hud` plugin (a status band above the Claude Code prompt),
|
|
@@ -108,6 +108,7 @@ raf gantt # docs/plan/gantt.html
|
|
|
108
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
|
+
| `raf todo <id> [--reason text]` | put a started lot back to `todo` (`doing` → `todo`, `started:` removed, dated note « remis en attente : reason »): refused when work commits cite the lot (plan-only and version commits do not count, as `raf commits`), when the lot is not `doing` (a `done` or `dropped` lot is reopened by hand), or on a sub-task; sub-tasks and other keys are left as they are |
|
|
111
112
|
| `raf note <id> "text"` | dated note — keep decisions next to the work |
|
|
112
113
|
| `raf show <id> [--notes]` | one lot: status, dates, `after`, public title, dated notes in order, then the counted commits (as `raf commits`, neighbouring repositories included); `--notes` prints the notes alone |
|
|
113
114
|
| `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first; a lot that declares `repos:` ([neighbouring repositories](#a-lot-whose-work-is-in-a-neighbouring-repository)) gets, after them, one `dépôt <path> :` section per neighbour with the commits there that cite the lot and fall within the lot's period (`started` to `finished`) and, when the lot declares a `cite:` for that neighbour, contain that string (`aucun commit du lot`, or `introuvable` for a path that is not there) |
|
|
@@ -161,6 +162,7 @@ lots:
|
|
|
161
162
|
quickwin: false
|
|
162
163
|
visible: true # user-facing: a news entry is expected when done
|
|
163
164
|
after: [L0]
|
|
165
|
+
gate: { count: 8, spec: true } # optional: not before 8 lots with `spec: true` are done (or `screen: page`)
|
|
164
166
|
created: 2026-09-28
|
|
165
167
|
started: 2026-09-29
|
|
166
168
|
notes:
|
|
@@ -226,9 +228,10 @@ plan:
|
|
|
226
228
|
```
|
|
227
229
|
|
|
228
230
|
- Fields: `id`, `title`, `status`, `estimate`, `quickwin`, `visible`, `screen`, `spec`, `public`, `every`, `last`, `after`, `created`, `started`,
|
|
229
|
-
`finished`, `notes`, `repos`, `parent`; one left out is read under its own name. A timestamp counts for
|
|
231
|
+
`finished`, `notes`, `repos`, `parent`, `gate`; one left out is read under its own name. A timestamp counts for
|
|
230
232
|
its day; a note written as plain text is one note.
|
|
231
|
-
- `spec` is the
|
|
233
|
+
- `gate` (L219) is a drawing condition added to `after`: `gate: { count: N, spec: true }` or `gate: { count: N, screen: page }` (N an integer ≥ 1, one criterion). It is written by hand in the plan (no `raf add` option). The count is the number of `done` lots of the same plan that carry the criterion (`todo`, `doing`, `dropped` lots and sub-tasks do not count; recurring lots do). Until the count reaches N, a `todo` lot is *waiting*, never *ready*: `raf now` lists it on a line `en attente de condition de tirage : L190 (2/8 lots spec: true faits)` (the existing `en attente de dépendances : …` line stays for lots waiting on `after`), `cadence session start` adds an `En attente` section (nothing when no lot is gated) and leaves the lot out of `À suivre` and of the proposals, `cadence lead tour` skips it for `prochain` and adds `attend L190` to the project's line (and `waiting` to `--json`, only when a lot waits), `cadence orchestrate` refuses it before any action (`a:L190 : condition de tirage non remplie : 2/8 lots spec: true faits`) and `--continue` never draws it, and `raf show` prints `gate : 8 lots spec: true faits`. Once the count is reached the lot is ready like any other (the field stays in the plan); a lot already `doing` is not refused for its condition. A malformed `gate` is reported by `raf check` (`gate invalide : …`) and the lot is never drawable. A plan without `gate` behaves exactly as before; `after` read by the Gantt scheduling is unchanged.
|
|
234
|
+
- `spec` is the exception to « a left-out field is read under its own name »: a read-only plan's `spec:` is often the path of a spec document, so it is read only when `plan.fields.spec` maps it, and then only `true` counts (any other value is neither an error nor a spec step). Natively, `spec:` stays `true` or `false` (L198). Consequence for a `gate: { count: N, spec: true }` in a read-only plan: it counts only lots whose `spec` is mapped by `plan.fields.spec`; without that mapping the count stays 0 and the lot waits for good, with no `raf check` problem. `gate: { count: N, screen: page }` has no such condition.
|
|
232
235
|
- `parent`: an entry `B33/t1-fusion` whose parent is `B33` becomes the sub-task `t1-fusion` of `B33`.
|
|
233
236
|
- Ids need no prefix: a commit belongs to a lot when its message cites one of the plan's ids as a
|
|
234
237
|
whole word (`E-A2`, `NC2.4`, `B33/t1-fusion`).
|
|
@@ -611,12 +614,13 @@ beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second
|
|
|
611
614
|
| `en cours` | ids of the lots in progress; `(aucune activité)` when it has none at all, `(silencieux Nj)` when the lot's last activity (commit, note or start) is more than `--idle` days old (default 3) |
|
|
612
615
|
| `dérive` | the gaps `raf check` reports (a lot whose commits leave a document behind, [`docs.sync`](#documentation-that-follows-the-code), included), counted and the first two shown |
|
|
613
616
|
| `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters |
|
|
614
|
-
| `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters |
|
|
617
|
+
| `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters; a lot whose [`gate`](#plan-file) is not met is not ready |
|
|
618
|
+
| `attend` | only when a lot waits on its `gate`: `attend L190` |
|
|
615
619
|
| `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
620
|
|
|
617
621
|
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
622
|
|
|
619
|
-
`--json` prints the same content as an array of objects (`project`, `doing`, `drift`, `notes`,
|
|
623
|
+
`--json` prints the same content as an array of objects (`project`, `doing`, `drift`, `notes`, `waiting` (only when a lot waits on its `gate`),
|
|
620
624
|
`next`, `repo`, and `error` when the project could not be read), plus `progress` — `{ done, doing, todo, added7 }`,
|
|
621
625
|
the lots done / in progress / to do (dropped and recurring lots left out) and those created in the last 7 days (L149), which
|
|
622
626
|
[`cadence-hud`](plugins/cadence-hud/README.md) draws as one progress bar per project. Projects come in the order of the
|
|
@@ -633,7 +637,7 @@ no plan. It exits 0 even when a project is in error — that project's line read
|
|
|
633
637
|
|
|
634
638
|
## corrections
|
|
635
639
|
|
|
636
|
-
`cadence corrections [folder] [--classify [--model m]] [--cache file] [--sample n] [--until date] [--json]` measures how often a wave
|
|
640
|
+
`cadence corrections [folder] [--classify [--model m]] [--cache file] [--sample n] [--until date] [--since date] [--json]` measures how often a wave
|
|
637
641
|
has to correct itself. From the folder a wave was launched from (default: the current one) it reads the journals
|
|
638
642
|
`<folder>/.cadence/runs/<wave>/<project>--<lot>.json` of **every project** and prints:
|
|
639
643
|
|
|
@@ -651,7 +655,7 @@ The classification is cached in `<folder>/.cadence/corrections.json` (or `--cach
|
|
|
651
655
|
new passes, and a pass the model could not answer stays out of the cache. `--sample n` prints `n` passes with findings spread
|
|
652
656
|
evenly over the sorted keys, with their findings and the model's cause, to be read by hand; a `manual` field written
|
|
653
657
|
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
|
-
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.
|
|
658
|
+
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. `--since date` (ISO, same format) keeps only the lots of the waves **created after** that date (`created` in the wave's `wave.json`), **with all their passes**: a wave renders every brief from the copy of the tool frozen when it is created (`<wave>/tool/`), so the creation date of the wave, not the start of a lot, tells which brief the lot was given. A `--continue` wave created before a change of brief therefore does not give it to its lots, even those that start after the change (a difference of two unbounded runs is wrong as soon as a wave straddles the date); the exception is a wave resumed (`--resume`) without a snapshot (older than L61) or, with `--budget project:lot=+N`, from a snapshot older than L223: it runs with the current `dist/` and templates, so it does receive the new brief. A wave with no readable `created` is left out, by `--since` as by `--until`. `--since` is strict and `--until` inclusive, so a wave created exactly at the date is on the `--until` side only and the two sets never overlap; `--since` and `--until` can be combined (a window of creation dates). Together `--until d` and `--since d` split the waves, and they add up to the unbounded total only when `d` is set while no wave is running (between two sessions, as `npm run service` requires; `cadence orchestrate --status` shows none, **and** no wave created before `d` is still resumable: `--status` lists only the live waves, an interrupted or suspended one is not shown and `--resume` would replay passes after `d`; list them with `jq -r 'select(.status != "done") | "\(.id) \(.status) \(.created)"' <folder>/.cadence/runs/*/wave.json`, and finish each before setting `d` or never resume it): a wave created before `d` that ends a pass after `d` leaves that pass in neither (`--until` does not read it, so that a published baseline does not move), and a gap between the sum and the unbounded total means `d` fell inside a wave. `--json` prints the measure as data. The command reads the journals and writes nothing but the cache.
|
|
655
659
|
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
660
|
|
|
657
661
|
## spec
|
|
@@ -919,6 +923,7 @@ cadence orchestrate --waits [--since 2026-10-12] # time spent waiting for a se
|
|
|
919
923
|
cadence orchestrate --status [<wave>] # the live waves and the repositories they hold, then the table (default: the last wave of this folder)
|
|
920
924
|
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)
|
|
921
925
|
cadence orchestrate --resume [<wave>] [--budget 1M] [--answer ol-companion:L22 "reply"] [--drop ccc:L29]
|
|
926
|
+
cadence orchestrate --resume <wave> --budget maritime:B26.4=+300k # replay a lot handed back for its budget, with the budget raised (below)
|
|
922
927
|
cadence orchestrate --drop ccc:L29 [--wave <wave>] # from another terminal: take a lot out of the running wave
|
|
923
928
|
cadence orchestrate --stop-after-current [--wave <wave>] # from another terminal: let the running sessions finish, start nothing else
|
|
924
929
|
```
|
|
@@ -939,6 +944,24 @@ handed back instead of being replayed (an unknown or finished lot is refused, ex
|
|
|
939
944
|
this way is left out of the resume checks (repository lock, `.nvmrc` Node, dirty tree): only the lots that will be replayed can
|
|
940
945
|
refuse the resume, and a refusal writes nothing to `control.log`.
|
|
941
946
|
`--resume --stop-after-current` is refused (exit 2): resuming contradicts stopping.
|
|
947
|
+
|
|
948
|
+
**`--resume --budget <project>:<lot>=+N` — replaying a lot handed back for its budget (L223)**: a lot handed back with
|
|
949
|
+
`correction non jouée (budget)` or `budget du lot atteint` kept its findings but not its budget, and a plain `--resume` does not replay a
|
|
950
|
+
finished lot. `--budget maritime:B26.4=+300k` (the project may be left out when only one project carries the lot; the option is
|
|
951
|
+
repeatable for several lots, and `--budget 1M` still raises the wave's budget — the `=` tells them apart) puts the lot back in play:
|
|
952
|
+
its budget becomes **what its sessions already counted plus N** (849 k spent of 750 k, `=+300k`: 1 149 000), its outcome is cleared,
|
|
953
|
+
and the step that was not played (`fix`, `implement`, `spec` or `accept` — the only ones a lot budget bounds; a review is never handed
|
|
954
|
+
back for it) runs in a **new session** (never `claude --resume` on the earlier one) with the findings the lot kept, then the normal cycle
|
|
955
|
+
goes on: review, verdict recorded with the sha re-read and the plan committed. Nothing is recorded twice, since a lot handed back for
|
|
956
|
+
its budget never had a compliant review. The reserve rule is unchanged: a `fix` leaves only if spent + 155 000 < the new budget,
|
|
957
|
+
so N of at most 155 000 on a `fix` is refused, and a budget still too small hands the lot back again with the same text. The wave's own budget
|
|
958
|
+
and quota still apply (exhausted: the lot stays suspended, exit 3). A wave that is `done` is resumed this way when a lot is named; without the
|
|
959
|
+
option it is still refused. The named lot undergoes the checks of a live lot (dirty tree, repository held by another orchestration, `.nvmrc`
|
|
960
|
+
Node). Refused with exit 2, before anything is written: a malformed `projet:lot=+N` or an option without `--resume`, an unknown or
|
|
961
|
+
ambiguous lot (`projet:lot` is then asked), a lot that is not handed back for its budget with a write step to replay, a lot named twice
|
|
962
|
+
(`p:L` then `L` counts) or also named by `--drop` or `--answer`.
|
|
963
|
+
A wave runs on its own snapshot of the tool, but one launched before L223 would refuse `projet:lot=+N` as an invalid budget:
|
|
964
|
+
for such a wave (B26.4's included) the resume that names a lot runs on the current `dist/` instead, with a warning on the error output.
|
|
942
965
|
A wave runs on its own snapshot of the tool: one launched before L79 never reads `control.log`, so the request is written but the
|
|
943
966
|
command warns `ne lit pas les demandes de contrôle` on the error output.
|
|
944
967
|
|
|
@@ -1003,16 +1026,19 @@ task « vague `<id>` : `<project>:<lot>`, … », starts the wave with `--wave <
|
|
|
1003
1026
|
The Claude Code status band is the `cadence-hud` plugin.
|
|
1004
1027
|
|
|
1005
1028
|
**Cycle of a lot**: preconditions (clean tracked files, lot `todo` or `doing`, dependencies met) →
|
|
1006
|
-
`raf start` (committed alone) → implementation → **
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
"ready to deliver". Not compliant → a correction in a new session,
|
|
1010
|
-
|
|
1029
|
+
`raf start` (committed alone) → implementation → **code review** → compliant (no blocking, no
|
|
1030
|
+
major finding) → **UX review** if the lot is `visible` and the app is declared, **once**, on the code the review
|
|
1031
|
+
found compliant (L221) → `raf review` is recorded by the orchestrator with the sha the *code* review read (the UX
|
|
1032
|
+
verdict stays in the lot state, `uxVerdict`), then "ready to deliver". Not compliant → a correction in a new session,
|
|
1033
|
+
then a new code review, with no UX session in between; a UX review that finds a blocking or major point sends the lot
|
|
1034
|
+
to a correction (which receives the UX findings), then the code review, then the UX review again. **Two passes at most** (a third when the review converges: the count of blocking + major findings dropped
|
|
1011
1035
|
since the previous review and exactly one is left; the lot budget still applies), then the lot goes back to
|
|
1012
1036
|
you with the findings. A small lot (`estimate` ≤ 0.5 or `quickwin`) gets one single Opus pass for code and
|
|
1013
1037
|
usability (code only when the lot is not `visible`). Failing tests (reported red, or red when
|
|
1014
1038
|
`orchestrate.test` is run) go straight to a correction.
|
|
1015
1039
|
|
|
1040
|
+
**One UX review per lot (L221)**: the UX review of a `visible` lot used to run before the code review and again after every correction (finance L75 on 2026-10-15: three Opus UX sessions for one useful verdict). It now runs once, as the last step, on the code the review found compliant: after the single pass on minor findings and its short review when there is one. Without `orchestrate.ux`, or when the app does not start (port taken, silent app), no UX session is played: the lot concludes on the compliant code review and keeps its note (« UX à faire par le lead », « UX non vérifiée : … »). The lot budget bounds writing, never this review; an exhausted wave budget suspends it like any session. Every write step (implementation, correction) clears the lot's UX state (`ux`, `uxVerdict`, UX findings), so a stale UX verdict never sends a compliant code review back to a correction. A resumed wave whose state says `next = ux` (written before this change) replays the code review first unless the last compliant one read the current `HEAD`. The recorded `raf review` verdict and sha are the code review's; a commit made by someone else since that review hands the lot back, as before. A small lot and a lot with no screen keep their single review. The measure before/after is in [`docs/mesures/ux-une-fois-2026-10-15.md`](docs/mesures/ux-une-fois-2026-10-15.md).
|
|
1041
|
+
|
|
1016
1042
|
**Tests already red at the start (L213)**: a lot is no longer handed back « tests rouges » for failures that were there before it. When `orchestrate.test` fails (or the session reports red tests), the program reads the failing tests from the runner's output (vitest/jest `FAIL`/`●`, pytest `FAILED`, go `--- FAIL:`, TAP `not ok`), replays the same command at the sha the lot started from (the parent of the lot's oldest work commit, plan-only commits left out, else the `HEAD` before its first session) in a throwaway `git worktree` outside the repository, and compares: if every failing test was already red, it is not a defect of the lot — no correction pass, the code review takes place, the reviewer is told, and the final table lists them apart (`déjà rouge au départ (<sha>) : <test>`, 30 at most then a count). The worktree gets links to the repository's ignored folders at any depth, such as `.venv` and, in a monorepo, `packages/*/node_modules`, when their parent exists at that sha; build outputs such as `dist` or `coverage` are left out. Each `node_modules` is rebuilt entry by entry, every entry linked to the live one except the workspace links of npm, pnpm and yarn (`node_modules/@x/shared -> ../../packages/shared`, a link to code of the repository outside any `node_modules`), which are remade towards the worktree's own copy so that the start sha is tested against its own sources, not the lot's (a workspace package that does not exist at the start sha gets no link). Known limit: only `node_modules` links are remade; whatever points into the live repository by an absolute path, such as an editable Python install (`.venv` with a `.pth` file naming the live `src`, `pip install -e`) or an absolute symlink in another ignored folder, still runs the lot's code in the baseline. Likewise, a workspace package consumed through its build output (`main: dist/...`) cannot load in the baseline worktree: output dirs are not linked and nothing is built there, so such a monorepo keeps the pre-L213 « tests rouges » outcome. A lot resumed in a later wave keeps the same baseline: the sha is the parent of its oldest work commit, not the `HEAD` of the new wave, so a failure the lot itself introduced in an earlier wave is never excused. A test red now and not at the start stays a blocking finding, and only that one is given to the correction (the others are counted as « already red »). When the output names no test, or the baseline cannot be run, the tests stay red as before (no comparison is guessed). A failure the output does not tie to a test (unhandled error, a spec file that fails to load such as jest « Test suite failed to run » or vitest `FAIL a.test.ts [ a.test.ts ]`, coverage threshold) keeps the tests red. Without `orchestrate.test` the program cannot replay anything: a session that reports green tests and lists under `echecsDeBase` the failures it saw at the start sha gets them listed apart, marked « non vérifié » in the table, and the review takes place; a session that reports the tests red stays red, the brief asking it not to report red for base failures alone — unless (L215) the report says so in a structured way: with `vert: false`, a non-empty `echecsDeBase` and an empty `nouveauxEchecs` (the list of tests red now that were not red at the start sha), `vert: false` is not a reason to correct, the lot carries a « non vérifié » warning, the reviewer is told that the baseline is unverified and the review takes place. A non-empty `nouveauxEchecs` with `vert: false` triggers a correction pass (the new failures are in the finding); an absent `nouveauxEchecs`, or an empty `echecsDeBase`, keeps the correction. The program never guesses from the text of the result (« 24 failed »). The `implement` and `fix` briefs ask for `echecsDeBase` and `nouveauxEchecs`.
|
|
1017
1043
|
|
|
1018
1044
|
**Review sized to the lot (L108)**: a lot whose `estimate` is ≤ `orchestrate.review.threshold` (0.25 day by default)
|
|
@@ -1056,7 +1082,7 @@ lot that already has commits is never pre-checked (resuming it is legitimate). `
|
|
|
1056
1082
|
### The spec step of a lot with `screen:`
|
|
1057
1083
|
|
|
1058
1084
|
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**
|
|
1059
|
-
the first implementation: `precheck → spec → maquette → accept → implement →
|
|
1085
|
+
the first implementation: `precheck → spec → maquette → accept → implement → review → ux` (`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
|
|
1060
1086
|
(`spec (sonnet)`), its command line and its brief `templates/orchestrate/spec.md`.
|
|
1061
1087
|
|
|
1062
1088
|
- **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.
|
|
@@ -1101,7 +1127,7 @@ A lot that changes a behaviour but has no screen (`raf add … --spec`, or `raf
|
|
|
1101
1127
|
|
|
1102
1128
|
### The mockup review: before any code
|
|
1103
1129
|
|
|
1104
|
-
After a valid spec, a lot with a sheet gets a UX review of its mockup, `maquette` (L172), unless `cadence.yaml` sets `orchestrate.maquette: false` (default `true`): `precheck → spec → maquette → accept → implement →
|
|
1130
|
+
After a valid spec, a lot with a sheet gets a UX review of its mockup, `maquette` (L172), unless `cadence.yaml` sets `orchestrate.maquette: false` (default `true`): `precheck → spec → maquette → accept → implement → review → ux`.
|
|
1105
1131
|
`--dry-run` lists the step (`maquette (opus)`, the `review.full` model), its `ux-reviewer` command line and its brief `templates/orchestrate/maquette.md`.
|
|
1106
1132
|
|
|
1107
1133
|
- **The session**: the `ux-reviewer` agent in **mockup mode**, read-only, Playwright loaded, a fresh session. It opens `maquette.html` as a local file (no application, no server), looks at the captures
|
|
@@ -1117,7 +1143,7 @@ After a valid spec, a lot with a sheet gets a UX review of its mockup, `maquette
|
|
|
1117
1143
|
### The accept step: red acceptance tests and their lock
|
|
1118
1144
|
|
|
1119
1145
|
After a valid spec, and only when `cadence.yaml` declares `orchestrate.accept` (the project's **dedicated command**, or a list of them, which run *only* the acceptance tests of one lot), a lot with a sheet gets one more
|
|
1120
|
-
session, `accept` (L173): `precheck → spec → maquette → accept → implement →
|
|
1146
|
+
session, `accept` (L173): `precheck → spec → maquette → accept → implement → review → ux`. Without `orchestrate.accept` the step is skipped, with a warning on the lot (`étape accept ignorée …`), and the lot goes from `spec` to `implement` as before.
|
|
1121
1147
|
`--dry-run` lists the step (`accept (sonnet)`) and its brief `templates/orchestrate/accept.md`.
|
|
1122
1148
|
|
|
1123
1149
|
- **The session**: Sonnet, the `acceptance-writer` agent, a session other than the implementer's. Its brief gives it the sheet, the mockup and how to run the project's tests (the dedicated command, and the runner template below) — not the lot's objective, notes or
|
|
@@ -1160,6 +1186,7 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
|
|
|
1160
1186
|
Once the sheet exists, the steps after it all read it (L174, `src/orchestrate/aval.ts`, the `{{fiche}}` marker of the templates `implement.md`, `fix.md`, `fix-minors.md`, `review.md`, `review-small.md` and `ux.md`). A lot without a sheet (no `screen:`, or under the threshold) keeps exactly the briefs it had.
|
|
1161
1187
|
|
|
1162
1188
|
- **`implement` and `fix`** get the path of the sheet and of the mockup, and **one line per rule** (`- R3 [erreur, hypothese] statement. Message: « … ». Tests: « R3 — … »`). The session makes the acceptance tests green **without modifying them** (they are named when the `accept` step ran; the lock above hands the lot back otherwise), **the tests it writes in addition cite the rule** in their name (`R2 — …`), and it **does not invent a rule**: a case the sheet does not cover is written in the report (`nonVerifie` or `choix`), not coded. In a `fix` the sheet stays the reference: a finding that contradicts it is contested under `choix`.
|
|
1189
|
+
- **A self-check list before handing back (L222).** Both `implement.md` and `fix.md` carry the same fixed list (plain text, no marker, no new variable), taken from the recurring findings of the reviews of 2026-10-15: the usage documentation (README and architecture documentation) updated in the same commits as the behaviour; one test named `Rn — …` per rule of the sheet (one per business rule of the lot when there is no sheet); no dead code and no test that cannot fail (an assertion always true, or a test that does not exercise its rule); for a modified screen, the capture due of each state at 1440 and 390 px with a relative file name (a lot with no modified screen owes none and says so); no false comment, docstring or message. The session goes through it point by point on its own commits before it reports, and a point it cannot hold is written under `choix` or `nonVerifie` of its report: the report schema has no new field. In `fix` the list covers only what the session changed, and "fix them, and nothing else" stays. The other briefs (`fix-minors`, `spec`, `spec-back`, `accept`, `review-small`…) are unchanged. The before/after measure — correction passes per lot, replayed with `cadence corrections` (`--until <bound>` for the "before", `--since <bound>` for the "after", the bound being the time of the `npm run service` that puts the list in service) — is in [docs/mesures/autocontrole-avant-revue-2026-10-15.md](docs/mesures/autocontrole-avant-revue-2026-10-15.md).
|
|
1163
1190
|
- **The code review (`review`, `review-small`) checks rule → test → green** (`règle → test → vert`). For each `Rn` of the sheet the reviewer checks that its test exists (named after the rule), that it is green, and that the behaviour is in the diff; when an acceptance file is named, that each listed test is unchanged since the red commit (`git diff <red commit> -- <file>` empty; the brief gives the red commit). A rule without a test, an acceptance test modified since the red commit, a rule coded that is not in the sheet, and a test that is green without exercising its rule are **major** findings. The reviewer no longer rebuilds the cases from the code.
|
|
1164
1191
|
- **The UX pass becomes a conformity pass.** For a lot with a sheet, the `ux` session (the `ux-reviewer` agent, **conformity mode**) no longer audits the whole screen: it compares the running app to the mockup and to its captures `maquette-<état>-<largeur>.png`, state by state, at 1440 and 390 px, and reports the differences of rendering and ergonomics the tests do not cover, each tied to a rule or a measure. The rules of the sheet are the tests' job. Its verdict feeds `raf ux`, which stays the lead's, decided on the captures. A small visible lot (single `review-small` pass) gets the conformity instruction in that pass.
|
|
1165
1192
|
- **The `## Hypothèses` section of the news entry.** The `implement` brief asks for an entry that ends with `## Hypothèses`: one line `- Rn — …` per rule whose status is `hypothese`, with what changes if it is wrong (its `si_faux`) — it is what the lead shows for the `raf ux` decision, the choices nobody validated.
|
|
@@ -1218,13 +1245,16 @@ correction passes stays the guard against a loop; with no commit on the lot it i
|
|
|
1218
1245
|
**Compliant with minor findings**: the minors are not left for a follow-up lot. One **minors pass** runs
|
|
1219
1246
|
before concluding: a new Sonnet session with its own brief (`fix-minors.md`) fixes the minors that are right
|
|
1220
1247
|
and lists in `choix`, with the reason, the ones it rejects (it never stops to ask); it does not count among the
|
|
1221
|
-
two defect passes. A short code re-review follows (for a visible lot,
|
|
1248
|
+
two defect passes. A short code re-review follows (for a visible lot, the UX review comes after it), and
|
|
1222
1249
|
concludes even when it finds new minors, which are returned to you as proposals (no second minors pass).
|
|
1223
1250
|
When the pass makes no commit (every minor judged wrong) and HEAD has not moved since the compliant review,
|
|
1224
1251
|
the lot concludes on that original review (if HEAD moved, e.g. a resume after a cut-off session that had
|
|
1225
1252
|
committed, a short re-review of that commit runs instead): `ready`, verdict recorded with the sha that review read (`… + passe des mineurs sans commit`),
|
|
1226
|
-
the untreated minors returned as proposals.
|
|
1227
|
-
review
|
|
1253
|
+
the untreated minors returned as proposals. For a visible, non-small lot with `orchestrate.ux` declared, the
|
|
1254
|
+
final UX review plays first and the lot concludes on that same original review once the UX is compliant. Same when the
|
|
1255
|
+
budget is exhausted right after a compliant review with minors: it concludes on that review instead of staying
|
|
1256
|
+
suspended, except for such a visible lot, where the UX session needs budget and the wave stays `suspended`
|
|
1257
|
+
(budget reached) like any other session.
|
|
1228
1258
|
|
|
1229
1259
|
**Sub-tasks covered by commits (L82)**: when the review concludes compliant, the open sub-tasks of the lot that a work commit of the lot cites (`feat(L3/t2): …`, in the project or in one of the lot's neighbouring repositories; a commit that cites only the lot covers none) are closed in the same plan commit as the verdict, with the warning `sous-tâches closes : L3/t2 (sha)`, so `raf done` does not refuse the lot over finished work. A read-only plan is never written: each such sub-task is returned as a proposal `[sous-tâche clore] L3/t2 — couverte par <sha>`, for you to close with the project's tool. On a plan in a foreign format (read-only, `cadence.yaml` `plan:`), sub-task ids are read as the plan reads them: `B53/t4-ux1` covers `t4-ux1` only, not `t4`, and a list such as `feat(B53/t4-ux1,t5)` covers each of its sub-tasks. The lot id is not read inside another id (`fix(E-A2/t3,t4, A2/t1)` covers `A2/t1` only for lot `A2`), and a list may repeat the lot id (`fix(L1/t1, L1/t2,t3)` covers `t1`, `t2` and `t3`). A non-compliant review closes nothing.
|
|
1230
1260
|
|
|
@@ -1236,8 +1266,11 @@ is linked, not copied) into `.cadence/runs/<wave>/tool/` and relaunches the orch
|
|
|
1236
1266
|
parent process waits, relays the signals and exits with the same code or signal). Code, brief templates and agents
|
|
1237
1267
|
therefore come from the snapshot: a wave can orchestrate cadence itself, and a session of the wave may edit
|
|
1238
1268
|
`templates/` or rebuild `dist/` without touching the wave running. `--resume` relaunches from the snapshot of the
|
|
1239
|
-
resumed wave,
|
|
1240
|
-
package and a warning.
|
|
1269
|
+
resumed wave, not from the current `dist/`; a wave without a snapshot (started before L61) resumes with the current
|
|
1270
|
+
package and a warning. One more exception (L223): a wave whose snapshot predates L223 (its `dist/orchestrate/command.js`
|
|
1271
|
+
has no `lotBudgets`) resumed with `--budget <project>:<lot>=+N` runs on the current `dist/` and templates, with a warning
|
|
1272
|
+
on the error output, because that snapshot would refuse the option as an invalid budget; the sessions of such a resume still
|
|
1273
|
+
get `<wave>/tool/bin` first in their `PATH` (see below), so the `raf` and `cadence` they call are the snapshot's. The copy is removed with the wave folder. The wave's pid in `--status` is the relaunched process.
|
|
1241
1274
|
The sessions of the wave also get `.cadence/runs/<wave>/tool/bin` (the `raf` and `cadence` entries of the snapshot) **first in their
|
|
1242
1275
|
`PATH`**, ahead of the per-project Node below and of the usual `PATH`: the `raf` a reviewer runs (`raf commits`…) is the
|
|
1243
1276
|
snapshot's, not the installed one. A wave without a snapshot leaves the `PATH` as it is.
|
|
@@ -1350,7 +1383,7 @@ upstream ref and `git ls-remote` are compared with the "before" (a `git checkout
|
|
|
1350
1383
|
**Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
|
|
1351
1384
|
shown apart. Each lot also has its own budget, the 90th percentile of the *total* measured per lot on `.cadence/runs` (all the waves a lot went through, 250 lots) for its category (L194): an ordinary lot of up to 0.5 day gets 540 k (floor: a write pass, its review, one fix pass and the short review after it, 155 k being reserved for those last two; measured p90 374 k), 0.5 to 1 day 750 k (p90 732 k), then 570 k per day of estimate, 750 k staying the floor until that exceeds it, i.e. up to 1.3 day (p90 1.14 M for 2 days); a lot with `screen: page` gets 1.3 M whatever its estimate (`component` and `journey` keep the ordinary budget; p90 1.28 M: spec, mockup, accept, conformity UX and passes), or its ordinary budget plus 450 k beyond that; a lot with `spec: true` gets its ordinary budget plus 450 k (the back chain measured on L120: 388 k); shown by
|
|
1352
1385
|
`--dry-run` with its basis): it bounds the *writing* passes, never the reviews. A lot that spent it gets no further implementation or
|
|
1353
|
-
fix and is handed back to the lead, but its review (and UX review) is played first; a fix pass starts only if
|
|
1386
|
+
fix and is handed back to the lead, but its review (and its single UX review, once the code review is compliant) is played first; a fix pass starts only if
|
|
1354
1387
|
the remaining budget can also pay the fix and the short review that follows it (155 k tokens reserved: the p90 of a fix
|
|
1355
1388
|
pass, 65 k over 497 passes, plus the p90 of a short Opus review, 88 k over 272; measured on 2026-10-15 in the wave journals
|
|
1356
1389
|
`.cadence/runs`). A lot refused that fix is handed back with the cause `correction non jouée (budget) :` first, so the lead
|
|
@@ -1432,8 +1465,8 @@ orchestrate:
|
|
|
1432
1465
|
**`orchestrate.ux`, who starts the app**: a string is a URL if it starts with `http://` or `https://`, a launch
|
|
1433
1466
|
command otherwise; both forms behave as before (the `ux` brief gives the URL, or tells the session to start the app
|
|
1434
1467
|
with the command and stop it). The object form `{ command, url, timeout? }` makes **the program** start the app, not
|
|
1435
|
-
the UX session. Before the `ux` step (and before the `review-small` single pass of a small `visible` lot, which follows
|
|
1436
|
-
the same rule
|
|
1468
|
+
the UX session. Before the `ux` step (played once, after the compliant code review, L221) and before the `review-small` single pass of a small `visible` lot, which follows
|
|
1469
|
+
the same rule, the orchestrator:
|
|
1437
1470
|
|
|
1438
1471
|
0. takes a lock on the `host:port` of `url` (a file `cadence-ux-<host>-<port>.lock` in the OS temp folder, created
|
|
1439
1472
|
exclusively, containing the pid of the orchestrator; a lock whose pid is dead is taken over). Another wave — in
|
|
@@ -1449,7 +1482,7 @@ the same rule) the orchestrator:
|
|
|
1449
1482
|
3. probes `url` until an HTTP status below 500, for `timeout` seconds (default 300); the answer only counts while
|
|
1450
1483
|
the process group it started is still alive. A command that exits early, or
|
|
1451
1484
|
no answer in time, means « UX not verified » with the end of the log in the note: the UX session is skipped,
|
|
1452
|
-
the
|
|
1485
|
+
the code review being already compliant the lot concludes with its note — it is never a failure of the wave (the single pass of a small lot still runs,
|
|
1453
1486
|
on the code alone, its brief saying the app could not be verified);
|
|
1454
1487
|
4. gives the session « The running app is at `<url>` », with no instruction to start anything;
|
|
1455
1488
|
5. at the end of the step — success, error or SIGTERM of the wave — kills the group: SIGTERM, then SIGKILL after 10 s.
|
|
@@ -1658,6 +1691,8 @@ A version exists in three places and is published in two; a release does all of
|
|
|
1658
1691
|
while an existing one is left alone. If the package is on npm but the release is still missing, re-run the job;
|
|
1659
1692
|
the by-hand fallback is `gh release create v<version> --title v<version> --notes-file <the section of CHANGELOG.md> --verify-tag`.
|
|
1660
1693
|
|
|
1694
|
+
`.github/workflows/tests.yml` runs the type-check and the test suite on the same runner image, Node 24 and `npm ci` as the publish job, and never publishes: start it with `gh workflow run tests.yml --ref <branch>` or by pushing a `diag/**` branch, to see on the runner what a red publish run saw.
|
|
1695
|
+
|
|
1661
1696
|
The Claude Code plugin is read from the repository, so pushing `main` is what updates it; npm is what
|
|
1662
1697
|
`npx @sylad/cadence` and a global install read, and only the tag publishes there. A missing tag, or a red
|
|
1663
1698
|
publish run, leaves npm behind without any other error — 0.3.0 and 0.4.0 were never published — hence
|
package/bin/cadence.js
CHANGED
|
@@ -44,7 +44,7 @@ if (tool === 'raf') {
|
|
|
44
44
|
cadence lead tour [dossier] [--idle 3] [--json]
|
|
45
45
|
le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier courant
|
|
46
46
|
s'il est lui-même un projet ; une première ligne « cadence.yaml · ✗ erreur » si priority: est illisible
|
|
47
|
-
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--json]
|
|
47
|
+
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--since date] [--json]
|
|
48
48
|
la mesure des passes de correction des vagues (dossier/.cadence/runs) : lots à ≥1 et ≥2 corrections,
|
|
49
49
|
par projet, visibles ou non, passes par cause ; --classify les classe par Ollama (hors quota)
|
|
50
50
|
cadence spec check <fiche.yaml>
|
package/dist/audit.js
CHANGED
|
@@ -283,6 +283,26 @@ export function reviewIssues(plan, root, byLot) {
|
|
|
283
283
|
: `${l.id} est terminé avec ${l.commits} commit(s) sans revue de code — raf review ${l.id} "verdict"`,
|
|
284
284
|
}));
|
|
285
285
|
}
|
|
286
|
+
export function gateProgress(all, lot) {
|
|
287
|
+
const g = lot.gate;
|
|
288
|
+
if (!g)
|
|
289
|
+
return undefined;
|
|
290
|
+
if (g.invalid)
|
|
291
|
+
return { count: 0, n: 0, label: '', met: false };
|
|
292
|
+
const count = all.filter((l) => l.status === 'done' && (g.criterion === 'spec' ? l.spec === true : l.screen === 'page')).length;
|
|
293
|
+
return { count, n: g.count, label: g.criterion === 'spec' ? 'spec: true' : 'screen: page', met: count >= g.count };
|
|
294
|
+
}
|
|
295
|
+
/** « <compte>/<N> lots <critère> faits » ; un gate mal formé n'a pas d'avancement à dire. */
|
|
296
|
+
export function gateText(p) {
|
|
297
|
+
return p.n === 0 ? 'condition de tirage invalide' : `${p.count}/${p.n} lots ${p.label} faits`;
|
|
298
|
+
}
|
|
299
|
+
/** Les lots en attente d'une condition de tirage non remplie (ou mal formée), avec leur avancement, parmi `blocked` de nextUp. */
|
|
300
|
+
export function gateWaiting(all, blocked) {
|
|
301
|
+
return blocked.flatMap((lot) => {
|
|
302
|
+
const p = gateProgress(all, lot);
|
|
303
|
+
return p && !p.met ? [{ lot, text: gateText(p) }] : [];
|
|
304
|
+
});
|
|
305
|
+
}
|
|
286
306
|
/** Ce qui vient ensuite : lots en cours, puis lots prêts (dépendances closes), gains rapides d'abord. */
|
|
287
307
|
export function nextUp(all) {
|
|
288
308
|
// Les lots récurrents ont leur propre section (raf now) : ils ne sont ni « en cours » ni « à suivre ».
|
|
@@ -290,7 +310,7 @@ export function nextUp(all) {
|
|
|
290
310
|
const byId = new Map(lots.map((l) => [l.id, l]));
|
|
291
311
|
const doing = lots.filter((l) => l.status === 'doing');
|
|
292
312
|
const ready = lots
|
|
293
|
-
.filter((l) => l.status === 'todo' && l.after.every((d) => !byId.has(d) || !isOpen(byId.get(d).status)))
|
|
313
|
+
.filter((l) => l.status === 'todo' && l.after.every((d) => !byId.has(d) || !isOpen(byId.get(d).status)) && (gateProgress(all, l)?.met ?? true))
|
|
294
314
|
.sort((a, b) => Number(b.quickwin) - Number(a.quickwin));
|
|
295
315
|
const blocked = lots.filter((l) => l.status === 'todo' && !ready.includes(l));
|
|
296
316
|
return { doing, ready, blocked };
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { basename, dirname, join, posix, relative, resolve } from 'node:path';
|
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
import { readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
6
6
|
import { repoSections, repoShas, resolveLotRepos } from './repos.js';
|
|
7
|
-
import { audit, exemptPlanOnly, hypothesesOf, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
7
|
+
import { audit, exemptPlanOnly, gateProgress, gateText, gateWaiting, hypothesesOf, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
8
8
|
import { short } from './check.js';
|
|
9
9
|
import { isDay, toDay } from './dates.js';
|
|
10
10
|
import { deliver, parseDeliverConfig, realDeps } from './deliver.js';
|
|
@@ -36,6 +36,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
36
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
|
+
raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits
|
|
39
40
|
raf note <id> "texte"
|
|
40
41
|
raf did <id> ["texte"] un lot récurrent (--every) a été refait : le « dû depuis N j » repart de aujourd'hui
|
|
41
42
|
raf ux enable revue UX obligatoire avant « done » pour les lots --visible
|
|
@@ -50,9 +51,9 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
50
51
|
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
52
|
raf gantt [-o docs/plan/gantt.html]
|
|
52
53
|
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] ; --waits [--since AAAA-MM-JJ]
|
|
54
|
+
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] [--budget projet:lot=+300k] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague] ; --waits [--since AAAA-MM-JJ]
|
|
54
55
|
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
|
-
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
|
+
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--since 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
57
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
57
58
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
58
59
|
|
|
@@ -300,6 +301,14 @@ function dispatch(argv, io) {
|
|
|
300
301
|
}
|
|
301
302
|
return 0;
|
|
302
303
|
}
|
|
304
|
+
case 'todo': {
|
|
305
|
+
need(1, 'todo <id> [--reason texte]');
|
|
306
|
+
const plan = loadPlan();
|
|
307
|
+
plan.putBack(rest[0], today, values.reason ?? '', () => lotWork(plan, root, rest[0]).length);
|
|
308
|
+
plan.save();
|
|
309
|
+
io.out(`${rest[0]} → todo`);
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
303
312
|
case 'note': {
|
|
304
313
|
need(2, 'note <id> "texte"');
|
|
305
314
|
const plan = loadPlan();
|
|
@@ -387,6 +396,9 @@ function dispatch(argv, io) {
|
|
|
387
396
|
io.out(`screen : ${lot.screen}`);
|
|
388
397
|
if (lot.spec)
|
|
389
398
|
io.out('spec : true');
|
|
399
|
+
const gate = gateProgress(plan.lots(), lot);
|
|
400
|
+
if (lot.gate && gate)
|
|
401
|
+
io.out(lot.gate.invalid ? `gate : ${gateText(gate)}` : `gate : ${gate.n} lots ${gate.label} faits`);
|
|
390
402
|
if (notes.length) {
|
|
391
403
|
io.out('notes :');
|
|
392
404
|
// Les lignes suivantes d'une note multiligne s'alignent sous son texte, pas en colonne 0.
|
|
@@ -612,8 +624,12 @@ function now(plan, root, newsDir, today, io) {
|
|
|
612
624
|
io.out(` ${describe(l, count(l.id))}`);
|
|
613
625
|
if (ready.length > 5)
|
|
614
626
|
io.out(` … et ${ready.length - 5} autre(s)`);
|
|
615
|
-
|
|
616
|
-
|
|
627
|
+
const gated = gateWaiting(lots, blocked);
|
|
628
|
+
const byDeps = blocked.filter((l) => !gated.some((g) => g.lot === l));
|
|
629
|
+
if (byDeps.length)
|
|
630
|
+
io.out(` en attente de dépendances : ${byDeps.map((l) => l.id).join(', ')}`);
|
|
631
|
+
if (gated.length)
|
|
632
|
+
io.out(` en attente de condition de tirage : ${gated.map((g) => `${g.lot.id} (${g.text})`).join(', ')}`);
|
|
617
633
|
const recurring = recurringByDue(lots, today);
|
|
618
634
|
if (recurring.length) {
|
|
619
635
|
io.out('\nRécurrent');
|
|
@@ -926,7 +942,7 @@ function spec(rest, io) {
|
|
|
926
942
|
/** `cadence corrections` (L165) : la mesure de référence des passes de correction d'une vague, lue dans <dossier>/.cadence/runs. */
|
|
927
943
|
async function corrections(rest, values, io) {
|
|
928
944
|
if (rest.length > 1)
|
|
929
|
-
throw new RafError('usage : cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--json]');
|
|
945
|
+
throw new RafError('usage : cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--since date] [--json]');
|
|
930
946
|
const parent = resolve(io.cwd, rest[0] ?? '.');
|
|
931
947
|
const runsDir = join(parent, '.cadence', 'runs');
|
|
932
948
|
if (!existsSync(runsDir))
|
|
@@ -936,9 +952,11 @@ async function corrections(rest, values, io) {
|
|
|
936
952
|
throw new RafError(`--sample invalide : ${values.sample}`);
|
|
937
953
|
if (values.until !== undefined && Number.isNaN(Date.parse(values.until)))
|
|
938
954
|
throw new RafError(`--until invalide : ${values.until} (une date ISO, ex. 2026-10-10T18:31Z)`);
|
|
955
|
+
if (values.since !== undefined && Number.isNaN(Date.parse(values.since)))
|
|
956
|
+
throw new RafError(`--since invalide : ${values.since} (une date ISO, ex. 2026-10-10T18:31Z)`);
|
|
939
957
|
const model = values.model ?? 'mistral-small3.2:24b';
|
|
940
958
|
const cacheFile = resolve(io.cwd, values.cache ?? join(parent, '.cadence', 'corrections.json'));
|
|
941
|
-
const data = readRuns(runsDir, values.until);
|
|
959
|
+
const data = readRuns(runsDir, values.until, values.since);
|
|
942
960
|
const cache = readCache(cacheFile);
|
|
943
961
|
if (values.classify) {
|
|
944
962
|
const classify = ollamaClassifier({ url: io.env.OLLAMA_HOST ?? 'http://localhost:11434', model });
|
package/dist/corrections.js
CHANGED
|
@@ -39,24 +39,28 @@ function findingsOf(report) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Lit les journaux de vague `<runs>/<vague>/<projet>--<lot>.json`. Un lot compte (dénominateur) dès qu'une session
|
|
42
|
-
* d'écriture a abouti (`until` : date ISO, ne lit que les vagues créées jusque-là et, dans chacune, les étapes terminées (`ended`) jusque-là, pour une mesure que ni les vagues suivantes ni une vague encore en cours ne changent ; une vague ou une étape sans date lisible est écartée) ; chaque pas `fix` abouti est une passe, rattachée aux constats de la dernière revue qui la précède.
|
|
42
|
+
* d'écriture a abouti (`until` : date ISO, ne lit que les vagues créées jusque-là et, dans chacune, les étapes terminées (`ended`) jusque-là, pour une mesure que ni les vagues suivantes ni une vague encore en cours ne changent ; une vague ou une étape sans date lisible est écartée ; `since` : date ISO, ne garde que les lots des vagues créées strictement après, avec toutes leurs passes : une vague rend tous ses briefs depuis la copie de l'outil figée à sa création, c'est la date de la vague, non celle du début d'un lot, qui dit quel brief il a reçu, sauf pour une vague reprise sans instantané ou, avec `--budget projet:lot=+N`, depuis un instantané d'avant L223 : elle tourne alors avec le dist/ et les gabarits courants ; `--until` plus `--since` ne font le total sans borne que si aucune vague créée avant la borne n'est encore reprenable) ; chaque pas `fix` abouti est une passe, rattachée aux constats de la dernière revue qui la précède.
|
|
43
43
|
*/
|
|
44
|
-
export function readRuns(runsDir, until) {
|
|
44
|
+
export function readRuns(runsDir, until, since) {
|
|
45
45
|
const lots = [];
|
|
46
46
|
const passes = [];
|
|
47
47
|
if (!existsSync(runsDir))
|
|
48
48
|
return { lots, passes };
|
|
49
49
|
const limit = until === undefined ? Infinity : Date.parse(until);
|
|
50
|
+
const from = since === undefined ? undefined : Date.parse(since);
|
|
50
51
|
// Sous `until`, une étape compte si elle est terminée avant la date ; sans date de fin lisible, elle est écartée.
|
|
51
52
|
const inTime = (s) => until === undefined || Date.parse(s.ended ?? '') <= limit;
|
|
52
53
|
for (const wave of readdirSync(runsDir).sort()) {
|
|
53
54
|
const waveDir = join(runsDir, wave);
|
|
54
55
|
if (!statSync(waveDir).isDirectory())
|
|
55
56
|
continue;
|
|
56
|
-
if (until !== undefined) {
|
|
57
|
+
if (until !== undefined || from !== undefined) {
|
|
57
58
|
// Les vagues nommées à la main (ol-gains-1) n'ont pas d'horodatage dans leur nom : on lit `created` dans wave.json.
|
|
58
59
|
const created = Date.parse(readJson(join(waveDir, 'wave.json'))?.created);
|
|
59
|
-
if (!(created <= limit))
|
|
60
|
+
if (until !== undefined && !(created <= limit))
|
|
61
|
+
continue;
|
|
62
|
+
// `since` est strict : une vague créée pile à la borne reste dans l'avant de `until` (inclusif), les deux mesures ne se recouvrent jamais ; sans date de création lisible, la vague est dans aucune.
|
|
63
|
+
if (from !== undefined && !(created > from))
|
|
60
64
|
continue;
|
|
61
65
|
}
|
|
62
66
|
for (const file of readdirSync(waveDir).sort()) {
|
package/dist/lead.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, join, relative, sep } from 'node:path';
|
|
3
|
-
import { audit, nextUp, planCommits } from './audit.js';
|
|
3
|
+
import { audit, gateWaiting, nextUp, planCommits } from './audit.js';
|
|
4
4
|
import { planLoader, readPlanConfig } from './config.js';
|
|
5
5
|
import { diffDays } from './dates.js';
|
|
6
6
|
import { gitRoot, repoStatus } from './git.js';
|
|
@@ -108,7 +108,7 @@ export function tourRow(dir, today, idle = TOUR_IDLE, name = basename(dir)) {
|
|
|
108
108
|
const plan = planLoader(root)();
|
|
109
109
|
const lots = plan.lots();
|
|
110
110
|
const all = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
111
|
-
const { doing, ready } = nextUp(lots);
|
|
111
|
+
const { doing, ready, blocked } = nextUp(lots);
|
|
112
112
|
row.doing = doing.map((l) => {
|
|
113
113
|
const last = lastActivity(l, all.byLot.get(l.id) ?? []);
|
|
114
114
|
if (!last)
|
|
@@ -125,6 +125,9 @@ export function tourRow(dir, today, idle = TOUR_IDLE, name = basename(dir)) {
|
|
|
125
125
|
todo: counted.filter((l) => l.status === 'todo').length,
|
|
126
126
|
added7: counted.filter((l) => l.created !== undefined && diffDays(l.created, today) >= 0 && diffDays(l.created, today) < ADDED_DAYS).length,
|
|
127
127
|
};
|
|
128
|
+
const waiting = gateWaiting(lots, blocked).map((g) => g.lot.id);
|
|
129
|
+
if (waiting.length)
|
|
130
|
+
row.waiting = waiting;
|
|
128
131
|
row.next = ready[0] ? { id: ready[0].id, title: ready[0].title } : null;
|
|
129
132
|
const s = repoStatus(root);
|
|
130
133
|
if (s.dirty || s.untracked)
|
|
@@ -179,5 +182,5 @@ export function tourLine(r) {
|
|
|
179
182
|
: 'aucune';
|
|
180
183
|
const notes = r.notes.length ? truncate(r.notes.join(' / '), NOTES_MAX) : 'aucune';
|
|
181
184
|
const next = r.next ? `${r.next.id} ${truncate(r.next.title, TITLE_MAX)}` : 'aucun';
|
|
182
|
-
return [r.project, `en cours ${doing}`, `dérive ${drift}`, `notes : ${notes}`, `prochain ${next}`, `dépôt ${r.repo.length ? r.repo.join(', ') : 'propre'}`].join(' · ');
|
|
185
|
+
return [r.project, `en cours ${doing}`, `dérive ${drift}`, `notes : ${notes}`, `prochain ${next}`, ...(r.waiting?.length ? [`attend ${r.waiting.join(', ')}`] : []), `dépôt ${r.repo.length ? r.repo.join(', ') : 'propre'}`].join(' · ');
|
|
183
186
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { gateProgress, gateText } from '../audit.js';
|
|
1
2
|
import { execFileSync } from 'node:child_process';
|
|
2
3
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -18,7 +19,7 @@ import { ficheReviewText } from './fiche.js';
|
|
|
18
19
|
import { maquetteText } from './maquette.js';
|
|
19
20
|
import { specScreen, specText } from './spec.js';
|
|
20
21
|
import { candidates, parsePriority, parseUntil, readPriority, stopReason } from './continue.js';
|
|
21
|
-
import { Budget, DROPPED, MAX_PASSES, MAX_PASSES_CONVERGING, briefName, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
|
|
22
|
+
import { Budget, DROPPED, MAX_PASSES, REVIEW_RESERVE, budgetStep, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
|
|
22
23
|
import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
|
|
23
24
|
import { buildArgs, buildLocalArgs, killSessions, mcpServersFor, readAgents, realClaude } from './launch.js';
|
|
24
25
|
import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
@@ -28,7 +29,7 @@ import { resolveLotRepos } from '../repos.js';
|
|
|
28
29
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
29
30
|
import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
30
31
|
import { quotaText, renderTable } from './table.js';
|
|
31
|
-
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, snapshotExists, waveReadsControl, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
32
|
+
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, snapshotExists, waveReadsControl, waveReadsLotBudget, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
32
33
|
export const DEFAULT_BUDGET = 2_000_000;
|
|
33
34
|
/**
|
|
34
35
|
* Budget d'un lot ordinaire (L78, L128, recalibré L194). Mesuré sur le TOTAL de chaque lot dans `.cadence/runs` (toutes les vagues
|
|
@@ -100,8 +101,27 @@ function lotArg(text) {
|
|
|
100
101
|
throw new RafError(`modèle inconnu : @${m[3]} (attendu : ${MODELS.join(', ')})`);
|
|
101
102
|
return { project: m[1], lot: m[2], model: m[3] };
|
|
102
103
|
}
|
|
104
|
+
/** `projet:lot=+200k`, `lot=+0.3M` (L223) : le « = » distingue le relèvement d'un lot du budget de la vague. */
|
|
105
|
+
function lotBudgetArg(text) {
|
|
106
|
+
const bad = () => new RafError(`--budget invalide : ${text} (projet:lot=+500k)`);
|
|
107
|
+
const m = /^([^=]+)=\+(.+)$/.exec(text);
|
|
108
|
+
if (!m)
|
|
109
|
+
throw bad();
|
|
110
|
+
let target;
|
|
111
|
+
let raise;
|
|
112
|
+
try {
|
|
113
|
+
target = lotArg(m[1]);
|
|
114
|
+
raise = parseBudget(m[2]);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw bad();
|
|
118
|
+
}
|
|
119
|
+
if (target.model)
|
|
120
|
+
throw bad();
|
|
121
|
+
return { project: target.project, lot: target.lot, raise, text: m[2] };
|
|
122
|
+
}
|
|
103
123
|
export function parseOrchestrateArgs(argv) {
|
|
104
|
-
const a = { lots: [], dryRun: false, waits: false, watch: false, continue: false, answers: [], drop: [], stopAfterCurrent: false };
|
|
124
|
+
const a = { lots: [], lotBudgets: [], dryRun: false, waits: false, watch: false, continue: false, answers: [], drop: [], stopAfterCurrent: false };
|
|
105
125
|
const optional = (i) => (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-') && !LOT_LIKE.test(argv[i + 1]) ? argv[i + 1] : undefined);
|
|
106
126
|
for (let i = 0; i < argv.length; i++) {
|
|
107
127
|
const t = argv[i];
|
|
@@ -129,8 +149,13 @@ export function parseOrchestrateArgs(argv) {
|
|
|
129
149
|
a.watch = true;
|
|
130
150
|
else if (t === '--interval')
|
|
131
151
|
a.interval = parseMaxSessions(value(t), '--interval');
|
|
132
|
-
else if (t === '--budget')
|
|
133
|
-
|
|
152
|
+
else if (t === '--budget') {
|
|
153
|
+
const v = value(t);
|
|
154
|
+
if (v.includes('='))
|
|
155
|
+
a.lotBudgets.push(lotBudgetArg(v));
|
|
156
|
+
else
|
|
157
|
+
a.budget = parseBudget(v);
|
|
158
|
+
}
|
|
134
159
|
else if (t === '--max-sessions')
|
|
135
160
|
a.maxSessions = parseMaxSessions(value(t));
|
|
136
161
|
else if (t === '--wave')
|
|
@@ -254,6 +279,12 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
254
279
|
refusals.push(`${key} : dépendance(s) ni terminée(s) ni plus tôt dans la vague : ${open.join(', ')}`);
|
|
255
280
|
continue;
|
|
256
281
|
}
|
|
282
|
+
// Condition de tirage (L219) : seuls les lots done du plan la lèvent, jamais un lot de la même vague ; un lot déjà doing n'est pas refusé pour elle.
|
|
283
|
+
const gate = lot.status === 'todo' ? gateProgress(plan.lots(), lot) : undefined;
|
|
284
|
+
if (gate && !gate.met) {
|
|
285
|
+
refusals.push(`${key} : condition de tirage non remplie : ${gate.n === 0 ? 'gate invalide (raf check)' : gateText(gate)}`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
257
288
|
// Dépôts voisins du lot (L62) : chacun doit exister et être un dépôt git, sinon refus avant d'agir comme pour le dépôt du projet.
|
|
258
289
|
const neighbours = resolveLotRepos(t.repo, lot);
|
|
259
290
|
if (neighbours.problems.length) {
|
|
@@ -348,9 +379,9 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
348
379
|
if (l.small)
|
|
349
380
|
steps.push({ kind: l.visible ? 'review-small' : 'review', model: l.light ? light : full });
|
|
350
381
|
else {
|
|
351
|
-
if (l.visible && env.config.ux)
|
|
352
|
-
steps.push({ kind: 'ux', model: full });
|
|
353
382
|
steps.push({ kind: 'review', model: l.light ? light : full });
|
|
383
|
+
if (l.visible && env.config.ux)
|
|
384
|
+
steps.push({ kind: 'ux', model: full }); // L221 : l'UX est jouée une fois, après la revue de code conforme
|
|
354
385
|
}
|
|
355
386
|
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)' : ''}`);
|
|
356
387
|
io.out(` corrections : ${MAX_PASSES} passe(s) au plus (${MAX_PASSES_CONVERGING} si la revue converge), en session neuve`);
|
|
@@ -455,6 +486,8 @@ export async function orchestrate(argv, io, deps) {
|
|
|
455
486
|
throw new RafError('--status ne se combine pas avec --drop / --stop-after-current');
|
|
456
487
|
if (args.resume !== undefined && args.stopAfterCurrent)
|
|
457
488
|
throw new RafError('--resume ne se combine pas avec --stop-after-current (il reprend la vague, il ne l\'arrête pas)');
|
|
489
|
+
if (args.lotBudgets.length && args.resume === undefined)
|
|
490
|
+
throw new RafError("--budget projet:lot=+N ne vaut qu'avec --resume");
|
|
458
491
|
if (args.since !== undefined && !args.waits)
|
|
459
492
|
throw new RafError('--since s\'utilise avec --waits');
|
|
460
493
|
if (args.waits) {
|
|
@@ -964,9 +997,15 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
964
997
|
throw new RafError(typeof args.resume === 'string' ? `vague inconnue : ${args.resume}` : 'aucune vague à reprendre dans ce dossier');
|
|
965
998
|
if (deps.snapshot) {
|
|
966
999
|
// La reprise tourne sur l'instantané de la vague, jamais sur le dist/ courant ; une vague d'avant L61 n'en a pas.
|
|
967
|
-
if (snapshotExists(store.dir))
|
|
968
|
-
|
|
969
|
-
|
|
1000
|
+
if (snapshotExists(store.dir)) {
|
|
1001
|
+
// Une vague d'avant L223 refuserait « projet:lot=+N » comme budget invalide : seul le lot visé justifie de la rejouer avec le dist/ courant.
|
|
1002
|
+
if (!args.lotBudgets.length || waveReadsLotBudget(store.dir))
|
|
1003
|
+
return deps.snapshot.reexec(toolDirOf(store.dir), argv, io.env, io);
|
|
1004
|
+
io.err(`orchestrate : vague ${store.id} : son instantané ne connaît pas --budget projet:lot=+N (antérieur à L223) : reprise avec le dist/ et les gabarits courants`);
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
io.err(`orchestrate : vague ${store.id} sans instantané (antérieure à L61) : reprise avec le dist/ et les gabarits courants`);
|
|
1008
|
+
}
|
|
970
1009
|
}
|
|
971
1010
|
const wave = store.readWave();
|
|
972
1011
|
const refusals = [];
|
|
@@ -976,7 +1015,8 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
976
1015
|
const alive = liveWaves(cadenceHome()).find((w) => w.wave === wave.id && w.cwd === launch && w.pid !== process.pid);
|
|
977
1016
|
if (alive)
|
|
978
1017
|
refusals.push(`la vague ${alive.wave} tourne encore (pid ${alive.pid})`);
|
|
979
|
-
|
|
1018
|
+
// Une vague terminée ne se reprend que pour rejouer un lot visé par --budget projet:lot=+N (L223) ; ce lot est vérifié plus bas.
|
|
1019
|
+
if (wave.status === 'done' && !args.lotBudgets.length)
|
|
980
1020
|
refusals.push(`la vague ${wave.id} est terminée`);
|
|
981
1021
|
const lots = store.lots();
|
|
982
1022
|
for (const a of args.answers) {
|
|
@@ -1003,11 +1043,37 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1003
1043
|
else
|
|
1004
1044
|
drops.push(lotKey(matches[0].project, matches[0].lot));
|
|
1005
1045
|
}
|
|
1046
|
+
// --resume --budget projet:lot=+N (L223) : un lot rendu sur budget est remis en jeu avec son budget relevé. Il subit ensuite les gardes d'un lot vivant.
|
|
1047
|
+
const raised = new Map();
|
|
1048
|
+
const answered = args.answers.map((a) => lots.filter((x) => x.lot === a.lot && (a.project ? x.project === a.project : true))).filter((m) => m.length === 1).map((m) => m[0]);
|
|
1049
|
+
const dropped = args.drop.map((d) => lots.filter((x) => x.lot === d.lot && (d.project ? x.project === d.project : true))).filter((m) => m.length === 1).map((m) => m[0]);
|
|
1050
|
+
for (const b of args.lotBudgets) {
|
|
1051
|
+
const matches = lots.filter((x) => x.lot === b.lot && (b.project ? x.project === b.project : true));
|
|
1052
|
+
const where = `${b.project ? `${b.project}:` : ''}${b.lot}`;
|
|
1053
|
+
if (matches.length !== 1) {
|
|
1054
|
+
refusals.push(matches.length ? `--budget ${where} : plusieurs projets portent ce lot, précisez projet:lot` : `--budget ${where} : lot inconnu dans la vague ${wave.id}`);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
const l = matches[0];
|
|
1058
|
+
const key = lotKey(l.project, l.lot);
|
|
1059
|
+
if (raised.has(l))
|
|
1060
|
+
refusals.push(`--budget ${key} : lot visé deux fois`);
|
|
1061
|
+
else if (dropped.includes(l))
|
|
1062
|
+
refusals.push(`--budget ${key} : lot aussi retiré par --drop`);
|
|
1063
|
+
else if (answered.includes(l))
|
|
1064
|
+
refusals.push(`--budget ${key} : lot aussi visé par --answer`);
|
|
1065
|
+
else if (!budgetStep(l))
|
|
1066
|
+
refusals.push(`--budget ${key} : le lot n'est pas rendu sur budget avec une étape d'écriture à rejouer (${l.status}${l.status === 'handed-back' ? `, étape ${l.next ?? 'aucune'} : ${l.outcome ?? 'sans cause'}` : ''})`);
|
|
1067
|
+
else if (budgetStep(l) === 'fix' && b.raise <= REVIEW_RESERVE)
|
|
1068
|
+
refusals.push(`--budget ${key}=+${b.text} : un fix ne part qu'au-dessus de ${REVIEW_RESERVE} tokens de réserve`);
|
|
1069
|
+
else
|
|
1070
|
+
raised.set(l, { from: l.budget ?? lotSpent(l), to: lotSpent(l) + b.raise, step: budgetStep(l) });
|
|
1071
|
+
}
|
|
1006
1072
|
// Un lot retiré ne jouera rien : il ne subit ni le verrou du dépôt, ni le .nvmrc, ni l'arbre sale — un refus pour LUI ne doit pas faire échouer la reprise des autres.
|
|
1007
1073
|
const live = [];
|
|
1008
1074
|
const removed = [];
|
|
1009
1075
|
for (const l of lots) {
|
|
1010
|
-
if (lotFinished(l))
|
|
1076
|
+
if (lotFinished(l) && !raised.has(l))
|
|
1011
1077
|
continue;
|
|
1012
1078
|
for (const s of l.steps) {
|
|
1013
1079
|
if (s.status !== 'running')
|
|
@@ -1065,6 +1131,17 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1065
1131
|
wave.cacheRead += recovered.cacheRead;
|
|
1066
1132
|
if (recovered.consumed || recovered.cacheRead)
|
|
1067
1133
|
store.writeWave(wave);
|
|
1134
|
+
for (const [l, r] of raised) {
|
|
1135
|
+
const k = lotKey(l.project, l.lot);
|
|
1136
|
+
const text = `budget relevé de ${r.from} à ${r.to} (--resume --budget)`;
|
|
1137
|
+
l.budget = r.to;
|
|
1138
|
+
l.status = 'suspended';
|
|
1139
|
+
l.next = r.step;
|
|
1140
|
+
l.outcome = null;
|
|
1141
|
+
l.warnings.push(text);
|
|
1142
|
+
store.journal(`${k} → suspended — ${text}`);
|
|
1143
|
+
io.out(`${k} → suspended — ${text}`);
|
|
1144
|
+
}
|
|
1068
1145
|
for (const l of live) {
|
|
1069
1146
|
if (l.status === 'implementing' || l.status === 'reviewing' || l.status === 'fixing')
|
|
1070
1147
|
l.status = 'suspended';
|
|
@@ -517,6 +517,20 @@ const lotOver = (l) => l.budget !== undefined && lotSpent(l) >= l.budget;
|
|
|
517
517
|
* alors être refusée et le lot revient au lead sans revue, avec ses constats.
|
|
518
518
|
*/
|
|
519
519
|
export const REVIEW_RESERVE = 155_000;
|
|
520
|
+
/** Étapes d'écriture : les seules que le budget du lot borne (`session`), donc les seules qu'il puisse laisser non jouées. */
|
|
521
|
+
const WRITE_STEPS = ['implement', 'fix', 'spec', 'accept'];
|
|
522
|
+
/**
|
|
523
|
+
* Étape d'écriture que le budget a laissée non jouée (L223), pour un lot rendu au lead sur budget ; null sinon. `stop` vide `next` : l'étape se lit
|
|
524
|
+
* dans l'outcome que `overBudget` écrit (« étape « fix » non jouée »), repli sur `next` s'il est resté. Reconnu par ce texte (aucun marqueur dans
|
|
525
|
+
* l'état) : « correction non jouée (budget) » ou « budget du lot atteint ».
|
|
526
|
+
*/
|
|
527
|
+
export function budgetStep(l) {
|
|
528
|
+
if (l.status !== 'handed-back' || !/correction non jouée \(budget\)|budget du lot atteint/.test(l.outcome ?? ''))
|
|
529
|
+
return null;
|
|
530
|
+
const named = /étape « ([\w-]+) » non jouée/.exec(l.outcome ?? '')?.[1];
|
|
531
|
+
const step = named ?? l.next;
|
|
532
|
+
return step && WRITE_STEPS.includes(step) ? step : null;
|
|
533
|
+
}
|
|
520
534
|
/** Le budget du lot paie-t-il la passe de correction et la revue courte qui la suit ? Sans budget de lot : toujours. */
|
|
521
535
|
const fixAffordable = (l) => l.budget === undefined || lotSpent(l) + REVIEW_RESERVE < l.budget;
|
|
522
536
|
/** Cause d'une suspension demandée par `--stop-after-current` (L79). */
|
|
@@ -1476,6 +1490,12 @@ async function work(c, kind) {
|
|
|
1476
1490
|
badReport(c, done.step, e);
|
|
1477
1491
|
return;
|
|
1478
1492
|
}
|
|
1493
|
+
// L'écriture a changé le code (L221) : le verdict et les constats de la revue UX d'avant ne valent plus, l'UX est rejouée après la prochaine revue de code conforme.
|
|
1494
|
+
if (!l.small) {
|
|
1495
|
+
l.ux = undefined;
|
|
1496
|
+
l.uxVerdict = null;
|
|
1497
|
+
l.constats = l.constats.filter((k) => k.source !== 'ux');
|
|
1498
|
+
}
|
|
1479
1499
|
const plan = c.loadPlan();
|
|
1480
1500
|
const range = done.step.headBefore ? `${done.step.headBefore}..HEAD` : undefined;
|
|
1481
1501
|
const commits = readCommits(l.repo, { range });
|
|
@@ -1552,7 +1572,12 @@ async function work(c, kind) {
|
|
|
1552
1572
|
propose(c, minorLine('code', k));
|
|
1553
1573
|
l.constats = [];
|
|
1554
1574
|
l.warnings.push('passe des mineurs sans commit : le lot conclut sur la revue conforme d\'origine, mineurs rendus en propositions');
|
|
1555
|
-
if (l.code)
|
|
1575
|
+
if (l.code && uxAfterReview(c)) {
|
|
1576
|
+
// L221 : l'UX finale suit la dernière revue de code conforme, y compris quand la passe des mineurs n'a rien commité.
|
|
1577
|
+
l.next = 'ux';
|
|
1578
|
+
save(c);
|
|
1579
|
+
}
|
|
1580
|
+
else if (l.code)
|
|
1556
1581
|
await conclude(c, l.code, ' sans commit');
|
|
1557
1582
|
else
|
|
1558
1583
|
stop(c, 'handed-back', 'passe des mineurs sans commit, mais aucune revue conforme gardée');
|
|
@@ -1637,25 +1662,24 @@ async function work(c, kind) {
|
|
|
1637
1662
|
l.next = firstReview(c);
|
|
1638
1663
|
save(c);
|
|
1639
1664
|
}
|
|
1665
|
+
/** Mention « sans commit » du verdict quand la passe des mineurs n'a rien commité (l'UX finale conclut alors à la place de la passe). */
|
|
1666
|
+
const noCommitNote = (l) => (l.warnings.some((w) => w.startsWith('passe des mineurs sans commit')) ? ' sans commit' : '');
|
|
1640
1667
|
/** Revue de code qui clôt le cycle : complète, ou courte une fois la passe des mineurs jouée. */
|
|
1641
1668
|
const codeReview = (l) => (l.minorPass ? 'review-small' : 'review');
|
|
1642
1669
|
/**
|
|
1643
|
-
* Première étape après un travail d'écriture
|
|
1644
|
-
*
|
|
1670
|
+
* Première étape après un travail d'écriture (L221) : la revue de code, jamais l'UX. La revue UX d'un lot visible est jouée une seule fois,
|
|
1671
|
+
* après la dernière revue de code conforme (`uxAfterReview`), sur le code qui sera livré.
|
|
1645
1672
|
*/
|
|
1646
1673
|
function firstReview(c) {
|
|
1647
1674
|
const l = c.lot;
|
|
1648
1675
|
if (l.small)
|
|
1649
1676
|
return l.visible || l.minorPass ? 'review-small' : 'review'; // passe unique code + ergonomie ; sans écran, une revue de code
|
|
1650
|
-
if (l.visible)
|
|
1651
|
-
|
|
1652
|
-
l.uxNote = 'UX à faire par le lead (aucune application déclarée : cadence.yaml orchestrate.ux)';
|
|
1653
|
-
return codeReview(l);
|
|
1654
|
-
}
|
|
1655
|
-
return 'ux';
|
|
1656
|
-
}
|
|
1677
|
+
if (l.visible && !c.config.ux)
|
|
1678
|
+
l.uxNote = 'UX à faire par le lead (aucune application déclarée : cadence.yaml orchestrate.ux)';
|
|
1657
1679
|
return codeReview(l);
|
|
1658
1680
|
}
|
|
1681
|
+
/** La revue de code conforme d'un lot visible non petit est-elle suivie de la revue UX ? Sans `orchestrate.ux`, le lot conclut (note « UX à faire par le lead »). */
|
|
1682
|
+
const uxAfterReview = (c) => !c.lot.small && c.lot.visible && !!c.config.ux;
|
|
1659
1683
|
/** Une passe de correction de plus, ou la main rendue au lead quand les deux sont faites. */
|
|
1660
1684
|
function toFix(c, constats, what) {
|
|
1661
1685
|
const l = c.lot;
|
|
@@ -1677,12 +1701,20 @@ const seesApp = (l, kind) => kind === 'ux' || (kind === 'review-small' && l.smal
|
|
|
1677
1701
|
const appNote = (state, ux) => state.kind === 'busy' ? `UX non vérifiée : port occupé (${ux.url} répond déjà, l'application n'a pas été lancée)` : state.kind === 'unverified' ? `UX non vérifiée : ${state.cause}` : '';
|
|
1678
1702
|
/**
|
|
1679
1703
|
* Avec `command` ET `url`, le programme lance l'application avant l'étape et la tue après (succès, erreur ou signal) :
|
|
1680
|
-
* port occupé ou application muette, la revue UX n'a pas lieu (note dans le lot
|
|
1681
|
-
* unique d'un petit lot, elle, a lieu sur le code seul.
|
|
1704
|
+
* port occupé ou application muette, la revue UX n'a pas lieu (étape `skipped`, note dans le lot) et le lot conclut
|
|
1705
|
+
* sur la dernière revue de code conforme (R6) ; la passe unique d'un petit lot, elle, a lieu sur le code seul.
|
|
1682
1706
|
*/
|
|
1683
1707
|
async function review(c, kind) {
|
|
1684
1708
|
const l = c.lot;
|
|
1685
1709
|
const ux = c.config.ux;
|
|
1710
|
+
// Un état repris, écrit avant L221, peut porter next = ux : l'UX ne part que sur une revue de code conforme du HEAD courant, sinon la revue de code passe d'abord (R9).
|
|
1711
|
+
if (kind === 'ux' && !(l.code?.conforme && l.code.head === git(l.repo, 'rev-parse', 'HEAD'))) {
|
|
1712
|
+
l.next = codeReview(l);
|
|
1713
|
+
if (l.status === 'suspended')
|
|
1714
|
+
l.status = 'reviewing'; // reprise d'un lot suspendu : la boucle de runLot enchaîne sur la revue de code au lieu de rendre la main
|
|
1715
|
+
save(c);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1686
1718
|
if (!ux?.command || !ux.url || !seesApp(l, kind))
|
|
1687
1719
|
return reviewStep(c, kind);
|
|
1688
1720
|
const halt = halted(c.wave, l);
|
|
@@ -1690,6 +1722,7 @@ async function review(c, kind) {
|
|
|
1690
1722
|
suspend(c, halt); // la session n'aurait pas lieu : l'application ne se lance pas pour rien
|
|
1691
1723
|
return;
|
|
1692
1724
|
}
|
|
1725
|
+
let skipped = false;
|
|
1693
1726
|
c.wave.log(`${lotKey(l.project, l.lot)} · lancement de l'application (${ux.url})`);
|
|
1694
1727
|
const app = await startApp({ command: ux.command, url: ux.url, cwd: l.repo, nodeBin: l.node?.link, log: join(c.wave.store.lotDir(l.project, l.lot), 'ux-app.log'), timeoutMs: (ux.timeout ?? APP_TIMEOUT_S) * 1000 });
|
|
1695
1728
|
try {
|
|
@@ -1698,12 +1731,7 @@ async function review(c, kind) {
|
|
|
1698
1731
|
l.uxNote = appNote(app.state, ux);
|
|
1699
1732
|
c.wave.log(`${lotKey(l.project, l.lot)} · ${l.uxNote.split('\n')[0]}`);
|
|
1700
1733
|
if (kind === 'ux') {
|
|
1701
|
-
//
|
|
1702
|
-
l.ux = undefined;
|
|
1703
|
-
l.uxVerdict = null;
|
|
1704
|
-
l.constats = l.constats.filter((k) => k.source !== 'ux');
|
|
1705
|
-
l.next = codeReview(l);
|
|
1706
|
-
save(c);
|
|
1734
|
+
skipped = true; // aucune session : la revue de code conforme suffit, le lot conclut avec la note
|
|
1707
1735
|
return;
|
|
1708
1736
|
}
|
|
1709
1737
|
}
|
|
@@ -1714,6 +1742,8 @@ async function review(c, kind) {
|
|
|
1714
1742
|
finally {
|
|
1715
1743
|
c.app = undefined;
|
|
1716
1744
|
await app.stop();
|
|
1745
|
+
if (skipped && l.code)
|
|
1746
|
+
await conclude(c, l.code, noCommitNote(l));
|
|
1717
1747
|
}
|
|
1718
1748
|
}
|
|
1719
1749
|
async function reviewStep(c, kind) {
|
|
@@ -1735,8 +1765,12 @@ async function reviewStep(c, kind) {
|
|
|
1735
1765
|
addProposals(c, rep, 'ux');
|
|
1736
1766
|
l.uxVerdict = summary.verdict;
|
|
1737
1767
|
l.constats = blockingConstats(rep, 'ux');
|
|
1738
|
-
l.next = codeReview(l); // la revue de code clôt toujours le cycle
|
|
1739
1768
|
save(c);
|
|
1769
|
+
// L'UX est la dernière étape (L221) : conforme, le lot conclut avec la revue de code ; sinon une correction (jamais une revue de code d'office), la revue de code et l'UX rejouée suivent.
|
|
1770
|
+
if (summary.conforme)
|
|
1771
|
+
await conclude(c, l.code, noCommitNote(l));
|
|
1772
|
+
else
|
|
1773
|
+
toFix(c, l.constats, 'UX non conforme');
|
|
1740
1774
|
return;
|
|
1741
1775
|
}
|
|
1742
1776
|
l.code = summary;
|
|
@@ -1746,18 +1780,18 @@ async function reviewStep(c, kind) {
|
|
|
1746
1780
|
l.ux = summary;
|
|
1747
1781
|
l.uxVerdict = summary.verdict;
|
|
1748
1782
|
}
|
|
1749
|
-
const uxOk = l.small || !l.ux || l.ux.conforme;
|
|
1750
1783
|
const minors = minorConstats(rep);
|
|
1751
1784
|
// Revue conforme avec mineurs : une seule passe de correction des mineurs, avant de conclure (rien sous le tapis).
|
|
1752
|
-
// Budget épuisé : la passe n'aurait aucune session pour la jouer,
|
|
1753
|
-
|
|
1785
|
+
// Budget épuisé : la passe n'aurait aucune session pour la jouer, les mineurs sont rendus en propositions ; la revue conforme conclut le lot,
|
|
1786
|
+
// sauf un lot visible avec orchestrate.ux, qui passe à next = 'ux' puis reste suspendu tant que le budget de la vague l'est.
|
|
1787
|
+
const wanted = summary.conforme && !l.minorPass && !l.light && minors.length > 0; // lot léger : les mineurs restent des notes (L108)
|
|
1754
1788
|
const noBudget = wanted && (w.budget.exhausted || !fixAffordable(l)) && !w.incident && !w.quota.hit;
|
|
1755
1789
|
const minorPass = wanted && !noBudget;
|
|
1756
1790
|
if (noBudget)
|
|
1757
1791
|
l.warnings.push(`${w.budget.exhausted ? 'budget atteint' : 'budget du lot trop juste pour payer la correction et la revue courte qui la suivrait'} : la passe des mineurs n'a pas eu lieu, mineurs rendus en propositions`);
|
|
1758
1792
|
// Les mineurs confiés à la passe ont pu être proposés par une revue non conforme antérieure : si la revue qui la suit est
|
|
1759
1793
|
// conforme, ils sont traités et ne restent pas en propositions (ceux que cette revue signale encore sont ajoutés juste après).
|
|
1760
|
-
if (l.minorPass && !minorPass && summary.conforme &&
|
|
1794
|
+
if (l.minorPass && !minorPass && summary.conforme && l.minorLines?.length) {
|
|
1761
1795
|
// Chaque mineur confié retire UNE proposition : la même ligne d'abord, sinon une de même fichier et même texte (le correctif a pu décaler la ligne).
|
|
1762
1796
|
// Un autre mineur de même fichier et même texte, à une autre ligne, reste donc proposé. Limite connue : un texte reformulé n'est pas reconnu et reste proposé.
|
|
1763
1797
|
const left = [...l.proposals];
|
|
@@ -1774,7 +1808,7 @@ async function reviewStep(c, kind) {
|
|
|
1774
1808
|
l.minorLines = [];
|
|
1775
1809
|
}
|
|
1776
1810
|
addProposals(c, rep, 'code', !minorPass);
|
|
1777
|
-
if (summary.conforme
|
|
1811
|
+
if (summary.conforme)
|
|
1778
1812
|
(l.reviewCounts ??= []).push(0); // une revue conforme compte pour 0 dans la comparaison de la convergence (L205)
|
|
1779
1813
|
if (minorPass) {
|
|
1780
1814
|
l.minorPass = true;
|
|
@@ -1786,18 +1820,19 @@ async function reviewStep(c, kind) {
|
|
|
1786
1820
|
save(c);
|
|
1787
1821
|
return;
|
|
1788
1822
|
}
|
|
1789
|
-
if (summary.conforme
|
|
1790
|
-
|
|
1823
|
+
if (summary.conforme) {
|
|
1824
|
+
if (uxAfterReview(c)) {
|
|
1825
|
+
l.next = 'ux';
|
|
1826
|
+
save(c);
|
|
1827
|
+
}
|
|
1828
|
+
else
|
|
1829
|
+
await conclude(c, summary);
|
|
1791
1830
|
return;
|
|
1792
1831
|
}
|
|
1793
|
-
const constats =
|
|
1832
|
+
const constats = blockingConstats(rep, 'code');
|
|
1794
1833
|
(l.reviewCounts ??= []).push(constats.length);
|
|
1795
1834
|
toFix(c, constats, 'revue non conforme');
|
|
1796
1835
|
}
|
|
1797
|
-
/** 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). */
|
|
1798
|
-
function uxConstatsKept(c) {
|
|
1799
|
-
return c.lot.constats.filter((k) => k.source === 'ux');
|
|
1800
|
-
}
|
|
1801
1836
|
/** Revue conforme : le verdict est enregistré avec le sha relu, puis le commit du plan. `raf done` reste au lead. */
|
|
1802
1837
|
async function conclude(c, code, minorNote = '') {
|
|
1803
1838
|
const l = c.lot;
|
|
@@ -89,6 +89,20 @@ export function waveReadsControl(waveDir) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* L'instantané de la vague connaît-il `--budget projet:lot=+N` (L223) ? Un instantané plus ancien relit l'argument avec son `parseBudget` et le refuse
|
|
94
|
+
* comme budget invalide. Sans instantané : rien à relire, on suppose que oui.
|
|
95
|
+
*/
|
|
96
|
+
export function waveReadsLotBudget(waveDir) {
|
|
97
|
+
if (!snapshotExists(waveDir))
|
|
98
|
+
return true;
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(join(toolDirOf(waveDir), 'dist', 'orchestrate', 'command.js'), 'utf8').includes('lotBudgets');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
92
106
|
/** Relance réelle : un processus node sur la copie, signaux transmis, code de sortie rendu. */
|
|
93
107
|
export function spawnReexec(toolDir, argv, env, sink) {
|
|
94
108
|
return new Promise((resolve, reject) => {
|
package/dist/plan.js
CHANGED
|
@@ -3,9 +3,10 @@ import { dirname } from 'node:path';
|
|
|
3
3
|
import { isMap, isSeq, parseDocument } from 'yaml';
|
|
4
4
|
import { isDay, toDay } from './dates.js';
|
|
5
5
|
export const STATUSES = ['todo', 'doing', 'done', 'dropped'];
|
|
6
|
+
export const GATE_INVALID = 'gate invalide : count: entier ≥ 1 et un critère spec: true ou screen: page attendus';
|
|
6
7
|
export class RafError extends Error {
|
|
7
8
|
}
|
|
8
|
-
export const FIELDS = ['id', 'title', 'status', 'estimate', 'quickwin', 'visible', 'screen', 'spec', 'public', 'every', 'last', 'after', 'created', 'started', 'finished', 'notes', 'repos', 'parent'];
|
|
9
|
+
export const FIELDS = ['id', 'title', 'status', 'estimate', 'quickwin', 'visible', 'screen', 'spec', 'public', 'every', 'last', 'after', 'created', 'started', 'finished', 'notes', 'repos', 'parent', 'gate'];
|
|
9
10
|
/** Nature de l'écran d'un lot visible : nouvelle page, nouveau composant, parcours modifié. */
|
|
10
11
|
export const SCREENS = ['page', 'component', 'journey'];
|
|
11
12
|
export function isOpen(status) {
|
|
@@ -398,6 +399,28 @@ export class Plan {
|
|
|
398
399
|
if (status === 'done' || status === 'dropped')
|
|
399
400
|
node.set('finished', today);
|
|
400
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Remet en attente un lot démarré (doing → todo, `started` retiré, note datée) — `raf todo` (L220). Les refus passent dans
|
|
404
|
+
* l'ordre : plan modifiable, lot et non sous-tâche, lot connu, lot doing ; `commits` n'est lu qu'ensuite (il lit git) et un lot
|
|
405
|
+
* qui a du travail est refusé. Les sous-tâches et les autres clés du lot ne bougent pas.
|
|
406
|
+
*/
|
|
407
|
+
putBack(ref, today, reason, commits) {
|
|
408
|
+
this.writable();
|
|
409
|
+
if (ref.includes('/'))
|
|
410
|
+
throw new RafError(`raf todo se pose sur un lot, pas une sous-tâche : ${ref}`);
|
|
411
|
+
const lot = this.lot(ref);
|
|
412
|
+
if (lot.status === 'todo')
|
|
413
|
+
throw new RafError(`${ref} est déjà todo`);
|
|
414
|
+
if (lot.status !== 'doing')
|
|
415
|
+
throw new RafError(`${ref} est ${lot.status} ; le rouvrir à la main dans le YAML si c'est voulu`);
|
|
416
|
+
const n = commits();
|
|
417
|
+
if (n > 0)
|
|
418
|
+
throw new RafError(`${ref} a ${n} commit(s) qui le citent : le remettre en attente perdrait leur lien — raf drop ou le laisser doing`);
|
|
419
|
+
const node = this.lotNode(ref);
|
|
420
|
+
node.set('status', 'todo');
|
|
421
|
+
node.delete('started');
|
|
422
|
+
this.note(ref, reason.trim() ? `remis en attente : ${reason.trim()}` : 'remis en attente', today);
|
|
423
|
+
}
|
|
401
424
|
/** Pose, remplace (texte) ou efface (null) le titre public d'un lot ; la clé suit `title`. */
|
|
402
425
|
setPublic(ref, text) {
|
|
403
426
|
this.writable();
|
|
@@ -541,6 +564,7 @@ function foreignLots(raw, format) {
|
|
|
541
564
|
finished: day('finished'),
|
|
542
565
|
notes: typeof notes === 'string' ? [{ text: notes }] : notes,
|
|
543
566
|
repos: pick('repos'),
|
|
567
|
+
gate: pick('gate'),
|
|
544
568
|
}, problems);
|
|
545
569
|
const parent = pick('parent');
|
|
546
570
|
return { lot, parent: parent == null ? undefined : String(parent) };
|
|
@@ -559,6 +583,25 @@ function foreignLots(raw, format) {
|
|
|
559
583
|
}
|
|
560
584
|
return lots;
|
|
561
585
|
}
|
|
586
|
+
/** Lit `gate: { count: N, spec: true }` ou `{ count: N, screen: page }` ; toute autre forme est un problème du lot et une condition jamais remplie. */
|
|
587
|
+
function readGate(raw, problems) {
|
|
588
|
+
if (raw == null)
|
|
589
|
+
return undefined;
|
|
590
|
+
const invalid = () => {
|
|
591
|
+
problems.push(GATE_INVALID);
|
|
592
|
+
return { invalid: true };
|
|
593
|
+
};
|
|
594
|
+
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
595
|
+
return invalid();
|
|
596
|
+
const g = raw;
|
|
597
|
+
if (!Number.isInteger(g.count) || g.count < 1)
|
|
598
|
+
return invalid();
|
|
599
|
+
const criteria = [...(g.spec === true ? ['spec'] : []), ...(g.screen === 'page' ? ['screen'] : [])];
|
|
600
|
+
const others = Object.keys(g).filter((k) => k !== 'count' && k !== 'spec' && k !== 'screen');
|
|
601
|
+
if (criteria.length !== 1 || others.length || ('spec' in g && g.spec !== true) || ('screen' in g && g.screen !== 'page'))
|
|
602
|
+
return invalid();
|
|
603
|
+
return { count: g.count, criterion: criteria[0] };
|
|
604
|
+
}
|
|
562
605
|
/** Un titre public vide ne dirait rien au public : refusé plutôt qu'écrit. */
|
|
563
606
|
function cleanPublic(text) {
|
|
564
607
|
const t = text.trim();
|
|
@@ -595,6 +638,7 @@ function normalizeLot(raw, problems = []) {
|
|
|
595
638
|
else if (raw.spec !== false)
|
|
596
639
|
problems.push(`spec : true ou false attendu (reçu « ${String(raw.spec)} »)`);
|
|
597
640
|
}
|
|
641
|
+
const gate = readGate(raw.gate, problems);
|
|
598
642
|
return {
|
|
599
643
|
id: String(raw.id),
|
|
600
644
|
title: String(raw.title ?? ''),
|
|
@@ -604,6 +648,7 @@ function normalizeLot(raw, problems = []) {
|
|
|
604
648
|
visible: raw.visible === true,
|
|
605
649
|
...(screen ? { screen } : {}),
|
|
606
650
|
...(spec ? { spec } : {}),
|
|
651
|
+
...(gate ? { gate } : {}),
|
|
607
652
|
...(typeof raw.public === 'string' && raw.public.trim() !== '' ? { public: raw.public.trim() } : {}),
|
|
608
653
|
...(Number.isInteger(raw.every) && raw.every > 0 ? { every: raw.every } : {}),
|
|
609
654
|
...(last ? { last } : {}),
|
package/dist/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { CLEAN_DAYS, scanStale } from './clean.js';
|
|
3
3
|
import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
4
|
-
import { audit, exemptPlanOnly, nextUp, planCommits } from './audit.js';
|
|
4
|
+
import { audit, exemptPlanOnly, gateWaiting, nextUp, planCommits } from './audit.js';
|
|
5
5
|
import { diffDays, maxDay } from './dates.js';
|
|
6
6
|
import { repoStatus } from './git.js';
|
|
7
7
|
import { linkCommits } from './link.js';
|
|
@@ -109,7 +109,7 @@ export function sessionStart(ctx, opts) {
|
|
|
109
109
|
const lock = lockStatus(ctx.shared);
|
|
110
110
|
if (lock)
|
|
111
111
|
out(`\n${lock.line}`);
|
|
112
|
-
const { doing, ready } = nextUp(lots);
|
|
112
|
+
const { doing, ready, blocked } = nextUp(lots);
|
|
113
113
|
out('\nEn cours');
|
|
114
114
|
if (doing.length === 0)
|
|
115
115
|
out(' (rien)');
|
|
@@ -119,6 +119,7 @@ export function sessionStart(ctx, opts) {
|
|
|
119
119
|
const state = last ? `dernière activité ${last}${idle > opts.idle ? `, silencieux depuis ${idle} j` : ''}` : 'aucune activité';
|
|
120
120
|
out(` ${l.id} ${l.title} (${state})`);
|
|
121
121
|
}
|
|
122
|
+
section(out, 'En attente', gateWaiting(lots, blocked).map(({ lot, text }) => `${lot.id} ${lot.title} (${text})`));
|
|
122
123
|
const recent = period(ctx, opts.since);
|
|
123
124
|
const done = byLotLines(recent);
|
|
124
125
|
if (recent.orphans.length)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
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
|
@@ -90,12 +90,11 @@ ends `interrupted`, and `--resume` takes it up again. To take a lot out of a wav
|
|
|
90
90
|
`--resume --drop <project>:<lot>` (the lot is handed back instead of being replayed). Neither kills a session.
|
|
91
91
|
|
|
92
92
|
`cadence orchestrate --dry-run …` first when a precondition is in doubt. The program, not the lead, runs
|
|
93
|
-
for each lot a fresh short session per step — implementation (Sonnet),
|
|
94
|
-
and the project declares how to see its app (`orchestrate.ux` in `cadence.yaml`), code review last
|
|
93
|
+
for each lot a fresh short session per step — implementation (Sonnet), code review
|
|
95
94
|
(Opus, the `code-reviewer` agent; Sonnet, with no pass on the minor findings, for a lot whose estimate is
|
|
96
95
|
≤ 0.25 d — `orchestrate.review` in `cadence.yaml`; a blocker or major finding still brings a correction and an
|
|
97
96
|
Opus review), a correction in a new session if the review is not compliant (two
|
|
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
|
|
97
|
+
passes at most, a third when the review converges: blocking + major findings down from one review to the next and only one left), then, once, a UX review if the lot is `visible` and the project declares how to see its app (`orchestrate.ux` in `cadence.yaml`) — and records `raf review` itself when the code review is compliant and the UX review too. 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
|
|
99
98
|
`.cadence/runs/<wave>/`, not in this conversation. `@haiku` only when the human writes it, for a
|
|
100
99
|
mechanical lot. Exit code: 0 all ready · 1 some lots handed back · 2 refused before acting · 3 wave
|
|
101
100
|
suspended (budget or usage limit; `--resume --budget …` continues).
|
|
@@ -13,6 +13,14 @@ Rules: test first; commit each fix as soon as its tests pass, with explicit path
|
|
|
13
13
|
full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
|
|
14
14
|
`raf review`. Read the project's CLAUDE.md first.
|
|
15
15
|
Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
|
|
16
|
+
Self-check before handing back (the recurring findings of the reviews), covering only what you changed: go through this list point by point on your own fix commits before you report, and touch nothing else:
|
|
17
|
+
- documentation: the README and the architecture documentation describe the behaviour, updated in the same commits as the behaviour (and the CHANGELOG);
|
|
18
|
+
- tests: one test per rule of the fiche, named « Rn — … »; with no fiche, one test per business rule of the lot, named after it;
|
|
19
|
+
- no dead code, and no test that cannot fail: a test whose assertion is always true, or that does not exercise the rule it names;
|
|
20
|
+
- screens: for a modified screen, the capture due of each state, at 1440 and 390 px, with a relative file name; a lot with no modified screen owes none, and says so;
|
|
21
|
+
- no false comment: a comment, a docstring or a message never describes a behaviour the code no longer has or does not have.
|
|
22
|
+
A point you cannot hold is written under "choix" or "nonVerifie" of your report, not left unsaid.
|
|
23
|
+
|
|
16
24
|
If a finding is wrong, say so under "choix" with the reason. Ask (under "questions") only when a fix needs a decision that passes the rule below.
|
|
17
25
|
A question is legitimate only if it names what it would change: the scope of the lot, the architecture, or a costly rollback (data, production, public API);
|
|
18
26
|
otherwise it is a choice: decide, write the alternative you did not take, go on. Minor choices to settle yourself:
|
|
@@ -9,6 +9,14 @@ full test suite and build before reporting; do not push, deliver, run `raf done`
|
|
|
9
9
|
`raf review`.
|
|
10
10
|
Never run `git checkout` or `git switch` to another commit or branch in this repository: the orchestrator watches it and a moved HEAD breaks the other sessions of the wave. To look at another sha, use a throwaway `git worktree add --detach <tmp> <sha>`, removed afterwards.
|
|
11
11
|
Documentation: the project's README (and its usage documentation) describes the behaviour you deliver, updated in the same commits, like the CHANGELOG — a README that does not follow your change is a major finding of the review.
|
|
12
|
+
Self-check before handing back (the recurring findings of the reviews). Go through this list point by point on your own commits before you report, and fix what is missing:
|
|
13
|
+
- documentation: the README and the architecture documentation describe the behaviour, updated in the same commits as the behaviour (and the CHANGELOG);
|
|
14
|
+
- tests: one test per rule of the fiche, named « Rn — … »; with no fiche, one test per business rule of the lot, named after it;
|
|
15
|
+
- no dead code, and no test that cannot fail: a test whose assertion is always true, or that does not exercise the rule it names;
|
|
16
|
+
- screens: for a modified screen, the capture due of each state, at 1440 and 390 px, with a relative file name; a lot with no modified screen owes none, and says so;
|
|
17
|
+
- no false comment: a comment, a docstring or a message never describes a behaviour the code no longer has or does not have.
|
|
18
|
+
A point you cannot hold is written under "choix" or "nonVerifie" of your report, not left unsaid.
|
|
19
|
+
|
|
12
20
|
Decide minor interpretation questions yourself (the wording of a message, a name, a default, the
|
|
13
21
|
reading of an ambiguous line of the lot) and list each one under "choix" in your report, with the
|
|
14
22
|
alternative you did not take — the reviewer re-reads them. Stop and ask (under "questions") only on a real blocker.
|