@sylad/cadence 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +45 -1734
  4. package/agents/acceptance-writer.md +1 -0
  5. package/dist/audit.js +4 -3
  6. package/dist/cli.js +4 -4
  7. package/dist/orchestrate/accept.js +8 -3
  8. package/dist/orchestrate/briefs.js +1 -1
  9. package/dist/orchestrate/command.js +3 -0
  10. package/dist/orchestrate/cycle.js +68 -6
  11. package/docs/cadence-hud.md +50 -0
  12. package/docs/corrections.md +24 -0
  13. package/docs/cycle-vague-dark.svg +42 -0
  14. package/docs/cycle-vague-light.svg +42 -0
  15. package/docs/deliver.md +204 -0
  16. package/docs/fonctionnement-lead-dark.svg +101 -0
  17. package/docs/fonctionnement-lead-light.svg +101 -0
  18. package/docs/fonctionnement-lot-dark.svg +38 -0
  19. package/docs/fonctionnement-lot-light.svg +38 -0
  20. package/docs/fonctionnement-service-dark.svg +23 -0
  21. package/docs/fonctionnement-service-light.svg +23 -0
  22. package/docs/fonctionnement-wave-dark.svg +66 -0
  23. package/docs/fonctionnement-wave-light.svg +66 -0
  24. package/docs/fonctionnement.md +106 -0
  25. package/docs/lead-workflow-dark.svg +200 -0
  26. package/docs/lead-workflow-light.svg +200 -0
  27. package/docs/lead.md +147 -0
  28. package/docs/news.md +245 -0
  29. package/docs/orchestrate.md +639 -0
  30. package/docs/overview.md +27 -0
  31. package/docs/raf.md +192 -0
  32. package/docs/releasing.md +59 -0
  33. package/docs/session.md +107 -0
  34. package/docs/skills.md +76 -0
  35. package/docs/spec.md +60 -0
  36. package/docs/stabilite.md +35 -0
  37. package/docs/whats-new.md +81 -0
  38. package/package.json +28 -1
  39. package/templates/acceptation-runner.mjs +1 -1
  40. package/templates/fiche-back.yaml +1 -1
  41. package/templates/fiche.yaml +1 -1
@@ -19,5 +19,6 @@ Follow the brief you are given. The method:
19
19
  For a back sheet (`sujet`, `cas`, `contrats[]`, no mockup), the cases call the contracts of `contrats[]` as close as possible — the controller or the service for an HTTP call, the method of the job for a `job` — and simulate the external sources from `entree`; there are no `data-testid`.
20
20
 
21
21
  - You edit only tests, the runner config and the lot's acceptation file (`acceptation-<lot>.json`): never application code, the sheet, the mockup or the plan. Do not push, deliver, run `raf start|done|note|review|ux`, or launch subagents.
22
+ - An acceptation file that already has `vert: true` and one fingerprint in `empreintes` for each listed test file is a green, sealed oracle: never rewrite it, its tests, `resceaux` or `empreintes`, and never touch `vert`. Write nothing, commit nothing, and hand the lot back with that finding in `questions` (a test to correct is resealed by the lead, `cadence spec reseal`). A `vert: true` file WITHOUT `empreintes` (or whose `empreintes` lack one of the listed test files) was never sealed by the program (only the cycle sets `vert`, after the seal): it proves nothing, write the oracle as the brief says, with `vert: false`.
22
23
  - Never write `vert: true` or the fingerprints: the program seals the file and the cycle turns it green.
23
24
  - Give the report in the structured output the brief asks for.
package/dist/audit.js CHANGED
@@ -57,6 +57,7 @@ export function isPlanOnly(sha, plan, root) {
57
57
  const VERSION_JSON = /(^|\/)(package\.json|package-lock\.json|\.claude-plugin\/(plugin|marketplace)\.json)$/;
58
58
  const CHANGELOG = /(^|\/)CHANGELOG\.md$/;
59
59
  const README = /^README\.md$/;
60
+ const WHATS_NEW = /^docs\/whats-new\.md$/;
60
61
  /** Le README sans sa section « What's new », rangée par version et donc réécrite à la release. */
61
62
  function withoutWhatsNew(text) {
62
63
  return (text ?? '').replace(/^## What's new\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, '');
@@ -81,17 +82,17 @@ function withoutVersion(text) {
81
82
  }
82
83
  /**
83
84
  * Commit de version (L72) : TOUS ses fichiers sont des fichiers de version — package.json, package-lock.json,
84
- * .claude-plugin/plugin.json et marketplace.json, où seule la `version` change, et CHANGELOG.md, plus la section « What's new » du README (seule). Il clôt le lot
85
+ * .claude-plugin/plugin.json et marketplace.json, où seule la `version` change, et CHANGELOG.md, plus la section « What's new » du README (seule) et docs/whats-new.md, sa forme longue (L232). Il clôt le lot
85
86
  * après sa revue : l'exiger relu rouvrirait la porte à chaque release. Les fichiers décident, jamais le sujet.
86
87
  */
87
88
  export function isReleaseOnly(sha, root) {
88
89
  const files = changedFiles(root, sha);
89
90
  // Les noms d'abord : un fichier hors version refuse le commit sans qu'aucun contenu ne soit lu.
90
- if (!files.length || !files.every((f) => CHANGELOG.test(f) || README.test(f) || VERSION_JSON.test(f)))
91
+ if (!files.length || !files.every((f) => CHANGELOG.test(f) || WHATS_NEW.test(f) || README.test(f) || VERSION_JSON.test(f)))
91
92
  return false;
92
93
  return (files.length > 0 &&
93
94
  files.every((f) => {
94
- if (CHANGELOG.test(f))
95
+ if (CHANGELOG.test(f) || WHATS_NEW.test(f))
95
96
  return true;
96
97
  if (README.test(f))
97
98
  return withoutWhatsNew(fileAt(root, `${sha}^`, f)) === withoutWhatsNew(fileAt(root, sha, f));
package/dist/cli.js CHANGED
@@ -66,7 +66,7 @@ publié par exemple, ainsi que le fichier d'attendus QA : docs/qa/expectations.m
66
66
  Un lot --visible attend une entrée Nouveautés (docs/nouveautes/, --dir) avec capture ; raf check le vérifie.
67
67
  Un texte qui commence par « - » se passe après « -- » : raf note L1 -- "-5 %".
68
68
  Le plan est docs/plan/raf.yaml, ou celui que nomme « plan: » dans cadence.yaml ; un plan tenu par un
69
- autre outil se lit sans migration (correspondance des champs, voir le README) et reste en lecture seule.
69
+ autre outil se lit sans migration (correspondance des champs, voir docs/raf.md du paquet @sylad/cadence) et reste en lecture seule.
70
70
  Options communes : --file chemin (ou RAF_FILE), RAF_TODAY=AAAA-MM-JJ pour figer la date.`;
71
71
  /** Ajoute `line` au .gitignore de `root` s'il n'y est pas déjà (avec ou sans `/` initial ou final). Rend vrai si écrit. */
72
72
  export function ensureGitignore(root, line) {
@@ -512,7 +512,7 @@ function dispatch(argv, io) {
512
512
  if (orchestrating)
513
513
  throw new RafError(`deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid}) — livrer une fois les lots de ce dépôt finis (le verrou tombe dès que tous sont prêts ou rendus)`);
514
514
  if (!existsSync(configPath))
515
- throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans le README)`);
515
+ throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
516
516
  const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
517
517
  const plan = existsSync(planPath) ? loadPlan() : null;
518
518
  const articles = readDocsConfig(configPath).articles;
@@ -525,7 +525,7 @@ function dispatch(argv, io) {
525
525
  if (!gitRoot(io.cwd))
526
526
  throw new RafError('verify : à lancer dans un dépôt git');
527
527
  if (!existsSync(configPath))
528
- throw new RafError(`pas de configuration : ${configPath} (voir « cadence.yaml » dans le README)`);
528
+ throw new RafError(`pas de configuration : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
529
529
  const retry = values.retry === undefined ? 0 : Number(values.retry);
530
530
  if (!Number.isFinite(retry) || retry < 0)
531
531
  throw new RafError(`--retry invalide : ${values.retry} (secondes, 0 ou plus)`);
@@ -866,7 +866,7 @@ function specReseal(args, reason, porteur, ctx, io) {
866
866
  const before = { json: readFileSync(join(repo, json), 'utf8'), test: existsSync(join(repo, test)) ? readFileSync(join(repo, test), 'utf8') : '' };
867
867
  let r;
868
868
  try {
869
- r = resealAcceptation(repo, json, test, reason, ctx.today, livre ? porteur : undefined, plan.refs);
869
+ r = resealAcceptation(repo, json, test, reason, ctx.today, livre ? porteur : undefined, plan.refs, plan.lots().filter((l) => l.status === 'done').map((l) => l.id));
870
870
  }
871
871
  catch (e) {
872
872
  throw new RafError(e.message);
@@ -126,9 +126,9 @@ export function sealAcceptation(repo, path) {
126
126
  * ne laisse aucun chemin de correction. Seule l'empreinte du test nommé est refaite (les autres fichiers listés restent inchangés), la raison et les deux
127
127
  * empreintes vont dans `resceaux`. Le commit (test + acceptation) est à faire : il devient le commit rouge.
128
128
  * La fiche est celle du commit rouge, ou (L217) une fiche amendée par des commits qui citent tous le lot (`refsOf` : `plan.refs`, lu comme `citedRefs` ;
129
- * lot livré : le lot ou son `porteur`) et commitée : l'entrée porte alors `ficheAmendee`, le sha court du dernier commit qui l'a touchée.
129
+ * lot livré : le lot, son `porteur` ou, L229, l'un des `livres`, les lots done du plan — la fiche vivante fait foi) et commitée : l'entrée porte alors `ficheAmendee`, le sha court du dernier commit qui l'a touchée.
130
130
  */
131
- export function resealAcceptation(repo, path, test, reason, today, porteur, refsOf = () => []) {
131
+ export function resealAcceptation(repo, path, test, reason, today, porteur, refsOf = () => [], livres = []) {
132
132
  const file = join(repo, path);
133
133
  const a = parseAcceptation(readFileSync(file, 'utf8'));
134
134
  if (typeof a === 'string')
@@ -156,9 +156,13 @@ export function resealAcceptation(repo, path, test, reason, today, porteur, refs
156
156
  if (!gitOk(repo, 'diff', '--quiet', red, '--', a.fiche)) {
157
157
  if (!existsSync(join(repo, a.fiche)))
158
158
  throw new Error(`la fiche ${a.fiche} a été supprimée ou renommée depuis le commit rouge ${red.slice(0, 7)} : le rescellé ne vaut que pour une fiche présente`);
159
- const citant = new Set([a.lot, ...(porteur ? [porteur] : [])]);
159
+ const citant = new Set([a.lot, ...(porteur ? [porteur, ...livres] : [])]);
160
160
  const commits = git(repo, 'log', '--format=%H%x1f%s%x1f%b%x1e', `${red}..HEAD`, '--', a.fiche).split('\x1e').map((r) => r.replace(/^\n/, '')).filter(Boolean).map((r) => { const [sha = '', subject = '', body = ''] = r.split('\x1f'); return { sha, day: '', subject, body: body.trim() }; });
161
161
  const fautif = commits.find((c) => !citedRefs(c, refsOf).some((r) => citant.has(r.lot)));
162
+ if (fautif && porteur) {
163
+ const cites = [...new Set(citedRefs(fautif, refsOf).map((r) => r.lot))];
164
+ throw new Error(`la fiche ${a.fiche} a été modifiée depuis le commit rouge ${red.slice(0, 7)} par un commit qui ne cite ni ${a.lot}, ni le porteur ${porteur}, ni un lot livré : ${fautif.sha.slice(0, 7)} (cite : ${cites.length ? cites.join(', ') : 'aucun lot'}) — faire citer le porteur ou un lot livré par ce commit (le réécrire avant qu'il soit poussé) ou, sinon, rescelle à la main avec le lead`);
165
+ }
162
166
  if (fautif)
163
167
  throw new Error(`la fiche ${a.fiche} a été modifiée depuis le commit rouge ${red.slice(0, 7)} par un commit qui ne cite pas ${a.lot} : ${fautif.sha.slice(0, 7)}`);
164
168
  ficheAmendee = commits[0]?.sha.slice(0, 7);
@@ -405,6 +409,7 @@ export function acceptText(o) {
405
409
  isAcceptList(o.command)
406
410
  ? `The project declares several dedicated commands (\`orchestrate.accept\`, one per test runner), each of which runs ONLY the acceptance tests of this lot that sit under its root (the folder of its leading \`cd <folder> &&\`, else the repository): ${commands.map((x) => `\`${x}\` (root \`${acceptRoot(x) || '.'}\`)`).join(', ')}. Each is run from the repository root and gets \`CADENCE_ACCEPTATION\` (the absolute path of the lot's \`acceptation.json\`, also substituted for \`{acceptation}\`) and \`CADENCE_ACCEPTATION_ROOT\` (its root); it takes only the files listed under its root, a command with no listed file under its root is not run, and a listed file under no root is refused. Put each test file where its runner looks, and list every file in \`fichiers\`.${o.test ? ` The project's ordinary test command is \`${o.test}\`: it must stay green with your red tests committed.` : ''}`
407
411
  : `The project declares the dedicated command that runs ONLY the acceptance tests of this lot (\`orchestrate.accept\`): \`${commands[0]}\` (from the repository root; the program sets \`CADENCE_ACCEPTATION\` to the absolute path of the lot's \`acceptation.json\`, and replaces \`{acceptation}\` with it).${o.test ? ` The project's ordinary test command is \`${o.test}\`: it must stay green with your red tests committed.` : ''}`,
412
+ `If \`${o.acceptation}\` already exists with \`vert: true\` and \`empreintes\`, the lot's oracle is green and sealed: never rewrite it, nor its tests, \`resceaux\` or \`empreintes\`, and never set \`vert\` (a corrected test is resealed by the lead, \`cadence spec reseal\`). Write nothing, commit nothing, and hand the lot back with that finding in \`questions\`. A \`vert: true\` file WITHOUT \`empreintes\` (or whose \`empreintes\` lack one of the listed test files) was never sealed by the program (only the cycle sets \`vert\`, after the seal): it proves nothing, so write the oracle as described below, with \`vert: false\`.`,
408
413
  `Write \`${o.acceptation}\` (one file per lot, next to the sheet: another lot on the same ${screen ? 'screen' : 'subject'} has its own, which you never touch, nor its tests — give your test files names of your own): \`{ "lot": "<lot>", "fiche": "${o.fiche}", "fichiers": ["<test file>", …], "vert": false }\` (paths relative to the repository). If the tests need a build or a browser (Playwright, Chromium, \`dist\`), add \`"e2e": true\` to it: those files then stay out of the ordinary runner and the unit CI even once green (they would turn it red without \`dist\` or Chromium) and belong to the project's e2e suite, run after the build (\`acceptationE2e()\` of the runner template lists them). The ordinary test config of the project (shared with its CI) must exclude those files while \`vert\` is false; the dedicated config includes only them. Both read the file with this runner template (copy it into the project, e.g. \`tests/acceptation-runner.mjs\`):`,
409
414
  ];
410
415
  if (o.runner)
@@ -24,7 +24,7 @@ export function reposText(role, lot, repos) {
24
24
  `\`raf commits ${lot}\` lists them all; read the diff of the neighbouring ones with \`git -C <path> log\` / \`git -C <path> show\` (${repos.map((r) => `git -C ${r.path} log`).join(' ; ')}), and review them like the rest. You are read-only there too.`,
25
25
  ].join('\n');
26
26
  }
27
- /** Consigne d'implémentation d'un lot `visible` : son entrée Nouveautés (convention de `cadence news`, voir le README). */
27
+ /** Consigne d'implémentation d'un lot `visible` : son entrée Nouveautés (convention de `cadence news`, voir docs/news.md du paquet @sylad/cadence). */
28
28
  export function newsText(lot, captureDir) {
29
29
  return [
30
30
  `This lot is visible: write its News entry. Run \`cadence news new ${lot}\` (it creates \`docs/nouveautes/<date>-<slug>.md\`), then write the text in terms of what changes for the user, factual (what the user sees or can now do, no implementation detail, no promise).`,
@@ -486,6 +486,9 @@ export async function orchestrate(argv, io, deps) {
486
486
  throw new RafError('--status ne se combine pas avec --drop / --stop-after-current');
487
487
  if (args.resume !== undefined && args.stopAfterCurrent)
488
488
  throw new RafError('--resume ne se combine pas avec --stop-after-current (il reprend la vague, il ne l\'arrête pas)');
489
+ // L228 : la reprise n'a pas de simulation — elle relève le budget, réécrit wave.json et relance les sessions. Refusée ici, avant le réexec de l'instantané (une vague ancienne ne connaît pas ce refus).
490
+ if (args.resume !== undefined && args.dryRun)
491
+ throw new RafError('--resume ne se combine pas avec --dry-run (la reprise écrit l\'état de la vague et relance les sessions, elle ne se simule pas)');
489
492
  if (args.lotBudgets.length && args.resume === undefined)
490
493
  throw new RafError("--budget projet:lot=+N ne vaut qu'avec --resume");
491
494
  if (args.since !== undefined && !args.waits)
@@ -1079,9 +1079,11 @@ function codeAfterFiche(c, fiche) {
1079
1079
  /** L'acceptation du lot : celle que git suit déjà pour lui (même un ancien `acceptation.json`), sinon `acceptation-<lot>.json` à côté de la fiche. */
1080
1080
  const lotAcceptation = (c, fiche) => findAcceptation(c.lot.repo, c.config.spec.dir, c.lot.lot) ?? acceptationFile(fiche, c.lot.lot);
1081
1081
  const listed = (c, json) => existsSync(join(c.lot.repo, json));
1082
+ /** Une acceptation porte-t-elle une empreinte par fichier listé ? Le contenu n'est pas comparé (L233) : `empreintes: {}` ou celles d'un autre fichier ne scellent rien. */
1083
+ const hasSeals = (doc) => !!doc.empreintes && doc.fichiers.every((f) => !!doc.empreintes[posix.normalize(f)]);
1082
1084
  /**
1083
- * Une acceptation conforme et SCELLÉE par le programme (empreintes posées), rouge ou verte (`vert: true`, lot relancé dans une nouvelle vague) :
1084
- * elle n'a pu être écrite qu'après la revue de la maquette et les tests rouges, qui ne sont donc pas rejoués. Sans `orchestrate.accept`, aucune preuve.
1085
+ * Une acceptation conforme et SCELLÉE par le programme (empreintes posées), rouge ou verte (`vert: true`, lot relancé dans une nouvelle vague ; une verte et scellée vaut
1086
+ * preuve même si son scellé ne tient plus, L233) : elle n'a pu être écrite qu'après la revue de la maquette et les tests rouges, qui ne sont donc pas rejoués. Sans `orchestrate.accept`, aucune preuve.
1085
1087
  */
1086
1088
  function acceptationProof(c, fiche) {
1087
1089
  const l = c.lot;
@@ -1089,18 +1091,24 @@ function acceptationProof(c, fiche) {
1089
1091
  if (!c.config.accept || !listed(c, json))
1090
1092
  return false;
1091
1093
  const doc = parseAcceptation(readFileSync(join(l.repo, json), 'utf8'));
1092
- if (typeof doc === 'string' || !doc.empreintes)
1094
+ if (typeof doc === 'string')
1093
1095
  return false;
1096
+ // Un lot déjà vert (L233) n'a plus d'oracle à écrire : le cycle seul a pu poser `vert: true`, après le scellé et les tests rouges. Même une preuve qui ne tient plus
1097
+ // (test rescellé ou corrigé depuis, fiche amendée) ne renvoie jamais à accept, qui réécrirait un oracle vert et exigerait des tests rouges : le lot part à implement ou à la revue.
1098
+ // Une verte porte une empreinte par fichier listé, sinon elle n'a jamais été scellée (`empreintes: {}` ou celles d'un autre fichier ne prouvent rien).
1099
+ if (!hasSeals(doc))
1100
+ return false;
1101
+ if (doc.vert)
1102
+ return true;
1094
1103
  // Le scellé n'est posé que par le programme : chaque fichier listé doit porter l'empreinte git de son contenu actuel (une clé `empreintes` vide ou copiée d'un autre lot ne prouve rien).
1095
- const emp = doc.empreintes;
1104
+ const emp = doc.empreintes ?? {};
1096
1105
  const sealed = doc.fichiers.every((f) => {
1097
1106
  const norm = posix.normalize(f);
1098
1107
  return existsSync(join(l.repo, norm)) && emp[norm] === git(l.repo, 'hash-object', '--', norm).trim();
1099
1108
  });
1100
1109
  if (!sealed)
1101
1110
  return false;
1102
- const vertMsg = checkAcceptation(l.repo, json, l.lot, fiche).filter((p) => !(doc.vert && p.includes('vert: true à l\'écriture')));
1103
- return vertMsg.length === 0;
1111
+ return checkAcceptation(l.repo, json, l.lot, fiche).length === 0;
1104
1112
  }
1105
1113
  /** Chemins salis par une session : suivis modifiés, nouveaux fichiers non suivis, et ceux des dépôts voisins. */
1106
1114
  function dirtAfter(done) {
@@ -1399,6 +1407,34 @@ async function acceptProblems(c, fiche, json, headBefore) {
1399
1407
  }
1400
1408
  return out;
1401
1409
  }
1410
+ /**
1411
+ * Écart de l'acceptation verte et scellée `json` par rapport à ce qu'elle était à `first` (tête d'avant la première session accept du lot), ou null.
1412
+ * Admis : le même contenu, ou un rescellé du lead (`cadence spec reseal`, L200) : `vert` et `empreintes` toujours posés, journal `resceaux` allongé, aucun commit
1413
+ * du fichier dans une session de la vague, fichier propre. Tout le reste (vert: false, journal effacé, réécriture commitée par la session) est rendu au lead (L233).
1414
+ */
1415
+ function greenDrift(c, json, first, before, was) {
1416
+ const l = c.lot;
1417
+ if (!listed(c, json))
1418
+ return 'acceptation.json supprimé depuis la première session accept (verrou des tests d\'acceptation)';
1419
+ const text = readFileSync(join(l.repo, json), 'utf8');
1420
+ // `before` vient de git() qui rogne les blancs de fin, le fichier écrit par le programme finit par un saut de ligne : les deux se comparent rognés.
1421
+ if (text.trim() === before.trim())
1422
+ return null;
1423
+ const now = parseAcceptation(text);
1424
+ const dirty = git(l.repo, 'status', '--porcelain', '--', json).trim() !== '';
1425
+ let bySession = false;
1426
+ try {
1427
+ const mine = waveCommits(c, l.repo);
1428
+ bySession = git(l.repo, 'log', '--format=%H', `${first}..HEAD`, '--', json).split('\n').some((sha) => sha && mine(sha));
1429
+ }
1430
+ catch {
1431
+ bySession = true;
1432
+ }
1433
+ const journal = (a) => a.resceaux?.length ?? 0;
1434
+ if (typeof now !== 'string' && now.vert && now.empreintes && journal(now) > journal(was) && !bySession && !dirty)
1435
+ return null;
1436
+ return 'acceptation verte réécrite depuis la première session accept (vert, empreintes ou resceaux changés ; seul un rescellé du lead, `cadence spec reseal`, est admis) : remettre le fichier comme avant la session, par exemple `git checkout <commit vert> -- <fichier>` puis `--resume`';
1437
+ }
1402
1438
  /**
1403
1439
  * Étape accept (L173, spec etape-spec S5) : une session distincte de l'implémenteur écrit, depuis la fiche et la maquette seules, les tests
1404
1440
  * d'acceptation rouges. Le programme vérifie (acceptation.json conforme, autres lots intacts, commande dédiée rouge, config.test verte),
@@ -1413,6 +1449,32 @@ async function acceptStep(c) {
1413
1449
  return;
1414
1450
  }
1415
1451
  const json = lotAcceptation(c, fiche);
1452
+ // Un lot déjà vert et scellé (L233) n'a plus d'oracle à écrire, même quand `next` vaut encore accept (question posée pendant accept, puis réponse du lead) : jamais de session accept.
1453
+ // Le fichier est lu tel qu'il était avant la première session accept du lot : la seconde passe voit le fichier écrit par la session, qui ne doit pas être vert (checkAcceptation).
1454
+ const first = l.steps.find((s) => s.kind === 'accept')?.headBefore;
1455
+ let text = null;
1456
+ try {
1457
+ text = first ? git(l.repo, 'show', `${first}:${json}`) : listed(c, json) ? readFileSync(join(l.repo, json), 'utf8') : null;
1458
+ }
1459
+ catch {
1460
+ text = null;
1461
+ }
1462
+ const doc = text === null ? null : parseAcceptation(text);
1463
+ if (doc && typeof doc !== 'string' && doc.vert && hasSeals(doc)) {
1464
+ // Le fichier actuel doit être celui de `first` : une session accept qui a réécrit puis commité l'oracle avant de poser sa question (le verrou ne s'applique pas à l'étape accept, `skip`)
1465
+ // ne se fait pas adopter comme nouveau commit rouge. Seul un rescellé du lead (journal `resceaux` allongé, commit hors de toute session, fichier propre) est une différence admise.
1466
+ const drift = first ? greenDrift(c, json, first, text, doc) : null;
1467
+ if (drift) {
1468
+ stop(c, 'handed-back', `verrou des tests d'acceptation : ${json} : ${drift}`);
1469
+ return;
1470
+ }
1471
+ l.accept = { acceptation: json, rouge: redSha(l.repo, json) ?? '' };
1472
+ l.next = 'implement';
1473
+ // Un lot en question n'a plus rien à demander : la réponse du lead (« ne réécris rien ») est prise en compte par ce choix, elle ne repart pas dans le brief d'implement.
1474
+ l.pendingAnswer = null;
1475
+ transition(c, 'implementing', 'acceptation déjà verte : accept ignoré');
1476
+ return;
1477
+ }
1416
1478
  const done = await session(c, 'accept');
1417
1479
  if (!done)
1418
1480
  return;
@@ -0,0 +1,50 @@
1
+ [← cadence](../README.md)
2
+
3
+ # cadence-hud
4
+
5
+ A band above the Claude Code prompt (terminal and desktop app), refreshed every 5 s:
6
+
7
+ ```
8
+ ctx ▰▰▰▰▱▱▱▱▱▱ 42 % 84k/200k │ 5h 23 % ↻ 2 h 10 │ 7j 61 % ↻ 6 j 15 h │ $1.23 │ fable 410k sonnet 85k │ ⚙ 2 agents │ 2 cmd
9
+ ⟳ cadence · 2026-10-07-2131 en cours │ budget ▱▱▱▱▱▱▱▱▱▱ 1 % 14k/2M │ 1 session/2
10
+ L112 implémente implement@sonnet 34 s global-setup coupe le cache de compilation de Node...
11
+ ```
12
+
13
+ Context of the session (green / orange / red at 50 and 75 %), the 5-hour and 7-day quota
14
+ windows with the time to their reset, the cost of the session and its split per model, the
15
+ subagents of this session, the background commands in progress (`2 cmd`: Bash run in the background and Monitor, subagents' included, as the app's "background commands" card counts them), and every live `cadence orchestrate` wave: budget, sessions, one
16
+ aligned line per active lot (status, step, model, elapsed, start of the title), the lots still
17
+ waiting. When no wave is running, the last finished one stays on a grey line for 30 minutes
18
+ after it ended, then disappears. A background command counted for more than an hour, or with no
19
+ record of when it started, is no longer shown as running: it moves to a grey `N cmd sans fin
20
+ vue` ("end never seen") right after `N cmd`, because its end notification may never have reached
21
+ the session. Waves are read from disk (`~/.cadence/orchestrate/waves/`, then `.cadence/runs/`); no
22
+ cadence command is run, and the CLI is not required for those. One more block needs it: the **progress of the plans**,
23
+ one line per active project (a project with open lots), in the order of `priority:`:
24
+
25
+ ```
26
+ ol ▮▮▮▮▮▯▯▯▯▯ 39/78 · 0 en cours · +3 cette semaine
27
+ cadence ▮▮▮▮▮▯▯▯▯▯ 10/20 · 2 en cours : L149 L153
28
+ ```
29
+
30
+ The bar is lots done over lots done + in progress + to do (dropped and recurring lots left out); the count of lots in progress is followed by their ids (`2 en cours : L149 L153`), cut to three then `…` (`5 en cours : L1 L2 L3 …`), the count alone at zero;
31
+ `+N cette semaine` counts the lots created in the last 7 days (today and the six before) and is left out at zero. The source is
32
+ `cadence lead tour <folder> --json` (the folder a wave was launched from, otherwise the session's: a folder of projects gives one line per project, a project with no sub-project gives its own single line),
33
+ read again at every wave transition and otherwise every minute; without `cadence` on the PATH, the
34
+ lines are simply not drawn.
35
+
36
+ Install it as a plugin from the same marketplace:
37
+
38
+ ```
39
+ /plugin marketplace add Sylad/cadence
40
+ /plugin install cadence-hud@cadence
41
+ ```
42
+
43
+ `/hud` hides or shows the band. `/hud cmd` lists the background commands being counted (id, tool,
44
+ origin `session` or `agent <id>`, age, name; `(sans fin vue)` on the ones above). Any other argument
45
+ `/hud projets` folds the project list to one line, and opens it again. Any other argument
46
+ answers `argument inconnu : … (attendu : cmd, projets)` and leaves the band as it is. The source lives in [`plugins/cadence-hud`](../plugins/cadence-hud/README.md)
47
+ (its own README has the details of every cell); it is not part of the npm package. To work on it:
48
+ `claude plugin validate plugins/cadence-hud`, `claude plugin test plugins/cadence-hud`, and
49
+ `tsc -p plugins/cadence-hud` once Claude Code has loaded the plugin at least once (it generates
50
+ the `.claude-plugin/types` the tsconfig extends).
@@ -0,0 +1,24 @@
1
+ [← cadence](../README.md)
2
+
3
+ # corrections
4
+
5
+ `cadence corrections [folder] [--classify [--model m]] [--cache file] [--sample n] [--until date] [--since date] [--json]` measures how often a wave
6
+ has to correct itself. From the folder a wave was launched from (default: the current one) it reads the journals
7
+ `<folder>/.cadence/runs/<wave>/<project>--<lot>.json` of **every project** and prints:
8
+
9
+ - the **lots with a correction**: of the lots that went through a successful write session, the share with at least one
10
+ and at least two `fix` passes, per project and for visible versus non-visible lots (`61 %` and `29 %` of 263 lots in the 10-10 baseline);
11
+ - the **passes by cause**, in total, per project and **per visibility** (visible lots apart from the others, with the share of `spec-floue`, `regle-inventee` and `test-manquant` among each one's passes — the reference an evolution aimed at screens is compared to is that of the visible lots, not « all lots »: 16 % of 45 passes against 31 % overall in the 10-10 baseline). The cause of a pass is read from the findings of the review that
12
+ precedes it, among: unclear spec or missing criterion (`spec-floue`), invented business rule (`regle-inventee`),
13
+ architecture drift (`derive-architecture`), data / SQL (`donnees-sql`), code bug (`bug-code`), missing test
14
+ (`test-manquant`), false review finding (`faux-constat`). One cause per pass: the root of what forced the fix. A pass with no review finding to read (triggered by red tests right after the write session) is counted apart as `sans-constat` and never sent to the model.
15
+
16
+ Without `--classify` the command calls no model: the passes not yet classified count as `non classé`. With `--classify`
17
+ each pass is classified by a **local model served by Ollama** (`OLLAMA_HOST`, default `http://localhost:11434`;
18
+ `--model`, default `mistral-small3.2:24b`; temperature 0, output constrained to the seven causes), off the Anthropic quota.
19
+ The classification is cached in `<folder>/.cadence/corrections.json` (or `--cache file`): a re-run classifies only the
20
+ new passes, and a pass the model could not answer stays out of the cache. `--sample n` prints `n` passes with findings spread
21
+ evenly over the sorted keys, with their findings and the model's cause, to be read by hand; a `manual` field written
22
+ 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
23
+ 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.
24
+ The first baseline is in [docs/mesures/corrections-2026-10-10.md](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](mesures/etape-spec-2026-10-12.md); the target of its third row is ≤ 8 %, half the 16 % reference.
@@ -0,0 +1,42 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 250" width="1100" height="250" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#e6edf3">
2
+ <title>Cycle of a wave and role of the lead</title>
3
+ <desc>A lot goes through spec, mockup, accept, implement, review, ux and deliver; the lead picks the lots, answers the questions, re-verifies and delivers.</desc>
4
+ <defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#8b949e"/></marker></defs>
5
+ <rect width="1100" height="250" fill="#0d1117"/>
6
+ <rect x="20" y="16" width="1060" height="52" rx="6" fill="#0c2d6b" stroke="#58a6ff" stroke-width="1.5"/>
7
+ <text x="40" y="38" font-weight="600" font-size="13.5">The lead (you, or the lead skill)</text>
8
+ <text x="40" y="57" font-size="12" fill="#8b949e">picks the lots · answers the questions · re-verifies each lot · delivers one project at a time</text>
9
+ <rect x="20" y="130" width="122" height="62" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="5 4"/>
10
+ <text x="81" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">spec</text>
11
+ <text x="81" y="176" text-anchor="middle" font-size="11.5" fill="#8b949e">sheet + rules</text>
12
+ <line x1="144" y1="161" x2="167" y2="161" stroke="#8b949e" stroke-width="1.5" marker-end="url(#a)"/>
13
+ <rect x="170" y="130" width="122" height="62" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="5 4"/>
14
+ <text x="231" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">mockup</text>
15
+ <text x="231" y="176" text-anchor="middle" font-size="11.5" fill="#8b949e">page review</text>
16
+ <line x1="294" y1="161" x2="317" y2="161" stroke="#8b949e" stroke-width="1.5" marker-end="url(#a)"/>
17
+ <rect x="320" y="130" width="122" height="62" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="5 4"/>
18
+ <text x="381" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">accept</text>
19
+ <text x="381" y="176" text-anchor="middle" font-size="11.5" fill="#8b949e">red tests, sealed</text>
20
+ <line x1="444" y1="161" x2="467" y2="161" stroke="#8b949e" stroke-width="1.5" marker-end="url(#a)"/>
21
+ <rect x="470" y="130" width="122" height="62" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
22
+ <text x="531" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">implement</text>
23
+ <text x="531" y="176" text-anchor="middle" font-size="11.5" fill="#8b949e">test first</text>
24
+ <line x1="594" y1="161" x2="617" y2="161" stroke="#8b949e" stroke-width="1.5" marker-end="url(#a)"/>
25
+ <rect x="620" y="130" width="122" height="62" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
26
+ <text x="681" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">review</text>
27
+ <text x="681" y="176" text-anchor="middle" font-size="11.5" fill="#8b949e">code review</text>
28
+ <line x1="744" y1="161" x2="767" y2="161" stroke="#8b949e" stroke-width="1.5" marker-end="url(#a)"/>
29
+ <rect x="770" y="130" width="122" height="62" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
30
+ <text x="831" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">ux</text>
31
+ <text x="831" y="176" text-anchor="middle" font-size="11.5" fill="#8b949e">usability, once</text>
32
+ <line x1="894" y1="161" x2="917" y2="161" stroke="#8b949e" stroke-width="1.5" marker-end="url(#a)"/>
33
+ <rect x="920" y="130" width="122" height="62" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
34
+ <text x="981" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">deliver</text>
35
+ <text x="981" y="176" text-anchor="middle" font-size="11.5" fill="#8b949e">CI + proof</text>
36
+ <line x1="831" y1="68" x2="831" y2="127" stroke="#58a6ff" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
37
+ <line x1="81" y1="68" x2="81" y2="127" stroke="#58a6ff" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
38
+ <line x1="981" y1="127" x2="981" y2="68" stroke="#58a6ff" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
39
+ <text x="73" y="216" font-size="11.5" fill="#8b949e">dashed: only for a lot with a page or a behaviour to pin down (screen: page, spec: true)</text>
40
+ <text x="661" y="216" font-size="11.5" fill="#8b949e">each step in a fresh session, driven by cadence orchestrate</text>
41
+ <text x="20" y="236" font-size="11.5" fill="#8b949e">Every verdict is written to the plan (docs/plan/raf.yaml); a delivery counts only when its checks pass.</text>
42
+ </svg>
@@ -0,0 +1,42 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 250" width="1100" height="250" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#1f2328">
2
+ <title>Cycle of a wave and role of the lead</title>
3
+ <desc>A lot goes through spec, mockup, accept, implement, review, ux and deliver; the lead picks the lots, answers the questions, re-verifies and delivers.</desc>
4
+ <defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#57606a"/></marker></defs>
5
+ <rect width="1100" height="250" fill="#ffffff"/>
6
+ <rect x="20" y="16" width="1060" height="52" rx="6" fill="#ddf4ff" stroke="#0969da" stroke-width="1.5"/>
7
+ <text x="40" y="38" font-weight="600" font-size="13.5">The lead (you, or the lead skill)</text>
8
+ <text x="40" y="57" font-size="12" fill="#57606a">picks the lots · answers the questions · re-verifies each lot · delivers one project at a time</text>
9
+ <rect x="20" y="130" width="122" height="62" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5" stroke-dasharray="5 4"/>
10
+ <text x="81" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">spec</text>
11
+ <text x="81" y="176" text-anchor="middle" font-size="11.5" fill="#57606a">sheet + rules</text>
12
+ <line x1="144" y1="161" x2="167" y2="161" stroke="#57606a" stroke-width="1.5" marker-end="url(#a)"/>
13
+ <rect x="170" y="130" width="122" height="62" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5" stroke-dasharray="5 4"/>
14
+ <text x="231" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">mockup</text>
15
+ <text x="231" y="176" text-anchor="middle" font-size="11.5" fill="#57606a">page review</text>
16
+ <line x1="294" y1="161" x2="317" y2="161" stroke="#57606a" stroke-width="1.5" marker-end="url(#a)"/>
17
+ <rect x="320" y="130" width="122" height="62" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5" stroke-dasharray="5 4"/>
18
+ <text x="381" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">accept</text>
19
+ <text x="381" y="176" text-anchor="middle" font-size="11.5" fill="#57606a">red tests, sealed</text>
20
+ <line x1="444" y1="161" x2="467" y2="161" stroke="#57606a" stroke-width="1.5" marker-end="url(#a)"/>
21
+ <rect x="470" y="130" width="122" height="62" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
22
+ <text x="531" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">implement</text>
23
+ <text x="531" y="176" text-anchor="middle" font-size="11.5" fill="#57606a">test first</text>
24
+ <line x1="594" y1="161" x2="617" y2="161" stroke="#57606a" stroke-width="1.5" marker-end="url(#a)"/>
25
+ <rect x="620" y="130" width="122" height="62" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
26
+ <text x="681" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">review</text>
27
+ <text x="681" y="176" text-anchor="middle" font-size="11.5" fill="#57606a">code review</text>
28
+ <line x1="744" y1="161" x2="767" y2="161" stroke="#57606a" stroke-width="1.5" marker-end="url(#a)"/>
29
+ <rect x="770" y="130" width="122" height="62" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
30
+ <text x="831" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">ux</text>
31
+ <text x="831" y="176" text-anchor="middle" font-size="11.5" fill="#57606a">usability, once</text>
32
+ <line x1="894" y1="161" x2="917" y2="161" stroke="#57606a" stroke-width="1.5" marker-end="url(#a)"/>
33
+ <rect x="920" y="130" width="122" height="62" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
34
+ <text x="981" y="157" text-anchor="middle" font-weight="600" font-size="14" font-family="ui-monospace,SFMono-Regular,Menlo,monospace">deliver</text>
35
+ <text x="981" y="176" text-anchor="middle" font-size="11.5" fill="#57606a">CI + proof</text>
36
+ <line x1="831" y1="68" x2="831" y2="127" stroke="#0969da" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
37
+ <line x1="81" y1="68" x2="81" y2="127" stroke="#0969da" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
38
+ <line x1="981" y1="127" x2="981" y2="68" stroke="#0969da" stroke-width="1.2" stroke-dasharray="3 3" marker-end="url(#a)"/>
39
+ <text x="73" y="216" font-size="11.5" fill="#57606a">dashed: only for a lot with a page or a behaviour to pin down (screen: page, spec: true)</text>
40
+ <text x="661" y="216" font-size="11.5" fill="#57606a">each step in a fresh session, driven by cadence orchestrate</text>
41
+ <text x="20" y="236" font-size="11.5" fill="#57606a">Every verdict is written to the plan (docs/plan/raf.yaml); a delivery counts only when its checks pass.</text>
42
+ </svg>