@sylad/cadence 1.17.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.17.0",
11
+ "version": "1.17.1",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
3
  "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
- "version": "1.17.0",
4
+ "version": "1.17.1",
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
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from
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';
5
- import { defaultPlan, PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readSessionConfig } from './config.js';
5
+ import { defaultPlan, PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readSessionConfig, readVerifyConfig } from './config.js';
6
6
  import { repoSections, repoShas, resolveLotRepos } from './repos.js';
7
7
  import { audit, exemptPlanOnly, gateProgress, gateText, gateWaiting, hypothesesOf, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork, workByLot } from './audit.js';
8
8
  import { short } from './check.js';
@@ -745,7 +745,7 @@ function dispatch(argv, io) {
745
745
  const articles = readDocsConfig(configPath).articles;
746
746
  // Sans public: d'un lot visible, le site reprend le titre de sa Nouveauté la plus récente.
747
747
  const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
748
- const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, args: rest, articles, publicTitle, out: io.out, err: io.err };
748
+ const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, waveId: orchestrating?.wave, planCommit: readPlanConfig(configPath, root)?.commit, args: rest, articles, publicTitle, out: io.out, err: io.err };
749
749
  return deliver(ctx, realDeps(root));
750
750
  }
751
751
  case 'verify': {
@@ -1304,7 +1304,7 @@ function specReseal(args, reason, porteur, ctx, io) {
1304
1304
  if (tracked) {
1305
1305
  try {
1306
1306
  // Les fichiers de plan.onWrite (L346) partent avec la note : la commande a tourné à l'écriture du plan.
1307
- const withPlan = [planRel, ...(produced.length ? dirtyPaths(repo, produced) : [])];
1307
+ const withPlan = [planRel, ...dirtyPaths(repo, produced)];
1308
1308
  git(repo, ['add', '--', ...withPlan]);
1309
1309
  git(repo, ['commit', '-q', '-m', planCommitMessage(readPlanConfig(join(repo, 'cadence.yaml'), repo)?.commit, { lot: cite, action: PLAN_ACTIONS.resealed(norm) }), '--', ...withPlan]);
1310
1310
  }
@@ -1481,7 +1481,7 @@ async function verifyGateCommand(values, io) {
1481
1481
  const sha = resolveCommit(top, values.sha);
1482
1482
  if (!sha)
1483
1483
  throw new RafError(`verify --gate : --sha ${values.sha} : commit introuvable dans ${top}`);
1484
- return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), out: io.out });
1484
+ return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), verify: readVerifyConfig(configPath), out: io.out });
1485
1485
  }
1486
1486
  /** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
1487
1487
  async function sessionAll(rest, values, io) {
@@ -232,7 +232,7 @@ export function configSet(parent, key, valueText, opts, today) {
232
232
  plan.setStatus(id, 'doing', today);
233
233
  plan.save();
234
234
  }
235
- const sha = commitPaths(dir, ['cadence.yaml', planRel, ...(produced.length ? dirtyPaths(dir, produced) : [])], `chore${id ? `(${id})` : ''}: cadence.yaml — ${key} = ${shown}`);
235
+ const sha = commitPaths(dir, ['cadence.yaml', planRel, ...dirtyPaths(dir, produced)], `chore${id ? `(${id})` : ''}: cadence.yaml — ${key} = ${shown}`);
236
236
  done++;
237
237
  lines.push(`✓ ${name} · ${id || 'lot de config'} · ${key} = ${shown} · commit ${sha}`);
238
238
  }
package/dist/config.js CHANGED
@@ -312,6 +312,31 @@ export function readSessionConfig(file) {
312
312
  }
313
313
  const EFFORTS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
314
314
  export const EFFORT_STEPS = ['precheck', 'spec', 'maquette', 'fiche', 'accept', 'implement', 'fix', 'review', 'ux'];
315
+ /** Clé `verify:` de cadence.yaml. Absente : `{}`. */
316
+ export function readVerifyConfig(file) {
317
+ if (!existsSync(file))
318
+ return {};
319
+ let raw;
320
+ try {
321
+ raw = parse(readFileSync(file, 'utf8'));
322
+ }
323
+ catch (e) {
324
+ throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
325
+ }
326
+ const v = raw?.verify;
327
+ if (v == null)
328
+ return {};
329
+ if (!isObject(v))
330
+ throw new RafError(`${file} : verify doit être un objet { inPlace }`);
331
+ for (const k of Object.keys(v))
332
+ if (k !== 'inPlace')
333
+ throw new RafError(`${file} : verify.${k} inconnu (attendu : inPlace)`);
334
+ if (v.inPlace == null)
335
+ return {};
336
+ if (typeof v.inPlace !== 'boolean')
337
+ throw new RafError(`${file} : verify.inPlace : true ou false attendu`);
338
+ return { inPlace: v.inPlace };
339
+ }
315
340
  const REVIEW_KEYS = ['threshold', 'light', 'full'];
316
341
  const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
317
342
  /** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
package/dist/deliver.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { execFileSync, spawn } from 'node:child_process';
2
2
  import { readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { constants } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, relative } from 'node:path';
5
5
  import { parse } from 'yaml';
6
6
  import { onTermination, readProcs, SIGNAL_GRACE_MS, SIGNAL_PENDING_MS, TreeTracker } from './proc.js';
7
7
  import { isPlanOnly } from './audit.js';
8
- import { headSha, isAncestor, onRemote, readCommits, repoStatus, resolveCommit } from './git.js';
8
+ import { commitPaths, dirtyPaths, headSha, isAncestor, onRemote, readCommits, repoStatus, resolveCommit } from './git.js';
9
+ import { PLAN_ACTIONS, planCommitMessage } from './plan-commit.js';
9
10
  import { citedRefs } from './link.js';
10
11
  import { describeArticle, openArticleLots } from './articles.js';
11
12
  import { RafError } from './plan.js';
@@ -535,8 +536,9 @@ function deliveredLots(ctx, ids, prev, sha) {
535
536
  }
536
537
  /**
537
538
  * Livraison verte (L144) : les lots livrés dont le titre est public changent ce que les articles externes racontent, on ouvre
538
- * donc le lot qui les rafraîchit. Le plan est écrit mais pas commité (deliver ne commite jamais) ; une erreur ici ne défait pas
539
- * une livraison déjà faite.
539
+ * donc le lot qui les rafraîchit. Hors vague le plan est écrit mais pas commité ; pendant une vague (L388) il est commité avec les fichiers que produit `plan.onWrite`, avec
540
+ * le gabarit `plan.commit`, pour ne pas laisser le plan modifié dans l'arbre où une session de la vague travaille — sauf si le plan ou ces fichiers l'étaient
541
+ * déjà avant l'écriture (le lead y a des modifications que ce commit emporterait). Une erreur ici ne défait pas une livraison déjà faite.
540
542
  */
541
543
  function refreshArticles(ctx, ids, sha) {
542
544
  if (!ctx.plan || !ctx.articles?.length || !ids.length)
@@ -544,6 +546,9 @@ function refreshArticles(ctx, ids, sha) {
544
546
  try {
545
547
  // Relu : le script du projet a pu fermer des lots ou commiter le plan depuis le départ de la livraison, save() ne doit pas l'écraser.
546
548
  const plan = ctx.plan.reloaded();
549
+ const planFile = relative(ctx.root, plan.path);
550
+ const held = [planFile, ...plan.produced];
551
+ const wasDirty = dirtyPaths(ctx.root, held).length > 0;
547
552
  const wanted = new Set(ids);
548
553
  const r = openArticleLots({
549
554
  plan,
@@ -558,10 +563,24 @@ function refreshArticles(ctx, ids, sha) {
558
563
  ctx.err(`deliver : ${w}`);
559
564
  if (r.opened.length || r.noted.length)
560
565
  plan.save();
566
+ const touched = [...r.opened, ...r.noted];
567
+ let state = 'plan modifié, à commiter';
568
+ if (touched.length && ctx.duringWave && wasDirty) {
569
+ state = 'plan déjà modifié avant la livraison, non commité : à commiter avec vos modifications';
570
+ }
571
+ else if (touched.length && ctx.duringWave) {
572
+ const message = planCommitMessage(ctx.planCommit, { lot: touched.join(', '), action: r.opened.length ? PLAN_ACTIONS.articleOpened : PLAN_ACTIONS.articleNoted, wave: ctx.waveId });
573
+ try {
574
+ state = `plan commité ${commitPaths(ctx.root, [planFile, ...dirtyPaths(ctx.root, plan.produced)], message)}`;
575
+ }
576
+ catch (e) {
577
+ state = `commit du plan refusé (${e.message}), plan modifié, à commiter`;
578
+ }
579
+ }
561
580
  for (const id of r.opened)
562
- ctx.out(`article à rafraîchir : ${id} « ${plan.lot(id).title} » ouvert (plan modifié, à commiter) — à jouer dans une vague ordinaire`);
581
+ ctx.out(`article à rafraîchir : ${id} « ${plan.lot(id).title} » ouvert (${state}) — à jouer dans une vague ordinaire`);
563
582
  for (const id of r.noted)
564
- ctx.out(`article à rafraîchir : ${id} déjà ouvert, titres livrés ajoutés en note (plan modifié, à commiter)`);
583
+ ctx.out(`article à rafraîchir : ${id} déjà ouvert, titres livrés ajoutés en note (${state})`);
565
584
  for (const m of r.manual)
566
585
  ctx.out(`article à rafraîchir : ${m}`);
567
586
  }
package/dist/git.js CHANGED
@@ -241,8 +241,10 @@ export function addedTimes(dir) {
241
241
  }
242
242
  return times;
243
243
  }
244
- /** Chemins parmi `paths` (relatifs à la racine) que git voit modifiés, stagés ou non suivis. */
244
+ /** Chemins parmi `paths` (relatifs à la racine) que git voit modifiés, stagés ou non suivis. Liste vide → rien (sans pathspec, git listerait tout l'arbre). */
245
245
  export function dirtyPaths(root, paths) {
246
+ if (paths.length === 0)
247
+ return [];
246
248
  const out = git(root, ['-c', 'core.quotepath=off', 'status', '--porcelain', '-z', '--', ...paths]);
247
249
  return out.split('\0').filter(Boolean).map((l) => l.slice(3));
248
250
  }
@@ -148,7 +148,7 @@ async function commitPlan(c, message, root = c.lot.repo, before = new Map()) {
148
148
  const ownFile = planOwnFile(c, repo);
149
149
  // Les fichiers de plan.onWrite (L346) entrent même non suivis : le premier plan publié n'est pas encore au dépôt.
150
150
  const produced = c.loadPlan().produced;
151
- const dirty = [...new Set([...trackedPaths(await snapshot(repo, { remote: false })), ...(produced.length ? dirtyPaths(repo, produced) : [])])];
151
+ const dirty = [...new Set([...trackedPaths(await snapshot(repo, { remote: false })), ...dirtyPaths(repo, produced)])];
152
152
  if (dirty.length === 0)
153
153
  return null;
154
154
  const foreign = dirty.filter((f) => !ownFile(f));
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, lstatSync, mkdirSync, readdirSync, rmSync, symlinkSync, realpathSync } from 'node:fs';
3
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, symlinkSync, realpathSync } from 'node:fs';
4
4
  import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
5
5
  import { lotSlug } from './state.js';
6
6
  import { cadenceHome, liveWaves } from './registry.js';
@@ -123,16 +123,52 @@ export function mirrorNodeModules(repo, dir, name, links) {
123
123
  * fichier ignoré le plus haut ; son parent doit exister dans le worktree (sinon rien où le relier). Rend les liens créés.
124
124
  */
125
125
  export function linkIgnored(repo, dir) {
126
- const ignored = git(repo, 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory').split('\n').map((f) => f.replace(/\/$/, '')).filter((f) => f && !f.startsWith('.cadence') && !f.split('/').some((p) => OUTPUT_DIRS.has(p)));
127
- return linkNames(repo, dir, ignored);
126
+ return linkNames(repo, dir, ignoredDeps(repo));
127
+ }
128
+ /** Les dossiers et fichiers ignorés du dépôt vivant les plus hauts, hors `.cadence` et sorties de build : ce que les worktrees relient et que `verify --gate` copie. */
129
+ export function ignoredDeps(repo) {
130
+ return git(repo, 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory').split('\n').map((f) => f.replace(/\/$/, '')).filter((f) => f && !f.startsWith('.cadence') && !f.split('/').some((p) => OUTPUT_DIRS.has(p)));
128
131
  }
129
132
  /**
130
- * Relie dans le worktree `dir` les dépendances que le dépôt vivant déclare (`orchestrate.worktreeDeps`, L348), par leur chemin relatif. Celles qui n'existent pas
131
- * dans le dépôt vivant ne sont pas liées et sont rendues dans `missing`. Rend aussi les liens créés.
133
+ * Copie dans le worktree `dir` ces dépendances du dépôt vivant (L389), au lieu de les y relier : un outil qui refuse de charger un `node_modules` lié depuis
134
+ * un autre arbre (Astro : « Could not load … ») les trouve chez lui. Les liens relatifs du dossier qui restent dans le dépôt (espaces de travail) sont
135
+ * copiés tels quels : ils pointent dans la copie. Ceux qui en sortent (`file:../voisin`, `link:`) pointeraient dans le vide depuis `dir` : ils sont refaits
136
+ * vers leur cible vivante. Rend les chemins copiés ; un chemin dont le parent n'existe pas dans le worktree est passé.
132
137
  */
133
- export function linkDeclared(repo, dir, names) {
134
- const present = names.filter((n) => lstatSync(join(repo, n), { throwIfNoEntry: false }));
135
- return { links: linkNames(repo, dir, present), missing: names.filter((n) => !present.includes(n)) };
138
+ export function copyDeps(repo, dir, names) {
139
+ const copied = [];
140
+ for (const name of names) {
141
+ const to = join(dir, name);
142
+ if (existsSync(to) || !existsSync(dirname(to)))
143
+ continue;
144
+ cpSync(join(repo, name), to, { recursive: true, verbatimSymlinks: true });
145
+ relinkOutside(realpathSync(repo), join(repo, name), to);
146
+ copied.push(name);
147
+ }
148
+ return copied;
149
+ }
150
+ /** Dans la copie `to` de `live`, refait vers leur cible vivante les liens relatifs dont la cible sort du dépôt `repoReal`. */
151
+ function relinkOutside(repoReal, live, to) {
152
+ for (const e of readdirSync(live, { withFileTypes: true })) {
153
+ const liveEntry = join(live, e.name);
154
+ const copy = join(to, e.name);
155
+ if (e.isSymbolicLink()) {
156
+ let real;
157
+ try {
158
+ real = realpathSync(liveEntry);
159
+ }
160
+ catch {
161
+ continue; // lien mort : recopié tel quel
162
+ }
163
+ if (relative(repoReal, real).startsWith('..')) {
164
+ rmSync(copy);
165
+ symlinkSync(real, copy);
166
+ }
167
+ }
168
+ else if (e.isDirectory()) {
169
+ relinkOutside(repoReal, liveEntry, copy);
170
+ }
171
+ }
136
172
  }
137
173
  function linkNames(repo, dir, names) {
138
174
  const links = [];
@@ -20,6 +20,9 @@ export const PLAN_ACTIONS = {
20
20
  done: 'terminé',
21
21
  reviewRecorded: 'revue de code enregistrée',
22
22
  reviewNoted: 'revue de code notée',
23
+ /** `cadence deliver` pendant une vague (L388) : le lot de l'article ouvert, ou ses titres ajoutés en note. */
24
+ articleOpened: 'article à rafraîchir ouvert',
25
+ articleNoted: 'article à rafraîchir : titres livrés notés',
23
26
  /** `spec green` (L366) : jamais la liste des tests rescellés, elle va dans le corps du commit (header-max-length de commitlint). */
24
27
  specGreen: 'acceptation verte, vert: true posé par cadence spec green',
25
28
  resealed: (norm) => `test d'acceptation rescellé (${norm})`,
@@ -1,12 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { lstatSync, mkdtempSync, rmSync, unlinkSync } from 'node:fs';
2
+ import { lstatSync, mkdtempSync, rmSync } from 'node:fs';
3
3
  import { availableParallelism, tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { basename, join } from 'node:path';
5
5
  import { onTermination } from './proc.js';
6
6
  import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
7
7
  import { configuredCap } from './orchestrate/command.js';
8
8
  import { replayRed, TEST_WORKERS_ENV, testWorkersFrom } from './orchestrate/workers.js';
9
- import { git, gitFailure, linkDeclared, linkIgnored } from './orchestrate/worktree.js';
9
+ import { copyDeps, git, gitFailure, ignoredDeps, linkIgnored } from './orchestrate/worktree.js';
10
10
  /** Délai de chaque commande de la porte : celui du rejeu de la ligne de base de la vague. */
11
11
  const GATE_TIMEOUT_MS = 30 * 60_000;
12
12
  /** Lignes de la fin de la sortie montrées quand aucun test n'est identifiable dans une sortie rouge. */
@@ -48,10 +48,26 @@ function sh(cmd, cwd, timeoutMs, group, env) {
48
48
  });
49
49
  });
50
50
  }
51
+ /** Pourquoi l'arbre principal ne peut pas porter la porte au sha : non propre ou à un autre commit ; null s'il le peut. */
52
+ function mainTreeObstacle(repo, sha) {
53
+ try {
54
+ const head = git(repo, 'rev-parse', 'HEAD');
55
+ if (head !== sha)
56
+ return `l'arbre principal est à ${head.slice(0, 7)}, pas au sha demandé`;
57
+ if (git(repo, 'status', '--porcelain'))
58
+ return 'l\'arbre principal a des modifications non validées ou des fichiers non suivis';
59
+ return null;
60
+ }
61
+ catch (e) {
62
+ return `l'état de l'arbre principal est illisible (${gitFailure(e)})`;
63
+ }
64
+ }
51
65
  /**
52
66
  * `cadence verify --gate` (L348) : la porte du projet (`orchestrate.build`, puis `orchestrate.test`, dans l'ordre de la vague : les tests lisent ce que le build produit)
53
- * rejouée au `sha` dans un worktree jetable, hors du dépôt, dont les dépendances non suivies sont reliées depuis le dépôt vivant (`orchestrate.worktreeDeps`,
54
- * sinon les dossiers ignorés de tout niveau, comme les worktrees de vague). Les deux commandes sont rejouées même si la première est rouge : le compte rendu est entier.
67
+ * rejouée au `sha` (L389) dans l'arbre principal quand il est propre et à ce sha (les outils qui refusent un `node_modules` venu d'un autre arbre y tournent chez eux) ;
68
+ * sinon dans un worktree jetable, hors du dépôt, dont les dépendances non suivies sont copiées depuis le dépôt vivant (`orchestrate.worktreeDeps`,
69
+ * sinon les `node_modules` de tout niveau ; les autres dossiers ignorés sont reliés, comme dans les worktrees de vague). `verify.inPlace` impose l'un (`true`, refus si l'arbre n'est pas propre au sha) ou l'autre (`false`).
70
+ * La première ligne de la sortie dit où la porte a tourné ; en place, l'arbre est relu à la fin et, s'il a bougé, le verdict est dit non probant (code 2). Les deux commandes sont rejouées même si la première est rouge : le compte rendu est entier.
55
71
  * Chaque commande reçoit `CADENCE_TEST_WORKERS` comme dans la porte d'une vague (L350, L387) : la valeur de l'environnement de l'appelant si c'est un entier positif, sinon cœurs ÷ plafond de sessions (`CADENCE_MAX_SESSIONS`, 2 par défaut) ;
56
72
  * sans elle le rejeu `--maxWorkers=$CADENCE_TEST_WORKERS` de `orchestrate.testReplay` sortirait en erreur et ne prouverait rien.
57
73
  * Le worktree est retiré dans tous les cas. 0 vert, 1 rouge (avec les tests en échec), 2 rien à rejouer ou worktree impossible.
@@ -69,15 +85,38 @@ export async function verifyGate(ctx) {
69
85
  out('verify --gate : ni orchestrate.test ni orchestrate.build déclarés dans cadence.yaml — rien à rejouer');
70
86
  return 2;
71
87
  }
88
+ const obstacle = mainTreeObstacle(repo, sha);
89
+ const mode = ctx.verify?.inPlace;
90
+ if (mode === true && obstacle) {
91
+ out(`verify --gate : verify.inPlace demande l'arbre principal, mais ${obstacle} — rien n'est joué`);
92
+ return 2;
93
+ }
94
+ const inPlace = mode === true || (mode === undefined && obstacle === null);
95
+ if (inPlace) {
96
+ out(`verify --gate ${short} : porte jouée dans l'arbre principal ${repo} (propre, au sha)`);
97
+ const group = {};
98
+ const forget = onTermination(() => {
99
+ if (group.pgid !== undefined)
100
+ killGroup(group.pgid);
101
+ });
102
+ try {
103
+ const code = await runSteps(steps, repo, ctx, group, env, short);
104
+ // Une vague intègre ses lots par fast-forward dans cet arbre : s'il a bougé pendant la porte, les commandes ont lu un mélange de commits.
105
+ const moved = mainTreeObstacle(repo, sha);
106
+ if (moved === null)
107
+ return code;
108
+ out(`verify --gate ${short} : verdict non probant — ${moved} à la fin de la porte (un lot a pu être intégré pendant qu'elle tournait, ou une commande a écrit hors des dossiers ignorés) ; la rejouer`);
109
+ return 2;
110
+ }
111
+ finally {
112
+ forget();
113
+ }
114
+ }
72
115
  const dir = mkdtempSync(join(tmpdir(), 'cadence-verify-'));
73
- const links = [];
74
116
  const group = {};
75
117
  const cleanup = () => {
76
118
  if (group.pgid !== undefined)
77
119
  killGroup(group.pgid);
78
- for (const k of links)
79
- if (lstatSync(k, { throwIfNoEntry: false })?.isSymbolicLink())
80
- unlinkSync(k);
81
120
  try {
82
121
  git(repo, 'worktree', 'remove', '--force', dir);
83
122
  }
@@ -101,69 +140,81 @@ export async function verifyGate(ctx) {
101
140
  out(`verify --gate : worktree de ${short} impossible — ${gitFailure(e)}`);
102
141
  return 2;
103
142
  }
143
+ let copied;
104
144
  if (config.worktreeDeps) {
105
- const linked = linkDeclared(repo, dir, config.worktreeDeps);
106
- links.push(...linked.links);
107
- if (linked.missing.length)
108
- out(`dépendances déclarées absentes de l'arbre principal, non liées : ${linked.missing.join(', ')}`);
145
+ const present = config.worktreeDeps.filter((n) => lstatSync(join(repo, n), { throwIfNoEntry: false }));
146
+ copied = copyDeps(repo, dir, present);
147
+ const missing = config.worktreeDeps.filter((n) => !present.includes(n));
148
+ if (missing.length)
149
+ out(`dépendances déclarées absentes de l'arbre principal, non copiées : ${missing.join(', ')}`);
109
150
  }
110
151
  else {
111
- links.push(...linkIgnored(repo, dir));
112
- }
113
- let red = 0;
114
- for (const step of steps) {
115
- const r = await sh(step.cmd, dir, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group, env);
116
- if (r.code === 0) {
117
- out(`✓ ${step.name} (${step.cmd})`);
118
- continue;
119
- }
120
- const tests = step.name === 'tests';
121
- const names = tests ? failedTests(r.output) : [];
122
- const unloaded = tests ? unloadedSuites(r.output) : [];
123
- // `orchestrate.testReplay` comme la porte d'une vague (L354, L387) : un rouge nommé est rejoué seul avant d'être compté ; vert au rejeu, il est instable sous charge.
124
- // Un échec que la sortie ne rattache à aucun test, ou un fichier qui ne se charge pas, n'est jamais rejoué.
125
- const replayable = tests && config.testReplay && names.length > 0 && unloaded.length === 0 && !hasUnnamedFailure(r.output);
126
- const replays = [];
127
- const { red: still, unstable } = replayable
128
- ? await replayRed(async (c) => {
129
- const p = await sh(c, dir, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group, env);
130
- replays.push(p.code);
131
- return p;
132
- }, config.testReplay, names, r.output)
133
- : { red: names, unstable: [] };
134
- if (replayable && still.length === 0) {
135
- out(`✓ ${step.name} (${step.cmd}) — ${unstable.length} test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :`);
136
- for (const n of unstable)
137
- out(` - ${n}`);
138
- continue;
139
- }
140
- red++;
141
- out(`✗ ${step.name} (${step.cmd}) — code ${r.code}`);
142
- for (const n of still)
143
- out(` - ${n}`);
144
- for (const n of unloaded)
145
- out(` - ${n} (ne se charge pas)`);
146
- // Un rejeu a eu lieu : dire lequel, pour qu'un rejeu en échec pour une autre raison qu'un vrai rouge (commande cassée, aucun test exécuté) ne passe pas pour une confirmation du rouge.
147
- if (replays.length)
148
- out(` rejeu seul (orchestrate.testReplay) : ${replays.length} rejeu(x), dernier code ${replays[replays.length - 1]} — ${names.length - still.length} test(s) confirmé(s) vert(s) sur ${names.length} ; un rejeu en échec ou sans test exécuté ne prouve rien`);
149
- if (unstable.length) {
150
- out(` ${unstable.length} autre(s) test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :`);
151
- for (const n of unstable)
152
- out(` - ${n}`);
153
- }
154
- if (still.length + unloaded.length === 0 || hasUnnamedFailure(r.output, true)) {
155
- if (still.length + unloaded.length)
156
- out(' (au moins un échec que la sortie ne rattache à aucun test)');
157
- if (r.output.trim())
158
- for (const l of r.output.trimEnd().split('\n').slice(-TAIL_LINES))
159
- out(` | ${l}`);
160
- }
152
+ const deps = ignoredDeps(repo);
153
+ // Seuls les node_modules sont copiés : le reste (.venv, données, .env) reste relié, comme dans un worktree de vague, sans rien dupliquer dans /tmp.
154
+ copied = copyDeps(repo, dir, deps.filter((n) => basename(n) === 'node_modules'));
155
+ linkIgnored(repo, dir);
161
156
  }
162
- out(red === 0 ? `verify --gate ${short} : vert` : `verify --gate ${short} : rouge (${red} étape${red > 1 ? 's' : ''} sur ${steps.length})`);
163
- return red === 0 ? 0 : 1;
157
+ const why = mode === false ? 'verify.inPlace: false' : obstacle;
158
+ out(`verify --gate ${short} : porte jouée dans une copie jetable ${dir} (${why}) ; dépendances copiées : ${copied.length ? copied.join(', ') : 'aucune'}`);
159
+ return await runSteps(steps, dir, ctx, group, env, short);
164
160
  }
165
161
  finally {
166
162
  forget();
167
163
  cleanup();
168
164
  }
169
165
  }
166
+ /** Les étapes de la porte jouées dans `cwd` ; le compte rendu et le code de sortie (0 vert, 1 rouge). */
167
+ async function runSteps(steps, cwd, ctx, group, env, short) {
168
+ const { config, out } = ctx;
169
+ let red = 0;
170
+ for (const step of steps) {
171
+ const r = await sh(step.cmd, cwd, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group, env);
172
+ if (r.code === 0) {
173
+ out(`✓ ${step.name} (${step.cmd})`);
174
+ continue;
175
+ }
176
+ const tests = step.name === 'tests';
177
+ const names = tests ? failedTests(r.output) : [];
178
+ const unloaded = tests ? unloadedSuites(r.output) : [];
179
+ // `orchestrate.testReplay` comme la porte d'une vague (L354, L387) : un rouge nommé est rejoué seul avant d'être compté ; vert au rejeu, il est instable sous charge.
180
+ // Un échec que la sortie ne rattache à aucun test, ou un fichier qui ne se charge pas, n'est jamais rejoué.
181
+ const replayable = tests && config.testReplay && names.length > 0 && unloaded.length === 0 && !hasUnnamedFailure(r.output);
182
+ const replays = [];
183
+ const { red: still, unstable } = replayable
184
+ ? await replayRed(async (c) => {
185
+ const p = await sh(c, cwd, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group, env);
186
+ replays.push(p.code);
187
+ return p;
188
+ }, config.testReplay, names, r.output)
189
+ : { red: names, unstable: [] };
190
+ if (replayable && still.length === 0) {
191
+ out(`✓ ${step.name} (${step.cmd}) — ${unstable.length} test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :`);
192
+ for (const n of unstable)
193
+ out(` - ${n}`);
194
+ continue;
195
+ }
196
+ red++;
197
+ out(`✗ ${step.name} (${step.cmd}) — code ${r.code}`);
198
+ for (const n of still)
199
+ out(` - ${n}`);
200
+ for (const n of unloaded)
201
+ out(` - ${n} (ne se charge pas)`);
202
+ // Un rejeu a eu lieu : dire lequel, pour qu'un rejeu en échec pour une autre raison qu'un vrai rouge (commande cassée, aucun test exécuté) ne passe pas pour une confirmation du rouge.
203
+ if (replays.length)
204
+ out(` rejeu seul (orchestrate.testReplay) : ${replays.length} rejeu(x), dernier code ${replays[replays.length - 1]} — ${names.length - still.length} test(s) confirmé(s) vert(s) sur ${names.length} ; un rejeu en échec ou sans test exécuté ne prouve rien`);
205
+ if (unstable.length) {
206
+ out(` ${unstable.length} autre(s) test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :`);
207
+ for (const n of unstable)
208
+ out(` - ${n}`);
209
+ }
210
+ if (still.length + unloaded.length === 0 || hasUnnamedFailure(r.output, true)) {
211
+ if (still.length + unloaded.length)
212
+ out(' (au moins un échec que la sortie ne rattache à aucun test)');
213
+ if (r.output.trim())
214
+ for (const l of r.output.trimEnd().split('\n').slice(-TAIL_LINES))
215
+ out(` | ${l}`);
216
+ }
217
+ }
218
+ out(red === 0 ? `verify --gate ${short} : vert` : `verify --gate ${short} : rouge (${red} étape${red > 1 ? 's' : ''} sur ${steps.length})`);
219
+ return red === 0 ? 0 : 1;
220
+ }
package/docs/deliver.md CHANGED
@@ -143,7 +143,7 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
143
143
  commits (see "Plan upkeep needs no lot") are skipped in that list: they cite
144
144
  lots without delivering anything.
145
145
  - With [`docs.articles`](news.md#an-article-elsewhere-that-follows-the-delivery), a green delivery of lots with a
146
- public title also opens the lot `Article <project> à rafraîchir` in the plan.
146
+ public title also opens the lot `Article <project> à rafraîchir` in the plan (committed, with the files `plan.onWrite` produces, when a wave holds the repository, L388).
147
147
 
148
148
  ## A project with its own delivery script
149
149
 
@@ -280,24 +280,34 @@ project without `deliver.verify`.
280
280
  replays the *gate* of the project, `orchestrate.build` then `orchestrate.test` of `cadence.yaml`
281
281
  (the order of the wave's gate: the tests read what the build produces), at one commit, for the
282
282
  lead's re-verification (« the lead re-verifies itself », skill `lead`). It never touches the shared
283
- tree, which holds the lots still running.
283
+ tree while it holds uncommitted work (the lots still running): it then replays in a throwaway copy.
284
284
 
285
285
  ```sh
286
286
  cadence verify --gate --sha 6b0d9aa # in the project
287
287
  cadence verify --gate --sha 6b0d9aa --project app # from the parent folder, the project `app` (its own git repository)
288
288
  ```
289
289
 
290
- It adds a throwaway worktree of the sha under the OS temporary folder (`cadence-verify-…`), links
291
- the **untracked dependencies** of the main tree into it, runs the commands there, and removes the worktree
292
- whatever the result. A bare worktree has no `node_modules` or `.venv`: on 26-10 the re-verification of a lot
293
- in one gave nine red suites that were only missing dependencies. Which dependencies:
290
+ **Where the gate runs (L389).** The first line of the output says it:
291
+
292
+ - the main tree is **clean and at the sha** (`git status --porcelain` empty, `HEAD` equal to the sha): the gate runs **there**, in the repository itself, with its own `node_modules` and its real paths (`verify --gate 6b0d9aa : porte jouée dans l'arbre principal /path/app (propre, au sha)`). A tool that refuses a `node_modules` linked from another tree (Astro: « Could not load …/node_modules/astro/… »), or a test that reads files by a path relative to the checkout, is red in a copy for reasons that have nothing to do with the sha; on 27-10 avatar-pandora and finance-tracker's ux-demo.spec were both green in the main tree and red in the copy. The commands may write their usual ignored outputs there (`dist`, caches);
293
+ - otherwise (uncommitted changes, untracked files, or another commit checked out: a wave's lots may be working in it): a throwaway worktree of the sha under the OS temporary folder (`cadence-verify-…`), removed whatever the result, where the **`node_modules` of the main tree are copied** (not linked: a copied `node_modules` is a real one for the tools that refuse a link; workspace links, relative, are copied as they are and point inside the copy) and the other ignored folders (`.venv`, data, `.env`) are linked, never copied. The line names the worktree, why the main tree was not used, and the copied dependencies (`… porte jouée dans une copie jetable /tmp/cadence-verify-x (l'arbre principal a des modifications non validées ou des fichiers non suivis) ; dépendances copiées : node_modules`). The copy of a large `node_modules` takes time: this is the price of a gate that does not lie.
294
+ - **The main tree is read again at the end of an in-place gate.** A wave integrates its lots into it by fast-forward and commits the plan there, possibly while the gate runs: if `HEAD` or `git status --porcelain` changed, the output adds `verdict non probant` (a lot may have been integrated meanwhile, or a command wrote outside the ignored folders) and the exit code is 2 whatever the result: replay it.
295
+
296
+ ```yaml
297
+ # cadence.yaml
298
+ verify:
299
+ inPlace: false # always the throwaway copy, even when the main tree is clean at the sha
300
+ # inPlace: true # the main tree or nothing: not clean at the sha → exit 2, nothing is run
301
+ ```
302
+
303
+ Absent, `verify.inPlace` chooses as above. Which dependencies are copied into the throwaway worktree:
294
304
 
295
305
  - `orchestrate.worktreeDeps: [node_modules, .venv, front/node_modules]` in `cadence.yaml`: those paths, relative to the repository
296
306
  (no absolute path, no `..`); one that does not exist in the main tree is reported and skipped;
297
- - absent: the same folders as the worktrees of a wave (the ignored folders of every level, `node_modules`, `.venv`…; build outputs such as `dist` excluded). The
298
- workspace links of a `node_modules` are made again towards the worktree, so the code run is the sha's.
307
+ - absent: the `node_modules` of every level are copied; the other ignored folders (`.venv`…, build outputs such as `dist` excluded) are linked, as in the worktrees of a wave. A bare worktree has no `node_modules` or `.venv`: on 26-10 the re-verification of a lot in one gave nine red suites that were only missing dependencies.
299
308
 
300
309
  ```
310
+ verify --gate 6b0d9aa : porte jouée dans l'arbre principal /path/app (propre, au sha)
301
311
  ✓ build (npx tsc --noEmit)
302
312
  ✗ tests (npx vitest run) — code 1
303
313
  - test/a.test.ts > R3 — …
@@ -309,5 +319,5 @@ tests the output names (the same reading as the wave's baseline) and the test fi
309
319
  With `orchestrate.testReplay` declared (`docs/orchestrate.md`, « A red test is replayed alone »), the gate does what a wave does: a red test the output names is **replayed alone, twice at most**, in the same throwaway worktree. Green at a replay it is *unstable under load*: the step prints `✓ tests (…) — N test(s) rouge(s) sous charge, verts rejoués seuls (instables sous charge) :` with the names and does not make the gate red; the tests still red stay listed and the unstable ones follow, under a line of their own. A failure the output ties to no test, or a file that does not load, is never replayed and stays red. When a replay took place and the step stays red, one line says so (`rejeu seul (orchestrate.testReplay) : N rejeu(x), dernier code C — k test(s) confirmé(s) vert(s) sur n`): a replay that fails or runs no test confirms nothing, and the line tells a real red from a replay that could not run. Without `testReplay` nothing is replayed.
310
320
  Each command has 30 minutes (then its whole process group is killed, code 124). Ctrl-C, SIGTERM or a hangup received by cadence kills the running command and removes the worktree before cadence dies.
311
321
  `--sha` is required (the moving head of a wave is not a sha to re-verify) and `--retry` does not apply. Exit code:
312
- **0** green · **1** red · **2** nothing to replay (neither `orchestrate.build` nor `orchestrate.test`), unknown sha or a worktree that cannot be made.
322
+ **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.
313
323
  The Node of the `.nvmrc` is not selected here: the commands run with the environment of the caller (`PATH` included). `CADENCE_TEST_WORKERS` is set for the build, the tests and the replays, as a wave does (`docs/orchestrate.md`, L350): the value of the caller's environment when it is a positive integer, otherwise cores ÷ session cap (`CADENCE_MAX_SESSIONS`, 2 by default), so a `testReplay` such as `--maxWorkers=$CADENCE_TEST_WORKERS` works from a plain shell.
package/docs/news.md CHANGED
@@ -201,8 +201,10 @@ else all the `##` headings. The lot is then played like any other, in an ordinar
201
201
  implementer rewrites the article in the neighbouring repository, the review checks it against the note). If a lot
202
202
  of that title is still open, it receives a note instead of a duplicate. Two entries with the same name (the same
203
203
  project told in two neighbouring repositories) share one lot, which declares both repositories in `repos:`. Internal lots (no public title) open nothing;
204
- a red delivery opens nothing; `cadence deliver --dry-run` announces the articles. The plan is written but **not
205
- committed** (`deliver` never commits): the line `article à rafraîchir : L9 …` says so. With a plan kept by another
204
+ a red delivery opens nothing; `cadence deliver --dry-run` announces the articles. Outside a wave the plan is written but **not
205
+ committed**: the line `article à rafraîchir : L9 …` says so. During a wave (`cadence deliver --sha`), the plan is committed with the files that `plan.onWrite` produces from it,
206
+ using the `plan.commit` template (`plan: L9 article à rafraîchir ouvert (orchestrate <wave>)` by default), so no plan change is left in the tree a session
207
+ of the wave works in; if the plan or one of those files already had uncommitted changes (the commit would carry them away), or the commit is refused (a `commit-msg` hook), nothing is committed, it stays written and the line says so. With a plan kept by another
206
208
  tool (read-only), the same line carries the instruction to open the lot with that tool. This replaces the manual
207
209
  `sync-site-docs` skill.
208
210
 
@@ -845,7 +845,7 @@ orchestrate:
845
845
  test: npm test # run by the orchestrator after a work step (optional)
846
846
  testReplay: npx vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS {files} # replay alone of a red test before it counts (L354, see « A red test is replayed alone »; `cadence verify --gate` applies it too, L387, `docs/deliver.md`); `{files}` required; absent: no replay, nothing held for unstable (optional)
847
847
  build: npm run build # run BEFORE the tests (L333); both results go to the reviewer, who does not redo them (optional)
848
- worktreeDeps: [node_modules, .venv] # untracked dependencies that `cadence verify --gate` links into its throwaway worktree (L348, `docs/deliver.md`); absent: the ignored folders of every level, as the worktrees of a wave (optional)
848
+ worktreeDeps: [node_modules, .venv] # untracked dependencies that `cadence verify --gate` copies into its throwaway worktree (L348, L389; the gate runs in the main tree instead when it is clean at the sha, `verify.inPlace`, `docs/deliver.md`); absent: the `node_modules` of every level are copied, the other ignored folders are linked as in the worktrees of a wave (optional)
849
849
  precheck: true # default: before the first implementation of a lot with no commit whose title cites a finding or an earlier lot, and whose title or `goal:` cites a finding (`constat`, `revue`, `review`, `audit`) or a plan id (an earlier `L<number>`, also followed by a hyphen as in `L12-L15` or `L12-bis`; any other id such as `Q-CAN5`; `project:L<number>`), a read-only Sonnet session checks whether the deliverable is already in the repository (see below); false skips it, local runs it on claude-local (Ollama) with Sonnet as fallback, for every lot with no commit whatever its title (L296); a lot created after the last code commit of the repository is not pre-checked, local or Sonnet, unless it cites a lot that already has commits (L322)
850
850
  accept: npx vitest run -c vitest.acceptation.config.ts # accept step (see « The accept step »): the dedicated command that runs ONLY the acceptance tests of the lot (optional; without it the step is skipped with a warning)
851
851
  maquette: true # default: UX review of the mockup between spec and accept (see « The mockup review »); false skips it
package/docs/whats-new.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.17.1**: `cadence deliver --sha` during a wave commits the plan it wrote, nothing else; `verify --gate` no longer turns red for reasons of its throwaway copy and says where it ran; the test suite no longer commits into the repository.
7
8
  - **1.17.0**: `cadence orchestrate` refuses a repository holding a `.git/index.lock`; `raf docs-ok` works on a plan kept by another tool; `cadence verify --gate` replays a red test alone, like the gate of a wave.
8
9
  - **1.16.1**: publishes 1.16.0 (tagged, never published), plus `raf gantt` counting commits without plan commits, one login check per `orchestrate --continue` launch, and a wider `CHANGELOG.md` conflict settled by worktree integration.
9
10
  - **1.16.0**: easier arrival for a team already equipped and for a new user: a `docs/with-your-tools.md` page, `raf --version`, no gap after `raf init`, Claude Code login checked before any write, commit counts without plan commits.
@@ -40,6 +41,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
40
41
 
41
42
  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).
42
43
 
44
+ **1.17.1**: three corrections that stop a wave from tripping over its own tools. `cadence deliver --sha` run during a wave commits the plan it wrote for the article to refresh (the `plan.commit` template, `docs/news.md`) instead of leaving it modified in the tree a session of the wave works in, which handed that lot back as « dépôt sale après implement » (L388): the commit holds that plan and the files `plan.onWrite` produces, and nothing else, so a draft or a file you were editing is never swept in; outside a wave, or when the plan or those files already had uncommitted changes, nothing changes. `cadence verify --gate` no longer turns red in its throwaway copy for reasons of the copy itself (L389, `docs/deliver.md`): when the main tree is clean and at the sha, the gate runs there; otherwise it runs in a throwaway worktree whose dependencies are copied instead of linked (links leaving the repository are re-pointed at their live target), so a project whose tests refused a linked `node_modules` or read their data from `/tmp` now gets the verdict of the real tree. The first line of the output says where the gate ran, an in-place gate whose main tree moved meanwhile answers « non probant » (exit 2), and the new `verify.inPlace` key of `cadence.yaml` forces the main tree (`true`) or the copy (`false`). Last, the test suite of cadence no longer commits into the real repository (L391): the fake `claude` of a test used to write `a.txt` and commit `feat(?): travail` there, and it now refuses to write or commit outside a temporary folder.
45
+
43
46
  **1.17.0**: fewer stops that were not the lot's fault. `cadence orchestrate` and its `--dry-run` refuse a repository holding a `.git/index.lock` before writing anything (L385, `docs/orchestrate.md`): the message gives the age of the lock, whether a git process is running, and the `rm` command to remove a stale one. A plan commit that fails (stale lock, refusing hook) now puts the plan back as it was even when the lock blocks the index, where the next lot used to be handed back as a « dépôt sale ». `raf docs-ok` no longer refuses a plan kept by another tool (read-only): the acknowledgement of a `docs.sync` gap is written to `.cadence/docs-ok.yaml` (`L12: { date, reason }`, next to the project's `cadence.yaml`), the plan is never touched, and `raf check`, `raf show`, `cadence lead tour` and `cadence orchestrate` read it; the librarian's brief names that file to commit (L386). `cadence verify --gate` applies `orchestrate.testReplay` like the gate of a wave (L387, `docs/deliver.md`): a red test the output names is replayed alone, twice at most, and a test that passes alone is reported as unstable under load instead of turning the gate red. A red step that names no test prints the last 10 lines of its output, prefixed ` | `. The gate and the replay get `CADENCE_TEST_WORKERS` like a wave's gate (the caller's value, else cores ÷ `CADENCE_MAX_SESSIONS`), so `--maxWorkers=$CADENCE_TEST_WORKERS` works from a plain shell, and a red step says that a replay took place and how many tests it confirmed green.
44
47
 
45
48
  **1.16.1**: 1.16.0 was tagged but never published: a test added with it (R2 of L377) supposed `raf` installed in the `PATH`, found it on the developer's machine and not on the runner, and the publish run went red, a fault of the test and not of the tool (L384); 1.16.1 publishes it as it stands, so everything listed under 1.16.0 below arrives with it. The test suite no longer depends on a `raf` or a `cadence` installed on the machine (L384, `docs/releasing.md`): the test now has its hook run `raf` from the sources, and the suite removes every installed `raf` and `cadence` from the `PATH` of the tests (the other tools stay), so such a test is red on the lead's machine and in the gate, not only at publication. `raf gantt` counts the commits of a lot as `raf commits` does (L381, `docs/raf.md`): plan commits no longer inflate the commit column nor the tooltip of a bar, a lot whose only commit is `plan: L1 démarré` reads 0, and the filter reads the files of the commits in one `git log` call instead of one `git diff-tree` per commit (about 20 s down to 2 s on 2,700 commits for the Gantt page); a test now holds the same count for `cadence session start` and `close`, and the test that held `raf now` to the lots it shows can now fail. With `--continue`, `cadence orchestrate` reads `claude auth status` once per launch instead of again, up to 20 s each, for every candidate lot of every round (L382, `docs/orchestrate.md`), and the message quoted in `docs/quickstart.md` and `docs/orchestrate.md` no longer nests backticks inside backticks, which broke the Markdown rendering. The worktree integration of `cadence orchestrate` also settles a `CHANGELOG.md` conflict where one lot adds a section under `[Unreleased]` (`### Fixed`) with its entry and the other a bare entry (L383, `docs/orchestrate.md`): the bare entry is filed under the section the other side added, one section title and one entry per lot, instead of handing the lot back; with no single section to file it under, the conflict stays by hand.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.17.0",
3
+ "version": "1.17.1",
4
4
  "description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvain Ladoire",
@@ -193,7 +193,7 @@ is always recorded by the lead's own `raf ux`, never by a subagent.
193
193
  2. **The lead re-verifies itself**: `git log` shows the commits, the test suite and build pass when
194
194
  run by the lead, `raf check` is clean. During a wave, or in any tree that is not the lead's own, replay the gate at the lot's last sha with
195
195
  `cadence verify --gate --sha <sha> [--project <dir>]` (exit 0 green, 1 red with the failing tests, 2 nothing to replay): a bare
196
- `git worktree add` has no `node_modules` or `.venv` and reddens suites that only miss their dependencies. A subagent is green on what it *could* test; say plainly
196
+ `git worktree add` has no `node_modules` or `.venv` and reddens suites that only miss their dependencies; the gate runs in the main tree when it is clean at the sha and in a throwaway copy with copied dependencies otherwise, and the first line of the output says which. A subagent is green on what it *could* test; say plainly
197
197
  what nobody could verify.
198
198
  3. Fix or re-delegate what the review found; then record the verdict, `raf review <lot> "…"` with
199
199
  the code reviewer's last line brought up to date (and `raf ux <lot> "…"` for a visible lot, with