@sylad/cadence 1.27.0 → 1.28.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.
@@ -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.27.0",
11
+ "version": "1.28.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.27.0",
4
+ "version": "1.28.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
@@ -17,3 +17,20 @@ Follow the brief you are given. The method:
17
17
  5. **A document outside the repository** is yours only when the configuration says so: the brief then lists the entries of `docs.articles` (the cadence.yaml of the project: « this project is also described by this file of that repository », with the absolute path of the repository, open to you for reading). Never guess such a file, never go looking for one. Do not edit it, and never write to the plan of that repository (no `raf add`, no edit of its plan file): write a proposed lot instead — a title and a body that name the repository, the file, the passage and the sentence to change — and put it in the `resume` of your report, starting with « lot proposé ». The lead decides whether it enters that repository's plan.
18
18
 
19
19
  Never push, deliver, or run `raf done|start|note|review|ux`. Never run `git checkout` or `git switch` to another commit or branch in this repository. Do not launch subagents. Do not stop to ask: an interpretation you settle yourself goes under "choix" with the alternative you did not take. A document you could not check against the code goes under "nonVerifie". Give the report in the structured output the brief asks for.
20
+
21
+ ## Short reply
22
+
23
+ Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
24
+ small by reading a report only when it needs to). That line is the only trigger; an orchestrated
25
+ session and a call by hand without it have none.
26
+
27
+ - The full report, with its measurements, is written to that file with Bash. Use the path the brief gives
28
+ and no other: it is outside the repository, so the working tree stays as you found it. The
29
+ verdict is the last line of the file. Write the file before you reply.
30
+ - Reply to the caller with 15 lines at most, and nothing before or after them:
31
+ `Verdict: <the one-line verdict>`; `Findings:` one line per finding, most severe first, with its rank and the rule or the
32
+ measure it rests on (e.g. `README.md:12 — still says the old flag, `git show <sha>` line 40`); `Not verified: <what could not be run or seen>`
33
+ (`Not verified: none` when nothing); `Sub-tasks: <n> — <the lines to add to the plan, by their first words>`; `Report: <the path>`. A « lot proposé » for a document outside the repository goes in `Sub-tasks:` (title first), its body in the file.
34
+ - If you cannot write the file, say so on the first line of the reply and give the whole report in the reply, as if
35
+ the brief had no report file: a verdict whose evidence nobody can read is lost.
36
+ - With no `Report file:` line in the brief, give the complete report in your reply.
@@ -98,3 +98,21 @@ A short report:
98
98
  - Judge on taste, or propose a new visual identity.
99
99
  - Report a finding you have not seen in a capture or measured.
100
100
  - Edit code, commit, or record `raf ux` yourself: the session that owns the lot does it.
101
+
102
+ ## Short reply
103
+
104
+ Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
105
+ small by reading a report only when it needs to). That line is the only trigger; an orchestrated
106
+ session and a call by hand without it have none.
107
+
108
+ - The full report, with its measurements, is written to that file with Bash. Use the path the brief gives
109
+ and no other: it is outside the repository, so the working tree stays as you found it. The
110
+ verdict is the last line of the file. Write the file before you reply.
111
+ - Reply to the caller with 15 lines at most, and nothing before or after them:
112
+ `Verdict: <the one-line verdict>`; `Findings:` one line per finding, most severe first, with its rank and the rule or the
113
+ measure it rests on (e.g. `major — /players: target 18×18 px, WCAG 2.5.8`); `Not verified: <what could not be run or seen>`
114
+ (`Not verified: none` when nothing); `Sub-tasks: <n> — <the lines to add to the plan, by their first words>`; `Report: <the path>`.
115
+ Add a last line `Captures: <the absolute path of the capture folder>` (the folder the brief names): the report file does not replace the captures, which the lead opens itself.
116
+ - If you cannot write the file, say so on the first line of the reply and give the whole report in the reply, as if
117
+ the brief had no report file: a verdict whose evidence nobody can read is lost.
118
+ - With no `Report file:` line in the brief, give the complete report in your reply.
package/dist/cli.js CHANGED
@@ -22,7 +22,8 @@ import { dueLine, isRecurring, recurringByDue } from './recurring.js';
22
22
  import { schedule } from './schedule.js';
23
23
  import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
24
24
  import { configSet } from './config-set.js';
25
- import { findProjects, readDepth, leadTour, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
25
+ import { movePriority } from './priority.js';
26
+ import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
26
27
  import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
27
28
  import { addHold, releaseHold } from './orchestrate/registry.js';
28
29
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
@@ -53,6 +54,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
53
54
  raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
54
55
  raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits ; rouvre un lot done ou dropped (→ todo, finished retiré, note « rouvert ») — la seule façon de rouvrir, archivé compris (il revient dans le fichier du plan)
55
56
  raf note <id> "texte"
57
+ --verbose avec done, ux, review, note, move ou public : imprime la ligne « id état titre entier » du lot touché ; avec hook post-commit : le titre entier (sinon 60 caractères au plus)
56
58
  raf title <id> "nouveau titre" remplace le titre d'un lot, de tout statut (done compris), et note l'ancien dans une note datée
57
59
  raf did <id> ["texte"] un lot récurrent (--every) a été refait : le « dû depuis N j » repart de aujourd'hui
58
60
  raf ux enable revue UX obligatoire avant « done » pour les lots --visible
@@ -74,6 +76,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
74
76
  cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--goal "…"] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague | --wave vague] [--budget 1M] [--budget projet:lot=+300k] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague] ; --waits [--since AAAA-MM-JJ]
75
77
  mode worktree par lot (orchestrate.worktrees: true) (activé par défaut dans cadence, désactivé ailleurs tant que la clé est absente) ; ses options : --resume --wave <vague> --integrate projet:lot (rejoue un lot rendu à l'intégration) · --resume --wave <vague> --replay projet:lot (idem, et après une écriture de plan refusée) · --remove-worktree projet:lot [--abandon-commits] (retire le worktree et la branche conservés) ; la liste des worktrees conservés : orchestrate --status et lead tour
76
78
  cadence lead tour [dossier] [--idle 3] [--depth n] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier seul s'il est lui-même un projet (en cours, dérive, notes de clôture, prochain lot prêt, dépôt), précédées de l'en-tête perf : une ligne par ⚠ de cadence perf, rien si tout est stable
79
+ cadence lead priority <projet> --before|--after <projet> [dossier] [--depth n] range un projet dans la liste priority: du cadence.yaml du dossier (ordre du tour, de orchestrate --continue et du panneau des lots), commentaires gardés ; ne commite pas
77
80
  cadence lead hold <étiquette> [--ttl minutes] | lead release <étiquette> déclare (60 min par défaut), puis rend, une session lancée à la main par le lead : elle est soustraite des créneaux libres de orchestrate --status et de lead tour ; au TTL échu, les deux la signalent en ⚠ jusqu'au release
78
81
  cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--cache-sous-causes fichier] [--sample n] [--until date] [--since date] [--json] mesure des passes de correction des journaux dossier/.cadence/runs : lots à ≥1 et ≥2 corrections, par projet, visibles ou non, passes par cause (classées par Ollama avec --classify, hors quota)
79
82
  cadence perf [dossier] [--since date] [--json] tableau de bord du workflow, sans modèle : deux fenêtres (7 derniers jours contre les 7 précédents) lues dans dossier/.cadence/runs et les plans, chaque vitesse à côté de sa qualité, ⚠ au-delà de 25 % de dégradation ; tokens par lot selon l'estimate, part de la revue, délais création → done → livraison, durée de mur par étape, coût en euros par lot livré (grille cadence.yaml perf.prices) ; --since est un jour qui tronque les fenêtres
@@ -197,6 +200,7 @@ function dispatch(argv, io) {
197
200
  ignored: { type: 'boolean' },
198
201
  notes: { type: 'boolean' },
199
202
  json: { type: 'boolean' },
203
+ verbose: { type: 'boolean' },
200
204
  classify: { type: 'boolean' },
201
205
  model: { type: 'string' },
202
206
  cache: { type: 'string' },
@@ -230,10 +234,12 @@ function dispatch(argv, io) {
230
234
  if (command === 'lead') {
231
235
  if (rest[0] === 'hold' || rest[0] === 'release')
232
236
  return leadHold(rest, values.ttl, io, today);
237
+ if (rest[0] === 'priority')
238
+ return leadPriority(rest, values.before, values.after, values.depth, io);
233
239
  if (values.ttl !== undefined)
234
240
  throw new RafError('--ttl ne va qu\'avec lead hold');
235
241
  if (rest[0] !== 'tour' || rest.length > 2)
236
- throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
242
+ throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead priority <projet> --before|--after <projet> [dossier] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
237
243
  const idle = values.idle === undefined ? undefined : Number(values.idle);
238
244
  if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
239
245
  throw new RafError(`--idle invalide : ${values.idle}`);
@@ -460,6 +466,7 @@ function dispatch(argv, io) {
460
466
  io.out(`${rest[0]} → ${status}`);
461
467
  if (status === 'done')
462
468
  warnIntegrating(root, io.err);
469
+ touchedLot(plan, rest[0], values.verbose, io);
463
470
  if (status === 'done' && !rest[0].includes('/')) {
464
471
  const lot = plan.lot(rest[0]);
465
472
  if (lot.visible && !loadEntries(newsDir).some((e) => e.lots.includes(lot.id))) {
@@ -515,6 +522,7 @@ function dispatch(argv, io) {
515
522
  plan.note(rest[0], rest.slice(1).join(' '), today);
516
523
  plan.save();
517
524
  warnIntegrating(root, io.err);
525
+ touchedLot(plan, rest[0], values.verbose, io);
518
526
  return 0;
519
527
  }
520
528
  case 'did': {
@@ -551,6 +559,7 @@ function dispatch(argv, io) {
551
559
  if (holding)
552
560
  throw new RafError(`move : orchestration en cours (vague ${holding.wave}, pid ${holding.pid}) — attendre que les lots de ce dépôt soient finis (le verrou tombe dès que tous sont prêts ou rendus), puis relancer`);
553
561
  plan.save();
562
+ touchedLot(plan, rest[0], values.verbose, io);
554
563
  return 0;
555
564
  }
556
565
  case 'public': {
@@ -562,6 +571,7 @@ function dispatch(argv, io) {
562
571
  refuseLongTitle(rest.slice(1).join(' '));
563
572
  plan.setPublic(rest[0], values.clear ? null : rest.slice(1).join(' '));
564
573
  plan.save();
574
+ touchedLot(plan, rest[0], values.verbose, io);
565
575
  return 0;
566
576
  }
567
577
  case 'screen': {
@@ -600,7 +610,7 @@ function dispatch(argv, io) {
600
610
  }
601
611
  case 'ux':
602
612
  case 'review':
603
- return gate(command, rest, loadPlan, root, today, io);
613
+ return gate(command, rest, loadPlan, root, today, io, !!values.verbose);
604
614
  case 'commits': {
605
615
  need(1, 'commits <lot>');
606
616
  const plan = loadPlan();
@@ -749,7 +759,7 @@ function dispatch(argv, io) {
749
759
  return 0;
750
760
  }
751
761
  if (rest[0] === 'post-commit')
752
- return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart);
762
+ return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart, !!values.verbose);
753
763
  if (rest[0] === 'pre-commit')
754
764
  return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
755
765
  if (rest[0] === 'pre-push')
@@ -854,7 +864,7 @@ const GATES = {
854
864
  review: { on: 'revue de code obligatoire pour les lots à commits', already: 'revue de code déjà active' },
855
865
  };
856
866
  /** « raf ux … » et « raf review … » ont la même forme : activer la porte, ou noter le verdict d'un lot. */
857
- function gate(kind, rest, loadPlan, root, today, io) {
867
+ function gate(kind, rest, loadPlan, root, today, io, verbose) {
858
868
  if (rest.length < 1)
859
869
  throw new RafError(`usage : raf ${kind} enable | ${kind} <lot> "verdict"`);
860
870
  const plan = loadPlan();
@@ -879,8 +889,19 @@ function gate(kind, rest, loadPlan, root, today, io) {
879
889
  plan.recordReview(rest[0], verdict, today, plan.readonly ? null : (lotWork(plan, root, rest[0])[0]?.sha ?? null), neighbours?.repos.length ? repoShas(plan, neighbours.repos, rest[0]) : undefined);
880
890
  }
881
891
  plan.save();
892
+ touchedLot(plan, rest[0], verbose, io);
882
893
  return 0;
883
894
  }
895
+ /** Un titre de lot n'est jamais rendu en entier par défaut (L453) : ~300 caractères répétés à chaque commande, chaque commit de plan. */
896
+ function shortTitle(title, verbose) {
897
+ return verbose ? title : truncate(title, TITLE_MAX);
898
+ }
899
+ /** `--verbose` (L453) : la ligne `id état titre entier` du lot touché par `done`, `ux`, `review`, `note`, `move`, `public` ; sans lui, rien de plus (une sous-tâche n'a pas de ligne). */
900
+ function touchedLot(plan, id, verbose, io) {
901
+ const lot = verbose ? plan.lots().find((l) => l.id === id) : undefined;
902
+ if (lot)
903
+ io.out(`${lot.id} ${lot.status} ${lot.title}`);
904
+ }
884
905
  function knownLot(plan, id, subtask) {
885
906
  const lot = plan.lots().find((l) => l.id === id);
886
907
  if (!lot)
@@ -1021,7 +1042,7 @@ function preCommit(load, mode, root, today, io) {
1021
1042
  }
1022
1043
  return 0;
1023
1044
  }
1024
- function postCommit(load, newsDir, root, today, io, mode) {
1045
+ function postCommit(load, newsDir, root, today, io, mode, verbose) {
1025
1046
  if (!load)
1026
1047
  return 0;
1027
1048
  try {
@@ -1050,7 +1071,7 @@ function postCommit(load, newsDir, root, today, io, mode) {
1050
1071
  else if (lot.status === 'todo' && !planning)
1051
1072
  io.err(`raf: ${r.lot} est encore todo${plan.readonly ? '' : ` — raf start ${r.lot}`}`);
1052
1073
  else
1053
- io.err(`raf: ${r.lot} (${lot.status}) ${lot.title}`);
1074
+ io.err(`raf: ${r.lot} (${lot.status}) ${shortTitle(lot.title, verbose)}`);
1054
1075
  }
1055
1076
  const entries = loadEntries(newsDir);
1056
1077
  for (const id of new Set(refs.map((r) => r.lot))) {
@@ -1445,6 +1466,23 @@ function specReseal(args, reason, porteur, ctx, io) {
1445
1466
  const HOLD_TTL_MINUTES = 60;
1446
1467
  /** Longueur maximale de l'étiquette encodée : un nom de fichier tient en 255 octets, `.json.tmp` en prend 9. */
1447
1468
  const HOLD_LABEL_MAX = 200;
1469
+ /** `cadence lead priority <projet> --before|--after <projet> [dossier]` (L449) : range un projet dans la liste `priority:` du cadence.yaml du dossier (celui du tour), sans modèle ni commit. */
1470
+ function leadPriority(rest, before, after, depth, io) {
1471
+ const usage = 'usage : cadence lead priority <projet> --before|--after <projet> [dossier] [--depth n]';
1472
+ if (rest.length < 2 || rest.length > 3 || (before === undefined) === (after === undefined))
1473
+ throw new RafError(usage);
1474
+ const place = before !== undefined ? 'before' : 'after';
1475
+ const levels = depth === undefined ? undefined : Number(depth);
1476
+ if (levels !== undefined && !(Number.isInteger(levels) && levels >= 1))
1477
+ throw new RafError(`--depth invalide : ${depth} (un entier ≥ 1)`);
1478
+ const parent = resolve(io.cwd, rest[2] ?? '.');
1479
+ if (!existsSync(parent))
1480
+ throw new RafError(`lead priority : dossier introuvable : ${parent}`);
1481
+ const target = (before ?? after);
1482
+ const moved = movePriority(parent, rest[1], place, target, levels);
1483
+ io.out(moved.changed ? `${rest[1]} placé ${place === 'before' ? 'avant' : 'après'} ${target} dans priority: de ${join(parent, 'cadence.yaml')} (ordre : ${moved.order.join(' > ')}) — le fichier est modifié, à commiter` : `${rest[1]} est déjà ${place === 'before' ? 'avant' : 'après'} ${target} : rien à changer`);
1484
+ return 0;
1485
+ }
1448
1486
  /** `cadence lead hold <étiquette> [--ttl minutes]` / `lead release <étiquette>` (L255) : inscrit, ou rend, une session lancée à la main par le lead, qui compte dans les créneaux libres. */
1449
1487
  function leadHold(rest, ttl, io, today) {
1450
1488
  const [verb, label] = rest;
@@ -1702,12 +1740,14 @@ async function sessionAll(rest, values, io) {
1702
1740
  }
1703
1741
  return worst;
1704
1742
  }
1705
- /** Le contexte de la session de `--session <id>`, sinon de `CLAUDE_CODE_SESSION_ID`, sinon (variable absente) de la seule session du dossier courant, tel que la bande cadence-hud le publie ; illisible = code 2, à lire comme « au seuil ». */
1743
+ /** Le contexte (et sa position par rapport à `lead.contextThreshold`, en tokens) de la session de `--session <id>`, sinon de `CLAUDE_CODE_SESSION_ID`, sinon (variable absente) de la seule session du dossier courant, tel que la bande cadence-hud le publie ; illisible = code 2, à lire comme « au seuil ». */
1706
1744
  function sessionContext(io, session) {
1707
1745
  const r = readHudContext(io.env, io.now().getTime(), { cwd: io.cwd, session });
1708
1746
  if (!r.ok)
1709
1747
  throw new RafError(`session context : contexte illisible — ${r.reason} ; à traiter comme au seuil`);
1710
1748
  io.out(`ctx ${Math.round(r.ctx.percent)} % (${r.ctx.tokens}/${r.ctx.window})`);
1749
+ const threshold = readLeadContextThreshold(io.cwd);
1750
+ io.out(`seuil ${threshold} tokens : ${r.ctx.tokens >= threshold ? 'atteint' : 'sous le seuil'}`);
1711
1751
  return 0;
1712
1752
  }
1713
1753
  function session([sub, ...args], ctx, values) {
@@ -6,7 +6,7 @@ import { isNode, isScalar, parse, parseDocument } from 'yaml';
6
6
  import { defaultPlan, readDocsConfig, readHookConfig, readMigrateConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readReferencesConfig, readSessionConfig } from './config.js';
7
7
  import { parseDeliverConfig } from './deliver.js';
8
8
  import { commitPaths, dirtyPaths, gitRoot, restorePaths } from './git.js';
9
- import { findProjects, isProject, readDepth, readLeadUx } from './lead.js';
9
+ import { findProjects, isProject, readDepth, readLeadContextThreshold, readLeadUx } from './lead.js';
10
10
  import { readPriority } from './orchestrate/continue.js';
11
11
  import { Plan, RafError } from './plan.js';
12
12
  const KEY = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
@@ -26,6 +26,7 @@ export function validateConfig(file, root) {
26
26
  readReferencesConfig(file);
27
27
  readMigrateConfig(file);
28
28
  readLeadUx(dirname(file));
29
+ readLeadContextThreshold(dirname(file));
29
30
  readDepth(dirname(file));
30
31
  readPriority(dirname(file));
31
32
  const text = readFileSync(file, 'utf8');
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', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
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
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 } };
@@ -392,6 +392,21 @@ export function readOrchestrateConfig(file) {
392
392
  throw bad('worktreeDeps : liste de chemins relatifs au dépôt, sans « .. » (node_modules, .venv…)');
393
393
  config.worktreeDeps = deps.map((x) => x.trim().replace(/\/$/, ''));
394
394
  }
395
+ if (o.gate != null) {
396
+ if (!isObject(o.gate))
397
+ throw bad('gate : objet attendu (gate: { readsPlan: true })');
398
+ for (const k of Object.keys(o.gate))
399
+ if (k !== 'readsPlan')
400
+ throw bad(`gate.${k} inconnu (attendu : readsPlan)`);
401
+ const r = o.gate.readsPlan;
402
+ const msg = 'gate.readsPlan : true, false ou liste de chemins de tests relatifs au dépôt, sans « .. »';
403
+ if (r === true)
404
+ config.gate = { readsPlan: true };
405
+ else if (Array.isArray(r) && r.length && r.every((x) => typeof x === 'string' && x.trim() && !isAbsolute(x) && !x.split(/[\\/]/).includes('..')))
406
+ config.gate = { readsPlan: r.map((x) => x.trim()) };
407
+ else if (r !== false)
408
+ throw bad(msg);
409
+ }
395
410
  if (o.maquette != null) {
396
411
  if (typeof o.maquette !== 'boolean')
397
412
  throw bad('maquette : true ou false attendu');
package/dist/lead.js CHANGED
@@ -16,11 +16,11 @@ import { cadenceHome } from './orchestrate/registry.js';
16
16
  import { staleNotes, staleText } from './stale-notes.js';
17
17
  import { noTestGate, noTestText } from './orchestrate/no-test.js';
18
18
  import { KEPT_TITLE, keptLine, keptTrees } from './orchestrate/worktree.js';
19
- /** Clé `lead.ux` du cadence.yaml du dossier parent (L197) : qui pose le verdict UX (`raf ux`) d'un lot visible. `human` (défaut) : l'humain ; `lead` : le lead, l'humain n'étant consulté qu'en cas de doute. */
20
- export function readLeadUx(parent) {
19
+ /** Le bloc `lead:` du cadence.yaml du dossier parent (absent = `undefined`) ; un `lead` qui n'est pas une table est refusé en nommant `key`. */
20
+ function readLeadBlock(parent, key, expected) {
21
21
  const file = join(parent, 'cadence.yaml');
22
22
  if (!existsSync(file))
23
- return 'human';
23
+ return undefined;
24
24
  let raw;
25
25
  try {
26
26
  raw = parse(readFileSync(file, 'utf8'));
@@ -30,16 +30,33 @@ export function readLeadUx(parent) {
30
30
  }
31
31
  const l = raw?.lead;
32
32
  if (l == null)
33
- return 'human';
34
- const ux = l.ux;
33
+ return undefined;
35
34
  if (typeof l !== 'object' || Array.isArray(l))
36
- throw new RafError(`${file} : lead.ux doit être « lead » ou « human »`);
35
+ throw new RafError(`${file} : ${key} doit être ${expected}`);
36
+ return { file, block: l };
37
+ }
38
+ /** Clé `lead.ux` du cadence.yaml du dossier parent (L197) : qui pose le verdict UX (`raf ux`) d'un lot visible. `human` (défaut) : l'humain ; `lead` : le lead, l'humain n'étant consulté qu'en cas de doute. */
39
+ export function readLeadUx(parent) {
40
+ const found = readLeadBlock(parent, 'lead.ux', '« lead » ou « human »');
41
+ const ux = found?.block.ux;
37
42
  if (ux == null)
38
43
  return 'human';
39
44
  if (ux !== 'lead' && ux !== 'human')
40
- throw new RafError(`${file} : lead.ux doit être « lead » ou « human »`);
45
+ throw new RafError(`${found.file} : lead.ux doit être « lead » ou « human »`);
41
46
  return ux;
42
47
  }
48
+ /** Seuil de contexte du lead quand `lead.contextThreshold` n'est pas posé (L437). */
49
+ export const CONTEXT_THRESHOLD_DEFAULT = 150_000;
50
+ /** 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. */
51
+ export function readLeadContextThreshold(parent) {
52
+ const found = readLeadBlock(parent, 'lead.contextThreshold', 'un entier ≥ 1 (tokens)');
53
+ const t = found?.block.contextThreshold;
54
+ if (t == null)
55
+ return CONTEXT_THRESHOLD_DEFAULT;
56
+ if (typeof t !== 'number' || !Number.isInteger(t) || t < 1)
57
+ throw new RafError(`${found.file} : lead.contextThreshold doit être un entier ≥ 1 (tokens), pas ${JSON.stringify(t)}`);
58
+ return t;
59
+ }
43
60
  /** Clé `depth:` du cadence.yaml du dossier parent (L201, issue #8), à côté de `priority:` : combien de niveaux `lead tour` parcourt (1 : sous-dossiers directs, le défaut). */
44
61
  export function readDepth(parent) {
45
62
  const file = join(parent, 'cadence.yaml');
@@ -6,7 +6,7 @@ import { delimiter, dirname, isAbsolute, join, posix, relative, resolve } from '
6
6
  import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { coveredTasks, lotCommits, lotWork } from '../audit.js';
9
- import { readDocsConfig, readPlanConfig } from '../config.js';
9
+ import { planLoader, readDocsConfig, readPlanConfig } from '../config.js';
10
10
  import { PLAN_ACTIONS, leadAnswerMessage, planCommitMessage } from '../plan-commit.js';
11
11
  import { articleRepoDir, articlesBrief, docSyncBrief, docSyncGaps, filesOf, gapMessage } from '../docsync.js';
12
12
  import { testGateMissing } from '../testgate.js';
@@ -2318,15 +2318,42 @@ async function work(c, kind) {
2318
2318
  const noCommitNote = (l) => (l.warnings.some((w) => w.startsWith('passe des mineurs sans commit')) ? ' sans commit' : '');
2319
2319
  /** Revue de code qui clôt le cycle : complète, ou courte une fois la passe des mineurs jouée. */
2320
2320
  const codeReview = (l) => (l.minorPass ? 'review-small' : 'review');
2321
+ /** Note d'un lot que sa session a rendu `visible` après le lancement de la vague (L454) : son verdict UX n'existe pas encore, le lead le sait. */
2322
+ const VISIBLE_ADDED = 'visible ajouté en cours de vague, étape UX à jouer';
2323
+ /**
2324
+ * Relit `visible` dans le plan du worktree après une session d'écriture (L454) : un lot que sa session a rendu visible (warhammer L37 et L43,
2325
+ * 30-10) est lancé non visible, et sortait « prêt » sans étape UX. Il le devient dans l'état du lot, donc l'UX est jouée (`uxAfterReview`, ou la
2326
+ * revue courte d'un petit lot) ; sans application déclarée, il est rendu avec la note. Un plan illisible, ou un lot qui n'est plus visible
2327
+ * dans le plan, laisse l'état tel quel (retirer `visible` en cours de vague n'est pas traité).
2328
+ */
2329
+ function addedVisible(c) {
2330
+ const l = c.lot;
2331
+ if (l.visible)
2332
+ return;
2333
+ let now;
2334
+ try {
2335
+ now = (wd(c) === l.repo ? c.loadPlan() : planLoader(wd(c))()).lot(l.lot).visible;
2336
+ }
2337
+ catch {
2338
+ return;
2339
+ }
2340
+ if (!now)
2341
+ return;
2342
+ l.visible = true;
2343
+ if (!c.config.ux)
2344
+ l.uxNote = `${VISIBLE_ADDED} (aucune application déclarée : cadence.yaml orchestrate.ux)`;
2345
+ c.wave.log(`${lotKey(l.project, l.lot)} · ${VISIBLE_ADDED}`);
2346
+ }
2321
2347
  /**
2322
2348
  * Première étape après un travail d'écriture (L221) : la revue de code, jamais l'UX. La revue UX d'un lot visible est jouée une seule fois,
2323
2349
  * après la dernière revue de code conforme (`uxAfterReview`), sur le code qui sera livré.
2324
2350
  */
2325
2351
  function firstReview(c) {
2326
2352
  const l = c.lot;
2353
+ addedVisible(c);
2327
2354
  if (l.small)
2328
2355
  return l.visible || l.minorPass ? 'review-small' : 'review'; // passe unique code + ergonomie ; sans écran, une revue de code
2329
- if (l.visible && !c.config.ux)
2356
+ if (l.visible && !c.config.ux && !l.uxNote?.startsWith(VISIBLE_ADDED))
2330
2357
  l.uxNote = 'UX à faire par le lead (aucune application déclarée : cadence.yaml orchestrate.ux)';
2331
2358
  return codeReview(l);
2332
2359
  }
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, join, relative, sep } from 'node:path';
3
+ import { Document, isScalar, isSeq, parseDocument } from 'yaml';
4
+ import { findProjects, readDepth } from './lead.js';
5
+ import { priorityRank, readPriority } from './orchestrate/continue.js';
6
+ import { RafError } from './plan.js';
7
+ import { setKey } from './config-set.js';
8
+ /**
9
+ * `cadence lead priority <projet> --before|--after <projet>` (L449) : place `project` juste avant ou après `target` dans l'ordre des projets du dossier,
10
+ * en réécrivant la liste `priority:` de son cadence.yaml (créé s'il manque). Les projets sont nommés comme `lead tour` les nomme.
11
+ * La liste écrite nomme tous les projets du dossier, dans l'ordre affiché : un nom qui en couvrait plusieurs par préfixe (`maritime` pour `maritime-atlas`) devient explicite
12
+ * (le commentaire de sa ligne passe sur le premier projet qu'il couvrait), et un nom qui ne couvre aucun projet reste en fin de liste. Le reste du fichier, commentaires compris, est gardé (setKey). Un fichier illisible, ou une clé qui n'est pas une liste, est refusé sans rien écrire.
13
+ */
14
+ export function movePriority(parent, project, place, target, depth) {
15
+ const file = join(parent, 'cadence.yaml');
16
+ const declared = readPriority(parent); // refuse un yaml illisible ou une clé qui n'est pas une liste de noms
17
+ const projects = findProjects(parent, depth ?? readDepth(parent)).map((dir) => ({ key: basename(dir), name: relative(parent, dir).split(sep).join('/') }));
18
+ const byName = new Map(projects.map((p) => [p.name, p]));
19
+ for (const n of [project, target])
20
+ if (!byName.has(n))
21
+ throw new RafError(`projet inconnu : ${n} (projets de ${parent} : ${projects.map((p) => p.name).join(', ') || 'aucun'})`);
22
+ if (project === target)
23
+ throw new RafError(`${project} : le même projet des deux côtés`);
24
+ const keys = projects.map((p) => p.key);
25
+ const twice = keys.find((k, i) => keys.indexOf(k) !== i);
26
+ if (twice)
27
+ throw new RafError(`deux projets s'appellent ${twice} : la liste priority: les nomme par leur dossier et ne peut pas les distinguer`);
28
+ const ranked = [...projects].sort((a, b) => priorityRank(a.key, declared) - priorityRank(b.key, declared) || a.key.localeCompare(b.key) || a.name.localeCompare(b.name));
29
+ const without = ranked.filter((p) => p.name !== project);
30
+ const at = without.findIndex((p) => p.name === target) + (place === 'after' ? 1 : 0);
31
+ const moved = [...without.slice(0, at), byName.get(project), ...without.slice(at)];
32
+ const order = moved.map((p) => p.name);
33
+ if (order.every((n, i) => n === ranked[i].name))
34
+ return { order, changed: false };
35
+ const unmatched = declared.filter((n) => !keys.some((k) => k === n || k.startsWith(`${n}-`)));
36
+ const list = [...moved.map((p) => p.key), ...unmatched];
37
+ // la liste écrite doit restituer l'ordre voulu : un nom qui en préfixe un autre (`shop` et `shop-web`) les rangerait ensemble
38
+ const back = [...projects].sort((a, b) => priorityRank(a.key, list) - priorityRank(b.key, list) || a.key.localeCompare(b.key) || a.name.localeCompare(b.name)).map((p) => p.name);
39
+ if (back.some((n, i) => n !== order[i])) {
40
+ const clash = keys.find((k) => keys.some((o) => o !== k && o.startsWith(`${k}-`)));
41
+ throw new RafError(`${project} ne peut pas être placé ${place === 'before' ? 'avant' : 'après'} ${target} : la liste priority: nomme un projet par son dossier, et ${clash ?? 'un nom'} est le début du nom d'un autre projet (« ${clash ?? 'x'}- »), qui le suit toujours — rien n'est écrit`);
42
+ }
43
+ const text = existsSync(file) ? readFileSync(file, 'utf8') : '';
44
+ const old = parseDocument(text).get('priority', true);
45
+ // les noms déjà dans la liste gardent leur nœud, donc le commentaire de leur ligne ; un nom qui couvrait des projets par préfixe (`maritime`)
46
+ // donne le sien au premier projet qu'il couvre, dans l'ordre écrit, sous le nom explicite ; les autres sont des nœuds neufs
47
+ const kept = new Map();
48
+ const scalars = isSeq(old) ? old.items.filter((item) => isScalar(item) && typeof item.value === 'string') : [];
49
+ for (const item of scalars)
50
+ if (!kept.has(item.value.trim()))
51
+ kept.set(item.value.trim(), item); // deux fois le même nom : le premier nœud compte, comme pour le rang
52
+ for (const item of scalars) {
53
+ const n = item.value.trim();
54
+ if (kept.get(n) !== item || keys.includes(n))
55
+ continue; // un nom qui est déjà un projet garde son nœud et n'en donne pas une copie à `nom-…`
56
+ const covered = list.find((k) => !kept.has(k) && k.startsWith(`${n}-`));
57
+ if (covered === undefined)
58
+ continue;
59
+ const explicit = item.clone();
60
+ explicit.value = covered;
61
+ kept.set(covered, explicit);
62
+ }
63
+ const value = new Document(list);
64
+ if (isSeq(value.contents)) {
65
+ value.contents.items = list.map((n) => kept.get(n) ?? value.createNode(n));
66
+ value.contents.flow = isSeq(old) && old.flow === true; // la liste en ligne reste en ligne
67
+ // la séquence porte le commentaire de la ligne de la clé et celui écrit avant son premier nom : la nouvelle les reprend
68
+ if (isSeq(old)) {
69
+ value.contents.commentBefore = old.commentBefore;
70
+ value.contents.comment = old.comment;
71
+ }
72
+ }
73
+ const next = setKey(text, 'priority', value);
74
+ if (next !== null)
75
+ writeFileSync(file, next);
76
+ return { order, changed: next !== null };
77
+ }
@@ -127,7 +127,8 @@ async function playGate(ctx) {
127
127
  }
128
128
  let recorded = { reason: 'aucune porte de vague enregistrée pour ce sha' };
129
129
  try {
130
- recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps, ctx.planFile);
130
+ // Un test qui lit le plan (orchestrate.gate.readsPlan, L450) voit un autre contenu après un `raf done` : les commits de plan seuls ne sont pas traversés.
131
+ recorded = recordedGate(ctx.launchDirs ?? [], repo, sha, steps, config.gate ? undefined : ctx.planFile);
131
132
  }
132
133
  catch (e) {
133
134
  recorded = { reason: `état des vagues illisible (${gitFailure(e)})` };
@@ -143,7 +144,7 @@ async function playGate(ctx) {
143
144
  out(`verify --gate ${short} : vert`);
144
145
  return 0;
145
146
  }
146
- const replayReason = `verdict rejoué : ${recorded.reason}`;
147
+ const replayReason = `verdict rejoué : ${recorded.reason}${config.gate && ctx.planFile ? ' (orchestrate.gate.readsPlan : un test lit le plan, les commits de plan seuls ne reprennent pas le verdict d\'un sha dessous)' : ''}`;
147
148
  const obstacle = mainTreeObstacle(repo, sha);
148
149
  const mode = ctx.verify?.inPlace;
149
150
  if (mode === true && obstacle) {
@@ -81,6 +81,8 @@ A project shows at most 5 lots (L411), the first ones in the order above (so tho
81
81
 
82
82
  **Reordering (L402).** Drag a lot onto another lot of the same project and of the same display group (or a sub-task onto another sub-task of its lot, same group): press on the row, move with the button held, release. While you drag, the row you started from shows `●` and the row under the pointer shows where it will land, `⇡` (before it) or `⇣` (after it), or a red `⊘` when it belongs to another parent or to another display group (L410) and the drop will be refused. The keyboard does the same wherever the panel is drawn by a `Client` region (terminal and desktop app): click a row (or press ↑ / ↓ once the panel has the focus), then **Alt**+↑ / **Alt**+↓ moves the selected lot before its previous sibling / after its next one. Only the rows the panel shows take part (L411): a lot hidden behind `+ N lots`, or the sub-lots of a closed lot, are not rows, so they are never a target — Alt+↓ on the fifth lot, the last row before the `+ N lots` button, moves nothing, like Alt+↓ on the last row of a project; open the rest (or the lot) first, and the drop runs `raf move` as usual. The `+ N lots` / `moins` button sits outside the `Client` region, after the project's rows. The drop runs `raf move <id> --before|--after <id>` ([raf move](raf.md)) in the project folder, then the panel reads the plans again and says, under the title, `✔ L13 placé avant L10 — le plan est modifié, à commiter (raf move ne commite pas)`: the panel never commits, the plan file is yours to commit. Everything is relative to what the panel shows, in the L410 order, not to the order of the plan: the row you drop on, and the "previous" and "next" sibling of Alt+↑ / Alt+↓, are the ones displayed around the lot, and `raf move` is given that lot as its target. **A lot is reordered only inside its display group** (L410: in progress, free, blocked): dropping a lot on a lot of another group is refused, `raf` is not called and nothing moves. While you drag, the row under the pointer shows the red `⊘` (as for another parent), and on release the panel says why, by group: `✖ L411 est en cours : un lot libre se range parmi les libres` (the words: `en cours` / `libre` / `bloqué`; the lot is named by what it is, `un lot` or `une sous-tâche`). A parent with an in-progress sub-lot is shown with the lots in progress, so it counts as one of them. Alt+↑ / Alt+↓ toward a displayed neighbour of another group gets the same refusal; at the edge of the displayed rows (or before the `+ N lots` button) nothing happens. (Case that led to this: plan L402 L403 L407 L411, L403 free dropped on L411 in progress moved L403 below L407 and hid it behind `+ 11 lots`.) Inside a group the screen follows the plan, so a gesture that goes through always shows: the lot moves where you dropped it. A drop that would leave the order of the group as it is — the lot is already right before (or after) the target among the lots of its group — runs nothing and says `• L10 est déjà avant L13 dans le plan : rien à déplacer`; no gesture of the panel leads there (the screen follows the plan inside a group), the check guards a request the drawing did not make. The same holds between the sub-tasks of a lot. The `déjà` check is made on the table the panel last read: from a successful `raf move` until the end of a re-reading started *after* it (`cadence lead tour` takes several seconds; a reading already in flight read the old plan, so another one follows it) the table is held stale: no `déjà` short-circuit, `raf move` runs and decides (the group refusal stays: it depends on the states, not on the order). A refusal is written under the title with its reason and nothing moves: a wave holds the repository (`move : orchestration en cours …`), a plan kept by another tool (`plan en lecture seule : … — le modifier avec l'outil du projet`, the message does not name that tool's command: `raf` does not know it, so look in the project), an archived lot, different parents (`L402 et L401/2 n'ont pas le même parent …`) or different display groups (`L411 est en cours : …`), the last two checked before `raf` runs; `raf` missing from the `PATH` says so. On a surface with no `Client` region (mobile, editor) the panel stays read-only (`lecture seule` in its title).
83
83
 
84
+ **Reordering the projects (L449).** Above the tree, `ordre des projets (priority:) — glisser, ou Alt+↑ / Alt+↓` and one row per project (`cadence 7 lots non clos`; `illisible` for a project the tour could not read), in the order of `priority:`. It is drawn only where the lots can be reordered (a `Client` region: terminal and desktop app), when the folder has two projects or more, and when the panel is tall enough to keep a row for each project and one lot under it (otherwise the tree has the room). Drag a project onto another (`●` the row you hold, `⇡` / `⇣` where it will land) or click it and press **Alt**+↑ / **Alt**+↓ (↑ / ↓ alone select): the drop runs `cadence lead priority <project> --before|--after <project> <folder>` ([lead priority](lead.md#lead-priority)), which rewrites the `priority:` list of the `cadence.yaml` of the parent folder, comments kept; the panel then reads the plans again and says under the title `✔ ol placé avant cadence — priority: du cadence.yaml est modifiée, à commiter`. This order is the one of the tour, of `cadence orchestrate --continue` and of the panel itself. The panel never commits. A refusal is written under the title with its reason (`✖ /dev/cadence.yaml illisible : …`, a `priority:` that is not a list) and nothing is written; `cadence` missing from the `PATH` says so; a project already at that place says `• … est déjà avant …`. The error row `cadence.yaml` that the tour adds for an unreadable file is not a project and cannot be moved or targeted.
85
+
84
86
  States: `▶ en cours` (doing), `○ libre` (todo, no open `after:`), `⊘ bloqué` (todo, an `after:` is still open); a lot in a wave takes the wave's state and words, as the band writes them (`… en attente`, `⟳ implémente`, `⟳ relit`, `⟳ corrige`, `? QUESTION`, `✔ prêt`, `↩ rendu`, `✖ échec`, `⏸ suspendu`). In a native plan the sub-tasks of an open lot (`L401/2`) are its sub-lots (`├`/`└`); they only read `▶ en cours` or `○ à faire`, and are not counted in the project's `N lots non clos`. In a plan kept by another tool the sub-lots come from `parent:`. An `after:` id that is closed or unknown is struck through and blocks nothing. A project the tour could not read shows `illisible : <error>`; the others stay.
85
87
 
86
88
  Before the first reading the panel says `Lecture des plans…`; if `cadence lead tour` does not answer it says `Lots illisibles : cadence lead tour n'a pas répondu. Nouvelle lecture dans une minute.` (a failed re-reading keeps the last table); a `cadence` too old to give the `lots` field says `Lots indisponibles : cadence est trop ancien, mettez-le à jour.`; with no open lot, `Aucun lot non clos dans les projets de ce dossier.`. A tree taller than the panel ends with `… +N lots de plus`. Under 60 columns the `after:` list moves to its own line and the title is cut with `…`, never the id or the state.
package/docs/deliver.md CHANGED
@@ -308,6 +308,8 @@ verify --gate 6b0d9aa : vert
308
308
 
309
309
  Nothing runs: no worktree, no copy of the dependencies, exit 0. Otherwise the gate is **replayed** exactly as described above, and the line right after the one telling where it ran gives the reason: `verdict rejoué : aucune porte de vague enregistrée pour ce sha` (no wave state, another sha such as the head after an integration that rebased, a commit that also touches another file (a plan-only commit is crossed, L444), another repository), `arbre différent de celui contrôlé`, `arbre non enregistré` (state written before L397), or `l'enregistrement (vague …, lot …) ne couvre pas en vert test (…)` (a red, missing or changed step). A recorded red is never repeated as a verdict: it is replayed. The verdict read is the one of the wave's gate (with `orchestrate.testReplay` applied), not a new run: to force a replay, ask for a sha the wave did not check.
310
310
 
311
+ **A test that reads the plan (L450).** Crossing the plan-only commits assumes that no test reads the plan. On 30-10 avatar-pandora's `plan-page.e2e`, which reads `.cadence/raf.yaml`, went red after the `raf done` of L8 while the head gate, taking over the wave's verdict under that commit, saw nothing. A project whose gate has such a test declares it: `orchestrate.gate: { readsPlan: true }` (or `readsPlan: [e2e/plan-page.e2e.ts]`, the paths of the tests that read it; a non-empty list of relative paths without `..`, anything else is refused when `cadence.yaml` is read). The verdict of a sha *below* plan-only commits is then never taken over: the gate is replayed at the sha asked, and the reason line ends with `(orchestrate.gate.readsPlan : un test lit le plan, …)`. A verdict recorded at the very sha asked is still taken over. Absent or `false`: unchanged. The pre-push hook keeps crossing plan-only commits (its rule, L432, is not changed here).
312
+
311
313
  ```yaml
312
314
  # cadence.yaml
313
315
  verify:
@@ -50,11 +50,11 @@
50
50
  <text x="920" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Delivers</text>
51
51
  <text x="920" y="186.5" text-anchor="middle" font-size="11.5" fill="#8b949e">push + deliver</text>
52
52
  <rect x="1000" y="146" width="128" height="58" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
53
- <text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">&lt; 60 % ?</text>
53
+ <text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">&lt; limit ?</text>
54
54
  <text x="1064" y="186.5" text-anchor="middle" font-size="11.5" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#8b949e">session context</text>
55
55
  <rect x="1144" y="146" width="128" height="58" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
56
56
  <text x="1208" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Close</text>
57
- <text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#8b949e">if ≥ 60 %</text>
57
+ <text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#8b949e">if limit reached</text>
58
58
  <rect x="280" y="246" width="128" height="58" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
59
59
  <text x="344" y="271.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Lots</text>
60
60
  <text x="344" y="286.5" text-anchor="middle" font-size="11.5" fill="#8b949e">figure 2, per lot</text>
@@ -50,11 +50,11 @@
50
50
  <text x="920" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Delivers</text>
51
51
  <text x="920" y="186.5" text-anchor="middle" font-size="11.5" fill="#57606a">push + deliver</text>
52
52
  <rect x="1000" y="146" width="128" height="58" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
53
- <text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">&lt; 60 % ?</text>
53
+ <text x="1064" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">&lt; limit ?</text>
54
54
  <text x="1064" y="186.5" text-anchor="middle" font-size="11.5" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">session context</text>
55
55
  <rect x="1144" y="146" width="128" height="58" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
56
56
  <text x="1208" y="171.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Close</text>
57
- <text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#57606a">if ≥ 60 %</text>
57
+ <text x="1208" y="186.5" text-anchor="middle" font-size="11.5" fill="#57606a">if limit reached</text>
58
58
  <rect x="280" y="246" width="128" height="58" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
59
59
  <text x="344" y="271.5" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Lots</text>
60
60
  <text x="344" y="286.5" text-anchor="middle" font-size="11.5" fill="#57606a">figure 2, per lot</text>
@@ -130,7 +130,7 @@
130
130
  <text x="1140" y="703.5" text-anchor="middle">findings → the human</text>
131
131
  </g>
132
132
  <g id="ctx"><rect x="1241" y="337" width="158" height="66" rx="8" fill="#0c2d6b" stroke="#58a6ff" stroke-width="1.5"/>
133
- <text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under 60 %?</text>
133
+ <text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under limit?</text>
134
134
  <text x="1320" y="374.5" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="11.5">session context</text>
135
135
  <text x="1320" y="390.5" text-anchor="middle">error = at threshold</text>
136
136
  </g>
@@ -175,7 +175,7 @@
175
175
  <polyline points="1320,403 1320,650" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
176
176
  <text x="1326" y="440" text-anchor="start" font-size="11" fill="#8b949e" class="lbl">yes: chain</text>
177
177
  <polyline points="1399,370 1421,370" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
178
- <text x="1410" y="329" text-anchor="middle" font-size="11" fill="#8b949e" class="lbl">≥ 60 %: close</text>
178
+ <text x="1410" y="329" text-anchor="middle" font-size="11" fill="#8b949e" class="lbl">limit reached: close</text>
179
179
  <polyline points="1500,337 1500,203" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
180
180
  <polyline points="1320,716 1320,746 420,746 420,716" fill="none" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
181
181
  <text x="870" y="740" text-anchor="middle" font-size="11" fill="#8b949e" class="lbl">next wave: back to step 2, the limits still apply</text>
@@ -130,7 +130,7 @@
130
130
  <text x="1140" y="703.5" text-anchor="middle">findings → the human</text>
131
131
  </g>
132
132
  <g id="ctx"><rect x="1241" y="337" width="158" height="66" rx="8" fill="#ddf4ff" stroke="#0969da" stroke-width="1.5"/>
133
- <text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under 60 %?</text>
133
+ <text x="1320" y="358.5" text-anchor="middle" font-weight="600">Context under limit?</text>
134
134
  <text x="1320" y="374.5" text-anchor="middle" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="11.5">session context</text>
135
135
  <text x="1320" y="390.5" text-anchor="middle">error = at threshold</text>
136
136
  </g>
@@ -175,7 +175,7 @@
175
175
  <polyline points="1320,403 1320,650" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
176
176
  <text x="1326" y="440" text-anchor="start" font-size="11" fill="#57606a" class="lbl">yes: chain</text>
177
177
  <polyline points="1399,370 1421,370" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
178
- <text x="1410" y="329" text-anchor="middle" font-size="11" fill="#57606a" class="lbl">≥ 60 %: close</text>
178
+ <text x="1410" y="329" text-anchor="middle" font-size="11" fill="#57606a" class="lbl">limit reached: close</text>
179
179
  <polyline points="1500,337 1500,203" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
180
180
  <polyline points="1320,716 1320,746 420,746 420,716" fill="none" stroke="#57606a" stroke-width="1.6" marker-end="url(#arr)"/>
181
181
  <text x="870" y="740" text-anchor="middle" font-size="11" fill="#57606a" class="lbl">next wave: back to step 2, the limits still apply</text>
package/docs/lead.md CHANGED
@@ -74,10 +74,14 @@ picture, not its source.
74
74
  The lead asks both reviewers (`code-reviewer`, `qa-reviewer`) for a short reply with a `Report file: <path>` line in
75
75
  the brief (L440): the full report goes to that file, outside the repository, and the agent answers in 5 lines at most
76
76
  (`Verdict:`, `Blocking:`, `Follow-ups:`, `Report:`). The lead reads the file only when it needs the detail; twenty
77
- reports of 3 to 5k each had swollen its context on 29-10.
78
- 7. **Threshold and chaining.** Before every new wave the lead reads `cadence session context`. Under
79
- **60 %** it chains: `cadence orchestrate --continue` draws the next ready lot of the plan, in the
80
- declared priority, without asking the human « next? ». From 60 % upward, or when the figure cannot
77
+ reports of 3 to 5k each had swollen its context on 29-10. The same line goes to the `ux-reviewer` and the `doc-keeper` (L452), which answer in 15 lines at most
78
+ (`Verdict:`, `Findings:` with the rule or measure of each, `Not verified:`, `Sub-tasks:`, `Report:`): about 25 reports of 2 to 4k had taken
79
+ the lead's context from 80k to 466k tokens on 30-10.
80
+ 7. **Threshold and chaining.** Before every new wave the lead reads `cadence session context`. While the context is under
81
+ the threshold it chains: `cadence orchestrate --continue` draws the next ready lot of the plan, in the
82
+ declared priority, without asking the human « next? ». The threshold is in tokens, not in %: `lead.contextThreshold` in the `cadence.yaml` of the folder where the lead runs
83
+ (an integer ≥ 1, `150000` by default, because 60 % of a 1M window is 600k), and `session context` prints its verdict on a
84
+ second line (`seuil 150000 tokens : sous le seuil` or `atteint`). When it is reached, or when the figure cannot
81
85
  be read (exit code 2: the band is not loaded, or its figure is stale), it starts nothing. A lot
82
86
  that was not handed back ready is reported, not chained over; a question or a UX reservation
83
87
  pending on a lot never stops the chain on the other lots.
@@ -119,6 +123,10 @@ The mechanics around it (the life of a lot in the plan, a lot inside a wave, put
119
123
  [fonctionnement.md](fonctionnement.md). See also the [lead tour](lead.md#lead-tour) section below and the [orchestrate](orchestrate.md#orchestrate)
120
124
  page.
121
125
 
126
+ ## lead priority
127
+
128
+ `cadence lead priority <project> --before|--after <project> [folder] [--depth n]` (L449) puts a project right before or after another one in the order of the projects of the folder: it rewrites the `priority:` list of the folder's `cadence.yaml` (created when missing), the list `lead tour`, `cadence orchestrate --continue` and the [lots panel](cadence-hud.md#the-lots-panel-hud-lots-l401) all read. The projects are named as `lead tour` names them (the folder name, or the path from the folder at `depth:` 2 and more). The list written names **every** project of the folder, in the displayed order: a name that covered several projects by prefix (`shop` for `shop-web`) becomes explicit (the comment on its line moves to the first project it covered), a name that matches no project stays at the end of the list. The rest of the file is kept as it is, comments included (the list stays in the style it had, block or `[a, b]`). Nothing is written, and the answer says so (`… est déjà avant … : rien à changer`), when the project is already at that place. A `cadence.yaml` that cannot be read, a `priority:` that is not a list of names, an unknown project, the same project on both sides or two projects with the same folder name are refused; a move the list cannot express is refused too: when a project's name begins another's (`shop` and `shop-web`, matched by prefix as `priority:` always does), `shop-web` cannot be put after a project that follows `shop`, so the move is refused naming the clash instead of announcing a placement that does not happen. In every refusal the file is left untouched. The command never commits: the `cadence.yaml` change is yours to commit.
129
+
122
130
  ## lead hold and release
123
131
 
124
132
  `cadence lead hold <label> [--ttl minutes]` and `cadence lead release <label>` (L255) declare the sessions the lead starts by hand — a fix subagent, a review, a QA pass launched outside an orchestrated wave. No wave carries such a session, so the slot registry could not see it: on 2026-10-23 the lead ran 4 sessions under a cap of 3 while `créneaux libres` said one slot was free. Orchestrated waves read the holds too (L424): a wave takes no session slot while the held sessions and the running ones already reach the cap, so a hold can no longer be overtaken by a wave starting a session on the « free » slot. `hold` registers the session under its label for `--ttl` minutes (an integer ≥ 1, 60 by default; holding the same label again renews it instead of counting twice); `release` hands it back (releasing a label that is not held answers `aucune session « <label> » inscrite` and exits 0). After the release (L416), `release` prints what it leaves: the slots line, the `prêts, ressources libres` line when a ready lot is free, and `⚠ N créneau(x) libre(s) sur M : lancer un lot prêt ou une session, un créneau vide ne sert à rien` while at least one slot is free (on 2026-10-29 a cap of 3 was taken for 2 and a slot stayed empty for more than an hour); `hold` prints neither. A hold whose time has run out stops counting, but is not forgotten: nothing stops the session (L278), so `cadence lead tour` and `cadence orchestrate --status` print `⚠ session du lead échue : <label> (inscrite depuis 5 h 10 min, échue depuis 2 h 10 min) — la reprendre (cadence lead hold <label> --ttl <minutes>) ou l'arrêter (cadence lead release <label>)` after the slots line, and the `cadence-hud` band shows the same ⚠ row, until the lead renews the hold or releases it (an expired hold nobody touched is dropped 24 h after its expiry). The `--ttl` is therefore the maximum duration declared for the session; the `lead` skill says to renew or stop it at each ⚠. The line is not part of `--json`.
@@ -133,8 +133,8 @@ Both keys are optional; no `continue:` key keeps the old behaviour. A lot whose
133
133
  **Chaining without the human (L148)**: the `lead` skill, once the human has chosen the first lots and the order, does not ask « next? »
134
134
  after a lot that comes back ready: it re-verifies it, records `raf done`, pushes, delivers, has the `qa-reviewer` check the
135
135
  delivered app, then starts the next wave with `--continue`, in the declared priority — one project at a time, within the two-session
136
- limit. Before each new wave it reads the context of its own session with `cadence session context` (the figure of the `ctx` segment of the `cadence-hud` band, which publishes the context of each session to `~/.cadence/orchestrate/hud-context/<session id>.json`; the command picks the file named by `$CLAUDE_CODE_SESSION_ID`, set in the environment of the session's Bash, never "the latest file of the folder" — two sessions of one folder alternate; without the band loaded in that session the command exits with code 2, and a missing or stale figure counts as at the threshold); from
137
- **60 %** upward it starts nothing: it runs `session-close` in each project touched, writes its memory and records three lines for next
136
+ limit. Before each new wave it reads the context of its own session with `cadence session context` (the figure of the `ctx` segment of the `cadence-hud` band, which publishes the context of each session to `~/.cadence/orchestrate/hud-context/<session id>.json`; the command picks the file named by `$CLAUDE_CODE_SESSION_ID`, set in the environment of the session's Bash, never "the latest file of the folder" — two sessions of one folder alternate; without the band loaded in that session the command exits with code 2, and a missing or stale figure counts as at the threshold); once it has reached
137
+ **`lead.contextThreshold`** (tokens, `150000` by default, see [session](session.md)) it starts nothing: it runs `session-close` in each project touched, writes its memory and records three lines for next
138
138
  time (`cadence session next`). Stays with the human: the first choice and the order, the questions a session raised, the UX
139
139
  reservations; a lot that is not ready, a red delivery or a blocking QA finding is reported and not chained over.
140
140
 
@@ -183,6 +183,8 @@ usability (code only when the lot is not `visible`). Failing tests (reported red
183
183
 
184
184
  **One UX review per lot (L221)**: the UX review of a `visible` lot used to run before the code review and again after every correction (finance L75 on 2026-10-15: three Opus UX sessions for one useful verdict). It now runs once, as the last step, on the code the review found compliant: after the single pass on minor findings and its short review when there is one. Without `orchestrate.ux`, or when the app does not start (port taken, silent app), no UX session is played: the lot concludes on the compliant code review and keeps its note (« UX à faire par le lead », « UX non vérifiée : … »). The lot budget bounds writing, never this review; an exhausted wave budget suspends it like any session. Every write step (implementation, correction) clears the lot's UX state (`ux`, `uxVerdict`, UX findings), so a stale UX verdict never sends a compliant code review back to a correction. A resumed wave whose state says `next = ux` (written before this change) replays the code review first unless the last compliant one read the current `HEAD`. The recorded `raf review` verdict and sha are the code review's; a commit made by someone else since that review hands the lot back, as before. A small lot and a lot with no screen keep their single review. The measure before/after is in [`docs/mesures/ux-une-fois-2026-10-15.md`](mesures/ux-une-fois-2026-10-15.md).
185
185
 
186
+ **A lot made visible by its own session (L454)**: `visible` is read from the plan when the wave starts, and a session may add it (warhammer L37 and L43 on 2026-10-30: the lot left « ready » with « — » in the UX column and no UX step). After every writing session (`implement`, `fix`), before the first review, the orchestrator re-reads `visible` in the plan of the lot's worktree: a lot that became visible follows the path of a visible lot — the UX review after the compliant code review (or the single `review-small` pass of a small lot) — and the table shows its UX column. Without `orchestrate.ux`, no UX session can be played: the lot concludes and carries the note « visible ajouté en cours de vague, étape UX à jouer », under the table, for the lead. An unreadable plan, or a `visible` removed during the wave, leaves the lot as it was; a `screen:` added during the wave does not reopen the spec stage.
187
+
186
188
  **A project without `orchestrate.test` is flagged (L327)**: without the key the wave never runs the suite itself — only the review does, if it decides to (it ran 12 test files of a project on 2026-10-25 and a red test reached « ready »). `cadence orchestrate --dry-run` writes one line per repository, under the first lot of that repository: `<repo> · porte de tests absente : orchestrate.test n'est pas déclaré, la vague ne lancera pas la suite (seule la revue le fera si elle le décide) · commande probable : npm test (à déclarer en orchestrate.test)`, and [`cadence lead tour`](lead.md#lead-tour) writes the same line for every project of the folder. The probable command is read from the repository, never run: `npm test` when `package.json` has a `scripts.test` (the `npm init` placeholder « no test specified » does not count), `pytest` when `pyproject.toml` names pytest, `mvn test` when there is a `pom.xml`; with none of them the line carries no command. A `cadence.yaml` that cannot be read gives no such line (it has its own error). Declaring `test:` (and `build:`) in the project's `cadence.yaml`, once the command is known to be green, removes the line.
187
189
 
188
190
  **Tests already red at the start (L213)**: a lot is no longer handed back « tests rouges » for failures that were there before it. When `orchestrate.test` fails (or the session reports red tests), the program reads the failing tests from the runner's output (vitest/jest `FAIL`/`●`, pytest `FAILED`, go `--- FAIL:`, TAP `not ok`), replays the same command at the sha the lot started from (the parent of the lot's oldest work commit, plan-only commits left out, else the `HEAD` before its first session) in a throwaway `git worktree` outside the repository, and compares: if every failing test was already red, it is not a defect of the lot — no correction pass, the code review takes place, the reviewer is told, and the final table lists them apart (`déjà rouge au départ (<sha>) : <test>`, 30 at most then a count). The worktree gets links to the repository's ignored folders at any depth, such as `.venv` and, in a monorepo, `packages/*/node_modules`, when their parent exists at that sha; build outputs such as `dist` or `coverage` are left out. Each `node_modules` is rebuilt entry by entry, every entry linked to the live one except the workspace links of npm, pnpm and yarn (`node_modules/@x/shared -> ../../packages/shared`, a link to code of the repository outside any `node_modules`), which are remade towards the worktree's own copy so that the start sha is tested against its own sources, not the lot's (a workspace package that does not exist at the start sha gets no link). Known limit: only `node_modules` links are remade; whatever points into the live repository by an absolute path, such as an editable Python install (`.venv` with a `.pth` file naming the live `src`, `pip install -e`) or an absolute symlink in another ignored folder, still runs the lot's code in the baseline. Likewise, a workspace package consumed through its build output (`main: dist/...`) cannot load in the baseline worktree: output dirs are not linked, so such a monorepo keeps the pre-L213 « tests rouges » outcome unless `orchestrate.build` is declared (the build is then played first in the baseline worktree, see below). A lot resumed in a later wave keeps the same baseline: the sha is the parent of its oldest work commit, not the `HEAD` of the new wave, so a failure the lot itself introduced in an earlier wave is never excused. A test red now and not at the start stays a blocking finding, and only that one is given to the correction (the others are counted as « already red »). When the output names no test, or the baseline cannot be run, the tests stay red as before (no comparison is guessed). A failure the output does not tie to a test (unhandled error, a spec file that fails to load such as jest « Test suite failed to run » or vitest `FAIL a.test.ts [ a.test.ts ]`, coverage threshold) keeps the tests red. Without `orchestrate.test` the program cannot replay anything: a session that reports green tests and lists under `echecsDeBase` the failures it saw at the start sha gets them listed apart, marked « non vérifié » in the table, and the review takes place; a session that reports the tests red stays red, the brief asking it not to report red for base failures alone — unless (L215) the report says so in a structured way: with `vert: false`, a non-empty `echecsDeBase` and an empty `nouveauxEchecs` (the list of tests red now that were not red at the start sha), `vert: false` is not a reason to correct, the lot carries a « non vérifié » warning, the reviewer is told that the baseline is unverified and the review takes place. A non-empty `nouveauxEchecs` with `vert: false` triggers a correction pass (the new failures are in the finding); an absent `nouveauxEchecs`, or an empty `echecsDeBase`, keeps the correction. With `orchestrate.test` declared (L361), the same structured report is honoured when the program's own gate is green (`orchestrate.test` exits 0): `vert: false` with a non-empty `echecsDeBase` and an empty `nouveauxEchecs` goes straight to the review, with a warning, without a correction pass and without the « tests rouges » escalation; without those two fields the claim of red tests still triggers the correction. The program never guesses from the text of the result (« 24 failed »). The `implement` and `fix` briefs ask for `echecsDeBase` and `nouveauxEchecs`.
@@ -870,6 +872,7 @@ orchestrate:
870
872
  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)
871
873
  build: npm run build # run BEFORE the tests (L333); both results go to the reviewer, who does not redo them (optional)
872
874
  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)
875
+ gate: { readsPlan: true } # a test of the gate reads the plan (`.cadence/raf.yaml`): `cadence verify --gate` no longer takes over a wave's verdict under commits that change only the plan (L444), it replays; `true` or the list of the paths of the tests that read it; absent or `false`: those commits are crossed (L450, `docs/deliver.md`) (optional)
873
876
  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)
874
877
  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)
875
878
  maquette: true # default: UX review of the mockup between spec and accept (see « The mockup review »); false skips it
package/docs/raf.md CHANGED
@@ -66,6 +66,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
66
66
  | `raf todo <id> [--reason text]` | put a started lot back to `todo` (`doing` → `todo`, `started:` removed, dated note « remis en attente : reason »): refused when work commits cite the lot (plan-only and version commits do not count, as `raf commits`), or on a sub-task; sub-tasks and other keys are left as they are. **Reopening** (L396): on a `done` or `dropped` lot, `raf todo` is THE command — never the YAML — that reopens it (`→ todo`, `finished:` removed, `started:` kept so its commits stay its own, dated note « rouvert (était done) : reason »); `raf start`/`raf done`/`raf drop` on a closed lot say so (`L3 est done ; le rouvrir si c'est voulu : raf todo L3`). An archived lot reopened this way comes back into the plan file at that write |
67
67
  | `raf migrate [--dry-run]` | move `docs/plan/raf.yaml` to `.cadence/raf.yaml` and rewrite the readers of the old path in the repository, see [Moving the plan: `raf migrate`](#moving-the-plan-raf-migrate) |
68
68
  | `raf note <id> "text"` | dated note — keep decisions next to the work |
69
+ | `--verbose` on `raf done`, `ux`, `review`, `note`, `move`, `public` · `raf hook post-commit --verbose` (L453) | those commands print no title of the lot they touch (nothing in the lead's context but `L12 → done` and the `plan non commité` command, which is unchanged); `--verbose` adds one line `L12 doing <full title>` (nothing for a sub-task). The post-commit hook warning `raf: L12 (doing) <title>` cuts the title at 60 characters (59 and `…` beyond; a title of 60 stays whole), `--verbose` on the hook gives it whole. `raf show`, `raf list`, `raf now` and the session tables are not concerned |
69
70
  | `raf title <id> "new title"` | replace the title of a lot of any status (`done` included) and add a dated note « titre remplacé : « old title » »; `raf now`, `raf show`, `raf list`, `session start` and the proposals read the new one. Refused on an empty or identical title, an unknown lot or a sub-task |
70
71
  | `raf archive [--before date] [--dry-run]` | move the `done` and `dropped` lots (those finished strictly before `--before`, when given; a lot with no `finished:` date stays then) out of the FILE `.cadence/raf.yaml` into `.cadence/archive/<year>.yaml`, the year of their end (L396). **The archive is a storage detail, invisible to every reader**: the loaded plan presents all the lots, active and archived, **in the order they had before the archiving** — each archived lot takes its place by number among the lots of the plan file (just before the first active lot with a greater number), the order of the plan file itself never moves — so `raf show`, `raf list`, `raf commits`, `raf check`, `raf now`, `raf gantt`, `cadence news`, `cadence perf`, `session start`, `lead tour`, `cadence ref coverage`, `cadence deliver`, the post-commit hook and `cadence orchestrate` print exactly what they printed before the archiving — nothing has to « also look in the archive », a lot id is never reused, and a commit or a News entry citing an archived lot is not « inconnu ». What changes is the file: the plan the sessions open stays short (the cadence plan weighed 559 KB, ≈ 150 k tokens) and the briefs of the implementation, the sheets, the code review and the pre-check tell the sessions never to open `.cadence/archive/`. **Writing**: every lot goes back to the file it came from — `raf note`, `raf review`, `raf ux`, `raf title`… on an archived lot rewrite its year file, the plan file is untouched; a lot reopened (`raf todo <id>`, or `status: doing` set by hand in a year file) comes back to the end of the plan file at the next write, so the archive only ever holds `done` and `dropped` lots. **Lossless, whatever fails**: the year files are written before the plan file (should the plan then fail to write, a moved lot is at worst duplicated — which the next load refuses by name — never lost), and any exception during the write (`EACCES` on a read-only file, a full disk, a `plan.onWrite` command that fails) puts the plan, the year files and the declared files back as they were — a year file born of that write disappears — and the error says so (`écriture du plan refusée (EACCES, …) — plan et archive inchangés`). **Guards**: a closed lot with an open gap (what `raf check` signals — code review, UX review, docs.sync, News entry…) stays in the plan with the gap as its printed reason (`L1 reste dans le plan : écart ouvert — L1 est visible et terminé sans entrée Nouveautés …`); a lot named in `after:` by another, or counted by a `gate:`, is archived like any other — the readers see the archive, so the dependency and the count read the same. A same id found in two files (plan and archive, or two years) is a named error that stops every command (`identifiant en double : L1 est dans .cadence/archive/2025.yaml et dans .cadence/raf.yaml`) until one copy is removed by hand — never a silent choice of one copy. `--dry-run` lists without writing; `--before` takes `YYYY-MM-DD`; refused on a read-only plan (its tool keeps its own history) and while the plan is still at the legacy `docs/plan/raf.yaml` (the archive would land under `docs/`, published, and `raf migrate` would not move it: `raf migrate` first, the refusal says so). For `cadence orchestrate`, the year files are plan files like `.cadence/raf.yaml`: a session that commits one has « touché le plan ». Like every plan write, it is not committed: the command to commit the plan and the archive files together, at explicit paths, is printed (`plan.commit` template, action `N lot(s) archivé(s)`); replaying moves nothing twice. The archive files are kept with the plan: touching them is not working on a lot. **The published plan**: a generator run by `plan.onWrite` (below) that reads only the file `.cadence/raf.yaml` publishes a plan without the archived lots; `raf archive` says so on stderr when `plan.onWrite` is declared, the generator must also read `.cadence/archive/*.yaml` (key `lots`, same lot format as the plan) |
71
72
  | `raf show <id> [--notes]` | one lot: status, dates, `goal : …` right after the title when the lot has a `goal:` (native key or `plan.fields.goal`, L272: the objective that replaces the title in the `orchestrate` briefs), `after`, public title, then for a visible lot two stable lines — `visible : oui` (followed by `screen : …` when the lot has one) and `ux : <verdict>` or `ux : absent` (the `raf ux` verdict, L439: what the `livreur` agent reads before closing a visible lot; a lot that is not visible prints neither line), `spec`, `uses : banc, passerelle` when the lot has `uses:`, dated notes in order, then the counted commits (as `raf commits`, neighbouring repositories included); `--notes` prints the notes alone |
package/docs/session.md CHANGED
@@ -20,8 +20,15 @@ cadence session context # `ctx 42 % (84000/200000)`: the context of Y
20
20
  # overrides it; with no variable, the folder decides only if a single session publishes for it
21
21
  # (two sessions in one folder = exit 2, "pass --session"); exit 2 too when no fresh figure
22
22
  # (band not loaded) = treat as at the threshold
23
+ # a second line gives the verdict against `lead.contextThreshold` (tokens, 150000 by default):
24
+ # `seuil 150000 tokens : sous le seuil` or `… : atteint` (tokens ≥ threshold)
23
25
  ```
24
26
 
27
+ `lead.contextThreshold` is read in the `cadence.yaml` of the folder where `session context` runs (the lead's folder): an integer ≥ 1,
28
+ in tokens, because a percentage moves with the window (60 % is 120k on 200k, 600k on 1M). A zero, a negative, a decimal or
29
+ a non-number is refused with the key's name (exit 2, read as at the threshold). It is not scaled to the window: on a window
30
+ smaller than the threshold the verdict never reads `atteint`, so set a lower one there.
31
+
25
32
  `session start` records its time in the worktree's state (`session.json`, under the git dir); the next
26
33
  `session close` takes it as its window and names it in the section title: `Commits de la période (depuis
27
34
  l'ouverture de 09:12)` (with the date when it is not today). A close never moves the boundary, refused
package/docs/skills.md CHANGED
@@ -50,6 +50,8 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
50
50
  edits code. Read-only for real: its `tools:` list is `Read, Grep, Glob, Bash` and the
51
51
  Playwright tools (`mcp__playwright`, and `mcp__plugin_playwright_playwright` when the
52
52
  browser comes from the Playwright plugin) — no `Edit`, no `Write`. In a wave it gives its captures the absolute path of the capture directory its brief names (a bare name would land in the repository); outside a wave, a path under `.playwright-mcp/` (e.g. `.playwright-mcp/page-home.png`; the Playwright MCP resolves a bare file name against the repository, where it would land at the root); `qa-reviewer` follows it too.
53
+ **Short reply (L452):** with a `Report file: <absolute path>` line in the brief it answers in 15 lines at most (`Verdict:`, `Findings:` with
54
+ the rule or measure of each, `Not verified:`, `Sub-tasks:`, `Report:`, `Captures:`) and writes the full report to that file (outside the repository, verdict last).
53
55
  - **code-reviewer** (agent): any stack; given a repository and a lot id, it reads
54
56
  the diff itself from the commits that cite the lot — not the author's summary —
55
57
  and the project's CLAUDE.md, when there is one, for its conventions; findings grounded in a
@@ -69,6 +71,8 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
69
71
  document behind the lot; the lead calls it by hand. A document outside the repository only when the project's
70
72
  `cadence.yaml` names it (`docs.articles`): the agent then proposes a lot for that repository's plan and never writes it
71
73
  ([news](news.md#the-librarian-doc-keeper)).
74
+ **Short reply (L452):** with a `Report file: <absolute path>` line in the brief it answers in 15 lines at most (`Verdict:`, `Findings:` with
75
+ the rule or measure of each, `Not verified:`, `Sub-tasks:`, `Report:`) and writes the full report to that file (outside the repository, verdict last).
72
76
  - **reference-analyst** (agent, L265): given the folder of a document registered by `cadence ref add` and the file to
73
77
  write, it looks at every rendered page whole, then every zone at full resolution, measures with `cadence ref measure`
74
78
  (never by eye), copies every text verbatim, links each figure to the passages that cite it and writes one reading in
package/docs/whats-new.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.28.0**: projects are reordered from the lots panel or `cadence lead priority`; the lead's context threshold is in tokens; reviewers answer the lead in a few lines; the gate replays under plan commits when tests read the plan.
7
8
  - **1.27.0**: `cadence corrections --classify` names the sub-cause of each code-bug pass and the dominant link of the chain, `cadence perf` shows it; the mockup review checks hierarchy, tokens, texts and extra accessibility.
8
9
  - **1.26.0**: a new `livreur` agent delivers a ready lot for the lead (gate, UX verdict, `raf done`, push, deliver) and hands back one line; `raf show` prints `visible` and `ux` for a visible lot.
9
10
  - **1.25.1**: publishes 1.25.0 (tagged, never published); tests now pass in a fresh clone and `cadence release` replays them in one; the panel of lots shows each project's last delivery.
@@ -54,6 +55,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
54
55
 
55
56
  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).
56
57
 
58
+ **1.28.0**: the lead spends less of its context, and the projects are ordered where they are read. `cadence lead priority <project> --before|--after <project>` rewrites the `priority:` list of the folder's `cadence.yaml`, comments kept, and the lots panel does the same by drag and drop or Alt+↑/↓ in a region « ordre des projets » above the tree (L449, `docs/lead.md`, `docs/cadence-hud.md`); a move it cannot write back faithfully (a name that prefixes another) is refused with its reason. The lead's context threshold is a number of tokens, `lead.contextThreshold` (150000 by default), read by `cadence session context` (L437). `ux-reviewer`, `doc-keeper` and the reviewers answer the lead in a few lines and keep the full report in the file named by `Report file:` (L452), and `raf` cuts the titles it echoes to 60 characters (L453). `orchestrate.gate.readsPlan` lets a project whose tests read the plan say so: `cadence verify --gate` then replays the suite under plan-only commits instead of taking over the wave's verdict (L450). A lot made `visible` by its own session during a wave now goes through the UX step instead of coming out ready without it (L454).
59
+
57
60
  **1.27.0**: the code-bug bottleneck is named, and the mockup review looks at more. `cadence corrections` sub-classifies the « bug de code » passes (L447, `docs/corrections.md`): one of seven sub-causes (edge case, broken test, misread contract, forgotten caller, false statement, empty test, other) per pass, asked of the local model with `--classify` and cached in `.cadence/corrections-sous-causes.json` (`--cache-sous-causes`); the report gains the section « Bug de code par sous-cause » and the dominant link of the chain (at least 10 passes sub-classified and 40 % of them), `--json` the key `bugCode`, and `cadence perf` prints the same count under « causes dominantes », read-only; nothing changes for a window with no `bug-code` pass. On the 24→30-10 window the dominant link is the `implement` brief (`docs/mesures/bug-code-2026-10-30.md`): 96 of the 136 `bug-code` passes are edge cases, so the « edge cases » bullet of the L428 block of `implement.md` now asks for the list before the first line of code and for each listed input to be played once on the real path before reporting (marked `[L447]`); the next measure counts L428 and L447 together, from 2026-10-30. The mockup review (L448, `docs/orchestrate.md`) checks four more families of design criteria, each tied to a named rule or a count: visual hierarchy (one primary element per state), consistency with the product tokens (values that do not come from the product's CSS variables), quality of the texts (error, empty and loading states, action labels, destructive confirmations, one word per notion) and complementary accessibility (focus order, name, role and label, error as text, text at 200 %). It applies in mockup mode only, with no new review or session and the same severity scale and report; the spec writer and its brief apply the drawing-time criteria before the review. Measure: « sessions ux par lot visible » of `cadence perf` (1.6 on the window of 30-10, target 1.2).
58
61
 
59
62
  **1.26.0**: the lead hands the delivery of a ready lot to a subagent. The `livreur` agent (L439, installed by `cadence skills install` like the others) takes one ready lot and one sha given by the lead, and runs `cadence verify --gate --sha`, the UX verdict according to `lead.ux`, `raf done` and the plan commit it prints, `git push origin <sha>:main` and `cadence deliver`, then hands back one line (`L84 livré 3d6edf4, QA à lancer <url>`) or one `Question : …`. It first checks that the sha is an ancestor of HEAD, has no lot that is not done below it and is not already in `origin/main` and in the delivery log; it launches no agent, never the `qa-reviewer` (the lead does, after the line), and the lead launches one `livreur` at a time (`docs/lead.md`). A relaunch on a lot already done weighs both proofs (`origin/main` and `deliveries.log`), so « pushed, never delivered » is no longer masked by « déjà clos ». It reads `git diff --name-only origin/main..<sha>` right after `git fetch origin main`, before the gate and the push, to decide whether a QA pass is due and refuses a brief without the base URL before any action. `raf show <lot>` prints two new stable lines for a visible lot, `visible : oui` (then `screen : …` when the lot has one) and `ux : <verdict>` or `ux : absent` (`docs/raf.md`): that is how the `livreur` knows whether the lot is visible and whether the human's UX verdict is recorded (`lead.ux: human`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.27.0",
3
+ "version": "1.28.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",
@@ -80,7 +80,7 @@ like (the human's words) and keeps the rest verbatim. **A stale title is no reas
80
80
 
81
81
  A lot that adds or changes a screen is `visible`: after the implementation, have the
82
82
  `ux-reviewer` agent review it (give it the URL or the way to run the app) and bring its verdict and
83
- proposed sub-tasks back to the human — or, where the folder's `cadence.yaml` says `lead.ux: lead`, form the UX verdict yourself (see « The UX verdict » in section 2c).
83
+ proposed sub-tasks back to the human (add a line `Report file: <absolute path>` outside the repository, and the folder of the captures: it answers in 15 lines at most — verdict, findings with their rule or measure, not verified, sub-tasks, the paths — and keeps the measurements in that file, L452) — or, where the folder's `cadence.yaml` says `lead.ux: lead`, form the UX verdict yourself (see « The UX verdict » in section 2c).
84
84
 
85
85
  ### 2b. A wave through `cadence orchestrate`
86
86
 
@@ -157,12 +157,15 @@ one named by `$CLAUDE_CODE_SESSION_ID` (Claude Code sets it in the environment o
157
157
  whatever the folder; `--session <id>` overrides it). Without the variable it falls back to the folder only if a single
158
158
  session publishes for it; two sessions in one folder make it exit 2 ("pass --session"), never guess the latest. It prints
159
159
  `ctx 42 % (84000/200000)`, the figure the `ctx` segment of the band shows, which the band publishes to
160
- a file because it is only a rendering the model cannot see. If the command fails (exit code 2: the band is not loaded in
160
+ a file because it is only a rendering the model cannot see, then a second line, `seuil 150000 tokens : sous le seuil` or
161
+ `… : atteint`: the threshold is in **tokens**, not in %, because a percentage moves with the window (60 % is 120k on 200k and
162
+ 600k on 1M). It is `lead.contextThreshold` in the `cadence.yaml` of the folder where the lead runs (150000 by default); read the
163
+ second line, do not recompute it. If the command fails (exit code 2: the band is not loaded in
161
164
  this session, several sessions share the folder without the variable, or the figure is older than two minutes), treat it as
162
165
  **at the threshold**: you cannot prove you are under it, and without the band loaded the lead does not chain.
163
- From **60 %** upward, start nothing: at the threshold the lead runs `session-close` in each project touched, writes its
166
+ When the second line says `atteint` (the context has reached the threshold), start nothing: at the threshold the lead runs `session-close` in each project touched, writes its
164
167
  memory (filtered, as `session-close` says), and records three lines for next time with `cadence session next` — the wave
165
- that was about to start comes first among them. Under it, chain. The lead says in one sentence which side of the threshold
168
+ that was about to start comes first among them. While it says `sous le seuil`, chain. The lead says in one sentence which side of the threshold
166
169
  it is on each time it chains or stops; it never waits for the figure to move (never a polling loop).
167
170
 
168
171
  **What stays with the human**: the first choice and the order; the questions a session raised (the lead brings them
@@ -193,7 +196,7 @@ is recorded by the lead's own `raf ux` or, for a lot it hands over, by the `livr
193
196
 
194
197
  0. After an orchestrated wave the review is already done, by a fresh session: read the table, then go to
195
198
  point 2. For a lot delegated by hand:
196
- 0b. For a lot delegated by hand, `raf check` or the `dérive` column reporting `documentation en retard` is handled **before** the review: hand the lot to the `doc-keeper` agent (a fresh subagent; the absolute path of the project and the lot id; it updates the documents, or only lists them if you say read-only). Read the `docs.articles` key of the project's `cadence.yaml` yourself and put its entries (`repo`, `file`, and the absolute path of that repository, `repo` being relative to the project) in the brief, with read access to that repository for the subagent: the agent examines a document outside the repository only if the brief names it, and with no entry (the key is absent by default) you name none. A proposed lot it returns for a document outside the repository (`docs.articles`) is yours to decide: it never writes to that plan.
199
+ 0b. For a lot delegated by hand, `raf check` or the `dérive` column reporting `documentation en retard` is handled **before** the review: hand the lot to the `doc-keeper` agent (a fresh subagent; the absolute path of the project and the lot id; it updates the documents, or only lists them if you say read-only). Read the `docs.articles` key of the project's `cadence.yaml` yourself and put its entries (`repo`, `file`, and the absolute path of that repository, `repo` being relative to the project) in the brief, with read access to that repository for the subagent: the agent examines a document outside the repository only if the brief names it, and with no entry (the key is absent by default) you name none. A proposed lot it returns for a document outside the repository (`docs.articles`) is yours to decide: it never writes to that plan. Add a line `Report file: <absolute path>` (outside the repository) to the brief: it answers in 15 lines at most and writes its full report there (L452); read the file only to apply a document it only listed.
197
200
  1. The `code-reviewer` agent, as a fresh subagent (most capable model), reviews the lot. Give it
198
201
  the absolute path of the project and the lot id, **not** the author's report: it reads the diff
199
202
  itself from the commits that cite the lot, runs the checks, and returns real defects only,