@sylad/cadence 1.17.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/audit.js +7 -1
- package/dist/cli.js +2 -2
- package/dist/config.js +21 -1
- package/dist/gate-record.js +71 -0
- package/dist/orchestrate/briefs.js +12 -1
- package/dist/orchestrate/command.js +3 -1
- package/dist/orchestrate/continue.js +47 -2
- package/dist/orchestrate/cycle.js +4 -4
- package/dist/orchestrate/worktree.js +32 -17
- package/dist/verify-gate.js +35 -12
- package/docs/deliver.md +17 -2
- package/docs/orchestrate.md +15 -2
- package/docs/raf.md +1 -1
- package/docs/whats-new.md +3 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
- package/templates/orchestrate/fix-docs.md +1 -0
- package/templates/orchestrate/fix-minors.md +1 -0
- package/templates/orchestrate/fix.md +1 -0
- package/templates/orchestrate/implement.md +1 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "cadence",
|
|
10
10
|
"description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.18.0",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Sylvain Ladoire"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cadence",
|
|
3
3
|
"description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.18.0",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/dist/audit.js
CHANGED
|
@@ -151,7 +151,10 @@ export function isAcknowledged(plan, c) {
|
|
|
151
151
|
export function exemptPlanOnly(linked, plan, root) {
|
|
152
152
|
const { patterns } = plan.ignore;
|
|
153
153
|
const acked = (c) => isAcknowledged(plan, c);
|
|
154
|
-
const
|
|
154
|
+
const candidates = linked.orphans.filter((c) => !patterns.some((re) => re.test(c.subject)) && !acked(c));
|
|
155
|
+
// Les fichiers des orphelins restants se lisent d'un seul git log, pas d'un diff-tree par commit (L394).
|
|
156
|
+
preloadChangedFiles(root, candidates.map((c) => c.sha));
|
|
157
|
+
const orphans = candidates.filter((c) => !isPlanOnly(c.sha, plan, root) && !isReleaseOnly(c.sha, root));
|
|
155
158
|
return { ...linked, orphans, unknown: linked.unknown.filter((u) => !acked(u.commit)) };
|
|
156
159
|
}
|
|
157
160
|
/** Fenêtre de l'audit : --since, sinon la date d'adoption du plan, sinon 30 jours. */
|
|
@@ -176,6 +179,7 @@ export function audit(plan, root, newsDir, today, opts = {}) {
|
|
|
176
179
|
const linked = exemptPlanOnly(linkCommits(lots, planCommits(plan, root, { since: auditSince(plan, opts.since) }), plan.refs), plan, root);
|
|
177
180
|
// L'inactivité se mesure sur tout l'historique, pas seulement la fenêtre --since.
|
|
178
181
|
const all = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
182
|
+
preloadChangedFiles(root, [...all.byLot.values()].flatMap((cs) => cs.map((c) => c.sha)));
|
|
179
183
|
// Un commit acquitté (`raf ignore`) qui cite des lots connus ne compte pour aucun : ni commencé, ni actif, ni à relire.
|
|
180
184
|
for (const [id, cs] of all.byLot)
|
|
181
185
|
all.byLot.set(id, cs.filter((c) => !isAcknowledged(plan, c)));
|
|
@@ -234,6 +238,8 @@ export function docSyncIssues(plan, root, byLot) {
|
|
|
234
238
|
/** Commits qui portent du travail sur un lot : ni antérieurs à l'adoption du plan, ni acquittés (`raf ignore`), ni réduits au plan, ni une simple version. */
|
|
235
239
|
function workCommits(plan, root, commits) {
|
|
236
240
|
const adopted = plan.since;
|
|
241
|
+
// Les fichiers de tous ces commits d'un seul `git log` : le filtre ci-dessous les lit un à un (L394, un `git diff-tree` par commit coûtait 89 % de `raf check`).
|
|
242
|
+
preloadChangedFiles(root, commits.map((c) => c.sha));
|
|
237
243
|
return commits.filter((c) => (!adopted || c.day >= adopted) && !isAcknowledged(plan, c) && !isPlanOnly(c.sha, plan, root) && !isReleaseOnly(c.sha, root));
|
|
238
244
|
}
|
|
239
245
|
/** Commits liés à un lot, du plus récent au plus ancien, avant tout tri : commits de plan et d'avant l'adoption compris. */
|
package/dist/cli.js
CHANGED
|
@@ -83,7 +83,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
83
83
|
cadence ref corpus <dossier> [--sans-rendu] écrit le corpus synthétique à vérité connue (7 pièges : texte ambigu levé par une figure, figure qui contredit, table en image, gabarit BUFR, contrôle translucide, page Proposal, scan bruité) : par cas document.html et attendu.json, puis document.pdf et page-<n>.png (1700×2200) rendus par Playwright (CADENCE_PLAYWRIGHT = dossier du paquet) ; dossier existant ou Playwright introuvable : code 2
|
|
84
84
|
cadence release patch|minor|major [--dry-run] prépare le commit de version (package.json, package-lock.json, plugin.json, marketplace.json, CHANGELOG.md) sans commiter ni publier ; imprime les étapes du lead et vérifie docs/whats-new.md : refus sur un arbre sale, un [Unreleased] vide ou une version déjà taguée
|
|
85
85
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
86
|
-
cadence verify --gate --sha rév [--project dossier]
|
|
86
|
+
cadence verify --gate --sha rév [--project dossier] porte du projet (orchestrate.build puis orchestrate.test) : reprend le verdict enregistré par la vague quand son sha est le même et son arbre identique, sans rien rejouer (la sortie dit « verdict repris de la porte de la vague ») ; sinon la rejoue (« verdict rejoué : raison ») au sha dans l'arbre principal s'il est propre au sha, sinon dans un worktree jetable aux dépendances copiées (orchestrate.worktreeDeps), puis le retire : 0 vert, 1 rouge avec les tests en échec, 2 rien à rejouer
|
|
87
87
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
88
88
|
|
|
89
89
|
Un commit appartient à un lot quand son message cite l'identifiant : « feat(L3): … », « L3/t1 ».
|
|
@@ -1481,7 +1481,7 @@ async function verifyGateCommand(values, io) {
|
|
|
1481
1481
|
const sha = resolveCommit(top, values.sha);
|
|
1482
1482
|
if (!sha)
|
|
1483
1483
|
throw new RafError(`verify --gate : --sha ${values.sha} : commit introuvable dans ${top}`);
|
|
1484
|
-
return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), verify: readVerifyConfig(configPath), out: io.out });
|
|
1484
|
+
return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), verify: readVerifyConfig(configPath), launchDirs: [io.cwd, top, dirname(top)], out: io.out });
|
|
1485
1485
|
}
|
|
1486
1486
|
/** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
|
|
1487
1487
|
async function sessionAll(rest, values, io) {
|
package/dist/config.js
CHANGED
|
@@ -338,7 +338,7 @@ export function readVerifyConfig(file) {
|
|
|
338
338
|
return { inPlace: v.inPlace };
|
|
339
339
|
}
|
|
340
340
|
const REVIEW_KEYS = ['threshold', 'light', 'full'];
|
|
341
|
-
const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
|
|
341
|
+
const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
|
|
342
342
|
/** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
|
|
343
343
|
export function readOrchestrateConfig(file) {
|
|
344
344
|
const config = { precheck: true, maquette: true, maquetteReviews: 3, fiche: true, precheckLocal: false, prodAccess: false, worktrees: false, spec: { dir: 'docs/specs' }, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', spec: 'medium', maquette: 'high', fiche: 'medium', accept: 'medium', implement: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
|
|
@@ -497,6 +497,26 @@ export function readOrchestrateConfig(file) {
|
|
|
497
497
|
throw bad('escalation.model : nom de modèle non vide attendu (passé tel quel à claude --model)');
|
|
498
498
|
config.escalation = { model: m.trim() };
|
|
499
499
|
}
|
|
500
|
+
if (o.continue != null) {
|
|
501
|
+
if (!isObject(o.continue))
|
|
502
|
+
throw bad('continue doit être un objet { maxEstimate, exclude }');
|
|
503
|
+
for (const k of Object.keys(o.continue))
|
|
504
|
+
if (!['maxEstimate', 'exclude'].includes(k))
|
|
505
|
+
throw bad(`continue.${k} inconnu (attendu : maxEstimate, exclude)`);
|
|
506
|
+
const { maxEstimate, exclude } = o.continue;
|
|
507
|
+
const setting = {};
|
|
508
|
+
if (maxEstimate != null) {
|
|
509
|
+
if (typeof maxEstimate !== 'number' || !(maxEstimate > 0))
|
|
510
|
+
throw bad('continue.maxEstimate : nombre de jours strictement positif attendu');
|
|
511
|
+
setting.maxEstimate = maxEstimate;
|
|
512
|
+
}
|
|
513
|
+
if (exclude != null) {
|
|
514
|
+
if (!Array.isArray(exclude) || exclude.some((x) => typeof x !== 'string' || !x.trim()))
|
|
515
|
+
throw bad('continue.exclude : liste de numéros de lots non vides attendue (ex. [L12, L40])');
|
|
516
|
+
setting.exclude = exclude.map((x) => x.trim());
|
|
517
|
+
}
|
|
518
|
+
config.continue = setting;
|
|
519
|
+
}
|
|
500
520
|
if (o.effort != null) {
|
|
501
521
|
if (!isObject(o.effort))
|
|
502
522
|
throw bad('effort doit être un objet { precheck, spec, maquette, fiche, accept, implement, fix, review, ux }');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { git } from './orchestrate/worktree.js';
|
|
3
|
+
import { RunStore } from './orchestrate/state.js';
|
|
4
|
+
const realOrSelf = (p) => {
|
|
5
|
+
try {
|
|
6
|
+
return realpathSync(p);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return p;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* La porte que le programme a déjà jouée pour `sha` (L397), pour que `verify --gate` la lise au lieu de la rejouer (≈ 5 min par lot prêt).
|
|
14
|
+
* Une vague range l'état de ses lots sous `<dossier de lancement>/.cadence/runs/<vague>` : `launchDirs` sont ceux à regarder (dossier courant, racine du dépôt, son parent), les vagues les plus récentes d'abord.
|
|
15
|
+
* Un enregistrement est repris quand ses contrôles ont tourné AU sha (même commit), que l'arbre de ce commit est celui enregistré, et qu'il couvre chaque étape de `steps`
|
|
16
|
+
* (même étiquette, même commande, code 0 : une étape rouge, absente ou rejouée avec une autre commande ne prouve rien). Sinon `reason` dit pourquoi la porte est rejouée.
|
|
17
|
+
*/
|
|
18
|
+
export function recordedGate(launchDirs, repo, sha, steps) {
|
|
19
|
+
const tree = git(repo, 'rev-parse', `${sha}^{tree}`);
|
|
20
|
+
const repoReal = realOrSelf(repo);
|
|
21
|
+
let reason = 'aucune porte de vague enregistrée pour ce sha';
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const dir of launchDirs) {
|
|
24
|
+
const base = RunStore.runsDir(dir);
|
|
25
|
+
if (seen.has(realOrSelf(base)) || !existsSync(base))
|
|
26
|
+
continue;
|
|
27
|
+
seen.add(realOrSelf(base));
|
|
28
|
+
const stores = readdirSync(base)
|
|
29
|
+
.map((id) => RunStore.find(dir, id))
|
|
30
|
+
.filter((s) => s !== null)
|
|
31
|
+
.map((store) => {
|
|
32
|
+
try {
|
|
33
|
+
return { store, created: store.readWave()?.created ?? '' };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { store, created: '' };
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.sort((a, b) => b.created.localeCompare(a.created) || b.store.id.localeCompare(a.store.id));
|
|
40
|
+
for (const { store } of stores) {
|
|
41
|
+
let lots;
|
|
42
|
+
try {
|
|
43
|
+
lots = store.lots();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue; // état illisible (écriture en cours) : cette vague ne fournit rien
|
|
47
|
+
}
|
|
48
|
+
for (const l of lots) {
|
|
49
|
+
if (l.checks?.head !== sha || realOrSelf(l.repo) !== repoReal)
|
|
50
|
+
continue;
|
|
51
|
+
const where = `vague ${store.id}, lot ${l.lot}`;
|
|
52
|
+
if (l.checks.tree === undefined) {
|
|
53
|
+
reason = `arbre non enregistré (${where}, enregistrement antérieur à L397)`;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (l.checks.tree !== tree) {
|
|
57
|
+
reason = `arbre différent de celui contrôlé (${where})`;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const label = (name) => (name === 'build' ? 'build' : 'tests');
|
|
61
|
+
const missing = steps.filter((s) => !l.checks.runs.some((r) => r.label === label(s.name) && r.command === s.cmd && r.code === 0));
|
|
62
|
+
if (missing.length) {
|
|
63
|
+
reason = `l'enregistrement (${where}) ne couvre pas en vert ${missing.map((s) => `${s.name} (${s.cmd})`).join(', ')}`;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return { found: { wave: store.id, lot: l.lot, tree, runs: l.checks.runs } };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { reason };
|
|
71
|
+
}
|
|
@@ -109,7 +109,7 @@ export function loadTemplates(dir = TEMPLATES_DIR) {
|
|
|
109
109
|
export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
|
|
110
110
|
const text = typeof source === 'string' ? loadTemplates(source)[kind] : source[kind];
|
|
111
111
|
const out = text.replace(/\{\{(\w+)\}\}/g, (_m, name) => {
|
|
112
|
-
const v = { ...vars, repos: vars.repos ?? '', integration: vars.integration ?? '', docs: vars.docs ?? '', elsewhere: vars.elsewhere ?? '', ack: vars.ack ?? '', spec: vars.spec ?? '', decisions: vars.decisions ?? '', accept: vars.accept ?? '', maquette: vars.maquette ?? '', tmp: vars.tmp ?? '', workers: vars.workers ?? '', shots: vars.shots ?? '', fiche: vars.fiche ?? '', lecture: vars.lecture ?? PLAN_READING }[name];
|
|
112
|
+
const v = { ...vars, repos: vars.repos ?? '', integration: vars.integration ?? '', docs: vars.docs ?? '', elsewhere: vars.elsewhere ?? '', ack: vars.ack ?? '', spec: vars.spec ?? '', decisions: vars.decisions ?? '', accept: vars.accept ?? '', maquette: vars.maquette ?? '', tmp: vars.tmp ?? '', workers: vars.workers ?? '', gate: vars.gate ?? '', shots: vars.shots ?? '', fiche: vars.fiche ?? '', lecture: vars.lecture ?? PLAN_READING }[name];
|
|
113
113
|
if (v === undefined)
|
|
114
114
|
throw new RafError(`gabarit ${FILES[kind]} : valeur manquante pour {{${name}}}`);
|
|
115
115
|
return v;
|
|
@@ -117,6 +117,17 @@ export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
|
|
|
117
117
|
// Un marqueur vide ne laisse pas de trou : lignes vides en double ramenées à une.
|
|
118
118
|
return `${out.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim()}\n`;
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Consigne `{{gate}}` (L399) : le programme rejoue après chaque écriture `orchestrate.test` et `orchestrate.build`, pas la commande que la session juge équivalente.
|
|
122
|
+
* Du 22 au 28-10, des lots annoncés verts arrivaient rouges à la porte parce que la session avait joué `npm run build` (tsconfig.build.json : `src` seul)
|
|
123
|
+
* alors que la porte joue `npx tsc --noEmit` (test et scripts compris) : L330, L353. Vide sans commande déclarée.
|
|
124
|
+
*/
|
|
125
|
+
export function gateText(gate) {
|
|
126
|
+
const lines = [gate.test ? `- tests: \`${gate.test}\`` : '', gate.build ? `- build: \`${gate.build}\`` : ''].filter(Boolean);
|
|
127
|
+
if (!lines.length)
|
|
128
|
+
return '';
|
|
129
|
+
return `The program's gate, played after every writing session, is exactly:\n${lines.join('\n')}\nRun these commands as written before reporting, not a narrower one (\`npm run build\` or \`npm test\` may cover less than the gate). A red there sends the lot back to a correction pass even when your own run was green.`;
|
|
130
|
+
}
|
|
120
131
|
/**
|
|
121
132
|
* L'objectif d'un lot (L258) : celui gardé dans l'état du lot (`LotState.goal`, fixé au lancement : `--goal`, sinon la clé `goal:` du plan),
|
|
122
133
|
* sinon son titre. Une seule source pour les briefs, la simulation et le tableau : la clé `goal:` du plan n'est pas relue en cours de vague,
|
|
@@ -25,7 +25,7 @@ import { ficheReviewText } from './fiche.js';
|
|
|
25
25
|
import { maquetteText } from './maquette.js';
|
|
26
26
|
import { costlyHypotheses, specScreen, specText } from './spec.js';
|
|
27
27
|
import { fichesDuDossier } from '../spec.js';
|
|
28
|
-
import { candidates, parsePriority, parseUntil, readPriority, stopReason } from './continue.js';
|
|
28
|
+
import { candidates, parsePriority, parseUntil, drawSettingsReport, readPriority, stopReason } from './continue.js';
|
|
29
29
|
import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, leadReviewedHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
|
|
30
30
|
import { canInstallPrePush, gitProcessRunning, indexLockRefusal, installPrePush, longDuration, removePrePush, snapshot } from './guard.js';
|
|
31
31
|
import { buildArgs, buildLocalArgs, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude } from './launch.js';
|
|
@@ -955,6 +955,8 @@ async function continueDryRun(args, io, deps, launch, given, budget, firstSkippe
|
|
|
955
955
|
};
|
|
956
956
|
skip(firstSkipped);
|
|
957
957
|
const exclude = new Set(given.map((l) => lotKey(l.project, l.lot)));
|
|
958
|
+
for (const line of drawSettingsReport(launch, exclude))
|
|
959
|
+
io.out(` ${line}`);
|
|
958
960
|
let remaining = budget - given.reduce((sum, l) => sum + (l.budget ?? 0), 0);
|
|
959
961
|
const cap = maxSessions(args, io);
|
|
960
962
|
const rounds = [];
|
|
@@ -55,8 +55,22 @@ export function priorityRank(project, priority) {
|
|
|
55
55
|
export function decisionPending(lot) {
|
|
56
56
|
return /à décider/i.test([lot.title, ...lot.notes.map((n) => n.text)].join('\n'));
|
|
57
57
|
}
|
|
58
|
+
/** Un lot marqué « non tirable » (titre ou note) n'est jamais tiré par `--continue` (L390) ; il reste lançable à la main. */
|
|
59
|
+
export function nonDrawable(lot) {
|
|
60
|
+
return /non tirable/i.test([lot.title, ...lot.notes.map((n) => n.text)].join('\n'));
|
|
61
|
+
}
|
|
62
|
+
/** Pourquoi le réglage `orchestrate.continue` du projet (et le marqueur « non tirable ») écarte ce lot du tirage, ou null quand il reste tirable (L390). */
|
|
63
|
+
export function drawFilter(lot, setting) {
|
|
64
|
+
if (setting?.exclude?.includes(lot.id))
|
|
65
|
+
return 'listé dans continue.exclude';
|
|
66
|
+
if (nonDrawable(lot))
|
|
67
|
+
return 'marqué « non tirable »';
|
|
68
|
+
if (setting?.maxEstimate !== undefined && lot.estimate !== undefined && lot.estimate > setting.maxEstimate)
|
|
69
|
+
return `estimate ${lot.estimate} > continue.maxEstimate ${setting.maxEstimate}`;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
58
72
|
/**
|
|
59
|
-
* Les lots tirables sous `launch` : prêts (à faire, `after` levé, ni récurrents ni en cours), ni « à décider avec Sylvain » ni déjà
|
|
73
|
+
* Les lots tirables sous `launch` : prêts (à faire, `after` levé, ni récurrents ni en cours), ni « à décider avec Sylvain », ni écartés par le réglage `orchestrate.continue` du projet ou marqués « non tirable » (L390), ni déjà
|
|
60
74
|
* dans la vague, dont le budget tient dans `remaining` ; projets dans l'ordre de priorité (puis alphabétique), lots dans l'ordre du plan
|
|
61
75
|
* (gains rapides d'abord). `launch` est un projet : lui seul ; sinon ses sous-dossiers qui en sont. Un plan illisible est sauté.
|
|
62
76
|
*/
|
|
@@ -74,22 +88,53 @@ export function candidates(launch, o) {
|
|
|
74
88
|
}
|
|
75
89
|
let specDir = 'docs/specs';
|
|
76
90
|
let escalating = false;
|
|
91
|
+
let setting;
|
|
77
92
|
try {
|
|
78
93
|
const config = projectEnv(dir).config;
|
|
79
94
|
specDir = config.spec.dir;
|
|
80
95
|
escalating = !!config.escalation;
|
|
96
|
+
setting = config.continue;
|
|
81
97
|
}
|
|
82
98
|
catch {
|
|
83
99
|
// configuration illisible : le dossier par défaut
|
|
84
100
|
}
|
|
85
101
|
for (const lot of ready) {
|
|
86
|
-
if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot)
|
|
102
|
+
if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot))
|
|
103
|
+
continue;
|
|
104
|
+
const why = drawFilter(lot, setting);
|
|
105
|
+
if (why) {
|
|
106
|
+
o.skipped?.push(`${project}:${lot.id} écarté du tirage : ${why}`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (lotBudget(lot.estimate, specScreen(dir, specDir, lot) ? 'screen' : lot.spec ? 'spec' : undefined, escalating) > o.remaining)
|
|
87
110
|
continue;
|
|
88
111
|
out.push({ project, dir, lot });
|
|
89
112
|
}
|
|
90
113
|
}
|
|
91
114
|
return out;
|
|
92
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Pour `--dry-run --continue` (L390) : une ligne par projet qui règle son tirage (`réglage de tirage — <projet> : maxEstimate 1, exclude L3`),
|
|
118
|
+
* puis une par lot prêt que ce réglage écarte (hors lots déjà dans `exclude`, donnés à la main : ceux-là se jouent).
|
|
119
|
+
*/
|
|
120
|
+
export function drawSettingsReport(launch, exclude) {
|
|
121
|
+
const dirs = isProject(launch) ? [launch] : findProjects(launch);
|
|
122
|
+
const lines = [];
|
|
123
|
+
for (const dir of dirs) {
|
|
124
|
+
try {
|
|
125
|
+
const setting = projectEnv(dir).config.continue;
|
|
126
|
+
const parts = [setting?.maxEstimate !== undefined ? `maxEstimate ${setting.maxEstimate}` : '', setting?.exclude?.length ? `exclude ${setting.exclude.join(' ')}` : ''].filter(Boolean);
|
|
127
|
+
if (parts.length)
|
|
128
|
+
lines.push(`réglage de tirage — ${basename(dir)} : ${parts.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// configuration illisible : le projet est signalé par ailleurs
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const skipped = [];
|
|
135
|
+
candidates(launch, { priority: [], exclude, remaining: Infinity, skipped });
|
|
136
|
+
return [...lines, ...skipped.map((s) => `lot écarté — ${s}`)];
|
|
137
|
+
}
|
|
93
138
|
/** Pourquoi la vague ne tire plus de lot, ou null quand elle peut continuer. */
|
|
94
139
|
export function stopReason(f) {
|
|
95
140
|
if (f.stopRequested)
|
|
@@ -19,7 +19,7 @@ import { isPlanOnly } from '../audit.js';
|
|
|
19
19
|
import { compareBaseline, failedTests, hasUnnamedFailure } from './baseline.js';
|
|
20
20
|
import { replayRed, TEST_WORKERS_ENV, testWorkersText } from './workers.js';
|
|
21
21
|
import { APP_TIMEOUT_S, startApp } from './app.js';
|
|
22
|
-
import { capturesText, headline, integrationText, localReading, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tmpText } from './briefs.js';
|
|
22
|
+
import { capturesText, gateText, headline, integrationText, localReading, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tmpText } from './briefs.js';
|
|
23
23
|
import { SlotAbandoned } from './registry.js';
|
|
24
24
|
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig } from './launch.js';
|
|
25
25
|
import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
|
|
@@ -488,7 +488,7 @@ function briefFor(c, kind, local = false) {
|
|
|
488
488
|
const l = c.lot;
|
|
489
489
|
const lot = plan.lot(l.lot);
|
|
490
490
|
const tmpDir = c.wave.store.tmpDir(l.project, l.lot);
|
|
491
|
-
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', tmp: tmpText(tmpDir), workers: kind === 'implement' || kind === 'fix' ? workersText(c) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
|
|
491
|
+
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', tmp: tmpText(tmpDir), workers: kind === 'implement' || kind === 'fix' ? workersText(c) : '', gate: kind === 'implement' || kind === 'fix' ? gateText(c.config) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
|
|
492
492
|
if (l.visible) {
|
|
493
493
|
const pwDir = playwrightDir(c.wave.store.lotDir(l.project, l.lot));
|
|
494
494
|
vars.news = newsText(l.lot, pwDir);
|
|
@@ -2285,7 +2285,7 @@ async function work(c, kind) {
|
|
|
2285
2285
|
}
|
|
2286
2286
|
}
|
|
2287
2287
|
l.constats = [];
|
|
2288
|
-
l.checks = runs.length ? { head: git(wd(c), 'rev-parse', 'HEAD'), runs } : undefined;
|
|
2288
|
+
l.checks = runs.length ? { head: git(wd(c), 'rev-parse', 'HEAD'), tree: git(wd(c), 'rev-parse', 'HEAD^{tree}'), runs } : undefined;
|
|
2289
2289
|
l.next = firstReview(c);
|
|
2290
2290
|
save(c);
|
|
2291
2291
|
}
|
|
@@ -2749,7 +2749,7 @@ async function integrateLot(c) {
|
|
|
2749
2749
|
if (tip !== reviewed) {
|
|
2750
2750
|
// Le rebase a changé les commits : tests et build rejoués au sommet rebasé, avant que la branche du dépôt ne bouge (R7).
|
|
2751
2751
|
const { red, runs } = await integratedChecks(c, key);
|
|
2752
|
-
l.checks = runs.length ? { head: tip, runs } : undefined;
|
|
2752
|
+
l.checks = runs.length ? { head: tip, tree: git(t.dir, 'rev-parse', 'HEAD^{tree}'), runs } : undefined;
|
|
2753
2753
|
save(c);
|
|
2754
2754
|
if (red.length)
|
|
2755
2755
|
return refuse(`intégration refusée : tests rouges au sommet intégré (${red.map(redExcerpt).join(' ; ')}) ; branche du dépôt inchangée, ${kept}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, rmSync, symlinkSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { cp, readdir, realpath, rm, symlink } from 'node:fs/promises';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
5
6
|
import { lotSlug } from './state.js';
|
|
6
7
|
import { cadenceHome, liveWaves } from './registry.js';
|
|
@@ -129,44 +130,58 @@ export function linkIgnored(repo, dir) {
|
|
|
129
130
|
export function ignoredDeps(repo) {
|
|
130
131
|
return git(repo, 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory').split('\n').map((f) => f.replace(/\/$/, '')).filter((f) => f && !f.startsWith('.cadence') && !f.split('/').some((p) => OUTPUT_DIRS.has(p)));
|
|
131
132
|
}
|
|
133
|
+
/** Les dépendances que `verify --gate` copie quand `orchestrate.worktreeDeps` est absente (L392) : les `node_modules` de tout niveau et le `.venv`. Rien d'autre (données, `.env`) : trop gros, ou secret. */
|
|
134
|
+
export const isCopiedDep = (name) => ['node_modules', '.venv'].includes(basename(name));
|
|
132
135
|
/**
|
|
133
136
|
* Copie dans le worktree `dir` ces dépendances du dépôt vivant (L389), au lieu de les y relier : un outil qui refuse de charger un `node_modules` lié depuis
|
|
134
|
-
* un autre arbre (Astro : « Could not load … ») les trouve chez lui.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
+
* un autre arbre (Astro : « Could not load … ») les trouve chez lui. Asynchrone (L392) : une copie synchrone de plusieurs centaines de Mo figerait la boucle
|
|
138
|
+
* d'événements, et le Ctrl-C reçu pendant ce temps n'aurait aucun effet. Un dossier qui est lui-même un lien est copié par son contenu (copier le lien ne
|
|
139
|
+
* copierait rien). Les liens relatifs du dossier qui restent dans le dépôt (espaces de travail) sont copiés tels quels : ils pointent dans la copie. Ceux qui en
|
|
140
|
+
* sortent (`file:../voisin`, `link:`) pointeraient dans le vide depuis `dir` : ils sont refaits vers leur cible vivante. Rend les chemins copiés ;
|
|
141
|
+
* un chemin dont le parent n'existe pas dans le worktree est passé.
|
|
137
142
|
*/
|
|
138
|
-
export function copyDeps(repo, dir, names) {
|
|
143
|
+
export async function copyDeps(repo, dir, names) {
|
|
139
144
|
const copied = [];
|
|
145
|
+
const repoReal = realpathSync(repo);
|
|
140
146
|
for (const name of names) {
|
|
141
147
|
const to = join(dir, name);
|
|
142
148
|
if (existsSync(to) || !existsSync(dirname(to)))
|
|
143
149
|
continue;
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
let from = join(repo, name);
|
|
151
|
+
try {
|
|
152
|
+
from = realpathSync(from);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
continue; // lien mort : rien à copier
|
|
156
|
+
}
|
|
157
|
+
await cp(from, to, { recursive: true, verbatimSymlinks: true });
|
|
158
|
+
// Un dossier venu d'hors du dépôt (lien) garde ses liens internes relatifs ; ses liens vers le reste du monde sont refaits vers leur cible vivante.
|
|
159
|
+
const inside = !relative(repoReal, from).startsWith('..');
|
|
160
|
+
await relinkOutside(inside ? repoReal : from, from, to);
|
|
146
161
|
copied.push(name);
|
|
147
162
|
}
|
|
148
163
|
return copied;
|
|
149
164
|
}
|
|
150
|
-
/** Dans la copie `to` de `
|
|
151
|
-
function relinkOutside(
|
|
152
|
-
for (const e of
|
|
153
|
-
const
|
|
165
|
+
/** Dans la copie `to` du dossier de vrai chemin `real`, refait vers leur cible vivante les liens dont la cible sort de `boundary`. */
|
|
166
|
+
async function relinkOutside(boundary, real, to) {
|
|
167
|
+
for (const e of await readdir(real, { withFileTypes: true })) {
|
|
168
|
+
const realEntry = join(real, e.name);
|
|
154
169
|
const copy = join(to, e.name);
|
|
155
170
|
if (e.isSymbolicLink()) {
|
|
156
|
-
let
|
|
171
|
+
let target;
|
|
157
172
|
try {
|
|
158
|
-
|
|
173
|
+
target = await realpath(realEntry);
|
|
159
174
|
}
|
|
160
175
|
catch {
|
|
161
176
|
continue; // lien mort : recopié tel quel
|
|
162
177
|
}
|
|
163
|
-
if (relative(
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
if (relative(boundary, target).startsWith('..')) {
|
|
179
|
+
await rm(copy);
|
|
180
|
+
await symlink(target, copy);
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
else if (e.isDirectory()) {
|
|
169
|
-
relinkOutside(
|
|
184
|
+
await relinkOutside(boundary, realEntry, copy);
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
}
|
package/dist/verify-gate.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { availableParallelism, tmpdir } from 'node:os';
|
|
4
|
-
import {
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { recordedGate } from './gate-record.js';
|
|
5
6
|
import { onTermination } from './proc.js';
|
|
6
7
|
import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
|
|
7
8
|
import { configuredCap } from './orchestrate/command.js';
|
|
8
9
|
import { replayRed, TEST_WORKERS_ENV, testWorkersFrom } from './orchestrate/workers.js';
|
|
9
|
-
import { copyDeps, git, gitFailure, ignoredDeps, linkIgnored } from './orchestrate/worktree.js';
|
|
10
|
+
import { copyDeps, git, gitFailure, ignoredDeps, isCopiedDep, linkIgnored } from './orchestrate/worktree.js';
|
|
10
11
|
/** Délai de chaque commande de la porte : celui du rejeu de la ligne de base de la vague. */
|
|
11
12
|
const GATE_TIMEOUT_MS = 30 * 60_000;
|
|
12
13
|
/** Lignes de la fin de la sortie montrées quand aucun test n'est identifiable dans une sortie rouge. */
|
|
@@ -66,10 +67,12 @@ function mainTreeObstacle(repo, sha) {
|
|
|
66
67
|
* `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)
|
|
67
68
|
* rejouée au `sha` (L389) dans l'arbre principal quand il est propre et à ce sha (les outils qui refusent un `node_modules` venu d'un autre arbre y tournent chez eux) ;
|
|
68
69
|
* sinon dans un worktree jetable, hors du dépôt, dont les dépendances non suivies sont copiées depuis le dépôt vivant (`orchestrate.worktreeDeps`,
|
|
69
|
-
* sinon les `node_modules` de tout niveau ; les autres dossiers ignorés sont reliés, comme dans les worktrees de vague). `verify.inPlace` impose l'un (`true`, refus si l'arbre n'est pas propre au sha) ou l'autre (`false`).
|
|
70
|
+
* 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`).
|
|
70
71
|
* La première ligne de la sortie dit où la porte a tourné ; en place, l'arbre est relu à la fin et, s'il a bougé, le verdict est dit non probant (code 2). Les deux commandes sont rejouées même si la première est rouge : le compte rendu est entier.
|
|
71
72
|
* 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) ;
|
|
72
73
|
* sans elle le rejeu `--maxWorkers=$CADENCE_TEST_WORKERS` de `orchestrate.testReplay` sortirait en erreur et ne prouverait rien.
|
|
74
|
+
* 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,
|
|
75
|
+
* 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.
|
|
73
76
|
* Le worktree est retiré dans tous les cas. 0 vert, 1 rouge (avec les tests en échec), 2 rien à rejouer ou worktree impossible.
|
|
74
77
|
*/
|
|
75
78
|
export async function verifyGate(ctx) {
|
|
@@ -85,6 +88,24 @@ export async function verifyGate(ctx) {
|
|
|
85
88
|
out('verify --gate : ni orchestrate.test ni orchestrate.build déclarés dans cadence.yaml — rien à rejouer');
|
|
86
89
|
return 2;
|
|
87
90
|
}
|
|
91
|
+
let recorded = { reason: 'aucune porte de vague enregistrée pour ce sha' };
|
|
92
|
+
try {
|
|
93
|
+
recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
recorded = { reason: `état des vagues illisible (${gitFailure(e)})` };
|
|
97
|
+
}
|
|
98
|
+
if ('found' in recorded) {
|
|
99
|
+
const f = recorded.found;
|
|
100
|
+
out(`verify --gate ${short} : verdict repris de la porte de la vague ${f.wave} (lot ${f.lot} : contrôles du programme au même sha, arbre ${f.tree.slice(0, 7)} identique) — rien rejoué`);
|
|
101
|
+
for (const step of steps) {
|
|
102
|
+
const run = f.runs.find((r) => r.label === (step.name === 'build' ? 'build' : 'tests') && r.command === step.cmd);
|
|
103
|
+
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` : ''}`);
|
|
104
|
+
}
|
|
105
|
+
out(`verify --gate ${short} : vert`);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
const replayReason = `verdict rejoué : ${recorded.reason}`;
|
|
88
109
|
const obstacle = mainTreeObstacle(repo, sha);
|
|
89
110
|
const mode = ctx.verify?.inPlace;
|
|
90
111
|
if (mode === true && obstacle) {
|
|
@@ -94,6 +115,7 @@ export async function verifyGate(ctx) {
|
|
|
94
115
|
const inPlace = mode === true || (mode === undefined && obstacle === null);
|
|
95
116
|
if (inPlace) {
|
|
96
117
|
out(`verify --gate ${short} : porte jouée dans l'arbre principal ${repo} (propre, au sha)`);
|
|
118
|
+
out(replayReason);
|
|
97
119
|
const group = {};
|
|
98
120
|
const forget = onTermination(() => {
|
|
99
121
|
if (group.pgid !== undefined)
|
|
@@ -141,21 +163,22 @@ export async function verifyGate(ctx) {
|
|
|
141
163
|
return 2;
|
|
142
164
|
}
|
|
143
165
|
let copied;
|
|
166
|
+
let missing = [];
|
|
144
167
|
if (config.worktreeDeps) {
|
|
145
|
-
const present = config.worktreeDeps.filter((n) =>
|
|
146
|
-
copied = copyDeps(repo, dir, present);
|
|
147
|
-
|
|
148
|
-
if (missing.length)
|
|
149
|
-
out(`dépendances déclarées absentes de l'arbre principal, non copiées : ${missing.join(', ')}`);
|
|
168
|
+
const present = config.worktreeDeps.filter((n) => existsSync(join(repo, n)));
|
|
169
|
+
copied = await copyDeps(repo, dir, present);
|
|
170
|
+
missing = config.worktreeDeps.filter((n) => !present.includes(n));
|
|
150
171
|
}
|
|
151
172
|
else {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
copied = copyDeps(repo, dir, deps.filter((n) => basename(n) === 'node_modules'));
|
|
173
|
+
// Seules les dépendances sont copiées ; les autres dossiers ignorés (données, .env) restent reliés, comme dans un worktree de vague, sans rien dupliquer dans /tmp.
|
|
174
|
+
copied = await copyDeps(repo, dir, ignoredDeps(repo).filter(isCopiedDep));
|
|
155
175
|
linkIgnored(repo, dir);
|
|
156
176
|
}
|
|
157
177
|
const why = mode === false ? 'verify.inPlace: false' : obstacle;
|
|
158
178
|
out(`verify --gate ${short} : porte jouée dans une copie jetable ${dir} (${why}) ; dépendances copiées : ${copied.length ? copied.join(', ') : 'aucune'}`);
|
|
179
|
+
out(replayReason);
|
|
180
|
+
if (missing.length)
|
|
181
|
+
out(`dépendances déclarées absentes de l'arbre principal, non copiées : ${missing.join(', ')}`);
|
|
159
182
|
return await runSteps(steps, dir, ctx, group, env, short);
|
|
160
183
|
}
|
|
161
184
|
finally {
|
package/docs/deliver.md
CHANGED
|
@@ -290,9 +290,24 @@ cadence verify --gate --sha 6b0d9aa --project app # from the parent folder, t
|
|
|
290
290
|
**Where the gate runs (L389).** The first line of the output says it:
|
|
291
291
|
|
|
292
292
|
- the main tree is **clean and at the sha** (`git status --porcelain` empty, `HEAD` equal to the sha): the gate runs **there**, in the repository itself, with its own `node_modules` and its real paths (`verify --gate 6b0d9aa : porte jouée dans l'arbre principal /path/app (propre, au sha)`). A tool that refuses a `node_modules` linked from another tree (Astro: « Could not load …/node_modules/astro/… »), or a test that reads files by a path relative to the checkout, is red in a copy for reasons that have nothing to do with the sha; on 27-10 avatar-pandora and finance-tracker's ux-demo.spec were both green in the main tree and red in the copy. The commands may write their usual ignored outputs there (`dist`, caches);
|
|
293
|
-
- otherwise (uncommitted changes, untracked files, or another commit checked out: a wave's lots may be working in it): a throwaway worktree of the sha under the OS temporary folder (`cadence-verify-…`), removed whatever the result, where the
|
|
293
|
+
- otherwise (uncommitted changes, untracked files, or another commit checked out: a wave's lots may be working in it): a throwaway worktree of the sha under the OS temporary folder (`cadence-verify-…`), removed whatever the result, where the **dependencies of the main tree are copied** (`node_modules` of every level and `.venv`; not linked: a copied `node_modules` is a real one for the tools that refuse a link; a `node_modules` that is itself a link is copied by its content; workspace links, relative, are copied as they are and point inside the copy; those that leave the repository, `file:../neighbour`, are redone towards their live target) and the other ignored folders (data, `.env`) are linked, never copied (on 28-10, jobmail's `data` weighed 14 GB, maritime's `docs/refdocs` 961 MB). The copy is asynchronous: Ctrl-C received during it is handled at once. The line names the worktree, why the main tree was not used, and the copied dependencies (`… porte jouée dans une copie jetable /tmp/cadence-verify-x (l'arbre principal a des modifications non validées ou des fichiers non suivis) ; dépendances copiées : node_modules`), always **first**: the notice of declared dependencies absent from the main tree (`orchestrate.worktreeDeps`) comes after it. The copy of a large `node_modules` takes time: this is the price of a gate that does not lie.
|
|
294
294
|
- **The main tree is read again at the end of an in-place gate.** A wave integrates its lots into it by fast-forward and commits the plan there, possibly while the gate runs: if `HEAD` or `git status --porcelain` changed, the output adds `verdict non probant` (a lot may have been integrated meanwhile, or a command wrote outside the ignored folders) and the exit code is 2 whatever the result: replay it.
|
|
295
295
|
|
|
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
|
+
|
|
298
|
+
- the recorded checks ran **at the requested sha** (the same commit, not a descendant);
|
|
299
|
+
- the git tree of the sha is **identical** to the recorded tree;
|
|
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
|
+
|
|
302
|
+
```
|
|
303
|
+
verify --gate 6b0d9aa : verdict repris de la porte de la vague 2026-10-28-0830c (lot L390 : contrôles du programme au même sha, arbre 3f9a1c2 identique) — rien rejoué
|
|
304
|
+
✓ build (npx tsc --noEmit) — enregistré par la vague
|
|
305
|
+
✓ tests (npx vitest run) — enregistré par la vague
|
|
306
|
+
verify --gate 6b0d9aa : vert
|
|
307
|
+
```
|
|
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 or a plan commit, 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
|
+
|
|
296
311
|
```yaml
|
|
297
312
|
# cadence.yaml
|
|
298
313
|
verify:
|
|
@@ -304,7 +319,7 @@ Absent, `verify.inPlace` chooses as above. Which dependencies are copied into th
|
|
|
304
319
|
|
|
305
320
|
- `orchestrate.worktreeDeps: [node_modules, .venv, front/node_modules]` in `cadence.yaml`: those paths, relative to the repository
|
|
306
321
|
(no absolute path, no `..`); one that does not exist in the main tree is reported and skipped;
|
|
307
|
-
- absent: the `node_modules` of every level are copied; the other ignored folders (`.
|
|
322
|
+
- absent: the `node_modules` of every level and the `.venv` are copied; the other ignored folders (data, `.env`…, build outputs such as `dist` excluded) are linked, as in the worktrees of a wave. A bare worktree has no `node_modules` or `.venv`: on 26-10 the re-verification of a lot in one gave nine red suites that were only missing dependencies.
|
|
308
323
|
|
|
309
324
|
```
|
|
310
325
|
verify --gate 6b0d9aa : porte jouée dans l'arbre principal /path/app (propre, au sha)
|
package/docs/orchestrate.md
CHANGED
|
@@ -101,7 +101,7 @@ command warns `ne lit pas les demandes de contrôle` on the error output.
|
|
|
101
101
|
plan too); then, each time the lots in play are finished, the wave **draws the next ready lots of the plan** — up to the
|
|
102
102
|
session cap — and plays them in the same wave (same budget, same state folder, journal line `continue : tire …`). A lot
|
|
103
103
|
is drawn when it is `todo`, its `after` is lifted, it is not recurring, neither its title nor a note says « à décider »
|
|
104
|
-
(« à décider avec … », « (à décider) »: a lot waiting for a human decision), it is not already in the wave, and its own budget (derived from its estimate; a lot with `screen: page` is worth 1.3 M whatever its estimate (`component` and `journey` have their ordinary budget), or its ordinary budget plus the 450 k spec-stage allocation if that is more, and a lot with `spec: true` gets that allocation on top of its ordinary budget)
|
|
104
|
+
(« à décider avec … », « (à décider) »: a lot waiting for a human decision), it is not already in the wave, the project's draw setting does not hold it back (L390, below), and its own budget (derived from its estimate; a lot with `screen: page` is worth 1.3 M whatever its estimate (`component` and `journey` have their ordinary budget), or its ordinary budget plus the 450 k spec-stage allocation if that is more, and a lot with `spec: true` gets that allocation on top of its ordinary budget)
|
|
105
105
|
fits in the budget left — otherwise the next one that fits is taken (a lot that does not fit in what is left of the current
|
|
106
106
|
round is not dropped: it is tried again at the next round, once the budget allows it). A round takes at most **one lot per
|
|
107
107
|
repository** while other repositories have ready lots — the pool plays the lots of one repository one after the other, so a
|
|
@@ -118,6 +118,17 @@ lot whose estimate fits the budget left. `--resume … --continue` draws again a
|
|
|
118
118
|
draws round by round with the same function as the wave (one lot per repository per round, pre-check, budget counted on the estimates) and
|
|
119
119
|
names the candidates skipped with their cause: `tour 1 : a:L2, b:L2 · tour 2 : a:L3, a:L4`. Drawing happens between rounds: a round of up to `--max-sessions` lots must finish before the next draw.
|
|
120
120
|
|
|
121
|
+
**Keeping a lot out of the draw (L390, `orchestrate.continue`)**: `--continue` draws the first ready lot of the plan, even a big one with no spec and no sub-task (maritime B6 « Profils verticaux, coupes (10) », effort L, drawn on 27-10 then withdrawn by the lead). A project says what its draw may not take, in its own `cadence.yaml`:
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
orchestrate:
|
|
125
|
+
continue:
|
|
126
|
+
maxEstimate: 1 # days: a lot whose estimate is above is not drawn (equal is drawn)
|
|
127
|
+
exclude: [L12, L40] # lot ids never drawn
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Both keys are optional; no `continue:` key keeps the old behaviour. A lot whose title or a note says « non tirable » (any case) is not drawn either, with no setting needed — the same convention as « à décider ». The setting belongs to the project: the other projects of the folder draw as before. It only governs the draw: a lot given on the command line (`cadence orchestrate maritime-atlas:B6`) is played whatever the setting says, and `--resume` replays the lots already in the wave. `--dry-run --continue` prints, after the `--continue : priorité …` line, `réglage de tirage — <project> : maxEstimate 1, exclude L12 L40` for each project that sets one, then `lot écarté — <project>:<lot> écarté du tirage : <cause>` for each ready lot held back (`estimate 3 > continue.maxEstimate 1`, `listé dans continue.exclude`, `marqué « non tirable »`); the wave itself says nothing about them, round after round. An unknown key, a `maxEstimate` that is not a positive number or an `exclude` that is not a list of ids refuses the configuration (`continue.<key> …`). The line `prêts, ressources libres` is a view of the ready lots by resource and does not apply this setting.
|
|
131
|
+
|
|
121
132
|
**Chaining without the human (L148)**: the `lead` skill, once the human has chosen the first lots and the order, does not ask « next? »
|
|
122
133
|
after a lot that comes back ready: it re-verifies it, records `raf done`, pushes, delivers, has the `qa-reviewer` check the
|
|
123
134
|
delivered app, then starts the next wave with `--continue`, in the declared priority — one project at a time, within the two-session
|
|
@@ -181,6 +192,8 @@ usability (code only when the lot is not `visible`). Failing tests (reported red
|
|
|
181
192
|
|
|
182
193
|
**Each session gets its share of the machine (L350)**: the wave computes `CADENCE_TEST_WORKERS = max(1, cores ÷ session cap)` (`--max-sessions` or `CADENCE_MAX_SESSIONS`; 32 cores and a cap of 4 give 8; a `CADENCE_TEST_WORKERS` already set in the environment that launches the wave wins when it is a positive integer). It is put in the environment of every session and of every command of the gate (tests, build, baseline, replays), recorded as `testWorkers` in the wave state, and the `implement`, `fix`, minors and librarian briefs tell the session to respect it: `vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS` (vitest 2 refuses `--maxWorkers` alone, its default minimum being the core count: « options.minThreads and options.maxThreads must not conflict »), `jest --maxWorkers=$CADENCE_TEST_WORKERS`, `pytest -n $CADENCE_TEST_WORKERS`. The gate command itself must honour it for the share to bind: cadence's own `vitest.config.ts` reads it (`poolOptions.forks.maxForks`, `threads.maxThreads`); in another project write `orchestrate.test: npx vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS` (or the equivalent of its runner).
|
|
183
194
|
|
|
195
|
+
**The session plays the gate's own commands (L399)**: the briefs of the writing sessions (implement, fix, minors pass, librarian pass) name the exact `orchestrate.test` and `orchestrate.build` of the project (`The program's gate, played after every writing session, is exactly: …`) and ask to run them as written, not a narrower equivalent. Measured on 22–28-10 (`docs/mesures/portes-rouges-2026-10-28.md`): sessions that played `npm run build` (`src` only) announced a green lot that `npx tsc --noEmit` (tests included) sent back to a correction pass. Without `orchestrate.test` and `orchestrate.build` there is no line.
|
|
196
|
+
|
|
184
197
|
**Review sized to the lot (L108)**: a lot whose `estimate` is ≤ `orchestrate.review.threshold` (0.25 day by default)
|
|
185
198
|
is *light*: one single review (Sonnet by default, `code-reviewer` agent, same criteria), no minors pass — the minors are
|
|
186
199
|
returned to the lead as notes (proposals). A blocking or major finding on a light lot still triggers a correction, and
|
|
@@ -845,7 +858,7 @@ orchestrate:
|
|
|
845
858
|
test: npm test # run by the orchestrator after a work step (optional)
|
|
846
859
|
testReplay: npx vitest run --minWorkers=1 --maxWorkers=$CADENCE_TEST_WORKERS {files} # replay alone of a red test before it counts (L354, see « A red test is replayed alone »; `cadence verify --gate` applies it too, L387, `docs/deliver.md`); `{files}` required; absent: no replay, nothing held for unstable (optional)
|
|
847
860
|
build: npm run build # run BEFORE the tests (L333); both results go to the reviewer, who does not redo them (optional)
|
|
848
|
-
worktreeDeps: [node_modules, .venv] # untracked dependencies that `cadence verify --gate` copies into its throwaway worktree (L348, L389; the gate runs in the main tree instead when it is clean at the sha, `verify.inPlace`, `docs/deliver.md`); absent: the `node_modules` of every level are copied, the other ignored folders are linked as in the worktrees of a wave (optional)
|
|
861
|
+
worktreeDeps: [node_modules, .venv] # untracked dependencies that `cadence verify --gate` copies into its throwaway worktree (L348, L389; the gate runs in the main tree instead when it is clean at the sha, `verify.inPlace`, `docs/deliver.md`); absent: the `node_modules` of every level and the `.venv` are copied (never data or `.env`, L392), the other ignored folders are linked as in the worktrees of a wave (optional)
|
|
849
862
|
precheck: true # default: before the first implementation of a lot with no commit whose title cites a finding or an earlier lot, and whose title or `goal:` cites a finding (`constat`, `revue`, `review`, `audit`) or a plan id (an earlier `L<number>`, also followed by a hyphen as in `L12-L15` or `L12-bis`; any other id such as `Q-CAN5`; `project:L<number>`), a read-only Sonnet session checks whether the deliverable is already in the repository (see below); false skips it, local runs it on claude-local (Ollama) with Sonnet as fallback, for every lot with no commit whatever its title (L296); a lot created after the last code commit of the repository is not pre-checked, local or Sonnet, unless it cites a lot that already has commits (L322)
|
|
850
863
|
accept: npx vitest run -c vitest.acceptation.config.ts # accept step (see « The accept step »): the dedicated command that runs ONLY the acceptance tests of the lot (optional; without it the step is skipped with a warning)
|
|
851
864
|
maquette: true # default: UX review of the mockup between spec and accept (see « The mockup review »); false skips it
|
package/docs/raf.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
`raf now` prints no commit and the tour `L1 (0 commit)`, L379; the commit column and the tooltip of `raf gantt` read the same count, L381), and
|
|
28
28
|
`cadence deliver` does not announce the lots it cites as delivered. The files
|
|
29
29
|
decide, never the subject: a `chore(plan): …` commit that touches a source
|
|
30
|
-
file is a commit like any other.
|
|
30
|
+
file is a commit like any other. `raf check`, `raf now` and `cadence session start` read the files of all the commits in one `git log` call, not one `git diff-tree` per commit (L394: 16.5 s down to 3.4 s for `raf check` on 2,764 commits, `docs/mesures/fichiers-des-commits-2026-10-28.md`).
|
|
31
31
|
- `raf gantt` writes a single self-contained HTML page (no server, no CDN).
|
|
32
32
|
|
|
33
33
|
```sh
|
package/docs/whats-new.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## In two lines
|
|
6
6
|
|
|
7
|
+
- **1.18.0**: `cadence verify --gate` reuses the green gate of the wave instead of replaying it; `orchestrate.continue` keeps `--continue` from drawing a big lot; `raf check` and `raf now` run about 5 times faster.
|
|
7
8
|
- **1.17.1**: `cadence deliver --sha` during a wave commits the plan it wrote, nothing else; `verify --gate` no longer turns red for reasons of its throwaway copy and says where it ran; the test suite no longer commits into the repository.
|
|
8
9
|
- **1.17.0**: `cadence orchestrate` refuses a repository holding a `.git/index.lock`; `raf docs-ok` works on a plan kept by another tool; `cadence verify --gate` replays a red test alone, like the gate of a wave.
|
|
9
10
|
- **1.16.1**: publishes 1.16.0 (tagged, never published), plus `raf gantt` counting commits without plan commits, one login check per `orchestrate --continue` launch, and a wider `CHANGELOG.md` conflict settled by worktree integration.
|
|
@@ -41,6 +42,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
41
42
|
|
|
42
43
|
The long form of « In two lines » above (the README no longer carries a « What's new » section since L326: it links here). The full history, with the date of each version, is in [CHANGELOG.md](../CHANGELOG.md).
|
|
43
44
|
|
|
45
|
+
**1.18.0**: a re-verification costs seconds instead of minutes, and a wave draws only the lots it can play. `cadence verify --gate` reuses the gate of the wave instead of replaying it (L397, `docs/deliver.md`, skill `lead`): when the state of a wave (`.cadence/runs/<wave>`, looked for in the current folder, the repository root and its parent) holds checks that the program ran at the requested sha, on an identical git tree, green and covering every current `orchestrate.build` / `orchestrate.test` command, the verdict is read from it, nothing runs (exit 0) and the first line says so (`verdict repris de la porte de la vague <vague> (lot …)`). A changed sha (integration, rebase), a different tree, a record without tree, a red or missing step, a changed command or no wave at all replays the gate as before, and the line after the one telling where it ran says why (`verdict rejoué : …`); the wave now records the tree of the checked commit next to its sha. `orchestrate.continue` in the `cadence.yaml` of a project (L390, `docs/orchestrate.md`) takes `maxEstimate` (days) and `exclude` (lot ids), so `cadence orchestrate --continue` no longer draws a big lot with no spec or sub-task (a lot whose title or note says « non tirable » is never drawn either); `--dry-run --continue` prints each project's setting and every ready lot it holds back, with the cause, and a lot given on the command line is still played. The briefs of the writing sessions (implement, fix, minors pass, librarian pass) name the exact `orchestrate.test` and `orchestrate.build` commands of the gate (L399, `docs/orchestrate.md`), so a session that played a narrower `npm run build` no longer announces a green lot that the gate sends back to a correction pass. `cadence verify --gate` in its throwaway copy copies only the dependencies (`node_modules` of every level and `.venv`), never every ignored folder (L392, `docs/deliver.md`); Ctrl-C received during the copy is handled, a `node_modules` that is itself a link is copied by its content, and the line telling where the gate ran is always the first of the output. Last, `raf check`, `raf now` and `cadence session start` read the files of all the commits of a lot in one `git log` call (L394): on 2,764 commits `raf check` goes from 16.5 s to 3.4 s and `raf now` from 14.7 s to 3.5 s, and `cadence lead tour --json` no longer exceeds the 20 s deadline of the HUD band, which showed an empty project list.
|
|
46
|
+
|
|
44
47
|
**1.17.1**: three corrections that stop a wave from tripping over its own tools. `cadence deliver --sha` run during a wave commits the plan it wrote for the article to refresh (the `plan.commit` template, `docs/news.md`) instead of leaving it modified in the tree a session of the wave works in, which handed that lot back as « dépôt sale après implement » (L388): the commit holds that plan and the files `plan.onWrite` produces, and nothing else, so a draft or a file you were editing is never swept in; outside a wave, or when the plan or those files already had uncommitted changes, nothing changes. `cadence verify --gate` no longer turns red in its throwaway copy for reasons of the copy itself (L389, `docs/deliver.md`): when the main tree is clean and at the sha, the gate runs there; otherwise it runs in a throwaway worktree whose dependencies are copied instead of linked (links leaving the repository are re-pointed at their live target), so a project whose tests refused a linked `node_modules` or read their data from `/tmp` now gets the verdict of the real tree. The first line of the output says where the gate ran, an in-place gate whose main tree moved meanwhile answers « non probant » (exit 2), and the new `verify.inPlace` key of `cadence.yaml` forces the main tree (`true`) or the copy (`false`). Last, the test suite of cadence no longer commits into the real repository (L391): the fake `claude` of a test used to write `a.txt` and commit `feat(?): travail` there, and it now refuses to write or commit outside a temporary folder.
|
|
45
48
|
|
|
46
49
|
**1.17.0**: fewer stops that were not the lot's fault. `cadence orchestrate` and its `--dry-run` refuse a repository holding a `.git/index.lock` before writing anything (L385, `docs/orchestrate.md`): the message gives the age of the lock, whether a git process is running, and the `rm` command to remove a stale one. A plan commit that fails (stale lock, refusing hook) now puts the plan back as it was even when the lock blocks the index, where the next lot used to be handed back as a « dépôt sale ». `raf docs-ok` no longer refuses a plan kept by another tool (read-only): the acknowledgement of a `docs.sync` gap is written to `.cadence/docs-ok.yaml` (`L12: { date, reason }`, next to the project's `cadence.yaml`), the plan is never touched, and `raf check`, `raf show`, `cadence lead tour` and `cadence orchestrate` read it; the librarian's brief names that file to commit (L386). `cadence verify --gate` applies `orchestrate.testReplay` like the gate of a wave (L387, `docs/deliver.md`): a red test the output names is replayed alone, twice at most, and a test that passes alone is reported as unstable under load instead of turning the gate red. A red step that names no test prints the last 10 lines of its output, prefixed ` | `. The gate and the replay get `CADENCE_TEST_WORKERS` like a wave's gate (the caller's value, else cores ÷ `CADENCE_MAX_SESSIONS`), so `--maxWorkers=$CADENCE_TEST_WORKERS` works from a plain shell, and a red step says that a replay took place and how many tests it confirmed green.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sylvain Ladoire",
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -193,7 +193,7 @@ is always recorded by the lead's own `raf ux`, never by a subagent.
|
|
|
193
193
|
2. **The lead re-verifies itself**: `git log` shows the commits, the test suite and build pass when
|
|
194
194
|
run by the lead, `raf check` is clean. During a wave, or in any tree that is not the lead's own, replay the gate at the lot's last sha with
|
|
195
195
|
`cadence verify --gate --sha <sha> [--project <dir>]` (exit 0 green, 1 red with the failing tests, 2 nothing to replay): a bare
|
|
196
|
-
`git worktree add` has no `node_modules` or `.venv` and reddens suites that only miss their dependencies; the gate runs in the main tree when it is clean at the sha and in a throwaway copy with copied dependencies otherwise, and the first line of the output says which. A subagent is green on what it *could* test; say plainly
|
|
196
|
+
`git worktree add` has no `node_modules` or `.venv` and reddens suites that only miss their dependencies; the gate runs in the main tree when it is clean at the sha and in a throwaway copy with copied dependencies otherwise, and the first line of the output says which. When the wave itself already ran the gate at that very sha on an identical tree (the usual case for a ready lot), `verify --gate` reads its recorded verdict instead of replaying the build and suite (~5 minutes saved per lot): the first line then reads `verdict repris de la porte de la vague …`, and a replay says why on the line after (`verdict rejoué : …`: another sha after an integration, no record, a changed tree or command). A subagent is green on what it *could* test; say plainly
|
|
197
197
|
what nobody could verify.
|
|
198
198
|
3. Fix or re-delegate what the review found; then record the verdict, `raf review <lot> "…"` with
|
|
199
199
|
the code reviewer's last line brought up to date (and `raf ux <lot> "…"` for a visible lot, with
|
|
@@ -27,4 +27,5 @@ Give your final report as the structured output (commits, tests, build, what you
|
|
|
27
27
|
{{captures}}
|
|
28
28
|
Browser (Playwright): if a Playwright MCP server is available in this session (only in an orchestrated wave: a brief delegated by hand has none), the orchestrator launched it with its own output directory, in the orchestrator's run directory outside the repository. {{shots}} If no such server is available, do not install or start one; if you use the Playwright CLI, write its outputs under the OS temp directory (or the temporary folder the project's CLAUDE.md names), never inside the repository, which must stay clean.
|
|
29
29
|
{{workers}}
|
|
30
|
+
{{gate}}
|
|
30
31
|
{{tmp}}
|
|
@@ -40,4 +40,5 @@ Give your final report as the structured output (commits, tests, build, what you
|
|
|
40
40
|
{{captures}}
|
|
41
41
|
Browser (Playwright): if a Playwright MCP server is available in this session (only in an orchestrated wave: a brief delegated by hand has none), the orchestrator launched it with its own output directory, in the orchestrator's run directory outside the repository. {{shots}} If no such server is available, do not install or start one; if you use the Playwright CLI, write its outputs under the OS temp directory (or the temporary folder the project's CLAUDE.md names), never inside the repository, which must stay clean.
|
|
42
42
|
{{workers}}
|
|
43
|
+
{{gate}}
|
|
43
44
|
{{tmp}}
|
|
@@ -46,4 +46,5 @@ Give your final report as the structured output, not as free text. If the plan i
|
|
|
46
46
|
{{reponse}}
|
|
47
47
|
Browser (Playwright): if a Playwright MCP server is available in this session (only in an orchestrated wave: a brief delegated by hand has none), the orchestrator launched it with its own output directory, in the orchestrator's run directory outside the repository. {{shots}} If no such server is available, do not install or start one; if you use the Playwright CLI, write its outputs under the OS temp directory (or the temporary folder the project's CLAUDE.md names), never inside the repository, which must stay clean.
|
|
48
48
|
{{workers}}
|
|
49
|
+
{{gate}}
|
|
49
50
|
{{tmp}}
|