@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli.js +4 -4
- package/dist/config-set.js +1 -1
- package/dist/config.js +25 -0
- package/dist/deliver.js +25 -6
- package/dist/git.js +3 -1
- package/dist/orchestrate/cycle.js +1 -1
- package/dist/orchestrate/worktree.js +44 -8
- package/dist/plan-commit.js +3 -0
- package/dist/verify-gate.js +116 -65
- package/docs/deliver.md +19 -9
- package/docs/news.md +4 -2
- package/docs/orchestrate.md +1 -1
- package/docs/whats-new.md +3 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
|
@@ -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.
|
|
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.
|
|
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, ...
|
|
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) {
|
package/dist/config-set.js
CHANGED
|
@@ -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, ...
|
|
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.
|
|
539
|
-
* une
|
|
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 (
|
|
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 (
|
|
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 })), ...
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
*
|
|
131
|
-
*
|
|
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
|
|
134
|
-
const
|
|
135
|
-
|
|
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 = [];
|
package/dist/plan-commit.js
CHANGED
|
@@ -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})`,
|
package/dist/verify-gate.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { lstatSync, mkdtempSync, rmSync
|
|
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,
|
|
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
|
|
54
|
-
* sinon
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
in one
|
|
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
|
|
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
|
|
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.
|
|
205
|
-
committed
|
|
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
|
|
package/docs/orchestrate.md
CHANGED
|
@@ -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`
|
|
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.
|
|
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",
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -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
|