@sylad/cadence 0.11.0 → 0.12.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 +16 -3
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +183 -17
- package/agents/code-reviewer.md +8 -3
- package/agents/precheck-reader.md +14 -0
- package/agents/qa-reviewer.md +19 -2
- package/agents/ux-reviewer.md +1 -1
- package/dist/audit.js +34 -7
- package/dist/cli.js +113 -13
- package/dist/config.js +104 -15
- package/dist/deliver.js +10 -3
- package/dist/news.js +17 -1
- package/dist/orchestrate/app.js +189 -0
- package/dist/orchestrate/briefs.js +16 -0
- package/dist/orchestrate/command.js +110 -17
- package/dist/orchestrate/cycle.js +208 -26
- package/dist/orchestrate/guard.js +148 -3
- package/dist/orchestrate/launch.js +43 -5
- package/dist/orchestrate/node-env.js +77 -0
- package/dist/orchestrate/registry.js +5 -4
- package/dist/orchestrate/schemas.js +7 -1
- package/dist/orchestrate/snapshot.js +99 -0
- package/dist/orchestrate/table.js +11 -1
- package/dist/plan.js +29 -0
- package/dist/session.js +6 -3
- package/dist/state.js +34 -14
- package/package.json +1 -1
- package/skills/lead/SKILL.md +17 -8
- package/templates/orchestrate/fix-minors.md +3 -0
- package/templates/orchestrate/fix.md +4 -0
- package/templates/orchestrate/implement.md +5 -1
- package/templates/orchestrate/precheck.md +10 -0
- package/templates/orchestrate/review-recheck.md +2 -0
- package/templates/orchestrate/review-small.md +3 -0
- package/templates/orchestrate/review.md +2 -0
- package/templates/orchestrate/ux.md +2 -0
|
@@ -1,22 +1,58 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
4
4
|
import { parse } from 'yaml';
|
|
5
5
|
import { RafError } from '../plan.js';
|
|
6
6
|
import { isQuotaMessage, lacksStructuredOutput, parseSession, salvageUsage, sumTokens, tokensOf } from './result.js';
|
|
7
|
+
/**
|
|
8
|
+
* Serveurs MCP d'une étape : Playwright pour `ux` et, sur un lot `visible`, pour `implement` et `fix` (fix-minors compris) ;
|
|
9
|
+
* et `review-small` d'un petit lot visible (sa seule revue d'ergonomie) ; rien pour `review`, ni pour un lot sans écran.
|
|
10
|
+
* Sorties dans `outputDir`, hors du dépôt.
|
|
11
|
+
*/
|
|
12
|
+
export function mcpServersFor(kind, visible, outputDir, small = false) {
|
|
13
|
+
if (!(kind === 'ux' || (visible && (kind === 'implement' || kind === 'fix' || (kind === 'review-small' && small)))))
|
|
14
|
+
return {};
|
|
15
|
+
return { playwright: { command: 'npx', args: ['-y', '@playwright/mcp@latest', '--output-dir', outputDir] } };
|
|
16
|
+
}
|
|
17
|
+
/** Dossier de sortie du Playwright MCP d'un lot : `<lotDir>/playwright`, chemin absolu. */
|
|
18
|
+
export function playwrightDir(lotDir) {
|
|
19
|
+
return join(resolve(lotDir), 'playwright');
|
|
20
|
+
}
|
|
21
|
+
/** Écrit `<lotDir>/mcp-<étape>.json` (et, si Playwright est chargé, le dossier `playwright/`) ; rend le chemin absolu du fichier. */
|
|
22
|
+
export function writeMcpConfig(lotDir, kind, visible, small = false) {
|
|
23
|
+
const dir = resolve(lotDir);
|
|
24
|
+
const out = playwrightDir(dir);
|
|
25
|
+
const servers = mcpServersFor(kind, visible, out, small);
|
|
26
|
+
if (servers.playwright)
|
|
27
|
+
mkdirSync(out, { recursive: true });
|
|
28
|
+
const file = join(dir, `mcp-${kind}.json`);
|
|
29
|
+
writeFileSync(file, `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`);
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
7
32
|
/** Outils interdits à toute session : pas de sous-agent, pas de push, de livraison ni de verdict. */
|
|
8
33
|
export const DISALLOWED = ['Agent', 'Bash(git push:*)', 'Bash(cadence deliver:*)', 'Bash(raf done:*)', 'Bash(raf review:*)', 'Bash(raf ux:*)'];
|
|
34
|
+
/** Outils MCP du serveur Playwright, par le préfixe du serveur (sonde réelle du 07-10 : `mcp__playwright` expose tous les `browser_*`). */
|
|
35
|
+
export const PLAYWRIGHT_TOOLS = 'mcp__playwright';
|
|
36
|
+
/** Agents passés à `--agents` : l'agent d'une étape qui charge Playwright, s'il a une liste d'outils, y gagne les outils du serveur (sinon il ne peut pas l'appeler). */
|
|
37
|
+
function agentsFor(spec, agents) {
|
|
38
|
+
const a = spec.agent ? agents[spec.agent] : undefined;
|
|
39
|
+
if (!spec.playwright || !spec.agent || !a?.tools)
|
|
40
|
+
return agents;
|
|
41
|
+
return { ...agents, [spec.agent]: { ...a, tools: [...a.tools, PLAYWRIGHT_TOOLS] } };
|
|
42
|
+
}
|
|
9
43
|
export function buildArgs(spec, agents) {
|
|
10
44
|
const args = ['-p', spec.brief, '--output-format', 'json', '--json-schema', JSON.stringify(spec.schema), '--model', spec.model];
|
|
11
45
|
if (spec.agent) {
|
|
12
46
|
if (!agents[spec.agent])
|
|
13
47
|
throw new RafError(`agent introuvable dans le paquet : ${spec.agent}`);
|
|
14
|
-
args.push('--agents', JSON.stringify(agents), '--agent', spec.agent);
|
|
48
|
+
args.push('--agents', JSON.stringify(agentsFor(spec, agents)), '--agent', spec.agent);
|
|
15
49
|
}
|
|
16
50
|
args.push('--session-id', spec.sessionId, '--permission-mode', spec.permissionMode);
|
|
17
51
|
for (const d of spec.addDirs)
|
|
18
52
|
args.push('--add-dir', d);
|
|
19
53
|
args.push('--disallowedTools', ...DISALLOWED);
|
|
54
|
+
if (spec.mcpConfig)
|
|
55
|
+
args.push('--strict-mcp-config', '--mcp-config', spec.mcpConfig);
|
|
20
56
|
return args;
|
|
21
57
|
}
|
|
22
58
|
/** Définitions d'agents du paquet (`agents/*.md`) au format de --agents. */
|
|
@@ -49,12 +85,14 @@ export function buildRetryArgs(spec, sessionId, agents) {
|
|
|
49
85
|
if (spec.agent) {
|
|
50
86
|
if (!agents[spec.agent])
|
|
51
87
|
throw new RafError(`agent introuvable dans le paquet : ${spec.agent}`);
|
|
52
|
-
args.push('--agents', JSON.stringify(agents), '--agent', spec.agent);
|
|
88
|
+
args.push('--agents', JSON.stringify(agentsFor(spec, agents)), '--agent', spec.agent);
|
|
53
89
|
}
|
|
54
90
|
args.push('--resume', sessionId, '--permission-mode', spec.permissionMode);
|
|
55
91
|
for (const d of spec.addDirs)
|
|
56
92
|
args.push('--add-dir', d);
|
|
57
93
|
args.push('--disallowedTools', ...DISALLOWED);
|
|
94
|
+
if (spec.mcpConfig)
|
|
95
|
+
args.push('--strict-mcp-config', '--mcp-config', spec.mcpConfig);
|
|
58
96
|
return args;
|
|
59
97
|
}
|
|
60
98
|
/**
|
|
@@ -64,7 +102,7 @@ export function buildRetryArgs(spec, sessionId, agents) {
|
|
|
64
102
|
* s'ajoutent à ceux de la session ; toujours rien après elle, c'est l'échec habituel.
|
|
65
103
|
*/
|
|
66
104
|
export async function runSession(spec, deps) {
|
|
67
|
-
const launch = (args) => deps.claude(args, { cwd: spec.cwd, env: { CADENCE_ORCHESTRATED: spec.wave }, timeoutMs: spec.timeoutMs, onSpawn: deps.onSpawn });
|
|
105
|
+
const launch = (args) => deps.claude(args, { cwd: spec.cwd, env: { CADENCE_ORCHESTRATED: spec.wave, ...(spec.nodeBin || spec.toolBin ? { PATH: [spec.toolBin, spec.nodeBin, process.env.PATH ?? ''].filter(Boolean).join(delimiter) } : {}) }, timeoutMs: spec.timeoutMs, onSpawn: deps.onSpawn });
|
|
68
106
|
const out = await launch(buildArgs(spec, deps.agents));
|
|
69
107
|
const first = classify(out);
|
|
70
108
|
const missing = first.kind === 'failed' && !out.timedOut && out.code === 0 ? lacksStructuredOutput(out.stdout) : null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export const realNodeFs = {
|
|
5
|
+
read: (f) => (existsSync(f) ? readFileSync(f, 'utf8') : null),
|
|
6
|
+
list: (d) => (existsSync(d) ? readdirSync(d) : []),
|
|
7
|
+
executable: (f) => {
|
|
8
|
+
try {
|
|
9
|
+
accessSync(f, constants.X_OK);
|
|
10
|
+
return statSync(f).isFile();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
/** Dossier des versions de nvm : `$NVM_DIR/versions/node`, par défaut `~/.nvm/versions/node`. */
|
|
18
|
+
export function nvmVersionsDir(env, home = homedir()) {
|
|
19
|
+
return join(env.NVM_DIR || join(home, '.nvm'), 'versions', 'node');
|
|
20
|
+
}
|
|
21
|
+
const parts = (v) => v.split('.').map(Number);
|
|
22
|
+
const cmp = (a, b) => {
|
|
23
|
+
const x = parts(a);
|
|
24
|
+
const y = parts(b);
|
|
25
|
+
for (let i = 0; i < 3; i++)
|
|
26
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
27
|
+
return (x[i] ?? 0) - (y[i] ?? 0);
|
|
28
|
+
return 0;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Node demandé par le `.nvmrc` à la racine d'un projet, résolu dans les versions installées de nvm : la plus haute
|
|
32
|
+
* dont le numéro commence par celui du fichier (`22`, `v22`, `22.22`, `v22.22.3`) et qui a un node exécutable. Pas de .nvmrc : rien à changer.
|
|
33
|
+
* .nvmrc illisible pour nous (`lts/*`, alias) ou version absente : refus nommant ce qui est demandé et où on a cherché
|
|
34
|
+
* — jamais de repli silencieux sur le node par défaut.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveNode(repo, versionsDir, fs = realNodeFs) {
|
|
37
|
+
const raw = fs.read(join(repo, '.nvmrc'));
|
|
38
|
+
if (raw === null)
|
|
39
|
+
return { kind: 'none' };
|
|
40
|
+
const wanted = raw.trim();
|
|
41
|
+
const m = /^v?(\d+(?:\.\d+){0,2})$/.exec(wanted);
|
|
42
|
+
if (!m)
|
|
43
|
+
return { kind: 'missing', message: `.nvmrc « ${wanted} » : version non résoluble (seuls 22, v22, 22.22, v22.22.3 sont compris)` };
|
|
44
|
+
const want = m[1];
|
|
45
|
+
const matching = fs
|
|
46
|
+
.list(versionsDir)
|
|
47
|
+
.map((n) => /^v(\d+\.\d+\.\d+)$/.exec(n)?.[1])
|
|
48
|
+
.filter((v) => !!v && (v === want || v.startsWith(`${want}.`)))
|
|
49
|
+
.sort(cmp);
|
|
50
|
+
if (matching.length === 0)
|
|
51
|
+
return { kind: 'missing', message: `.nvmrc ${wanted} : aucun Node installé correspondant dans ${versionsDir}` };
|
|
52
|
+
// Une version dont le bin n'a pas de node exécutable (install vide ou cassée) donnerait un dossier de liens vide,
|
|
53
|
+
// donc un repli silencieux sur le node par défaut : on l'écarte, et une plus basse valide peut être retenue.
|
|
54
|
+
const found = matching.filter((v) => fs.executable(join(versionsDir, `v${v}`, 'bin', 'node')));
|
|
55
|
+
if (found.length === 0) {
|
|
56
|
+
const names = matching.map((v) => `v${v}`).join(', ');
|
|
57
|
+
return { kind: 'missing', message: `.nvmrc ${wanted} : ${names} trouvée(s) dans ${versionsDir} mais sans node exécutable dans bin (installation vide ou cassée)` };
|
|
58
|
+
}
|
|
59
|
+
const best = found[found.length - 1];
|
|
60
|
+
const skipped = matching.filter((v) => cmp(v, best) > 0).reverse().map((v) => `v${v}`);
|
|
61
|
+
return { kind: 'ok', version: `v${best}`, wanted, bin: join(versionsDir, `v${best}`, 'bin'), ...(skipped.length ? { skipped } : {}) };
|
|
62
|
+
}
|
|
63
|
+
/** Les seuls binaires d'une version de Node que les sessions voient : le reste de son bin (raf, cadence, claude… installés en global) ne doit pas masquer ceux du PATH. */
|
|
64
|
+
export const NODE_LINKED = ['node', 'npm', 'npx', 'corepack'];
|
|
65
|
+
/**
|
|
66
|
+
* Dossier de liens `<runDir>/node-bin/<version>/` : un lien vers chacun des `NODE_LINKED` présents dans `bin`. Recréé à
|
|
67
|
+
* chaque appel (départ et reprise), donc idempotent. C'est lui, et non `bin`, qui passe en tête du PATH des sessions.
|
|
68
|
+
*/
|
|
69
|
+
export function linkNodeBin(runDir, version, bin) {
|
|
70
|
+
const dir = join(runDir, 'node-bin', version);
|
|
71
|
+
rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
mkdirSync(dir, { recursive: true });
|
|
73
|
+
for (const name of NODE_LINKED)
|
|
74
|
+
if (existsSync(join(bin, name)))
|
|
75
|
+
symlinkSync(join(bin, name), join(dir, name));
|
|
76
|
+
return dir;
|
|
77
|
+
}
|
|
@@ -90,7 +90,8 @@ const WAIT_REPORT_MS = 60_000;
|
|
|
90
90
|
*/
|
|
91
91
|
export async function acquireSlot(home, cap, opts) {
|
|
92
92
|
mkdirSync(slotsDir(home), { recursive: true });
|
|
93
|
-
const
|
|
93
|
+
const now = opts.now ?? Date.now; // injectable : les tests pilotent l'horloge
|
|
94
|
+
const t0 = now();
|
|
94
95
|
let reportedAt = null;
|
|
95
96
|
const pollMs = opts.pollMs ?? 2000;
|
|
96
97
|
const live = opts.slots ?? liveSlots; // injectable : les tests simulent une vague qui prend un créneau entre les deux comptées
|
|
@@ -103,15 +104,15 @@ export async function acquireSlot(home, cap, opts) {
|
|
|
103
104
|
continue;
|
|
104
105
|
if (live(home).length <= cap) {
|
|
105
106
|
if (reportedAt !== null)
|
|
106
|
-
opts.onGot?.(
|
|
107
|
+
opts.onGot?.(now() - t0);
|
|
107
108
|
return () => releaseLock(file, process.pid);
|
|
108
109
|
}
|
|
109
110
|
releaseLock(file, process.pid); // pris à plusieurs en même temps : on rend, on retente
|
|
110
111
|
break;
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
|
-
if (reportedAt === null ||
|
|
114
|
-
reportedAt =
|
|
114
|
+
if (reportedAt === null || now() - reportedAt >= (opts.reportMs ?? WAIT_REPORT_MS)) {
|
|
115
|
+
reportedAt = now();
|
|
115
116
|
opts.onWait?.(live(home), reportedAt - t0);
|
|
116
117
|
}
|
|
117
118
|
await new Promise((r) => setTimeout(r, pollMs + Math.random() * pollMs * 0.25));
|
|
@@ -30,7 +30,13 @@ export const REVIEW_SCHEMA = {
|
|
|
30
30
|
},
|
|
31
31
|
required: ['bloquants', 'majeurs', 'mineurs', 'constats', 'sousTaches', 'nonVerifie', 'verdict'],
|
|
32
32
|
};
|
|
33
|
-
|
|
33
|
+
/** Sortie du contrôle préalable : `dejaPresent` vaut « oui » (rien à faire), « partiel » ou « non ». */
|
|
34
|
+
export const PRECHECK_SCHEMA = {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: { dejaPresent: { type: 'string', enum: ['oui', 'partiel', 'non'] }, preuves: strings, resume: str },
|
|
37
|
+
required: ['dejaPresent', 'preuves', 'resume'],
|
|
38
|
+
};
|
|
39
|
+
export const schemaFor = (kind) => (kind === 'precheck' ? PRECHECK_SCHEMA : kind === 'implement' || kind === 'fix' ? WORK_SCHEMA : REVIEW_SCHEMA);
|
|
34
40
|
/** Contrôle la forme d'une sortie structurée : un champ absent ou mal typé est une erreur nommée. */
|
|
35
41
|
export function checkShape(value, schema) {
|
|
36
42
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, realpathSync, symlinkSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { join, sep } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
/** Racine du paquet en cours d'exécution (dist/orchestrate/ ou src/orchestrate/ → deux niveaux au-dessus). */
|
|
8
|
+
export const PACKAGE_ROOT = fileURLToPath(new URL('../..', import.meta.url));
|
|
9
|
+
/** Posée par le processus relancé depuis la copie : il tourne déjà sur l'instantané, il n'en refait pas. */
|
|
10
|
+
export const SNAPSHOT_ENV = 'CADENCE_SNAPSHOT';
|
|
11
|
+
/** Vague déjà réservée (et copiée) par le processus d'origine : le processus relancé la reprend telle quelle. */
|
|
12
|
+
export const RESERVED_ENV = 'CADENCE_WAVE_RESERVED';
|
|
13
|
+
/** Environnement sans les variables de relance : le fils les lit, il ne les transmet ni aux sessions ni aux commandes du projet. */
|
|
14
|
+
export function withoutLaunchVars(env) {
|
|
15
|
+
const { [SNAPSHOT_ENV]: _s, [RESERVED_ENV]: _r, ...rest } = env;
|
|
16
|
+
return rest;
|
|
17
|
+
}
|
|
18
|
+
const real = (p) => {
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(p);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Vrai fils : CADENCE_SNAPSHOT désigne CE paquet (la copie qui s'exécute). Une valeur héritée d'une autre vague ne compte pas. */
|
|
27
|
+
export const isSnapshotChild = (env, root = PACKAGE_ROOT) => !!env[SNAPSHOT_ENV] && real(env[SNAPSHOT_ENV]) === real(root);
|
|
28
|
+
/** Entrées de `package.json#bin` : nom de commande → fichier de `bin/`. */
|
|
29
|
+
const BIN_ENTRIES = { raf: 'raf.js', cadence: 'cadence.js' };
|
|
30
|
+
/** Ce du paquet qui sert à l'exécution d'une vague ; node_modules est lié, jamais copié. */
|
|
31
|
+
const COPIED = ['bin', 'dist', 'templates', 'agents', 'skills', 'package.json'];
|
|
32
|
+
export const toolDirOf = (waveDir) => join(waveDir, 'tool');
|
|
33
|
+
/** `<vague>/tool/bin` si la vague a un instantané (entrées `raf` et `cadence`), sinon rien : le PATH de la session ne change pas. */
|
|
34
|
+
export const toolBinOf = (waveDir) => (existsSync(join(toolDirOf(waveDir), 'bin', 'raf')) ? join(toolDirOf(waveDir), 'bin') : undefined);
|
|
35
|
+
/** Refus de prendre l'instantané (avant toute copie) : le message dit quoi faire. */
|
|
36
|
+
export class SnapshotRefusal extends Error {
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Le `node_modules` où les dépendances du paquet se résolvent réellement : celui du paquet, ou — installation npx ou
|
|
40
|
+
* locale — un dossier parent. On résout `yaml` depuis le paquet et on remonte jusqu'à son `node_modules`.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveModulesDir(packageRoot) {
|
|
43
|
+
let file;
|
|
44
|
+
try {
|
|
45
|
+
file = createRequire(join(packageRoot, 'package.json')).resolve('yaml');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new SnapshotRefusal(`dépendances introuvables : « yaml » ne se résout pas depuis ${packageRoot} (installation incomplète ?) — instantané impossible, aucune vague lancée`);
|
|
49
|
+
}
|
|
50
|
+
const marker = `${sep}node_modules${sep}`;
|
|
51
|
+
const at = file.lastIndexOf(marker);
|
|
52
|
+
if (at < 0)
|
|
53
|
+
throw new SnapshotRefusal(`dépendances introuvables : « yaml » résolu hors de tout node_modules (${file}) — instantané impossible, aucune vague lancée`);
|
|
54
|
+
return file.slice(0, at + marker.length - 1);
|
|
55
|
+
}
|
|
56
|
+
/** Instantané du paquet dans `<vague>/tool/` : le code, les gabarits et les agents d'une vague ne bougent plus. */
|
|
57
|
+
export function takeSnapshot(waveDir, packageRoot) {
|
|
58
|
+
const modules = resolveModulesDir(packageRoot); // avant toute copie
|
|
59
|
+
const tool = toolDirOf(waveDir);
|
|
60
|
+
mkdirSync(tool, { recursive: true });
|
|
61
|
+
for (const name of COPIED) {
|
|
62
|
+
const from = join(packageRoot, name);
|
|
63
|
+
if (existsSync(from))
|
|
64
|
+
cpSync(from, join(tool, name), { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
symlinkSync(modules, join(tool, 'node_modules'), 'dir');
|
|
67
|
+
// Comme npm à l'installation : `raf` et `cadence` sont les bin/*.js de la copie (shebang `env node` conservé).
|
|
68
|
+
for (const [name, target] of Object.entries(BIN_ENTRIES)) {
|
|
69
|
+
const js = join(tool, 'bin', target);
|
|
70
|
+
if (!existsSync(js))
|
|
71
|
+
continue;
|
|
72
|
+
chmodSync(js, 0o755);
|
|
73
|
+
symlinkSync(target, join(tool, 'bin', name));
|
|
74
|
+
}
|
|
75
|
+
return tool;
|
|
76
|
+
}
|
|
77
|
+
export const snapshotExists = (waveDir) => existsSync(join(toolDirOf(waveDir), 'bin', 'cadence.js'));
|
|
78
|
+
/** Relance réelle : un processus node sur la copie, signaux transmis, code de sortie rendu. */
|
|
79
|
+
export function spawnReexec(toolDir, argv, env, sink) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const child = spawn(process.execPath, [join(toolDir, 'bin', 'cadence.js'), 'orchestrate', ...argv], { cwd: sink.cwd, env: { ...env, [SNAPSHOT_ENV]: toolDir }, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
82
|
+
const lines = [createInterface({ input: child.stdout }).on('line', sink.out), createInterface({ input: child.stderr }).on('line', sink.err)];
|
|
83
|
+
const drained = Promise.all(lines.map((l) => new Promise((r) => l.once('close', r))));
|
|
84
|
+
const relay = (sig) => () => child.kill(sig);
|
|
85
|
+
const handlers = ['SIGINT', 'SIGTERM', 'SIGHUP'].map((s) => [s, relay(s)]);
|
|
86
|
+
for (const [s, h] of handlers)
|
|
87
|
+
process.on(s, h);
|
|
88
|
+
child.once('error', reject);
|
|
89
|
+
child.once('close', async (code, signal) => {
|
|
90
|
+
for (const [s, h] of handlers)
|
|
91
|
+
process.off(s, h);
|
|
92
|
+
await drained;
|
|
93
|
+
// Le fils est mort d'un signal (il se nettoie puis se le renvoie) : on meurt du même, comme sans instantané.
|
|
94
|
+
if (signal)
|
|
95
|
+
process.kill(process.pid, signal);
|
|
96
|
+
resolve(code ?? 1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -18,7 +18,15 @@ function review(l) {
|
|
|
18
18
|
const fixes = l.pass === 0 ? '' : ` après ${l.pass} correction${l.pass > 1 ? 's' : ''}`;
|
|
19
19
|
if (l.status === 'ready')
|
|
20
20
|
return `conforme${fixes}`;
|
|
21
|
-
|
|
21
|
+
const verdict = l.code.conforme ? 'conforme' : `non conforme (${l.code.bloquants} bloquant, ${l.code.majeurs} majeur)`;
|
|
22
|
+
// Rendu, échoué ou suspendu pour une autre cause que la revue (dépôt sale, session coupée…) : la colonne ne présente pas
|
|
23
|
+
// un verdict de revue comme la cause ; elle donne le dernier verdict réel, la cause est dans les lignes sous le tableau.
|
|
24
|
+
const stopped = l.status === 'handed-back' || l.status === 'failed' || l.status === 'suspended' || l.status === 'question';
|
|
25
|
+
const last = l.steps[l.steps.length - 1];
|
|
26
|
+
const byReview = !l.code.conforme && !!last && last.status === 'ok' && (last.kind === 'review' || last.kind === 'review-small');
|
|
27
|
+
if (stopped && !byReview)
|
|
28
|
+
return `${verdict} (dernier verdict ; rendu pour une autre cause)`;
|
|
29
|
+
return verdict;
|
|
22
30
|
}
|
|
23
31
|
function ux(l) {
|
|
24
32
|
if (!l.visible)
|
|
@@ -51,6 +59,8 @@ export function renderTable(wave, lots) {
|
|
|
51
59
|
for (const l of lots) {
|
|
52
60
|
if (l.uxNote)
|
|
53
61
|
out.push(`${l.project}:${l.lot} — ${l.uxNote}`);
|
|
62
|
+
if (l.uxCaptures)
|
|
63
|
+
out.push(`${l.project}:${l.lot} — captures Playwright : ${l.uxCaptures}`);
|
|
54
64
|
for (const w of l.warnings)
|
|
55
65
|
out.push(`${l.project}:${l.lot} — ⚠ ${w}`);
|
|
56
66
|
for (const x of l.choix ?? [])
|
package/dist/plan.js
CHANGED
|
@@ -122,6 +122,35 @@ export class Plan {
|
|
|
122
122
|
}
|
|
123
123
|
return { patterns, invalid };
|
|
124
124
|
}
|
|
125
|
+
/** Commits acquittés (`raf ignore`) : exemptés de « sans lot » et d'« inconnu », par sha complet ou par sujet exact. */
|
|
126
|
+
get acknowledged() {
|
|
127
|
+
const raw = this.doc.get('acknowledged');
|
|
128
|
+
if (!isSeq(raw))
|
|
129
|
+
return [];
|
|
130
|
+
return raw.toJSON().flatMap((e) => {
|
|
131
|
+
const o = (e ?? {});
|
|
132
|
+
const sha = o.sha == null ? undefined : String(o.sha);
|
|
133
|
+
const subject = o.subject == null ? undefined : String(o.subject);
|
|
134
|
+
if (!sha && !subject)
|
|
135
|
+
return [];
|
|
136
|
+
return [{ sha, subject, date: String(o.date ?? ''), reason: o.reason == null ? undefined : String(o.reason) }];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** Acquitte un commit : une ligne datée dans la section `acknowledged:` du plan. */
|
|
140
|
+
acknowledge(entry) {
|
|
141
|
+
this.writable();
|
|
142
|
+
// Déjà acquitté (même sha, ou même sujet pour une entrée par sujet) : on garde la première ligne et son motif.
|
|
143
|
+
if (this.acknowledged.some((a) => (entry.sha ? a.sha === entry.sha : a.sha === undefined && a.subject === entry.subject)))
|
|
144
|
+
return;
|
|
145
|
+
let list = this.doc.get('acknowledged');
|
|
146
|
+
if (!isSeq(list)) {
|
|
147
|
+
list = this.doc.createNode([]);
|
|
148
|
+
this.doc.set('acknowledged', list);
|
|
149
|
+
}
|
|
150
|
+
const node = this.doc.createNode(Object.fromEntries(Object.entries(entry).filter(([, v]) => v !== undefined)));
|
|
151
|
+
node.flow = true;
|
|
152
|
+
list.add(node);
|
|
153
|
+
}
|
|
125
154
|
/** Fichier de configuration lu pour ce plan, null quand on n'en connaît pas (cadence.yaml à la racine alors). */
|
|
126
155
|
get configFile() {
|
|
127
156
|
return this.settings.config ?? null;
|
package/dist/session.js
CHANGED
|
@@ -108,7 +108,7 @@ export function sessionStart(ctx, opts) {
|
|
|
108
108
|
done.push(`${recent.orphans.length} commit(s) sans lot`);
|
|
109
109
|
section(out, `Fait depuis ${opts.since}`, done);
|
|
110
110
|
section(out, 'Récurrent', recurringByDue(lots, today).map((l) => `${l.id} ${l.title} — ${dueLine(l, today)}`));
|
|
111
|
-
section(out, 'Écarts (raf check)', audit(plan, ctx.root, ctx.newsDir, today).map((i) =>
|
|
111
|
+
section(out, 'Écarts (raf check)', audit(plan, ctx.root, ctx.newsDir, today).map((i) => `${i.warning ? '⚠' : '✗'} ${i.message}`));
|
|
112
112
|
section(out, 'Dépôt', [repoLine(ctx.root).line]);
|
|
113
113
|
section(out, 'Effets en production (cadence verify)', ctx.effects ?? []);
|
|
114
114
|
section(out, 'Faits propres au projet', projectFacts(ctx, opts.since));
|
|
@@ -131,7 +131,7 @@ export function sessionClose(ctx, opts) {
|
|
|
131
131
|
const quiet = plan.lots().filter((l) => l.status === 'doing' && !isRecurring(l) && !recent.byLot.has(l.id));
|
|
132
132
|
section(out, 'Lots en cours', quiet.map((l) => `${l.id} ${l.title} — aucun commit sur la période : ${plan.readonly ? "le fermer ou l'annoter avec l'outil du projet" : 'raf done ou raf note'}`));
|
|
133
133
|
const issues = audit(plan, ctx.root, ctx.newsDir, today);
|
|
134
|
-
section(out, 'Écarts (raf check)', issues.map((i) =>
|
|
134
|
+
section(out, 'Écarts (raf check)', issues.map((i) => `${i.warning ? '⚠' : '✗'} ${i.message}`));
|
|
135
135
|
const repo = repoLine(ctx.root);
|
|
136
136
|
const lock = lockStatus(ctx.shared);
|
|
137
137
|
section(out, 'Dépôt', [repo.line, ...(lock ? [lock.line] : [])]);
|
|
@@ -149,7 +149,10 @@ export function sessionClose(ctx, opts) {
|
|
|
149
149
|
section(out, "Nettoyage : parcours interrompu, rien n'est proposé", [e instanceof Error ? e.message : String(e)]);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
else {
|
|
153
|
+
out(`\n(aucun motif de nettoyage déclaré — exemple de session.clean, dans cadence.yaml : session: { clean: [ "tmp/*", "~/partage/capture-*.png" ], cleanDays: 7 } ; chemins relatifs à la racine ou absolus, cleanDays = âge minimal en jours)`);
|
|
154
|
+
}
|
|
155
|
+
const open = issues.filter((i) => !i.warning).length + repo.open + (lock?.live ? 1 : 0);
|
|
153
156
|
out(open === 0 ? '\n✓ prêt à fermer' : `\n✗ pas fermé : ${open} point(s)`);
|
|
154
157
|
return open === 0 ? 0 : 1;
|
|
155
158
|
}
|
package/dist/state.js
CHANGED
|
@@ -61,38 +61,43 @@ export function readLock(dir) {
|
|
|
61
61
|
export function lockAlive(lock) {
|
|
62
62
|
return lock.unreadable ? lock.ageMs < 5_000 : pidAlive(lock.pid);
|
|
63
63
|
}
|
|
64
|
-
/** Pose
|
|
65
|
-
export function
|
|
66
|
-
const tmp =
|
|
67
|
-
writeFileSync(tmp,
|
|
64
|
+
/** Pose un fichier de façon atomique (lien vers un fichier complet : il n'existe jamais à moitié écrit) ; false s'il existe déjà (EEXIST) ; toute autre erreur est relancée. `tag` distingue les fichiers temporaires d'un même processus. */
|
|
65
|
+
export function linkNewFile(file, content, tag) {
|
|
66
|
+
const tmp = `${file}.${tag}.${Date.now()}.tmp`;
|
|
67
|
+
writeFileSync(tmp, content);
|
|
68
68
|
try {
|
|
69
|
-
linkSync(tmp,
|
|
69
|
+
linkSync(tmp, file);
|
|
70
70
|
return true;
|
|
71
71
|
}
|
|
72
|
-
catch {
|
|
73
|
-
|
|
72
|
+
catch (e) {
|
|
73
|
+
// Seul EEXIST veut dire « déjà là » ; EPERM, EMLINK… (pas de liens physiques ici) remontent au lieu de passer pour un verrou tenu.
|
|
74
|
+
if (e.code === 'EEXIST')
|
|
75
|
+
return false;
|
|
76
|
+
throw e;
|
|
74
77
|
}
|
|
75
78
|
finally {
|
|
76
79
|
rmSync(tmp, { force: true });
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Retire un verrou périmé seulement s'il est encore celui qu'on a lu : il est écarté par renommage, `same` relit
|
|
84
|
+
* l'écart, et s'il a changé il est rendu à son nouveau porteur. Partagé par le verrou de livraison et celui de l'URL UX.
|
|
85
|
+
*/
|
|
86
|
+
export function removeStaleFile(file, same) {
|
|
87
|
+
const aside = `${file}.stale.${process.pid}`;
|
|
82
88
|
try {
|
|
83
|
-
renameSync(
|
|
89
|
+
renameSync(file, aside);
|
|
84
90
|
}
|
|
85
91
|
catch {
|
|
86
92
|
return false;
|
|
87
93
|
}
|
|
88
|
-
|
|
89
|
-
if (now && now.pid === seen.pid && now.sha === seen.sha && now.started === seen.started) {
|
|
94
|
+
if (same(aside)) {
|
|
90
95
|
rmSync(aside, { force: true });
|
|
91
96
|
return true;
|
|
92
97
|
}
|
|
93
98
|
// Un autre processus a pris le verrou entre-temps : le lui rendre.
|
|
94
99
|
try {
|
|
95
|
-
linkSync(aside,
|
|
100
|
+
linkSync(aside, file);
|
|
96
101
|
}
|
|
97
102
|
catch {
|
|
98
103
|
// Un troisième l'a déjà repris : le sien fait foi.
|
|
@@ -100,6 +105,21 @@ export function removeStaleLock(dir, seen) {
|
|
|
100
105
|
rmSync(aside, { force: true });
|
|
101
106
|
return false;
|
|
102
107
|
}
|
|
108
|
+
/** Cause d'une pose de verrou impossible, en une ligne : « verrou <chemin> : <errno> » (le code, sinon le message). */
|
|
109
|
+
export function lockFault(file, e) {
|
|
110
|
+
return `verrou ${file} : ${e?.code ?? (e instanceof Error ? e.message : String(e))}`;
|
|
111
|
+
}
|
|
112
|
+
/** Pose le verrou de façon atomique (lien vers un fichier complet) ; false s'il existe déjà. */
|
|
113
|
+
export function writeLock(dir, lock) {
|
|
114
|
+
return linkNewFile(lockPath(dir), JSON.stringify(lock), lock.pid);
|
|
115
|
+
}
|
|
116
|
+
/** Retire un verrou périmé seulement s'il est encore celui qu'on a lu ; sinon le laisse en place. */
|
|
117
|
+
export function removeStaleLock(dir, seen) {
|
|
118
|
+
return removeStaleFile(lockPath(dir), (aside) => {
|
|
119
|
+
const now = readLockFile(aside);
|
|
120
|
+
return !!now && now.pid === seen.pid && now.sha === seen.sha && now.started === seen.started;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
103
123
|
/** Retire le verrou s'il appartient à ce processus. */
|
|
104
124
|
export function releaseLock(dir, pid) {
|
|
105
125
|
const lock = readLock(dir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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
|
@@ -14,7 +14,13 @@ commands (its CLAUDE.md names them), never with `raf start|done|note|ux|review`.
|
|
|
14
14
|
|
|
15
15
|
## Limits that always apply
|
|
16
16
|
|
|
17
|
-
- **At most two subagents running at once.** Queue the rest.
|
|
17
|
+
- **At most two subagents running at once.** Queue the rest. The sessions of an orchestrated wave
|
|
18
|
+
(section 2b) count toward this limit: orchestrate sessions plus subagents never exceed two. A wave
|
|
19
|
+
already caps itself (`--max-sessions`, 2 by default, shared by every wave); before starting a
|
|
20
|
+
subagent, check `cadence orchestrate --status`, and while a wave runs, start none beyond the free
|
|
21
|
+
slots. Count the free slots from what `--status` shows per wave, not from the live sessions (a
|
|
22
|
+
wave's steps take a slot and give it back between two steps): 2 − Σ min(cap, number of repositories
|
|
23
|
+
of the wave that still have lots). Never raise `--max-sessions` above two to speed a wave up.
|
|
18
24
|
- **Never two subagents in the same repository at the same time**, and the lead does not commit in a
|
|
19
25
|
repository where a subagent is working.
|
|
20
26
|
- **Deliveries and `raf ux` / `raf review` verdicts are done by the lead, one project at a time** —
|
|
@@ -35,11 +41,12 @@ progress, then drift that blocks a delivery, then ready quick wins. **Stop and w
|
|
|
35
41
|
## 2. Delegation
|
|
36
42
|
|
|
37
43
|
The default way to delegate is **`cadence orchestrate`** (section 2b): a program, not a conversation, that runs
|
|
38
|
-
one fresh short session per step. When the lead delegates by hand, the brief is the template
|
|
39
|
-
`templates/orchestrate/implement.md` of the cadence package — the single source, tested
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
one fresh short session per step; the wave runs from a frozen copy of the cadence package, so it can orchestrate cadence itself and its templates can be edited meanwhile. When the lead delegates by hand, the brief is the template
|
|
45
|
+
`templates/orchestrate/implement.md` of the cadence package — the single source, tested. The lead runs
|
|
46
|
+
`cd <project> && raf start <lot>`, then starts from the brief written by `cadence orchestrate --dry-run
|
|
47
|
+
<project>:<lot>`: it is already rendered (`{{chemin}}`, `{{lot}}`, `{{titre}}`, `{{objectif}}` and `{{news}}`,
|
|
48
|
+
the News instruction of a `visible` lot, are filled in; it names no wave Playwright directory — a hand delegation has none — so the screenshot goes under the OS temp directory, then into `docs/nouveautes/captures/`). The lead only adjusts the goal to what done looks
|
|
49
|
+
like (the human's words) and keeps the rest verbatim.
|
|
43
50
|
|
|
44
51
|
A lot that adds or changes a screen is `visible`: after the implementation, have the
|
|
45
52
|
`ux-reviewer` agent review it (give it the URL or the way to run the app) and bring its verdict and
|
|
@@ -57,7 +64,9 @@ cadence orchestrate <project>:<lot> <project>:<lot>@haiku … [--budget 2M]
|
|
|
57
64
|
`cadence orchestrate --dry-run …` first when a precondition is in doubt. The program, not the lead, runs
|
|
58
65
|
for each lot a fresh short session per step — implementation (Sonnet), UX review if the lot is `visible`
|
|
59
66
|
and the project declares how to see its app (`orchestrate.ux` in `cadence.yaml`), code review last
|
|
60
|
-
(Opus, the `code-reviewer` agent
|
|
67
|
+
(Opus, the `code-reviewer` agent; Sonnet, with no pass on the minor findings, for a lot whose estimate is
|
|
68
|
+
≤ 0.25 d — `orchestrate.review` in `cadence.yaml`; a blocker or major finding still brings a correction and an
|
|
69
|
+
Opus review), a correction in a new session if the review is not compliant (two
|
|
61
70
|
passes at most) — and records `raf review` itself when the code review is compliant. The state is in
|
|
62
71
|
`.cadence/runs/<wave>/`, not in this conversation. `@haiku` only when the human writes it, for a
|
|
63
72
|
mechanical lot. Exit code: 0 all ready · 1 some lots handed back · 2 refused before acting · 3 wave
|
|
@@ -67,7 +76,7 @@ What the orchestrator does **not** do, and stays with the lead: choose the lots
|
|
|
67
76
|
questions back (`--resume --answer <project>:<lot> "…"`), look at the UX reviewer's captures and record
|
|
68
77
|
`raf ux` (the orchestrator reports its verdict, it never records it), re-verify (section 3, point 2),
|
|
69
78
|
`raf done`, push and deliver. Minor findings and proposed sub-tasks come back in the table: adding them to
|
|
70
|
-
the plan is the lead's decision. If an orchestrated wave is running in a repository, do not commit there
|
|
79
|
+
the plan is the lead's decision. A minor finding becomes a lot of its own only if it describes an observable bug (a wrong output, a crash, a measured regression); otherwise it stays a note of the originating lot, so that a review never feeds the next one. If an orchestrated wave is running in a repository, do not commit there
|
|
71
80
|
and do not deliver it (`cadence deliver` refuses).
|
|
72
81
|
|
|
73
82
|
## 3. Check
|
|
@@ -17,6 +17,9 @@ Minor choices to settle yourself, writing the alternative you did not take:
|
|
|
17
17
|
a spacing, colour or label value when a measurement or a rule justifies it; the URL of a link (the most general official page if a precise one is not certain);
|
|
18
18
|
a News entry: only if a finding asks for it; a written rule of the project's CLAUDE.md (apply it); who launches the review or UX pass (never the session: the program and the lead do).
|
|
19
19
|
"not my job to touch the plan" is not a question: say it in the report.
|
|
20
|
+
Never stop to ask about how to organise your own work (split or squash a commit, whether to continue): decide, list it under "choix", go on. A finding you contest is noted under "choix" with the evidence, not raised to the lead.
|
|
20
21
|
You are one short session of an orchestrated wave: do not launch subagents (the Agent tool is disabled).
|
|
21
22
|
Give your final report as the structured output (commits, tests, build, what you could not verify, choices).
|
|
22
23
|
{{reponse}}
|
|
24
|
+
{{captures}}
|
|
25
|
+
Browser (Playwright): if a Playwright MCP server is available in this session, it was launched by the orchestrator with its own output directory, in the orchestrator's run directory outside the repository: give screenshots, snapshots and traces a relative file name only (e.g. `page-home.png`), they land there. Never an absolute path, never the `--output-dir` option (the server's launch argument, which a session cannot set). 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.
|
|
@@ -16,6 +16,10 @@ otherwise it is a choice: decide, write the alternative you did not take, go on.
|
|
|
16
16
|
a spacing, colour or label value when a measurement or a rule justifies it; the URL of a link (the most general official page if a precise one is not certain);
|
|
17
17
|
a News entry: only if a finding asks for it; a written rule of the project's CLAUDE.md (apply it); who launches the review or UX pass (never the session: the program and the lead do).
|
|
18
18
|
"not my job to touch the plan" is not a question: say it in the report.
|
|
19
|
+
Never stop to ask about how to organise your own work (split or squash a commit, order of the sub-parts, whether to continue, "I have no question"): decide, list it under "choix", go on. Leave "questions" empty rather than writing a placeholder such as "no question".
|
|
20
|
+
A finding you contest (it describes a state older than the code, or you think it wrong) is not a question and not a reason to stop: do not ask the lead to arbitrate. Write it under "choix" with the evidence (the command, the line, the commit), fix what is right, go on; the next fresh review re-reads it.
|
|
19
21
|
You are one short session of an orchestrated wave: do not launch subagents (the Agent tool is disabled).
|
|
20
22
|
Give your final report as the structured output (commits, tests, build, what you could not verify, choices, questions).
|
|
21
23
|
{{reponse}}
|
|
24
|
+
{{captures}}
|
|
25
|
+
Browser (Playwright): if a Playwright MCP server is available in this session, it was launched by the orchestrator with its own output directory, in the orchestrator's run directory outside the repository: give screenshots, snapshots and traces a relative file name only (e.g. `page-home.png`), they land there. Never an absolute path, never the `--output-dir` option (the server's launch argument, which a session cannot set). 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.
|
|
@@ -5,14 +5,16 @@ Rules: test first; commit each sub-part as soon as its tests pass, with explicit
|
|
|
5
5
|
`git add -A` or `commit -a`), and a message that cites the lot (`feat({{lot}}): …`); run the project's
|
|
6
6
|
full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
|
|
7
7
|
`raf review`.
|
|
8
|
+
Documentation: the project's README (and its usage documentation) describes the behaviour you deliver, updated in the same commits, like the CHANGELOG — a README that does not follow your change is a major finding of the review.
|
|
8
9
|
Decide minor interpretation questions yourself (the wording of a message, a name, a default, the
|
|
9
10
|
reading of an ambiguous line of the lot) and list each one under "choix" in your report, with the
|
|
10
11
|
alternative you did not take — the reviewer re-reads them. Stop and ask (under "questions") only on a real blocker.
|
|
11
12
|
A question is legitimate only if it names what it would change: the scope of the lot, the architecture, or a costly rollback (data, production, public API), AND the plan, its notes and the project's CLAUDE.md do not settle it;
|
|
12
13
|
otherwise it is a choice: decide, write the alternative you did not take, go on. Minor choices to settle yourself:
|
|
13
14
|
a spacing, colour or label value when a measurement or a rule justifies it; the URL of a link (the most general official page if a precise one is not certain);
|
|
14
|
-
a
|
|
15
|
+
a written rule of the project's CLAUDE.md (apply it); who launches the review or UX pass (never the session: the program and the lead do).
|
|
15
16
|
"not my job to touch the plan" is not a question: say it in the report.
|
|
17
|
+
Never stop to ask about how to organise your own work (split or squash a commit, order of the sub-parts, whether to continue, "I have no question"): decide, list it under "choix", go on. Leave "questions" empty rather than writing a placeholder such as "no question".
|
|
16
18
|
If the lot's commits already cover its open sub-tasks, do not ask whether to continue: make no commit and say so
|
|
17
19
|
in your report (the review that follows re-reads those commits). If they do not cover them all, implement the rest.
|
|
18
20
|
Report: commits (sha + subject), tests and build results with their numbers, what you could not
|
|
@@ -21,5 +23,7 @@ verify, choices made, open questions.
|
|
|
21
23
|
You are one short session of an orchestrated wave: do not launch subagents (the Agent tool is disabled).
|
|
22
24
|
Give your final report as the structured output, not as free text. If the plan is kept by the project's own tool
|
|
23
25
|
(read-only for `raf`), use that tool's commands named in the project's CLAUDE.md, never `raf start|done|note`.
|
|
26
|
+
{{news}}
|
|
24
27
|
{{commits}}
|
|
25
28
|
{{reponse}}
|
|
29
|
+
Browser (Playwright): if a Playwright MCP server is available in this session, it was launched by the orchestrator with its own output directory, in the orchestrator's run directory outside the repository: give screenshots, snapshots and traces a relative file name only (e.g. `page-home.png`), they land there. Never an absolute path, never the `--output-dir` option (the server's launch argument, which a session cannot set). 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.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Pre-check for lot `{{lot}}` — "{{titre}}" — of the repository `{{chemin}}`, as the read-only pre-check reader.
|
|
2
|
+
Question: is the deliverable of this lot ALREADY in the repository (done by another lot, or by a correction)?
|
|
3
|
+
Goal of the lot: {{objectif}}
|
|
4
|
+
Read the lot, its notes and sub-tasks in the plan (`docs/plan/raf.yaml`, or the file named by `plan:` in `cadence.yaml`),
|
|
5
|
+
then look for the deliverable in the code and tests (`git log`, `grep`, reading the files the lot names). Do not
|
|
6
|
+
implement anything and do not run the full test suite: this is a quick look.
|
|
7
|
+
Answer `oui` only when every part of the lot is present, with a proof for each (file:line or commit sha); `partiel` when
|
|
8
|
+
some parts are present (say which); `non` otherwise. When in doubt, answer `non`: an implementation session follows.
|
|
9
|
+
You are read-only: do not modify, commit, push or run `raf start|done|note|review|ux`. Do not launch subagents.
|
|
10
|
+
Give your report as the structured output: `dejaPresent` (oui | partiel | non), `preuves` (the proofs), `resume` (one sentence).
|
|
@@ -4,7 +4,9 @@ treated them. Check that the minor findings fixed are fixed, that the fixes brok
|
|
|
4
4
|
rest of the lot is still sound: read the diff yourself from the commits that cite the lot
|
|
5
5
|
(`raf commits {{lot}}`, or `git log`), run the checks, report real defects only. A new minor finding
|
|
6
6
|
is reported, not a reason to ask for another pass. This is a code review only.
|
|
7
|
+
{{checks}}
|
|
7
8
|
{{choix}}
|
|
9
|
+
A proposed sub-task describes an observable bug (a wrong output, a crash, a measured regression); any other minor finding stays a note of this lot, not a sub-task.
|
|
8
10
|
You are read-only: do not modify, commit, push or run `raf review|ux|done`. Do not launch subagents.
|
|
9
11
|
Give your report as the structured output: counts of blocking / major / minor findings, each finding
|
|
10
12
|
(severity, file, line, text), proposed sub-tasks, what you could not verify, and the one-line verdict
|