@sylad/cadence 1.13.0 → 1.14.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/README.md +3 -0
- package/agents/reference-planner.md +26 -0
- package/dist/cli.js +174 -20
- package/dist/config-set.js +6 -3
- package/dist/config.js +47 -9
- package/dist/corrections.js +1 -0
- package/dist/git.js +11 -1
- package/dist/lead.js +5 -1
- package/dist/orchestrate/accept.js +63 -1
- package/dist/orchestrate/baseline.js +39 -3
- package/dist/orchestrate/changelog-merge.js +40 -1
- package/dist/orchestrate/command.js +97 -10
- package/dist/orchestrate/cycle.js +242 -63
- package/dist/orchestrate/decisions.js +2 -0
- package/dist/orchestrate/integrate.js +26 -3
- package/dist/orchestrate/launch.js +2 -0
- package/dist/orchestrate/state.js +1 -1
- package/dist/orchestrate/workers.js +51 -0
- package/dist/orchestrate/worktree.js +15 -4
- package/dist/perf.js +9 -1
- package/dist/plan-commit.js +88 -0
- package/dist/plan.js +41 -3
- package/dist/ref/analyse.js +34 -4
- package/dist/ref/command.js +2 -1
- package/dist/ref/coverage.js +136 -0
- package/dist/ref/diff.js +99 -15
- package/dist/release.js +5 -1
- package/dist/session.js +7 -1
- package/dist/stale-notes.js +22 -0
- package/dist/verify-gate.js +138 -0
- package/docs/deliver.md +37 -0
- package/docs/fonctionnement.md +2 -1
- package/docs/lead.md +5 -4
- package/docs/orchestrate.md +57 -11
- package/docs/overview.md +3 -2
- package/docs/perf.md +3 -1
- package/docs/raf.md +48 -1
- package/docs/ref.md +61 -3
- package/docs/releasing.md +5 -3
- package/docs/session.md +8 -0
- package/docs/skills.md +1 -0
- package/docs/spec.md +2 -0
- package/docs/stabilite.md +2 -0
- package/docs/whats-new.md +6 -0
- package/package.json +3 -3
- package/skills/lead/SKILL.md +6 -4
- package/skills/orchestrate/SKILL.md +1 -1
- package/skills/session-close/SKILL.md +2 -0
- package/skills/session-start/SKILL.md +2 -1
package/dist/ref/diff.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { RafError } from '../plan.js';
|
|
3
3
|
import { TYPES, checkInventaire, readInventaire } from './check.js';
|
|
4
4
|
import { affirmation, identite } from './evaluate.js';
|
|
@@ -116,13 +116,16 @@ function confianceConcordant(ea, eb) {
|
|
|
116
116
|
return { confiance: ea.confiance === 'mesure' || eb.confiance === 'mesure' ? 'mesure' : 'lu' };
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
|
-
* L'inventaire réconcilié (genre « inventaire »), ids
|
|
119
|
+
* L'inventaire réconcilié (genre « inventaire »), ids `<slug>#<type>-<n>` (renumérotés, sauf pour un élément de `precedent`) et renvois suivis. Un divergent est `incertain` avec les deux
|
|
120
120
|
* versions ; un seul écarté par le troisième regard disparaît (avec ce qui ne tient plus sans lui), un seul confirmé garde sa confiance (un doute
|
|
121
121
|
* reste un doute), un seul sans décision reste `incertain` : `regard` dit si le troisième regard a eu lieu (sa cause le dit).
|
|
122
122
|
* Un élément apparié tient tant que l'une de ses deux versions renvoie à des éléments gardés (la lecture A d'abord) : quand l'autre version renvoyait à un
|
|
123
123
|
* élément écarté, il est gardé, rattaché d'après la version qui tient, et `incertain` avec la cause du rattachement ; il disparaît quand aucune ne tient.
|
|
124
|
+
* `precedent` : l'inventaire que ce passage remplace, du même document. Un élément relu (même type, même page, même identité, zone qui recouvre le plus)
|
|
125
|
+
* y garde son id ; un élément neuf prend un numéro au-dessus du plus grand de son type, jamais celui d'un élément disparu : un lot qui cite un id
|
|
126
|
+
* continue de viser le même élément, et les `exclusions` écrites passent dans le nouvel inventaire.
|
|
124
127
|
*/
|
|
125
|
-
export function assembler(a, b, rec, regard = false) {
|
|
128
|
+
export function assembler(a, b, rec, regard = false, precedent) {
|
|
126
129
|
const entrees = rec.items.map((item) => ({ item, supprimee: item.statut === 'seul' && item.decision?.verdict === 'ecarte' }));
|
|
127
130
|
const proprietaire = new Map();
|
|
128
131
|
for (const en of entrees) {
|
|
@@ -162,16 +165,7 @@ export function assembler(a, b, rec, regard = false) {
|
|
|
162
165
|
en.version = v;
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
|
-
const
|
|
166
|
-
const nouvelId = new Map();
|
|
167
|
-
for (const en of entrees) {
|
|
168
|
-
if (en.supprimee)
|
|
169
|
-
continue;
|
|
170
|
-
const type = en.item.element.type;
|
|
171
|
-
const n = (compteur.get(type) ?? 0) + 1;
|
|
172
|
-
compteur.set(type, n);
|
|
173
|
-
nouvelId.set(en, `${a.document.slug}#${type}-${n}`);
|
|
174
|
-
}
|
|
168
|
+
const nouvelId = idsStables(entrees, a, b, precedent);
|
|
175
169
|
const elements = entrees.filter((en) => !en.supprimee).map((en) => {
|
|
176
170
|
const { item } = en;
|
|
177
171
|
const v = en.version;
|
|
@@ -213,7 +207,43 @@ export function assembler(a, b, rec, regard = false) {
|
|
|
213
207
|
}
|
|
214
208
|
return e;
|
|
215
209
|
});
|
|
216
|
-
|
|
210
|
+
const exclusions = memeDocument(precedent, a) ? precedent.exclusions : undefined;
|
|
211
|
+
return { version: 1, genre: 'inventaire', document: { slug: a.document.slug, sha256: a.document.sha256 }, elements, ...(exclusions !== undefined ? { exclusions } : {}) };
|
|
212
|
+
}
|
|
213
|
+
const memeDocument = (precedent, a) => (precedent?.genre === 'inventaire' || precedent?.genre === 'lecture') && precedent.document?.slug === a.document.slug && Array.isArray(precedent.elements);
|
|
214
|
+
/** Les ids du nouvel inventaire : ceux de `precedent` pour les éléments relus, des numéros neufs pour les autres (ordre de l'inventaire, numérotation par type). */
|
|
215
|
+
function idsStables(entrees, a, b, precedent) {
|
|
216
|
+
const slug = a.document.slug;
|
|
217
|
+
const anciens = memeDocument(precedent, a) ? precedent.elements : [];
|
|
218
|
+
const parAncien = new Map(anciens.map((e) => [e.id, e]));
|
|
219
|
+
const parA = new Map(a.elements.map((e) => [e.id, e]));
|
|
220
|
+
const parB = new Map(b.elements.map((e) => [e.id, e]));
|
|
221
|
+
const reprise = new Set();
|
|
222
|
+
const nouvelId = new Map();
|
|
223
|
+
for (const en of entrees.filter((x) => !x.supprimee)) {
|
|
224
|
+
const e = en.item.element;
|
|
225
|
+
const par = en.item.a ? parA : parB;
|
|
226
|
+
const libres = anciens.filter((p) => !reprise.has(p) && p.type === e.type && p.source?.page === e.source?.page && identite(p, parAncien) === identite(e, par));
|
|
227
|
+
const ancien = libres.reduce((best, c) => (best === undefined || recouvrement(e, c) > recouvrement(e, best) ? c : best), undefined);
|
|
228
|
+
if (ancien) {
|
|
229
|
+
reprise.add(ancien);
|
|
230
|
+
nouvelId.set(en, ancien.id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const compteur = new Map();
|
|
234
|
+
for (const e of anciens) {
|
|
235
|
+
const n = Number(String(e.id).slice(String(e.id).lastIndexOf('-') + 1));
|
|
236
|
+
compteur.set(e.type, Math.max(compteur.get(e.type) ?? 0, Number.isInteger(n) ? n : 0));
|
|
237
|
+
}
|
|
238
|
+
for (const en of entrees) {
|
|
239
|
+
if (en.supprimee || nouvelId.has(en))
|
|
240
|
+
continue;
|
|
241
|
+
const type = en.item.element.type;
|
|
242
|
+
const n = (compteur.get(type) ?? 0) + 1;
|
|
243
|
+
compteur.set(type, n);
|
|
244
|
+
nouvelId.set(en, `${slug}#${type}-${n}`);
|
|
245
|
+
}
|
|
246
|
+
return nouvelId;
|
|
217
247
|
}
|
|
218
248
|
function lireLecture(path) {
|
|
219
249
|
const doc = readInventaire(path);
|
|
@@ -232,6 +262,60 @@ export function parseSeuil(text, commande) {
|
|
|
232
262
|
throw new RafError(`${commande} : --seuil invalide : « ${text} » (une part en pour cent, de 0 à 100)`);
|
|
233
263
|
return Number(text) / 100;
|
|
234
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Ce qu'un passage de `ref analyse` remplace et dont il reprend les ids : l'inventaire du dossier du document, sinon sa lecture `lectures/a.json`
|
|
267
|
+
* (une lecture unique ou la lecture A d'une double), du moment qu'elle est valide ; sinon rien.
|
|
268
|
+
*/
|
|
269
|
+
export function precedentDe(inventaire, lecture) {
|
|
270
|
+
const inv = inventairePrecedent(inventaire);
|
|
271
|
+
if (inv)
|
|
272
|
+
return inv;
|
|
273
|
+
if (!existsSync(lecture))
|
|
274
|
+
return undefined;
|
|
275
|
+
try {
|
|
276
|
+
const doc = readInventaire(lecture);
|
|
277
|
+
return doc.genre === 'lecture' && checkInventaire(doc).length === 0 ? doc : undefined;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Une lecture seule (`ref analyse` sans `--double`) numérote ses éléments par son propre ordre : ceux qui relisent un élément de `precedent` (même règle
|
|
285
|
+
* que `assembler`) reprennent son id, les renvois suivent, et les `exclusions` écrites passent dans la lecture. Sans `precedent` du même document, rien ne change.
|
|
286
|
+
*/
|
|
287
|
+
export function reprendreIds(lecture, precedent) {
|
|
288
|
+
if (!memeDocument(precedent, lecture))
|
|
289
|
+
return;
|
|
290
|
+
const elements = lecture.elements;
|
|
291
|
+
const entrees = elements.map((e) => ({ item: { statut: 'concordant', element: e, a: e }, supprimee: false }));
|
|
292
|
+
const nouvelId = idsStables(entrees, lecture, lecture, precedent);
|
|
293
|
+
const ancienVersNeuf = new Map(entrees.map((en) => [en.item.element.id, nouvelId.get(en)]));
|
|
294
|
+
for (const e of elements) {
|
|
295
|
+
if (e.type === 'composant')
|
|
296
|
+
e.ecran = ancienVersNeuf.get(e.ecran) ?? e.ecran;
|
|
297
|
+
if (e.type === 'etat')
|
|
298
|
+
e.cible = ancienVersNeuf.get(e.cible) ?? e.cible;
|
|
299
|
+
if (e.type === 'statut')
|
|
300
|
+
e.couvre = e.couvre.map((id) => ancienVersNeuf.get(id) ?? id);
|
|
301
|
+
}
|
|
302
|
+
for (const e of elements)
|
|
303
|
+
e.id = ancienVersNeuf.get(e.id);
|
|
304
|
+
if (precedent.exclusions !== undefined)
|
|
305
|
+
lecture.exclusions = precedent.exclusions;
|
|
306
|
+
}
|
|
307
|
+
/** L'inventaire que `--out` va remplacer, s'il en est un de valide ; sinon rien (un fichier illisible ou d'une autre forme n'est pas un inventaire précédent). */
|
|
308
|
+
export function inventairePrecedent(path) {
|
|
309
|
+
if (!existsSync(path))
|
|
310
|
+
return undefined;
|
|
311
|
+
try {
|
|
312
|
+
const doc = readInventaire(path);
|
|
313
|
+
return doc.genre === 'inventaire' && checkInventaire(doc).length === 0 ? doc : undefined;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
235
319
|
/** `ref diff <a> <b> [--out inventaire.json] [--seuil 30]` : 0 réconcilié, 1 trop d'éléments non appariés (rien d'écrit), 2 fichier ou appel refusé. */
|
|
236
320
|
export function diffLectures(o) {
|
|
237
321
|
const seuil = parseSeuil(o.seuil, 'ref diff');
|
|
@@ -257,6 +341,6 @@ export function diffLectures(o) {
|
|
|
257
341
|
return 1;
|
|
258
342
|
}
|
|
259
343
|
if (o.out !== undefined)
|
|
260
|
-
writeFileSync(o.out, `${JSON.stringify(assembler(a, b, rec), null, 2)}\n`);
|
|
344
|
+
writeFileSync(o.out, `${JSON.stringify(assembler(a, b, rec, false, inventairePrecedent(o.out)), null, 2)}\n`);
|
|
261
345
|
return 0;
|
|
262
346
|
}
|
package/dist/release.js
CHANGED
|
@@ -204,7 +204,11 @@ function steps(p, files, branch) {
|
|
|
204
204
|
`git push origin ${branch} ${tag} (le tag lance le workflow de publication)`,
|
|
205
205
|
'gh run watch (jusqu\'à la fin du run de publication)',
|
|
206
206
|
...(p.isPrivate || p.packageName === '' ? [] : [`npm view ${p.packageName} version (doit répondre ${p.version})`]),
|
|
207
|
-
...(p.pluginName ? [
|
|
207
|
+
...(p.pluginName ? [
|
|
208
|
+
...(p.marketplaceName ? [`claude plugin marketplace update ${p.marketplaceName} (sans elle, le plugin installé reste à l'ancienne version)`] : []),
|
|
209
|
+
`claude plugin update ${p.pluginName}${p.marketplaceName ? `@${p.marketplaceName}` : ''} (le plugin se lit depuis le dépôt poussé)`,
|
|
210
|
+
'/reload-plugins (dans la session Claude Code ouverte, pour charger la nouvelle version)',
|
|
211
|
+
] : []),
|
|
208
212
|
];
|
|
209
213
|
return out.map((s, i) => ` ${i + 1}. ${s}`);
|
|
210
214
|
}
|
package/dist/session.js
CHANGED
|
@@ -5,6 +5,7 @@ import { audit, exemptPlanOnly, gateWaiting, nextUp, planCommits } from './audit
|
|
|
5
5
|
import { diffDays, maxDay } from './dates.js';
|
|
6
6
|
import { repoStatus } from './git.js';
|
|
7
7
|
import { linkCommits } from './link.js';
|
|
8
|
+
import { staleNoteLines, staleNotes, staleText } from './stale-notes.js';
|
|
8
9
|
import { lockAlive, readLock, readNext, readSessionMark, writeSessionMark } from './state.js';
|
|
9
10
|
const FACTS_TIMEOUT = 120_000;
|
|
10
11
|
/**
|
|
@@ -107,7 +108,7 @@ export function sessionStart(ctx, opts) {
|
|
|
107
108
|
out(`${plan.project} — reprise du ${today}`);
|
|
108
109
|
const next = readNext(ctx.state);
|
|
109
110
|
if (next)
|
|
110
|
-
section(out, `Notes de la dernière clôture (${next.date})`, next.lines.map((l) => `- ${l}`));
|
|
111
|
+
section(out, `Notes de la dernière clôture (${next.date})`, staleNoteLines(next.lines, lots).map((l) => `- ${l}`));
|
|
111
112
|
const lock = lockStatus(ctx.shared);
|
|
112
113
|
if (lock)
|
|
113
114
|
out(`\n${lock.line}`);
|
|
@@ -152,6 +153,11 @@ export function sessionClose(ctx, opts) {
|
|
|
152
153
|
commits.push(`${recent.orphans.length} commit(s) sans lot :`, ...recent.orphans.map((c) => ` ${short(c)}`));
|
|
153
154
|
}
|
|
154
155
|
section(out, `Commits de la période (${window.label})`, commits.length ? commits : ['(aucun)']);
|
|
156
|
+
const next = readNext(ctx.state);
|
|
157
|
+
if (next) {
|
|
158
|
+
const stale = staleNotes(next.lines, plan.lots()).filter((n) => n.stale.length);
|
|
159
|
+
section(out, `Notes de la dernière clôture à réécrire (${next.date})`, stale.map((n) => `${n.line} ${staleText(n.stale)} — cadence session next les remplace`));
|
|
160
|
+
}
|
|
155
161
|
const quiet = plan.lots().filter((l) => l.status === 'doing' && !isRecurring(l) && !recent.byLot.has(l.id));
|
|
156
162
|
section(out, 'Lots en cours', quiet.map((l) => `${l.id} ${l.title} — aucun commit sur la période : ${plan.readonly ? "le fermer ou l'annoter avec l'outil du projet" : 'raf done ou raf note'}`));
|
|
157
163
|
const issues = audit(plan, ctx.root, ctx.newsDir, today);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2
|
+
/** « 2026-10-24 » → « 24-10 » : la note est récente, l'année ne se lit pas. */
|
|
3
|
+
const dayMonth = (d) => `${d.slice(8, 10)}-${d.slice(5, 7)}`;
|
|
4
|
+
/**
|
|
5
|
+
* Pour chaque ligne de note, les lots `done` ou `dropped` qu'elle cite, dans l'ordre de la ligne. Un identifiant se
|
|
6
|
+
* lit entier : `L2` n'est pas cité par `L217`, ni `B28` par `B28.8`, mais un point final (`reprendre L2.`) ne le prolonge pas.
|
|
7
|
+
*/
|
|
8
|
+
export function staleNotes(lines, lots) {
|
|
9
|
+
const finished = lots.filter((l) => l.status === 'done' || l.status === 'dropped');
|
|
10
|
+
return lines.map((line) => {
|
|
11
|
+
const hits = finished
|
|
12
|
+
.map((l) => ({ l, at: line.search(new RegExp(`(?<![\\w./-])${escape(l.id)}(?![\\w/-]|\\.\\w)`)) }))
|
|
13
|
+
.filter((h) => h.at >= 0)
|
|
14
|
+
.sort((a, b) => a.at - b.at);
|
|
15
|
+
return { line, stale: hits.map(({ l }) => `${l.id} ${l.status}${l.finished ? ` le ${dayMonth(l.finished)}` : ''}`) };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export const staleText = (stale) => `⚠ note périmée : ${stale.join(', ')}`;
|
|
19
|
+
/** Les notes telles quelles, celles qui citent un lot fini suivies de « ⚠ note périmée : … ». */
|
|
20
|
+
export function staleNoteLines(lines, lots) {
|
|
21
|
+
return staleNotes(lines, lots).map((n) => (n.stale.length ? `${n.line} ${staleText(n.stale)}` : n.line));
|
|
22
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { lstatSync, mkdtempSync, rmSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { onTermination } from './proc.js';
|
|
6
|
+
import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
|
|
7
|
+
import { git, gitFailure, linkDeclared, linkIgnored } from './orchestrate/worktree.js';
|
|
8
|
+
/** Délai de chaque commande de la porte : celui du rejeu de la ligne de base de la vague. */
|
|
9
|
+
const GATE_TIMEOUT_MS = 30 * 60_000;
|
|
10
|
+
/** Lignes de la fin de la sortie montrées quand aucun test n'est identifiable dans une sortie rouge. */
|
|
11
|
+
const TAIL_LINES = 10;
|
|
12
|
+
function killGroup(pgid) {
|
|
13
|
+
try {
|
|
14
|
+
process.kill(-pgid, 'SIGKILL');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// groupe déjà vide
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Une commande `sh -c` dans `cwd`, sortie standard et d'erreur réunies ; au délai, tout son groupe de processus est tué.
|
|
22
|
+
* `group` reçoit le groupe en cours : le nettoyage d'un signal reçu par cadence le tue avant de retirer le worktree.
|
|
23
|
+
*/
|
|
24
|
+
function sh(cmd, cwd, timeoutMs, group) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const child = spawn('sh', ['-c', cmd], { cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
|
|
27
|
+
const pgid = child.pid;
|
|
28
|
+
if (pgid === undefined) {
|
|
29
|
+
child.once('error', (e) => resolve({ code: 127, output: e.message }));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
group.pgid = pgid;
|
|
33
|
+
let output = '';
|
|
34
|
+
for (const stream of [child.stdout, child.stderr])
|
|
35
|
+
stream.on('data', (d) => (output += d.toString()));
|
|
36
|
+
let timedOut = false;
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
timedOut = true;
|
|
39
|
+
killGroup(pgid);
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
child.once('close', (code) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
killGroup(pgid); // ce que la commande a laissé en arrière-plan ne survit pas à elle
|
|
44
|
+
group.pgid = undefined;
|
|
45
|
+
resolve({ code: timedOut ? 124 : (code ?? 1), output: timedOut ? `${output}\ndélai dépassé (${Math.round(timeoutMs / 60_000)} min)` : output });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `cadence verify --gate` (L348) : la porte du projet (`orchestrate.build`, puis `orchestrate.test`, dans l'ordre de la vague : les tests lisent ce que le build produit)
|
|
51
|
+
* rejouée au `sha` dans un worktree jetable, hors du dépôt, dont les dépendances non suivies sont reliées depuis le dépôt vivant (`orchestrate.worktreeDeps`,
|
|
52
|
+
* 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.
|
|
53
|
+
* Le worktree est retiré dans tous les cas. 0 vert, 1 rouge (avec les tests en échec), 2 rien à rejouer ou worktree impossible.
|
|
54
|
+
*/
|
|
55
|
+
export async function verifyGate(ctx) {
|
|
56
|
+
const { repo, sha, config, out } = ctx;
|
|
57
|
+
const steps = [
|
|
58
|
+
...(config.build ? [{ name: 'build', cmd: config.build }] : []),
|
|
59
|
+
...(config.test ? [{ name: 'tests', cmd: config.test }] : []),
|
|
60
|
+
];
|
|
61
|
+
const short = sha.slice(0, 7);
|
|
62
|
+
if (steps.length === 0) {
|
|
63
|
+
out('verify --gate : ni orchestrate.test ni orchestrate.build déclarés dans cadence.yaml — rien à rejouer');
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
const dir = mkdtempSync(join(tmpdir(), 'cadence-verify-'));
|
|
67
|
+
const links = [];
|
|
68
|
+
const group = {};
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
if (group.pgid !== undefined)
|
|
71
|
+
killGroup(group.pgid);
|
|
72
|
+
for (const k of links)
|
|
73
|
+
if (lstatSync(k, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
74
|
+
unlinkSync(k);
|
|
75
|
+
try {
|
|
76
|
+
git(repo, 'worktree', 'remove', '--force', dir);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
rmSync(dir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
git(repo, 'worktree', 'prune');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// rien à élaguer
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// Ctrl-C, SIGTERM ou raccrochage reçu par cadence : la commande est tuée et le worktree retiré avant de mourir du signal.
|
|
89
|
+
const forget = onTermination(cleanup);
|
|
90
|
+
try {
|
|
91
|
+
try {
|
|
92
|
+
git(repo, 'worktree', 'add', '--detach', '-f', dir, sha);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
out(`verify --gate : worktree de ${short} impossible — ${gitFailure(e)}`);
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
if (config.worktreeDeps) {
|
|
99
|
+
const linked = linkDeclared(repo, dir, config.worktreeDeps);
|
|
100
|
+
links.push(...linked.links);
|
|
101
|
+
if (linked.missing.length)
|
|
102
|
+
out(`dépendances déclarées absentes de l'arbre principal, non liées : ${linked.missing.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
links.push(...linkIgnored(repo, dir));
|
|
106
|
+
}
|
|
107
|
+
let red = 0;
|
|
108
|
+
for (const step of steps) {
|
|
109
|
+
const r = await sh(step.cmd, dir, ctx.timeoutMs ?? GATE_TIMEOUT_MS, group);
|
|
110
|
+
if (r.code === 0) {
|
|
111
|
+
out(`✓ ${step.name} (${step.cmd})`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
red++;
|
|
115
|
+
out(`✗ ${step.name} (${step.cmd}) — code ${r.code}`);
|
|
116
|
+
const tests = step.name === 'tests';
|
|
117
|
+
const names = tests ? failedTests(r.output) : [];
|
|
118
|
+
const unloaded = tests ? unloadedSuites(r.output) : [];
|
|
119
|
+
for (const n of names)
|
|
120
|
+
out(` - ${n}`);
|
|
121
|
+
for (const n of unloaded)
|
|
122
|
+
out(` - ${n} (ne se charge pas)`);
|
|
123
|
+
if (names.length + unloaded.length === 0 || hasUnnamedFailure(r.output, true)) {
|
|
124
|
+
if (names.length + unloaded.length)
|
|
125
|
+
out(' (au moins un échec que la sortie ne rattache à aucun test)');
|
|
126
|
+
if (r.output.trim())
|
|
127
|
+
for (const l of r.output.trimEnd().split('\n').slice(-TAIL_LINES))
|
|
128
|
+
out(` | ${l}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
out(red === 0 ? `verify --gate ${short} : vert` : `verify --gate ${short} : rouge (${red} étape${red > 1 ? 's' : ''} sur ${steps.length})`);
|
|
132
|
+
return red === 0 ? 0 : 1;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
forget();
|
|
136
|
+
cleanup();
|
|
137
|
+
}
|
|
138
|
+
}
|
package/docs/deliver.md
CHANGED
|
@@ -273,3 +273,40 @@ everything is green, the red effects and the summary otherwise. It is a fact lik
|
|
|
273
273
|
the others: it never changes the exit code, and an unreachable network shows up
|
|
274
274
|
as red lines ("erreur réseau") without blocking the session. No section for a
|
|
275
275
|
project without `deliver.verify`.
|
|
276
|
+
|
|
277
|
+
## verify --gate: replay the project's gate at a sha (L348)
|
|
278
|
+
|
|
279
|
+
`cadence verify` above replays the *effect* of a delivery. `cadence verify --gate --sha <sha>`
|
|
280
|
+
replays the *gate* of the project, `orchestrate.build` then `orchestrate.test` of `cadence.yaml`
|
|
281
|
+
(the order of the wave's gate: the tests read what the build produces), at one commit, for the
|
|
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.
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
cadence verify --gate --sha 6b0d9aa # in the project
|
|
287
|
+
cadence verify --gate --sha 6b0d9aa --project app # from the parent folder, the project `app` (its own git repository)
|
|
288
|
+
```
|
|
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:
|
|
294
|
+
|
|
295
|
+
- `orchestrate.worktreeDeps: [node_modules, .venv, front/node_modules]` in `cadence.yaml`: those paths, relative to the repository
|
|
296
|
+
(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.
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
✓ build (npx tsc --noEmit)
|
|
302
|
+
✗ tests (npx vitest run) — code 1
|
|
303
|
+
- test/a.test.ts > R3 — …
|
|
304
|
+
verify --gate 6b0d9aa : rouge (1 étape sur 2)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Both commands always run, even when the first is red, so the report is whole. A red step lists the failing
|
|
308
|
+
tests the output names (the same reading as the wave's baseline) and the test files that do not load (`- test/a.test.ts (ne se charge pas)`, the typical sign of a missing dependency); with none identifiable, the last 10 lines of the output.
|
|
309
|
+
Each command has 30 minutes (then its whole process group is killed, code 124). Ctrl-C, SIGTERM or a hangup received by cadence kills the running command and removes the worktree before cadence dies.
|
|
310
|
+
`--sha` is required (the moving head of a wave is not a sha to re-verify) and `--retry` does not apply. Exit code:
|
|
311
|
+
**0** green · **1** red · **2** nothing to replay (neither `orchestrate.build` nor `orchestrate.test`), unknown sha or a worktree that cannot be made.
|
|
312
|
+
The Node of the `.nvmrc` is not selected here, and `CADENCE_TEST_WORKERS` is not set: the commands run with the environment of the caller (`PATH` included), so a re-verification run during a wave should export the session's `CADENCE_TEST_WORKERS` itself, or vitest takes every core.
|
package/docs/fonctionnement.md
CHANGED
|
@@ -100,7 +100,8 @@ The final review is always played; the budget bounds the writing passes. A `scre
|
|
|
100
100
|
- Never two sessions in the same repository at the same time. (The worktree mode, `orchestrate.worktrees: true` — on in cadence's own `cadence.yaml`, off where the key is absent — lets two independent lots of one repository play at once, each in its own worktree — [docs/orchestrate.md](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279).)
|
|
101
101
|
- A session cannot ask the human anything: it stops and reports, and the lead brings the question back.
|
|
102
102
|
- A sealed acceptance test that contradicts its sheet is corrected by the lead with
|
|
103
|
-
`cadence spec reseal <lot> <test> --reason "…"`, never by editing `acceptation.json
|
|
103
|
+
`cadence spec reseal <lot> <test> --reason "…"`, never by editing `acceptation.json`; a lot closed by hand
|
|
104
|
+
and left at `vert: false` is put back to green with `cadence spec green <lot>` (L366).
|
|
104
105
|
- Deliveries and the `raf ux` / `raf review` verdicts belong to the lead, one project at a time.
|
|
105
106
|
|
|
106
107
|
See also the [orchestrate](orchestrate.md#orchestrate) page and the
|
package/docs/lead.md
CHANGED
|
@@ -85,6 +85,7 @@ picture, not its source.
|
|
|
85
85
|
| At most **as many subagents as the session cap** running at once (2 by default; the human sets it with `CADENCE_MAX_SESSIONS`), orchestrate sessions included | `cadence orchestrate --status` and `cadence lead tour` print `créneaux libres : N sur M`, M being the configured cap and the sessions the lead started by hand (`cadence lead hold`, L255) being subtracted; the lead never raises `--max-sessions` or `CADENCE_MAX_SESSIONS` itself |
|
|
86
86
|
| Never two sessions in the same repository at the same time; the lead does not commit where a wave runs (with `orchestrate.worktrees: true` two lots of one repository play at once, each in its own worktree: not a rule for the lead) | a wave's lock refuses `cadence deliver` while a lot still works there |
|
|
87
87
|
| A locked acceptance test that contradicts the sheet is corrected by the lead with `cadence spec reseal <lot> <test> --reason "…"` (lot in progress, outside a wave and with no wave that is not `done` keeping open a lot of the repository still `doing` in its plan, the resealed lot or another, e.g. a question waiting: `--resume --wave <wave> --answer` or `--drop` first, the one error lists every wave concerned, the live one that holds the lock (to wait for) and the stopped ones, and gives for each stopped wave the single `cd <launch folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> [--drop …]` to run, a `--drop` per open lot of the repository in that wave, the queued ones that are not `doing` in the plan included, since `--resume` would relaunch them and they would take the lock back, L238 L239; an amended sheet is accepted once committed, every commit that touched it citing the lot, L217, never put it back to the red state; a delivered lot with an unstable test: add `--porteur <lot in progress>`, L212), then a new `cadence orchestrate <project>:<lot>` for a lot handed back (`--resume` skips it) | never by a session, never by editing `acceptation.json` |
|
|
88
|
+
| A lot closed by hand (`raf done`) and left at `vert: false` in its `acceptation-<lot>.json` is put back to green by the lead with `cadence spec green <lot>` (L366, GitHub issue #18): the program runs the lot's acceptance command and, green, sets `vert: true`, reseals the listed tests refined since the red commit and commits the file alone, citing the lot; red, nothing changes. `spec reseal` does not cover it (a lot `doing`, or `vert: true` with `--porteur`). `raf done` refuses such a lot without `--force`, and `cadence orchestrate --dry-run` prints `acceptation rouge d'un lot clos` before the wave starts | closed like that it keeps the lock total on its tests, and every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them; never by editing `acceptation.json` |
|
|
88
89
|
| Deliveries and the `raf ux` / `raf review` verdicts belong to the lead, one project at a time | never to a subagent, never two deliveries in parallel |
|
|
89
90
|
| The UX verdict is the human's unless `lead.ux: lead` (parent `cadence.yaml`, default `human`) | then the lead decides from the report and the captures, and brings any doubt to the human |
|
|
90
91
|
| A session cannot ask the human anything | it stops and reports; the lead brings the question back |
|
|
@@ -136,9 +137,9 @@ cadence config set orchestrate.build 'npx tsc --noEmit' --all
|
|
|
136
137
|
|
|
137
138
|
- `<key>` is a dotted path (`orchestrate.escalation`); `<value>` is read as YAML, so `true`, a command, or a flow mapping like `{ model: fable }` all work. A key already at that value is left alone.
|
|
138
139
|
- The rest of the file is kept as it is, byte for byte — comments, key order, blank lines, inline lists (`[a, b]`) and mappings: only the lines of the key change (a new key goes at the end of its parent; a value replaced in place keeps its end-of-line comment). The only exception is a file whose rewrite by the YAML library moves lines other than the key's (its line count changes, a comment lands on another line, or the lines carried over would not read back as the same document): then the whole file is rewritten in the library's style. A project with no `cadence.yaml` gets one.
|
|
139
|
-
- In each project it creates a **config lot** that quotes the key (`config cadence.yaml : <key> = <value>`, estimate 0.05), as `raf add` then `raf start` would, and commits `cadence.yaml
|
|
140
|
-
- The new text is read back with cadence's own config loaders (plan, news, hook, perf, session, orchestrate, docs, references, migrate, lead.ux, depth, priority, deliver) before anything is written. A `cadence.yaml` that does not read back — before the change or after it — **stops the run** (exit code 1) at that project, with nothing written there; the projects after it are named `non traités`. A commit the repository refuses (a hook) stops it the same way, the
|
|
141
|
-
- A project is **skipped**, with the reason, when: it is not its own git repository; the key is already at that value; the key cannot be set in its file (a plain value in the way); `cadence.yaml
|
|
140
|
+
- In each project it creates a **config lot** that quotes the key (`config cadence.yaml : <key> = <value>`, estimate 0.05), as `raf add` then `raf start` would, and commits `cadence.yaml`, the plan file and, when the project names `plan.onWrite`, the files that command produces (it runs when the plan is written; see `docs/raf.md`) with explicit paths (`chore(L12): cadence.yaml — <key> = <value>`): nothing else in the index or the working tree goes in. The lot is left `doing`: closing it (`raf done`) stays the lead's step. **Nothing is pushed.**
|
|
141
|
+
- The new text is read back with cadence's own config loaders (plan, news, hook, perf, session, orchestrate, docs, references, migrate, lead.ux, depth, priority, deliver) before anything is written. A `cadence.yaml` that does not read back — before the change or after it — **stops the run** (exit code 1) at that project, with nothing written there; the projects after it are named `non traités`. A commit the repository refuses (a hook) or a `plan.onWrite` command that fails stops it the same way (`lot ou commit impossible`), `cadence.yaml`, the plan file and the produced files put back as they were (a produced file HEAD does not know is deleted).
|
|
142
|
+
- A project is **skipped**, with the reason, when: it is not its own git repository; the key is already at that value; the key cannot be set in its file (a plain value in the way); `cadence.yaml`, the plan file or a file named by `plan.onWrite.files` has uncommitted changes (the explicit-path commit would take them); or its plan is read-only (kept by another tool) and `cadence.yaml` names no creation command. A read-only plan that names one with `plan.add` (a shell command containing `{title}`, replaced by the quoted title of the config lot, e.g. `plan: { path: taches.yaml, lots: taches, add: 'mytool add {title}' }`) gets its config lot from that command, run in the project folder; the lot id is the first id-like word of its output (the commit subject has none when there is none), and `cadence.yaml` and the plan file are committed together. The command alone decides the lot's status.
|
|
142
143
|
- `--dry-run` prints the same lines, prefixed `+ … serait fait`, and writes and commits nothing. The last line counts what was done and skipped. Without `--all` the command refuses (run `raf`/an editor inside the project for a single one).
|
|
143
144
|
|
|
144
145
|
## lead tour
|
|
@@ -158,7 +159,7 @@ beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second
|
|
|
158
159
|
|---|---|
|
|
159
160
|
| `en cours` | ids of the lots in progress, each with its number of commits (`0 commit` included: a close note on a lot without commit is not « done, `raf done` forgotten »); then `aucune activité` when it has no activity at all, `silencieux Nj` when the lot's last activity (commit, note or start) is more than `--idle` days old (default 3) |
|
|
160
161
|
| `dérive` | the gaps `raf check` reports (a lot whose commits leave a document behind, [`docs.sync`](news.md#documentation-that-follows-the-code), included), counted and the first two shown |
|
|
161
|
-
| `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters |
|
|
162
|
+
| `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters; when one quotes a lot already `done` or `dropped` (L347), ` ⚠ note périmée : L217 done le 24-10` follows (not cut) — relay it as stale, never as work to do (`--json`: `staleNotes`, absent when none is stale) |
|
|
162
163
|
| `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters; a lot whose [`gate`](raf.md#plan-file) is not met is not ready |
|
|
163
164
|
| `attend` | only when a lot waits on its `gate`: `attend L190` |
|
|
164
165
|
| `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
|