@sylad/cadence 1.23.0 → 1.24.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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "cadence",
10
10
  "description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
11
- "version": "1.23.0",
11
+ "version": "1.24.0",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
3
  "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
- "version": "1.23.0",
4
+ "version": "1.24.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { basename, dirname, join, posix, relative, resolve } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
@@ -14,7 +14,7 @@ import { ganttData, renderGantt } from './gantt.js';
14
14
  import { PLAN_ACTIONS, planCommitMessage } from './plan-commit.js';
15
15
  import { dirtyPaths, gitRoot, isTracked, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
16
16
  import { installHook, pendingCommitMessage, REFUSED } from './hook.js';
17
- import { pushRefusal } from './push-guard.js';
17
+ import { gateRefusal, pushRefusal, UNGATED_ENV } from './push-guard.js';
18
18
  import { citedRefs, linkCommits } from './link.js';
19
19
  import { buildNews, loadEntries, newEntry, newsData, newsIssues, publicTitleTooLong, stampEntries } from './news.js';
20
20
  import { invalidResource, Plan, RafError, SCREENS, STATUSES, setPlanNotice } from './plan.js';
@@ -26,6 +26,7 @@ import { findProjects, readDepth, leadTour, readLeadUx, tourFreeLine, tourLine,
26
26
  import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
27
27
  import { addHold, releaseHold } from './orchestrate/registry.js';
28
28
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
29
+ import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
29
30
  import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
30
31
  import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
31
32
  import { classifyAll, measure, ollamaClassifier, readCache, readJournals, readRuns, renderReport, sampleKeys, serializeCache } from './corrections.js';
@@ -86,7 +87,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
86
87
  cadence ref corpus <dossier> [--sans-rendu] écrit le corpus synthétique à vérité connue (7 pièges : texte ambigu levé par une figure, figure qui contredit, table en image, gabarit BUFR, contrôle translucide, page Proposal, scan bruité) : par cas document.html et attendu.json, puis document.pdf et page-<n>.png (1700×2200) rendus par Playwright (CADENCE_PLAYWRIGHT = dossier du paquet) ; dossier existant ou Playwright introuvable : code 2
87
88
  cadence release patch|minor|major [--dry-run] prépare le commit de version (package.json, package-lock.json, plugin.json, marketplace.json, CHANGELOG.md) sans commiter ni publier ; imprime les étapes du lead et vérifie docs/whats-new.md : refus sur un arbre sale, un [Unreleased] vide ou une version déjà taguée
88
89
  cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
89
- cadence verify --gate --sha rév [--project dossier] porte du projet (orchestrate.build puis orchestrate.test) : reprend le verdict enregistré par la vague quand son sha est le même et son arbre identique, sans rien rejouer (la sortie dit « verdict repris de la porte de la vague ») ; sinon la rejoue (« verdict rejoué : raison ») au sha dans l'arbre principal s'il est propre au sha, sinon dans un worktree jetable aux dépendances copiées (orchestrate.worktreeDeps), puis le retire : 0 vert, 1 rouge avec les tests en échec, 2 rien à rejouer
90
+ cadence verify --gate --sha rév [--project dossier] porte du projet (orchestrate.build puis orchestrate.test) : reprend le verdict enregistré par la vague quand son sha est le même et son arbre identique, sans rien rejouer (la sortie dit « verdict repris de la porte de la vague ») ; sinon la rejoue (« verdict rejoué : raison ») au sha dans l'arbre principal s'il est propre au sha, sinon dans un worktree jetable aux dépendances copiées (orchestrate.worktreeDeps), puis le retire : 0 vert, 1 rouge avec les tests en échec, 2 rien à rejouer ; le verdict (0 ou 1) est noté par sha et le hook pre-push refuse un push dont le sha n'a pas de verdict vert (CADENCE_PUSH_UNGATED=1 passe outre)
90
91
  raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
91
92
 
92
93
  Un commit appartient à un lot quand son message cite l'identifiant : « feat(L3): … », « L3/t1 ».
@@ -414,6 +415,7 @@ function dispatch(argv, io) {
414
415
  }
415
416
  plan.save();
416
417
  io.out(id);
418
+ warnIntegrating(root, io.err);
417
419
  return 0;
418
420
  }
419
421
  case 'start':
@@ -455,6 +457,8 @@ function dispatch(argv, io) {
455
457
  plan.note(rest[0], `abandonné : ${values.reason}`, today);
456
458
  plan.save();
457
459
  io.out(`${rest[0]} → ${status}`);
460
+ if (status === 'done')
461
+ warnIntegrating(root, io.err);
458
462
  if (status === 'done' && !rest[0].includes('/')) {
459
463
  const lot = plan.lot(rest[0]);
460
464
  if (lot.visible && !loadEntries(newsDir).some((e) => e.lots.includes(lot.id))) {
@@ -509,6 +513,7 @@ function dispatch(argv, io) {
509
513
  const plan = loadPlan();
510
514
  plan.note(rest[0], rest.slice(1).join(' '), today);
511
515
  plan.save();
516
+ warnIntegrating(root, io.err);
512
517
  return 0;
513
518
  }
514
519
  case 'did': {
@@ -742,7 +747,7 @@ function dispatch(argv, io) {
742
747
  if (rest[0] === 'pre-commit')
743
748
  return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
744
749
  if (rest[0] === 'pre-push')
745
- return prePush(root, io, rest[1]);
750
+ return prePush(root, io, rest[1], configPath);
746
751
  throw new RafError('usage : raf hook install|pre-commit|post-commit|pre-push');
747
752
  }
748
753
  case 'session': {
@@ -1125,13 +1130,11 @@ function morningEffects(configPath, root) {
1125
1130
  }
1126
1131
  /**
1127
1132
  * `raf hook pre-push` (L405), appelé par le hook que la vague pose : refuse (code 3) un push qui emporte un commit d'un lot pas prêt de la vague vivante
1128
- * du dépôt, ou dont un lot pas prêt a des commits illisibles (la garde de `deliver --sha`). Sans vague qui tient le dépôt, ou avec un état de vague illisible
1129
- * (avertissement), un hook ne gêne jamais un push.
1133
+ * du dépôt, ou dont un lot pas prêt a des commits illisibles (la garde de `deliver --sha`). Puis (L426), quand le projet déclare une porte, refuse un push dont
1134
+ * le sha de tête n'a pas de verdict vert de `cadence verify --gate` (`CADENCE_PUSH_UNGATED=1` passe outre, et le dit). Sans vague qui tient le dépôt, ou avec un état de vague illisible
1135
+ * (avertissement), la première garde ne gêne jamais un push ; une config illisible laisse passer la seconde.
1130
1136
  */
1131
- function prePush(root, io, remoteName) {
1132
- const holding = waveHolding(root);
1133
- if (!holding)
1134
- return 0;
1137
+ function prePush(root, io, remoteName, configPath) {
1135
1138
  let input = '';
1136
1139
  try {
1137
1140
  input = io.stdin ? io.stdin() : readFileSync(0, 'utf8');
@@ -1139,17 +1142,51 @@ function prePush(root, io, remoteName) {
1139
1142
  catch {
1140
1143
  return 0;
1141
1144
  }
1142
- const lots = waveLotsIn(cadenceHome(), holding.pid, root);
1143
- if (lots === null) {
1144
- io.err(`raf: vague ${holding.wave} : état illisible, push non vérifié`);
1145
+ const holding = waveHolding(root);
1146
+ if (holding) {
1147
+ const lots = waveLotsIn(cadenceHome(), holding.pid, root);
1148
+ if (lots === null) {
1149
+ io.err(`raf: vague ${holding.wave} : état illisible, push non vérifié`);
1150
+ }
1151
+ else {
1152
+ const refusal = pushRefusal(root, holding.wave, lots, input, remoteName || undefined);
1153
+ if (refusal) {
1154
+ io.err(`raf: ${refusal}`);
1155
+ return REFUSED;
1156
+ }
1157
+ }
1158
+ }
1159
+ // La porte du projet (L426) : une config illisible ne gêne pas un push, comme un état de vague illisible.
1160
+ let gate = false;
1161
+ try {
1162
+ const config = readOrchestrateConfig(configPath);
1163
+ gate = Boolean(config.test || config.build);
1164
+ }
1165
+ catch {
1145
1166
  return 0;
1146
1167
  }
1147
- const refusal = pushRefusal(root, holding.wave, lots, input, remoteName || undefined);
1148
- if (refusal) {
1149
- io.err(`raf: ${refusal}`);
1150
- return REFUSED;
1168
+ const refusal = gateRefusal(root, gate, input);
1169
+ if (!refusal)
1170
+ return 0;
1171
+ if (io.env[UNGATED_ENV] === '1') {
1172
+ io.err(`raf: ${refusal.replace(/^push refusé/, 'push passé outre')} — ${UNGATED_ENV}=1 est posée`);
1173
+ return 0;
1151
1174
  }
1152
- return 0;
1175
+ io.err(`raf: ${refusal}`);
1176
+ return REFUSED;
1177
+ }
1178
+ /**
1179
+ * Avertit (stderr, sans refuser) quand une intégration du dépôt est en cours (L425) : le plan écrit dans l'arbre principal est un fichier suivi modifié, et
1180
+ * l'intégration en cours le voit et refuse son lot (« arbre du dépôt modifié »). Seul l'arbre principal est concerné : dans le worktree d'un lot, l'écriture
1181
+ * ne change pas l'arbre que l'intégration contrôle.
1182
+ */
1183
+ function warnIntegrating(root, err) {
1184
+ const top = gitRoot(root);
1185
+ if (!top || !statSync(join(top, '.git')).isDirectory())
1186
+ return;
1187
+ const holder = activeLock(join(sharedStateDir(root), INTEGRATION_LOCK));
1188
+ if (holder)
1189
+ err(`avertissement : intégration en cours (vague ${holder.wave}, pid ${holder.pid}) — ce plan est un fichier suivi modifié de l'arbre principal, l'intégration en cours peut refuser son lot (arbre du dépôt modifié) : committer le plan sans attendre`);
1153
1190
  }
1154
1191
  /** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec reseal`. */
1155
1192
  function waveHolding(root) {
@@ -1566,7 +1603,7 @@ async function verifyGateCommand(values, io) {
1566
1603
  const sha = resolveCommit(top, values.sha);
1567
1604
  if (!sha)
1568
1605
  throw new RafError(`verify --gate : --sha ${values.sha} : commit introuvable dans ${top}`);
1569
- return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), verify: readVerifyConfig(configPath), launchDirs: [io.cwd, top, dirname(top)], out: io.out });
1606
+ return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), verify: readVerifyConfig(configPath), launchDirs: [io.cwd, top, dirname(top)], now: io.now, out: io.out });
1570
1607
  }
1571
1608
  /** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
1572
1609
  async function sessionAll(rest, values, io) {
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { sharedStateDir } from './state.js';
4
+ const FILE = 'gate-verdicts.json';
5
+ /** Verdicts gardés : les plus récents ; un push ne porte que des shas récents. */
6
+ const KEEP = 200;
7
+ const file = (repo) => join(sharedStateDir(repo), FILE);
8
+ /** Tout le fichier ; absent ou illisible (écriture coupée, édition à la main), il vaut « aucun verdict ». */
9
+ function readAll(repo) {
10
+ const path = file(repo);
11
+ if (!existsSync(path))
12
+ return {};
13
+ try {
14
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
15
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
16
+ return {};
17
+ const all = {};
18
+ for (const [sha, v] of Object.entries(raw)) {
19
+ const { code, at } = v;
20
+ if ((code === 0 || code === 1) && typeof at === 'string')
21
+ all[sha] = { code, at };
22
+ }
23
+ return all;
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ /**
30
+ * Note le verdict de `cadence verify --gate` pour `sha` (L426), dans l'état partagé du dépôt (`.git/cadence/gate-verdicts.json`, commun à tous les worktrees).
31
+ * Le dernier verdict d'un sha remplace le précédent ; au-delà de `KEEP` shas, les plus anciens partent. Écriture atomique (fichier voisin renommé).
32
+ */
33
+ export function recordVerdict(repo, sha, code, at) {
34
+ const all = readAll(repo);
35
+ delete all[sha]; // réinséré en dernier : l'ordre des clés est celui des écritures
36
+ all[sha] = { code, at: at.toISOString() };
37
+ const kept = Object.fromEntries(Object.entries(all).slice(-KEEP));
38
+ const path = file(repo);
39
+ const tmp = `${path}.${process.pid}.tmp`;
40
+ writeFileSync(tmp, `${JSON.stringify(kept, null, 2)}\n`);
41
+ renameSync(tmp, path);
42
+ }
43
+ /** Le dernier verdict noté pour `sha` ; null si la porte n'a jamais rendu de verdict pour ce sha. */
44
+ export function verdictFor(repo, sha) {
45
+ return readAll(repo)[sha] ?? null;
46
+ }
@@ -27,7 +27,7 @@ import { costlyHypotheses, specScreen, specText } from './spec.js';
27
27
  import { fichesDuDossier } from '../spec.js';
28
28
  import { candidates, parsePriority, parseUntil, drawSettingsReport, readPriority, stopReason } from './continue.js';
29
29
  import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, leadReviewedHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
30
- import { canInstallPrePush, gitProcessRunning, indexLockRefusal, installPrePush, longDuration, removePrePush, snapshot } from './guard.js';
30
+ import { canInstallPrePush, gitProcessRunning, indexLockRefusal, installPrePush, keepGateGuard, longDuration, removePrePush, snapshot } from './guard.js';
31
31
  import { buildArgs, buildLocalArgs, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude, enabledPlugins, settingsFiles } from './launch.js';
32
32
  import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
33
33
  import { groupLots, runPool } from './pool.js';
@@ -1101,6 +1101,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
1101
1101
  releaseLock(file, process.pid);
1102
1102
  held.splice(0, held.length, ...held.filter((f) => f !== file));
1103
1103
  removePrePush(r, wave.id);
1104
+ keepGateGuard(r);
1104
1105
  try {
1105
1106
  updateWaveRepos(home, process.pid, repos.filter((x) => !freed.has(x)), lanes());
1106
1107
  }
@@ -1112,9 +1113,12 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
1112
1113
  for (const f of held)
1113
1114
  releaseLock(f, process.pid);
1114
1115
  held.length = 0;
1115
- for (const r of repos)
1116
- if (!freed.has(r))
1117
- removePrePush(r, wave.id);
1116
+ for (const r of repos) {
1117
+ if (freed.has(r))
1118
+ continue;
1119
+ removePrePush(r, wave.id);
1120
+ keepGateGuard(r);
1121
+ }
1118
1122
  try {
1119
1123
  unregisterWave(home, process.pid);
1120
1124
  }
@@ -1159,7 +1163,10 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
1159
1163
  return DROPPED;
1160
1164
  return ctl.stopAfterCurrent ? STOP_REQUESTED : null;
1161
1165
  },
1162
- onWait: (h, ms, limit) => wctx.log(`${o.lot ? `${o.lot} · ` : ''}en attente d'un créneau de session depuis ${duration(ms)} (${h.length}/${limit} en cours : ${[...new Set(h.map((x) => x.wave))].join(', ')})`),
1166
+ onWait: (h, ms, limit, lead) => {
1167
+ const who = [...new Set(h.map((x) => x.wave)), ...lead.map((x) => `session du lead ${x.label}`)];
1168
+ wctx.log(`${o.lot ? `${o.lot} · ` : ''}en attente d'un créneau de session depuis ${duration(ms)} (${h.length + lead.length}/${limit} en cours : ${who.join(', ')})`);
1169
+ },
1163
1170
  onGot: (ms) => wctx.log(`${o.lot ? `${o.lot} · ` : ''}créneau de session obtenu après ${duration(ms)} d'attente`),
1164
1171
  });
1165
1172
  try {
@@ -2,8 +2,18 @@ import { chmodSync, existsSync, lstatSync, readdirSync, statSync, readFileSync,
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 { readOrchestrateConfig } from '../config.js';
5
6
  import { gitCommonDir, gitRoot, hooksDir, indexLockPath } from '../git.js';
6
7
  const MARK = '# cadence orchestrate — hook pre-push temporaire (retiré à la fin de la vague)';
8
+ /** Le hook que la vague laisse derrière elle quand le projet déclare une porte (L426) : il ne garde que le verdict de la porte au push. */
9
+ const GATE_MARK = '# cadence — hook pre-push de la porte (L426) : refuse un push sans verdict vert de `cadence verify --gate`';
10
+ const gateHook = `#!/bin/sh
11
+ ${GATE_MARK}
12
+ if command -v raf >/dev/null 2>&1; then raf hook pre-push "$@"; [ $? -ne 3 ] || exit 1; fi
13
+ exit 0
14
+ `;
15
+ /** Un hook de cadence (de la vague ou de la porte) : le remplacer ne touche à aucune règle du projet. */
16
+ const ours = (text) => text.includes(MARK) || text.includes(GATE_MARK);
7
17
  const waveLine = (wave) => `# wave: ${wave}`;
8
18
  const hook = (wave) => `#!/bin/sh
9
19
  ${MARK}
@@ -204,7 +214,7 @@ export function cleanPlaywrightOutput(repo) {
204
214
  */
205
215
  export function installPrePush(repo, wave = '?') {
206
216
  const file = hookPath(repo);
207
- if (existsSync(file) && !readFileSync(file, 'utf8').includes(MARK)) {
217
+ if (existsSync(file) && !ours(readFileSync(file, 'utf8'))) {
208
218
  return { ok: false, reason: `un hook pre-push existe déjà (${file}) : l'orchestrateur ne le remplace pas` };
209
219
  }
210
220
  mkdirSync(dirname(file), { recursive: true });
@@ -234,10 +244,32 @@ export function removePrePush(repo, wave) {
234
244
  removePlaywrightExclude(repo);
235
245
  restoreExclude(repo);
236
246
  }
247
+ /**
248
+ * Fin de la garde de la vague sur un dépôt (L426) : quand le projet déclare une porte (`orchestrate.test` ou `orchestrate.build`), le hook de la vague
249
+ * est remplacé par un hook qui ne garde que la porte, sinon le push de livraison, qui vient après la fin des lots du dépôt, ne serait plus contrôlé.
250
+ * Rien n'est laissé quand aucune porte n'est déclarée (ou la config illisible), quand un autre hook pre-push est en place (celui du projet, ou celui d'une autre
251
+ * vague vivante : il reste), ni quand le dossier des hooks est dans l'arbre suivi (le fichier y serait un fichier non suivi du dépôt).
252
+ */
253
+ export function keepGateGuard(repo) {
254
+ try {
255
+ const config = readOrchestrateConfig(join(gitRoot(repo) ?? repo, 'cadence.yaml'));
256
+ if (!config.test && !config.build)
257
+ return;
258
+ }
259
+ catch {
260
+ return;
261
+ }
262
+ const file = hookPath(repo);
263
+ if (existsSync(file) || excludeLine(repo) !== null)
264
+ return;
265
+ mkdirSync(dirname(file), { recursive: true });
266
+ writeFileSync(file, gateHook);
267
+ chmodSync(file, 0o755);
268
+ }
237
269
  /** Pourrait-on poser le hook ? (préconditions, sans écrire) */
238
270
  export function canInstallPrePush(repo) {
239
271
  const file = hookPath(repo);
240
- return existsSync(file) && !readFileSync(file, 'utf8').includes(MARK) ? `un hook pre-push existe déjà (${file}) : l'orchestrateur ne le remplace pas` : null;
272
+ return existsSync(file) && !ours(readFileSync(file, 'utf8')) ? `un hook pre-push existe déjà (${file}) : l'orchestrateur ne le remplace pas` : null;
241
273
  }
242
274
  /** git sans bloquer la boucle d'événements (un `ls-remote` peut durer 20 s) ; null en cas d'échec ou de délai. */
243
275
  function out(repo, args, timeout = 20_000) {
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { gitCommonDir } from '../git.js';
5
5
  import { sharedStateDir } from '../state.js';
6
- import { activeLock, releaseLock, takeLock } from './lock.js';
6
+ import { releaseLock, takeLock } from './lock.js';
7
7
  import { mergeUnreleased, moveToUnreleased, settleLotEntries, unreleasedSections } from './changelog-merge.js';
8
8
  import { mutexFor } from './mutex.js';
9
9
  import { Plan } from '../plan.js';
@@ -19,20 +19,17 @@ export const MAX_REPLAYS = 2;
19
19
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
20
  /**
21
21
  * Prend la main sur le dépôt pour une intégration ou une écriture de plan du programme (R10, R21, R26) : d'abord la file en mémoire du dépôt (les lots d'une
22
- * vague partagent un pid, `takeLock` rendrait chacun déjà porteur), puis le verrou fichier, qui ne départage que les autres processus. L'attente est bornée
23
- * par `waitMs` (R25) ; au-delà, le porteur est rendu et rien n'est pris. Une attente ne retient aucun créneau de session : ce verrou n'a rien à voir avec eux.
22
+ * vague partagent un pid, `takeLock` rendrait chacun déjà porteur), puis le verrou fichier, qui ne départage que les autres processus. La file en mémoire
23
+ * s'attend sans borne (L425) : son porteur est un lot de ce processus, qui libère toujours, et une intégration (rebase rejoué + porte) dure plus que la borne.
24
+ * L'attente du verrou fichier d'un autre processus est bornée par `waitMs` (R25), comptée à partir du tour ; au-delà, le porteur est rendu et rien n'est pris.
25
+ * Une attente ne retient aucun créneau de session : ce verrou n'a rien à voir avec eux.
24
26
  */
25
27
  export async function takeRepoWrite(repo, who, o = {}) {
26
28
  const waitMs = o.waitMs ?? INTEGRATE_WAIT_MS;
27
29
  const pollMs = o.pollMs ?? INTEGRATE_POLL_MS;
28
30
  const file = join(sharedStateDir(repo), INTEGRATION_LOCK);
31
+ const turn = (await mutexFor(`integrate:${gitCommonDir(repo)}`).acquire());
29
32
  const deadline = Date.now() + waitMs;
30
- const turn = await mutexFor(`integrate:${gitCommonDir(repo)}`).acquire(waitMs);
31
- if (!turn) {
32
- // Un autre lot de ce processus tient la main : c'est lui le porteur (il a posé le verrou fichier).
33
- const holder = activeLock(file);
34
- return { ok: false, holder: { wave: holder?.wave ?? who.wave, pid: holder?.pid ?? process.pid } };
35
- }
36
33
  for (;;) {
37
34
  const lock = { pid: process.pid, wave: who.wave, started: new Date().toISOString() };
38
35
  const got = takeLock(file, lock);
@@ -192,7 +192,7 @@ export class SlotAbandoned extends Error {
192
192
  }
193
193
  /**
194
194
  * Attend que le nombre de sessions vivantes, TOUTES vagues confondues et quel que soit l'index de leur créneau,
195
- * soit sous le plafond commun — `cap` (le plafond de cette vague) ou, s'il est plus bas, celui d'une autre vague vivante
195
+ * — plus les sessions du lead inscrites par `lead hold` (L424), qu'aucune vague ne porte — soit sous le plafond commun — `cap` (le plafond de cette vague) ou, s'il est plus bas, celui d'une autre vague vivante
196
196
  * (`commonCap`, relu à chaque tour, L395) —, puis prend le premier créneau libre (un pid mort est repris) et
197
197
  * rend sa libération. Toutes les vagues appliquent le même plafond : jamais « 4/3 en cours ».
198
198
  * Deux vagues qui prennent en même temps se départagent au recomptage : si le total dépasse `cap` une fois
@@ -221,13 +221,15 @@ export async function acquireSlot(home, cap, opts) {
221
221
  const limit = commonCap(liveWaves(home), cap); // relu à chaque tour : une vague au plafond bas qui s'arrête le relève
222
222
  if (reportedAt !== null)
223
223
  checkAbandon(); // pas à la première tentative : un créneau libre est pris même si une demande est posée
224
- if (live(home).length < limit) {
224
+ // Les sessions du lead (`lead hold`) tournent hors de toute vague mais prennent un créneau : relues à chaque comptage (L424).
225
+ const taken = () => live(home).length + liveHolds(home, new Date()).length;
226
+ if (taken() < limit) {
225
227
  for (let k = 0; k < MAX_SLOT_INDEX; k++) {
226
228
  const file = join(slotsDir(home), `slot-${k}.lock`);
227
229
  const got = takeLock(file, { pid: process.pid, wave: opts.wave, started: new Date().toISOString() });
228
230
  if (!got.ok)
229
231
  continue;
230
- if (live(home).length <= limit) {
232
+ if (taken() <= limit) {
231
233
  if (reportedAt !== null)
232
234
  opts.onGot?.(now() - t0);
233
235
  return () => releaseLock(file, process.pid);
@@ -240,7 +242,7 @@ export async function acquireSlot(home, cap, opts) {
240
242
  checkAbandon(); // la première tentative a échoué : l'attente commence, la demande déjà posée l'interrompt sans attendre un tour
241
243
  if (reportedAt === null || now() - reportedAt >= (opts.reportMs ?? WAIT_REPORT_MS)) {
242
244
  reportedAt = now();
243
- opts.onWait?.(live(home), reportedAt - t0, limit);
245
+ opts.onWait?.(live(home), reportedAt - t0, limit, liveHolds(home, new Date()));
244
246
  }
245
247
  await new Promise((r) => setTimeout(r, pollMs + Math.random() * pollMs * 0.25));
246
248
  }
package/dist/plan.js CHANGED
@@ -6,6 +6,17 @@ import { isDay, toDay } from './dates.js';
6
6
  import { ARCHIVE_DIR, archiveFiles, archiveLots, archiveYear, mergeArchived, newArchiveDoc, readArchiveDoc, selectArchivable } from './archive.js';
7
7
  /** Délai de la commande `plan.onWrite` : une republication du plan, pas un build. */
8
8
  const ON_WRITE_TIMEOUT_MS = 120_000;
9
+ /**
10
+ * La raison d'un échec de `plan.onWrite` en une ligne (L429) : la première ligne « Error: … » de la sortie d'erreur (un script Node qui jette
11
+ * imprime d'abord le fichier, le code fautif et un caret, puis le motif, puis la pile : garder la fin de la pile cachait le motif) ; sans
12
+ * ligne de ce genre, la première ligne non vide de la sortie d'erreur, puis de la sortie standard. Les lignes de pile (« at … ») et la
13
+ * bannière « Node.js v… » ne sont jamais la raison.
14
+ */
15
+ function failureReason(stderr, stdout) {
16
+ const lines = (text) => (text ?? '').split('\n').map((l) => l.trim()).filter((l) => l && !/^at\s/.test(l) && !/^Node\.js v\d/.test(l));
17
+ const err = lines(stderr);
18
+ return err.find((l) => /^(?:\w+)?Error\b/.test(l)) ?? err[0] ?? lines(stdout)[0] ?? '';
19
+ }
9
20
  /** Où va un avertissement de `save()` (la commande a réussi mais quelque chose cloche) : stderr, ou le `err` du CLI qui a lancé l'écriture. */
10
21
  let notice = (line) => void process.stderr.write(`${line}\n`);
11
22
  export function setPlanNotice(fn) {
@@ -192,7 +203,7 @@ export class Plan {
192
203
  }
193
204
  restore();
194
205
  const why = r.error ? r.error.code === 'ETIMEDOUT' ? `délai de ${limit / 1000} s dépassé` : r.error.message : `code ${r.status ?? r.signal}`;
195
- const tail = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().split('\n').slice(-5).join(' | ');
206
+ const tail = failureReason(r.stderr, r.stdout);
196
207
  throw new RafError(`plan.onWrite en échec (${hook.command}, ${why})${tail ? ` : ${tail}` : ''} — écriture refusée, plan inchangé`);
197
208
  }
198
209
  get project() {
@@ -1,3 +1,4 @@
1
+ import { verdictFor } from './gate-verdict.js';
1
2
  import { tryGit } from './git.js';
2
3
  import { blockingLots, LABEL } from './orchestrate/table.js';
3
4
  const ZERO = /^0+$/;
@@ -40,3 +41,29 @@ export function pushRefusal(root, wave, lots, input, remoteName = 'origin') {
40
41
  }
41
42
  return null;
42
43
  }
44
+ /** La variable qui passe outre le verdict de la porte au push (L426) ; la garde des lots de la vague (`pushRefusal`) n'en dépend pas. */
45
+ export const UNGATED_ENV = 'CADENCE_PUSH_UNGATED';
46
+ /**
47
+ * Le refus d'un push dont le sha de tête n'a pas de verdict vert de `cadence verify --gate` (L426) : pour chaque ligne de l'entrée standard du hook pre-push,
48
+ * le sha local d'une branche (`refs/heads/`) doit avoir son dernier verdict à 0 dans l'état partagé du dépôt. Absent ou rouge, le refus nomme le sha, dit lequel des deux
49
+ * et donne la commande qui rend le verdict (`cadence verify --gate --sha <sha>`) ; il rappelle la variable qui passe outre. Une suppression de branche, un sha illisible
50
+ * et une réf hors `refs/heads/` (un tag : son sha est celui de l'objet tag, pas d'un commit à vérifier) passent. Sans porte déclarée (`gate` faux) rien n'est exigé.
51
+ */
52
+ export function gateRefusal(root, gate, input) {
53
+ if (!gate)
54
+ return null;
55
+ for (const entry of input.split('\n')) {
56
+ const [ref, local, remoteRef] = entry.trim().split(/\s+/);
57
+ if (!ref || !local || ZERO.test(local) || !/^[0-9a-f]{7,64}$/.test(local))
58
+ continue;
59
+ if (!(remoteRef || ref).startsWith('refs/heads/'))
60
+ continue;
61
+ const verdict = verdictFor(root, local);
62
+ if (verdict?.code === 0)
63
+ continue;
64
+ const short = local.slice(0, 7);
65
+ const state = verdict ? `sa porte est rouge (verdict du ${verdict.at.slice(0, 10)})` : 'aucun verdict de la porte pour ce sha';
66
+ return `push refusé — ${short} : ${state} ; la rejouer : cadence verify --gate --sha ${short} (vert, le push passe) ; ${UNGATED_ENV}=1 git push … pour passer outre`;
67
+ }
68
+ return null;
69
+ }
@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
3
  import { availableParallelism, tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { recordedGate } from './gate-record.js';
6
+ import { recordVerdict } from './gate-verdict.js';
6
7
  import { onTermination } from './proc.js';
7
8
  import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
8
9
  import { configuredCap } from './orchestrate/command.js';
@@ -92,9 +93,24 @@ function mainTreeMoved(repo, sha) {
92
93
  * sans elle le rejeu `--maxWorkers=$CADENCE_TEST_WORKERS` de `orchestrate.testReplay` sortirait en erreur et ne prouverait rien.
93
94
  * La porte déjà jouée par le programme au même sha n'est pas rejouée (L397) : si l'état d'une vague (`ctx.launchDirs`) porte des contrôles enregistrés AU sha, sur un arbre identique, verts et couvrant chaque commande actuelle,
94
95
  * leur verdict est repris et la première ligne le dit (vague, lot) ; sinon la porte est rejouée et la ligne qui suit celle du lieu en donne la raison.
96
+ * Le verdict (0 ou 1, pas 2) est noté par sha dans l'état partagé du dépôt (`gate-verdict.ts`, L426) : le hook pre-push refuse un push dont le sha n'a pas de verdict vert.
95
97
  * Le worktree est retiré dans tous les cas. 0 vert, 1 rouge (avec les tests en échec), 2 rien à rejouer ou worktree impossible.
96
98
  */
97
99
  export async function verifyGate(ctx) {
100
+ const code = await playGate(ctx);
101
+ // Seuls 0 (vert) et 1 (rouge) sont des verdicts du sha ; 2 (rien à rejouer, verdict non probant) n'en est pas un. Le hook pre-push les lit (L426).
102
+ if (code === 0 || code === 1) {
103
+ try {
104
+ recordVerdict(ctx.repo, ctx.sha, code, (ctx.now ?? (() => new Date()))());
105
+ }
106
+ catch (e) {
107
+ // Un verdict qu'on ne peut pas noter ne change jamais le code de la porte : le push sera refusé faute de verdict, et la sortie dit pourquoi.
108
+ ctx.out(`verify --gate ${ctx.sha.slice(0, 7)} : verdict non noté (${e.message}) — le hook pre-push refusera ce sha tant qu'il n'a pas de verdict`);
109
+ }
110
+ }
111
+ return code;
112
+ }
113
+ async function playGate(ctx) {
98
114
  const { repo, sha, config, out } = ctx;
99
115
  const steps = [
100
116
  ...(config.build ? [{ name: 'build', cmd: config.build }] : []),
@@ -22,3 +22,4 @@ evenly over the sorted keys, with their findings and the model's cause, to be re
22
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
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
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. The measure of the back-end spec step against the non-visible lots (L190: 10 lots with a back-end sheet, passes by cause, token cost beside the passes avoided) is in [docs/mesures/etape-spec-back-2026-10-16.md](mesures/etape-spec-back-2026-10-16.md).
25
+ The reading of the « bug de code » passes of the 23–29-10 window, the block added to the `implement` brief against them (L428) and the protocol to measure its effect on two `cadence perf` windows are in [docs/mesures/bug-code-2026-10-29.md](mesures/bug-code-2026-10-29.md).
package/docs/deliver.md CHANGED
@@ -336,3 +336,12 @@ Each command has 30 minutes (then its whole process group is killed, code 124).
336
336
  `--sha` is required (the moving head of a wave is not a sha to re-verify) and `--retry` does not apply. Exit code:
337
337
  **0** green · **1** red · **2** nothing to replay (neither `orchestrate.build` nor `orchestrate.test`), unknown sha, a worktree that cannot be made, or `verify.inPlace: true` with a main tree not clean at the sha.
338
338
  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.
339
+
340
+ **The verdict is kept per sha, and a push is checked against it (L426).** Each `cadence verify --gate` that ends **green (0) or red (1)** writes its verdict under the sha in `.git/cadence/gate-verdicts.json` (shared by every worktree of the repository; the last verdict of a sha replaces the previous one; the 200 newest shas are kept; a verdict read from the wave's state counts, it is a green gate of that sha). Exit 2 (nothing to replay, `verdict non probant`) writes nothing: it is not a verdict. The `pre-push` hook (`raf hook pre-push`, the one a wave installs, `docs/orchestrate.md`) then refuses, with exit 3, a push of a branch (`refs/heads/…`) **whose local sha has no green verdict**, when the project declares a gate (`orchestrate.test` or `orchestrate.build`; none declared, nothing is asked):
341
+
342
+ ```
343
+ raf: push refusé — 6b0d9aa : aucun verdict de la porte pour ce sha ; la rejouer : cadence verify --gate --sha 6b0d9aa (vert, le push passe) ; CADENCE_PUSH_UNGATED=1 git push … pour passer outre
344
+ raf: push refusé — 6b0d9aa : sa porte est rouge (verdict du 2026-10-29) ; la rejouer : …
345
+ ```
346
+
347
+ This is the exit code of the gate itself, not a pipe's: on 29-10 a project was pushed with a red gate because its exit code had been hidden by `| tail -1` (the same loss as 28-10). `CADENCE_PUSH_UNGATED=1` in the environment of the `git push` lets the push through and says so on stderr (`push passé outre`); `--no-verify` still skips the hook as for any hook. A deleted branch, a tag and an unreadable verdict file (read as « no verdict ») are handled as follows: deletions and tags are not checked, a damaged file refuses the push until the gate is replayed (which rewrites it). A `cadence.yaml` that cannot be read lets the push through, like an unreadable wave state. The check applies where the hook is installed: the hook a wave installs, and, once the wave frees the repository (all its lots finished, so before the delivery push), a gate-only `pre-push` hook it leaves behind when the project declares a gate (`orchestrate.test` or `orchestrate.build`). That hook is not left when another `pre-push` hook is in place (the project's own, or that of another live wave), nor when `core.hooksPath` points inside the tracked tree; the next wave replaces it, and a repository that never saw a wave has none.
package/docs/lead.md CHANGED
@@ -65,7 +65,10 @@ picture, not its source.
65
65
  UX reviewer's captures: the lead records `raf ux` on the human's verdict, never on its own — unless the parent folder's `cadence.yaml` says `lead.ux: lead`: then the lead forms the verdict from the reviewer's report and its own look at the captures, and asks the human only at the slightest doubt. Then
66
66
  `raf done`.
67
67
  6. **Delivery.** Push (for a « livrable jusqu'à <sha> », that sha alone, never HEAD), then
68
- `cadence deliver`. A lot that turns ready is delivered while the wave runs (`cadence deliver --sha <sha>`, accepted when no commit of the sha belongs to a lot that is not ready), not at the end of the wave. After a green delivery that changes what a page shows or what it is served, the
68
+ `cadence deliver`. Where the project declares a gate, the `pre-push` hook refuses a sha with no green verdict of
69
+ `cadence verify --gate` (L426): the lot's sha has the verdict of the re-check, the plan commit above it (the head of `main`
70
+ after `raf done` and its commit) has none and gets a full replay with `cadence verify --gate --sha HEAD` before the push;
71
+ `CADENCE_PUSH_UNGATED=1` is the human's explicit override, never the routine. A lot that turns ready is delivered while the wave runs (`cadence deliver --sha <sha>`, accepted when no commit of the sha belongs to a lot that is not ready), not at the end of the wave. After a green delivery that changes what a page shows or what it is served, the
69
72
  `qa-reviewer` agent walks the delivered pages in a real browser, reads only, and brings its blocking
70
73
  findings to the human. It is not a gate: a finding becomes a new lot.
71
74
  7. **Threshold and chaining.** Before every new wave the lead reads `cadence session context`. Under
@@ -114,9 +117,9 @@ page.
114
117
 
115
118
  ## lead hold and release
116
119
 
117
- `cadence lead hold <label> [--ttl minutes]` and `cadence lead release <label>` (L255) declare the sessions the lead starts by hand — a fix subagent, a review, a QA pass launched outside an orchestrated wave. No wave carries such a session, so the slot registry could not see it: on 2026-10-23 the lead ran 4 sessions under a cap of 3 while `créneaux libres` said one slot was free. `hold` registers the session under its label for `--ttl` minutes (an integer ≥ 1, 60 by default; holding the same label again renews it instead of counting twice); `release` hands it back (releasing a label that is not held answers `aucune session « <label> » inscrite` and exits 0). After the release (L416), `release` prints what it leaves: the slots line, the `prêts, ressources libres` line when a ready lot is free, and `⚠ N créneau(x) libre(s) sur M : lancer un lot prêt ou une session, un créneau vide ne sert à rien` while at least one slot is free (on 2026-10-29 a cap of 3 was taken for 2 and a slot stayed empty for more than an hour); `hold` prints neither. A hold whose time has run out stops counting, but is not forgotten: nothing stops the session (L278), so `cadence lead tour` and `cadence orchestrate --status` print `⚠ session du lead échue : <label> (inscrite depuis 5 h 10 min, échue depuis 2 h 10 min) — la reprendre (cadence lead hold <label> --ttl <minutes>) ou l'arrêter (cadence lead release <label>)` after the slots line, and the `cadence-hud` band shows the same ⚠ row, until the lead renews the hold or releases it (an expired hold nobody touched is dropped 24 h after its expiry). The `--ttl` is therefore the maximum duration declared for the session; the `lead` skill says to renew or stop it at each ⚠. The line is not part of `--json`.
120
+ `cadence lead hold <label> [--ttl minutes]` and `cadence lead release <label>` (L255) declare the sessions the lead starts by hand — a fix subagent, a review, a QA pass launched outside an orchestrated wave. No wave carries such a session, so the slot registry could not see it: on 2026-10-23 the lead ran 4 sessions under a cap of 3 while `créneaux libres` said one slot was free. Orchestrated waves read the holds too (L424): a wave takes no session slot while the held sessions and the running ones already reach the cap, so a hold can no longer be overtaken by a wave starting a session on the « free » slot. `hold` registers the session under its label for `--ttl` minutes (an integer ≥ 1, 60 by default; holding the same label again renews it instead of counting twice); `release` hands it back (releasing a label that is not held answers `aucune session « <label> » inscrite` and exits 0). After the release (L416), `release` prints what it leaves: the slots line, the `prêts, ressources libres` line when a ready lot is free, and `⚠ N créneau(x) libre(s) sur M : lancer un lot prêt ou une session, un créneau vide ne sert à rien` while at least one slot is free (on 2026-10-29 a cap of 3 was taken for 2 and a slot stayed empty for more than an hour); `hold` prints neither. A hold whose time has run out stops counting, but is not forgotten: nothing stops the session (L278), so `cadence lead tour` and `cadence orchestrate --status` print `⚠ session du lead échue : <label> (inscrite depuis 5 h 10 min, échue depuis 2 h 10 min) — la reprendre (cadence lead hold <label> --ttl <minutes>) ou l'arrêter (cadence lead release <label>)` after the slots line, and the `cadence-hud` band shows the same ⚠ row, until the lead renews the hold or releases it (an expired hold nobody touched is dropped 24 h after its expiry). The `--ttl` is therefore the maximum duration declared for the session; the `lead` skill says to renew or stop it at each ⚠. The line is not part of `--json`.
118
121
 
119
- The held sessions are kept in the `holds/` folder of the cadence home (`CADENCE_HOME`, shared by every folder and every wave). While at least one is held, the line of the live waves of `cadence orchestrate --status` and of `cadence lead tour` gains a segment and subtracts them: `vagues en cours : 1 · sessions en cours : 0 · sessions du lead : 1 (correctif-L12) · créneaux libres : 0 sur 3`. A held session alone is enough to print the line (`vagues en cours : 0`). It is a display, like the rest of the slots line: a hold blocks no session and never raises the cap (a wave that is already running keeps its own slot counting), and the `lead` skill declares every hand-started session with it.
122
+ The held sessions are kept in the `holds/` folder of the cadence home (`CADENCE_HOME`, shared by every folder and every wave). While at least one is held, the line of the live waves of `cadence orchestrate --status` and of `cadence lead tour` gains a segment and subtracts them: `vagues en cours : 1 · sessions en cours : 0 · sessions du lead : 1 (correctif-L12) · créneaux libres : 0 sur 3`. A held session alone is enough to print the line (`vagues en cours : 0`). It is a display, like the rest of the slots line: a hold blocks no session of the lead, but orchestrated waves wait for it (L424: they take no slot while the held and running sessions reach the cap), and it never raises the cap (a wave that is already running keeps its own slot counting), and the `lead` skill declares every hand-started session with it.
120
123
 
121
124
  ## The librarian by hand
122
125
 
@@ -394,9 +394,10 @@ When the title, the goal or a note of a lot quotes ids of a reference inventory
394
394
 
395
395
  Once the sheet exists, the steps after it all read it (L174, `src/orchestrate/aval.ts`, the `{{fiche}}` marker of the templates `implement.md`, `fix.md`, `fix-minors.md`, `review.md`, `review-small.md` and `ux.md`). A lot without a sheet (no `screen:`, or under the threshold) keeps exactly the briefs it had.
396
396
 
397
- - **`implement` and `fix`** get the path of the sheet and of the mockup, and **one line per rule** (`- R3 [erreur, hypothese] statement. Message: « … ». Tests: « R3 — … »`). The session makes the acceptance tests green **without modifying them** (they are named when the `accept` step ran; the lock above hands the lot back otherwise), **the tests it writes in addition cite the rule** in their name (`R2 — …`), and it **does not invent a rule**: a case the sheet does not cover is written in the report (`nonVerifie` or `choix`), not coded. In a `fix` the sheet stays the reference: a finding that contradicts it is contested under `choix`. Both briefs also say that a conflict with the sealed test of **another** lot is a question to the lead naming both tests (file and `Rn — …` name), never a modification or a choice (L231). The `accept` and `accept-back` briefs and the `acceptance-writer` agent require each assertion to cite, in a comment, the rule and the sentence of its statement that it verifies, to assert nothing beyond that sentence, and to go through a self-check before the commit (an assertion with no sentence behind it is removed or listed under `choix`); the `review` and `review-small` briefs and the `code-reviewer` agent re-read each sealed assertion against the sentence it cites, and one that asserts more than the sheet is a *major* finding (« sealed test stronger than the sheet », worded « than its rule » in the briefs), corrected by the lead with `cadence spec reseal` (L231).
397
+ - **`implement` and `fix`** get the path of the sheet and of the mockup, and **one line per rule** (`- R3 [erreur, hypothese] statement. Message: « … ». Tests: « R3 — … »`). The session makes the acceptance tests green **without modifying them** (they are named when the `accept` step ran; the lock above hands the lot back otherwise), **the tests it writes in addition cite the rule** in their name (`R2 — …`), and it **does not invent a rule**: a case the sheet does not cover is written in the report (`nonVerifie` or `choix`), not coded nor tested (the « edge cases » point of the L428 block below says it explicitly). In a `fix` the sheet stays the reference: a finding that contradicts it is contested under `choix`. Both briefs also say that a conflict with the sealed test of **another** lot is a question to the lead naming both tests (file and `Rn — …` name), never a modification or a choice (L231). The `accept` and `accept-back` briefs and the `acceptance-writer` agent require each assertion to cite, in a comment, the rule and the sentence of its statement that it verifies, to assert nothing beyond that sentence, and to go through a self-check before the commit (an assertion with no sentence behind it is removed or listed under `choix`); the `review` and `review-small` briefs and the `code-reviewer` agent re-read each sealed assertion against the sentence it cites, and one that asserts more than the sheet is a *major* finding (« sealed test stronger than the sheet », worded « than its rule » in the briefs), corrected by the lead with `cadence spec reseal` (L231).
398
398
  - **A self-check list before handing back (L222).** Both `implement.md` and `fix.md` carry the same fixed list (plain text, no marker, no new variable), taken from the recurring findings of the reviews of 2026-10-15: the usage documentation (README and architecture documentation) updated in the same commits as the behaviour; one test named `Rn — …` per rule of the sheet (one per business rule of the lot when there is no sheet); no dead code and no test that cannot fail (an assertion always true, or a test that does not exercise its rule); for a modified screen, the capture due of each state at 1440 and 390 px with a file name (where it goes is the Playwright paragraph's rule) (a lot with no modified screen owes none and says so); no false comment, docstring or message. The session goes through it point by point on its own commits before it reports, and a point it cannot hold is written under `choix` or `nonVerifie` of its report: the report schema has no new field. In `fix` the list covers only what the session changed, and "fix them, and nothing else" stays. The other briefs (`fix-minors`, `spec`, `spec-back`, `accept`, `review-small`…) are unchanged. The before/after measure — correction passes per lot, replayed with `cadence corrections` (`--until <bound>` for the "before", `--since <bound>` for the "after", the bound being the time of the `npm run service` that puts the list in service) — is in [docs/mesures/autocontrole-avant-revue-2026-10-15.md](mesures/autocontrole-avant-revue-2026-10-15.md).
399
399
  - **Auto-verification once the tests are green (L236).** After the self-check list, `implement.md` and `fix.md` carry a second fixed block: when the full suite and the build pass, the session re-reads the complete diff of its lot (of its fix commits, in `fix`) against the grid of the `code-reviewer` agent — code bug, architecture drift, `docs/architecture.md` (or the project's architecture documentation) up to date, edge cases, one test per changed behaviour — fixes what it finds at once (test first, a `fix(<lot>): …` commit, suite and build replayed), then starts `resume` with an « auto-vérification » paragraph: what it re-read, found and fixed, could not hold. No new report field. The cost bound is written in the brief: one reading of the diff, not a second review (no subagent, no files the diff does not touch). Measure before the block (`cadence corrections --classify`, 10 to 17-10): 356 passes, 53 % code bug, 19 % architecture drift, 14 % unclear spec; `cadence perf` of 17-10: 37 % right the first time. Target: above 50 % on the next window with tokens per lot stable (`cadence perf`).
400
+ - **Limits and rules before the walkthrough (L428).** `implement.md` (not `fix.md`) carries a third fixed block between the self-check list and the real walkthrough, aimed at the first cause of correction passes: a code bug the review finds in the diff (100 of the 143 classified passes of the 23–29-10 window, `cadence perf`). The session re-reads its diff against the fiche and the lot and, **per rule** (`Rn`), names the test that fails if the code breaks it and the line that holds it; **per function, parser, pattern, threshold or command it adds or changes**, lists the inputs at the edge (empty, absent, twice, wrong order, wrong type, refused, the real shape of the data, the other callers) and gives each a test that fails without its code, except a case the fiche does not cover: with a fiche, the session does not code a rule the fiche does not state, so that case goes under `"nonVerifie"` or `"choix"`, neither coded nor tested (a lot without a fiche has no such exception: every listed case is due a test, and one left untested goes under `"nonVerifie"`); **per figure, « all », « never » or « always »** written in README, docs, comments or tests, checks it against the code or by running it. A listed case left untested goes under `"nonVerifie"`. The reading of the 23–29-10 passes that led to it, and the way to measure its effect on two windows, are in [mesures/bug-code-2026-10-29.md](mesures/bug-code-2026-10-29.md). Test: `test/limites-et-regles-l428.test.ts`.
400
401
  - **Real walkthrough before the auto-verification (L344).** Reading a diff did not show three major review findings of 26-10 (a command impossible in its own documented path, a « champs exclus » read against a decision of 22-10, a failure banner out of view). `implement.md` therefore asks, once the tests are green, to play what was delivered as a user would: a command in a throwaway repository (`git init` in a fresh temporary folder), following the documentation the lot wrote, command by command; a screen in each of its states, failure included and shown where the action takes place, with a capture of each. What breaks is fixed in the same session (test first) and played again; the report says it in a paragraph of `"resume"` that starts with « parcours réel » (no new field; a lot with nothing to play says so). `fix.md` does not carry it: a correction pass fixes the findings it is given.
401
402
  - **Decisions to respect (L344).** The program (not the pre-check session, which many lots skip) reads the notes of the lot and of its parent lot (`parent:`) and keeps those that name who decided (Sylvain, the human, the lead, the user) and say that they decided, validated, asked or refused (`src/orchestrate/decisions.ts`). The notes the program writes itself are never taken for one, whatever they quote: a hypothesis of a fiche (« Hypothèses (fiche …) — », a choice nobody validated), a minor of a review (« Revue de … — mineur : »), a replaced title (« titre remplacé : », the stale one), « remis en attente » and « abandonné : ». Each is given with its date and its text as written (cut at 1500 characters) under « Decisions to respect » in the `implement` and `fix` briefs (and in the `--dry-run` briefs), with the rule: an interpretation that departs from one of them is a question under `"questions"`, never a choice under `"choix"`. Nothing is written when no note qualifies. The effect is measured by the share right the first time of `cadence perf` over two windows.
402
403
  - **The code review (`review`, `review-small`) checks rule → test → green** (`règle → test → vert`). For each `Rn` of the sheet the reviewer checks that its test exists (named after the rule), that it is green, and that the behaviour is in the diff; when an acceptance file is named, that each listed test is unchanged since the red commit (`git diff <red commit> -- <file>` empty; the brief gives the red commit). A rule without a test, an acceptance test modified since the red commit, a rule coded that is not in the sheet, and a test that is green without exercising its rule are **major** findings. The reviewer no longer rebuilds the cases from the code.
@@ -581,7 +582,7 @@ nothing below applies and the wave behaves as before; any other value is refused
581
582
  - **Integration, before `ready`.** At the end of its cycle (compliant review, UX included) the lot is integrated into the repository's branch, and only
582
583
  then does it become `ready`; the repository's branch gets the lots one by one, on a linear history. Under the repository's **integration lock**
583
584
  (a file next to the wave lock, same format, pid and start time, stale when the holder is dead, plus an in-memory queue because the lots of a wave share
584
- a pid; the wait is bounded to 10 minutes and holds no session slot):
585
+ a pid; a lot queues behind the other lots of its wave without limit (L425), the wait for another process's lock is bounded to 10 minutes, and no wait holds a session slot):
585
586
  1. the repository must still be on its start branch (`le dépôt est sur <branch> et non sur <start branch>`) and have no modified tracked file
586
587
  (`arbre du dépôt modifié (<files>)`): the program never changes the lead's branch or tree. The check is made again right before the
587
588
  fast-forward (the replayed checks and the short review can last) and right before the plan is written: a lead moved to another branch, or whose
@@ -635,7 +636,7 @@ nothing below applies and the wave behaves as before; any other value is refused
635
636
  6. the worktree and the branch, clean and merged, are removed; the lot is `ready`.
636
637
  A stop between the advance and the plan write is resumed at the `integrate` step: the branch being already contained in the repository's, the rebase
637
638
  and the advance are skipped and the verdict is written once.
638
- - **A lot handed back at the integration step** (lock held for more than 10 minutes, repository's branch changed, tracked file modified, rebase conflict,
639
+ - **A lot handed back at the integration step** (lock held by another process for more than 10 minutes, repository's branch changed, tracked file modified, rebase conflict,
639
640
  red tests, blocking finding of the short review, plan write refused) keeps `next: integrate`, its worktree and its branch. **Only
640
641
  `--resume --wave <wave> --integrate <project:lot>` replays it** (repeatable): a plain `--resume`, `--drop`, `--answer` or `--budget` never does. The lot
641
642
  restarts at `suspended`, step `integrate` (`<project>:<lot> → suspended — intégration reprise (--resume --integrate)`), and the rebase and the checks run
@@ -756,7 +757,7 @@ on elsewhere, so `cadence deliver` accepts it (a repository with a lot still to
756
757
  held until the wave ends). `cadence deliver` also reads the wave's own state when a lock is still held: it refuses only if the wave still has a lot running, queued or suspended **in this repository**, and goes ahead when the lots left are elsewhere (an unreadable wave state still refuses). They share a **cap on simultaneous sessions**, counted
757
758
  across all live waves: 2 by default, `--max-sessions N` (or `CADENCE_MAX_SESSIONS=N`) to change it — give
758
759
  the **lowest cap of the live waves applies to all of them** (L395): each wave counts ALL live sessions, whatever their slot, and waits while that
759
- count has reached the lowest cap declared by any live wave (re-read at every turn: it rises again when the wave with the low cap ends), so a wave at 4 next to a wave at 3 plays 3 sessions, never « 4/3 en cours » (on 28-10 L51 waited 16 min 28 s behind it). The `en attente d'un créneau` line and the free slots (`créneaux libres`) use that common cap. A session that finds no free slot **waits** (the wave is not refused; the journal says
760
+ count has reached the lowest cap declared by any live wave (re-read at every turn: it rises again when the wave with the low cap ends), so a wave at 4 next to a wave at 3 plays 3 sessions, never « 4/3 en cours » (on 28-10 L51 waited 16 min 28 s behind it). The `en attente d'un créneau` line and the free slots (`créneaux libres`) use that common cap. The sessions the lead started by hand and declared with `cadence lead hold` (L424, [lead.md](lead.md#lead-hold-and-release)) count in that total like wave sessions, until their `release` or their time limit: the wave waits for a slot instead of starting a session over the cap, and the line names them (`2/3 en cours : w1, session du lead correctif-L12`). A session that finds no free slot **waits** (the wave is not refused; the journal says
760
761
  `en attente d'un créneau de session depuis …`, repeated every minute, then `créneau de session obtenu après …`). A free slot is always taken, even when a request is already written (relaunching a cut review is not a wait; the request applies afterwards). Once the wait has really started (a first try found no free slot), it reads the wave's requests at every turn (L332): a `--drop` aimed at that lot ends it (the slot is never taken, no session starts, the lot is *handed back* `retiré de la vague (--drop)` like a lot not started) and a `--stop-after-current` ends it too (the lot is *suspended* `arrêt demandé (--stop-after-current)`, the wave **interrupted** and resumable); a `--drop` aimed at another lot does not. The step announced but never started is removed from the state and the lead's answer it was carrying (`pendingAnswer`, taken out of the lot when the brief was built) is put back: `--resume` replays the step with the answer in its brief, and a lot handed back by `--drop` says « la réponse du lead (« … ») n'a pas été jouée » in its outcome. When it is the relaunch of a session already run in the step (a cut review relaunched in a fresh session, L256; the formatting retry) that was waiting, the step ends `failed` with its cause (« relance en session neuve non faite : … ») and keeps what the first session left: its id, its tokens (step and wave budget), its stderr in `<n>-<kind>.first.err` (and its output in `.first.json` for the formatting retry).
761
762
  `cadence orchestrate --waits [--since YYYY-MM-DD]` adds those waits up from the journals of `.cadence/runs/` of the launch folder — per day (cumulated wait, number of waits, waves, quota stops — one per "→ suspended — quota atteint" line of the journals, counted on the day of that line, so a wave resumed and stopped again on another day counts once on each day, not once as a wave, tokens) and per wave that waited (the longest wait, a wait that never got its slot) — to decide the cap from figures; each wait is paired with its lot (the journal line carries `project:lot`), so two lots of one wave waiting at the same time (one per repository) are each counted once, and only a wait that never got its slot is reported as such; journals from before this carry no lot and are paired per wave. it reads only, and refuses any other action in the same call. The figures of 2026-10-12 and the cost of a third slot are in `docs/mesures/attente-creneaux-2026-10-12.md`.
762
763
  A slot or a registry entry is owned by a pid **and** its start time: a reused pid is a dead owner. Wave
@@ -770,7 +771,7 @@ still holds (those not yet released), the cap of each wave, the slots in use and
770
771
  **Guards, imposed by the code**: a global cap of simultaneous sessions, one wave per repository at
771
772
  a time (above); `Agent`, `Monitor`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions, and they run with `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` (no `run_in_background` on Bash or sub-agents: `claude -p` waits for any background task before exiting, up to 10 min of inactivity, so a finished session kept its slot; L285 — the session is **not** killed at its `StructuredOutput`, the real cost is only in the final `result` message); a
772
773
  temporary `pre-push` hook, installed for the duration of the wave and removed at its end (or, for a repository, as soon as all the lots that touch it are finished), refuses any push
773
- from a session (`CADENCE_ORCHESTRATED` is in their environment; L405: the same hook also calls `raf hook pre-push` for every other push, the lead's or a session started by hand: a push whose local sha has in its history — all of it, not only the range `remote..local`: a commit of a not-ready lot already on the remote still blocks, the wave would stop on « push détecté » —, a deleted branch carrying nothing, a commit of a lot of the live wave that is not ready (running, queued, in question, handed back, failed) is refused with exit 3, the message naming the lot, its state and the safe command `git push <remote> <last deliverable sha>:<remote branch>` (the newest commit of the range `remote..local`, first-parent, that holds no commit of a lot that is not ready; « aucun commit livrable » when there is none); a not-ready lot whose commits cannot be read (a plan that no longer knows the lot, a neighbour that is not a git repository) refuses the push as well, naming the lot (« commits illisibles … rétablir le plan ou attendre la fin de ces lots »), with no safe command since no sha passes the guard while it stays unreadable; a ref pushed outside `refs/heads/` (a tag) is refused the same way with no safe command at all, since pushing another sha under that name would publish a tag that does not name its own commit (wait for the lots to be ready, or tag a deliverable commit): the wave would not tolerate that push either and would stop on « push détecté »; a wave state that cannot be read only warns, an absent or broken `raf` lets the push through, `--no-verify` overrides — the same check as the guard of `cadence deliver --sha`; a repository that already has another
774
+ from a session (`CADENCE_ORCHESTRATED` is in their environment; L405: the same hook also calls `raf hook pre-push` for every other push, the lead's or a session started by hand: a push whose local sha has in its history — all of it, not only the range `remote..local`: a commit of a not-ready lot already on the remote still blocks, the wave would stop on « push détecté » —, a deleted branch carrying nothing, a commit of a lot of the live wave that is not ready (running, queued, in question, handed back, failed) is refused with exit 3, the message naming the lot, its state and the safe command `git push <remote> <last deliverable sha>:<remote branch>` (the newest commit of the range `remote..local`, first-parent, that holds no commit of a lot that is not ready; « aucun commit livrable » when there is none); a not-ready lot whose commits cannot be read (a plan that no longer knows the lot, a neighbour that is not a git repository) refuses the push as well, naming the lot (« commits illisibles … rétablir le plan ou attendre la fin de ces lots »), with no safe command since no sha passes the guard while it stays unreadable; a ref pushed outside `refs/heads/` (a tag) is refused the same way with no safe command at all, since pushing another sha under that name would publish a tag that does not name its own commit (wait for the lots to be ready, or tag a deliverable commit): the wave would not tolerate that push either and would stop on « push détecté »; a wave state that cannot be read only warns, an absent or broken `raf` lets the push through, `--no-verify` overrides — the same check as the guard of `cadence deliver --sha`; then (L426), when the project declares a gate, a push of a branch whose local sha has no green verdict of `cadence verify --gate` is refused too (`docs/deliver.md`; `CADENCE_PUSH_UNGATED=1` overrides it; when the hook is removed and the project declares a gate, a gate-only `pre-push` hook stays so that the delivery push, which comes after the last lot of the repository, is still checked against the verdict); a repository that already has another
774
775
  `pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
775
776
  upstream ref and `git ls-remote` are compared with the "before" (a `git checkout <sha>` or `git switch` that only moves `HEAD` — detached `HEAD`, another branch, no upstream any more — is not a push: the upstream ref is compared only when it is the same branch on both sides, `git ls-remote` stays the judge; the write briefs forbid `git checkout` and `git switch` in the repository and point to a throwaway `git worktree add --detach <tmp> <sha>`; should a write session still end off its starting branch, a clean tree with nothing outside the branch gets the branch put back (`git checkout <branch>`, a warning in the log, the lot carries on from the right `HEAD`), anything else — including a put-back that fails (`index.lock`, a conflicting file), cause « HEAD déplacé — remise de la branche <b> impossible : <git message> » — hands the lot back with the cause « HEAD déplacé » and holds the queue of that repository, the other repositories carrying on; a push or a removed guard hook in ANY watched repository, project or neighbour, is checked first and wins over a moved `HEAD`, L214), and a read-only step that left only UNTRACKED files (screenshots written at the root by a script with a relative path) is not an incident: the files are moved to `.cadence/runs/<wave>/<project>--<lot>/stray/` (kept in their tree; for a neighbour repository, under `stray/<its relative path, `/` replaced by `_`>/`, e.g. `stray/.._gitops/`), a warning naming them goes to the lot's warnings and the wave log, the verdict is kept and the lot continues (L162; a move that is impossible, or an untracked file that vanished during the step, stays an incident); a review that changed `HEAD` or a tracked file (a commit, a modified file) is an incident of that lot only: the lot is handed back to the lead, the lots of the other repositories carry on while the lots still queued behind it in the same repository are suspended (resumable, nothing started on a dirty tree), and a verdict the review had already produced is reported in the outcome and the lot's warnings (never recorded in the plan). **The lead's own push is the one exception (L352)**: `git push origin <sha>:main` to deliver a ready lot while the wave runs (the « livrable jusqu'à <sha> » path of the lead skill) is tolerated, because the pushed state is one the lead has re-verified. The check is the guard of `cadence deliver --sha`: every remote branch that moved advanced fast-forward (or was created) to a sha that is an ancestor of the repository's `HEAD` and holds no commit of a lot of the wave that is not ready (running, queued, in question, handed back, failed, or whose commits cannot be read), so no commit of a lot of the wave that is not ready (a commit that cites no lot, such as one of the plan, passes); the journal says `push du lead (livraison de <lot>) toléré` (`livraison du plan` when no ready lot is in the range) and the session carries on. Any other push — a deleted branch, a forced push, a sha outside the history of `HEAD`, a commit of a lot still running — and a removed guard hook still stop the wave. `raf done|ux|review` and `cadence deliver` refuse when
776
777
  `CADENCE_ORCHESTRATED` is set.
package/docs/raf.md CHANGED
@@ -59,6 +59,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
59
59
  | `raf spec <id> on\|off` | set or remove `spec:` on an existing lot (a lot that changes a behaviour, for the back-end spec step of `cadence orchestrate`, see [The spec step of a lot with `spec: true`](orchestrate.md#the-spec-step-of-a-lot-with-spec-true)); refused on a sub-task; `off` is refused on a lot that has `screen: page` (the page implies the spec: remove `screen:` first) and `on` there only says so and changes nothing; on `component` and `journey` (ordinary path since L209) both work and set or remove the back-end `spec:`. `raf spec` only takes `on`/`off`: `cadence spec check\|qa <fiche.yaml>` keeps its own subcommands. `raf check` reports a `spec:` that is neither `true` nor `false` (`spec : true ou false attendu`) |
60
60
  | `raf uses <lot> [<resource>…]` | replace the `uses:` list of a lot of any status (`done` and `dropped` included) by the names given (duplicates removed, order kept); with no name, remove the key. Prints nothing. A name is a word of lowercase letters, digits, `-` and `_` (else `ressource invalide : <name> (lettres minuscules, chiffres, - et _)`, plan unchanged); a sub-task is refused (`uses se pose sur un lot, pas sur une sous-tâche : L1/t1`); an unknown lot is `lot inconnu : <id>`. A name not yet declared is accepted: `raf check` refuses it (L251) |
61
61
  | `raf move <id> --before\|--after <id>` | reorder a lot in the plan file (L400): `<id>` is placed right before or right after the other lot, the others keep their order, and the lot travels with all its keys, its sub-tasks and its comments. Only among siblings: a lot among lots, a sub-task (`L1/t1`) among the sub-tasks of its own lot (`move : L2 et L1/t1 n'ont pas le même parent …`, plan unchanged). Exactly one of `--before` / `--after`, one id (else `usage : raf move <id> --before <id> \| raf move <id> --after <id>`); a lot placed where it already is, or an unknown id (`lot inconnu : <id>`), changes nothing. **Refused** on a read-only plan, plan kept by another tool (`plan en lecture seule : … — le modifier avec l'outil du projet`: reorder it with the project's own tool), on an archived lot (`move : L1 est archivé …`: the archive is presented in its former order and the order of the plan file never moves, see `raf archive`) and while a wave holds the repository (`move : orchestration en cours (vague …, pid …) — attendre …`: the lots of the wave rely on the order they read; the lock falls as soon as all of them are ready or handed back). The order is the order of the file, so it is the order `raf list`, `raf now`, the Gantt page and the draw of `cadence orchestrate` follow; the command only writes the plan, it does not commit it (`plan.commit` prints the command, as for every plan write) |
62
+ | `raf add`, `raf note`, `raf done` during an integration (L425) | while a wave integrates a lot of the repository (the integration lock is held by a live process), these three commands write the plan as usual and print on stderr `avertissement : intégration en cours (vague …, pid …) — …`: the plan is a tracked file of the main tree, and an integration that sees it modified refuses its lot (`arbre du dépôt modifié`, 2026-10-29: a `raf add` of the lead during the integration of L416). Commit the plan right away. Exit code 0; nothing is printed in a lot's worktree (its plan is not the tree the integration checks) nor when the lock belongs to a dead process |
62
63
  | `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
63
64
  | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr. Like every `raf` command it only writes the plan file and **never commits** it: when the plan or a file `plan.onWrite` produces is left with changes, `done` prints the `git add` / `git commit` command that takes them, the subject from the `plan.commit` template, L371) |
64
65
  | `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
@@ -76,7 +77,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
76
77
  | `raf check --ignored` | list the acknowledged commits with their date and reason |
77
78
  | `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) |
78
79
  | `raf gantt [-o file]` | standalone Gantt page |
79
- | `raf hook pre-push` | called by the temporary `pre-push` hook that `cadence orchestrate` installs (not by `raf hook install`): reads git's lines on the standard input and exits 3 when the push carries a commit of a lot of the live wave that is not ready, naming the lot and the safe `git push <remote> <sha>:<remote branch>`, or when the commits of a not-ready lot cannot be read (the guard of `deliver --sha`: the push is then unverifiable, the lot is named); exits 0 with no live wave on the repository |
80
+ | `raf hook pre-push` | called by the temporary `pre-push` hook that `cadence orchestrate` installs (not by `raf hook install`): reads git's lines on the standard input and exits 3 when the push carries a commit of a lot of the live wave that is not ready, naming the lot and the safe `git push <remote> <sha>:<remote branch>`, or when the commits of a not-ready lot cannot be read (the guard of `deliver --sha`: the push is then unverifiable, the lot is named); exits 3 as well, with or without a live wave, when the project declares a gate (`orchestrate.test` or `orchestrate.build`) and the local sha of a pushed branch has no green verdict of `cadence verify --gate` (L426: « aucun verdict de la porte » or red, naming `cadence verify --gate --sha <sha>`; `CADENCE_PUSH_UNGATED=1` in the environment of the `git push` lets it through and says so); exits 0 when nothing is to refuse |
80
81
  | `raf hook install` | add the post-commit hook (read-only, never blocks) and the pre-commit hook (does nothing unless `hook.autostart` says so, see below) |
81
82
 
82
83
  ### A commit on a lot that is still `todo`
@@ -267,7 +268,7 @@ plan:
267
268
  them and the plan is plan upkeep, and they go **in the same commit as the plan** everywhere the program commits the plan (`cadence orchestrate`, `cadence config set`, `cadence spec reseal`, the
268
269
  `hook.autostart: start` pre-commit). A produced file that is not tracked yet is added. `raf` itself never commits: for its commands, `git add` the plan and the produced files together.
269
270
  - A command that fails (non-zero code, cannot start, time limit) **refuses the write**: the plan and the declared files are put back as they were, the command stops with
270
- `plan.onWrite en échec (<command>, code 7) : <last lines of its output> — écriture refusée, plan inchangé`, and in a wave the lot is handed back with that message. With `hook.autostart: start`, the pre-commit hook says `démarrage automatique refusé : plan.onWrite en échec …`, the lot stays todo and the commit goes through without the plan (never the « raf start fait par le hook » line).
271
+ `plan.onWrite en échec (<command>, code 7) : <reason> — écriture refusée, plan inchangé`, where the reason is one line: the first `Error: …` line of the command's error output (a Node script that throws prints the file, the faulty line, then the reason, then the stack: the reason is what shows, e.g. `titre public … non conforme (93 caractères > 80)`, L429), else the first line of its error output, else of its standard output, and in a wave the lot is handed back with that message. With `hook.autostart: start`, the pre-commit hook says `démarrage automatique refusé : plan.onWrite en échec …`, the lot stays todo and the commit goes through without the plan (never the « raf start fait par le hook » line).
271
272
  `cadence spec reseal` commits the reseal before writing its note: if `plan.onWrite` then fails, the error says `le rescellé a eu lieu (commit <sha>) mais la note du lot n’a pas été écrite` (exit 2, plan unchanged); running the command again answers « rien à resceller », so add the note by hand with `raf note` once the generator works.
272
273
  A command that succeeds without producing a declared file (a wrong path in `files`) does not refuse the write, but says `raf: plan.onWrite a réussi mais le fichier déclaré est absent : <path>` on stderr — the file it really wrote would otherwise stay out of the plan's commit.
273
274
  - A read-only plan (kept by another tool) never runs the command: the project's tool owns the write.
package/docs/whats-new.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.24.0**: a push whose test gate is not green is refused; a wave counts the sessions the lead declared; a ready lot waits for its wave's integration; the `implement` brief gains « Limits and rules »; `plan.onWrite` errors read clearly.
7
8
  - **1.23.0**: the panel of lots shows five lots per project, in progress first, and reorders a lot by dragging it inside its group; `cadence lead tour` always shows the free slots.
8
9
  - **1.22.0**: the panel of lots folds a project with a click on its name; a project of more than 20 open lots starts folded.
9
10
  - **1.21.1**: `/hud lots` opens on the desktop app whatever the width; a correction session that makes no commit must say why, or the lot is handed back.
@@ -49,6 +50,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
49
50
 
50
51
  The long form of « In two lines » above (the README no longer carries a « What's new » section since L326: it links here). The full history, with the date of each version, is in [CHANGELOG.md](../CHANGELOG.md).
51
52
 
53
+ **1.24.0**: the push becomes a gate, and the wave counts its sessions better. `cadence verify --gate` keeps its verdict per sha (`.git/cadence/gate-verdicts.json`), and the `pre-push` hook refuses (exit 3) a push of a branch whose sha has no green verdict when the project declares a gate (L426, `docs/deliver.md`): the message says « aucun verdict » or « rouge » and gives `cadence verify --gate --sha <sha>`, and `CADENCE_PUSH_UNGATED=1` overrides it and says so; the guard outlives the wave, a gate-only `pre-push` hook being left behind when the wave frees the repository, so the delivery push after the last lot is still checked. The plan commit at the head of `main` has no verdict and gets a full replay with `cadence verify --gate --sha HEAD`. A wave counts the sessions the lead declared with `cadence lead hold` before taking a session slot (L424): with a cap of 3, one held session and two wave sessions are 3/3 and the next one waits. In worktree mode a ready lot queues behind the other lots of its own wave on the integration lock instead of being handed back after 10 minutes, the bound applying now only to a lock held by another process (L425), and `raf add`, `raf note` and `raf done` warn on stderr while an integration of the repository is in progress. The `implement` brief carries a block « Limits and rules, before the walkthrough » against the code bug found by the review: per rule the test that fails and the line that holds it, per changed code the edge inputs each tested, per figure or « all / never / always » a check against the code (L428, `docs/orchestrate.md`). `plan.onWrite en échec` shows the first `Error: …` line of the script instead of the tail of the Node stack (L429, `docs/raf.md`).
54
+
52
55
  **1.23.0**: the panel of lots (`/hud lots`, `docs/cadence-hud.md`) becomes a place to steer from. In each project the lots in progress come first, then the free ones, then the blocked ones, plan order kept in each group (L410); at most five lots per project are shown, with a `+ N lots` / `moins` button for the rest, and a lot that has sub-lots folds and unfolds with a click (L411); `… +N lots de plus` counts every lot the height cuts, sub-lots included. A lot is reordered by dragging it onto another lot of its group, or with Alt+↑ / Alt+↓, through `raf move` (L402): dropping it on a lot of another group (a free lot on a lot in progress) is refused with its reason, a drop that changes nothing says so, and the panel never commits the plan. For the lead, `cadence lead tour` prints `créneaux libres : N sur M` even when nothing runs, and `cadence lead release` prints the free slots and the ready lots, with a ⚠ when a slot stays free (L416, `docs/lead.md`); cadence's own test gate also plays the tests and the type-check of the `cadence-hud` mod (L417); and `npm run service` also updates the local copy of the mod (L419).
53
56
 
54
57
  **1.22.0**: the panel of lots no longer lets one big project hide the others (L409, cadence-hud 0.7.0, `docs/cadence-hud.md`): each project folds and unfolds with a click on its name (Enter under the focus, or `/hud lots <project>`), a project of more than 20 open lots starts folded on a single line (name, open lots, in progress), and the folded state is kept between two openings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvain Ladoire",
@@ -59,6 +59,14 @@ The push targets the branch being delivered (`main` here). Two cases where it do
59
59
  has moved past it): **never force it**, `--force` would rewind the remote branch. The sha cannot be delivered
60
60
  that way: stop and ask the human.
61
61
 
62
+ **The sha pushed needs a green verdict of the gate (L426).** Where the project declares a gate (`orchestrate.test` or
63
+ `orchestrate.build`), the `pre-push` hook a wave installs, and the gate-only hook it leaves behind, refuse with exit 3 a push
64
+ of a branch whose local sha has no green verdict of `cadence verify --gate`; the message names the sha and the command. The
65
+ lot's sha the lead re-checked has one; a plan or release commit above it has none, and the wave's record does not cover it:
66
+ run `cadence verify --gate --sha <sha>` on the very sha you push (a full replay for such a commit, ~5 minutes) before the
67
+ push, HEAD included when you push `main`. `CADENCE_PUSH_UNGATED=1 git push …` lets a push through with no verdict and says so
68
+ on stderr: only when the human asks for it, never the routine.
69
+
62
70
  ## Where to run
63
71
 
64
72
  Every `cadence` / `raf` command works on the git repository of the current directory. When the
@@ -30,7 +30,7 @@ commands (its CLAUDE.md names them), never with `raf start|done|note|ux|review`.
30
30
  nothing stops the session: `lead tour` and `orchestrate --status` print `⚠ session du lead échue : <label> (…)` until you
31
31
  release it, and the band shows the same ⚠ row. **At each ⚠, decide at once**: either it is still worth the slot, so renew it
32
32
  with `cadence lead hold <label> --ttl <minutes>` and say why in one sentence, or stop it (`TaskStop` or the agent's end) and
33
- `cadence lead release <label>`. A fix that ran 5 h on a 3 h slot (23-10) is the case this catches. Holding does not block anything and never raises the cap.
33
+ `cadence lead release <label>`. A fix that ran 5 h on a 3 h slot (23-10) is the case this catches. A hold blocks no session of the lead, but orchestrated waves wait for it (they take no slot while held and running sessions reach the cap); it never raises the cap.
34
34
  Never raise `--max-sessions` or `CADENCE_MAX_SESSIONS` yourself to speed a wave up: raising the cap is a decision of
35
35
  the human (a trial at 3 was decided on 2026-10-12); if the slots are the bottleneck, say so and let them decide.
36
36
  - **Never two subagents in the same repository at the same time**, and the lead does not commit in a
@@ -215,6 +215,20 @@ is always recorded by the lead's own `raf ux`, never by a subagent.
215
215
  One project at a time, by the lead: push, then the `deliver` skill (`cadence deliver --dry-run`, then
216
216
  `cadence deliver`). Follow the human's standing instructions about confirmation before production.
217
217
 
218
+ **The sha pushed needs a green verdict of the gate (L426).** Where the project declares a gate (`orchestrate.test` or
219
+ `orchestrate.build`), the `pre-push` hook (the wave's, or the gate-only hook it leaves behind once the repository is freed)
220
+ refuses with exit 3 a push of a branch whose local sha has no green verdict of `cadence verify --gate`, naming the sha and
221
+ the command to run. The verdict of the re-check (section 3, point 2) belongs to the lot's sha, and the head of `main` is
222
+ almost never that sha: `raf done` does not commit, so the plan commit that follows it (« plan: Lx terminé »), a release
223
+ commit or the plan commit of the next lot sits above. Such a plan commit has no verdict, and `verify --gate --sha <head>`
224
+ cannot reuse the wave's record (made at the lot's sha, `checks.head`): it replays the whole gate, ~5 minutes. Count that
225
+ replay in: after the plan commit, `cadence verify --gate --sha HEAD`, then push `main`. Delivering while the wave runs
226
+ (section 2b) pushes the lot's sha alone (`git push origin <sha>:main`): its verdict is the one the re-check recorded, and the
227
+ plan commit is pushed later, after its own replay. `CADENCE_PUSH_UNGATED=1 git push …` lets a push through with no verdict
228
+ and says so on stderr: it is for the push the human explicitly asks ungated (a plan-only or docs-only commit above a verified
229
+ sha, when the human says so), never the routine: the incident the hook answers (29-10, a project pushed with a red gate whose
230
+ exit code a `| tail -1` had hidden) was exactly a push nobody had checked.
231
+
218
232
  When the wave's final table gives a line « livrable jusqu'à <sha> » for the repository (a ready lot stacked under a
219
233
  handed-back or failed one), HEAD holds the code of the lot above, which was not reviewed: never push or deliver HEAD.
220
234
  Push that sha alone first (`git push origin <sha>:main`), then deliver it (`cadence deliver --dry-run --sha <sha>`, then
@@ -19,9 +19,14 @@ Self-check before handing back (the recurring findings of the reviews). Go throu
19
19
  - no false comment: a comment, a docstring or a message never describes a behaviour the code no longer has or does not have.
20
20
  A point you cannot hold is written under "choix" or "nonVerifie" of your report, not left unsaid.
21
21
 
22
+ Limits and rules, before the walkthrough (a code bug found by the review is the first cause of correction passes: 100 of the 143 classified passes of the 23–29-10 window). Re-read your diff against the fiche (if there is one) and the lot, then:
23
+ - rules: for each rule of the fiche (`Rn`), or of the lot when it has no fiche, name the test that fails if the code breaks it and the line of code that holds it; a rule with no such pair is not done;
24
+ - edge cases: for every function, parser, pattern, threshold or command you add or change, list the inputs at its edge — empty, absent, twice, wrong order, wrong type, refused, the real shape of the data (not the one of your fixture), the other callers of what you changed — and give each one a test that fails without your code, before handing back, except a case the fiche does not cover: with a fiche, you do not code a rule it does not state, so that case is written under "nonVerifie" or "choix", neither coded nor tested; a case you listed and did not test goes under "nonVerifie";
25
+ - claims: each figure, « all », « never » or « always » that your README, docs, comments or tests state is checked against the code or by running it, not from memory.
26
+
22
27
  Real walkthrough once the tests are green, before the auto-verification below: play what you delivered as a user would, not by re-reading the diff. A command: run it in a throwaway repository (`git init` in a fresh folder of the session's temporary folder, never inside the repository), following step by step the documentation the lot wrote (README, `docs/`), every command exactly as written there; a command the documentation cannot run, or whose documented path is impossible from the state the previous steps leave, is broken. A screen: open it in each of its states, failure included, the failure shown where the action takes place (in view from the control the user just used, not above or below the fold), and take a capture of each. What breaks, fix in this same session (test first, a `fix({{lot}}): …` commit) and play it again. Then write in "resume" a paragraph that starts with « parcours réel »: what you played, what broke and was fixed, what you could not play (also under "nonVerifie"); a lot with nothing to play (no command, no screen) says so in that paragraph.
23
28
 
24
- Auto-verification once the tests are green, before the review: when the full test suite and the build pass, re-read the complete diff of the lot, commit by commit (the commits `raf commits {{lot}}` lists, each with `git show <sha>`; never a `git diff` range, which would take in the commits of other lots) against the grid of the code-reviewer agent: code bug (wrong result, unhandled error, input trusted); architecture drift (a responsibility put in the wrong module, a duplicated block, a layer skipped); docs/architecture.md (or the project's architecture documentation) up to date; edge cases (empty, absent, twice, wrong order, refused); one test per changed behaviour. What you find, fix it at once (test first, a `fix({{lot}}): …` commit) and rerun the suite and the build. Then write in "resume" a paragraph that starts with « auto-vérification »: what you re-read, what you found and fixed, what you could not hold. It is one reading of the diff, not a second review: no subagent, no new exploration, no re-reading of files the diff does not touch.
29
+ Auto-verification once the tests are green, before the review: when the full test suite and the build pass, re-read the complete diff of the lot, commit by commit (the commits `raf commits {{lot}}` lists, each with `git show <sha>`; never a `git diff` range, which would take in the commits of other lots) against the grid of the code-reviewer agent: code bug (wrong result, unhandled error, input trusted); architecture drift (a responsibility put in the wrong module, a duplicated block, a layer skipped); docs/architecture.md (or the project's architecture documentation) up to date; edge cases (the list of the edge-cases point above, each with its test); one test per changed behaviour. What you find, fix it at once (test first, a `fix({{lot}}): …` commit) and rerun the suite and the build. Then write in "resume" a paragraph that starts with « auto-vérification »: what you re-read, what you found and fixed, what you could not hold. It is one reading of the diff, not a second review: no subagent, no new exploration, no re-reading of files the diff does not touch.
25
30
 
26
31
  Decide minor interpretation questions yourself (the wording of a message, a name, a default, the
27
32
  reading of an ambiguous line of the lot) and list each one under "choix" in your report, with the