@sylad/cadence 1.27.0 → 1.29.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.
@@ -0,0 +1,125 @@
1
+ import { lstatSync, readdirSync, realpathSync } from 'node:fs';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
+ import { git, gitFailure, tryGit } from './worktree.js';
4
+ /** Voie prévue pour un travail qui doit toucher un dépôt tiers (L456) : une branche à part, jamais la branche principale. */
5
+ export const wipBranch = (project, lot) => `wip/${project}-${lot}`;
6
+ const real = (p) => {
7
+ try {
8
+ return realpathSync(p);
9
+ }
10
+ catch {
11
+ return p;
12
+ }
13
+ };
14
+ /**
15
+ * Les dépôts tiers d'un lot (L456) : les enfants directs du dossier parent du dépôt du lot (le parent du chemin du dépôt, pas celui d'un worktree) dont `.git`
16
+ * est un dossier. Ni le dépôt du lot, ni le dossier parent, ni `excluded` (voisins déclarés, dépôts des autres lots de la vague) ; un worktree lié (`.git` fichier)
17
+ * est ignoré, et un dépôt atteint par deux chemins (lien symbolique) n'est rendu qu'une fois, sous son chemin réel.
18
+ */
19
+ export function tierRepos(lotRepo, excluded) {
20
+ const parent = dirname(resolve(lotRepo));
21
+ const skip = new Set([lotRepo, parent, ...excluded].map(real));
22
+ let names;
23
+ try {
24
+ names = readdirSync(parent).sort();
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ const found = new Set();
30
+ for (const name of names) {
31
+ const root = real(join(parent, name));
32
+ if (skip.has(root) || found.has(root))
33
+ continue;
34
+ try {
35
+ if (!lstatSync(join(root, '.git')).isDirectory())
36
+ continue;
37
+ }
38
+ catch {
39
+ continue; // pas un dépôt git (dossier ordinaire, fichier)
40
+ }
41
+ found.add(root);
42
+ }
43
+ return [...found];
44
+ }
45
+ /**
46
+ * Relevé d'un dépôt tiers, sans réseau : une seule lecture des références locales. Branches principales : celle que désigne `origin/HEAD` (posé par clone), `main` et
47
+ * `master`, parmi les branches locales qui existent. Lève quand git échoue (dépôt illisible).
48
+ */
49
+ export function readTier(repo) {
50
+ // %(HEAD) en dernier : la sortie est rognée, un « espace » de tête d'une première ligne serait perdu.
51
+ const out = git(repo, 'for-each-ref', '--format=%(refname)\t%(objectname)\t%(symref)\t%(HEAD)', 'refs/heads', 'refs/remotes/origin/HEAD');
52
+ const heads = new Map();
53
+ let branch = null;
54
+ let declared = null;
55
+ for (const line of out.split('\n').filter(Boolean)) {
56
+ const [ref, sha, symref = '', current = ''] = line.split('\t');
57
+ if (ref === 'refs/remotes/origin/HEAD') {
58
+ if (symref.startsWith('refs/remotes/origin/'))
59
+ declared = symref.slice('refs/remotes/origin/'.length);
60
+ continue;
61
+ }
62
+ const name = ref.slice('refs/heads/'.length);
63
+ heads.set(name, sha);
64
+ if (current === '*')
65
+ branch = name;
66
+ }
67
+ const mains = {};
68
+ for (const name of new Set([declared, 'main', 'master'])) {
69
+ const sha = name ? heads.get(name) : undefined;
70
+ if (name && sha)
71
+ mains[name] = sha;
72
+ }
73
+ return { branch, mains };
74
+ }
75
+ /** Relève chaque dépôt ; un dépôt illisible est dit au journal et laissé de côté. */
76
+ export function takeTiers(repos, log) {
77
+ const reads = new Map();
78
+ for (const repo of repos) {
79
+ try {
80
+ reads.set(repo, readTier(repo));
81
+ }
82
+ catch (e) {
83
+ log(`dépôt tiers ${basename(repo)} ignoré : lecture impossible (${gitFailure(e)})`);
84
+ }
85
+ }
86
+ return reads;
87
+ }
88
+ /** Plus de commits que cela dans le constat : le reste est compté. */
89
+ const SHOWN = 5;
90
+ /** Un commit du lead (R18) : le sujet commence par « plan: ». Il n'est toléré que s'il ne change que le fichier du plan. */
91
+ const LEAD_SUBJECT = /^\S+ plan: /;
92
+ /** Une ligne « sha sujet » est-elle un commit du lead : sujet « plan: » et le seul fichier changé est celui du plan ? */
93
+ function isLeadCommit(repo, line, planFile) {
94
+ if (!LEAD_SUBJECT.test(line))
95
+ return false;
96
+ const files = (tryGit(repo, 'diff-tree', '--root', '--no-commit-id', '--name-only', '-r', line.split(' ')[0]) ?? '').split('\n').filter(Boolean);
97
+ return files.length > 0 && files.every((f) => f === planFile);
98
+ }
99
+ /**
100
+ * Première branche principale dont le sha a bougé entre deux relevés, avec ce qu'il faut en dire : les commits ajoutés (sha court et sujet), ou la branche créée,
101
+ * supprimée ou déplacée sans commit nouveau ; les commits du lead (R18 : « plan: » et rien d'autre que `planFile`) ne comptent pas. Une branche absente d'un seul côté a bougé (créée ou supprimée pendant la session). Null : rien n'a bougé.
102
+ */
103
+ export function tierMoved(repo, before, after, planFile) {
104
+ for (const branch of [...new Set([...Object.keys(before.mains), ...Object.keys(after.mains)])].sort()) {
105
+ const b = before.mains[branch];
106
+ const a = after.mains[branch];
107
+ if (a === b)
108
+ continue;
109
+ if (!a)
110
+ return { branch, detail: `branche supprimée (${b.slice(0, 7)})` };
111
+ // branche créée : seuls comptent les commits que aucune branche principale d'avant ne portait déjà
112
+ const range = b ? [`${b}..${a}`] : [a, '--not', ...Object.values(before.mains)];
113
+ const all = (tryGit(repo, 'log', '--abbrev=7', '--format=%h %s', ...range) ?? '').split('\n').filter(Boolean);
114
+ const lines = all.filter((l) => !isLeadCommit(repo, l, planFile));
115
+ if (all.length && !lines.length)
116
+ continue; // que des commits du lead
117
+ if (!lines.length && !b)
118
+ return { branch, detail: `branche créée (${a.slice(0, 7)})` };
119
+ if (!lines.length)
120
+ return { branch, detail: `branche déplacée de ${b.slice(0, 7)} à ${a.slice(0, 7)}` };
121
+ const more = lines.length > SHOWN ? ` | … (${lines.length - SHOWN} de plus)` : '';
122
+ return { branch, detail: lines.slice(0, SHOWN).join(' | ') + more };
123
+ }
124
+ return null;
125
+ }
@@ -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
+ }
@@ -10,7 +10,7 @@ import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
10
10
  import { failedTests, hasUnnamedFailure, unloadedSuites } from './orchestrate/baseline.js';
11
11
  import { configuredCap } from './orchestrate/command.js';
12
12
  import { replayRed, TEST_WORKERS_ENV, testWorkersFrom } from './orchestrate/workers.js';
13
- import { copyDeps, git, gitFailure, ignoredDeps, isCopiedDep, linkIgnored } from './orchestrate/worktree.js';
13
+ import { copyDeps, git, gitFailure, ignoredDeps, isCopiedDep } from './orchestrate/worktree.js';
14
14
  /** Délai de chaque commande de la porte : celui du rejeu de la ligne de base de la vague. */
15
15
  const GATE_TIMEOUT_MS = 30 * 60_000;
16
16
  /** Lignes de la fin de la sortie montrées quand aucun test n'est identifiable dans une sortie rouge. */
@@ -89,7 +89,7 @@ function mainTreeMoved(repo, sha) {
89
89
  * `cadence verify --gate` (L348) : la porte du projet (`orchestrate.build`, puis `orchestrate.test`, dans l'ordre de la vague : les tests lisent ce que le build produit)
90
90
  * rejouée au `sha` (L389) dans l'arbre principal quand il est propre et à ce sha (les outils qui refusent un `node_modules` venu d'un autre arbre y tournent chez eux) ;
91
91
  * sinon (arbre non propre ou à un autre commit, ou vague d'orchestration vivante sur le dépôt, L431) dans un worktree jetable, hors du dépôt, dont les dépendances non suivies sont copiées depuis le dépôt vivant (`orchestrate.worktreeDeps`,
92
- * sinon les `node_modules` de tout niveau et le `.venv` ; les autres dossiers ignorés sont reliés, comme dans les worktrees de vague ; la copie est asynchrone pour que Ctrl-C la coupe). `verify.inPlace` impose l'un (`true`, refus si l'arbre n'est pas propre au sha) ou l'autre (`false`).
92
+ * sinon les `node_modules` de tout niveau et le `.venv` ; les autres dossiers ignorés (données, `.env`) sont absents de la copie, L455 ; la copie est asynchrone pour que Ctrl-C la coupe). `verify.inPlace` impose l'un (`true`, refus si l'arbre n'est pas propre au sha) ou l'autre (`false`).
93
93
  * La première ligne de la sortie dit où la porte a tourné ; en place, HEAD et `git status` sont relus après les étapes et, si l'arbre a bougé (HEAD avancé, status sale), la sortie dit ce qui a changé, que le verdict n'est pas celui du sha et sort en code 2, rouge ou vert ; la ligne « vert » / « rouge » n'est alors pas écrite. Les deux commandes sont rejouées même si la première est rouge : le compte rendu est entier.
94
94
  * Chaque commande reçoit `CADENCE_TEST_WORKERS` comme dans la porte d'une vague (L350, L387) : la valeur de l'environnement de l'appelant si c'est un entier positif, sinon cœurs ÷ plafond de sessions (`CADENCE_MAX_SESSIONS`, 2 par défaut) ;
95
95
  * sans elle le rejeu `--maxWorkers=$CADENCE_TEST_WORKERS` de `orchestrate.testReplay` sortirait en erreur et ne prouverait rien.
@@ -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) {
@@ -218,9 +219,8 @@ async function playGate(ctx) {
218
219
  missing = config.worktreeDeps.filter((n) => !present.includes(n));
219
220
  }
220
221
  else {
221
- // Seules les dépendances sont copiées ; les autres dossiers ignorés (données, .env) restent reliés, comme dans un worktree de vague, sans rien dupliquer dans /tmp.
222
+ // Seules les dépendances sont copiées ; les autres dossiers ignorés (données, .env) ne sont ni copiés ni reliés (L455) : un lien rendrait à un test qui compare des chemins par realpath l'arbre principal.
222
223
  copied = await copyDeps(repo, dir, ignoredDeps(repo).filter(isCopiedDep));
223
- linkIgnored(repo, dir);
224
224
  }
225
225
  const why = mode === false ? 'verify.inPlace: false' : (obstacle ?? `la vague ${liveWave} est vivante sur ce dépôt : HEAD et le plan y bougent`);
226
226
  out(`verify --gate ${short} : porte jouée dans une copie jetable ${dir} (${why}) ; dépendances copiées : ${copied.length ? copied.join(', ') : 'aucune'}`);
@@ -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
@@ -290,7 +290,7 @@ cadence verify --gate --sha 6b0d9aa --project app # from the parent folder, t
290
290
  **Where the gate runs (L389).** The first line of the output says it:
291
291
 
292
292
  - the main tree is **clean and at the sha** (`git status --porcelain` empty, `HEAD` equal to the sha): the gate runs **there**, in the repository itself, with its own `node_modules` and its real paths (`verify --gate 6b0d9aa : porte jouée dans l'arbre principal /path/app (propre, au sha)`). A tool that refuses a `node_modules` linked from another tree (Astro: « Could not load …/node_modules/astro/… »), or a test that reads files by a path relative to the checkout, is red in a copy for reasons that have nothing to do with the sha; on 27-10 avatar-pandora and finance-tracker's ux-demo.spec were both green in the main tree and red in the copy. The commands may write their usual ignored outputs there (`dist`, caches);
293
- - otherwise (uncommitted changes, untracked files, or another commit checked out: a wave's lots may be working in it): a throwaway worktree of the sha under the OS temporary folder (`cadence-verify-…`), removed whatever the result, where the **dependencies of the main tree are copied** (`node_modules` of every level and `.venv`; not linked: a copied `node_modules` is a real one for the tools that refuse a link; a `node_modules` that is itself a link is copied by its content; workspace links, relative, are copied as they are and point inside the copy; those that leave the repository, `file:../neighbour`, are redone towards their live target) and the other ignored folders (data, `.env`) are linked, never copied (on 28-10, jobmail's `data` weighed 14 GB, maritime's `docs/refdocs` 961 MB). The copy is asynchronous: Ctrl-C received during it is handled at once. The line names the worktree, why the main tree was not used, and the copied dependencies (`… porte jouée dans une copie jetable /tmp/cadence-verify-x (l'arbre principal a des modifications non validées ou des fichiers non suivis) ; dépendances copiées : node_modules`), always **first**: the notice of declared dependencies absent from the main tree (`orchestrate.worktreeDeps`) comes after it. The copy of a large `node_modules` takes time: this is the price of a gate that does not lie.
293
+ - otherwise (uncommitted changes, untracked files, or another commit checked out: a wave's lots may be working in it): a throwaway worktree of the sha under the OS temporary folder (`cadence-verify-…`), removed whatever the result, where the **dependencies of the main tree are copied** (`node_modules` of every level and `.venv`; not linked: a copied `node_modules` is a real one for the tools that refuse a link; a `node_modules` that is itself a link is copied by its content; workspace links, relative, are copied as they are and point inside the copy; those that leave the repository, `file:../neighbour`, are redone towards their live target) and the other ignored folders (data, `.env`) are **neither copied nor linked** (L455: a link gave a test that compares paths by `realpath` the main tree back, and turned it red in the copy only; on 28-10, jobmail's `data` weighed 14 GB, maritime's `docs/refdocs` 961 MB). The copy is asynchronous: Ctrl-C received during it is handled at once. The line names the worktree, why the main tree was not used, and the copied dependencies (`… porte jouée dans une copie jetable /tmp/cadence-verify-x (l'arbre principal a des modifications non validées ou des fichiers non suivis) ; dépendances copiées : node_modules`), always **first**: the notice of declared dependencies absent from the main tree (`orchestrate.worktreeDeps`) comes after it. The copy of a large `node_modules` takes time: this is the price of a gate that does not lie.
294
294
  - **The main tree is read again at the end of an in-place gate (L389, L393).** A wave integrates its lots into it by fast-forward and commits the plan there, possibly while the gate runs. After the steps, `HEAD` and `git status --porcelain` are read again: if `HEAD` left the sha or the status is no longer empty, the output says what changed (`HEAD est passé de 6b0d9aa à 1c2d3e4`, `git status n'est plus vide (2 entrée(s))`), adds `verdict non probant … les ✓/✗ ci-dessus ne sont pas ceux de <sha>` and the exit code is 2 whatever the result, red included. The usual `vert` / `rouge` line is then **not written**: the verdict is not the sha's, replay it.
295
295
 
296
296
  **A gate the wave already played is read, not replayed (L397).** A lot that turns ready has just been through the program's gate (`orchestrate.build`, then `orchestrate.test`, at the commit it integrates), and the wave keeps the result in its state (`.cadence/runs/<wave>/<lot>.json`, key `checks`: the commit, its git tree, each command and its code). Replaying the same build and suite on the same commit costs ~5 minutes and says nothing new (L390, 28-10). `cadence verify --gate` therefore looks in the current folder, the repository root and its parent for a wave state of this repository (most recent first) and **reads the verdict** when all of this holds:
@@ -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:
@@ -319,7 +321,7 @@ Absent, `verify.inPlace` chooses as above, with one more case for the copy (L431
319
321
 
320
322
  - `orchestrate.worktreeDeps: [node_modules, .venv, front/node_modules]` in `cadence.yaml`: those paths, relative to the repository
321
323
  (no absolute path, no `..`); one that does not exist in the main tree is reported and skipped;
322
- - absent: the `node_modules` of every level and the `.venv` are copied; the other ignored folders (data, `.env`…, build outputs such as `dist` excluded) are linked, as in the worktrees of a wave. A bare worktree has no `node_modules` or `.venv`: on 26-10 the re-verification of a lot in one gave nine red suites that were only missing dependencies.
324
+ - absent: the `node_modules` of every level and the `.venv` are copied; the other ignored folders (data, `.env`…, build outputs such as `dist`) are neither copied nor linked. A bare worktree has no `node_modules` or `.venv`: on 26-10 the re-verification of a lot in one gave nine red suites that were only missing dependencies.
323
325
 
324
326
  ```
325
327
  verify --gate 6b0d9aa : porte jouée dans l'arbre principal /path/app (propre, au sha)
@@ -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`.
@@ -869,7 +871,8 @@ orchestrate:
869
871
  test: npm test # run by the orchestrator after a work step (optional)
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
- 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)
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 neither copied nor linked (L455) (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
@@ -987,3 +990,39 @@ it is cut. A cut write step is no longer a bare « failure »: the table gives t
987
990
  (with those that are tests, and the lines added) and says the work stays in the repository. A cut that left commits or files
988
991
  hands the lot back to the lead; a cut that left nothing is a failure. For a lot with neighbouring repositories (`repos:`, L303),
989
992
  the report counts the commits and the uncommitted files of each neighbour too, prefixed with its name (`../gitops: chart.yaml`).
993
+
994
+ ## A session never commits on another repository (L456)
995
+
996
+ A writing session (`spec`, `accept`, `implement`, `fix`) works in its lot's repository and in the neighbours the lot declares with `repos:`. It **never commits on the
997
+ main branch of any other repository**, whether the lot's text cites it or not: on 2026-10-30 the first lot of a cluster project put 3 commits on
998
+ `developpeur-gitops` `main`, one `deploy.sh` away from production. The brief of every writing session says so and gives the way: create the branch
999
+ `wip/<project>-<lot>` in that repository, commit on it, never push, and say it under « choix ».
1000
+
1001
+ **What the program checks.** Before and after each writing session it reads, with no network (one `git for-each-ref` per repository, no `ls-remote`, no `fetch`),
1002
+ the sha of the main branches of the other repositories. The main branches of a repository are, among its local branches, the one `origin/HEAD` names, `main` and
1003
+ `master`; one that moves is enough. When one moved, the lot is handed back to the lead (`handed-back`, the step `failed`) before the checks and the next lot, with
1004
+ a finding: `incident : commit sur la branche principale d'un autre dépôt (<repository>, <branch>) pendant <project>:<lot> (<step>) : <short sha> <subject> ; la voie
1005
+ est la branche wip/<project>-<lot>`. The repository is marked dirty for the wave. A branch created or deleted during the session counts as a moved sha. The comparison
1006
+ is on the branch, not on `HEAD`: a commit made then left with `HEAD` elsewhere is found too. The program **does not undo the commit** (no reset, no revert, no move of
1007
+ the branch): it states and hands back, the lead decides. The check also runs when the session ends in failure, at the time limit or at the quota, not only on a
1008
+ successful one: otherwise the next attempt would take the moved sha as its reference and the commit would never be named.
1009
+
1010
+ **What is watched.** The repositories that are a **direct child** of the folder that holds the lot's repository (the parent of the repository's path, not of a worktree).
1011
+ That makes every sibling repository, cited by the lot or not.
1012
+
1013
+ **What is not watched.**
1014
+
1015
+ - the lot's own repository, and the neighbours declared by `repos:`: they keep their behaviour (the session commits on their current branch, a moved `HEAD` is put back
1016
+ with a warning, `raf commits` and the review read them as before);
1017
+ - the repositories of the **other lots of the same wave**, and their neighbours, and those of the lots of another wave **running at the same time** from the same launch folder
1018
+ (its state says `running` and its process lives): their main branch moves legitimately through their own sessions and their integration;
1019
+ - the commits of the **lead** on a main branch: a commit whose subject starts with `plan: ` and that changes only the plan file (same relative path as the lot's) is left
1020
+ out of the finding; any other commit is found, including a `plan: ` commit that touches another file (a GitOps delivery that changes a `values.yaml` is found);
1021
+ - a repository two levels below the folder (`folder/group/repository`), a child that is not a git repository, and a **linked worktree** (a child whose `.git` is a file);
1022
+ - a symbolic link to a repository already counted (each repository is counted once, under its real path), and the folder itself when it is a git repository;
1023
+ - a repository whose references cannot be read, or that has no main branch before and after the session: it is skipped with a line of the wave's log
1024
+ (`dépôt tiers <repository> ignoré : lecture impossible (<git cause>)`), never an incident.
1025
+
1026
+ A commit on the `wip/<project>-<lot>` branch of another repository, with its main branch unmoved, is not an incident. If the session leaves that repository's `HEAD` on
1027
+ that branch, the lot gets a warning (`<step> : la session a laissé le dépôt tiers <repository> sur la branche wip/<project>-<lot> ; elle n'est pas remise`) and the
1028
+ branch is not put back. A read-only session (review, UX) is not concerned: any change to a watched repository stays an incident of the step, as before.
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