@sylad/cadence 1.32.0 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.32.0",
11
+ "version": "1.34.1",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
3
  "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
- "version": "1.32.0",
4
+ "version": "1.34.1",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -48,7 +48,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
48
48
  ## Where to read more
49
49
 
50
50
  - [docs/raf.md](docs/raf.md): the plan, its commands and its file.
51
- - [docs/orchestrate.md](docs/orchestrate.md): waves of sessions and their budgets, and which repositories a session may commit in.
51
+ - [docs/orchestrate.md](docs/orchestrate.md): waves, budgets, and [the edge-case probe](docs/orchestrate.md#the-edge-case-probe-orchestrateprobe-l497).
52
52
  - [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect.
53
53
  - [docs/spec.md](docs/spec.md): screen and back-end sheets.
54
54
  - [docs/ref.md](docs/ref.md): reference documents (specs, norms, mock-ups) and the coverage of their elements by the lots.
package/dist/articles.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { isOpen } from './plan.js';
4
4
  /** Titre du lot ouvert à la livraison. */
5
- export const articleTitle = (name) => `Article ${name} à rafraîchir`;
5
+ const articleTitle = (name) => `Article ${name} à rafraîchir`;
6
6
  const SECTIONS_SHOWN = 12;
7
7
  /** Titres d'un article : lignes `#` du Markdown (hors en-tête YAML) et balises `<h1>`…`<h6>` d'une page Astro ou MDX. */
8
8
  export function articleHeadings(text) {
package/dist/audit.js CHANGED
@@ -140,7 +140,7 @@ export function isReleaseOnly(sha, root) {
140
140
  }));
141
141
  }
142
142
  /** Le commit a été acquitté par « raf ignore » : par son sha, ou par son sujet exact. */
143
- export function isAcknowledged(plan, c) {
143
+ function isAcknowledged(plan, c) {
144
144
  return plan.acknowledged.some((a) => (a.sha !== undefined && a.sha === c.sha) || (a.sha === undefined && a.subject === c.subject));
145
145
  }
146
146
  /**
@@ -158,7 +158,7 @@ export function exemptPlanOnly(linked, plan, root) {
158
158
  return { ...linked, orphans, unknown: linked.unknown.filter((u) => !acked(u.commit)) };
159
159
  }
160
160
  /** Fenêtre de l'audit : --since, sinon la date d'adoption du plan, sinon 30 jours. */
161
- export function auditSince(plan, explicit) {
161
+ function auditSince(plan, explicit) {
162
162
  if (explicit)
163
163
  return explicit;
164
164
  // Une date seule vaudrait « ce jour-là à l'heure actuelle » pour git : minuit explicite.
@@ -168,7 +168,7 @@ export function auditSince(plan, explicit) {
168
168
  * Avertissements : un lot visible (hors abandonné) sans titre public — le site retombe sur le titre technique du plan,
169
169
  * sauf pour un lot terminé qu'une Nouveauté cite : le site en reprend le titre (cf. reusedNewsTitle).
170
170
  */
171
- export function missingPublicTitles(lots, entries) {
171
+ function missingPublicTitles(lots, entries) {
172
172
  return lots
173
173
  .filter((l) => l.visible && l.status !== 'dropped' && !l.public && reusedNewsTitle(l, entries) === null)
174
174
  .map((l) => ({ message: `${l.id} : lot visible sans titre public — raf public ${l.id} "…"`, warning: true }));
@@ -225,7 +225,7 @@ export function hypothesesOf(root, configFile) {
225
225
  * travail touchent des chemins sans toucher le document que la règle désigne. Les commits de plan ne comptent pas.
226
226
  * Un lot dont l'écart est acquitté et motivé (`raf docs-ok`, L317) n'est pas compté.
227
227
  */
228
- export function docSyncIssues(plan, root, byLot) {
228
+ function docSyncIssues(plan, root, byLot) {
229
229
  const { sync, since } = readDocsConfig(plan.configFile ?? join(root, 'cadence.yaml'));
230
230
  if (!sync.length)
231
231
  return [];
@@ -349,7 +349,7 @@ export function unreviewedWork(plan, root, lotId) {
349
349
  * à la journée près, on ne distingue pas un lot fermé avant l'activation d'un lot fermé après
350
350
  * (et `raf done` refuse de toute façon dès l'activation).
351
351
  */
352
- export function uxIssues(plan) {
352
+ function uxIssues(plan) {
353
353
  const since = plan.uxSince;
354
354
  if (!since)
355
355
  return [];
@@ -363,7 +363,7 @@ export function uxIssues(plan) {
363
363
  * pas : aucun verdict, ou un verdict antérieur à ces commits. Même règle de date que `uxIssues` ; un
364
364
  * lot sans commit n'a rien à faire relire.
365
365
  */
366
- export function reviewIssues(plan, root, byLot) {
366
+ function reviewIssues(plan, root, byLot) {
367
367
  const since = plan.reviewSince;
368
368
  if (!since)
369
369
  return [];
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ import { DEFAULT_TEST_PATTERNS } from './testgate.js';
6
6
  import { gitRoot } from './git.js';
7
7
  import { planCommitProblem } from './plan-commit.js';
8
8
  import { isComposedGate } from './orchestrate/workers.js';
9
+ import { booleanKey } from './yaml-keys.js';
9
10
  import { FIELDS, Plan, RafError, STATUSES } from './plan.js';
10
11
  const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit'];
11
12
  const ON_WRITE_KEYS = ['command', 'files'];
@@ -311,7 +312,7 @@ export function readSessionConfig(file) {
311
312
  return config;
312
313
  }
313
314
  const EFFORTS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
314
- export const EFFORT_STEPS = ['precheck', 'spec', 'maquette', 'fiche', 'accept', 'implement', 'fix', 'review', 'ux'];
315
+ const EFFORT_STEPS = ['precheck', 'spec', 'maquette', 'fiche', 'accept', 'implement', 'probe', 'fix', 'review', 'ux'];
315
316
  /** Clé `verify:` de cadence.yaml. Absente : `{}`. */
316
317
  export function readVerifyConfig(file) {
317
318
  if (!existsSync(file))
@@ -338,10 +339,10 @@ export function readVerifyConfig(file) {
338
339
  return { inPlace: v.inPlace };
339
340
  }
340
341
  const REVIEW_KEYS = ['threshold', 'light', 'full'];
341
- const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'gate', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
342
+ const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'gate', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'probe', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
342
343
  /** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
343
344
  export function readOrchestrateConfig(file) {
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 } };
345
+ 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', probe: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
345
346
  if (!existsSync(file))
346
347
  return config;
347
348
  let raw;
@@ -417,11 +418,9 @@ export function readOrchestrateConfig(file) {
417
418
  throw bad('maquetteReviews : entier supérieur ou égal à 1 attendu');
418
419
  config.maquetteReviews = o.maquetteReviews;
419
420
  }
420
- if (o.fiche != null) {
421
- if (typeof o.fiche !== 'boolean')
422
- throw bad('fiche : true ou false attendu');
423
- config.fiche = o.fiche;
424
- }
421
+ for (const k of ['fiche', 'probe'])
422
+ if (o[k] != null)
423
+ config[k] = booleanKey(o[k], bad(`${k} : true ou false attendu`));
425
424
  if (o.precheck != null) {
426
425
  if (typeof o.precheck !== 'boolean' && o.precheck !== 'local')
427
426
  throw bad('precheck : true, false ou local attendu');
@@ -534,7 +533,7 @@ export function readOrchestrateConfig(file) {
534
533
  }
535
534
  if (o.effort != null) {
536
535
  if (!isObject(o.effort))
537
- throw bad('effort doit être un objet { precheck, spec, maquette, fiche, accept, implement, fix, review, ux }');
536
+ throw bad('effort doit être un objet { precheck, spec, maquette, fiche, accept, implement, probe, fix, review, ux }');
538
537
  for (const [k, v] of Object.entries(o.effort)) {
539
538
  if (!EFFORT_STEPS.includes(k))
540
539
  throw bad(`effort.${k} inconnu (attendu : ${EFFORT_STEPS.join(', ')})`);
@@ -99,11 +99,10 @@ export function readJournals(runsDir, until, since) {
99
99
  const name = basename(file, '.json');
100
100
  const fixSteps = steps.filter((s) => s.kind === 'fix' && done(s));
101
101
  const passes = fixSteps.map((fix) => {
102
- const review = [...steps].reverse().find((s) => s.n < fix.n && isReview(s.kind) && s.report);
103
- // Une passe fix entre la revue et celle-ci a déjà traité ces constats : cette passe-ci (tests rouges) n'en a pas.
104
- const handled = review ? fixSteps.some((f) => f.n > review.n && f.n < fix.n) : false;
105
- const findings = review?.report && !handled ? findingsOf(readJson(join(waveDir, name, review.report))) : [];
106
- return { key: `${wave}/${name}/${fix.n}`, project: rec.project, lot: rec.lot, visible: !!rec.visible, findings, commits: fix.commits ?? [], ...(fix.ended ? { fixEnded: fix.ended } : {}), ...(findings.length > 0 && review ? { reviewKind: review.kind } : {}) };
102
+ const review = [...steps].reverse().find((s) => s.n < fix.n && ((isReview(s.kind) && s.report) || (s.kind === 'probe' && Array.isArray(s.constats) && s.constats.length > 0)));
103
+ // La revue (ou la sonde, L497, qui porte ses constats dans son étape) qui précède. Une passe fix entre elle et celle-ci a déjà traité ces constats : cette passe-ci (tests rouges) n'en a pas.
104
+ const findings = !review || fixSteps.some((f) => f.n > review.n && f.n < fix.n) ? [] : review.kind === 'probe' ? review.constats.filter((t) => typeof t === 'string') : review.report ? findingsOf(readJson(join(waveDir, name, review.report))) : [];
105
+ return { key: `${wave}/${name}/${fix.n}`, project: rec.project, lot: rec.lot, visible: !!rec.visible, findings, commits: fix.commits ?? [], ...(fix.ended ? { fixEnded: fix.ended } : {}), ...(findings.length > 0 && review && review.kind !== 'probe' ? { reviewKind: review.kind } : {}) };
107
106
  });
108
107
  const precheck = precheckVerdictOf(rec, steps);
109
108
  const esc = rec.escalation;
@@ -296,9 +295,9 @@ const SOUS_DEFINITIONS = {
296
295
  autre: 'tout bug de code qui n\'entre dans aucune des valeurs précédentes',
297
296
  };
298
297
  /** Les maillons de la chaîne où un mécanisme peut être posé (R2). */
299
- export const MAILLONS = ['brief implement', 'auto-vérification avant rendu', 'porte locale', 'fiche'];
298
+ const MAILLONS = ['brief implement', 'auto-vérification avant rendu', 'porte locale', 'fiche'];
300
299
  /** Le maillon dont relève chaque sous-cause (R2, hypothèse) ; `autre` n'en désigne aucun. */
301
- export const MAILLON_DE = {
300
+ const MAILLON_DE = {
302
301
  'cas-limite': 'brief implement',
303
302
  'test-vide': 'brief implement',
304
303
  'affirmation-fausse': 'auto-vérification avant rendu',
@@ -308,8 +307,8 @@ export const MAILLON_DE = {
308
307
  autre: null,
309
308
  };
310
309
  /** Seuils de désignation du maillon dominant (R2, hypothèse) : passes sous-classées au moins, et part minimale du maillon en tête, en %. */
311
- export const MAILLON_MIN_PASSES = 10;
312
- export const MAILLON_MIN_PART = 40;
310
+ const MAILLON_MIN_PASSES = 10;
311
+ const MAILLON_MIN_PART = 40;
313
312
  /** Extrait une sous-cause connue d'une réponse (JSON `{sous}`) ; null sinon. */
314
313
  export function parseSousCause(text) {
315
314
  try {
@@ -428,7 +427,7 @@ export function sousCausesRanked(b) {
428
427
  return SOUS_CAUSES.map((sous, i) => ({ sous, label: SOUS_LABELS[sous], passes: b.parSousCause[sous], i })).filter((x) => x.passes > 0).sort((a, b2) => b2.passes - a.passes || a.i - b2.i);
429
428
  }
430
429
  /** La section « Bug de code par sous-cause » de `cadence corrections` (A1) : sous-cause, passes, part des passes sous-classées, maillon. */
431
- export function renderSousCauses(b) {
430
+ function renderSousCauses(b) {
432
431
  const out = ['### Bug de code par sous-cause', '', `${b.classees} passes sous-classées sur ${b.classees + b.sansSousCause}${b.sansSousCause > 0 ? ` · sans sous-cause : ${b.sansSousCause} (à classer : cadence corrections --classify)` : ''}.`, ''];
433
432
  if (b.classees > 0)
434
433
  out.push('| Sous-cause | Passes | Part | Maillon |', '|---|---|---|---|', ...sousCausesRanked(b).map((r) => `| ${r.label} | ${r.passes} | ${pct(r.passes, b.classees)} | ${MAILLON_DE[r.sous] ?? 'aucun'} |`), '');
package/dist/dates.js CHANGED
@@ -27,7 +27,7 @@ export function addDays(day, n) {
27
27
  d.setUTCDate(d.getUTCDate() + n);
28
28
  return format(d);
29
29
  }
30
- export function isWeekend(day) {
30
+ function isWeekend(day) {
31
31
  const w = parse(day).getUTCDay();
32
32
  return w === 0 || w === 6;
33
33
  }
package/dist/deliver.js CHANGED
@@ -14,7 +14,7 @@ import { appendDelivery, appendFailedDelivery, lastDelivery, lockAlive, lockFaul
14
14
  const POLL_CI = 15_000;
15
15
  const CI_APPEAR = 300_000;
16
16
  /** Intervalle de réessai d'une vérification : celui de deliver ET de `cadence verify --retry`. */
17
- export const POLL_VERIFY = 10_000;
17
+ const POLL_VERIFY = 10_000;
18
18
  const CI_OK = new Set(['success', 'skipped', 'neutral']);
19
19
  const GH_TIMEOUT = 60_000;
20
20
  export const TIMED_OUT = 124;
@@ -474,7 +474,7 @@ async function waitCi(ctx, deps, sha, env) {
474
474
  }
475
475
  }
476
476
  /** Un essai d'une vérification : cause de l'échec, ou null. Partagé par deliver et `cadence verify`. */
477
- export async function tryCheck(c, deps, sha, env, budgetMs) {
477
+ async function tryCheck(c, deps, sha, env, budgetMs) {
478
478
  if (c.command !== undefined) {
479
479
  const code = await deps.exec(c.command, env, budgetMs);
480
480
  return code === 0 ? null : codeText(code);
@@ -2,15 +2,15 @@ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSy
2
2
  import { basename, join, resolve } from 'node:path';
3
3
  import { cadenceHome } from './orchestrate/registry.js';
4
4
  /** Au-delà, le chiffre écrit par la bande est trop vieux pour guider un arrêt (la bande le rafraîchit toutes les 5 s). */
5
- export const HUD_CONTEXT_MAX_AGE_MS = 120_000;
5
+ const HUD_CONTEXT_MAX_AGE_MS = 120_000;
6
6
  /** Un fichier de session plus vieux que cela est ignoré et supprimé à la lecture (une session fermée ne le rafraîchit plus). */
7
- export const HUD_CONTEXT_PURGE_MS = 24 * 3600_000;
7
+ const HUD_CONTEXT_PURGE_MS = 24 * 3600_000;
8
8
  /** Dossier où la bande cadence-hud publie le contexte de chaque session : un fichier `<id de session>.json` par session. */
9
- export function hudContextDir(env = process.env) {
9
+ function hudContextDir(env = process.env) {
10
10
  return join(cadenceHome(env), 'hud-context');
11
11
  }
12
12
  /** Le nom de fichier d'une session : le même nettoyage que le script d'écriture de la bande (lettres, chiffres, `-`, `_`). */
13
- export function hudContextFile(session) {
13
+ function hudContextFile(session) {
14
14
  return `${session.replace(/[^A-Za-z0-9_-]/g, '_')}.json`;
15
15
  }
16
16
  function sameFolder(a, b) {
@@ -56,7 +56,7 @@ function livingFiles(dir, nowMs) {
56
56
  return kept;
57
57
  }
58
58
  /** La variable que Claude Code pose dans l'environnement de ses commandes Bash : l'id de la session, nom du fichier que la bande écrit. */
59
- export const SESSION_ID_ENV = 'CLAUDE_CODE_SESSION_ID';
59
+ const SESSION_ID_ENV = 'CLAUDE_CODE_SESSION_ID';
60
60
  /**
61
61
  * Le contexte publié par la bande pour la session de l'appelant, dans l'ordre : `session` quand on la désigne, sinon la session de la
62
62
  * variable `CLAUDE_CODE_SESSION_ID` ; seulement si elle est absente ET qu'un seul fichier publie pour `cwd`, celui-là (deux sessions
package/dist/lead.js CHANGED
@@ -64,7 +64,7 @@ export function findLeadDir(start) {
64
64
  }
65
65
  }
66
66
  /** Seuil de contexte du lead quand `lead.contextThreshold` n'est pas posé (L437). */
67
- export const CONTEXT_THRESHOLD_DEFAULT = 150_000;
67
+ const CONTEXT_THRESHOLD_DEFAULT = 150_000;
68
68
  /** Clé `lead.contextThreshold` du cadence.yaml du dossier (L437) : le nombre de tokens de contexte à partir duquel le lead ne lance plus de vague. En tokens et non en % : 60 % d'une fenêtre de 1M valait 600k. Un entier ≥ 1, sinon refus. */
69
69
  export function readLeadContextThreshold(parent) {
70
70
  const found = readLeadBlock(parent, 'lead.contextThreshold', 'un entier ≥ 1 (tokens)');
@@ -94,15 +94,15 @@ export function readDepth(parent) {
94
94
  throw new RafError(`${file} : depth doit être un entier ≥ 1`);
95
95
  return d;
96
96
  }
97
- export const NOTES_MAX = 120;
97
+ const NOTES_MAX = 120;
98
98
  export const TITLE_MAX = 60;
99
99
  /** Au-delà de ce nombre de jours sans activité, un lot en cours est dit silencieux. */
100
- export const TOUR_IDLE = 3;
100
+ const TOUR_IDLE = 3;
101
101
  /** Un lot créé depuis moins de ce nombre de jours (aujourd'hui compris : 0 à 6 jours d'écart) compte parmi les « ajoutés cette semaine ». */
102
- export const ADDED_DAYS = 7;
102
+ const ADDED_DAYS = 7;
103
103
  const empty = (project) => ({ project, doing: [], drift: [], notes: [], next: null, progress: { done: 0, doing: 0, todo: 0, added7: 0 }, repo: [] });
104
104
  /** Les lots non clos du plan, hors récurrents, chacun suivi de ses sous-tâches non closes ; celles d'un lot clos ou récurrent sortent avec lui. */
105
- export function openLots(lots) {
105
+ function openLots(lots) {
106
106
  return lots
107
107
  .filter((l) => isOpen(l.status) && !isRecurring(l))
108
108
  .flatMap((l) => [
package/dist/link.js CHANGED
@@ -1,4 +1,4 @@
1
- export function isMerge(c) {
1
+ function isMerge(c) {
2
2
  return /^Merge\b/.test(c.subject);
3
3
  }
4
4
  /** Portée d'un sujet « type(L24,L25)! : … » ; null sans parenthèses. */
package/dist/news.js CHANGED
@@ -197,7 +197,7 @@ export function newsIssues(lots, entries, dir, hypotheses = new Map()) {
197
197
  }
198
198
  return issues;
199
199
  }
200
- export function slugify(title) {
200
+ function slugify(title) {
201
201
  return (title
202
202
  .normalize('NFD')
203
203
  .replace(/[̀-ͯ]/g, '')
@@ -90,6 +90,7 @@ const FILES = {
90
90
  accept: 'accept.md',
91
91
  maquette: 'maquette.md',
92
92
  implement: 'implement.md',
93
+ probe: 'probe.md',
93
94
  fix: 'fix.md',
94
95
  'fix-minors': 'fix-minors.md',
95
96
  'fix-docs': 'fix-docs.md',
@@ -33,7 +33,7 @@ import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
33
33
  import { groupLots, runPool } from './pool.js';
34
34
  import { repoBranch } from './integrate.js';
35
35
  import { baseBranchOf, keptLines, keptTrees, lotBranch, lotTreeDir, planFileSet, recoverTree, removeTree } from './worktree.js';
36
- import { schemaFor } from './schemas.js';
36
+ import { agentOf, schemaFor } from './schemas.js';
37
37
  import { resolveLotRepos } from '../repos.js';
38
38
  import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
39
39
  import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
@@ -302,7 +302,7 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
302
302
  refusals.push(`${key} : ${e.message}`);
303
303
  continue;
304
304
  }
305
- if (info && olderThanEffort(info.version) && Object.values(env.config.effort).some((e) => e !== 'default')) {
305
+ if (info && olderThanEffort(info.version) && Object.entries(env.config.effort).some(([step, e]) => e !== 'default' && (step !== 'probe' || env.config.probe))) {
306
306
  refusals.push(`${key} : claude ${info.version} n'a pas --effort (2.1.284 requise) : mettre claude à jour, ou orchestrate.effort à « default » pour chaque passe`);
307
307
  continue;
308
308
  }
@@ -518,6 +518,7 @@ function dryRun(lots, io, deps, budget, id) {
518
518
  else
519
519
  steps.unshift({ kind: 'precheck', model: env.config.precheckLocal ? 'local' : 'sonnet' });
520
520
  }
521
+ steps.push(...(env.config.probe ? [{ kind: 'probe', model: 'sonnet' }] : [])); // L497 : entre l'écriture et la revue
521
522
  const { light, full } = env.config.review;
522
523
  if (l.small)
523
524
  steps.push({ kind: l.visible ? 'review-small' : 'review', model: l.light ? light : full });
@@ -550,7 +551,7 @@ function dryRun(lots, io, deps, budget, id) {
550
551
  io.out(l.light ? ' revue conforme avec mineurs : pas de passe des mineurs, ils sont rendus en notes' : ' revue conforme avec mineurs : une passe de correction des mineurs (session neuve), puis une revue courte ; les mineurs refusés sont rendus en « choix »');
551
552
  const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id, lotSlug(l.project, l.lot), 'playwright');
552
553
  // brief écrit par --dry-run = base d'une délégation à la main : aucun dossier de vague (il n'existe pas)
553
- const vars = { chemin: l.repo, lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '' };
554
+ const vars = { chemin: l.repo, lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '', diff: 'git diff <sha de départ>..HEAD' };
554
555
  vars.decisions = decisionsText(humanDecisions(lot, lot.parent ? env.loadPlan().lots().find((x) => x.id === lot.parent) : undefined));
555
556
  if (specStage)
556
557
  vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, back: !!l.specLot });
@@ -564,11 +565,10 @@ function dryRun(lots, io, deps, budget, id) {
564
565
  }
565
566
  for (const s of steps) {
566
567
  const file = join(tmp, `${lotSlug(l.project, l.lot)}--${s.kind}.md`);
567
- const brief = renderBrief(briefName(l, s.kind), s.kind === 'fiche' || vars.fiche === undefined ? vars : { ...vars, fiche: '' }, deps.templatesDir); // la consigne de revue de fiche ne sert qu'au brief de la fiche : implement et review en sont la base d'une délégation à la main
568
- writeFileSync(file, brief);
568
+ writeFileSync(file, renderBrief(briefName(l, s.kind), s.kind === 'fiche' || vars.fiche === undefined ? vars : { ...vars, fiche: '' }, deps.templatesDir)); // la consigne de revue de fiche ne sert qu'au brief de la fiche : implement et review en sont la base d'une délégation à la main
569
569
  const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen).playwright;
570
570
  const local = s.model === 'local';
571
- const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: s.kind === 'implement' ? undefined : s.kind === 'spec' ? 'spec-writer' : s.kind === 'accept' ? 'acceptance-writer' : s.kind === 'ux' || s.kind === 'maquette' ? 'ux-reviewer' : s.kind === 'precheck' ? 'precheck-reader' : 'code-reviewer', cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && (s.kind === 'implement' || s.kind === 'spec') ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright, disablePlugins: enabledPlugins(settingsFiles(deps.claudeHome, l.repo)) }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
571
+ const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: agentOf(s.kind), cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && (s.kind === 'implement' || s.kind === 'spec') ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright, disablePlugins: enabledPlugins(settingsFiles(deps.claudeHome, l.repo)) }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
572
572
  io.out(` ${s.kind} : ${local ? 'claude-local' : 'claude'} ${args.join(' ')}`);
573
573
  io.out(` brief : ${file}`);
574
574
  const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen));