@sylad/cadence 1.23.0 → 1.25.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/agents/code-reviewer.md +17 -0
- package/agents/qa-reviewer.md +17 -0
- package/dist/cli.js +69 -19
- package/dist/gate-record.js +34 -8
- package/dist/gate-verdict.js +46 -0
- package/dist/git.js +31 -1
- package/dist/lead.js +4 -1
- package/dist/orchestrate/command.js +224 -131
- package/dist/orchestrate/guard.js +34 -2
- package/dist/orchestrate/integrate.js +6 -9
- package/dist/orchestrate/registry.js +6 -4
- package/dist/orchestrate/table.js +60 -28
- package/dist/perf.js +24 -2
- package/dist/plan.js +25 -2
- package/dist/push-guard.js +50 -1
- package/dist/release.js +11 -0
- package/dist/verify-gate.js +32 -5
- package/docs/cadence-hud.md +8 -0
- package/docs/corrections.md +1 -0
- package/docs/deliver.md +12 -3
- package/docs/lead.md +11 -4
- package/docs/orchestrate.md +10 -8
- package/docs/perf.md +3 -1
- package/docs/raf.md +4 -2
- package/docs/releasing.md +9 -3
- package/docs/skills.md +6 -0
- package/docs/whats-new.md +9 -0
- package/package.json +1 -1
- package/skills/deliver/SKILL.md +8 -0
- package/skills/lead/SKILL.md +26 -1
- package/templates/orchestrate/implement.md +6 -1
|
@@ -194,7 +194,59 @@ export function resumeCommand(waveId, ...extra) {
|
|
|
194
194
|
export function resumeAnswerHint(waveId, l) {
|
|
195
195
|
return resumeCommand(waveId, '--answer', `${l.project}:${l.lot}`, '"…"');
|
|
196
196
|
}
|
|
197
|
-
/**
|
|
197
|
+
/** Les lignes d'un lot qui restent dans le tableau compact : une escalade et les avertissements sont des signaux, pas du détail. */
|
|
198
|
+
function signalLines(l) {
|
|
199
|
+
const out = [];
|
|
200
|
+
if (l.escalation)
|
|
201
|
+
out.push(`${l.project}:${l.lot} — escalade ${l.escalation.model} : ${escalationText(l)}`);
|
|
202
|
+
for (const w of l.warnings)
|
|
203
|
+
out.push(`${l.project}:${l.lot} — ⚠ ${w}`);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
/** Le détail d'un lot (L441) : ce que le tableau compact de `--status` n'imprime pas et que `--status --detail <lot>` donne. */
|
|
207
|
+
function detailLines(l) {
|
|
208
|
+
const out = [];
|
|
209
|
+
if (l.goal)
|
|
210
|
+
out.push(`${l.project}:${l.lot} — objectif donné : ${l.goal}`);
|
|
211
|
+
if (l.uxNote)
|
|
212
|
+
out.push(`${l.project}:${l.lot} — ${l.uxNote}`);
|
|
213
|
+
if (l.uxCaptures)
|
|
214
|
+
out.push(`${l.project}:${l.lot} — captures Playwright : ${l.uxCaptures}`);
|
|
215
|
+
// Ligne de base des tests (L213) : les échecs déjà présents au sha de départ, à part de ceux du lot.
|
|
216
|
+
const base = l.baseline;
|
|
217
|
+
if (base?.failures.length) {
|
|
218
|
+
const where = `${base.sha ? ` (${base.sha.slice(0, 7)}${base.verified ? '' : ', annoncé par la session, non vérifié'})` : ''}`;
|
|
219
|
+
for (const t of base.failures.slice(0, BASELINE_LISTED))
|
|
220
|
+
out.push(`${l.project}:${l.lot} — déjà rouge au départ${where} : ${t}`);
|
|
221
|
+
if (base.failures.length > BASELINE_LISTED)
|
|
222
|
+
out.push(`${l.project}:${l.lot} — déjà rouge au départ${where} : … et ${base.failures.length - BASELINE_LISTED} autre(s)`);
|
|
223
|
+
}
|
|
224
|
+
for (const x of l.choix ?? [])
|
|
225
|
+
out.push(`${l.project}:${l.lot} — choix fait : ${x}`);
|
|
226
|
+
for (const h of l.hypotheses ?? [])
|
|
227
|
+
out.push(`${l.project}:${l.lot} — hypothèse de la fiche : ${h}`);
|
|
228
|
+
for (const p of l.proposals)
|
|
229
|
+
out.push(`${l.project}:${l.lot} — proposé : ${p}`);
|
|
230
|
+
if (l.uxVerdict && l.visible)
|
|
231
|
+
out.push(`${l.project}:${l.lot} — verdict UX pour « raf ux » : ${l.uxVerdict}`);
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* `--status --detail <lot>` (L441) : les lignes de détail d'un lot de la vague, lues dans l'état de la vague (le fichier de chaque lot).
|
|
236
|
+
* `sel` : `projet:lot` ou le seul `lot` s'il est unique dans la vague. Lot inconnu ou ambigu : lève.
|
|
237
|
+
*/
|
|
238
|
+
export function renderDetail(lots, sel) {
|
|
239
|
+
const hit = lots.filter((l) => `${l.project}:${l.lot}` === sel);
|
|
240
|
+
const found = hit.length ? hit : sel.includes(':') ? [] : lots.filter((l) => l.lot === sel);
|
|
241
|
+
if (found.length === 0)
|
|
242
|
+
throw new Error(`lot inconnu dans cette vague : ${sel} (lots : ${lots.map((l) => `${l.project}:${l.lot}`).join(', ')})`);
|
|
243
|
+
if (found.length > 1)
|
|
244
|
+
throw new Error(`lot ambigu : ${sel} (${found.map((l) => `${l.project}:${l.lot}`).join(', ')}) : donner projet:lot`);
|
|
245
|
+
const l = found[0];
|
|
246
|
+
const lines = detailLines(l);
|
|
247
|
+
return lines.length ? lines : [`${l.project}:${l.lot} — aucun détail (ni objectif, capture, choix, proposition, ni verdict UX)`];
|
|
248
|
+
}
|
|
249
|
+
/** Le tableau de fin de vague (ou de `--status`), une ligne par lot puis le total et les questions. `compact` (L441, `--status`) : sans le détail des lots, une seule ligne renvoie à `--status --detail`. */
|
|
198
250
|
export function renderTable(wave, lots, deps = {}) {
|
|
199
251
|
const rows = lots.map((l) => [`${l.project}:${l.lot}`, stateText(l, wave, deps.now ?? Date.now()), String(l.pass), review(l), ux(l), String(commits(l)), k(counted(l)), `${minutes(l)} min`]);
|
|
200
252
|
const head = ['lot', 'état', 'passes', 'revue', 'UX', 'commits', 'tokens', 'durée'];
|
|
@@ -216,34 +268,14 @@ export function renderTable(wave, lots, deps = {}) {
|
|
|
216
268
|
out.push(`${d.ready.project}:${d.ready.lot} — livrable jusqu'à ${d.sha} (dernier commit de ${d.ready.lot}, sous ${d.under.lot} ${d.under.status === 'handed-back' || d.under.status === 'failed' ? 'rendu' : LABEL[d.under.status]}) : git push origin ${d.sha}:main && cadence deliver --sha ${d.sha}`);
|
|
217
269
|
}
|
|
218
270
|
for (const l of lots) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (l.uxNote)
|
|
224
|
-
out.push(`${l.project}:${l.lot} — ${l.uxNote}`);
|
|
225
|
-
if (l.uxCaptures)
|
|
226
|
-
out.push(`${l.project}:${l.lot} — captures Playwright : ${l.uxCaptures}`);
|
|
227
|
-
for (const w of l.warnings)
|
|
228
|
-
out.push(`${l.project}:${l.lot} — ⚠ ${w}`);
|
|
229
|
-
// Ligne de base des tests (L213) : les échecs déjà présents au sha de départ, à part de ceux du lot.
|
|
230
|
-
const base = l.baseline;
|
|
231
|
-
if (base?.failures.length) {
|
|
232
|
-
const where = `${base.sha ? ` (${base.sha.slice(0, 7)}${base.verified ? '' : ', annoncé par la session, non vérifié'})` : ''}`;
|
|
233
|
-
for (const t of base.failures.slice(0, BASELINE_LISTED))
|
|
234
|
-
out.push(`${l.project}:${l.lot} — déjà rouge au départ${where} : ${t}`);
|
|
235
|
-
if (base.failures.length > BASELINE_LISTED)
|
|
236
|
-
out.push(`${l.project}:${l.lot} — déjà rouge au départ${where} : … et ${base.failures.length - BASELINE_LISTED} autre(s)`);
|
|
237
|
-
}
|
|
238
|
-
for (const x of l.choix ?? [])
|
|
239
|
-
out.push(`${l.project}:${l.lot} — choix fait : ${x}`);
|
|
240
|
-
for (const h of l.hypotheses ?? [])
|
|
241
|
-
out.push(`${l.project}:${l.lot} — hypothèse de la fiche : ${h}`);
|
|
242
|
-
for (const p of l.proposals)
|
|
243
|
-
out.push(`${l.project}:${l.lot} — proposé : ${p}`);
|
|
244
|
-
if (l.uxVerdict && l.visible)
|
|
245
|
-
out.push(`${l.project}:${l.lot} — verdict UX pour « raf ux » : ${l.uxVerdict}`);
|
|
271
|
+
out.push(...signalLines(l));
|
|
272
|
+
if (deps.compact)
|
|
273
|
+
continue;
|
|
274
|
+
out.push(...detailLines(l));
|
|
246
275
|
}
|
|
276
|
+
const withDetail = lots.filter((l) => detailLines(l).length > 0).map((l) => `${l.project}:${l.lot}`);
|
|
277
|
+
if (deps.compact && withDetail.length)
|
|
278
|
+
out.push(`détail (objectif, captures, base de tests, choix, propositions, verdict UX) : cadence orchestrate --status --detail <projet:lot> — ${withDetail.join(', ')}`);
|
|
247
279
|
return out;
|
|
248
280
|
}
|
|
249
281
|
const two = (n) => String(n).padStart(2, '0');
|
package/dist/perf.js
CHANGED
|
@@ -16,6 +16,8 @@ const BOTTLENECK_SHARE = 0.5;
|
|
|
16
16
|
const MIN_CLASSIFIED = 10;
|
|
17
17
|
/** Les étapes de la chaîne spec : un lot qui en a une est mesuré à part pour ses tokens (L240). */
|
|
18
18
|
const SPEC_CHAIN = new Set(['spec', 'fiche', 'maquette', 'accept']);
|
|
19
|
+
/** La classe de taille d'un lot : son estimate, « +spec » s'il est passé par la chaîne spec. Les tokens et la durée de mur (`lotMinutes`) la partagent ; le collecteur du mod (plugins/cadence-hud/hooks/collect.ts) en recopie la chaîne, `test/hud-classe-l414.test.ts` les relie. */
|
|
20
|
+
const sizeClass = (estimate, steps) => `${estimate}${steps.some((x) => SPEC_CHAIN.has(x.kind)) ? '+spec' : ''}`;
|
|
19
21
|
/**
|
|
20
22
|
* Les deux fenêtres de 7 jours calendaires locaux : la courante finit à `today` inclus, la précédente la précède.
|
|
21
23
|
* `since` (jour ISO ou instant) ignore tout ce qui le précède : la fenêtre qui commence avant lui est tronquée à lui.
|
|
@@ -272,7 +274,7 @@ export function computePerf(input) {
|
|
|
272
274
|
if (!p || counted.length === 0)
|
|
273
275
|
continue;
|
|
274
276
|
// Un lot passé par la chaîne spec (écran ou back) coûte plus que le lot ordinaire de même estimate (mesuré le 12–18-10 : +150 k à l'estimate 0,5, +30 k à 1 ; 450 k est l'écart de budget, pas l'écart mesuré) : classe à part, sinon l'arrivée de ces lots dans une fenêtre passe pour un lot ordinaire plus lourd (L240).
|
|
275
|
-
const key =
|
|
277
|
+
const key = sizeClass(p.estimate, d.j.steps);
|
|
276
278
|
const kinds = new Map();
|
|
277
279
|
for (const x of counted)
|
|
278
280
|
kinds.set(carrierKind(x.kind), (kinds.get(carrierKind(x.kind)) ?? 0) + x.counted);
|
|
@@ -288,6 +290,26 @@ export function computePerf(input) {
|
|
|
288
290
|
const vals = both(windows, (w) => median((w === windows.current ? lotsOfClass.current : lotsOfClass.previous).map((l) => l.total)));
|
|
289
291
|
byEstimate[key] = { ...mk(vals, { current: lotsOfClass.current.length, previous: lotsOfClass.previous.length }, 'up', twinFirstTime), carrier: carrierOf(lotsOfClass.current.map((l) => l.kinds), lotsOfClass.previous.map((l) => l.kinds)) };
|
|
290
292
|
}
|
|
293
|
+
// Durée de mur d'un lot rendu par classe d'estimate (L414) : la même clé que les tokens ; un journal sans début ni fin lisibles, ou à fin avant début, n'est pas compté
|
|
294
|
+
const minutesByClass = (w) => {
|
|
295
|
+
const classes = new Map();
|
|
296
|
+
for (const d of rendered(w)) {
|
|
297
|
+
const p = planLot(plans, d.j.project, d.j.lot);
|
|
298
|
+
const starts = d.j.steps.map((x) => Date.parse(x.started ?? '')).filter((t) => !Number.isNaN(t));
|
|
299
|
+
const ends = d.j.steps.map((x) => Date.parse(x.ended ?? '')).filter((t) => !Number.isNaN(t));
|
|
300
|
+
if (!p || starts.length === 0 || ends.length === 0 || Math.max(...ends) < Math.min(...starts))
|
|
301
|
+
continue;
|
|
302
|
+
const key = sizeClass(p.estimate, d.j.steps);
|
|
303
|
+
classes.set(key, [...(classes.get(key) ?? []), (Math.max(...ends) - Math.min(...starts)) / 60_000]);
|
|
304
|
+
}
|
|
305
|
+
return classes;
|
|
306
|
+
};
|
|
307
|
+
const minutesClasses = both(windows, minutesByClass);
|
|
308
|
+
const lotMinutes = {};
|
|
309
|
+
for (const key of classKeys) {
|
|
310
|
+
const v = both(windows, (w) => (w === windows.current ? minutesClasses.current : minutesClasses.previous).get(key) ?? []);
|
|
311
|
+
lotMinutes[key] = mk({ current: median(v.current), previous: median(v.previous) }, { current: v.current.length, previous: v.previous.length }, null, null);
|
|
312
|
+
}
|
|
291
313
|
const reviewShare = both(windows, (w) => {
|
|
292
314
|
let review = 0;
|
|
293
315
|
let all = 0;
|
|
@@ -417,7 +439,7 @@ export function computePerf(input) {
|
|
|
417
439
|
redDeliveries: mk(reds, deliveriesOf, 'up', 'throughput.delivered'),
|
|
418
440
|
qaBlocking: mk(both(windows, blockingOf), both(windows, deliveredCount), 'up', twinEscaped, escapedNote),
|
|
419
441
|
};
|
|
420
|
-
const result = { since: input.since ?? null, today, windows, throughput, corrections: correctionsInd, interventions, precheck, escalations: both(windows, (w) => escalationCounts(rendered(w))), instables: both(windows, (w) => instableCounts(rendered(w))), tokens, delays, durations, cost, quality, orphans, warnings: [] };
|
|
442
|
+
const result = { since: input.since ?? null, today, windows, throughput, corrections: correctionsInd, interventions, precheck, escalations: both(windows, (w) => escalationCounts(rendered(w))), instables: both(windows, (w) => instableCounts(rendered(w))), tokens, lotMinutes, delays, durations, cost, quality, orphans, warnings: [] };
|
|
421
443
|
const bottleneck = bottleneckOf(causes.current);
|
|
422
444
|
for (const [key, ind] of indicatorLines(result))
|
|
423
445
|
if (ind.warn)
|
package/dist/plan.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { dirtyContents } from './git.js';
|
|
4
5
|
import { Document, isMap, isSeq, parseDocument } from 'yaml';
|
|
5
6
|
import { isDay, toDay } from './dates.js';
|
|
6
7
|
import { ARCHIVE_DIR, archiveFiles, archiveLots, archiveYear, mergeArchived, newArchiveDoc, readArchiveDoc, selectArchivable } from './archive.js';
|
|
7
8
|
/** Délai de la commande `plan.onWrite` : une republication du plan, pas un build. */
|
|
8
9
|
const ON_WRITE_TIMEOUT_MS = 120_000;
|
|
10
|
+
/**
|
|
11
|
+
* La raison d'un échec de `plan.onWrite` en une ligne (L429) : la première ligne « Error: … » de la sortie d'erreur (un script Node qui jette
|
|
12
|
+
* imprime d'abord le fichier, le code fautif et un caret, puis le motif, puis la pile : garder la fin de la pile cachait le motif) ; sans
|
|
13
|
+
* ligne de ce genre, la première ligne non vide de la sortie d'erreur, puis de la sortie standard. Les lignes de pile (« at … ») et la
|
|
14
|
+
* bannière « Node.js v… » ne sont jamais la raison.
|
|
15
|
+
*/
|
|
16
|
+
function failureReason(stderr, stdout) {
|
|
17
|
+
const lines = (text) => (text ?? '').split('\n').map((l) => l.trim()).filter((l) => l && !/^at\s/.test(l) && !/^Node\.js v\d/.test(l));
|
|
18
|
+
const err = lines(stderr);
|
|
19
|
+
return err.find((l) => /^(?:\w+)?Error\b/.test(l)) ?? err[0] ?? lines(stdout)[0] ?? '';
|
|
20
|
+
}
|
|
9
21
|
/** Où va un avertissement de `save()` (la commande a réussi mais quelque chose cloche) : stderr, ou le `err` du CLI qui a lancé l'écriture. */
|
|
10
22
|
let notice = (line) => void process.stderr.write(`${line}\n`);
|
|
11
23
|
export function setPlanNotice(fn) {
|
|
@@ -134,6 +146,8 @@ export class Plan {
|
|
|
134
146
|
* déplacé est au pire en double, ce que le chargement refuse en le nommant, jamais perdu — et toute exception d'écriture (EACCES, disque plein,
|
|
135
147
|
* commande `plan.onWrite` en échec, L346) remet le plan, les fichiers d'année et les fichiers déclarés tels qu'avant ; l'erreur le dit.
|
|
136
148
|
*/
|
|
149
|
+
/** Fichiers que `plan.onWrite` a écrits sans les déclarer, relevés à chaque `save()` de ce processus (L436). */
|
|
150
|
+
writtenByHook = new Set();
|
|
137
151
|
save() {
|
|
138
152
|
this.writable();
|
|
139
153
|
const hook = this.settings.onWrite;
|
|
@@ -182,8 +196,17 @@ export class Plan {
|
|
|
182
196
|
if (!hook)
|
|
183
197
|
return;
|
|
184
198
|
const limit = hook.timeoutMs ?? ON_WRITE_TIMEOUT_MS;
|
|
199
|
+
const before = dirtyContents(hook.cwd);
|
|
185
200
|
const r = spawnSync('sh', ['-c', hook.command], { cwd: hook.cwd, encoding: 'utf8', timeout: limit, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
186
201
|
if (r.status === 0) {
|
|
202
|
+
// Ce que la commande a réellement écrit (L436) : un fichier hors de `files` (déclaration incomplète) doit quand même être commité avec le plan,
|
|
203
|
+
// sans quoi la livraison refuse « 1 fichier modifié ». Le plan, les archives et les fichiers déclarés ont leur propre chemin.
|
|
204
|
+
const known = new Set([...written.map((p) => relative(hook.cwd, p)), ...hook.files]);
|
|
205
|
+
const undeclared = [...dirtyContents(hook.cwd)].filter(([f, content]) => before.get(f) !== content && !known.has(f)).map(([f]) => f);
|
|
206
|
+
for (const f of undeclared)
|
|
207
|
+
this.writtenByHook.add(f);
|
|
208
|
+
if (undeclared.length > 0)
|
|
209
|
+
notice(`raf: plan.onWrite a écrit ${undeclared.join(', ')}, absent de plan.onWrite.files — l'ajouter dans cadence.yaml (il est cité dans le commit proposé cette fois)`);
|
|
187
210
|
// Un chemin mal déclaré laisse le vrai fichier produit hors du commit du plan (le constat d'origine du lot) : le dire.
|
|
188
211
|
const missing = hook.files.filter((f) => !existsSync(resolve(hook.cwd, f)));
|
|
189
212
|
if (missing.length > 0)
|
|
@@ -192,7 +215,7 @@ export class Plan {
|
|
|
192
215
|
}
|
|
193
216
|
restore();
|
|
194
217
|
const why = r.error ? r.error.code === 'ETIMEDOUT' ? `délai de ${limit / 1000} s dépassé` : r.error.message : `code ${r.status ?? r.signal}`;
|
|
195
|
-
const tail =
|
|
218
|
+
const tail = failureReason(r.stderr, r.stdout);
|
|
196
219
|
throw new RafError(`plan.onWrite en échec (${hook.command}, ${why})${tail ? ` : ${tail}` : ''} — écriture refusée, plan inchangé`);
|
|
197
220
|
}
|
|
198
221
|
get project() {
|
|
@@ -262,7 +285,7 @@ export class Plan {
|
|
|
262
285
|
}
|
|
263
286
|
/** Fichiers que produit `plan.onWrite` (L346), relatifs à la racine du dépôt : à commiter avec le plan, même s'ils n'étaient pas encore suivis. */
|
|
264
287
|
get produced() {
|
|
265
|
-
return this.settings.onWrite?.files ?? [];
|
|
288
|
+
return [...(this.settings.onWrite?.files ?? []), ...this.writtenByHook];
|
|
266
289
|
}
|
|
267
290
|
/** Fichier d'attendus QA du `qa-reviewer`, relatif à la racine du dépôt. */
|
|
268
291
|
get qaExpectations() {
|
package/dist/push-guard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { verdictFor } from './gate-verdict.js';
|
|
2
|
+
import { changedFiles, tryGit } from './git.js';
|
|
2
3
|
import { blockingLots, LABEL } from './orchestrate/table.js';
|
|
3
4
|
const ZERO = /^0+$/;
|
|
4
5
|
/** Combien de commits de la plage on essaie, du plus récent au plus ancien, pour trouver le dernier livrable. */
|
|
@@ -40,3 +41,51 @@ export function pushRefusal(root, wave, lots, input, remoteName = 'origin') {
|
|
|
40
41
|
}
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
44
|
+
/** La variable qui passe outre le verdict de la porte au push (L426) ; la garde des lots de la vague (`pushRefusal`) n'en dépend pas. */
|
|
45
|
+
export const UNGATED_ENV = 'CADENCE_PUSH_UNGATED';
|
|
46
|
+
/** Combien de commits de plan empilés on remonte au plus ; au-delà, le sha n'a pas de verdict (la porte se rejoue). */
|
|
47
|
+
const PLAN_HOPS = 50;
|
|
48
|
+
/**
|
|
49
|
+
* Le verdict de la porte pour `sha`, ou, quand `sha` ne change que le plan (`planFile`) et n'a qu'un parent, celui de son parent, de proche en proche
|
|
50
|
+
* (L432 : un commit de `raf done` au-dessus d'un sha à porte verte ne réclame pas une suite complète rejouée). Son propre verdict, s'il en a un, prime.
|
|
51
|
+
* Une fusion (deux parents), un commit racine, un commit qui touche autre chose que le plan ou un commit sans fichier n'héritent de rien.
|
|
52
|
+
*/
|
|
53
|
+
function planVerdict(root, sha, planFile) {
|
|
54
|
+
let at = sha;
|
|
55
|
+
for (let hop = 0; hop <= PLAN_HOPS; hop++) {
|
|
56
|
+
const verdict = verdictFor(root, at);
|
|
57
|
+
if (verdict)
|
|
58
|
+
return verdict;
|
|
59
|
+
const parents = (tryGit(root, ['rev-list', '--parents', '-n1', at]) ?? '').trim().split(/\s+/).slice(1);
|
|
60
|
+
const files = parents.length === 1 ? changedFiles(root, at) : [];
|
|
61
|
+
if (files.length !== 1 || files[0] !== planFile)
|
|
62
|
+
return null;
|
|
63
|
+
at = parents[0];
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Le refus d'un push dont le sha de tête n'a pas de verdict vert de `cadence verify --gate` (L426) : pour chaque ligne de l'entrée standard du hook pre-push,
|
|
69
|
+
* le sha local d'une branche (`refs/heads/`) doit avoir son dernier verdict à 0 dans l'état partagé du dépôt. Absent ou rouge, le refus nomme le sha, dit lequel des deux
|
|
70
|
+
* et donne la commande qui rend le verdict (`cadence verify --gate --sha <sha>`) ; il rappelle la variable qui passe outre. Une suppression de branche, un sha illisible
|
|
71
|
+
* et une réf hors `refs/heads/` (un tag : son sha est celui de l'objet tag, pas d'un commit à vérifier) passent. Sans porte déclarée (`gate` faux) rien n'est exigé.
|
|
72
|
+
* Un commit qui ne change que le plan (`planFile`, relatif à la racine du dépôt) reprend le verdict de son parent (L432, `planVerdict`).
|
|
73
|
+
*/
|
|
74
|
+
export function gateRefusal(root, gate, input, planFile) {
|
|
75
|
+
if (!gate)
|
|
76
|
+
return null;
|
|
77
|
+
for (const entry of input.split('\n')) {
|
|
78
|
+
const [ref, local, remoteRef] = entry.trim().split(/\s+/);
|
|
79
|
+
if (!ref || !local || ZERO.test(local) || !/^[0-9a-f]{7,64}$/.test(local))
|
|
80
|
+
continue;
|
|
81
|
+
if (!(remoteRef || ref).startsWith('refs/heads/'))
|
|
82
|
+
continue;
|
|
83
|
+
const verdict = planVerdict(root, local, planFile);
|
|
84
|
+
if (verdict?.code === 0)
|
|
85
|
+
continue;
|
|
86
|
+
const short = local.slice(0, 7);
|
|
87
|
+
const state = verdict ? `sa porte est rouge (verdict du ${verdict.at.slice(0, 10)})` : 'aucun verdict de la porte pour ce sha';
|
|
88
|
+
return `push refusé — ${short} : ${state} ; la rejouer : cadence verify --gate --sha ${short} (vert, le push passe) ; ${UNGATED_ENV}=1 git push … pour passer outre`;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
package/dist/release.js
CHANGED
|
@@ -164,6 +164,7 @@ function plan(root, bump, today) {
|
|
|
164
164
|
packageName: String(pkg.name ?? ''),
|
|
165
165
|
isPrivate: pkg.private === true,
|
|
166
166
|
hasEvalScript: typeof pkg.scripts?.['eval:plugin'] === 'string',
|
|
167
|
+
cloneScripts: ['typecheck', 'test'].filter((n) => typeof pkg.scripts?.[n] === 'string'),
|
|
167
168
|
};
|
|
168
169
|
}
|
|
169
170
|
/**
|
|
@@ -194,12 +195,22 @@ export function whatsNewIssues(text, version) {
|
|
|
194
195
|
check('paragraphe long', 'manquant', 'mal placé', /^\*\*(\d+\.\d+\.\d+)\*\*:/, split, lines.length, 'juste après l’introduction de « Version by version »');
|
|
195
196
|
return issues;
|
|
196
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Le passage en clone neuf (L446) : la porte voit l'arbre du lead, avec ses fichiers non suivis (la 1.25.0 a cassé à la publication sur des
|
|
200
|
+
* types que Claude Code génère dans plugins/cadence-hud et que seul un clone ne porte pas). `git clone` du dépôt courant ne prend que ce qui est commité,
|
|
201
|
+
* `--no-hardlinks` pour ne rien partager avec lui ; le clone est retiré, vert ou rouge.
|
|
202
|
+
*/
|
|
203
|
+
function cloneStep(scripts) {
|
|
204
|
+
const run = scripts.map((n) => (n === 'test' ? 'npm test' : `npm run ${n}`)).join(' && ');
|
|
205
|
+
return `d=$(mktemp -d) && git clone -q --no-hardlinks . "$d/c" && (cd "$d/c" && npm ci && ${run}); r=$?; rm -rf "$d"; test $r -eq 0 (la suite dans un git clone neuf, comme publish.yml ; rouge = pas de tag)`;
|
|
206
|
+
}
|
|
197
207
|
/** Les étapes que le lead joue, dans l'ordre : la commande n'en fait aucune. */
|
|
198
208
|
function steps(p, files, branch) {
|
|
199
209
|
const tag = `v${p.version}`;
|
|
200
210
|
const out = [
|
|
201
211
|
`git add ${files.join(' ')} && git commit -m "release: ${p.version}"`,
|
|
202
212
|
'cadence verify --gate --sha HEAD (la suite et le build rejoués sur le commit de version, où le CHANGELOG n\'a plus de [Unreleased] plein ; rouge = pas d\'éval ni de tag)',
|
|
213
|
+
...(p.cloneScripts.length ? [cloneStep(p.cloneScripts)] : []),
|
|
203
214
|
...(p.hasEvalScript ? ['npm run eval:plugin -- --trust-plugin (facturé ; un cas sous le seuil arrête la release)'] : []),
|
|
204
215
|
`git tag ${tag}`,
|
|
205
216
|
`git push origin ${branch} ${tag} (le tag lance le workflow de publication)`,
|
package/dist/verify-gate.js
CHANGED
|
@@ -3,7 +3,10 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { availableParallelism, tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { recordedGate } from './gate-record.js';
|
|
6
|
+
import { recordVerdict } from './gate-verdict.js';
|
|
6
7
|
import { onTermination } from './proc.js';
|
|
8
|
+
import { sharedStateDir } from './state.js';
|
|
9
|
+
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
7
10
|
import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
|
|
8
11
|
import { configuredCap } from './orchestrate/command.js';
|
|
9
12
|
import { replayRed, TEST_WORKERS_ENV, testWorkersFrom } from './orchestrate/workers.js';
|
|
@@ -85,16 +88,31 @@ function mainTreeMoved(repo, sha) {
|
|
|
85
88
|
/**
|
|
86
89
|
* `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)
|
|
87
90
|
* 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) ;
|
|
88
|
-
* 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`,
|
|
91
|
+
* sinon (arbre non propre ou à un autre commit, ou vague d'orchestration vivante sur le dépôt, L431) 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`,
|
|
89
92
|
* sinon les `node_modules` de tout niveau et le `.venv` ; les autres dossiers ignorés sont reliés, comme dans les worktrees de vague ; la copie est asynchrone pour que Ctrl-C la coupe). `verify.inPlace` impose l'un (`true`, refus si l'arbre n'est pas propre au sha) ou l'autre (`false`).
|
|
90
93
|
* La première ligne de la sortie dit où la porte a tourné ; en place, HEAD et `git status` sont relus après les étapes et, si l'arbre a bougé (HEAD avancé, status sale), la sortie dit ce qui a changé, que le verdict n'est pas celui du sha et sort en code 2, rouge ou vert ; la ligne « vert » / « rouge » n'est alors pas écrite. Les deux commandes sont rejouées même si la première est rouge : le compte rendu est entier.
|
|
91
94
|
* 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) ;
|
|
92
95
|
* sans elle le rejeu `--maxWorkers=$CADENCE_TEST_WORKERS` de `orchestrate.testReplay` sortirait en erreur et ne prouverait rien.
|
|
93
96
|
* La porte déjà jouée par le programme au même sha n'est pas rejouée (L397) : si l'état d'une vague (`ctx.launchDirs`) porte des contrôles enregistrés AU sha, sur un arbre identique, verts et couvrant chaque commande actuelle,
|
|
94
97
|
* leur verdict est repris et la première ligne le dit (vague, lot) ; sinon la porte est rejouée et la ligne qui suit celle du lieu en donne la raison.
|
|
98
|
+
* Le verdict (0 ou 1, pas 2) est noté par sha dans l'état partagé du dépôt (`gate-verdict.ts`, L426) : le hook pre-push refuse un push dont le sha n'a pas de verdict vert.
|
|
95
99
|
* Le worktree est retiré dans tous les cas. 0 vert, 1 rouge (avec les tests en échec), 2 rien à rejouer ou worktree impossible.
|
|
96
100
|
*/
|
|
97
101
|
export async function verifyGate(ctx) {
|
|
102
|
+
const code = await playGate(ctx);
|
|
103
|
+
// Seuls 0 (vert) et 1 (rouge) sont des verdicts du sha ; 2 (rien à rejouer, verdict non probant) n'en est pas un. Le hook pre-push les lit (L426).
|
|
104
|
+
if (code === 0 || code === 1) {
|
|
105
|
+
try {
|
|
106
|
+
recordVerdict(ctx.repo, ctx.sha, code, (ctx.now ?? (() => new Date()))());
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
// Un verdict qu'on ne peut pas noter ne change jamais le code de la porte : le push sera refusé faute de verdict, et la sortie dit pourquoi.
|
|
110
|
+
ctx.out(`verify --gate ${ctx.sha.slice(0, 7)} : verdict non noté (${e.message}) — le hook pre-push refusera ce sha tant qu'il n'a pas de verdict`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return code;
|
|
114
|
+
}
|
|
115
|
+
async function playGate(ctx) {
|
|
98
116
|
const { repo, sha, config, out } = ctx;
|
|
99
117
|
const steps = [
|
|
100
118
|
...(config.build ? [{ name: 'build', cmd: config.build }] : []),
|
|
@@ -109,14 +127,15 @@ export async function verifyGate(ctx) {
|
|
|
109
127
|
}
|
|
110
128
|
let recorded = { reason: 'aucune porte de vague enregistrée pour ce sha' };
|
|
111
129
|
try {
|
|
112
|
-
recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps);
|
|
130
|
+
recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps, ctx.planFile);
|
|
113
131
|
}
|
|
114
132
|
catch (e) {
|
|
115
133
|
recorded = { reason: `état des vagues illisible (${gitFailure(e)})` };
|
|
116
134
|
}
|
|
117
135
|
if ('found' in recorded) {
|
|
118
136
|
const f = recorded.found;
|
|
119
|
-
|
|
137
|
+
const where = f.checked === sha ? 'au même sha' : `au sha ${f.checked.slice(0, 7)}, sous les seuls commits de plan de ${short}`;
|
|
138
|
+
out(`verify --gate ${short} : verdict repris de la porte de la vague ${f.wave} (lot ${f.lot} : contrôles du programme ${where}, arbre ${f.tree.slice(0, 7)} identique) — rien rejoué`);
|
|
120
139
|
for (const step of steps) {
|
|
121
140
|
const run = f.runs.find((r) => r.label === (step.name === 'build' ? 'build' : 'tests') && r.command === step.cmd);
|
|
122
141
|
out(`✓ ${step.name} (${step.cmd}) — enregistré par la vague${run.instables?.length ? `, ${run.instables.length} test(s) instable(s) sous charge, verts rejoués seuls` : ''}`);
|
|
@@ -131,7 +150,15 @@ export async function verifyGate(ctx) {
|
|
|
131
150
|
out(`verify --gate : verify.inPlace demande l'arbre principal, mais ${obstacle} — rien n'est joué`);
|
|
132
151
|
return 2;
|
|
133
152
|
}
|
|
134
|
-
|
|
153
|
+
// Une vague vivante intègre ses lots (HEAD bouge) et commite son plan : l'arbre principal n'est pas un lieu stable, la porte joue dans une copie (L431) ; `verify.inPlace: true` reste obéi.
|
|
154
|
+
let liveWave = null;
|
|
155
|
+
try {
|
|
156
|
+
liveWave = activeLock(join(sharedStateDir(repo), REPO_LOCK))?.wave ?? null;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// verrou illisible : comme s'il n'y avait pas de vague
|
|
160
|
+
}
|
|
161
|
+
const inPlace = mode === true || (mode === undefined && obstacle === null && liveWave === null);
|
|
135
162
|
if (inPlace) {
|
|
136
163
|
out(`verify --gate ${short} : porte jouée dans l'arbre principal ${repo} (propre, au sha)`);
|
|
137
164
|
out(replayReason);
|
|
@@ -195,7 +222,7 @@ export async function verifyGate(ctx) {
|
|
|
195
222
|
copied = await copyDeps(repo, dir, ignoredDeps(repo).filter(isCopiedDep));
|
|
196
223
|
linkIgnored(repo, dir);
|
|
197
224
|
}
|
|
198
|
-
const why = mode === false ? 'verify.inPlace: false' : obstacle;
|
|
225
|
+
const why = mode === false ? 'verify.inPlace: false' : (obstacle ?? `la vague ${liveWave} est vivante sur ce dépôt : HEAD et le plan y bougent`);
|
|
199
226
|
out(`verify --gate ${short} : porte jouée dans une copie jetable ${dir} (${why}) ; dépendances copiées : ${copied.length ? copied.join(', ') : 'aucune'}`);
|
|
200
227
|
out(replayReason);
|
|
201
228
|
if (missing.length)
|
package/docs/cadence-hud.md
CHANGED
|
@@ -85,6 +85,14 @@ States: `▶ en cours` (doing), `○ libre` (todo, no open `after:`), `⊘ bloqu
|
|
|
85
85
|
|
|
86
86
|
Before the first reading the panel says `Lecture des plans…`; if `cadence lead tour` does not answer it says `Lots illisibles : cadence lead tour n'a pas répondu. Nouvelle lecture dans une minute.` (a failed re-reading keeps the last table); a `cadence` too old to give the `lots` field says `Lots indisponibles : cadence est trop ancien, mettez-le à jour.`; with no open lot, `Aucun lot non clos dans les projets de ce dossier.`. A tree taller than the panel ends with `… +N lots de plus`. Under 60 columns the `after:` list moves to its own line and the title is cut with `…`, never the id or the state.
|
|
87
87
|
|
|
88
|
+
**The cycle of a lot as a strip (L413).** Under a lot, one line draws where it stands in its cycle, read from the wave state (`.cadence/runs/<wave>/<project>--<lot>.json`, whose steps the collector returns as `trail`: kind, status, start, end) and from the plan (`reviewed: true` in the lot's tour row when the plan holds its code review). The wave's stages, in order: `fiche` (the `spec` and `fiche` sessions), `maquette`, `accept` — these three only when the lot played them — then `implement`, `revue` (`review`, `review-small`), `correction` (`fix`), `porte` (the program's build or tests gate, which has no session), `UX`, `intégration`, `prêt`; the control `precheck` is not a stage. Each shows a glyph (`✔` done, `▶` current in bold, `○` to come, `✖` failed), its name, its passes — `correction n/3` (the lot's `pass`), `revue ×2` when the review ran more than once, the gate's `build` / `tests` — and its duration (the sum of its sessions, up to now for the one running; none when its dates are unreadable or the stage was not played). The current stage is the running session; else the gate while it runs; else, between two sessions, the lot's `next`; a lot `failed` or `handed-back` shows in `✖` the stage of its `next` (a lot handed back at integration: `intégration`), else its last stage played, a lot in `question` keeps it current. A stage never played that a later one has passed counts done (`porte`, a `correction 0/3`). Then the plan's: `revue posée → clos → livré` — the review is done when the plan holds it; the first undone one is current once the wave is `ready`. A lot with no wave gets the plan's strip only when it is `doing` **and** its review is posed (before, nothing happened that a strip could show); a `todo` lot, a sub-task and a lot of another project's wave get none. The strip takes one more row (it counts in the panel's height, in the `… +N lots de plus` cut and in the pointer region's rows) and is as wide as the body minus the id's indent; too long, it folds the finished stages into `✔ n`, then replaces the arrows with spaces, then cuts its end with `…`. Computed by `hooks/cycle.ts` (`cycleOf`, `cycleSegments`), drawn by `hooks/lot-row.tsx`; tested by `tests/cycle.test.ts` (L413 R1 to R15), `tests/lots-pane.test.ts` (R16, R17), `test/hud-cycle.test.ts` (R13: the real collector) and `test/lead-tour-lots.test.ts` (R12: `reviewed`).
|
|
89
|
+
|
|
90
|
+
**Lots in progress: budget and duration (L414).** Under the decisions block, `Lots en cours : budget et durée (N)` shows, for each lot being worked in a **live** wave (`implementing`, `reviewing`, `fixing`, `question`), one line `▶ project:lot 100k/750k tokens · 10 min (habituel 30 min)`: the tokens counted by the lot's steps over its budget, the time elapsed since its first step started, up to now — a program gate (build, tests) runs with no session while the lot stays in progress, and the usual time counts those gates too —, and the usual time of a lot of the same size. A lot that goes over comes first, in the warning colour, prefixed `⚠`: `⚠ cadence:L64 786k/750k tokens dépassé · 53 min (habituel 30 min, dépassé)`. **Budget over**: tokens above the lot's `budget` minus the share reserved for model escalation (`escalationReserve`) while the escalation is not engaged (ordinary passes do not spend it; once engaged it is released and counts); a lot state without `budget` shows the tokens alone and is never over. **Time over**: elapsed time above 1.5 times the usual time. The usual time is `lotMinutes` of `cadence perf --json` (`docs/perf.md`): the median wall time of the rendered lots of the same size class (the estimate, `0.5+spec` for a lot that went through the spec chain: the keys of `tokens.byEstimate`), the current window, else the previous one, the first with at least 5 rendered lots (`MIN_SAMPLE`, the same bound under which `cadence perf` refuses a trend): a window with fewer lots, even non-empty, gives no usual time. The mod reads it (`cadence perf <folder> --json`) with each re-read of the board **while a wave is live** (none without one, and no block), and keeps the last successful read when `cadence perf` fails or is missing. With no usual time for the lot's class (or no estimate in its state) the line shows the duration alone and never flags the time. The block is capped at a quarter of the body, decisions and lots in progress together at half of it (the tree keeps the rest); past that, the lots over first, then `… +N lots en cours`. **A lot that is over never loses its signal**: as soon as one lot is over, the block keeps at least two lines, the title and the first lot over, taken from the tree, even in a body of 7 lines or less or when the decisions fill their half (the finished waves of the last 7 days can). With no lot over, a block that has no room for two lines is not drawn and the tree keeps the body. Details in the plugin's README.
|
|
91
|
+
|
|
92
|
+
**Last delivery of each project (L415).** Under the lots in progress, `Dernière livraison par projet (N)` shows one line per project that has delivered: `✔ finance 3f9a1c2 · il y a 2 j · verte · 2 lots clos non livrés : L12, L13`, or `✖ … · rouge` in red when the last delivery failed; `rien à livrer` when every closed lot was delivered, and nothing after `verte` when the plan is not known. It reads the journal `cadence deliver` writes (`.git/cadence/deliveries.log`, one `day sha` line per green delivery, and `deliveries-failed.log`) through the `LAST_DELIVERIES` script (python3, the folder and each direct sub-folder whose `.git` is a folder, like the failed-delivery script; run at each 5 s refresh **while the panel is open**): the last delivery is the red one when `deliveries-failed.log` was modified after `deliveries.log` (the files' modification date decides, as for the failed deliveries), the green one otherwise; a repository with no readable line is absent. The age counts days from the journal's day (the journal has no hour): `aujourd’hui`, `hier`, `il y a N j`. **Closed lots not delivered**: `cadence lead tour --json` gives each project row `closed`, the closed lots (`done`, archived ones included, lots with an `every:` left out) as `{ id, finished }`; a lot is delivered when a green delivery happened on the day it was closed or after (the rule of the delivery delay in `cadence perf`), so it is not delivered when closed **after the day of the last green delivery** — a red delivery delivers nothing, and a project that never delivered green counts all its closed lots. A project absent from the tour, in error, or read by a `cadence` too old to give `closed` shows no count (not « rien à livrer »: we do not know). Order: red first, then the most undelivered lots, then the name. Height (`deliveriesHeight`): the title and one line per project, within what the decisions and the lots in progress leave of the half of the body, `… +N projets` when they do not all fit, nothing under two lines; the tree keeps the rest. Calculation: `hooks/deliveries.ts` (`deliveriesOf`, `buildDeliveries`, `parseLastDeliveries`). Tested by `tests/deliveries.test.ts` (L415 R4 to R11), `tests/lots-pane.test.ts` (R12 to R14, panel mounted), `test/hud-livraisons-l415.test.ts` (R2, R3: the real script on real journals) and `test/lead-tour-lots.test.ts` (R1: the `closed` field).
|
|
93
|
+
|
|
94
|
+
**What is waiting for a decision (L412).** Above the tree, a block `Ce qui attend une décision (N)` gathers what the person has to settle; it exists only when there is something to settle. It reads the band's waves (live ones, and the last finished one only when none is live), the finished waves of the folder that the collector returns apart in `ended` (whether a wave is live or not, without the band's 30-minute delay: **all** the finished waves of the last 7 days, with no cap on their number (a cost guard of 500 waves, far above the 162 waves per 7 days measured, is not a rule), newest first by the date of their `wave.json` — a question or a handed-back lot waits as long as the plan keeps the lot open and its wave is less than 7 days old (beyond that, the decision leaves the block even if the lot is still open in the plan), a newer wave finishing without that lot does not erase it; a live wave is not among them: its `wave.json`, always the most recent of the folder since orchestrate rewrites `consumed` in it, is skipped by the collector; the band and the tree keep reading `waves` only, their drawing does not change) and the failed deliveries; a lot counts once, in the first wave carrying it, live waves first then the finished ones newest first (taken over in a newer wave, that wave decides). Four kinds: `? project:lot <questions>` (a lot in the `question` state; the short text is the first line of each question, joined by ` / `) with, below it in grey, the command that answers it (`cd <launch folder of the wave> && cadence orchestrate --resume --wave <wave> --answer project:lot "…"`, the one the end-of-wave table prints, preceded by a `cd` to the wave's `cwd`: orchestrate finds the wave only from there) — while the wave is live, no command but `réponse possible à la fin de la vague`, since orchestrate refuses to resume a wave that is still running; `↩ project:lot <cause>` (a `handed-back` or `failed` lot: the first line of its outcome, the status in words when empty; a `suspended` lot waits for nothing from the person), in red; `◐ project:lot verdict UX : <verdict>` (a `ready`, visible lot whose review returned a verdict) with `cd <repository of the lot> && raf ux <lot> "verdict"` below (the `repo` of the lot state: from the parent folder raf answers « pas de plan », from another project it would record the verdict on that project's lot of the same id) — the commands to copy start with `cd` and run as they are from any folder, a path with a space or a quote is quoted for sh; `✖ project livraison : <short sha> · <reason> (<day>)` in red (7 characters: deliver writes the full 40-character sha in the log). The block never takes more than half of the body (3 lines at least): past that, the first decisions that fit, then `… +N décisions de plus`. Failed deliveries come from the `DELIVERIES` script (collect.ts, python3), run at every 5 s refresh **while the panel is open**: for the folder and each direct sub-folder whose `.git` is a directory, the last line of `.git/cadence/deliveries-failed.log`, unless `deliveries.log` was modified after it (a green delivery clears the failure; file modification times decide, the day on the line cannot order two writes of the same day). The plan decides: the panel's board (read from `cadence lead tour --json`) drops any decision whose lot is no longer open in a project the tour read, so a UX verdict recorded then `raf done`, or a handed-back lot taken over and closed, disappears at the next plan read; a project missing from the board, in error or without lots keeps its decisions. The command to copy is wrapped over several lines at the panel's width, never cut, every line but the last ending with the shell continuation ` \` (`\` glued to the piece when a word longer than the line is cut): pasted as they are into a shell, the lines run one command, the whole one (without it, a first line `cadence orchestrate --resume --wave <wave>` ran alone, without its `--answer`); the wrapped lines count in the block's ceiling. Details in the plugin's README.
|
|
95
|
+
|
|
88
96
|
The data is the `lots` field of `cadence lead tour --json` ([lead tour](lead.md#lead-tour)), read together with the band's `progress` (same call, same folder). While the panel is open, the modification date of each plan (`.cadence/raf.yaml` and `cadence.yaml` of the folder and of its direct sub-folders) is read every 5 s, one `stat` each: a changed plan, like a wave transition, triggers a new reading before the minute is over. Cost of the `lots` field (R18, measured on 2026-10-28 with `cadence lead tour --json` on the parent folder of ten projects, three passes each): before L401 (cadence 1.19.1, no `lots`) 8.0 / 9.1 / 7.3 s and 86 KB of output; after (the L401 code) 7.5 / 7.0 / 8.4 s and 188 KB (192,503 bytes). The output more than doubles in size but the duration stays in the same order, under the 20 s refresh delay of the band, so the rule holds.
|
|
89
97
|
|
|
90
98
|
The source lives in [`plugins/cadence-hud`](../plugins/cadence-hud/README.md)
|
package/docs/corrections.md
CHANGED
|
@@ -22,3 +22,4 @@ evenly over the sorted keys, with their findings and the model's cause, to be re
|
|
|
22
22
|
beside a pass in the cache file (one of the seven causes, else the command refuses the file) overrides the model's cause in every table and gives the **agreement rate** of the model
|
|
23
23
|
on the reviewed sample. `--until date` (ISO, e.g. `2026-10-10T18:31Z`) reads only the waves created up to then (`created` in the wave's `wave.json`) and, in them, only the steps ended by then (`ended`), so that neither later waves nor a wave still running at that time change a published baseline; a wave or a step with no readable date is left out. `--since date` (ISO, same format) keeps only the lots of the waves **created after** that date (`created` in the wave's `wave.json`), **with all their passes**: a wave renders every brief from the copy of the tool frozen when it is created (`<wave>/tool/`), so the creation date of the wave, not the start of a lot, tells which brief the lot was given. A `--continue` wave created before a change of brief therefore does not give it to its lots, even those that start after the change (a difference of two unbounded runs is wrong as soon as a wave straddles the date); the exception is a wave resumed (`--resume`) without a snapshot (older than L61) or, with `--budget project:lot=+N`, from a snapshot older than L223: it runs with the current `dist/` and templates, so it does receive the new brief. A wave with no readable `created` is left out, by `--since` as by `--until`. `--since` is strict and `--until` inclusive, so a wave created exactly at the date is on the `--until` side only and the two sets never overlap; `--since` and `--until` can be combined (a window of creation dates). Together `--until d` and `--since d` split the waves, and they add up to the unbounded total only when `d` is set while no wave is running (between two sessions, as `npm run service` requires; `cadence orchestrate --status` shows none, **and** no wave created before `d` is still resumable: `--status` lists only the live waves, an interrupted or suspended one is not shown and `--resume` would replay passes after `d`; list them with `jq -r 'select(.status != "done") | "\(.id) \(.status) \(.created)"' <folder>/.cadence/runs/*/wave.json`, and finish each before setting `d` or never resume it): a wave created before `d` that ends a pass after `d` leaves that pass in neither (`--until` does not read it, so that a published baseline does not move), and a gap between the sum and the unbounded total means `d` fell inside a wave. `--json` prints the measure as data. The command reads the journals and writes nothing but the cache.
|
|
24
24
|
The first baseline is in [docs/mesures/corrections-2026-10-10.md](mesures/corrections-2026-10-10.md); `test/corrections-mesure.test.ts` replays its command on the journals and checks the document's results word for word (skipped when the journals are not there). The measure of the screen-sheet step against the visible lots of this baseline (L176: 8 lots with a sheet, verdict per row of the success table, token cost beside the passes avoided) is in [docs/mesures/etape-spec-2026-10-12.md](mesures/etape-spec-2026-10-12.md); the target of its third row is ≤ 8 %, half the 16 % reference. The measure of the back-end spec step against the non-visible lots (L190: 10 lots with a back-end sheet, passes by cause, token cost beside the passes avoided) is in [docs/mesures/etape-spec-back-2026-10-16.md](mesures/etape-spec-back-2026-10-16.md).
|
|
25
|
+
The reading of the « bug de code » passes of the 23–29-10 window, the block added to the `implement` brief against them (L428) and the protocol to measure its effect on two `cadence perf` windows are in [docs/mesures/bug-code-2026-10-29.md](mesures/bug-code-2026-10-29.md).
|
package/docs/deliver.md
CHANGED
|
@@ -295,7 +295,7 @@ cadence verify --gate --sha 6b0d9aa --project app # from the parent folder, t
|
|
|
295
295
|
|
|
296
296
|
**A gate the wave already played is read, not replayed (L397).** A lot that turns ready has just been through the program's gate (`orchestrate.build`, then `orchestrate.test`, at the commit it integrates), and the wave keeps the result in its state (`.cadence/runs/<wave>/<lot>.json`, key `checks`: the commit, its git tree, each command and its code). Replaying the same build and suite on the same commit costs ~5 minutes and says nothing new (L390, 28-10). `cadence verify --gate` therefore looks in the current folder, the repository root and its parent for a wave state of this repository (most recent first) and **reads the verdict** when all of this holds:
|
|
297
297
|
|
|
298
|
-
- the recorded checks ran **at the requested sha** (the same commit, not a descendant);
|
|
298
|
+
- the recorded checks ran **at the requested sha** (the same commit, not a descendant) — or at the commit under it when the commits between them change **only the plan file** (the review record and `raf done` that the wave and the lead commit above an integrated lot, L444; the walk stops at a commit that touches another file, a merge or a root commit; the first line then says « au sha <checked>, sous les seuls commits de plan »);
|
|
299
299
|
- the git tree of the sha is **identical** to the recorded tree;
|
|
300
300
|
- every current step (`orchestrate.build`, `orchestrate.test`) has a recorded run with **the same command and code 0** (an unstable test the wave replayed alone is still green and is said).
|
|
301
301
|
|
|
@@ -306,7 +306,7 @@ verify --gate 6b0d9aa : verdict repris de la porte de la vague 2026-10-28-0830c
|
|
|
306
306
|
verify --gate 6b0d9aa : vert
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
-
Nothing runs: no worktree, no copy of the dependencies, exit 0. Otherwise the gate is **replayed** exactly as described above, and the line right after the one telling where it ran gives the reason: `verdict rejoué : aucune porte de vague enregistrée pour ce sha` (no wave state, another sha such as the head after an integration that rebased
|
|
309
|
+
Nothing runs: no worktree, no copy of the dependencies, exit 0. Otherwise the gate is **replayed** exactly as described above, and the line right after the one telling where it ran gives the reason: `verdict rejoué : aucune porte de vague enregistrée pour ce sha` (no wave state, another sha such as the head after an integration that rebased, a commit that also touches another file (a plan-only commit is crossed, L444), another repository), `arbre différent de celui contrôlé`, `arbre non enregistré` (state written before L397), or `l'enregistrement (vague …, lot …) ne couvre pas en vert test (…)` (a red, missing or changed step). A recorded red is never repeated as a verdict: it is replayed. The verdict read is the one of the wave's gate (with `orchestrate.testReplay` applied), not a new run: to force a replay, ask for a sha the wave did not check.
|
|
310
310
|
|
|
311
311
|
```yaml
|
|
312
312
|
# cadence.yaml
|
|
@@ -315,7 +315,7 @@ verify:
|
|
|
315
315
|
# inPlace: true # the main tree or nothing: not clean at the sha → exit 2, nothing is run
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
-
Absent, `verify.inPlace` chooses as above. Which dependencies are copied into the throwaway worktree:
|
|
318
|
+
Absent, `verify.inPlace` chooses as above, with one more case for the copy (L431): **a live wave on the repository** (the `orchestrate.lock` of the repository is held by a running process). A wave integrates its lots by fast-forward into the main tree and commits its plan there, so HEAD can move during the gate and the verdict would be « non probant » (5 minutes lost, 29-10); while a wave is live the gate therefore always runs in the throwaway copy, and the first line says why (`la vague … est vivante sur ce dépôt`). A stale lock (dead process) does not count. `verify.inPlace: true` is still obeyed, as the project's explicit choice. Which dependencies are copied into the throwaway worktree:
|
|
319
319
|
|
|
320
320
|
- `orchestrate.worktreeDeps: [node_modules, .venv, front/node_modules]` in `cadence.yaml`: those paths, relative to the repository
|
|
321
321
|
(no absolute path, no `..`); one that does not exist in the main tree is reported and skipped;
|
|
@@ -336,3 +336,12 @@ Each command has 30 minutes (then its whole process group is killed, code 124).
|
|
|
336
336
|
`--sha` is required (the moving head of a wave is not a sha to re-verify) and `--retry` does not apply. Exit code:
|
|
337
337
|
**0** green · **1** red · **2** nothing to replay (neither `orchestrate.build` nor `orchestrate.test`), unknown sha, a worktree that cannot be made, or `verify.inPlace: true` with a main tree not clean at the sha.
|
|
338
338
|
The Node of the `.nvmrc` is not selected here: the commands run with the environment of the caller (`PATH` included). `CADENCE_TEST_WORKERS` is set for the build, the tests and the replays, as a wave does (`docs/orchestrate.md`, L350): the value of the caller's environment when it is a positive integer, otherwise cores ÷ session cap (`CADENCE_MAX_SESSIONS`, 2 by default), so a `testReplay` such as `--maxWorkers=$CADENCE_TEST_WORKERS` works from a plain shell.
|
|
339
|
+
|
|
340
|
+
**The verdict is kept per sha, and a push is checked against it (L426).** Each `cadence verify --gate` that ends **green (0) or red (1)** writes its verdict under the sha in `.git/cadence/gate-verdicts.json` (shared by every worktree of the repository; the last verdict of a sha replaces the previous one; the 200 newest shas are kept; a verdict read from the wave's state counts, it is a green gate of that sha). Exit 2 (nothing to replay, `verdict non probant`) writes nothing: it is not a verdict. The `pre-push` hook (`raf hook pre-push`, the one a wave installs, `docs/orchestrate.md`) then refuses, with exit 3, a push of a branch (`refs/heads/…`) **whose local sha has no green verdict**, when the project declares a gate (`orchestrate.test` or `orchestrate.build`; none declared, nothing is asked):
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
raf: push refusé — 6b0d9aa : aucun verdict de la porte pour ce sha ; la rejouer : cadence verify --gate --sha 6b0d9aa (vert, le push passe) ; CADENCE_PUSH_UNGATED=1 git push … pour passer outre
|
|
344
|
+
raf: push refusé — 6b0d9aa : sa porte est rouge (verdict du 2026-10-29) ; la rejouer : …
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
This is the exit code of the gate itself, not a pipe's: on 29-10 a project was pushed with a red gate because its exit code had been hidden by `| tail -1` (the same loss as 28-10). `CADENCE_PUSH_UNGATED=1` in the environment of the `git push` lets the push through and says so on stderr (`push passé outre`); `--no-verify` still skips the hook as for any hook. **A commit that changes only the plan takes its parent's verdict (L432).** The `raf done` commit above a sha with a green gate touches nothing but the plan (`.cadence/raf.yaml`, or the file named by `plan:` in `cadence.yaml`): the hook walks down the commits whose only changed file is the plan, each with a single parent (up to 50 stacked), and uses the first verdict it finds, so no full suite is replayed for it. A verdict of the commit itself takes precedence; a red verdict below stays red; a commit that also touches another file, a merge commit, a root commit and a stack with no verdict below get none (the message is the usual one, naming the pushed sha). A deleted branch, a tag and an unreadable verdict file (read as « no verdict ») are handled as follows: deletions and tags are not checked, a damaged file refuses the push until the gate is replayed (which rewrites it). A `cadence.yaml` that cannot be read lets the push through, like an unreadable wave state. The check applies where the hook is installed: the hook a wave installs, and, once the wave frees the repository (all its lots finished, so before the delivery push), a gate-only `pre-push` hook it leaves behind when the project declares a gate (`orchestrate.test` or `orchestrate.build`). That hook is not left when another `pre-push` hook is in place (the project's own, or that of another live wave), nor when `core.hooksPath` points inside the tracked tree; the next wave replaces it, and a repository that never saw a wave has none.
|
package/docs/lead.md
CHANGED
|
@@ -65,9 +65,16 @@ picture, not its source.
|
|
|
65
65
|
UX reviewer's captures: the lead records `raf ux` on the human's verdict, never on its own — unless the parent folder's `cadence.yaml` says `lead.ux: lead`: then the lead forms the verdict from the reviewer's report and its own look at the captures, and asks the human only at the slightest doubt. Then
|
|
66
66
|
`raf done`.
|
|
67
67
|
6. **Delivery.** Push (for a « livrable jusqu'à <sha> », that sha alone, never HEAD), then
|
|
68
|
-
`cadence deliver`.
|
|
68
|
+
`cadence deliver`. Where the project declares a gate, the `pre-push` hook refuses a sha with no green verdict of
|
|
69
|
+
`cadence verify --gate` (L426): the lot's sha has the verdict of the re-check, the plan commit above it (the head of `main`
|
|
70
|
+
after `raf done` and its commit) changes only the plan and takes that verdict (L432); a commit that also touches another file (a release commit, say) has none and gets a full replay with `cadence verify --gate --sha HEAD` before the push;
|
|
71
|
+
`CADENCE_PUSH_UNGATED=1` is the human's explicit override, never the routine. A lot that turns ready is delivered while the wave runs (`cadence deliver --sha <sha>`, accepted when no commit of the sha belongs to a lot that is not ready), not at the end of the wave. After a green delivery that changes what a page shows or what it is served, the
|
|
69
72
|
`qa-reviewer` agent walks the delivered pages in a real browser, reads only, and brings its blocking
|
|
70
73
|
findings to the human. It is not a gate: a finding becomes a new lot.
|
|
74
|
+
The lead asks both reviewers (`code-reviewer`, `qa-reviewer`) for a short reply with a `Report file: <path>` line in
|
|
75
|
+
the brief (L440): the full report goes to that file, outside the repository, and the agent answers in 5 lines at most
|
|
76
|
+
(`Verdict:`, `Blocking:`, `Follow-ups:`, `Report:`). The lead reads the file only when it needs the detail; twenty
|
|
77
|
+
reports of 3 to 5k each had swollen its context on 29-10.
|
|
71
78
|
7. **Threshold and chaining.** Before every new wave the lead reads `cadence session context`. Under
|
|
72
79
|
**60 %** it chains: `cadence orchestrate --continue` draws the next ready lot of the plan, in the
|
|
73
80
|
declared priority, without asking the human « next? ». From 60 % upward, or when the figure cannot
|
|
@@ -114,9 +121,9 @@ page.
|
|
|
114
121
|
|
|
115
122
|
## lead hold and release
|
|
116
123
|
|
|
117
|
-
`cadence lead hold <label> [--ttl minutes]` and `cadence lead release <label>` (L255) declare the sessions the lead starts by hand — a fix subagent, a review, a QA pass launched outside an orchestrated wave. No wave carries such a session, so the slot registry could not see it: on 2026-10-23 the lead ran 4 sessions under a cap of 3 while `créneaux libres` said one slot was free. `hold` registers the session under its label for `--ttl` minutes (an integer ≥ 1, 60 by default; holding the same label again renews it instead of counting twice); `release` hands it back (releasing a label that is not held answers `aucune session « <label> » inscrite` and exits 0). After the release (L416), `release` prints what it leaves: the slots line, the `prêts, ressources libres` line when a ready lot is free, and `⚠ N créneau(x) libre(s) sur M : lancer un lot prêt ou une session, un créneau vide ne sert à rien` while at least one slot is free (on 2026-10-29 a cap of 3 was taken for 2 and a slot stayed empty for more than an hour); `hold` prints neither. A hold whose time has run out stops counting, but is not forgotten: nothing stops the session (L278), so `cadence lead tour` and `cadence orchestrate --status` print `⚠ session du lead échue : <label> (inscrite depuis 5 h 10 min, échue depuis 2 h 10 min) — la reprendre (cadence lead hold <label> --ttl <minutes>) ou l'arrêter (cadence lead release <label>)` after the slots line, and the `cadence-hud` band shows the same ⚠ row, until the lead renews the hold or releases it (an expired hold nobody touched is dropped 24 h after its expiry). The `--ttl` is therefore the maximum duration declared for the session; the `lead` skill says to renew or stop it at each ⚠. The line is not part of `--json`.
|
|
124
|
+
`cadence lead hold <label> [--ttl minutes]` and `cadence lead release <label>` (L255) declare the sessions the lead starts by hand — a fix subagent, a review, a QA pass launched outside an orchestrated wave. No wave carries such a session, so the slot registry could not see it: on 2026-10-23 the lead ran 4 sessions under a cap of 3 while `créneaux libres` said one slot was free. Orchestrated waves read the holds too (L424): a wave takes no session slot while the held sessions and the running ones already reach the cap, so a hold can no longer be overtaken by a wave starting a session on the « free » slot. `hold` registers the session under its label for `--ttl` minutes (an integer ≥ 1, 60 by default; holding the same label again renews it instead of counting twice); `release` hands it back (releasing a label that is not held answers `aucune session « <label> » inscrite` and exits 0). After the release (L416), `release` prints what it leaves: the slots line, the `prêts, ressources libres` line when a ready lot is free, and `⚠ N créneau(x) libre(s) sur M : lancer un lot prêt ou une session, un créneau vide ne sert à rien` while at least one slot is free (on 2026-10-29 a cap of 3 was taken for 2 and a slot stayed empty for more than an hour); `hold` prints neither. A hold whose time has run out stops counting, but is not forgotten: nothing stops the session (L278), so `cadence lead tour` and `cadence orchestrate --status` print `⚠ session du lead échue : <label> (inscrite depuis 5 h 10 min, échue depuis 2 h 10 min) — la reprendre (cadence lead hold <label> --ttl <minutes>) ou l'arrêter (cadence lead release <label>)` after the slots line, and the `cadence-hud` band shows the same ⚠ row, until the lead renews the hold or releases it (an expired hold nobody touched is dropped 24 h after its expiry). The `--ttl` is therefore the maximum duration declared for the session; the `lead` skill says to renew or stop it at each ⚠. The line is not part of `--json`.
|
|
118
125
|
|
|
119
|
-
The held sessions are kept in the `holds/` folder of the cadence home (`CADENCE_HOME`, shared by every folder and every wave). While at least one is held, the line of the live waves of `cadence orchestrate --status` and of `cadence lead tour` gains a segment and subtracts them: `vagues en cours : 1 · sessions en cours : 0 · sessions du lead : 1 (correctif-L12) · créneaux libres : 0 sur 3`. A held session alone is enough to print the line (`vagues en cours : 0`). It is a display, like the rest of the slots line: a hold blocks no session and never raises the cap (a wave that is already running keeps its own slot counting), and the `lead` skill declares every hand-started session with it.
|
|
126
|
+
The held sessions are kept in the `holds/` folder of the cadence home (`CADENCE_HOME`, shared by every folder and every wave). While at least one is held, the line of the live waves of `cadence orchestrate --status` and of `cadence lead tour` gains a segment and subtracts them: `vagues en cours : 1 · sessions en cours : 0 · sessions du lead : 1 (correctif-L12) · créneaux libres : 0 sur 3`. A held session alone is enough to print the line (`vagues en cours : 0`). It is a display, like the rest of the slots line: a hold blocks no session of the lead, but orchestrated waves wait for it (L424: they take no slot while the held and running sessions reach the cap), and it never raises the cap (a wave that is already running keeps its own slot counting), and the `lead` skill declares every hand-started session with it.
|
|
120
127
|
|
|
121
128
|
## The librarian by hand
|
|
122
129
|
|
|
@@ -167,7 +174,7 @@ The commit count of a lot in progress is the one of `raf commits`: plan commits
|
|
|
167
174
|
| `attend` | only when a lot waits on its `gate`: `attend L190` |
|
|
168
175
|
| `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
|
|
169
176
|
|
|
170
|
-
With `--json`, each project row also carries `lots` (L401): the open lots (`todo`, `doing`, recurring ones left out) as `{ id, title, status, after, parent? }`, in plan order, `after` as the plan writes it (a closed or unknown id is kept as is). The open sub-tasks of those lots follow their lot as entries of their own (`id` = `<lot>/<n>`, `parent` = the lot, no `after`); those of a closed, dropped or recurring lot are left out with it. In a plan kept by another tool, `parent` is the lot's own `parent:` field. A row in error has no `lots`. The [lots panel](cadence-hud.md#the-lots-panel-hud-lots-l401) of `cadence-hud` reads this field.
|
|
177
|
+
With `--json`, each project row also carries `lots` (L401): the open lots (`todo`, `doing`, recurring ones left out) as `{ id, title, status, after, parent?, reviewed? }`, in plan order, `after` as the plan writes it (a closed or unknown id is kept as is). `reviewed: true` is set when the plan holds the lot's code review (`review:` posed by `raf review`, L413), absent otherwise: the [cycle strip](cadence-hud.md#the-lots-panel-hud-lots-l401) of the lots panel reads it for its « revue posée » stage. The open sub-tasks of those lots follow their lot as entries of their own (`id` = `<lot>/<n>`, `parent` = the lot, no `after`); those of a closed, dropped or recurring lot are left out with it. In a plan kept by another tool, `parent` is the lot's own `parent:` field. A row in error has no `lots`. With `--json`, a row also carries `closed` (L415): the closed lots (`done`, archived ones included, lots with an `every:` left out, a lot with no `finished:` day left out) as `{ id, finished }` in plan order, absent when there is none; the lots panel compares `finished` with the day of the project's last green delivery to list the closed lots not delivered. The [lots panel](cadence-hud.md#the-lots-panel-hud-lots-l401) of `cadence-hud` reads this field.
|
|
171
178
|
|
|
172
179
|
After them, one line per project that does not declare `orchestrate.test` (L327): `alpha · porte de tests absente : orchestrate.test n'est pas déclaré, la vague ne lancera pas la suite (seule la revue le fera si elle le décide) · commande probable : npm test (à déclarer en orchestrate.test)`. The probable command is read from `package.json` (`scripts.test`), `pyproject.toml` (pytest) or `pom.xml` (`mvn test`) and omitted when none exists; the lead declares `test:` and `build:` in that project's `cadence.yaml` after checking the command is green, one config commit per project. `--json`: `noTest: { probable }` on the project's row, absent when the key is declared. Same line in [`orchestrate --dry-run`](orchestrate.md).
|
|
173
180
|
|