@sylad/cadence 1.40.0 → 1.42.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.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/spec-writer.md +1 -0
  4. package/agents/ux-reviewer.md +2 -0
  5. package/dist/archive.js +103 -6
  6. package/dist/audit.js +2 -2
  7. package/dist/clean.js +0 -4
  8. package/dist/cli/context.js +129 -0
  9. package/dist/cli/deliver-commands.js +114 -0
  10. package/dist/cli/gates.js +234 -0
  11. package/dist/cli/lead-commands.js +161 -0
  12. package/dist/cli/lot-commands.js +257 -0
  13. package/dist/cli/read-commands.js +227 -0
  14. package/dist/cli/session-commands.js +152 -0
  15. package/dist/cli/setup-commands.js +72 -0
  16. package/dist/cli/spec-commands.js +298 -0
  17. package/dist/cli/stats-commands.js +138 -0
  18. package/dist/cli-add.js +37 -0
  19. package/dist/cli.js +85 -1669
  20. package/dist/config.js +21 -108
  21. package/dist/corrections.js +28 -42
  22. package/dist/deliver-lots.js +37 -0
  23. package/dist/deliver.js +15 -39
  24. package/dist/docs-ok-file.js +25 -0
  25. package/dist/docsync.js +15 -3
  26. package/dist/lead.js +6 -24
  27. package/dist/lot-lines.js +51 -0
  28. package/dist/orchestrate/attente.js +82 -0
  29. package/dist/orchestrate/command.js +26 -25
  30. package/dist/orchestrate/continue.js +6 -14
  31. package/dist/orchestrate/cycle.js +18 -27
  32. package/dist/orchestrate/integrate.js +7 -2
  33. package/dist/orchestrate/integration-vivante.js +138 -0
  34. package/dist/orchestrate/launch.js +6 -18
  35. package/dist/orchestrate/lock.js +3 -20
  36. package/dist/orchestrate/lot-budget.js +2 -2
  37. package/dist/orchestrate/maquette.js +3 -0
  38. package/dist/orchestrate/registry.js +8 -1
  39. package/dist/orchestrate/schemas.js +5 -1
  40. package/dist/orchestrate/snapshot.js +13 -0
  41. package/dist/orchestrate/spec.js +51 -6
  42. package/dist/orchestrate/state.js +17 -0
  43. package/dist/plan-extras.js +119 -0
  44. package/dist/plan-ids.js +95 -0
  45. package/dist/plan-read.js +146 -0
  46. package/dist/plan-states.js +119 -0
  47. package/dist/plan.js +119 -190
  48. package/dist/raf-close-checks.js +25 -0
  49. package/dist/raf-status.js +28 -0
  50. package/dist/ref/command.js +2 -3
  51. package/dist/resources.js +3 -9
  52. package/dist/state.js +31 -8
  53. package/dist/verify-gate.js +33 -2
  54. package/dist/verify.js +20 -3
  55. package/dist/yaml-keys.js +21 -0
  56. package/docs/cadence-hud.md +1 -1
  57. package/docs/deliver.md +25 -3
  58. package/docs/fonctionnement.md +9 -5
  59. package/docs/lead.md +9 -7
  60. package/docs/news.md +2 -0
  61. package/docs/orchestrate.md +24 -7
  62. package/docs/perf.md +1 -0
  63. package/docs/qualite.md +14 -4
  64. package/docs/raf.md +62 -7
  65. package/docs/spec.md +5 -2
  66. package/docs/whats-new.md +9 -0
  67. package/package.json +1 -1
  68. package/skills/lead/SKILL.md +1 -1
  69. package/templates/orchestrate/spec.md +2 -2
@@ -53,6 +53,37 @@ function sh(cmd, cwd, timeoutMs, group, env) {
53
53
  });
54
54
  });
55
55
  }
56
+ /**
57
+ * Les fichiers suivis au sha, hors documentation (`docs/`, `.md`, `.txt`), journal `CHANGELOG` (nom exact), plan et `cadence.yaml`, qui citent le chemin du plan (L482) ; vide si aucun ou si git échoue.
58
+ * Chaîne cherchée telle quelle (`-F`), pas comme un motif.
59
+ */
60
+ function planReaders(repo, sha, planFile) {
61
+ if (!planFile)
62
+ return [];
63
+ try {
64
+ const out = git(repo, 'grep', '-l', '-z', '-I', '-F', '-e', planFile, sha, '--', '.', ':(exclude)docs', ':(exclude)*.md', ':(exclude)*.txt', ':(exclude).cadence', ':(exclude)cadence.yaml', ':(exclude)CHANGELOG');
65
+ return out.split('\0').map((l) => l.slice(sha.length + 1)).filter(Boolean);
66
+ }
67
+ catch {
68
+ return []; // git grep rend 1 sans correspondance
69
+ }
70
+ }
71
+ /** Le plan dont les commits seuls sont traversés : aucun si un test le lit, déclaré (L450) ou deviné d'après le code qui cite son chemin sans que `gate.readsPlan` le dise (L482). */
72
+ function crossablePlan(repo, sha, planFile, declared) {
73
+ return declared || planReaders(repo, sha, planFile).length ? undefined : planFile;
74
+ }
75
+ /** La fin de la raison du rejeu quand les commits de plan seuls ne sont pas traversés : déclaré (L450) ou deviné d'après le code (L482) ; vide sinon. */
76
+ function planNote(repo, sha, planFile, declared) {
77
+ const crossing = ' ; les commits de plan seuls ne reprennent pas le verdict d\'un sha dessous';
78
+ if (!planFile)
79
+ return '';
80
+ if (declared)
81
+ return ` (orchestrate.gate.readsPlan : un test lit le plan${crossing.replace(' ;', ',')})`;
82
+ const readers = planReaders(repo, sha, planFile);
83
+ if (!readers.length)
84
+ return '';
85
+ return ` (le code lit le plan, gate.readsPlan absent : ${readers.slice(0, 3).join(', ')}${readers.length > 3 ? ', …' : ''} cite ${planFile}${crossing} — déclarer orchestrate.gate.readsPlan)`;
86
+ }
56
87
  /** Pourquoi l'arbre principal ne peut pas porter la porte au sha : non propre ou à un autre commit ; null s'il le peut. */
57
88
  function mainTreeObstacle(repo, sha) {
58
89
  try {
@@ -142,14 +173,14 @@ async function playGate(ctx) {
142
173
  let recorded = { reason: 'aucune porte de vague enregistrée pour ce sha' };
143
174
  try {
144
175
  // Un test qui lit le plan (orchestrate.gate.readsPlan, L450) voit un autre contenu après un `raf done` : les commits de plan seuls ne sont pas traversés.
145
- recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps, config.gate?.readsPlan ? undefined : ctx.planFile);
176
+ recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps, crossablePlan(repo, sha, ctx.planFile, config.gate?.readsPlan));
146
177
  }
147
178
  catch (e) {
148
179
  recorded = { reason: `état des vagues illisible (${gitFailure(e)})` };
149
180
  }
150
181
  if ('found' in recorded)
151
182
  return reuseRecorded(recorded.found, steps, sha, out);
152
- const replayReason = `verdict rejoué : ${recorded.reason}${config.gate?.readsPlan && ctx.planFile ? ' (orchestrate.gate.readsPlan : un test lit le plan, les commits de plan seuls ne reprennent pas le verdict d\'un sha dessous)' : ''}`;
183
+ const replayReason = `verdict rejoué : ${recorded.reason}${planNote(repo, sha, ctx.planFile, config.gate?.readsPlan)}`;
153
184
  const obstacle = mainTreeObstacle(repo, sha);
154
185
  const mode = ctx.verify?.inPlace;
155
186
  const remote = await tryRemote(ctx, short, replayReason, (exec, emit) => runSteps(steps, exec, emit, config, short));
package/dist/verify.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { constants } from 'node:os';
3
3
  import { headSha, repoStatus, upstreamHead, upstreamIsDefaultBranch } from './git.js';
4
- import { lastDelivery, sharedStateDir } from './state.js';
4
+ import { lastDelivery, recordVerifiedDelivery, sharedStateDir } from './state.js';
5
5
  import { onTermination } from './proc.js';
6
- import { describeCheck, realDeps, retryCheck, TIMED_OUT } from './deliver.js';
6
+ import { announceDelivered, describeCheck, realDeps, retryCheck, TIMED_OUT } from './deliver.js';
7
7
  function killGroup(pgid, sig) {
8
8
  try {
9
9
  process.kill(-pgid, sig);
@@ -115,7 +115,24 @@ export async function verifyCommand(ctx, deps) {
115
115
  for (const r of results)
116
116
  out(resultLine(r));
117
117
  out(summaryLine(results));
118
- return results.some((r) => r.reason !== null) ? 1 : 0;
118
+ if (results.some((r) => r.reason !== null))
119
+ return 1;
120
+ if (ctx.journal) {
121
+ const prev = lastDelivery(ctx.journal.dir); // lu avant l'écriture : la livraison précédente, d'où partent les lots annoncés
122
+ if (recordVerifiedDelivery(ctx.journal.dir, ctx.journal.day, ctx.sha)) {
123
+ out(`verify : ${ctx.sha.slice(0, 7)} avait une livraison rouge, l'effet est vert — inscrit au journal des livraisons`);
124
+ let effects;
125
+ try {
126
+ effects = ctx.journal.effects();
127
+ }
128
+ catch (e) {
129
+ out(`verify : lots livrés non annoncés — ${e.message.split('\n')[0]}`);
130
+ return 0;
131
+ }
132
+ announceDelivered(effects, prev, ctx.sha);
133
+ }
134
+ }
135
+ return 0;
119
136
  }
120
137
  /**
121
138
  * Délai de chaque vérification de « session start », lancées en parallèle : c'est aussi, à la mise à mort
package/dist/yaml-keys.js CHANGED
@@ -1,6 +1,27 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { parse } from 'yaml';
3
+ import { RafError } from './plan.js';
1
4
  /** Une clé de cadence.yaml qui vaut `true` ou `false` : toute autre valeur lève l'erreur donnée. */
2
5
  export function booleanKey(value, refusal) {
3
6
  if (typeof value !== 'boolean')
4
7
  throw refusal;
5
8
  return value;
6
9
  }
10
+ export const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
11
+ export const list = (v) => (v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)]);
12
+ /** Le document yaml de `file` : `undefined` si le fichier manque, `RafError` « illisible » (première ligne de l'erreur du parseur) s'il ne se lit pas. */
13
+ export function readYamlFile(file) {
14
+ if (!existsSync(file))
15
+ return undefined;
16
+ try {
17
+ return parse(readFileSync(file, 'utf8'));
18
+ }
19
+ catch (e) {
20
+ throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
21
+ }
22
+ }
23
+ /** La clé `key` du document yaml de `file` : `undefined` si le fichier manque ou si le document n'est pas une table qui la porte. */
24
+ export function readYamlKey(file, key) {
25
+ const doc = readYamlFile(file);
26
+ return isObject(doc) ? doc[key] : undefined;
27
+ }
@@ -83,7 +83,7 @@ A project shows at most 5 lots (L411), the first ones in the order above (so tho
83
83
 
84
84
  **Reordering the projects (L449).** Above the tree, `ordre des projets (priority:) — glisser, ou Alt+↑ / Alt+↓` and one row per project, in the order of `priority:`: the name (in a `Box` of fixed width, the longest name's: the desktop app's font is proportional, so padding with spaces would align nothing; L495), the progress bar of the band — closed lots over total, filled cells coloured by the share done (red under 33 %, orange under 66 %, green above), the lots in progress with their ids and `+N cette semaine` (L481; `cadence ▮▮▮▯▯▯▯▯▯▯ 3/10 · 1 en cours : L401 · +2 cette semaine`) — so the progress reads without scrolling the band. A project with no `progress` in the tour (an older `cadence`) keeps `7 lots non clos`; `illisible` for a project the tour could not read. It is drawn only where the lots can be reordered (a `Client` region: terminal and desktop app), when the folder has two projects or more, and when the panel is tall enough to keep a row for each project and one lot under it (otherwise the tree has the room). Drag a project onto another (`●` the row you hold, `⇡` / `⇣` where it will land) or click it and press **Alt**+↑ / **Alt**+↓ (↑ / ↓ alone select): the drop runs `cadence lead priority <project> --before|--after <project> <folder>` ([lead priority](lead.md#lead-priority)), which rewrites the `priority:` list of the `cadence.yaml` of the parent folder, comments kept; the panel then reads the plans again and says under the title `✔ ol placé avant cadence — priority: du cadence.yaml est modifiée, à commiter`. This order is the one of the tour, of `cadence orchestrate --continue` and of the panel itself. The panel never commits. A refusal is written under the title with its reason (`✖ /dev/cadence.yaml illisible : …`, a `priority:` that is not a list) and nothing is written; `cadence` missing from the `PATH` says so; a project already at that place says `• … est déjà avant …`. The error row `cadence.yaml` that the tour adds for an unreadable file is not a project and cannot be moved or targeted.
85
85
 
86
- States: `▶ en cours` (doing), `○ libre` (todo, no open `after:`), `⊘ bloqué` (todo, an `after:` is still open); a lot in a wave takes the wave's state and words, as the band writes them (`… en attente`, `⟳ implémente`, `⟳ relit`, `⟳ corrige`, `? QUESTION`, `✔ prêt`, `↩ rendu`, `✖ échec`, `⏸ suspendu`). In a native plan the sub-tasks of an open lot (`L401/2`) are its sub-lots (`├`/`└`); they only read `▶ en cours` or `○ à faire`, and are not counted in the project's `N lots non clos`. In a plan kept by another tool the sub-lots come from `parent:`. An `after:` id that is closed or unknown is struck through and blocks nothing. A project the tour could not read shows `illisible : <error>`; the others stay.
86
+ States: `▶ en cours` (doing), `○ libre` (todo, no open `after:`), `⊘ bloqué` (todo, an `after:` is still open); a lot in a wave takes the wave's state and words, as the band writes them (`… en attente`, `⟳ implémente`, `⟳ relit`, `⟳ corrige`, `? QUESTION`, `✔ prêt`, `↩ rendu`, `✖ échec`, `⏸ suspendu`). In a native plan the sub-tasks of an open lot (`L401/2`) are its sub-lots (`├`/`└`); they only read `▶ en cours` or `○ à faire`, and are not counted in the project's `N lots non clos`. A lot carrying `parent:` (a sub-lot laid flat by `raf add --under`, L465, or the field of a plan kept by another tool) is shown under its parent and counted in `N lots non clos`: it is a lot of its own, not a sub-task. An `after:` id that is closed or unknown is struck through and blocks nothing. A project the tour could not read shows `illisible : <error>`; the others stay.
87
87
 
88
88
  Before the first reading the panel says `Lecture des plans…`; if `cadence lead tour` does not answer it says `Lots illisibles : cadence lead tour n'a pas répondu. Nouvelle lecture dans une minute.` (a failed re-reading keeps the last table); a `cadence` too old to give the `lots` field says `Lots indisponibles : cadence est trop ancien, mettez-le à jour.`; with no open lot, `Aucun lot non clos dans les projets de ce dossier.`. A tree taller than the panel ends with `… +N lots de plus`. Under 60 columns the `after:` list moves to its own line and the title is cut with `…`, never the id or the state.
89
89
 
package/docs/deliver.md CHANGED
@@ -234,8 +234,9 @@ about it anywhere, the script declares it in `CADENCE_DEPLOYED_FILE` (above). Ex
234
234
  ## verify: replay the effect checks, outside a delivery
235
235
 
236
236
  A green delivery says the effect was right *then*. `cadence verify` replays the
237
- `deliver.verify` checks of `cadence.yaml` at any time — no CI, no deploy, no lock,
238
- no journal entry, nothing written.
237
+ `deliver.verify` checks of `cadence.yaml` at any time — no CI, no deploy, no lock.
238
+ It writes nothing, with one exception (below): a green verify at the sha of the last
239
+ red delivery.
239
240
 
240
241
  ```sh
241
242
  cadence verify # one pass, one line per check, then a one-line summary
@@ -249,6 +250,27 @@ cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default
249
250
  verify : 1 effet rouge sur 2 vérifications
250
251
  ```
251
252
 
253
+ **A green verify clears a red delivery of the same sha** (L435). When every check is
254
+ green and the sha verified is the one of the *last* line of `deliveries-failed.log`
255
+ (a `[verification]` delivery that failed on an outage, say, while the effect is there), `verify` appends
256
+ `day sha` to `deliveries.log` and prints `verify : <sha> avait une livraison rouge,
257
+ l'effet est vert — inscrit au journal des livraisons`; the `cadence-hud` panel, which
258
+ shows the red line until `deliveries.log` is newer than `deliveries-failed.log`, drops
259
+ it without delivering again. That line **is** a delivery: as after `cadence deliver`, the
260
+ lots cited by the commits since the previous green delivery are announced (`livré : L… — raf
261
+ done …`) and the « Article <project> à rafraîchir » lot is opened when one has a public title
262
+ (`docs.articles`, L144) — the next delivery starts from this line, so the lots in between are
263
+ said here or never. Nothing is written when a check is red, when the last red
264
+ delivery is another sha's (writing would hide it), when it is not of class `[verification]`
265
+ (a red `[ci]`, `[refus]` or `[deploiement]` stays: checks do not always read the sha, so a green
266
+ verify proves nothing about a commit that was never built or deployed), when there is no red delivery, or when
267
+ `deliveries.log` was already modified after `deliveries-failed.log`. `deliveries-failed.log`
268
+ is never rewritten, so `cadence perf` still counts the red delivery and now also the green one.
269
+ One limit: the red line keeps the sha the delivery *started* from, so the green line written by
270
+ `verify` is that sha too, even when a green `deliver` would have written the sha its script
271
+ declared (`CADENCE_DEPLOYED_FILE`) or the head it moved to — that sha is not kept anywhere once the
272
+ delivery is red; deliver again when the journal must name it.
273
+
252
274
  Exit code: **0** every check green · **1** at least one red effect · **2** nothing
253
275
  to verify or invalid configuration.
254
276
 
@@ -337,7 +359,7 @@ Nothing runs: no worktree, no copy of the dependencies, exit 0. Otherwise the ga
337
359
 
338
360
  **On another machine (L403).** With `orchestrate.gate.host` in `cadence.yaml` the gate is played by ssh on that host, at the sha pushed there, with a dependency cache; it falls back to the local gate when the host does not answer, and replays locally a command whose connection was lost: [docs/remote-gate.md](remote-gate.md).
339
361
 
340
- **A test that reads the plan (L450).** Crossing the plan-only commits assumes that no test reads the plan. On 30-10 avatar-pandora's `plan-page.e2e`, which reads `.cadence/raf.yaml`, went red after the `raf done` of L8 while the head gate, taking over the wave's verdict under that commit, saw nothing. A project whose gate has such a test declares it: `orchestrate.gate: { readsPlan: true }` (or `readsPlan: [e2e/plan-page.e2e.ts]`, the paths of the tests that read it; a non-empty list of relative paths without `..`, anything else is refused when `cadence.yaml` is read). The verdict of a sha *below* plan-only commits is then never taken over: the gate is replayed at the sha asked, and the reason line ends with `(orchestrate.gate.readsPlan : un test lit le plan, …)`. A verdict recorded at the very sha asked is still taken over. Absent or `false`: unchanged. The pre-push hook keeps crossing plan-only commits (its rule, L432, is not changed here).
362
+ **A test that reads the plan (L450).** Crossing the plan-only commits assumes that no test reads the plan. On 30-10 avatar-pandora's `plan-page.e2e`, which reads `.cadence/raf.yaml`, went red after the `raf done` of L8 while the head gate, taking over the wave's verdict under that commit, saw nothing. A project whose gate has such a test declares it: `orchestrate.gate: { readsPlan: true }` (or `readsPlan: [e2e/plan-page.e2e.ts]`, the paths of the tests that read it; a non-empty list of relative paths without `..`, anything else is refused when `cadence.yaml` is read). The verdict of a sha *below* plan-only commits is then never taken over: the gate is replayed at the sha asked, and the reason line ends with `(orchestrate.gate.readsPlan : un test lit le plan, …)`. A verdict recorded at the very sha asked is still taken over. Absent or `false`: unchanged. The pre-push hook keeps crossing plan-only commits (its rule, L432, is not changed here). **Undeclared (L482).** When `readsPlan` is absent, `cadence verify --gate` also looks, at the sha asked, for tracked files outside `docs/`, `*.md`, `*.txt`, `.cadence/`, `cadence.yaml` and the file named exactly `CHANGELOG` that quote the plan's path (`git grep -F`, the path as a plain string). If one does, it behaves as if `readsPlan: true` were declared (plan-only commits are not crossed) and the reason line says `(le code lit le plan, gate.readsPlan absent : <files> cite .cadence/raf.yaml … — déclarer orchestrate.gate.readsPlan)`. Declare the key to get the L450 message; there is no opt-out yet, so a project whose tests merely quote the path as a fixture replays the gate under a plan-only commit. Only `verify --gate` is covered, not `orchestrate --dry-run` nor `lead tour`.
341
363
 
342
364
  ```yaml
343
365
  # cadence.yaml
@@ -31,8 +31,11 @@ A lot without commits yet whose title cites a finding (`constat`, `revue`, `revi
31
31
  to the lead; « partial » passes what exists to the implementation. Then:
32
32
 
33
33
  - `screen: page` only: **spec** (mockup and screen sheet), **mockup review** (`ux-reviewer`, two
34
- passes at most), then **accept**. `screen: component` and `screen: journey` lots take the ordinary
35
- path, unless their sheet already exists.
34
+ passes at most), then **accept**.
35
+ - a visible lot above 0.1 day with `screen: component`, `screen: journey` or no `screen:` (L459): **light
36
+ mockup**, a `spec` session (current screen and target for the touched states) and the same mockup review,
37
+ no **accept**; the `ux` pass after the code checks conformity to the mockup. Other lots take the ordinary
38
+ path (`orchestrate.maquette: false` brings it back for these too).
36
39
  - `spec: true` (a lot with a behaviour to pin down and no screen): **back-end spec** (cases, rules,
37
40
  contracts), **sheet review** (`code-reviewer` in sheet mode, two passes at most), then **accept**.
38
41
  A mixed lot (a page whose sheet has a `sujet` block) gets both reviews and a single second spec pass.
@@ -88,12 +91,13 @@ The budget a wave gives a lot is derived from its estimate, measured on the runs
88
91
  | beyond | 570 k per day, 750 k at least |
89
92
  | `screen: page` | 1.3 M at least, or the ordinary budget + 450 k if that is more |
90
93
  | `spec: true` | ordinary budget + 450 k |
94
+ | visible, above 0.1 day, `screen: component` or `journey` or none (light mockup, L459) | ordinary budget + 450 k |
91
95
  | fix pass and short review | 155 k reserved |
92
96
  | escalation (`orchestrate.escalation` declared, L349) | + 175 k reserved (estimate: one correction and one fresh review on the escalation model) |
93
97
 
94
- The final review is always played; the budget bounds the writing passes. A `screen: component` or
95
- `screen: journey` lot takes the ordinary budget, unless its sheet already exists: it then gets the
96
- `screen: page` budget.
98
+ The final review is always played; the budget bounds the writing passes. A non-visible lot, or one of
99
+ 0.1 day or less, with `screen: component`, `screen: journey` or none takes the ordinary budget; a visible one
100
+ above 0.1 day gets the light mockup budget of the table.
97
101
 
98
102
  ## Permanent rules
99
103
 
package/docs/lead.md CHANGED
@@ -10,7 +10,7 @@ summaries, decides with the human, and leaves the work to fresh short sessions r
10
10
  <img alt="The lead workflow in three lanes (human, lead session, orchestrate program and agents), from the tour of the projects to the close of the session" src="lead-workflow-light.svg">
11
11
  </picture>
12
12
 
13
- Dashed boxes are conditional steps: the precheck runs only for a lot without a commit yet whose title cites a finding (`constat`, `revue`, `review`, `audit`) or another lot (unless `orchestrate.precheck: false`), and the spec, sheet review and accept steps run only for a `screen: page` lot or a `spec: true` lot. The skill itself is `skills/lead/SKILL.md`; this page is its
13
+ Dashed boxes are conditional steps: the precheck runs only for a lot without a commit yet whose title cites a finding (`constat`, `revue`, `review`, `audit`) or another lot (unless `orchestrate.precheck: false`), and the spec steps depend on the lot: a `screen: page` lot or a `spec: true` lot plays the full chain (spec, sheet review or mockup review, accept); a visible lot above 0.1 day without `screen: page` plays spec then the light mockup review, with no accept unless it already has a sealed acceptation (L459). The skill itself is `skills/lead/SKILL.md`; this page is its
14
14
  picture, not its source.
15
15
 
16
16
  ## The steps
@@ -24,11 +24,13 @@ picture, not its source.
24
24
  For each lot the program runs, in fresh sessions and in this order: `precheck → spec → maquette →
25
25
  accept → implement → review → ux`. The **precheck** applies to a lot without a commit yet whose title cites a finding (`constat`, `revue`, `review`, `audit`) or another lot (unless
26
26
  `orchestrate.precheck: false`; L288). The spec steps exist only for a lot that declares
27
- a `screen:` of `page` (a visible lot that creates or changes a whole screen; `screen: component` and `screen: journey`
28
- lots take the ordinary path, straight to the implementation, unless they already have a sheet) or `spec: true` (a lot without a screen
27
+ a `screen:` of `page` (a visible lot that creates or changes a whole screen) or `spec: true` (a lot without a screen
29
28
  that still has a behaviour to pin down: `precheck → spec → fiche → accept → implement → review`);
30
- spec, sheet review and accept apply to `screen: page` and to `spec: true` alike; the other lots go
31
- straight from the precheck (when it applies) to the implementation:
29
+ spec, sheet review and accept apply to `screen: page` and to `spec: true` alike. A visible lot above
30
+ 0.1 day with `screen: component`, `screen: journey` or no `screen:` (and no `spec: true`) plays the
31
+ **light mockup** instead (L459): `precheck → spec → maquette → implement → review → ux`, no accept, and
32
+ the `ux` pass checks conformity to the mockup. The other lots go straight from the precheck (when it
33
+ applies) to the implementation:
32
34
  - **spec**: the `spec-writer` agent draws a static mockup to the project's own style and writes the
33
35
  screen sheet (`fiche.yaml`: fields, states, numbered rules), rendered at 1440 and 390 px;
34
36
  - **fiche** (a lot with a back-end part: `spec: true`, or a screen lot whose sheet has a `sujet` block):
@@ -105,7 +107,7 @@ picture, not its source.
105
107
  | The UX verdict is the human's unless `lead.ux: lead` (parent `cadence.yaml`, default `human`) | then the lead decides from the report and the captures, and brings any doubt to the human |
106
108
  | A session cannot ask the human anything | it stops and reports; the lead brings the question back |
107
109
  | Never push or deliver HEAD above a handed-back lot | HEAD holds code that was not reviewed |
108
- | A lot handed back for its budget or a non-compliant review that the lead had corrected outside the wave is integrated by `cadence orchestrate --resume --wave <wave> --integrate <project>:<lot>` (L370, [`--integrate`](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279)): the fix is made in the kept worktree, then `raf review <lot> "conforme : …"` is run in that worktree and its plan committed there, after the last work commit of the lot; the program rebases, replays the checks, keeps the verdict or makes a short review, and advances the branch | never by hand (L367, 26-10), and never a review recorded in the main repository's plan: `--integrate` reads the worktree's plan and would keep refusing |
110
+ | A lot handed back for its budget or a non-compliant review that the lead had corrected outside the wave is integrated by `cadence orchestrate --resume --wave <wave> --integrate <project>:<lot>` (L370, [`--integrate`](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279)): the fix is made in the kept worktree, then `raf review <lot> "conforme : …"` is run in that worktree and its plan committed there, after the last work commit of the lot; the program rebases, replays the checks, keeps the verdict or makes a short review, and advances the branch; the command is accepted while the wave runs (L532, the wave takes the request between two steps) | never by hand (L367, 26-10), and never a review recorded in the main repository's plan: `--integrate` reads the worktree's plan and would keep refusing |
109
111
  | A plan kept by another tool is read-only for `raf` | its own commands record the lot's state |
110
112
 
111
113
  ## Exit codes
@@ -186,7 +188,7 @@ The commit count of a lot in progress is the one of `raf commits`: plan commits
186
188
  | `attend` | only when a lot waits on its `gate`: `attend L190` |
187
189
  | `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
188
190
 
189
- With `--json`, each project row also carries `lots` (L401): the open lots (`todo`, `doing`, recurring ones left out) as `{ id, title, status, after, parent?, reviewed? }`, in plan order, `after` as the plan writes it (a closed or unknown id is kept as is). `reviewed: true` is set when the plan holds the lot's code review (`review:` posed by `raf review`, L413), absent otherwise: the [cycle strip](cadence-hud.md#the-lots-panel-hud-lots-l401) of the lots panel reads it for its « revue posée » stage. The open sub-tasks of those lots follow their lot as entries of their own (`id` = `<lot>/<n>`, `parent` = the lot, no `after`); those of a closed, dropped or recurring lot are left out with it. In a plan kept by another tool, `parent` is the lot's own `parent:` field. A row in error has no `lots`. With `--json`, a row also carries `closed` (L415): the closed lots (`done`, archived ones included, lots with an `every:` left out, a lot with no `finished:` day left out) as `{ id, finished }` in plan order, absent when there is none; the lots panel compares `finished` with the day of the project's last green delivery to list the closed lots not delivered. The [lots panel](cadence-hud.md#the-lots-panel-hud-lots-l401) of `cadence-hud` reads this field.
191
+ With `--json`, each project row also carries `lots` (L401): the open lots (`todo`, `doing`, recurring ones left out) as `{ id, title, status, after, parent?, reviewed? }`, in plan order, `after` as the plan writes it (a closed or unknown id is kept as is). `reviewed: true` is set when the plan holds the lot's code review (`review:` posed by `raf review`, L413), absent otherwise: the [cycle strip](cadence-hud.md#the-lots-panel-hud-lots-l401) of the lots panel reads it for its « revue posée » stage. The open sub-tasks of those lots follow their lot as entries of their own (`id` = `<lot>/<n>`, `parent` = the lot, no `after`); those of a closed, dropped or recurring lot are left out with it. A lot that carries `parent:` itself (a sub-lot laid flat, `raf add --under` in a native plan, L465, or the field of a plan kept by another tool) carries it as `parent`, with its own id (`AB-1.2` under `AB-1`, not `AB-1/2`). A row in error has no `lots`. With `--json`, a row also carries `closed` (L415): the closed lots (`done`, archived ones included, lots with an `every:` left out, a lot with no `finished:` day left out) as `{ id, finished }` in plan order, absent when there is none; the lots panel compares `finished` with the day of the project's last green delivery to list the closed lots not delivered. The [lots panel](cadence-hud.md#the-lots-panel-hud-lots-l401) of `cadence-hud` reads this field.
190
192
 
191
193
  After them, one line per project that does not declare `orchestrate.test` (L327): `alpha · porte de tests absente : orchestrate.test n'est pas déclaré, la vague ne lancera pas la suite (seule la revue le fera si elle le décide) · commande probable : npm test (à déclarer en orchestrate.test)`. The probable command is read from `package.json` (`scripts.test`), `pyproject.toml` (pytest) or `pom.xml` (`mvn test`) and omitted when none exists; the lead declares `test:` and `build:` in that project's `cadence.yaml` after checking the command is green, one config commit per project. `--json`: `noTest: { probable }` on the project's row, absent when the key is declared. Same line in [`orchestrate --dry-run`](orchestrate.md).
192
194
 
package/docs/news.md CHANGED
@@ -150,6 +150,8 @@ to be reported as one **major** finding per line — a stale document is not lef
150
150
  memory of a sentence. Touching a document is what the program checks; whether its text is true is
151
151
  still the review's reading. The commits of neighbouring repositories (`repos:`) are not audited.
152
152
 
153
+ A commit of type `refactor` (`refactor(L7): …`, no `!`) is left out of that reading (L537): a cleanup or an internal split declared with no change of command, key or output has no document to write, so a lot made only of such commits is no gap, and a lot that also holds a `feat` or `fix` is judged on those commits' files alone. `refactor!:` or a `BREAKING CHANGE:` footer in the body (a breaking change) still counts, and a document written in a refactor commit still satisfies the rule. `cadence orchestrate` reads the same commits, so the librarian is not called for a pure refactor.
154
+
153
155
  A gap is closed by writing the document, or **acknowledged** when the documents really stay true: `raf docs-ok <lot> --reason "why"` (L317; the reason is required) writes a dated `docs_ok:` key on the lot, which `raf check`, `cadence lead tour` and `cadence orchestrate` all read. There is no silent third way: a lot with an open gap does not end ready in a wave.
154
156
 
155
157
  ### The librarian: `doc-keeper`
@@ -64,6 +64,8 @@ refuse the resume, and a refusal writes nothing to `control.log`.
64
64
 
65
65
  **Answering on a live wave (L407).** `--answer <project:lot> "…"` (from the folder that launched the wave, `--wave` when several are live) no longer waits for the wave to end: on 28-10 a lot waited about 15 minutes for its answer because another lot of the wave was still running, and `--resume` refuses a wave that is running. The answer is appended to the wave's queue (`answers.log` in `.cadence/runs/<wave>/`, one JSON line per answer and per answer taken), the command prints `<lot> : réponse déposée` and returns at once. The live wave reads the queue every 2 seconds and each time a lot finishes, applies the answer to the lot (the same state as `--resume --answer`: the answer joins the brief of its next session) and replays it as one more line of the pool, as soon as a session slot is free; a lot with no worktree never plays together with another lot of its repository. The documented form `--resume --wave <wave> --answer …` does the same when that wave is still running (it used to be refused with `tourne encore`); combined with another resume option (`--budget`, `--drop`, `--integrate`, `--continue`) it is still refused. Refused (exit 2) before anything is written: a lot that is unknown, that several projects carry, or that is not waiting for an answer (`question`); no live wave in that folder (the message gives the `--resume --wave <wave> --answer` form for a stopped wave); a wave started before L407, whose snapshot does not read the queue. `--answer` alone does not combine with `--drop`, `--stop-after-current`, `--budget`, `--dry-run`, `--continue` or lots to launch. An answer whose lot is no longer waiting when the wave reads it (dropped, or already answered by an earlier line) is discarded and the journal says so (`réponse écartée`), it is never played twice; if the wave finishes between the check and the write, the answer is withdrawn and the command says to use `--resume`. Not covered: a lot that depends (`after`) on the answered lot and was left waiting in the pool is not started by this wave when the answer makes the lot ready; `--resume` plays it.
66
66
 
67
+ **Integrating a lot on a live wave (L532).** `--resume --wave <wave> --integrate <project:lot>` (from the folder that launched the wave) no longer fails with `la vague … tourne encore` when that wave is still running: on 2 November the lead had a lot (L527, then L435) reviewed in its worktree and could not integrate it until the wave ended, hours later under `--continue`. The request is appended to the wave's queue (`integrates.log` in `.cadence/runs/<wave>/`, one JSON line per request and per request taken), the command prints `<lot> : intégration déposée` and returns at once. The live wave reads the queue every 2 seconds and each time a lot finishes, and replays the lot at the `integrate` step as one more line of the pool, as soon as a session slot is free (`<project>:<lot> → suspended — intégration reprise (--resume --integrate)` in the journal, or `… : revue conforme du lead hors vague, <sha>` for a lot the lead corrected and reviewed, L370). The lot is checked twice, when the command is typed and when the wave takes the request: it must be handed back at the integration step, or handed back elsewhere and reviewed `conforme` by the lead in its worktree at the last commit; a request whose lot no longer qualifies by then (already taken by an earlier line, a commit after the review, tracked file modified in the worktree) is discarded and the journal says so (`intégration écartée (…)`), it is never played twice. Refused (exit 2) before anything is written: a lot that is unknown, that several projects carry, named twice, or not eligible (same message as the resume); a wave started before L532, whose snapshot does not read the queue. Combined with another resume option (`--answer`, `--budget`, `--drop`, `--continue`) or with `--replay`, the command is still refused with `tourne encore`. If the wave finishes between the check and the write, the request is withdrawn and the command says to use `--resume`.
68
+
67
69
  **Adding a lot to a live wave (L421).** A second `cadence orchestrate <project:lot>` in a repository that a live wave holds is refused (`une orchestration y est déjà en cours (<wave>, pid …)`), and the lead used to wait for the whole wave to end. The refusal now names the way out when that wave was launched from the same folder: `… ; pour y ajouter ce lot : cadence orchestrate --add <project>:<lot> --wave <wave>`. `--add <project:lot> [--wave <wave>]` (repeatable; from the folder that launched the wave, `--wave` when several are live) checks each lot like a lot given at launch (known, open, dependencies, Node, `repos:`), with three differences: a repository held **by that wave** is accepted (its dirty tree and its push guard are the wave's own), a dependency counts when it is a lot of the wave or an earlier `--add` of the same call, and the remaining budget of the wave (`budget − consumed`) must hold the budget of the lot. Then it appends the lot to the wave's queue (`adds.log` in `.cadence/runs/<wave>/`, one JSON line per request and per request taken), prints `<lot> : ajout déposé` and returns at once. The live wave reads the queue with the answers (every 2 seconds and each time a lot finishes), checks the lot again at that moment, and adds it: the lot is written in the wave state and in `wave.json`, the repository lock and push guard are taken if the wave does not hold the repository (any more), and the journal says `<lot> : ajouté à la vague <wave>`. The lot then plays as one more line of the pool as soon as a session slot is free; in a repository in `orchestrate.worktrees: true` mode with a cap above one it plays in its own worktree at once, in parallel with the lot already playing there (L527, every lot of such a repository has its own); otherwise a lot of a repository where another lot of the wave is playing waits for it (never two sessions in one repository), and plays in its own worktree only when every lot still to play in that repository does. Refused (exit 2) before anything is written, nothing queued for any lot as soon as one is refused: a lot unknown or already finished, a lot already in the wave or already queued, a dependency neither done nor in the wave, a repository held by another wave, a budget that does not fit, no live wave in that folder, several live waves without `--wave`, a wave started before L421 (its snapshot does not read the queue). `--add` combines with nothing but `--wave`. A request that no longer holds when the wave reads it (lot done meanwhile, repository taken by another wave, budget spent, `--stop-after-current` requested, usage limit, incident) is discarded and the journal says so (`ajout écarté (<cause>)`) — a request kept waiting for its repository is checked again at the moment it is taken, so a stop requested or a budget spent while it waited discards it too, and the lot never enters `wave.json`; the same goes for a request still queued when no session is left to play (`la vague a fini de jouer`) — if the wave ends between the check and the write, the command withdraws the request and says to launch the lot in a new wave. A legitimate commit of an added lot on the main branch of its repository is not an incident for a session already running in a sibling repository (L456). With `--continue`, the lots added by hand count as lots of the wave and are not drawn a second time. The case of `--resume --integrate` refused while another wave holds the repository is not part of `--add`: the resume waits for that wave (L443).
68
70
 
69
71
  **`--goal "…"` and `goal:` — the objective of the briefs when the title is stale (L258)**: the first line of the `{{objectif}}` of every
@@ -108,7 +110,7 @@ command warns `ne lit pas les demandes de contrôle` on the error output.
108
110
  plan too); then, each time the lots in play are finished, the wave **draws the next ready lots of the plan** — up to the
109
111
  session cap — and plays them in the same wave (same budget, same state folder, journal line `continue : tire …`). A lot
110
112
  is drawn when it is `todo`, its `after` is lifted, it is not recurring, neither its title nor a note says « à décider »
111
- (« à décider avec … », « (à décider) »: a lot waiting for a human decision), it is not already in the wave, the project's draw setting does not hold it back (L390, below), and its own budget (derived from its estimate; a lot with `screen: page` is worth 1.3 M whatever its estimate (`component` and `journey` have their ordinary budget), or its ordinary budget plus the 450 k spec-stage allocation if that is more, and a lot with `spec: true` gets that allocation on top of its ordinary budget)
113
+ (« à décider avec … », « (à décider) »: a lot waiting for a human decision), it is not already in the wave, the project's draw setting does not hold it back (L390, below), and its own budget (derived from its estimate; a lot with `screen: page` is worth 1.3 M whatever its estimate, or its ordinary budget plus the 450 k spec-stage allocation if that is more, a visible lot with another `screen:` (or none) above 0.1 day has its ordinary budget plus that allocation (light mockup, L459), and a lot with `spec: true` gets that allocation on top of its ordinary budget)
112
114
  fits in the budget left — otherwise the next one that fits is taken (a lot that does not fit in what is left of the current
113
115
  round is not dropped: it is tried again at the next round, once the budget allows it). A round takes at most **one lot per
114
116
  repository** while other repositories have ready lots — the pool plays the lots of one repository one after the other, so a
@@ -310,10 +312,10 @@ lot that already has commits is never pre-checked (resuming it is legitimate), a
310
312
  ## The spec step of a lot with `screen:`
311
313
 
312
314
  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.md#raf)) gets, when it is `page`, one more session, `spec`, **after** the pre-check and **before**
313
- 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
315
+ 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 unless it is visible and above 0.1 day (light mockup, below) (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
314
316
  (`spec (sonnet)`), its command line and its brief `templates/orchestrate/spec.md`.
315
317
 
316
- - **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.
318
+ - **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; since L459, for a visible lot without `spec: true`, only the sealed acceptation keeps the full chain (a sheet alone is also what the light mockup writes). The `spec: true` step and the back part of a mixed lot do not change. The next measure covers 8 `page` lots. Since L459 a *visible* lot above 0.1 day with `component`, `journey` or no `screen:` no longer takes the ordinary path: it plays the light chain of [The light mockup](#the-light-mockup-l459) (no `accept`, ordinary budget plus 450 k); a non-visible one, or one with `orchestrate.maquette: false`, still does.
317
319
  - **The session** (L171): Sonnet, the `spec-writer` agent (it writes only the spec folder, never application code), Playwright loaded like for `implement`. It reads the lot,
318
320
  the project's CLAUDE.md and its charter (CSS, neighbouring components, `docs/qa/expectations.md`, existing routes and API contracts), then writes in
319
321
  `<orchestrate.spec.dir>/<écran>/` (`docs/specs` by default): `maquette.html` (static HTML + CSS to the project's own style; one section `#etat-vide`, `#etat-chargement`,
@@ -331,6 +333,19 @@ the first implementation: `precheck → spec → maquette → accept → impleme
331
333
  - **Under the threshold**: if the session finds that the lot changes no screen, it gives the reason in one line (`sousSeuil`), writes no mockup, and the lot goes on to `implement` as before; the reason is
332
334
  kept as a warning of the lot. On resume the spec step is skipped only when a sheet of the lot (`ecran.lot` is the lot) already passes the program's checks (`cadence spec check`, mockup, captures) and carries no costly hypothesis; otherwise the spec is played again. The sheet path in the report is relative to the repository (an absolute or `./` path is normalised, a path outside the repository is a finding).
333
335
 
336
+ ## The light mockup (L459)
337
+
338
+ (L459, spec [docs/specs/maquette-legere-lots-visibles/fiche.yaml](specs/maquette-legere-lots-visibles/fiche.yaml)) A mockup validated by the `ux-reviewer` **before** the code was only for the lots of a whole page; the retouches and screen reviews (a visible lot with `screen: component` or `journey`, or none) were coded first and discovered their UX defects in the `ux` pass (1.6 `ux` sessions per visible lot on 30-10, target 1.2). Those lots now get a light mockup:
339
+
340
+ - **Which lots**: visible, without `spec: true`, with an estimate above 0.1 day (the express lane, below, stays one session), and whose `screen:` in the plan is not `page` (`component`, `journey` or absent). It is read from the plan at every call, with no field in the lot's state: it survives a resume and the writing of the sheet. `orchestrate.maquette: false` takes the lot back to the ordinary path (no `spec`, no `maquette`). A lot with `screen: page` keeps its full chain, unchanged. A lot of `spec: true` keeps its back chain. A lot that entered the full chain before L459 with an `acceptation-<lot>.json` next to a screen sheet keeps it too (`accept` already played, its sealed tests checked, budget of 1.3 M), even if it now matches these rules.
341
+ - **The chain**: `precheck → spec → maquette → implement → review → ux` (`review-small` alone for a small lot, as for any small visible lot). There is **no `accept` step**, even when `orchestrate.accept` is declared: a retouche carries no behaviour rules to lock under red tests.
342
+ - **The session `spec`** writes a sheet limited to the states, fields and rules the lot touches, plus `maquette.html` with one section `#etat-<état>` per touched state, and, per touched state and per width (1440 and 390), two captures: `actuel-<état>-<largeur>.png`, the current screen, and `maquette-<état>-<largeur>.png`, the target. If the current screen cannot be captured (the application does not start, the page is protected), the agent says so under `nonVerifie` and bases the mockup on the code; it never invents a capture. Only a lot that changes nothing the user can see (no screen, component, state, text or style: an internal rename, a refactoring with the same rendering) answers `sousSeuil` and goes to `implement`, as for a page; the brief says that a change to a component of an existing screen is not under the threshold, even though it adds no page.
343
+ - **What the program checks** is what it checks for a page: `cadence spec check`, a section and a `data-testid` per state and field, the target captures at both widths, commit, clean tree, and the costly-choice-is-a-question rule. It does **not** check the `actuel-*` captures: their name is a convention of the brief, a free file like `contraste.md`.
344
+ - **The review** is the loop of [The mockup review](#the-mockup-review-before-any-code): up to `orchestrate.maquetteReviews` reviews (3 by default), blocking and major findings sent back to `spec`, the lot handed back with `maquette non conforme après N revue(s) : …` if the last one is still not compliant. The brief asks the reviewer to sweep the touched states only and to compare each target with its `actuel-*` capture.
345
+ - **After the code**, the `ux` pass (or the single `review-small` pass of a small lot) is the conformity pass of the sheet lots (see « The UX pass becomes a conformity pass »): the running app against the mockup and its captures, state by state. Without `orchestrate.ux`, or with an application that does not start, no `ux` session is played and the note « UX à faire par le lead » is written.
346
+ - **Budget**: the ordinary budget of the estimate plus the 450 k spec-stage allocation (`--dry-run` says « plus 450000 pour les étapes spec et maquette »), not the 1.3 M of a page. The 450 k is the allocation of a `spec: true` lot, taken as the first figure (hypothesis, to measure again over two windows).
347
+ - **Measure**: `cadence perf` is unchanged: « sessions ux par lot visible » (see [perf](perf.md)) is judged against 1.2 over two windows after the change; an empty window stays empty.
348
+
334
349
  ## The spec step of a lot with `spec: true`
335
350
 
336
351
  A lot that changes a behaviour but has no screen (`raf add … --spec`, or `raf spec <id> on`, see [raf](raf.md#raf); L188, design: [docs/superpowers/specs/2026-10-11-etape-spec-back-design.md](superpowers/specs/2026-10-11-etape-spec-back-design.md)) is specified **before** it is coded, on a sheet, like a screen:
@@ -348,7 +363,7 @@ A lot that changes a behaviour but has no screen (`raf add … --spec`, or `raf
348
363
  Findings are ranked blocking / major / minor, each with its measurement. A blocking or major one sends the sheet back for a **second `spec` session** (its brief carries the findings), then the sheet is reviewed again; still not conforming, the lot is handed back with what remains (`fiche non conforme après 2 revues : …`).
349
364
  The `spec` session has **one budget of two sessions**, shared by the program's check and the reviews: if the check already used the second one, a non-conforming review hands the lot back at once (`fiche non conforme et la spec a déjà joué 2 sessions`), with no second review. A question to the lead (the session's own, or a costly hypothesis) and its answer open a new budget.
350
365
  Minor findings become notes of the lot (`Revue de la fiche — mineur : …`, one per finding, committed with the plan; on a read-only plan they become `[note à poser]` proposals), not lines of the final table. This verdict is **not** `raf review`: the review of the code keeps that role. `orchestrate.fiche: false` skips the step.
351
- - **A lot with a screen and a behaviour**: it is a lot with `screen:` whose **sheet** has a back part (a `sujet` block next to `ecran`, same folder), never a `spec: true` beside `screen: page` (`raf spec <id> on` only says so on such a lot and changes nothing; on `component` or `journey`, which follow the ordinary path since L209, `spec: true` makes a back-end spec lot, ordinary budget plus 450 k). The program reads the sheet, not the plan: as soon as the sheet has a `sujet` block (the contracts), the `code-reviewer` also reviews it,
366
+ - **A lot with a screen and a behaviour**: it is a lot with `screen:` whose **sheet** has a back part (a `sujet` block next to `ecran`, same folder), never a `spec: true` beside `screen: page` (`raf spec <id> on` only says so on such a lot and changes nothing; on `component` or `journey`, `spec: true` makes a back-end spec lot, ordinary budget plus 450 k, instead of the [light mockup](#the-light-mockup-l459) such a visible lot plays without it). The program reads the sheet, not the plan: as soon as the sheet has a `sujet` block (the contracts), the `code-reviewer` also reviews it,
352
367
  the chain is `spec → fiche → maquette → accept`, and the findings of the two reviews go together to a single `spec` session, which counts for one in the budget shared by both (`orchestrate.maquetteReviews` sessions, 3 by default, see the mockup review below). The brief of the spec of a lot with a screen offers the back part (and joins its template) without making it compulsory; a lot whose sheet has no `sujet` keeps its chain, without `fiche`. `--dry-run` cannot list `fiche` for a lot with a screen (the sheet does not exist yet).
353
368
  - **Acceptance and code**: `accept` is the screen's step, with a back brief (`accept-back.md`: cases call the contracts of `contrats[]` as close as possible, external sources simulated from `entree`; no `data-testid`). `implement`, `fix` and `review` receive the sheet (cases, rules, contracts) as for a screen, without mockup; the review also checks that each contract is held (output, exact codes and messages of the errors).
354
369
  - **Hypotheses**: a lot without a screen has no News entry. Once the sheet is final — after its review and the second `spec` session if there was one, just before `accept` or `implement`, and also for a sheet already present on a resume — the program writes **one note of the lot per hypothesis** (`Hypothèses (fiche <sujet>) — R3 : … ; si faux : …`, not duplicated on a resume) and commits it with the plan; on a plan kept by another tool the lines become proposals (`[note à poser] …`). The end-of-wave table lists them under each lot (`hypothèse de la fiche : …`) for the lead to relay.
@@ -835,7 +850,9 @@ on elsewhere, so `cadence deliver` accepts it (a repository with a lot still to
835
850
  held until the wave ends). `cadence deliver` also reads the wave's own state when a lock is still held: it refuses only if the wave still has a lot running, queued or suspended **in this repository**, and goes ahead when the lots left are elsewhere (an unreadable wave state still refuses). They share a **cap on simultaneous sessions**, counted
836
851
  across all live waves: 2 by default, `--max-sessions N` (or `CADENCE_MAX_SESSIONS=N`) to change it — give
837
852
  the **lowest cap of the live waves applies to all of them** (L395): each wave counts ALL live sessions, whatever their slot, and waits while that
838
- count has reached the lowest cap declared by any live wave (re-read at every turn: it rises again when the wave with the low cap ends), so a wave at 4 next to a wave at 3 plays 3 sessions, never « 4/3 en cours » (on 28-10 L51 waited 16 min 28 s behind it). The `en attente d'un créneau` line and the free slots (`créneaux libres`) use that common cap. The sessions the lead started by hand and declared with `cadence lead hold` (L424, [lead.md](lead.md#lead-hold-and-release)) count in that total like wave sessions, until their `release` or their time limit: the wave waits for a slot instead of starting a session over the cap, and the line names them (`2/3 en cours : w1, session du lead correctif-L12`). A session that finds no free slot **waits** (the wave is not refused; the journal says
853
+ count has reached the lowest cap declared by any live wave (re-read at every turn: it rises again when the wave with the low cap ends), so a wave at 4 next to a wave at 3 plays 3 sessions, never « 4/3 en cours » (on 28-10 L51 waited 16 min 28 s behind it). The `en attente d'un créneau` line and the free slots (`créneaux libres`) use that common cap. The sessions the lead started by hand and declared with `cadence lead hold` (L424, [lead.md](lead.md#lead-hold-and-release)) count in that total like wave sessions, until their `release` or their time limit: the wave waits for a slot instead of starting a session over the cap, and the line names them (`2/3 en cours : w1, session du lead correctif-L12`). **Fairness between waves (L531).** The sessions waiting for a slot, in every live wave, register in `waiters/` of the cadence home and a freed slot goes to the first of them, never to whoever polls first: (1) the next step of a lot already playing (implementation done, review, fix…) before the first step of a lot just drawn; (2) then the project that is highest in `priority:` (`--priority` or the `cadence.yaml` of the launch folder; a project not listed comes last); (3) then the oldest wait. A free slot with no one waiting before is taken at once. A waiter whose process died is ignored and dropped; a wait abandoned (`--drop`, `--stop-after-current`) is unregistered. The order is over the slot only: the pool and the fill (L527) are unchanged, and a lot drawn ahead of its turn simply waits for its first slot behind the steps in progress.
854
+
855
+ A session that finds no free slot **waits** (the wave is not refused; the journal says
839
856
  `en attente d'un créneau de session depuis …`, repeated every minute, then `créneau de session obtenu après …`). A free slot is always taken, even when a request is already written (relaunching a cut review is not a wait; the request applies afterwards). Once the wait has really started (a first try found no free slot), it reads the wave's requests at every turn (L332): a `--drop` aimed at that lot ends it (the slot is never taken, no session starts, the lot is *handed back* `retiré de la vague (--drop)` like a lot not started) and a `--stop-after-current` ends it too (the lot is *suspended* `arrêt demandé (--stop-after-current)`, the wave **interrupted** and resumable); a `--drop` aimed at another lot does not. The step announced but never started is removed from the state and the lead's answer it was carrying (`pendingAnswer`, taken out of the lot when the brief was built) is put back: `--resume` replays the step with the answer in its brief, and a lot handed back by `--drop` says « la réponse du lead (« … ») n'a pas été jouée » in its outcome. When it is the relaunch of a session already run in the step (a cut review relaunched in a fresh session, L256; the formatting retry) that was waiting, the step ends `failed` with its cause (« relance en session neuve non faite : … ») and keeps what the first session left: its id, its tokens (step and wave budget), its stderr in `<n>-<kind>.first.err` (and its output in `.first.json` for the formatting retry).
840
857
  `cadence orchestrate --waits [--since YYYY-MM-DD]` adds those waits up from the journals of `.cadence/runs/` of the launch folder — per day (cumulated wait, number of waits, waves, quota stops — one per "→ suspended — quota atteint" line of the journals, counted on the day of that line, so a wave resumed and stopped again on another day counts once on each day, not once as a wave, tokens) and per wave that waited (the longest wait, a wait that never got its slot) — to decide the cap from figures; each wait is paired with its lot (the journal line carries `project:lot`), so two lots of one wave waiting at the same time (one per repository) are each counted once, and only a wait that never got its slot is reported as such; journals from before this carry no lot and are paired per wave. it reads only, and refuses any other action in the same call. The figures of 2026-10-12 and the cost of a third slot are in `docs/mesures/attente-creneaux-2026-10-12.md`.
841
858
  A slot or a registry entry is owned by a pid **and** its start time: a reused pid is a dead owner. Wave
@@ -855,7 +872,7 @@ upstream ref and `git ls-remote` are compared with the "before" (a `git checkout
855
872
  `CADENCE_ORCHESTRATED` is set.
856
873
 
857
874
  **Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
858
- shown apart. Each lot also has its own budget, the 90th percentile of the *total* measured per lot on `.cadence/runs` (all the waves a lot went through, 250 lots) for its category (L194): an ordinary lot of up to 0.5 day gets 600 k (floor: a write pass, its review, one fix pass and the short review after it, 155 k being reserved for those last two; measured p90 374 k on 11-10, 451 k on 16–22-10: 3 of 145 lots over 540 k, all maritime-atlas, hence 600 k, L247; remeasured on 17–23-10: 0 of 309 lots over budget, p90 452 k, so unchanged, L280; and on 18–24-10: 0 of 322 over, p90 460 k at 600 k, 557 k at 750 k, 744 k at 1.3 M, unchanged again although the tokens per lot rose by a third at estimates 0.25 and 0.5: the rise is the write pass, which the budget bounds with room, L312), 0.5 to 1 day 750 k (p90 732 k), then 570 k per day of estimate, 750 k staying the floor until that exceeds it, i.e. up to 1.3 day (p90 1.14 M for 2 days); a lot with `screen: page` gets 1.3 M whatever its estimate (`component` and `journey` keep the ordinary budget; p90 1.28 M: spec, mockup, accept, conformity UX and passes), or its ordinary budget plus 450 k beyond that; a lot with `spec: true` gets its ordinary budget plus 450 k (the back chain measured on L120: 388 k); shown by
875
+ shown apart. Each lot also has its own budget, the 90th percentile of the *total* measured per lot on `.cadence/runs` (all the waves a lot went through, 250 lots) for its category (L194): an ordinary lot of up to 0.5 day gets 600 k (floor: a write pass, its review, one fix pass and the short review after it, 155 k being reserved for those last two; measured p90 374 k on 11-10, 451 k on 16–22-10: 3 of 145 lots over 540 k, all maritime-atlas, hence 600 k, L247; remeasured on 17–23-10: 0 of 309 lots over budget, p90 452 k, so unchanged, L280; and on 18–24-10: 0 of 322 over, p90 460 k at 600 k, 557 k at 750 k, 744 k at 1.3 M, unchanged again although the tokens per lot rose by a third at estimates 0.25 and 0.5: the rise is the write pass, which the budget bounds with room, L312), 0.5 to 1 day 750 k (p90 732 k), then 570 k per day of estimate, 750 k staying the floor until that exceeds it, i.e. up to 1.3 day (p90 1.14 M for 2 days); a lot with `screen: page` gets 1.3 M whatever its estimate (p90 1.28 M: spec, mockup, accept, conformity UX and passes), or its ordinary budget plus 450 k beyond that; a visible lot with `component`, `journey` or no `screen:` above 0.1 day gets its ordinary budget plus 450 k (light mockup, L459; figure of the `spec: true` chain, to measure again); a lot with `spec: true` gets its ordinary budget plus 450 k (the back chain measured on L120: 388 k); shown by
859
876
  `--dry-run` with its basis): it bounds the *writing* passes, never the reviews. A lot that spent it gets no further implementation or
860
877
  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
861
878
  the remaining budget can also pay the fix and the short review that follows it (155 k tokens reserved: the p90 of a fix
@@ -949,7 +966,7 @@ orchestrate:
949
966
  build: npm run build # run BEFORE the tests (L333); both results go to the reviewer, who does not redo them (optional)
950
967
  worktreeDeps: [node_modules, .venv] # untracked dependencies that `cadence verify --gate` copies into its throwaway worktree (L348, L389; the gate runs in the main tree instead when it is clean at the sha, `verify.inPlace`, `docs/deliver.md`); absent: the `node_modules` of every level and the `.venv` are copied (never data or `.env`, L392), the other ignored folders are neither copied nor linked (L455) (optional)
951
968
  gate: { host: build-box, install: npm ci } # play the gate (build, tests, baseline, replay alone) on another machine through ssh, with a dependency cache and a local fallback, see docs/remote-gate.md (L403); `host` alone is not `readsPlan` (both optional)
952
- gate: { readsPlan: true } # a test of the gate reads the plan (`.cadence/raf.yaml`): `cadence verify --gate` no longer takes over a wave's verdict under commits that change only the plan (L444), it replays; `true` or the list of the paths of the tests that read it; absent or `false`: those commits are crossed (L450, `docs/deliver.md`) (optional)
969
+ gate: { readsPlan: true } # a test of the gate reads the plan (`.cadence/raf.yaml`): `cadence verify --gate` no longer takes over a wave's verdict under commits that change only the plan (L444), it replays; `true` or the list of the paths of the tests that read it; absent: those commits are crossed unless tracked code quotes the plan's path (then it replays and says `gate.readsPlan absent`, L482); `false` is read as absent (L450, `docs/deliver.md`) (optional)
953
970
  precheck: true # default: before the first implementation of a lot with no commit whose title cites a finding or an earlier lot, and whose title or `goal:` cites a finding (`constat`, `revue`, `review`, `audit`) or a plan id (an earlier `L<number>`, also followed by a hyphen as in `L12-L15` or `L12-bis`; any other id such as `Q-CAN5`; `project:L<number>`), a read-only Sonnet session checks whether the deliverable is already in the repository (see below); false skips it, local runs it on claude-local (Ollama) with Sonnet as fallback, for every lot with no commit whatever its title (L296); a lot created after the last code commit of the repository is not pre-checked, local or Sonnet, unless it cites a lot that already has commits (L322)
954
971
  accept: npx vitest run -c vitest.acceptation.config.ts # accept step (see « The accept step »): the dedicated command that runs ONLY the acceptance tests of the lot (optional; without it the step is skipped with a warning)
955
972
  maquette: true # default: UX review of the mockup between spec and accept (see « The mockup review »); false skips it
package/docs/perf.md CHANGED
@@ -32,6 +32,7 @@ current window, with at least 10 classified passes) is flagged by a `⚠` even w
32
32
  A journal joins its plan by the project name (the path of the project relative to the folder) and the `lot` field of the journal; an id `Q4/accueil-4-ux12` joins the plan lot `Q4`. Journals with no plan lot are counted aside (`N lot(s) de journal sans plan`) and never counted as finished outside the wave.
33
33
  An unreadable plan skips its project with a warning on the error output and the other projects are read; the indicators that read the plans are `n/d` only when no plan is readable.
34
34
  - **Sessions that see the application.** `sessions ux par lot visible` counts, per visible lot rendered in the window, its `ux` steps that ended `ok` **and** the `review-small` steps that ended `ok` of a small visible lot (journal `small: true`): the single pass of a small visible lot is its only ergonomics review, run with the application launched (L283; measured on 24-10, 139 of 140 small lots were visible and the line showed 0.0, −97 %, a false signal). The `review-small` of a lot that is not small is only a code review and is not counted; a journal written before the `small` field counts none.
35
+ The formula is unchanged by the light mockup (L459, [docs/orchestrate.md](orchestrate.md#the-light-mockup-l459)): the lots that now play `spec` and `maquette` before the code are expected to lower this value from 1.6 (30-10) towards 1.2, to be judged over two windows after the change; a window with no visible lot rendered leaves the line empty (`n/d`).
35
36
  `part des lots visibles revus par une étape ux` is the share of the visible lots rendered in the window with at least one `ux` step that ended `ok` (so a small visible lot, reviewed in `review-small`, is not in it). It is informational: no `⚠`, no quality twin (`interventions.dedicatedUx` in `--json`), `n/d` without a visible lot.
36
37
  - **Pre-check share of `oui`** (L288). `part de « oui » du précheck` is the share of the lots rendered in the window whose journal records a pre-check verdict (`precheckVerdict`) that is `oui` (the deliverable was already there; an unproven `oui` is recorded `partiel`). Compare the current window with the previous one before and after a change of `orchestrate.precheck` or of the pre-check criterion (the title rule of L288): a share that stays low means the pre-check costs more than it saves. A journal older than L288 has no `precheckVerdict`: its verdict is read back from the lot (`livrable déjà présent` → `oui`, pre-check finding in the brief → `partiel`, otherwise `non`), so the window before the change is measurable. A journal written since L288 whose pre-check report was unreadable (it keeps the `contrôle préalable illisible` warning, no `precheckVerdict`) has no verdict and is not counted, not read back as `non` (L293). Lots with no successful `precheck` step are not counted; informational, no `⚠`, no quality twin (`precheck.yes` in `--json`), `n/d` without a verdict.
37
38
  - **Pre-check measured per lot** (L322). Under the section « Contrôle préalable », for the current window: the split `oui` / `partiel` / `non` of the rendered lots (`précheck : oui 1 · partiel 0 · non 1`); the local sessions (`précheck local : n sessions, k échecs, m min`: how many, how many ended without a usable report, wall time; a local session that ended `ok` but whose report was unreadable, so that the lot fell back on Sonnet, counts as a failure, and its line says `local échec`, `--json` `local.status: "illisible"`); the Sonnet sessions of the pre-check, which are the fallback of a local failure and the confirmation of a local `oui` (`précheck Sonnet : n sessions, t k tokens, m min`; the local model counts no token); the gain of the `oui` (`gain des « oui » : n sessions d'implémentation évitées (≈ t k tokens)`: the lots handed back without implementation, times the median of the `implement` steps of the window; without any, the tokens read `(gain en tokens : n/d)` and are `null` in `--json`); then one line per lot (`projet:lot · verdict · local échec 10 min · Sonnet 2 min, 20 k tokens`). Nothing is printed for a window without pre-check session or verdict. In `--json`: `precheck.verdicts`, `precheck.cost` (`local`, `sonnet`, `avoided`, each `{ current, previous }` as a whole) and `precheck.lots` (current window). Read it before changing `orchestrate.precheck` or the lots it applies to: the cost of the pre-check is what it spends (`local` minutes, `sonnet` tokens), its worth is `avoided`.
package/docs/qualite.md CHANGED
@@ -24,7 +24,7 @@ Putting a threshold on existing code turns hundreds of findings red on day one,
24
24
  threshold until it means nothing, or sprinkle `eslint-disable`. The ratchet keeps the thresholds honest instead:
25
25
 
26
26
  1. `node scripts/qualite.mjs --init` measures once and writes `qualite-baseline.json`, a flat map sorted by key,
27
- versioned with the code. A key is `tool|file|rule` (`eslint|src/cli.ts|max-lines`, `knip|src/audit.ts|exports`,
27
+ versioned with the code. A key is `tool|file|rule` (`eslint|src/plan.ts|max-lines`, `knip|src/audit.ts|exports`,
28
28
  `jscpd|src/config.ts`) and its value a count. Two metrics have a `…|total` or `…|lignes` twin that sums the figures:
29
29
  for ESLint, every key has a `…|total` (the sum of the figures measured, a file of 1397 lines records 1397, three
30
30
  functions of cognitive complexity 463, 20 and 18 record 501), and for jscpd every file has a `…|lignes` (the lines
@@ -63,9 +63,19 @@ anything, and the ratchet closes them:
63
63
  `modifié`). That is not only the three files this repository uses: ESLint 9 loads `eslint.config.js` **before**
64
64
  `eslint.config.mjs` (then `.cjs`, `.ts`, `.mts`, `.cts`), knip reads `knip.json`, `knip.jsonc`, `.knip.json`,
65
65
  `.knip.jsonc`, `knip.ts`, `knip.js`, `knip.config.ts`, `knip.config.js` and `package.json#knip`, jscpd reads
66
- `.jscpd.json` and `package.json#jscpd`, and knip as jscpd respect `.gitignore`. Of `package.json`, only the `knip` and
67
- `jscpd` keys are compared. Changing a threshold is a decision of the plan, delivered alone; a lot judged by those
68
- thresholds cannot move them. The lot that adopts the tools (no baseline at its start) introduces its configs freely.
66
+ `.jscpd.json` and `package.json#jscpd`, and knip as jscpd respect `.gitignore`. Changing a threshold is a decision of
67
+ the plan, delivered alone; a lot judged by those thresholds cannot move them. The lot that adopts the tools (no baseline at its start) introduces its configs freely.
68
+ The comparison also covers what knip and vitest read: `vitest.config.*`, `vitest.workspace.*`, `vite.config.*` (all of
69
+ `ts`, `mts`, `cts`, `js`, `mjs`, `cjs`), `tsconfig.json`, every nested `.gitignore` (at the start of the lot or now;
70
+ `node_modules` and `.git` are not walked; an untracked one that another `.gitignore` rule already ignores, such as the `types/.gitignore` Claude Code generates, is not counted, while one that ignores itself with `*` is, and so is one that a
71
+ negation pattern such as `!.gitignore` keeps un-ignored), and in `package.json` the keys `scripts`, `main`, `bin` and `exports` next to
72
+ `knip` and `jscpd` (knip finds its entry points there).
73
+ - **A hidden path**: `.git/info/exclude` is not versioned, so it cannot be compared to the start of the lot. When it holds
74
+ a pattern (a line that is neither blank nor a comment) the command adds a warning to its output, success or refusal,
75
+ naming the first three patterns. It does not fail: that file legitimately holds local paths.
76
+
77
+ A tool missing from `node_modules` (a fresh clone without `npm install`) exits 2 with « eslint est absent de node_modules :
78
+ lancer « npm install » », instead of the JSON error the empty output of a node that could not start used to give.
69
79
 
70
80
  **When the start of the lot has no baseline yet** (the lot that introduces the ratchet, while `main` moves under it):
71
81
  the baseline measured at the start no longer matches the code once `main` is merged or the lot rebased, since `main`'s