@sylad/cadence 1.15.1 → 1.17.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.
@@ -1,8 +1,8 @@
1
- import { chmodSync, existsSync, lstatSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { chmodSync, existsSync, lstatSync, readdirSync, statSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { execFile, execFileSync } from 'node:child_process';
3
3
  import { appendFileSync, mkdirSync } from 'node:fs';
4
4
  import { dirname, isAbsolute, join, relative } from 'node:path';
5
- import { gitCommonDir, gitRoot, hooksDir } from '../git.js';
5
+ import { gitCommonDir, gitRoot, hooksDir, indexLockPath } from '../git.js';
6
6
  const MARK = '# cadence orchestrate — hook pre-push temporaire (retiré à la fin de la vague)';
7
7
  const waveLine = (wave) => `# wave: ${wave}`;
8
8
  const hook = (wave) => `#!/bin/sh
@@ -300,3 +300,45 @@ export function remoteAdvances(before, after) {
300
300
  const moved = [...now].filter(([ref, sha]) => was.get(ref) !== sha).map(([ref, to]) => ({ ref, from: was.get(ref) ?? null, to }));
301
301
  return moved.length > 0 ? moved : null;
302
302
  }
303
+ /** Processus git en cours sur la machine ; null quand `ps` n'est pas lisible. */
304
+ export function gitProcessRunning() {
305
+ try {
306
+ const out = execFileSync('ps', ['-A', '-o', 'comm='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10_000 });
307
+ return out.split('\n').some((l) => l.trim() === 'git');
308
+ }
309
+ catch {
310
+ return null;
311
+ }
312
+ }
313
+ /** Durée à la minute : « 12 min », « 5 h 10 min », « 2 j 3 h ». */
314
+ export function longDuration(ms) {
315
+ const m = Math.max(0, Math.floor(ms / 60_000));
316
+ if (m < 60)
317
+ return `${m} min`;
318
+ const h = Math.floor(m / 60);
319
+ if (h < 24)
320
+ return `${h} h ${String(m % 60).padStart(2, '0')} min`;
321
+ return `${Math.floor(h / 24)} j ${h % 24} h`;
322
+ }
323
+ /**
324
+ * Un `index.lock` dans `repo` (L385) : tout commit y échoue, dont celui du plan au démarrage d'un lot, et le plan resterait modifié pour le lot suivant.
325
+ * Rend le refus (âge du verrou, processus git en cours ou non, commande pour le retirer), null s'il n'y en a pas. `gitRunning` : null = illisible.
326
+ */
327
+ export function indexLockRefusal(repo, now, gitRunning) {
328
+ let lock;
329
+ let mtime;
330
+ try {
331
+ lock = indexLockPath(repo);
332
+ mtime = statSync(lock).mtime;
333
+ }
334
+ catch {
335
+ return null;
336
+ }
337
+ const hhmm = `${String(mtime.getHours()).padStart(2, '0')}:${String(mtime.getMinutes()).padStart(2, '0')}`;
338
+ const seen = `.git/index.lock présent depuis ${hhmm} (${longDuration(now.getTime() - mtime.getTime())})`;
339
+ const running = gitRunning();
340
+ if (running === true)
341
+ return `${seen}, un processus git tourne : attendre sa fin, puis relancer`;
342
+ const state = running === false ? 'aucun processus git en cours : verrou périmé' : 'processus git non vérifiable (ps illisible) : vérifier qu\'aucun ne tourne';
343
+ return `${seen}, ${state} ; le retirer avec \`rm ${lock}\` puis relancer`;
344
+ }
@@ -1,5 +1,5 @@
1
1
  // Part de la machine de chaque session (L350) : le 26-10, 32 cœurs et 4 sessions, vitest à 31 workers par suite, et des tests qui passaient seuls rougissaient ensemble.
2
- import { stripAnsi } from './baseline.js';
2
+ import { jestTestFiles, stripAnsi } from './baseline.js';
3
3
  /** Variable posée dans l'environnement des sessions et de la porte : le nombre de workers que leurs lanceurs de tests ont le droit d'ouvrir. */
4
4
  export const TEST_WORKERS_ENV = 'CADENCE_TEST_WORKERS';
5
5
  /** Cœurs ÷ plafond de sessions, arrondi vers le bas, jamais moins de 1 (cœurs inconnus ou plafond absurde compris). */
@@ -69,3 +69,30 @@ export function replayRanFile(output, file) {
69
69
  .split('\n')
70
70
  .some((line) => !LAUNCHER_ECHO.test(line) && re.test(line));
71
71
  }
72
+ /** Sortie d'un lanceur qui n'a trouvé aucun test à rejouer : la ligne de fin du lanceur (vitest « No test files found, exiting with code », jest « No tests found, exiting with code », pytest « no tests ran in »), en début de ligne — pas ces mots cités dans le titre ou la sortie d'un test. */
73
+ const NO_TEST_RAN = /^\W*(?:no test files? found, exiting with code|no tests? found, exiting with code|no tests ran in)\b/im;
74
+ /** Rejeux d'un test rouge de la porte, seul (L354) : rouge aux deux, il est compté. */
75
+ const REPLAYS = 2;
76
+ /**
77
+ * Rejoue SEULS les tests `names` rouges dans `output` (L354, L387) : `run` exécute la commande `template` où `{files}` devient les fichiers de ces tests, dans le dossier de l'appelant (worktree du lot, de la ligne de base ou de `verify --gate`).
78
+ * Un test vert à un rejeu est `unstable` ; rouge aux `REPLAYS` rejeux, ou sans fichier désignable (rien à rejouer seul), il reste dans `red`.
79
+ */
80
+ export async function replayRed(run, template, names, output) {
81
+ const jest = jestTestFiles(output);
82
+ const fileOf = (n) => replayFile(n) ?? jest.get(n) ?? null;
83
+ let red = names;
84
+ const unstable = [];
85
+ for (let i = 0; i < REPLAYS; i++) {
86
+ const replayable = red.filter((n) => fileOf(n));
87
+ const files = [...new Set(replayable.map((n) => fileOf(n)))];
88
+ if (!files.length)
89
+ break;
90
+ const r = await run(template.replaceAll('{files}', () => files.map(shellArg).join(' ')));
91
+ // Seul un rejeu qui sort en 0 en ayant exécuté le FICHIER du test vaut vert. Un rejeu en échec ne conclut rien pour un test qu'il ne nomme pas : sur une porte composée (`a && b`) les fichiers n'atteignent que le dernier lanceur, un premier lanceur qui rougit coupe avant lui. Un rejeu sans test exécuté (filtre sans fichier, `passWithNoTests`) sort en 0 sans rien prouver, et un lanceur qui ignore sans rien dire un fichier qu'il ne trouve pas (vitest d'un autre espace de travail) sort en 0 sans l'avoir joué : le fichier doit figurer, chemin entier, sur une ligne du lanceur de la sortie du rejeu, hors écho de la commande par npm/pnpm/yarn (`replayRanFile`).
92
+ const ran = r.code === 0 && !NO_TEST_RAN.test(r.output);
93
+ const passed = ran ? replayable.filter((n) => replayRanFile(r.output, fileOf(n))) : [];
94
+ unstable.push(...passed);
95
+ red = red.filter((n) => !passed.includes(n));
96
+ }
97
+ return { red, unstable };
98
+ }
package/dist/plan.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
3
- import { dirname, resolve } from 'node:path';
4
- import { isMap, isSeq, parseDocument } from 'yaml';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { Document, isMap, isSeq, parseDocument } from 'yaml';
5
5
  import { isDay, toDay } from './dates.js';
6
6
  /** Délai de la commande `plan.onWrite` : une republication du plan, pas un build. */
7
7
  const ON_WRITE_TIMEOUT_MS = 120_000;
@@ -218,9 +218,9 @@ export class Plan {
218
218
  get configFile() {
219
219
  return this.settings.config ?? null;
220
220
  }
221
- /** Fichiers tenus avec le plan, relatifs à la racine du dépôt. */
221
+ /** Fichiers tenus avec le plan, relatifs à la racine du dépôt (le fichier d'acquittement docs-ok d'un plan en lecture seule en est un, L386). */
222
222
  get files() {
223
- return this.settings.files ?? [];
223
+ return [...(this.settings.files ?? []), ...(this.readonly && this.settings.config ? [DOCS_OK_FILE] : [])];
224
224
  }
225
225
  /** Fichiers que produit `plan.onWrite` (L346), relatifs à la racine du dépôt : à commiter avec le plan, même s'ils n'étaient pas encore suivis. */
226
226
  get produced() {
@@ -318,9 +318,15 @@ export class Plan {
318
318
  node.flow = true;
319
319
  this.lotNode(lotId).set(key, node);
320
320
  }
321
+ /** Chemin du fichier d'acquittement docs-ok d'un plan en lecture seule : `.cadence/docs-ok.yaml` à côté du cadence.yaml lu (L386) ; null sans cadence.yaml. */
322
+ get docsOkFile() {
323
+ return this.settings.config ? join(dirname(this.settings.config), DOCS_OK_FILE) : null;
324
+ }
321
325
  lots() {
322
- if (this.settings.format)
323
- return this.read().lots;
326
+ if (this.settings.format) {
327
+ const ack = readDocsOkFile(this.docsOkFile);
328
+ return this.read().lots.map((l) => (l.docsOk || !ack[l.id] ? l : { ...l, docsOk: ack[l.id] }));
329
+ }
324
330
  const raw = this.doc.get('lots').toJSON();
325
331
  return raw.map((r) => normalizeLot(r));
326
332
  }
@@ -400,12 +406,32 @@ export class Plan {
400
406
  }
401
407
  /** Acquitte l'écart `docs.sync` d'un lot (L317) : clé `docs_ok: { date, reason }`, la raison est obligatoire. Jamais sur une sous-tâche. */
402
408
  setDocsOk(ref, reason, today) {
403
- this.writable();
404
409
  if (ref.includes('/'))
405
410
  throw new RafError(`docs-ok se pose sur un lot, pas sur une sous-tâche : ${ref}`);
406
411
  const why = reason.trim();
407
412
  if (!why)
408
413
  throw new RafError('docs-ok exige une raison : raf docs-ok <lot> --reason "pourquoi la documentation reste telle quelle"');
414
+ if (this.readonly) {
415
+ // Le plan est tenu par un autre outil : l'acquittement va dans un fichier de cadence, le plan n'est pas touché (L386).
416
+ const file = this.docsOkFile;
417
+ if (!file)
418
+ throw new RafError(`plan en lecture seule : ${this.path} est tenu par un autre outil — pas de cadence.yaml lu, donc pas de ${DOCS_OK_FILE} où acquitter`);
419
+ if (!this.lots().some((l) => l.id === ref))
420
+ throw new RafError(`lot inconnu : ${ref}`);
421
+ const doc = existsSync(file) ? parseDocument(readFileSync(file, 'utf8')) : new Document({});
422
+ if (doc.errors.length > 0)
423
+ throw new RafError(`${file} : ${doc.errors[0].message}`);
424
+ if (!isMap(doc.contents))
425
+ doc.contents = doc.createNode({});
426
+ const entry = doc.createNode({ date: today, reason: why });
427
+ entry.flow = true;
428
+ doc.set(ref, entry);
429
+ if (!existsSync(file))
430
+ doc.commentBefore = ' Écarts docs.sync acquittés (raf docs-ok) pour un plan tenu par un autre outil, lu par raf check et cadence orchestrate.';
431
+ mkdirSync(dirname(file), { recursive: true });
432
+ writeFileSync(file, doc.toString({ lineWidth: 0 }));
433
+ return;
434
+ }
409
435
  const node = this.doc.createNode({ date: today, reason: why });
410
436
  node.flow = true;
411
437
  this.lotNode(ref).set('docs_ok', node);
@@ -642,6 +668,28 @@ export class Plan {
642
668
  }
643
669
  }
644
670
  /** Lit une liste écrite dans un autre format : champs traduits, entrées à parent repliées en sous-tâches. */
671
+ /** Fichier d'acquittement docs-ok d'un plan en lecture seule, relatif à la racine du dépôt (L386). */
672
+ const DOCS_OK_FILE = '.cadence/docs-ok.yaml';
673
+ /** Les acquittements du fichier : lot → { date, reason }. Une entrée sans raison n'acquitte rien ; un fichier illisible est une erreur qui le nomme. */
674
+ function readDocsOkFile(file) {
675
+ if (!file || !existsSync(file))
676
+ return {};
677
+ const doc = parseDocument(readFileSync(file, 'utf8'));
678
+ if (doc.errors.length > 0)
679
+ throw new RafError(`${file} : ${doc.errors[0].message}`);
680
+ const raw = doc.toJSON();
681
+ if (raw == null)
682
+ return {};
683
+ if (typeof raw !== 'object' || Array.isArray(raw))
684
+ throw new RafError(`${file} : la racine doit être un objet (lot → { date, reason })`);
685
+ const out = {};
686
+ for (const [id, v] of Object.entries(raw)) {
687
+ const e = v;
688
+ if (e && typeof e === 'object' && typeof e.reason === 'string' && e.reason.trim() !== '')
689
+ out[id] = { date: String(e.date ?? ''), reason: e.reason.trim() };
690
+ }
691
+ return out;
692
+ }
645
693
  function foreignLots(raw, format) {
646
694
  const entries = raw.map((r, i) => {
647
695
  if (!r || typeof r !== 'object' || Array.isArray(r))
package/dist/release.js CHANGED
@@ -199,6 +199,7 @@ function steps(p, files, branch) {
199
199
  const tag = `v${p.version}`;
200
200
  const out = [
201
201
  `git add ${files.join(' ')} && git commit -m "release: ${p.version}"`,
202
+ 'cadence verify --gate --sha HEAD (la suite et le build rejoués sur le commit de version, où le CHANGELOG n\'a plus de [Unreleased] plein ; rouge = pas d\'éval ni de tag)',
202
203
  ...(p.hasEvalScript ? ['npm run eval:plugin -- --trust-plugin (facturé ; un cas sous le seuil arrête la release)'] : []),
203
204
  `git tag ${tag}`,
204
205
  `git push origin ${branch} ${tag} (le tag lance le workflow de publication)`,
package/dist/session.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { CLEAN_DAYS, scanStale } from './clean.js';
3
3
  import { dueLine, isRecurring, recurringByDue } from './recurring.js';
4
- import { audit, exemptPlanOnly, gateWaiting, nextUp, planCommits } from './audit.js';
4
+ import { audit, exemptPlanOnly, gateWaiting, nextUp, planCommits, workByLot } from './audit.js';
5
5
  import { diffDays, maxDay } from './dates.js';
6
6
  import { repoStatus } from './git.js';
7
7
  import { linkCommits } from './link.js';
@@ -32,10 +32,12 @@ function projectFacts(ctx, since) {
32
32
  return lines;
33
33
  }
34
34
  const short = (c) => `${c.sha.slice(0, 7)} ${c.subject}`;
35
- /** Commits de la fenêtre reliés aux lots, sans compter comme « sans lot » ceux qui ne touchent que le plan. */
35
+ /** Commits de la fenêtre reliés aux lots : ceux qui ne touchent que le plan ne comptent ni « sans lot » ni comme travail d'un lot (comme `raf commits`). */
36
36
  function period(ctx, since) {
37
37
  const commits = planCommits(ctx.plan, ctx.root, { since });
38
- const linked = exemptPlanOnly(linkCommits(ctx.plan.lots(), commits, ctx.plan.refs), ctx.plan, ctx.root);
38
+ const exempt = exemptPlanOnly(linkCommits(ctx.plan.lots(), commits, ctx.plan.refs), ctx.plan, ctx.root);
39
+ const byLot = new Map([...workByLot(ctx.plan, ctx.root, exempt)].filter(([, cs]) => cs.length > 0));
40
+ const linked = { ...exempt, byLot };
39
41
  // Comme raf check : un commit antérieur à l'adoption du plan n'avait pas à citer de lot.
40
42
  const adopted = ctx.plan.since;
41
43
  return adopted ? { ...linked, orphans: linked.orphans.filter((c) => c.day >= adopted) } : linked;
@@ -113,6 +115,7 @@ export function sessionStart(ctx, opts) {
113
115
  if (lock)
114
116
  out(`\n${lock.line}`);
115
117
  const { doing, ready, blocked } = nextUp(lots);
118
+ const work = workByLot(plan, ctx.root, all, doing.map((l) => l.id));
116
119
  out('\nEn cours');
117
120
  if (doing.length === 0)
118
121
  out(' (rien)');
@@ -121,7 +124,7 @@ export function sessionStart(ctx, opts) {
121
124
  const last = lastActivity(l, commits);
122
125
  const idle = last ? diffDays(last, today) : 0;
123
126
  const state = last ? `dernière activité ${last}${idle > opts.idle ? `, silencieux depuis ${idle} j` : ''}` : 'aucune activité';
124
- out(` ${l.id} ${l.title} (${commitCount(commits.length)}, ${state})`);
127
+ out(` ${l.id} ${l.title} (${commitCount(work.get(l.id)?.length ?? 0)}, ${state})`);
125
128
  }
126
129
  section(out, 'En attente', gateWaiting(lots, blocked).map(({ lot, text }) => `${lot.id} ${lot.title} (${text})`));
127
130
  const recent = period(ctx, opts.since);
@@ -1,9 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { lstatSync, mkdtempSync, rmSync, unlinkSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
3
+ import { availableParallelism, tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { onTermination } from './proc.js';
6
6
  import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
7
+ import { configuredCap } from './orchestrate/command.js';
8
+ import { replayRed, TEST_WORKERS_ENV, testWorkersFrom } from './orchestrate/workers.js';
7
9
  import { git, gitFailure, linkDeclared, linkIgnored } from './orchestrate/worktree.js';
8
10
  /** Délai de chaque commande de la porte : celui du rejeu de la ligne de base de la vague. */
9
11
  const GATE_TIMEOUT_MS = 30 * 60_000;
@@ -21,9 +23,9 @@ function killGroup(pgid) {
21
23
  * Une commande `sh -c` dans `cwd`, sortie standard et d'erreur réunies ; au délai, tout son groupe de processus est tué.
22
24
  * `group` reçoit le groupe en cours : le nettoyage d'un signal reçu par cadence le tue avant de retirer le worktree.
23
25
  */
24
- function sh(cmd, cwd, timeoutMs, group) {
26
+ function sh(cmd, cwd, timeoutMs, group, env) {
25
27
  return new Promise((resolve) => {
26
- const child = spawn('sh', ['-c', cmd], { cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
28
+ const child = spawn('sh', ['-c', cmd], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
27
29
  const pgid = child.pid;
28
30
  if (pgid === undefined) {
29
31
  child.once('error', (e) => resolve({ code: 127, output: e.message }));
@@ -50,6 +52,8 @@ function sh(cmd, cwd, timeoutMs, group) {
50
52
  * `cadence verify --gate` (L348) : la porte du projet (`orchestrate.build`, puis `orchestrate.test`, dans l'ordre de la vague : les tests lisent ce que le build produit)
51
53
  * rejouée au `sha` dans un worktree jetable, hors du dépôt, dont les dépendances non suivies sont reliées depuis le dépôt vivant (`orchestrate.worktreeDeps`,
52
54
  * sinon les dossiers ignorés de tout niveau, comme les worktrees de vague). Les deux commandes sont rejouées même si la première est rouge : le compte rendu est entier.
55
+ * Chaque commande reçoit `CADENCE_TEST_WORKERS` comme dans la porte d'une vague (L350, L387) : la valeur de l'environnement de l'appelant si c'est un entier positif, sinon cœurs ÷ plafond de sessions (`CADENCE_MAX_SESSIONS`, 2 par défaut) ;
56
+ * sans elle le rejeu `--maxWorkers=$CADENCE_TEST_WORKERS` de `orchestrate.testReplay` sortirait en erreur et ne prouverait rien.
53
57
  * Le worktree est retiré dans tous les cas. 0 vert, 1 rouge (avec les tests en échec), 2 rien à rejouer ou worktree impossible.
54
58
  */
55
59
  export async function verifyGate(ctx) {
@@ -59,6 +63,8 @@ export async function verifyGate(ctx) {
59
63
  ...(config.test ? [{ name: 'tests', cmd: config.test }] : []),
60
64
  ];
61
65
  const short = sha.slice(0, 7);
66
+ const base = ctx.env ?? process.env;
67
+ const env = { ...base, [TEST_WORKERS_ENV]: String(testWorkersFrom(base, ctx.cores ?? availableParallelism(), configuredCap(base))) };
62
68
  if (steps.length === 0) {
63
69
  out('verify --gate : ni orchestrate.test ni orchestrate.build déclarés dans cadence.yaml — rien à rejouer');
64
70
  return 2;
@@ -106,22 +112,47 @@ export async function verifyGate(ctx) {
106
112
  }
107
113
  let red = 0;
108
114
  for (const step of steps) {
109
- const r = await sh(step.cmd, dir, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group);
115
+ const r = await sh(step.cmd, dir, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group, env);
110
116
  if (r.code === 0) {
111
117
  out(`✓ ${step.name} (${step.cmd})`);
112
118
  continue;
113
119
  }
114
- red++;
115
- out(`✗ ${step.name} (${step.cmd}) — code ${r.code}`);
116
120
  const tests = step.name === 'tests';
117
121
  const names = tests ? failedTests(r.output) : [];
118
122
  const unloaded = tests ? unloadedSuites(r.output) : [];
119
- for (const n of names)
123
+ // `orchestrate.testReplay` comme la porte d'une vague (L354, L387) : un rouge nommé est rejoué seul avant d'être compté ; vert au rejeu, il est instable sous charge.
124
+ // Un échec que la sortie ne rattache à aucun test, ou un fichier qui ne se charge pas, n'est jamais rejoué.
125
+ const replayable = tests && config.testReplay && names.length > 0 && unloaded.length === 0 && !hasUnnamedFailure(r.output);
126
+ const replays = [];
127
+ const { red: still, unstable } = replayable
128
+ ? await replayRed(async (c) => {
129
+ const p = await sh(c, dir, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group, env);
130
+ replays.push(p.code);
131
+ return p;
132
+ }, config.testReplay, names, r.output)
133
+ : { red: names, unstable: [] };
134
+ if (replayable && still.length === 0) {
135
+ out(`✓ ${step.name} (${step.cmd}) — ${unstable.length} test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :`);
136
+ for (const n of unstable)
137
+ out(` - ${n}`);
138
+ continue;
139
+ }
140
+ red++;
141
+ out(`✗ ${step.name} (${step.cmd}) — code ${r.code}`);
142
+ for (const n of still)
120
143
  out(` - ${n}`);
121
144
  for (const n of unloaded)
122
145
  out(` - ${n} (ne se charge pas)`);
123
- if (names.length + unloaded.length === 0 || hasUnnamedFailure(r.output, true)) {
124
- if (names.length + unloaded.length)
146
+ // Un rejeu a eu lieu : dire lequel, pour qu'un rejeu en échec pour une autre raison qu'un vrai rouge (commande cassée, aucun test exécuté) ne passe pas pour une confirmation du rouge.
147
+ if (replays.length)
148
+ out(` rejeu seul (orchestrate.testReplay) : ${replays.length} rejeu(x), dernier code ${replays[replays.length - 1]} — ${names.length - still.length} test(s) confirmé(s) vert(s) sur ${names.length} ; un rejeu en échec ou sans test exécuté ne prouve rien`);
149
+ if (unstable.length) {
150
+ out(` ${unstable.length} autre(s) test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :`);
151
+ for (const n of unstable)
152
+ out(` - ${n}`);
153
+ }
154
+ if (still.length + unloaded.length === 0 || hasUnnamedFailure(r.output, true)) {
155
+ if (still.length + unloaded.length)
125
156
  out(' (au moins un échec que la sortie ne rattache à aucun test)');
126
157
  if (r.output.trim())
127
158
  for (const l of r.output.trimEnd().split('\n').slice(-TAIL_LINES))
package/docs/deliver.md CHANGED
@@ -305,8 +305,9 @@ verify --gate 6b0d9aa : rouge (1 étape sur 2)
305
305
  ```
306
306
 
307
307
  Both commands always run, even when the first is red, so the report is whole. A red step lists the failing
308
- tests the output names (the same reading as the wave's baseline) and the test files that do not load (`- test/a.test.ts (ne se charge pas)`, the typical sign of a missing dependency); with none identifiable, the last 10 lines of the output.
308
+ tests the output names (the same reading as the wave's baseline) and the test files that do not load (`- test/a.test.ts (ne se charge pas)`, the typical sign of a missing dependency); with none identifiable (or when an unnamed failure is mixed with them), the last 10 lines of the output, each prefixed ` | `: a red that names no test, seen three times under load on 27-10 (L380, B28.17), is no longer a bare exit code.
309
+ With `orchestrate.testReplay` declared (`docs/orchestrate.md`, « A red test is replayed alone »), the gate does what a wave does: a red test the output names is **replayed alone, twice at most**, in the same throwaway worktree. Green at a replay it is *unstable under load*: the step prints `✓ tests (…) — N test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :` with the names and does not make the gate red; the tests still red stay listed and the unstable ones follow, under a line of their own. A failure the output ties to no test, or a file that does not load, is never replayed and stays red. When a replay took place and the step stays red, one line says so (`rejeu seul (orchestrate.testReplay) : N rejeu(x), dernier code C — k test(s) confirmé(s) vert(s) sur n`): a replay that fails or runs no test confirms nothing, and the line tells a real red from a replay that could not run. Without `testReplay` nothing is replayed.
309
310
  Each command has 30 minutes (then its whole process group is killed, code 124). Ctrl-C, SIGTERM or a hangup received by cadence kills the running command and removes the worktree before cadence dies.
310
311
  `--sha` is required (the moving head of a wave is not a sha to re-verify) and `--retry` does not apply. Exit code:
311
312
  **0** green · **1** red · **2** nothing to replay (neither `orchestrate.build` nor `orchestrate.test`), unknown sha or a worktree that cannot be made.
312
- The Node of the `.nvmrc` is not selected here, and `CADENCE_TEST_WORKERS` is not set: the commands run with the environment of the caller (`PATH` included), so a re-verification run during a wave should export the session's `CADENCE_TEST_WORKERS` itself, or vitest takes every core.
313
+ The Node of the `.nvmrc` is not selected here: the commands run with the environment of the caller (`PATH` included). `CADENCE_TEST_WORKERS` is set for the build, the tests and the replays, as a wave does (`docs/orchestrate.md`, L350): the value of the caller's environment when it is a positive integer, otherwise cores ÷ session cap (`CADENCE_MAX_SESSIONS`, 2 by default), so a `testReplay` such as `--maxWorkers=$CADENCE_TEST_WORKERS` works from a plain shell.
package/docs/lead.md CHANGED
@@ -156,6 +156,8 @@ alpha · en cours L1 (1 commit, silencieux 4j) · dérive 1 (✗ L2 a 1 commit(s
156
156
  beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second · dépôt propre
157
157
  ```
158
158
 
159
+ The commit count of a lot in progress is the one of `raf commits`: plan commits (`plan: L1 démarré` and the like, which touch only plan files) are left out, so a lot just started reads `L1 (0 commit)` (L379).
160
+
159
161
  | Column | Content |
160
162
  |---|---|
161
163
  | `en cours` | ids of the lots in progress, each with its number of commits (`0 commit` included: a close note on a lot without commit is not « done, `raf done` forgotten »); then `aucune activité` when it has no activity at all, `silencieux Nj` when the lot's last activity (commit, note or start) is more than `--idle` days old (default 3) |
@@ -11,6 +11,7 @@ exception is a **formatting retry** (below).
11
11
  ```sh
12
12
  cadence orchestrate finance-tracker:L41 ol-companion:L22 cadence:L18@haiku
13
13
  cadence orchestrate L18 # from inside a project
14
+ cadence orchestrate .:L18 # the same, with the project named « . » (inside a repository)
14
15
  cadence orchestrate maritime-atlas:Q4/accueil-4-ux12@haiku # a lot id may contain "/" (sub-tasks of read-only plans)
15
16
  cadence orchestrate maritime:B28.4 --goal "…" # the goal replaces the stale title as the objective of the briefs (below)
16
17
  cadence orchestrate … --budget 1.5M # 1500000, 1.5M, 800k; default 2M
@@ -35,6 +36,12 @@ cadence orchestrate --drop ccc:L29 [--wave <wave>] # from another terminal: ta
35
36
  cadence orchestrate --stop-after-current [--wave <wave>] # from another terminal: let the running sessions finish, start nothing else
36
37
  ```
37
38
 
39
+ **Claude Code must be installed and logged in** (L378). Before starting a lot, `cadence orchestrate` runs `claude auth status` (no model, no cost); when it says `loggedIn: false` the command is refused (exit 2) before anything is written: no `raf start`, no plan commit, the lot stays `todo`. The message says what to do: « claude n'est pas connecté : lancer `claude` puis /login (état : `claude auth status`) ». `--dry-run` starts no session, so it is not refused: it prints the same message after its first line, as a `⚠`. `claude auth status` is read once per launch, so with `--continue` the rounds of a wave do not run it again for every candidate lot. An unreadable answer (a `claude` without `auth status`) is not a refusal.
40
+
41
+ **A stale `.git/index.lock` is refused up front** (L385). While the lock exists, every commit of the repository fails, the plan commit at the start of a lot included, and the plan would stay modified so that the next lot is handed back as a dirty repository. `cadence orchestrate` (and `--dry-run`, and `--resume`) therefore looks for `<git-dir>/index.lock` in each repository of the lot before writing anything, and refuses (exit 2): `<repo> : .git/index.lock présent depuis 13:46 (5 h 12 min), aucun processus git en cours : verrou périmé ; le retirer avec `rm <chemin>` puis relancer`. The process check is `ps`: when a `git` process is running the message says so and does not suggest removing the lock; when `ps` is unreadable it says the check could not be made. If a plan commit still fails during a wave (a lock taken after the start, a refusing hook), the plan is put back as it was, without going through the index, which the lock blocks: the lot fails, the next one finds a clean repository.
42
+
43
+ A `<project>` is a sub-folder of the current folder. Launched from inside `alpha`, `alpha:L1` looks for `alpha/alpha` and is refused: `alpha:L1 : dossier …/alpha/alpha introuvable (le projet est un sous-dossier du dossier courant ; dans un dépôt, `.:L1`)` (L376). Inside a repository, `.:L1` or `L1` names the lot of the repository itself.
44
+
38
45
  **`--drop` and `--stop-after-current` — steering a running wave (L79)**: both are run from the folder the wave was launched
39
46
  from (`--wave <id>` when several waves run from it), and write a request in the wave's state folder
40
47
  (`.cadence/runs/<wave>/control.log`, append-only), which the wave reads before each session — no signal, no edit of
@@ -225,7 +232,7 @@ orchestrate:
225
232
  the commits of the lot touch code without touching the document that describes it, the lot goes to a documentation pass
226
233
  before its review: a `fix` session launched with the `doc-keeper` agent (`agents/doc-keeper.md`) and its own brief
227
234
  (`fix-docs`), which lists the lines the program computed and asks for the documents to be brought up to date, nothing else. The
228
- pass counts as one of the two correction passes and plays once per lot: if the librarian leaves the gap open — it commits no document, and has not acknowledged the gap with `raf docs-ok <lot> --reason "…"` (L317) — the lot is **handed back** (cause « documentation en retard (docs.sync) ») and the review does not play: a lot never ends ready while `raf check` reports a documentation gap. The same check runs at the end of the conforming review, for a lot whose librarian gate could not play (passes or budget used up). An acknowledged gap (`docs_ok:` on the lot, committed with the plan) is no gap: no pass, no finding in the review brief. In a lot's worktree (`orchestrate.worktrees`) only the program writes the plan: the librarian's brief there does not ask it to run `raf docs-ok` — it makes no commit and says why under "choix", the lot is handed back and the lead acknowledges in the main checkout; an acknowledgement the lead committed before the wave is read from the lead's plan (L320). It does not play during the minors pass, nor when a
235
+ pass counts as one of the two correction passes and plays once per lot: if the librarian leaves the gap open — it commits no document, and has not acknowledged the gap with `raf docs-ok <lot> --reason "…"` (L317) — the lot is **handed back** (cause « documentation en retard (docs.sync) ») and the review does not play: a lot never ends ready while `raf check` reports a documentation gap. The same check runs at the end of the conforming review, for a lot whose librarian gate could not play (passes or budget used up). An acknowledged gap (`docs_ok:` on the lot, committed with the plan; for a read-only plan, an entry of `.cadence/docs-ok.yaml`, which the librarian's brief then names to commit instead of the plan, L386) is no gap: no pass, no finding in the review brief. In a lot's worktree (`orchestrate.worktrees`) only the program writes the plan: the librarian's brief there does not ask it to run `raf docs-ok` — it makes no commit and says why under "choix", the lot is handed back and the lead acknowledges in the main checkout; an acknowledgement the lead committed before the wave is read from the lead's plan (L320). It does not play during the minors pass, nor when a
229
236
  correction pass is already used (its pass would take the last one), nor when the lot budget no longer pays it and the review after
230
237
  it (a warning `bibliothécaire non appelé` says so each time; the review goes ahead). Without `docs.sync`, or with no gap: no pass.
231
238
  When the project's `cadence.yaml` declares `docs.articles` (the same key as the article refreshed after a delivery, see [news](news.md#an-article-elsewhere-that-follows-the-delivery)), the brief names those
@@ -586,7 +593,7 @@ nothing below applies and the wave behaves as before; any other value is refused
586
593
  stacked). After the rebase the program lists the entries the lot's commits wrote under `[Unreleased]` that are neither at the starting point nor at
587
594
  the lot's tip (nor under a version the tip itself carries: an entry the lot published itself is final), drops them from the result (under `[Unreleased]` and under a version), and puts back an entry of the tip that is missing, in a commit of
588
595
  its own (`docs: CHANGELOG — une entrée finale par lot …`); an entry the repository's branch also carries is left alone. The union of an earlier replayed commit files the lot's entry at the end of its section; where the lot first put it (top of the section included) does not matter for the next replayed commit. The journal says
589
- `<project>:<lot> CHANGELOG.md : une entrée finale par lot, les versions intermédiaires du lot écartées`. Anything else — an entry that
596
+ `<project>:<lot> CHANGELOG.md : une entrée finale par lot, les versions intermédiaires du lot écartées`. **A bare entry** (L383): an entry written under `## [Unreleased]` without a section on one side, against a section (`### Fixed`) the other side added, is not a free text: it is filed under that section (the entries of the repository's branch first), so the result has one title and one entry per lot. The section is the only one the two sides added to the base, or the only one they carry; with none, or several candidates, the conflict is left by hand. Anything else — an entry that
590
597
  was already there removed or reworded, free text under `[Unreleased]`, the rest of the
591
598
  file changed on both sides, another file in conflict with it — aborts the rebase and hands the lot back as above. The rebased commits differ, so
592
599
  steps 3 and 4 apply: tests replayed, and a short review since the patch-id changed;
@@ -836,7 +843,7 @@ News instruction (`cadence news new <lot>`, factual user-side text, a screenshot
836
843
  ```yaml
837
844
  orchestrate:
838
845
  test: npm test # run by the orchestrator after a work step (optional)
839
- testReplay: npx vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS {files} # replay alone of a red test before it counts (L354, see « A red test is replayed alone »); `{files}` required; absent: no replay, nothing held for unstable (optional)
846
+ testReplay: npx vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS {files} # replay alone of a red test before it counts (L354, see « A red test is replayed alone »; `cadence verify --gate` applies it too, L387, `docs/deliver.md`); `{files}` required; absent: no replay, nothing held for unstable (optional)
840
847
  build: npm run build # run BEFORE the tests (L333); both results go to the reviewer, who does not redo them (optional)
841
848
  worktreeDeps: [node_modules, .venv] # untracked dependencies that `cadence verify --gate` links into its throwaway worktree (L348, `docs/deliver.md`); absent: the ignored folders of every level, as the worktrees of a wave (optional)
842
849
  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)
package/docs/overview.md CHANGED
@@ -62,6 +62,7 @@ The two plugins are independent. `cadence@cadence` carries the rituals, which ca
62
62
  | Page | What it covers |
63
63
  |---|---|
64
64
  | [docs/quickstart.md](quickstart.md) | getting started as a lead of several projects, in 10 minutes: install, the parent folder, `raf init`, the tour, the first wave |
65
+ | [docs/with-your-tools.md](with-your-tools.md) | cadence next to a tracker, `HANDOFF.md`, ADRs and a commit convention: what it does not replace, what is optional, what plugs in (`plan:` read-only, `plan.commit`, `docs.sync`), what it adds |
65
66
  | [docs/raf.md](raf.md) | the plan: commands, plan file, foreign plan formats, Gantt |
66
67
  | [docs/orchestrate.md](orchestrate.md) | waves of sessions: budgets, spec, accept and review steps, neighbouring repositories, one worktree per lot (`orchestrate.worktrees: true`, on in cadence's own `cadence.yaml`, off by default elsewhere; `--integrate`, `--replay`, `--remove-worktree`; recovery of a handed-back lot's branch and the `CHANGELOG.md` union at the integration are described there) |
67
68
  | [docs/deliver.md](deliver.md) | delivery: wait for CI, deploy, verify the effect |
@@ -27,7 +27,8 @@ You need four things; the page says how to check each.
27
27
  ```
28
28
 
29
29
  - **Claude Code, installed and logged in.** `claude auth status` must print `"loggedIn": true`. A wave opens
30
- `claude -p` sessions and stops at the first one with `Not logged in · Please run /login` otherwise.
30
+ `claude -p` sessions; `cadence orchestrate` checks `claude auth status` first and, when Claude Code is not logged in,
31
+ is refused before it writes anything in the plan (« claude n'est pas connecté : lancer `claude` puis /login »).
31
32
  Install it from [claude.com/claude-code](https://claude.com/claude-code), then run `claude` once and log in.
32
33
  - **A test command** for each project (`npm test`, `pytest`, `go test ./...`). The wave runs it after every change.
33
34
 
@@ -70,14 +71,13 @@ orchestrate:
70
71
  test: test -f hello.txt # passes once the first lot has created hello.txt
71
72
  EOF
72
73
  git add -A && git commit -m "chore: init raf"
73
- raf ignore $(git rev-parse --short HEAD)
74
- git add .cadence/raf.yaml && git commit -m "plan: acknowledge init"
75
74
  cd ..
76
75
  ```
77
76
 
78
77
  What it does: `raf init` writes the plan (`.cadence/raf.yaml`), a commit hook and a `.gitignore` line. The commit
79
- that adds the plan cites no lot, so `raf` warns `commit sans lot` and `raf check` would list it as a gap: `raf ignore`
80
- acknowledges it once, and the plan records that. `cadence.yaml` is the project's settings file; its single
78
+ that adds them cites no lot and is no gap: like any commit that touches only the plan files, it is left out of `raf check`
79
+ and of the tour's drift column (what `raf init` writes counts as plan files, `cadence.yaml` created in the same commit
80
+ with its `orchestrate.test` line included; a code file in it would make it a `commit sans lot` again). `cadence.yaml` is the project's settings file; its single
81
81
  `orchestrate.test` line is the minimum the wave needs, and without it the tour warns `porte de tests absente`.
82
82
  The folder is empty, so the gate here is `test -f hello.txt`, which the toy lot satisfies. On a project that already exists,
83
83
  put its own test command there (`npm test`, `pytest`, `go test ./...`): `npm test` in a folder without `package.json` fails.
@@ -126,7 +126,7 @@ The sessions of a wave never push and never deliver. You read the table, answer
126
126
  | `npm: command not found` | no Node | install Node 22.18+ (above) |
127
127
  | `EACCES: permission denied, mkdir '/usr/lib/node_modules/@sylad'` | system-owned Node | `npm config set prefix ~/.npm-global` (above) |
128
128
  | `fatal: empty ident name … not allowed` | no git identity | the two `git config --global` lines (above) |
129
- | `Not logged in · Please run /login` | Claude Code not logged in | `claude`, log in, then `claude auth status` |
129
+ | « claude n'est pas connecté : lancer `claude` puis /login » | Claude Code not logged in | `claude`, log in, then `claude auth status` |
130
130
  | `porte de tests absente` | no `orchestrate.test` | the `cadence.yaml` line of the project block |
131
131
  | `alpha:L1 … introuvable` | launched from inside `alpha` | use `.:L1` |
132
132
 
package/docs/raf.md CHANGED
@@ -21,7 +21,10 @@
21
21
  the plan, a journal), or the QA expectations file (`docs/qa/expectations.md`,
22
22
  or the file named by `qa.expectations`; the lead commits the `qa-reviewer`'s draft of it). Such a commit is never a "commit without a lot", and it
23
23
  does not count as work on the lots it cites: it is absent from `raf commits`,
24
- does not start a `todo` lot, does not make a code review stale, and
24
+ does not start a `todo` lot, does not make a code review stale, is not in the commit
25
+ count that `raf now`, `cadence lead tour` and `cadence session start` show for a lot in
26
+ progress (the same count as `raf commits` and `raf show`: after `plan: L1 démarré` alone,
27
+ `raf now` prints no commit and the tour `L1 (0 commit)`, L379; the commit column and the tooltip of `raf gantt` read the same count, L381), and
25
28
  `cadence deliver` does not announce the lots it cites as delivered. The files
26
29
  decide, never the subject: a `chore(plan): …` commit that touches a source
27
30
  file is a commit like any other.
@@ -47,6 +50,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
47
50
 
48
51
  | Command | Effect |
49
52
  |---|---|
53
+ | `raf --version` · `raf -v` | print the version of the package, as `cadence --version` does (L376); only as the first argument, `raf show --version` stays an unknown option |
50
54
  | `raf init [--project name] [--prefix L] [--no-hook]` | create the plan, install the hook, add `.playwright-mcp/` to `.gitignore` |
51
55
  | `raf add "title" [--estimate d] [--quickwin] [--visible [--screen page\|component\|journey]] [--spec] [--public "title"] [--after L2,L4] [--uses banc,passerelle] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`; `--screen`: the screen a visible lot creates or changes — a new `page`, a new `component` or a modified `journey` — written as `screen:` right after `visible:`, shown by `raf show`; it requires `--visible`, and `raf check` reports a `screen:` without `visible:` or with another value as an error. Only `page` triggers the spec step of `orchestrate` (L209), never inferred from the title; `--spec`: the lot changes a behaviour (something a user, an API client or another service can observe), written as `spec: true` right after `screen:`, `visible:`, `quickwin:` or `estimate:` (whichever comes last), shown by `raf show`, refused on a sub-task. Also set by the lead at planning time, never inferred from the title); `--uses`: the shared resources the lot occupies, written as `uses: [banc, passerelle]` after `after:` and before `created:` ([shared resources](#shared-resources-uses-and-resources), L251), refused on a sub-task) |
52
56
  | `raf add "title" --every <days>` · `raf did <id> ["text"]` | recurring lot: `every` (days) and `last` (last time done) fields; the lot stays `todo`, `raf now` and `session start` list it under « Récurrent » with « dû depuis N j » / « prochain dans N j »; `raf did` resets the count (spec: `docs/superpowers/specs/2026-10-04-L11-tache-recurrente.md`) |
@@ -65,7 +69,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
65
69
  | `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first; a lot that declares `repos:` ([neighbouring repositories](orchestrate.md#a-lot-whose-work-is-in-a-neighbouring-repository)) gets, after them, one `dépôt <path> :` section per neighbour with the commits there that cite the lot and fall within the lot's period (`started` to `finished`) and, when the lot declares a `cite:` for that neighbour, contain that string (`aucun commit du lot`, or `introuvable` for a path that is not there) |
66
70
  | `raf now` | what to do next |
67
71
  | `raf list [--status s]` | flat list |
68
- | `raf docs-ok <lot> --reason text` | acknowledge the `docs.sync` gap of a lot (a document behind its commits, `raf check` red) when the documentation really stays true: a `docs_ok: { date, reason }` key on the lot, **the reason is required** (refused without it, for an unknown lot or for a sub-task); `raf check` and `cadence lead tour` no longer count the lot, `raf show` prints it, and `cadence orchestrate` neither calls the librarian nor holds the lot back for it. Commit the plan with it |
72
+ | `raf docs-ok <lot> --reason text` | acknowledge the `docs.sync` gap of a lot (a document behind its commits, `raf check` red) when the documentation really stays true: a `docs_ok: { date, reason }` key on the lot, **the reason is required** (refused without it, for an unknown lot or for a sub-task); `raf check` and `cadence lead tour` no longer count the lot, `raf show` prints it, and `cadence orchestrate` neither calls the librarian nor holds the lot back for it. Commit the plan with it; on a read-only plan (kept by another tool) the key goes to `.cadence/docs-ok.yaml` instead, and that file is committed (L386) |
69
73
  | `raf ignore <sha> \| "exact subject" [--reason text]` | acknowledge a commit without a lot (tooling chore, a plan commit citing an unknown id) or a commit that cites known lots without working on them (a lead's plan commit creating lots, a setting cited in parentheses): it then counts for none of them — not started, not active, not to review (`raf commits` omits it) — without rewriting history: a dated, reasoned line in the plan's `acknowledged:` section; a sha is exact, a subject covers every commit carrying it; exactly one commit argument, the reason goes through `--reason "text"` (`raf ignore 1a2b3c4 "tooling"` is refused with « un seul commit attendu ; la raison se passe par --reason "texte" », exit 2) |
70
74
  | `raf check --ignored` | list the acknowledged commits with their date and reason |
71
75
  | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default (a visible lot without a public title is only a `⚠` warning: it never changes the exit code): commits without a lot (commits touching only plan files, or only version fields, `CHANGELOG.md`, the README's « What's new » section and `docs/whats-new.md` are exempt), unknown ids, `todo` lots that already have commits, idle lots (an open lot whose sub-lots, entries carrying `parent:` in a plan kept by another tool, have a recent commit or are in progress is not idle: L321), `done` lots with open sub-tasks, bad or circular dependencies, and an open lot titled « Revue UX — <screen> » without `visible: true` (`ux-review-not-visible`, an error: such a lot goes through precheck → implement → review with no UX step, so the screen is never reviewed — finance L32 on 2026-10-23; L271; the message names the fix, `raf visible <id> on`, L273; on a read-only plan, plan kept by another tool, it never names a `raf` command, which would refuse: with `plan.fields.visible` mapped it says to set `visible:` with the project's tool, without it it says the plan carries no `visible`, to map the field in `cadence.yaml` (`plan.fields.visible`) if the project's tool writes it, and that otherwise the UX review of that screen happens outside orchestrate, L321) |
@@ -129,6 +133,10 @@ then the legacy `docs/plan/raf.yaml`.
129
133
  - `raf init` creates `.cadence/raf.yaml` and `.cadence/notes/` (with a `.gitkeep`, so git tracks the empty folder). It refuses
130
134
  when a plan already exists where it would create one, a legacy `docs/plan/raf.yaml` included: it never creates a second plan
131
135
  beside it. With `plan: path` in `cadence.yaml`, or `--file`, the plan and nothing else is created there.
136
+ The commit that follows `raf init` opens no « commit sans lot » gap (L377): the plan, `.cadence/notes/.gitkeep`, the
137
+ `.playwright-mcp/` line of `.gitignore` and a `cadence.yaml` created in the same commit with only `plan:` and `orchestrate:` keys are plan files, so such a commit is
138
+ a plan-only commit for `raf check`, the hook and the tour. Any other file in it, another `.gitignore` line, a `deliver:` or `session:` key
139
+ in a new `cadence.yaml` or a later edit of it keeps it a commit like any other.
132
140
  - A project that still has `docs/plan/raf.yaml` (and no `.cadence/raf.yaml`) keeps working, read and written as before. Every command
133
141
  that loads the plan prints one line on **stderr** (standard output, `--json` included, is unchanged) with the move to do —
134
142
  `cd <repository root> && mkdir -p .cadence && git mv docs/plan/raf.yaml .cadence/raf.yaml` (`mv` outside a git repository or when git does not track the file, where `git mv` would fail; the root is spelled out, quoted if
@@ -268,6 +276,10 @@ the file, and `raf now`, `raf list`, `raf commits`, `raf check`, `raf gantt` and
268
276
  `cadence session start|close` work on it. Such a plan is **read-only** —
269
277
  `raf add|start|done|note|ux|review` refuse and leave the file to the project's tool, and the two
270
278
  review gates do not apply to it (a `uxSince` or `reviewSince` written in it is ignored).
279
+ One acknowledgement is kept outside the plan so it works here too: `raf docs-ok <lot> --reason "…"`
280
+ writes `.cadence/docs-ok.yaml` (next to `cadence.yaml`, one line `L12: { date: 2026-10-27, reason: … }` per lot),
281
+ never the plan; commit that file. `raf check`, `raf show`, `cadence lead tour` and `cadence orchestrate` read it
282
+ (an entry without a reason acknowledges nothing, an unreadable file is an error that names it) (L386).
271
283
 
272
284
  ```yaml
273
285
  plan:
package/docs/releasing.md CHANGED
@@ -3,7 +3,7 @@
3
3
  # Releasing
4
4
 
5
5
  A version exists in three places and is published in two; a release does all of it, in this order.
6
- `cadence release patch|minor|major [--dry-run]` (L341) prepares step 1 and prints steps 2 to 5 in order; it publishes nothing.
6
+ `cadence release patch|minor|major [--dry-run]` (L341) prepares step 1 and prints steps 2 to 6 in order; it publishes nothing.
7
7
 
8
8
  1. Bump `version` in `package.json` (then `npm install` to refresh `package-lock.json`),
9
9
  `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`, and write the version's section of
@@ -16,15 +16,22 @@ A version exists in three places and is published in two; a release does all of
16
16
  in that commit, not before the review. A commit that touches any other file
17
17
  — or another field of a manifest, or a dependency in `package-lock.json` — is work like any other and has
18
18
  to be reviewed.
19
- 2. Run the plugin evaluation, `npm run eval:plugin` (billed, see [Evaluating the plugin](#evaluating-the-plugin-before-a-release)),
19
+ 2. Replay the project's gate on that commit: `cadence verify --gate --sha HEAD` (L380). The version commit empties
20
+ `## [Unreleased]` of `CHANGELOG.md`, and a test that read only that section would pass before it and fail after — the red
21
+ publish run of 1.14.0 (L368) and the stopped 1.15.2 (L380) — so the suite is run on the commit that will be tagged, before the
22
+ billed evaluation and before the tag. Red: fix it in a commit of its own and redo the version commit. A test that looks
23
+ for the entry of a lot in the CHANGELOG reads the section that carries it (`[Unreleased]` or the published version) through
24
+ `changelogSectionOf` (`test/changelog-section.ts`); `test/changelog-garde.test.ts` refuses a test that selects `## [Unreleased]`
25
+ of the repository's CHANGELOG itself.
26
+ 3. Run the plugin evaluation, `npm run eval:plugin` (billed, see [Evaluating the plugin](#evaluating-the-plugin-before-a-release)),
20
27
  on that version commit; a case below the threshold stops the release until you have read it in the report.
21
- 3. `git tag v<version> && git push origin main v<version>` — the tag starts `.github/workflows/publish.yml`,
28
+ 4. `git tag v<version> && git push origin main v<version>` — the tag starts `.github/workflows/publish.yml`,
22
29
  which publishes to npm through Trusted Publishing (OIDC, no token stored anywhere): it checks the tag
23
30
  matches `package.json`, `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` and that `CHANGELOG.md` has a `## [x.y.z]` section for it (no section, no publication), then `npm publish --provenance`, where `prepublishOnly` runs the type-check and the
24
31
  tests and `prepare` builds `dist/`; a red suite stops the publication; then it creates the GitHub release with that CHANGELOG section as its text. The trusted publisher is declared
25
32
  once on npmjs.com (package settings → Trusted Publisher → GitHub Actions, `Sylad/cadence`, `publish.yml`).
26
- 4. Watch the run: `gh run watch` (or `gh run list --workflow publish.yml`).
27
- 5. Check the effect: `npm view @sylad/cadence version` answers the new version.
33
+ 5. Watch the run: `gh run watch` (or `gh run list --workflow publish.yml`).
34
+ 6. Check the effect: `npm view @sylad/cadence version` answers the new version.
28
35
  A run is safe to re-run, and two runs for one tag queue instead of racing (`concurrency` per ref, never cancelling the one that publishes):
29
36
  a version already on npm skips `npm publish`, and a GitHub release that is missing is created (`--verify-tag`)
30
37
  while an existing one is left alone. If the package is on npm but the release is still missing, re-run the job;
@@ -32,10 +39,12 @@ A version exists in three places and is published in two; a release does all of
32
39
 
33
40
  `.github/workflows/tests.yml` runs the type-check and the test suite on the same runner image and `npm ci` as the publish job, in a matrix of Node 22 and 24 (the minimum `engines.node` allows, and the Node the publish job uses), and never publishes: start it with `gh workflow run tests.yml --ref <branch>` or by pushing a `diag/**` branch, to see on the runner what a red publish run saw.
34
41
 
42
+ The suite runs the same way on your machine as on that runner, **without `raf` or `cadence` installed** (L384): `test/global-setup.ts` replaces, in the `PATH` of the tests, every folder outside the repository that carries a `raf` or a `cadence` (`~/.local/bin`, the global npm folder, the plugin's `bin/`) by a mirror of that folder without those two names — the other tools stay, `node` included. A test that relies on the installed tool turns red on the lead's machine and in the gate, not only in the publish run: the 1.16.0 publication failed because the post-commit hook of `test/raf-init-commit.test.ts` (R2) found `raf` in the developer's `PATH` and not on the runner. A test that needs `raf` (a hook, a script) builds its own from the sources (`CADENCE_TEST_PACKAGE`).
43
+
35
44
  The Claude Code plugin is read from the repository, so pushing `main` is what updates it; npm is what
36
45
  `npx @sylad/cadence` and a global install read, and only the tag publishes there. A missing tag, or a red
37
46
  publish run, leaves npm behind without any other error — 0.3.0 and 0.4.0 were never published — hence
38
- step 5.
47
+ step 6.
39
48
 
40
49
  ## `cadence release`
41
50
 
@@ -70,6 +79,7 @@ introduction (the newest version first, as the file is read). Exit code 1 when a
70
79
  version files are written, the commit is not ready. `--dry-run` reads the forms the same way (nothing is written) and exits 1 on the same findings, so the forms can be checked before the real run.
71
80
 
72
81
  It prints the lead's steps, in order, and runs none: the `git add` of exactly the files of the version commit and the commit,
82
+ `cadence verify --gate --sha HEAD` (always printed, before the evaluation and the tag),
73
83
  `npm run eval:plugin -- --trust-plugin` (only when `package.json` has that script), `git tag v<x.y.z>`, `git push origin <branch> v<x.y.z>`,
74
84
  `gh run watch`, `npm view <package> version` (not for a private package) and, only with a `.claude-plugin/plugin.json`,
75
85
  the plugin update: `claude plugin marketplace update <marketplace>` first (without it the installed plugin stays at the old