@sylad/cadence 1.14.1 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "cadence",
10
10
  "description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
11
- "version": "1.14.1",
11
+ "version": "1.15.1",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
3
  "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
- "version": "1.14.1",
4
+ "version": "1.15.1",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -17,6 +17,12 @@ raf add "My first lot" --estimate 1
17
17
 
18
18
  Then `raf start L1`, commit with `feat(L1): …`, `raf now` to see where you are, `raf done L1` when it is finished.
19
19
 
20
+ ## Lead several projects in 10 minutes
21
+
22
+ A parent folder, a plan in each project, a `/cadence:lead` session that picks the lots, a wave that does the work.
23
+ [docs/quickstart.md](docs/quickstart.md) goes from install to the first wave, and lists the usual stops.
24
+ Something broken or missing? [Open an issue](https://github.com/Sylad/cadence/issues/new/choose).
25
+
20
26
  <picture>
21
27
  <source media="(prefers-color-scheme: dark)" srcset="docs/cycle-vague-dark.svg">
22
28
  <img alt="A lot goes through spec, mockup, accept, implement, review, ux and deliver; the lead picks the lots, answers the questions, re-verifies and delivers" src="docs/cycle-vague-light.svg">
package/dist/audit.js CHANGED
@@ -149,6 +149,9 @@ export function audit(plan, root, newsDir, today, opts = {}) {
149
149
  const linked = exemptPlanOnly(linkCommits(lots, planCommits(plan, root, { since: auditSince(plan, opts.since) }), plan.refs), plan, root);
150
150
  // L'inactivité se mesure sur tout l'historique, pas seulement la fenêtre --since.
151
151
  const all = linkCommits(lots, planCommits(plan, root), plan.refs);
152
+ // Un commit acquitté (`raf ignore`) qui cite des lots connus ne compte pour aucun : ni commencé, ni actif, ni à relire.
153
+ for (const [id, cs] of all.byLot)
154
+ all.byLot.set(id, cs.filter((c) => !isAcknowledged(plan, c)));
152
155
  // Planifier un lot (commit qui ne touche que le plan) n'est pas y travailler : un lot « todo »
153
156
  // cité seulement par de tels commits n'a pas à être démarré. Ni par des commits antérieurs à
154
157
  // l'adoption du plan : citer un identifiant n'engageait alors à rien.
@@ -201,10 +204,10 @@ export function docSyncIssues(plan, root, byLot) {
201
204
  .filter((l) => l.status === 'doing' || (l.status === 'done' && !!since && !!l.finished && l.finished >= since))
202
205
  .flatMap((l) => docSyncGaps(sync, filesOf(root, workCommits(plan, root, byLot.get(l.id) ?? []))).map((g) => ({ message: gapMessage(l.id, g) })));
203
206
  }
204
- /** Commits qui portent du travail sur un lot : ni antérieurs à l'adoption du plan, ni réduits au plan, ni une simple version. */
207
+ /** Commits qui portent du travail sur un lot : ni antérieurs à l'adoption du plan, ni acquittés (`raf ignore`), ni réduits au plan, ni une simple version. */
205
208
  function workCommits(plan, root, commits) {
206
209
  const adopted = plan.since;
207
- return commits.filter((c) => (!adopted || c.day >= adopted) && !isPlanOnly(c.sha, plan, root) && !isReleaseOnly(c.sha, root));
210
+ return commits.filter((c) => (!adopted || c.day >= adopted) && !isAcknowledged(plan, c) && !isPlanOnly(c.sha, plan, root) && !isReleaseOnly(c.sha, root));
208
211
  }
209
212
  /** Commits liés à un lot, du plus récent au plus ancien, avant tout tri : commits de plan et d'avant l'adoption compris. */
210
213
  export function lotCommits(plan, root, lotId) {
package/dist/cli.js CHANGED
@@ -61,7 +61,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
61
61
  raf now ce qui est en cours, la suite, les derniers terminés
62
62
  raf list [--status todo|doing|done|dropped]
63
63
  raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
64
- raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot (ou citant un id inconnu) sans réécrire l'historique ; raf check --ignored les liste
64
+ raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot, citant un id inconnu ou des lots sans y travailler, sans réécrire l'historique ; raf check --ignored les liste
65
65
  raf docs-ok <lot> --reason texte acquitte l'écart docs.sync d'un lot (documentation en retard) avec la raison pour laquelle la documentation reste telle quelle ; lu par raf check et par orchestrate, qui ne rend jamais prêt un lot avec un écart ouvert
66
66
  raf gantt [-o .cadence/gantt.html]
67
67
  cadence config set <clé> <valeur> --all [--dry-run] [--depth n] pose la même clé dans le cadence.yaml de chaque projet du dossier (ceux de lead tour), commentaires et ordre gardés, un lot de config qui la cite (raf add + start) et un commit à chemin explicite par projet ; ne pousse pas ; liste ce qui est fait et sauté (dépôt sale, déjà posée, plan en lecture seule) ; s'arrête sur une config qui ne se relit pas
@@ -448,6 +448,9 @@ function dispatch(argv, io) {
448
448
  if (lot.visible && !loadEntries(newsDir).some((e) => e.lots.includes(lot.id))) {
449
449
  io.out(`${lot.id} est visible : écrire l'entrée : cadence news new ${lot.id}`);
450
450
  }
451
+ const uncommitted = uncommittedPlanCommand(plan, root, lot.id, planConfig?.commit);
452
+ if (uncommitted)
453
+ io.out(uncommitted);
451
454
  }
452
455
  return 0;
453
456
  }
@@ -810,6 +813,23 @@ function knownLot(plan, id, subtask) {
810
813
  throw new RafError(id.includes('/') ? `${subtask} : ${id}` : `lot inconnu : ${id}`);
811
814
  return lot;
812
815
  }
816
+ /**
817
+ * `raf done` écrit le plan et ne le commite jamais (voulu : raf n'a pas de réglage qui commite, docs/raf.md) : la commande de commit du plan et des fichiers
818
+ * que `plan.onWrite` produit, au gabarit `plan.commit` du projet (L371). Null hors d'un dépôt git ou quand rien n'est à commiter.
819
+ */
820
+ function uncommittedPlanCommand(plan, root, lot, template) {
821
+ const repo = gitRoot(root);
822
+ if (!repo)
823
+ return null;
824
+ // Seuls les chemins que git voit modifiés : `git add` d'un chemin absent ou ignoré est fatal.
825
+ const paths = dirtyPaths(repo, [relative(repo, plan.path), ...plan.files, ...plan.produced]);
826
+ if (paths.length === 0)
827
+ return null;
828
+ const quote = (t) => (/^[\w./-]+$/.test(t) ? t : `'${t.replaceAll("'", "'\\''")}'`);
829
+ const message = planCommitMessage(template, { lot, action: PLAN_ACTIONS.done });
830
+ const list = paths.map(quote).join(' ');
831
+ return `plan non commité : raf ne commite jamais (voulu, ce n'est pas un réglage), à commiter :\n git add -- ${list} && git commit -m ${quote(message)} -- ${list}`;
832
+ }
813
833
  function describe(l, commits) {
814
834
  const bits = [`${l.estimate} j`];
815
835
  if (commits)
@@ -47,6 +47,8 @@ function onlyAdds(base, side) {
47
47
  i++;
48
48
  return i === base.length;
49
49
  }
50
+ /** `text` porte l'entrée `e` entière, sur ses propres lignes : « - Fix typo » n'est pas dans « - Fix typo in README ». */
51
+ const hasEntry = (text, e) => `\n${text}\n`.includes(`\n${e}\n`);
50
52
  /** Un côté qui n'a pas bougé laisse l'autre gagner ; deux côtés qui ont bougé de la même façon aussi ; sinon, pas de réponse. */
51
53
  function pick(base, ours, theirs) {
52
54
  if (ours === theirs)
@@ -78,22 +80,27 @@ export function mergeUnreleased(base, ours, theirs, start) {
78
80
  const st = start === undefined ? null : split(start);
79
81
  const startEntries = new Set([...(st ? (sections(st.block)?.values() ?? []) : [])].flat());
80
82
  const own = new Set(start === undefined || !st ? [] : [...bs.values()].flat().filter((e) => !startEntries.has(e)));
83
+ // Une entrée de base que ours range sous une version publiée (la branche a été publiée pendant la vie du lot) n'a pas été retirée : elle a changé de place.
84
+ // Seule une entrée de base compte, entière : une entrée nouvelle de theirs qui est un bout d'une entrée publiée n'est pas la même, elle reste au lot.
85
+ // Les entrées du lot (`own`) ne comptent pas dans l'ordre de ours : l'union d'un commit précédent les y a recasées en fin de section.
86
+ const baseEntries = new Set([...bs.values()].flat());
87
+ const released = (e) => baseEntries.has(e) && o.tail !== b.tail && hasEntry(o.tail, e) && !hasEntry(b.tail, e);
81
88
  for (const [title, entries] of bs) {
82
89
  const kept = entries.filter((e) => !own.has(e));
83
- if (!onlyAdds(kept, (os.get(title) ?? []).filter((e) => !own.has(e))) || !onlyAdds(entries, os.get(title) ?? []) || !onlyAdds(kept, ts.get(title) ?? [])) {
90
+ if (!onlyAdds(kept.filter((e) => !released(e)), (os.get(title) ?? []).filter((e) => !own.has(e))) || !onlyAdds(kept, ts.get(title) ?? [])) {
84
91
  return null;
85
92
  }
86
93
  }
87
94
  const titles = [...new Set([...os.keys(), ...ts.keys()])];
88
95
  titles.sort((x, y) => rank(x) - rank(y));
89
- const body = titles.map((title) => {
90
- const entries = [...new Set([...(os.get(title) ?? []).filter((e) => !own.has(e)), ...(ts.get(title) ?? [])])];
91
- return `### ${title}\n\n${entries.join('\n')}\n`;
96
+ const body = titles.flatMap((title) => {
97
+ const entries = [...new Set([...(os.get(title) ?? []).filter((e) => !own.has(e)), ...(ts.get(title) ?? []).filter((e) => !released(e))])];
98
+ return entries.length ? [`### ${title}\n\n${entries.join('\n')}\n`] : [];
92
99
  });
93
100
  return `${head}\n\n${body.join('\n')}\n${tail}`;
94
101
  }
95
102
  /** Entrées du bloc `[Unreleased]` de `text`, par section ; null sans bloc ou avec du texte hors entrée. */
96
- function unreleasedEntries(text) {
103
+ export function unreleasedSections(text) {
97
104
  const s = split(text);
98
105
  return s ? sections(s.block) : null;
99
106
  }
@@ -105,7 +112,7 @@ function unreleasedEntries(text) {
105
112
  */
106
113
  export function moveToUnreleased(rebased, lotTip, start, main) {
107
114
  const [r, mainSplit] = [split(rebased), split(main)];
108
- const [tip, was] = [unreleasedEntries(lotTip), unreleasedEntries(start)];
115
+ const [tip, was] = [unreleasedSections(lotTip), unreleasedSections(start)];
109
116
  const [now, published] = [sections(r?.block ?? []), mainSplit ? mainSplit.tail : ''];
110
117
  if (!r || !tip || !was || !now)
111
118
  return null;
@@ -115,9 +122,9 @@ export function moveToUnreleased(rebased, lotTip, start, main) {
115
122
  const added = new Map();
116
123
  for (const [title, entries] of tip) {
117
124
  for (const e of entries) {
118
- if (known.has(e) || published.includes(e) || (now.get(title) ?? []).includes(e) || !tail.includes(e))
125
+ if (known.has(e) || hasEntry(published, e) || (now.get(title) ?? []).includes(e) || !hasEntry(tail, e))
119
126
  continue;
120
- tail = tail.replace(`${e}\n`, '');
127
+ tail = tail.replace(`\n${e}\n`, '\n');
121
128
  added.set(title, [...(added.get(title) ?? []), e]);
122
129
  moved++;
123
130
  }
@@ -131,3 +138,50 @@ export function moveToUnreleased(rebased, lotTip, start, main) {
131
138
  const body = titles.map((title) => `### ${title}\n\n${(now.get(title) ?? []).join('\n')}\n`);
132
139
  return { text: `${r.head}\n\n${body.join('\n')}\n${tail}`, moved };
133
140
  }
141
+ /**
142
+ * Une entrée finale par lot (L369) : après le rebase, le `[Unreleased]` ne porte du lot que ses entrées du sommet de sa branche. Les `intermediates` — les
143
+ * entrées que ses commits successifs ont écrites puis réécrites, absentes du sommet comme de ses versions publiées — en sont retirées (sous `[Unreleased]` comme sous une version), à moins
144
+ * que `main` ne les porte aussi : elles sont alors à elle. Une entrée du sommet absente du texte (ni sous `[Unreleased]`, ni sous une version, ni déjà publiée
145
+ * sur `main`) y est remise sous sa section. `lotTip`, `start`, `main` : comme pour `moveToUnreleased`. Rend le texte corrigé, ou null quand il n'y a rien à
146
+ * changer (ou que le fichier n'a pas la forme attendue).
147
+ */
148
+ export function settleLotEntries(rebased, lotTip, start, main, intermediates) {
149
+ const [r, mainSplit] = [split(rebased), split(main)];
150
+ const [tip, was] = [unreleasedSections(lotTip), unreleasedSections(start)];
151
+ const now = sections(r?.block ?? []);
152
+ if (!r || !mainSplit || !tip || !was || !now)
153
+ return null;
154
+ const mainOwn = new Set([...(sections(mainSplit.block)?.values() ?? [])].flat());
155
+ const known = new Set([...was.values()].flat());
156
+ const final = new Set([...tip.values()].flat());
157
+ // Une entrée que le sommet du lot porte sous une version (le lot l'a publiée lui-même) est finale, pas intermédiaire.
158
+ const tipTail = split(lotTip)?.tail ?? '';
159
+ const stale = intermediates.filter((e) => !final.has(e) && !hasEntry(tipTail, e) && !mainOwn.has(e) && !hasEntry(mainSplit.tail, e));
160
+ let changed = false;
161
+ for (const [title, entries] of now) {
162
+ const kept = entries.filter((e) => !stale.includes(e));
163
+ if (kept.length !== entries.length)
164
+ changed = true;
165
+ now.set(title, kept);
166
+ }
167
+ let tail = r.tail;
168
+ for (const e of stale) {
169
+ if (hasEntry(tail, e)) {
170
+ tail = tail.replace(`\n${e}\n`, '\n');
171
+ changed = true;
172
+ }
173
+ }
174
+ const present = (e) => [...now.values()].some((v) => v.includes(e)) || hasEntry(tail, e) || hasEntry(mainSplit.tail, e);
175
+ for (const [title, entries] of tip) {
176
+ for (const e of entries.filter((x) => !known.has(x) && !present(x))) {
177
+ now.set(title, [...(now.get(title) ?? []), e]);
178
+ changed = true;
179
+ }
180
+ }
181
+ if (!changed)
182
+ return null;
183
+ tail = tail.replace(/^### .+\n\n(\n|$)/gm, '');
184
+ const titles = [...now.keys()].filter((t) => (now.get(t) ?? []).length > 0).sort((x, y) => rank(x) - rank(y));
185
+ const body = titles.map((title) => `### ${title}\n\n${(now.get(title) ?? []).join('\n')}\n`);
186
+ return `${r.head}\n\n${body.join('\n')}\n${tail}`;
187
+ }
@@ -26,7 +26,7 @@ import { maquetteText } from './maquette.js';
26
26
  import { costlyHypotheses, specScreen, specText } from './spec.js';
27
27
  import { fichesDuDossier } from '../spec.js';
28
28
  import { candidates, parsePriority, parseUntil, readPriority, stopReason } from './continue.js';
29
- import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
29
+ import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, leadReviewedHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
30
30
  import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
31
31
  import { buildArgs, buildLocalArgs, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude } from './launch.js';
32
32
  import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
@@ -1432,6 +1432,8 @@ async function resume(args, argv, io, deps, launch, today) {
1432
1432
  }
1433
1433
  // --resume --integrate projet:lot (L279, R27) : un lot rendu à l'étape d'intégration (next: integrate) n'est rejoué que si le lead le nomme. Ni --resume seul, ni --drop, --answer ou --budget ne le rejouent.
1434
1434
  const integ = [];
1435
+ // L370 : un lot rendu hors de l'intégration que le lead a fait corriger puis revoir hors vague (revue conforme au dernier commit du worktree) est repris à l'intégration avec ce verdict.
1436
+ const leadReviewed = new Map();
1435
1437
  const budgeted = args.lotBudgets.map((b) => lots.filter((x) => x.lot === b.lot && (b.project ? x.project === b.project : true))).filter((m) => m.length === 1).map((m) => m[0]);
1436
1438
  for (const t of args.integrate) {
1437
1439
  const matches = lots.filter((x) => x.lot === t.lot && (t.project ? x.project === t.project : true));
@@ -1451,6 +1453,15 @@ async function resume(args, argv, io, deps, launch, today) {
1451
1453
  refusals.push(`${opt} ${key} : lot aussi visé par --answer`);
1452
1454
  else if (budgeted.includes(l))
1453
1455
  refusals.push(`${opt} ${key} : lot aussi visé par --budget`);
1456
+ else if (!t.replay && l.next !== 'integrate' && (() => {
1457
+ // La revue du lead est écrite par `raf review` lancé dans le worktree : c'est le plan du worktree qui la porte, pas celui du dépôt principal.
1458
+ const tree = lotTreeDir(store.dir, l.project, l.lot, l.repo);
1459
+ const r = existsSync(join(tree, '.git')) ? leadReviewedHandBack(l, planLoader(tree)(), tree) : null;
1460
+ if (r)
1461
+ leadReviewed.set(l, r);
1462
+ return r;
1463
+ })())
1464
+ integ.push(l);
1454
1465
  else if (l.status !== 'handed-back' || (l.next !== 'integrate' && !(t.replay && planNoteHandBack(l))))
1455
1466
  refusals.push(`${opt} ${key} : le lot n'est pas rendu ${t.replay ? "à l'étape d'intégration ni sur une écriture de plan refusée" : "à l'étape d'intégration"} (${l.status}, étape ${l.next ?? 'aucune'} : ${l.outcome ?? 'sans cause'})`);
1456
1467
  else
@@ -1545,12 +1556,20 @@ async function resume(args, argv, io, deps, launch, today) {
1545
1556
  }
1546
1557
  for (const l of integ) {
1547
1558
  const k = lotKey(l.project, l.lot);
1559
+ const lead = leadReviewed.get(l);
1560
+ if (lead) {
1561
+ // La revue du lead tient lieu de revue du lot : le rebase reporte son verdict (même patch-id) ou joue la revue courte du diff rebasé.
1562
+ l.next = 'integrate';
1563
+ l.code = { conforme: true, bloquants: 0, majeurs: 0, mineurs: 0, verdict: lead.verdict, sousTaches: [], nonVerifie: [], head: lead.head };
1564
+ l.verdict = lead.verdict;
1565
+ l.constats = [];
1566
+ }
1548
1567
  const planNote = l.next !== 'integrate'; // rendu sur une écriture de plan refusée (--replay) : le lot repart de l'étape gardée
1549
1568
  if (planNote && l.outcome?.startsWith(HYPOTHESES_HANDBACK))
1550
1569
  l.hypotheses = undefined; // les notes d'hypothèses se posent de nouveau (sans doublon) à l'entrée de l'étape
1551
1570
  l.status = 'suspended';
1552
1571
  l.outcome = null;
1553
- const text = planNote ? `écriture du plan reprise à l'étape ${l.next} (--resume --replay)` : 'intégration reprise (--resume --integrate)';
1572
+ const text = lead ? `intégration reprise (--resume --integrate) : revue conforme du lead hors vague, ${lead.head.slice(0, 7)}` : planNote ? `écriture du plan reprise à l'étape ${l.next} (--resume --replay)` : 'intégration reprise (--resume --integrate)';
1554
1573
  store.journal(`${k} → suspended — ${text}`);
1555
1574
  io.out(`${k} → suspended — ${text}`);
1556
1575
  }
@@ -2688,6 +2688,17 @@ const MINORS_HANDBACK = 'notes des mineurs de la revue de ';
2688
2688
  * (décision du lead du 24-10). Aucun champ d'état : la cause rendue le désigne.
2689
2689
  */
2690
2690
  export const planNoteHandBack = (l) => l.status === 'handed-back' && l.next !== null && l.next !== undefined && !!l.outcome && (l.outcome.startsWith(HYPOTHESES_HANDBACK) || l.outcome.startsWith(MINORS_HANDBACK));
2691
+ /**
2692
+ * Un lot rendu hors de l'étape d'intégration (budget, revue non conforme…) que le lead a fait corriger puis revoir hors vague (L370) : sa revue de code du plan
2693
+ * porte le dernier commit de travail du lot dans son worktree, donc rien n'a été commité après elle. Rend le sha relu et le verdict du lead, ou null.
2694
+ */
2695
+ export function leadReviewedHandBack(l, plan, treeDir) {
2696
+ if (l.status !== 'handed-back' || l.next === 'integrate' || planNoteHandBack(l) || !existsSync(join(treeDir, '.git')))
2697
+ return null;
2698
+ const review = plan.lots().find((x) => x.id === l.lot)?.review;
2699
+ const tip = lotWork(plan, treeDir, l.lot)[0]?.sha;
2700
+ return review?.commit && tip && review.commit === tip ? { head: tip, verdict: review.verdict } : null;
2701
+ }
2691
2702
  /** Une écriture de plan refusée rend le lot au lead ; en worktree, il garde l'étape atteinte pour la reprise (R26), sinon `stop` la vide comme avant. */
2692
2703
  function handBackPlan(c, why) {
2693
2704
  if (c.tree)
@@ -2750,6 +2761,8 @@ async function integrateLot(c) {
2750
2761
  c.wave.log(`${key} CHANGELOG.md : conflit d'ajout pur résolu par union sous [Unreleased] (${rebased.changelog} commit(s))`);
2751
2762
  if (rebased.kind === 'rebased' && rebased.moved > 0)
2752
2763
  c.wave.log(`${key} CHANGELOG.md : ${rebased.moved} entrée(s) du lot replacée(s) sous [Unreleased], une publication a eu lieu pendant la branche`);
2764
+ if (rebased.kind === 'rebased' && rebased.settled)
2765
+ c.wave.log(`${key} CHANGELOG.md : une entrée finale par lot, les versions intermédiaires du lot écartées`);
2753
2766
  const tip = git(t.dir, 'rev-parse', 'HEAD');
2754
2767
  if (tip !== reviewed) {
2755
2768
  // Le rebase a changé les commits : tests et build rejoués au sommet rebasé, avant que la branche du dépôt ne bouge (R7).
@@ -4,8 +4,10 @@ import { join } from 'node:path';
4
4
  import { gitCommonDir } from '../git.js';
5
5
  import { sharedStateDir } from '../state.js';
6
6
  import { activeLock, releaseLock, takeLock } from './lock.js';
7
- import { mergeUnreleased, moveToUnreleased } from './changelog-merge.js';
7
+ import { mergeUnreleased, moveToUnreleased, settleLotEntries, unreleasedSections } from './changelog-merge.js';
8
8
  import { mutexFor } from './mutex.js';
9
+ import { Plan } from '../plan.js';
10
+ import { parseDocument } from 'yaml';
9
11
  import { git, gitFailure, tryGit } from './worktree.js';
10
12
  /** Verrou d'intégration d'un dépôt (L279, R10) : dans l'état commun aux worktrees, au format du verrou d'orchestration, sous un autre nom. */
11
13
  export const INTEGRATION_LOCK = 'orchestrate-integration.lock';
@@ -79,6 +81,48 @@ function resolveChangelog(work, start) {
79
81
  return false;
80
82
  }
81
83
  }
84
+ /**
85
+ * Résout le conflit en cours quand c'est le plan seul et que le commit du lot rejoué n'y ajoute que des revues de lot (L370 : le `raf review` du lead dans
86
+ * le worktree) : le plan de la branche du dépôt est gardé, et chaque revue ajoutée est réécrite dessus par l'API du plan. Faux, sans rien toucher, si le
87
+ * commit change autre chose que des revues — un autre champ d'un lot, un lot ajouté ou retiré, ou la tête du plan hors `lots` (`acknowledged:` après un
88
+ * `raf ignore`, `reviewSince`, un réglage) : repris tel quel, le plan du dépôt perdrait ce changement sans message —, ou si la branche du dépôt a elle-même
89
+ * changé une de ces revues.
90
+ */
91
+ function resolvePlan(work) {
92
+ const files = (tryGit(work, 'diff', '--name-only', '--diff-filter=U') ?? '').split('\n').filter(Boolean);
93
+ if (files.length !== 1)
94
+ return false;
95
+ const file = files[0];
96
+ try {
97
+ const stage = (n) => {
98
+ const text = run(work, 'show', `:${n}:${file}`);
99
+ writeFileSync(join(work, file), text);
100
+ const head = parseDocument(text).toJSON();
101
+ return { lots: Plan.load(join(work, file)).lots(), head: JSON.stringify({ ...(head ?? {}), lots: undefined }) };
102
+ };
103
+ const [b, o, t] = [stage(1), stage(2), stage(3)];
104
+ const [base, ours, theirs] = [b.lots, o.lots, t.lots];
105
+ if (t.head !== b.head)
106
+ return false;
107
+ const bare = (l) => JSON.stringify({ ...l, review: undefined });
108
+ const review = (l) => JSON.stringify(l?.review ?? null);
109
+ const added = theirs.filter((t) => review(t) !== review(base.find((b) => b.id === t.id)));
110
+ if (!added.length || theirs.length !== base.length || theirs.some((t) => bare(t) !== bare(base.find((b) => b.id === t.id) ?? { review: 1 })))
111
+ return false;
112
+ if (added.some((t) => review(ours.find((o) => o.id === t.id)) !== review(base.find((b) => b.id === t.id))))
113
+ return false;
114
+ writeFileSync(join(work, file), run(work, 'show', `:2:${file}`));
115
+ const plan = Plan.load(join(work, file));
116
+ for (const t of added)
117
+ plan.recordReview(t.id, t.review.verdict, t.review.date, t.review.commit ?? null, t.review.repos, t.review.reporte);
118
+ plan.save();
119
+ run(work, 'add', '--', file);
120
+ return true;
121
+ }
122
+ catch {
123
+ return false; // le fichier écrasé est restauré par le `rebase --abort` de l'appelant
124
+ }
125
+ }
82
126
  /**
83
127
  * Après un rebase réussi : les entrées que le lot a ajoutées sous `[Unreleased]` et qu'une publication de la branche a laissées sous la version publiée
84
128
  * (git les y range sans conflit quand le bloc a été renommé) sont remises sous le `[Unreleased]` de la branche, dans un commit à part (L362). Rend leur nombre.
@@ -97,9 +141,33 @@ function moveOwnEntries(work, base, before, start) {
97
141
  run(work, 'commit', '--no-verify', '-q', '-m', 'docs: CHANGELOG — les entrées du lot replacées sous [Unreleased] (une publication a eu lieu pendant la branche)');
98
142
  return fixed.moved;
99
143
  }
144
+ /** Lignes d'entrée de `text` sous `[Unreleased]` (une entrée sur plusieurs lignes : le bloc entier), ou aucune sans bloc `[Unreleased]` lisible. */
145
+ const entriesOf = (text) => [...(unreleasedSections(text)?.values() ?? [])].flat();
146
+ /**
147
+ * Après un rebase réussi : une seule entrée par lot, celle du sommet de sa branche (L369). Les entrées que les commits successifs du lot ont écrites sous
148
+ * `[Unreleased]` puis réécrites (absentes du sommet et du point de départ) sont retirées du résultat, celles du sommet qui y manquent remises, dans un commit
149
+ * à part. Rend vrai quand ce commit a eu lieu.
150
+ */
151
+ export function settleOwnEntries(work, base, before, start) {
152
+ if (start === undefined || !existsSync(join(work, CHANGELOG)))
153
+ return false;
154
+ const [tip, main] = [tryGit(work, 'show', `${before}:${CHANGELOG}`), tryGit(work, 'show', `refs/heads/${base}:${CHANGELOG}`)];
155
+ if (tip === null || main === null)
156
+ return false;
157
+ const known = new Set(entriesOf(start));
158
+ const commits = (tryGit(work, 'rev-list', `${mergeBase(work, before, base)}..${before}`) ?? '').split('\n').filter(Boolean);
159
+ const written = commits.flatMap((c) => entriesOf(tryGit(work, 'show', `${c}:${CHANGELOG}`) ?? '')).filter((e) => !known.has(e));
160
+ const fixed = settleLotEntries(readFileSync(join(work, CHANGELOG), 'utf8'), tip, start, main, [...new Set(written)]);
161
+ if (fixed === null)
162
+ return false;
163
+ writeFileSync(join(work, CHANGELOG), fixed);
164
+ run(work, 'add', '--', CHANGELOG);
165
+ run(work, 'commit', '--no-verify', '-q', '-m', 'docs: CHANGELOG — une entrée finale par lot, celle du sommet de sa branche (les versions intermédiaires écartées)');
166
+ return true;
167
+ }
100
168
  /**
101
169
  * Rebase le worktree `work` sur la branche `base` (R6). Une branche qui descend déjà du sommet de `base` n'a rien à rebaser (fast-forward pur : aucun sha ne
102
- * change). Un conflit d'ajout pur dans le CHANGELOG est résolu par union à chaque commit qui le porte (L314, `changelog` les compte). Tout autre conflit — ou
170
+ * change). Un conflit d'ajout pur dans le CHANGELOG est résolu par union à chaque commit qui le porte (L314, `changelog` les compte), un conflit du plan quand le commit n'y ajoute que des revues de lot (L370). Tout autre conflit — ou
103
171
  * tout autre échec du rebase — est abandonné (`rebase --abort`) : la branche du lot et son arbre restent tels qu'avant, la branche du dépôt n'a pas été
104
172
  * touchée, personne ne résout le conflit à l'aveugle.
105
173
  */
@@ -116,7 +184,7 @@ export function rebaseOnto(work, base) {
116
184
  let failure = first;
117
185
  // Chaque tour règle le commit sur lequel le rebase s'est arrêté ; un lot ne porte pas des centaines de commits, la borne évite seulement une boucle sans fin.
118
186
  for (let turn = 0;; turn++) {
119
- if (turn >= 200 || !resolveChangelog(work, start)) {
187
+ if (turn >= 200 || !(resolveChangelog(work, start) || resolvePlan(work))) {
120
188
  const files = (tryGit(work, 'diff', '--name-only', '--diff-filter=U') ?? '').split('\n').filter(Boolean);
121
189
  tryGit(work, 'rebase', '--abort');
122
190
  return { kind: 'conflict', files, detail: gitFailure(failure) };
@@ -132,7 +200,8 @@ export function rebaseOnto(work, base) {
132
200
  }
133
201
  }
134
202
  const moved = moveOwnEntries(work, base, before, start);
135
- return { kind: 'rebased', before, after: git(work, 'rev-parse', 'HEAD'), changelog, moved };
203
+ const settled = settleOwnEntries(work, base, before, start);
204
+ return { kind: 'rebased', before, after: git(work, 'rev-parse', 'HEAD'), changelog, moved, settled };
136
205
  }
137
206
  /**
138
207
  * Patch-id stable (`git patch-id --stable`) du diff cumulé de `from` à `to` (R9) : le même diff reste le même quand le contexte se déplace, il change quand
@@ -16,6 +16,8 @@ export const PLAN_ACTIONS = {
16
16
  started: 'démarré',
17
17
  hypotheses: (sujet) => `hypothèses de la fiche ${sujet} notées`,
18
18
  minors: (label) => `mineurs de la revue de ${label} notés`,
19
+ /** `raf done` ne commite pas : l'action de la commande de commit qu'il propose (L371). */
20
+ done: 'terminé',
19
21
  reviewRecorded: 'revue de code enregistrée',
20
22
  reviewNoted: 'revue de code notée',
21
23
  /** `spec green` (L366) : jamais la liste des tests rescellés, elle va dans le corps du commit (header-max-length de commitlint). */
package/docs/lead.md CHANGED
@@ -90,6 +90,7 @@ picture, not its source.
90
90
  | The UX verdict is the human's unless `lead.ux: lead` (parent `cadence.yaml`, default `human`) | then the lead decides from the report and the captures, and brings any doubt to the human |
91
91
  | A session cannot ask the human anything | it stops and reports; the lead brings the question back |
92
92
  | Never push or deliver HEAD above a handed-back lot | HEAD holds code that was not reviewed |
93
+ | A lot handed back for its budget or a non-compliant review that the lead had corrected outside the wave is integrated by `cadence orchestrate --resume --wave <wave> --integrate <project>:<lot>` (L370, [`--integrate`](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279)): the fix is made in the kept worktree, then `raf review <lot> "conforme : …"` is run in that worktree and its plan committed there, after the last work commit of the lot; the program rebases, replays the checks, keeps the verdict or makes a short review, and advances the branch | never by hand (L367, 26-10), and never a review recorded in the main repository's plan: `--integrate` reads the worktree's plan and would keep refusing |
93
94
  | A plan kept by another tool is read-only for `raf` | its own commands record the lot's state |
94
95
 
95
96
  ## Exit codes
@@ -574,12 +574,19 @@ nothing below applies and the wave behaves as before; any other value is refused
574
574
  entry of the replayed commit's parent that was not in the `CHANGELOG.md` at the branch's starting point (the merge-base with the repository's
575
575
  branch) was added by an earlier commit of the same lot, so the replayed commit may reword or remove it — its version wins, the earlier one is dropped
576
576
  from the repository's side — while an entry that was already there at the starting point, reworded, stays a conflict by hand. The
577
- journal says `<project>:<lot> CHANGELOG.md : conflit d'ajout pur résolu par union sous [Unreleased] (<n> commit(s))`. Anything else — an entry that
578
- **A release during the branch's life** (L362): when the repository's branch was released after the lot's branch was cut, its `## [Unreleased]` became
577
+ journal says `<project>:<lot> CHANGELOG.md : conflit d'ajout pur résolu par union sous [Unreleased] (<n> commit(s))`. **A release during the branch's life** (L362):
578
+ when the repository's branch was released after the lot's branch was cut, its `## [Unreleased]` became
579
579
  `## [x.y.z]` and git, merging without conflict, files the lot's entries under that published version. After the rebase the program moves the entries of
580
580
  the lot (those in its `[Unreleased]` and not in the `CHANGELOG.md` of the starting point) that landed in the published part, and were not already
581
581
  there on the repository's branch, back under the repository's `## [Unreleased]`, in a commit of its own (`docs: CHANGELOG — les entrées du lot replacées
582
- sous [Unreleased] …`); the journal says `<project>:<lot> CHANGELOG.md : <n> entrée(s) du lot replacée(s) sous [Unreleased], …`.
582
+ sous [Unreleased] …`); the journal says `<project>:<lot> CHANGELOG.md : <n> entrée(s) du lot replacée(s) sous [Unreleased], …`. When the release and the
583
+ lot's addition touch the same lines, the conflict is resolved too (L369): an entry of the commit's parent that the repository's branch now keeps under a
584
+ published version was **moved by the release, not removed**, so it does not count as a removal and is not written again under `[Unreleased]`. Only an entry of the parent, whole, counts as moved: a new entry of the lot whose text is a fragment of a published one (« Fix typo » when the release carries « Fix typo in README ») is not that entry, it stays under `[Unreleased]`, and the same whole-entry match applies when the program looks for a lot's entry in the published part.
585
+ **One final entry per lot** (L369): the lot's commits may write then reword their entry several times (L360 on 2026-10-26: four contradictory entries
586
+ stacked). After the rebase the program lists the entries the lot's commits wrote under `[Unreleased]` that are neither at the starting point nor at
587
+ the lot's tip (nor under a version the tip itself carries: an entry the lot published itself is final), drops them from the result (under `[Unreleased]` and under a version), and puts back an entry of the tip that is missing, in a commit of
588
+ its own (`docs: CHANGELOG — une entrée finale par lot …`); an entry the repository's branch also carries is left alone. The union of an earlier replayed commit files the lot's entry at the end of its section; where the lot first put it (top of the section included) does not matter for the next replayed commit. The journal says
589
+ `<project>:<lot> CHANGELOG.md : une entrée finale par lot, les versions intermédiaires du lot écartées`. Anything else — an entry that
583
590
  was already there removed or reworded, free text under `[Unreleased]`, the rest of the
584
591
  file changed on both sides, another file in conflict with it — aborts the rebase and hands the lot back as above. The rebased commits differ, so
585
592
  steps 3 and 4 apply: tests replayed, and a short review since the patch-id changed;
@@ -615,7 +622,8 @@ nothing below applies and the wave behaves as before; any other value is refused
615
622
  fiche is replayed and counted once). `--replay` is refused for a lot handed back in any other way, with the same exit code and rules as `--integrate`. Refused (exit 2, before
616
623
  anything is written, like the other resume refusals): a lot unknown or named twice, also named by `--drop`, `--answer` or `--budget`, a lot that is not
617
624
  handed back at the integration step (`le lot n'est pas rendu à l'étape d'intégration (<status>, étape <step or aucune> : <cause>)`: a lot handed back for
618
- review, tests or budget has `next` empty and takes the recovery path below), and, like any resume, a repository with a modified tracked file.
625
+ review, tests or budget has `next` empty and takes the recovery path below, unless the lead had it corrected and reviewed off the wave: next paragraph), and, like any resume, a repository with a modified tracked file.
626
+ **A lot handed back for its budget or a non-compliant review, that the lead had corrected and reviewed outside the wave (L370)** is accepted by `--resume --wave <wave> --integrate <project:lot>` too: the lead fixed it in the kept worktree, then recorded a compliant review with `raf review <lot> "…"` run in that worktree and committed the plan there (the review is read from the worktree's plan, not from the main repository's), and the recorded `commit` of that review is the **last work commit of the lot in its worktree** (no commit after the review; a review that predates a commit, or none, keeps the refusal above). The lot restarts at the integration step (`<project>:<lot> → suspended — intégration reprise (--resume --integrate) : revue conforme du lead hors vague, <sha>`) with the lead's verdict in place of the wave's review: rebase on the repository's branch, checks replayed, the verdict carried over when the diff is the same (same patch-id) or a short review of the rebased diff (the plan commit made in the worktree is part of that diff, so a short review is the usual case; if the lead also wrote on that lot in the repository's plan meanwhile — `raf note`, `raf add --parent` — the plan conflict of the rebase is settled by the program, which keeps the repository's plan and writes the lead's review back on it, provided the commit only adds lot reviews — nothing else on a lot, no lot added or removed, and nothing outside `lots` such as an `acknowledged:` entry from `raf ignore` or a head setting, which a kept repository plan would silently lose — and the repository did not change that review itself; any other plan conflict hands the lot back as before), then the advance. Handed back for another cause (acceptance lock, `HEAD` moved…), a lot that meets the condition is accepted the same way; `--replay` never takes this path. The lead no longer integrates by hand for it.
619
627
  - **A lot handed back outside the integration step** (non-compliant review, red tests, `HEAD` moved, plan touched, acceptance lock, quota…) keeps its worktree and its
620
628
  branch `cadence/<wave>/<project>--<lot>` but is not replayed by `--resume`. Its recovery path (L301) is **a new wave of the same lot** (`cadence orchestrate <project>:<lot>`):
621
629
  with the worktree mode on (`orchestrate.worktrees: true`), the new wave looks for the **most recent** (creation time noted by cadence) recognised branch `cadence/<other wave>/<project>--<lot>`
package/docs/overview.md CHANGED
@@ -61,6 +61,7 @@ The two plugins are independent. `cadence@cadence` carries the rituals, which ca
61
61
 
62
62
  | Page | What it covers |
63
63
  |---|---|
64
+ | [docs/quickstart.md](quickstart.md) | getting started as a lead of several projects, in 10 minutes: install, the parent folder, `raf init`, the tour, the first wave |
64
65
  | [docs/raf.md](raf.md) | the plan: commands, plan file, foreign plan formats, Gantt |
65
66
  | [docs/orchestrate.md](orchestrate.md) | waves of sessions: budgets, spec, accept and review steps, neighbouring repositories, one worktree per lot (`orchestrate.worktrees: true`, on in cadence's own `cadence.yaml`, off by default elsewhere; `--integrate`, `--replay`, `--remove-worktree`; recovery of a handed-back lot's branch and the `CHANGELOG.md` union at the integration are described there) |
66
67
  | [docs/deliver.md](deliver.md) | delivery: wait for CI, deploy, verify the effect |
@@ -0,0 +1,33 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1010 190" width="1010" height="190" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#e6edf3">
2
+ <title>One lead, several projects, one wave</title>
3
+ <desc>A parent folder holds the projects alpha and beta, each with its own plan. The lead session reads their tour and you choose the lots. A wave of fresh sessions does the work, then you review and deliver.</desc>
4
+ <defs><marker id="arq" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#8b949e"/></marker></defs>
5
+ <rect width="1010" height="190" fill="#0d1117"/>
6
+ <text x="20" y="22" font-weight="600" font-size="13.5" fill="#e6edf3">1. Parent folder</text>
7
+ <rect x="20" y="32" width="220" height="140" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5" stroke-dasharray="5 4"/>
8
+ <text x="130" y="52" text-anchor="middle" font-size="11.5" fill="#8b949e">atelier/ (not a repository)</text>
9
+ <rect x="40" y="64" width="180" height="42" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
10
+ <text x="130" y="83" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">alpha</text>
11
+ <text x="130" y="98" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#8b949e">raf init · plan</text>
12
+ <rect x="40" y="116" width="180" height="42" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
13
+ <text x="130" y="135" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">beta</text>
14
+ <text x="130" y="150" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#8b949e">raf init · plan</text>
15
+ <text x="300" y="22" font-weight="600" font-size="13.5" fill="#e6edf3">2. The lead</text>
16
+ <rect x="300" y="64" width="210" height="94" rx="6" fill="#0c2d6b" stroke="#58a6ff" stroke-width="2"/>
17
+ <text x="405.0" y="96" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Lead session</text>
18
+ <text x="405.0" y="114" text-anchor="middle" font-size="11.5" fill="#c9d1d9">reads the tour, asks you</text>
19
+ <text x="405.0" y="132" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#c9d1d9">/cadence:lead</text>
20
+ <text x="570" y="22" font-weight="600" font-size="13.5" fill="#e6edf3">3. The wave</text>
21
+ <rect x="570" y="64" width="200" height="94" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
22
+ <text x="670.0" y="96" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Fresh sessions</text>
23
+ <text x="670.0" y="114" text-anchor="middle" font-size="11.5" fill="#8b949e">implement, then review</text>
24
+ <text x="670.0" y="132" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#8b949e">cadence orchestrate</text>
25
+ <text x="830" y="22" font-weight="600" font-size="13.5" fill="#e6edf3">4. You</text>
26
+ <rect x="830" y="64" width="160" height="94" rx="6" fill="#0d1117" stroke="#8b949e" stroke-width="1.5"/>
27
+ <text x="910.0" y="96" text-anchor="middle" font-weight="600" font-size="13.5" fill="#e6edf3">Review, deliver</text>
28
+ <text x="910.0" y="114" text-anchor="middle" font-size="11.5" fill="#8b949e">one project at a time</text>
29
+ <text x="910.0" y="132" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#8b949e">/cadence:deliver</text>
30
+ <polyline points="240,111 298,111" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arq)"/>
31
+ <polyline points="510,111 568,111" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arq)"/>
32
+ <polyline points="770,111 828,111" fill="none" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arq)"/>
33
+ </svg>
@@ -0,0 +1,33 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1010 190" width="1010" height="190" role="img" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif" font-size="13" fill="#1f2328">
2
+ <title>One lead, several projects, one wave</title>
3
+ <desc>A parent folder holds the projects alpha and beta, each with its own plan. The lead session reads their tour and you choose the lots. A wave of fresh sessions does the work, then you review and deliver.</desc>
4
+ <defs><marker id="arq" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#57606a"/></marker></defs>
5
+ <rect width="1010" height="190" fill="#ffffff"/>
6
+ <text x="20" y="22" font-weight="600" font-size="13.5" fill="#1f2328">1. Parent folder</text>
7
+ <rect x="20" y="32" width="220" height="140" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5" stroke-dasharray="5 4"/>
8
+ <text x="130" y="52" text-anchor="middle" font-size="11.5" fill="#57606a">atelier/ (not a repository)</text>
9
+ <rect x="40" y="64" width="180" height="42" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
10
+ <text x="130" y="83" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">alpha</text>
11
+ <text x="130" y="98" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">raf init · plan</text>
12
+ <rect x="40" y="116" width="180" height="42" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
13
+ <text x="130" y="135" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">beta</text>
14
+ <text x="130" y="150" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">raf init · plan</text>
15
+ <text x="300" y="22" font-weight="600" font-size="13.5" fill="#1f2328">2. The lead</text>
16
+ <rect x="300" y="64" width="210" height="94" rx="6" fill="#ddf4ff" stroke="#0969da" stroke-width="2"/>
17
+ <text x="405.0" y="96" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Lead session</text>
18
+ <text x="405.0" y="114" text-anchor="middle" font-size="11.5" fill="#57606a">reads the tour, asks you</text>
19
+ <text x="405.0" y="132" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">/cadence:lead</text>
20
+ <text x="570" y="22" font-weight="600" font-size="13.5" fill="#1f2328">3. The wave</text>
21
+ <rect x="570" y="64" width="200" height="94" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
22
+ <text x="670.0" y="96" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Fresh sessions</text>
23
+ <text x="670.0" y="114" text-anchor="middle" font-size="11.5" fill="#57606a">implement, then review</text>
24
+ <text x="670.0" y="132" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">cadence orchestrate</text>
25
+ <text x="830" y="22" font-weight="600" font-size="13.5" fill="#1f2328">4. You</text>
26
+ <rect x="830" y="64" width="160" height="94" rx="6" fill="#ffffff" stroke="#57606a" stroke-width="1.5"/>
27
+ <text x="910.0" y="96" text-anchor="middle" font-weight="600" font-size="13.5" fill="#1f2328">Review, deliver</text>
28
+ <text x="910.0" y="114" text-anchor="middle" font-size="11.5" fill="#57606a">one project at a time</text>
29
+ <text x="910.0" y="132" text-anchor="middle" font-size="11" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" fill="#57606a">/cadence:deliver</text>
30
+ <polyline points="240,111 298,111" fill="none" stroke="#57606a" stroke-width="1.5" marker-end="url(#arq)"/>
31
+ <polyline points="510,111 568,111" fill="none" stroke="#57606a" stroke-width="1.5" marker-end="url(#arq)"/>
32
+ <polyline points="770,111 828,111" fill="none" stroke="#57606a" stroke-width="1.5" marker-end="url(#arq)"/>
33
+ </svg>
@@ -0,0 +1,138 @@
1
+ [← cadence](../README.md)
2
+
3
+ # Start in 10 minutes: one lead, several projects
4
+
5
+ You keep a folder with a few small projects. A **lead** session looks at all of them, you choose what to
6
+ do, and a program runs each lot in fresh sessions. This page takes you from nothing to the first wave.
7
+
8
+ <picture>
9
+ <source media="(prefers-color-scheme: dark)" srcset="quickstart-dark.svg">
10
+ <img alt="A parent folder holds the projects alpha and beta; the lead session reads their tour and you choose; a wave of fresh sessions works; you review and deliver" src="quickstart-light.svg">
11
+ </picture>
12
+
13
+ The commands below were played on a clean Windows 11 machine with WSL (Ubuntu 24.04, L357): about 40 seconds of commands,
14
+ 29 of them for Node and Claude Code. The rest of the ten minutes is reading and typing.
15
+
16
+ ## Before you start
17
+
18
+ You need four things; the page says how to check each.
19
+
20
+ - **Node 22.18 or later** (`node --version`). If you have none: [nvm](https://github.com/nvm-sh/nvm), or the NodeSource
21
+ package of your distribution (`setup_22.x`). Node 20 is not enough.
22
+ - **git, with an identity.** Without one, the first `git commit` stops with `empty ident name`:
23
+
24
+ ```sh
25
+ git config --global user.name "Your Name"
26
+ git config --global user.email "you@example.com"
27
+ ```
28
+
29
+ - **Claude Code, installed and logged in.** `claude auth status` must print `"loggedIn": true`. A wave opens
30
+ `claude -p` sessions and stops at the first one with `Not logged in · Please run /login` otherwise.
31
+ Install it from [claude.com/claude-code](https://claude.com/claude-code), then run `claude` once and log in.
32
+ - **A test command** for each project (`npm test`, `pytest`, `go test ./...`). The wave runs it after every change.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ npm install -g @sylad/cadence
38
+ cadence --version
39
+ ```
40
+
41
+ If npm answers `EACCES: permission denied, mkdir '/usr/lib/node_modules/@sylad'`, your Node belongs to the system.
42
+ Give npm a folder of your own, then install again:
43
+
44
+ ```sh
45
+ npm config set prefix ~/.npm-global
46
+ export PATH="$HOME/.npm-global/bin:$PATH" # add this line to ~/.bashrc or ~/.zshrc too
47
+ npm install -g @sylad/cadence
48
+ ```
49
+
50
+ The skills and agents come as a Claude Code plugin:
51
+
52
+ ```sh
53
+ claude plugin marketplace add Sylad/cadence
54
+ claude plugin install cadence@cadence
55
+ ```
56
+
57
+ ## The parent folder
58
+
59
+ The lead works from a folder that holds the projects as sub-folders. The folder itself is not a repository.
60
+ Each project is one: give it a plan with `raf init`, a first lot, and the test command, in one commit.
61
+ Run the block once per project (here `alpha`, then `beta`):
62
+
63
+ ```sh
64
+ mkdir alpha && cd alpha
65
+ git init -b main
66
+ raf init
67
+ raf add "Create hello.txt containing hello" --estimate 0.25
68
+ cat > cadence.yaml <<'EOF'
69
+ orchestrate:
70
+ test: test -f hello.txt # passes once the first lot has created hello.txt
71
+ EOF
72
+ git add -A && git commit -m "chore: init raf"
73
+ raf ignore $(git rev-parse --short HEAD)
74
+ git add .cadence/raf.yaml && git commit -m "plan: acknowledge init"
75
+ cd ..
76
+ ```
77
+
78
+ What it does: `raf init` writes the plan (`.cadence/raf.yaml`), a commit hook and a `.gitignore` line. The commit
79
+ that adds the plan cites no lot, so `raf` warns `commit sans lot` and `raf check` would list it as a gap: `raf ignore`
80
+ acknowledges it once, and the plan records that. `cadence.yaml` is the project's settings file; its single
81
+ `orchestrate.test` line is the minimum the wave needs, and without it the tour warns `porte de tests absente`.
82
+ The folder is empty, so the gate here is `test -f hello.txt`, which the toy lot satisfies. On a project that already exists,
83
+ put its own test command there (`npm test`, `pytest`, `go test ./...`): `npm test` in a folder without `package.json` fails.
84
+ The reference for every other key is [orchestrate](orchestrate.md) and [deliver](deliver.md).
85
+
86
+ Then, from the parent folder:
87
+
88
+ ```sh
89
+ cadence lead tour
90
+ ```
91
+
92
+ It prints one line per project: lots in progress, drift, next lot, repository state. Expect `alpha · en cours rien ·
93
+ dérive aucune · … · prochain L1 Create hello.txt containing hello · dépôt propre`, and the same for `beta`. This is the table the lead reads.
94
+
95
+ ## Start the lead
96
+
97
+ Open Claude Code in the parent folder and call the skill:
98
+
99
+ ```sh
100
+ claude
101
+ ```
102
+
103
+ then type `/cadence:lead` (or say « pilot all projects »). The lead runs the tour, reports in a table, proposes three
104
+ lots and **waits for your priority**: nothing starts before you answer. [The lead workflow](lead.md) draws every step.
105
+
106
+ ## The first wave
107
+
108
+ When you pick `alpha:L1`, the lead launches a wave. You can do it yourself, from the parent folder:
109
+
110
+ ```sh
111
+ cadence orchestrate alpha:L1 --dry-run
112
+ cadence orchestrate alpha:L1 --wave first
113
+ ```
114
+
115
+ The first line only prints what would run: each step, its model and its budget. The second runs the lot in fresh
116
+ sessions: implement, then review, and hands the lot back to you. From inside a project, write `.:L1` instead of
117
+ `alpha:L1` (the project is a sub-folder of the current folder, so `alpha:L1` from inside `alpha` finds nothing).
118
+
119
+ The sessions of a wave never push and never deliver. You read the table, answer the questions it brings back, then
120
+ `/cadence:deliver` ships one project at a time ([deliver](deliver.md)). How waves, budgets and reviews work: [orchestrate](orchestrate.md).
121
+
122
+ ## If it stops
123
+
124
+ | You see | Cause | Do |
125
+ |---|---|---|
126
+ | `npm: command not found` | no Node | install Node 22.18+ (above) |
127
+ | `EACCES: permission denied, mkdir '/usr/lib/node_modules/@sylad'` | system-owned Node | `npm config set prefix ~/.npm-global` (above) |
128
+ | `fatal: empty ident name … not allowed` | no git identity | the two `git config --global` lines (above) |
129
+ | `Not logged in · Please run /login` | Claude Code not logged in | `claude`, log in, then `claude auth status` |
130
+ | `porte de tests absente` | no `orchestrate.test` | the `cadence.yaml` line of the project block |
131
+ | `alpha:L1 … introuvable` | launched from inside `alpha` | use `.:L1` |
132
+
133
+ A problem that is not here: [open an issue](https://github.com/Sylad/cadence/issues/new/choose) with `cadence --version` and the command you ran.
134
+
135
+ ## Not played
136
+
137
+ The lead skill and a wave played to the end (review, `raf done`, delivery) were not played on that machine, which was not logged
138
+ in to Claude Code; the tour, the dry run and the dry delivery were. Other systems (macOS, native Linux, PowerShell without WSL) were not played either.
package/docs/raf.md CHANGED
@@ -55,7 +55,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
55
55
  | `raf spec <id> on\|off` | set or remove `spec:` on an existing lot (a lot that changes a behaviour, for the back-end spec step of `cadence orchestrate`, see [The spec step of a lot with `spec: true`](orchestrate.md#the-spec-step-of-a-lot-with-spec-true)); refused on a sub-task; `off` is refused on a lot that has `screen: page` (the page implies the spec: remove `screen:` first) and `on` there only says so and changes nothing; on `component` and `journey` (ordinary path since L209) both work and set or remove the back-end `spec:`. `raf spec` only takes `on`/`off`: `cadence spec check\|qa <fiche.yaml>` keeps its own subcommands. `raf check` reports a `spec:` that is neither `true` nor `false` (`spec : true ou false attendu`) |
56
56
  | `raf uses <lot> [<resource>…]` | replace the `uses:` list of a lot of any status (`done` and `dropped` included) by the names given (duplicates removed, order kept); with no name, remove the key. Prints nothing. A name is a word of lowercase letters, digits, `-` and `_` (else `ressource invalide : <name> (lettres minuscules, chiffres, - et _)`, plan unchanged); a sub-task is refused (`uses se pose sur un lot, pas sur une sous-tâche : L1/t1`); an unknown lot is `lot inconnu : <id>`. A name not yet declared is accepted: `raf check` refuses it (L251) |
57
57
  | `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
58
- | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr) |
58
+ | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr. Like every `raf` command it only writes the plan file and **never commits** it: when the plan or a file `plan.onWrite` produces is left with changes, `done` prints the `git add` / `git commit` command that takes them, the subject from the `plan.commit` template, L371) |
59
59
  | `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
60
60
  | `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`), when the lot is not `doing` (a `done` or `dropped` lot is reopened by hand), or on a sub-task; sub-tasks and other keys are left as they are |
61
61
  | `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) |
@@ -66,7 +66,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
66
66
  | `raf now` | what to do next |
67
67
  | `raf list [--status s]` | flat list |
68
68
  | `raf docs-ok <lot> --reason text` | acknowledge the `docs.sync` gap of a lot (a document behind its commits, `raf check` red) when the documentation really stays true: a `docs_ok: { date, reason }` key on the lot, **the reason is required** (refused without it, for an unknown lot or for a sub-task); `raf check` and `cadence lead tour` no longer count the lot, `raf show` prints it, and `cadence orchestrate` neither calls the librarian nor holds the lot back for it. Commit the plan with it |
69
- | `raf ignore <sha> \| "exact subject" [--reason text]` | acknowledge a commit without a lot (tooling chore, a plan commit citing an unknown id) without rewriting history: a dated, reasoned line in the plan's `acknowledged:` section; a sha is exact, a subject covers every commit carrying it; exactly one commit argument, the reason goes through `--reason "text"` (`raf ignore 1a2b3c4 "tooling"` is refused with « un seul commit attendu ; la raison se passe par --reason "texte" », exit 2) |
69
+ | `raf ignore <sha> \| "exact subject" [--reason text]` | acknowledge a commit without a lot (tooling chore, a plan commit citing an unknown id) or a commit that cites known lots without working on them (a lead's plan commit creating lots, a setting cited in parentheses): it then counts for none of them — not started, not active, not to review (`raf commits` omits it) — without rewriting history: a dated, reasoned line in the plan's `acknowledged:` section; a sha is exact, a subject covers every commit carrying it; exactly one commit argument, the reason goes through `--reason "text"` (`raf ignore 1a2b3c4 "tooling"` is refused with « un seul commit attendu ; la raison se passe par --reason "texte" », exit 2) |
70
70
  | `raf check --ignored` | list the acknowledged commits with their date and reason |
71
71
  | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default (a visible lot without a public title is only a `⚠` warning: it never changes the exit code): commits without a lot (commits touching only plan files, or only version fields, `CHANGELOG.md`, the README's « What's new » section and `docs/whats-new.md` are exempt), unknown ids, `todo` lots that already have commits, idle lots (an open lot whose sub-lots, entries carrying `parent:` in a plan kept by another tool, have a recent commit or are in progress is not idle: L321), `done` lots with open sub-tasks, bad or circular dependencies, and an open lot titled « Revue UX — <screen> » without `visible: true` (`ux-review-not-visible`, an error: such a lot goes through precheck → implement → review with no UX step, so the screen is never reviewed — finance L32 on 2026-10-23; L271; the message names the fix, `raf visible <id> on`, L273; on a read-only plan, plan kept by another tool, it never names a `raf` command, which would refuse: with `plan.fields.visible` mapped it says to set `visible:` with the project's tool, without it it says the plan carries no `visible`, to map the field in `cadence.yaml` (`plan.fields.visible`) if the project's tool writes it, and that otherwise the UX review of that screen happens outside orchestrate, L321) |
72
72
  | `raf gantt [-o file]` | standalone Gantt page |
@@ -155,6 +155,7 @@ plan:
155
155
  commit: 'chore({lot}): {action} (orchestrate {vague})'
156
156
  ```
157
157
 
158
+ - `raf done <lot>` does not commit the plan (it never does, see below) but prints the command that would, with this template: `{action}` is `terminé` and, outside a wave, the group holding `{vague}` is dropped (`chore(L12): terminé`). Without a template: `plan: L12 terminé`.
158
159
  - `{lot}` is required (a commit must cite its lot); `{action}` is what was done (`démarré`, `revue de code enregistrée`, `revue de code notée`, `hypothèses de la fiche <sujet> notées`, `mineurs de la revue de <revue> notés`, `règles R1, R2 sourcées par la réponse du lead`, `test d'acceptation rescellé (<test>)`);
159
160
  `{vague}` is the wave id. A word between braces that is none of these, an empty template or one on several lines is refused when the config is read.
160
161
  - Outside a wave (`spec reseal`) there is no wave: the parenthesised group that holds `{vague}` is dropped, so the template above gives `chore(L17): test d'acceptation rescellé (…)`.
package/docs/whats-new.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.15.1**: a getting-started page (`docs/quickstart.md`) takes a lead of several projects from install to the first wave and lists the usual stops; GitHub issue templates for bugs and requests; a contrast fix in the dark figure.
8
+ - **1.15.0**: worktree integration settles the `CHANGELOG.md` conflict by itself; `--integrate` accepts a lot corrected and reviewed outside the wave; `raf ignore` clears a plan commit citing lots; `raf done` gives the plan commit.
7
9
  - **1.14.1**: publishes 1.14.0 (tagged, never published): Node 22.18 or later is required; `plan.commit`, `plan.onWrite`, `orchestrate.testReplay`, `spec green`, `ref coverage` and `verify --gate --sha`.
8
10
  - **1.14.0**: Node 22.18 or later is now required (Node 20 dropped). `plan.commit`, `plan.onWrite` and `orchestrate.testReplay` fit a wave to your hooks, files and load; `spec green`, `ref coverage` and `verify --gate --sha` are new.
9
11
  - **1.13.0**: `cadence release` prepares the version commit; `cadence config set --all` sets one key in every project; waves share the machine between sessions, reserve a budget for the escalation and tolerate the lead's delivery push.
@@ -35,6 +37,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
35
37
 
36
38
  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).
37
39
 
40
+ **1.15.1**: a way in for a lead who has never used cadence. A new getting-started page, `docs/quickstart.md` (L358, a section of the README), goes from install to the first wave: the parent folder, `raf init` in each project with the minimal `cadence.yaml`, the tour, `/cadence:lead`, the first wave, and a table of the usual stops (no Node, `EACCES`, no git identity, Claude Code not logged in). It comes from a walkthrough measured on a clean machine. Two GitHub issue templates, bug and request, are forms (L358, `.github/ISSUE_TEMPLATE/`); a `config.yml` turns off blank issues and points to the getting-started page, and the bug form asks for `cadence --version`, the steps and what was printed. The dark getting-started figure (`docs/quickstart-dark.svg`) draws the grey texts of « Lead session » in a lighter grey, 4.26:1 on the blue before and now above the 4.5:1 of WCAG 2.2 AA, and `docs/overview.md` lists `docs/quickstart.md` in its map of the pages (L374).
41
+
42
+ **1.15.0**: fewer lots handed back, and fewer manual detours, on the lead's side. The integration of `cadence orchestrate` in worktree mode no longer hands a lot back for a `CHANGELOG.md` conflict when the branch was released while the lot lived (L369, `docs/orchestrate.md`): an entry that the release moved under the published version is no longer read as a removal, and the entries a lot's commits wrote then reworded are dropped in favour of the version at the tip of the lot's branch, so one final entry per lot is left. `cadence orchestrate --resume --integrate <project:lot>` now accepts a lot handed back for its budget or a non-compliant review that the lead corrected and reviewed outside the wave (L370, `docs/orchestrate.md`, `docs/lead.md`): fix in the worktree, `raf review` there with the plan committed there, then `--integrate`; the lot restarts at the integration step with that verdict instead of the refusal « le lot n'est pas rendu à l'étape d'intégration ». A review older than a commit of the lot, or none, keeps the refusal. If the lead also wrote on that lot in the repository's plan meanwhile, the plan conflict of the rebase is settled by the program, provided that plan commit adds nothing but lot reviews. `raf ignore` also acknowledges a commit that cites known lots (L372): a plan commit that creates lots or cites a setting in parentheses was counted as work on those lots and could not be cleared; an acknowledged commit now counts for no lot, `raf commits` no longer lists it, and `raf check --ignored` does. `raf done <lot>` says that it did not commit the plan (L371): it never has, `raf` only writes the plan file, so when the plan or a file `plan.onWrite` produces has changes, the message prints the `git add` and `git commit` that take them, the subject following the project's `plan.commit` template.
43
+
38
44
  **1.14.1**: 1.14.0 was tagged but never published (a test of the Node minimum read only `[Unreleased]`, which the version commit empties, a fault of the test and not of the tool, L368); 1.14.1 publishes it as it stands, so everything listed under 1.14.0 below arrives with it, Node 22.18 or later first.
39
45
 
40
46
  **1.14.0**: **Node 22.18 or later is now required (Node 20 dropped)** (L365, `docs/stabilite.md`, `docs/releasing.md`): `engines.node` goes from `>=20` to `>=22.18`, the first release that runs `.ts` files without a tool, which the test suite depends on, and Node 20 is out of maintenance; it ships in a minor version because the minimum Node version now follows the maintained LTS lines. Upgrade Node before installing it. A wave fits the project it runs in. `plan.commit` in `cadence.yaml` (L343) is a template (`{lot}`, `{action}`, `{vague}`) for the subject of every plan commit, so a `commit-msg` hook that enforces Conventional Commits no longer fails a lot before any session, and `--dry-run` plays each subject through the repository's real hook. `plan.onWrite: { command, files }` (L346) regenerates the files derived from the plan after every write and commits them with it; a failing command refuses the write. Models are free (L345): `@<model>` of a lot and `orchestrate.review.light` / `full` take any name `claude --model` accepts. The gate runs under the project's Node, the `.nvmrc` first (L351). Fewer lots lost for nothing. `orchestrate.testReplay` (L354, `docs/orchestrate.md`) replays a red test of the gate alone before it counts, so a test that fails only under load is reported as unstable instead of triggering correction passes; a lot whose gate is green is no longer corrected (L361); the integration rebases again, up to twice, when the branch moved meanwhile (L364), and files the entries under the right `[Unreleased]` (L362). For the lead. `cadence spec green <lot>` and a refusal of `raf done` on a lot whose acceptance is still red (L366) stop a lot closed by hand from locking the tests of every later wave. `cadence ref coverage <inventaire.json>` and the `reference-planner` agent (L367) list the elements of a reference document that no lot covers. `cadence verify --gate --sha <sha>` (L348) replays the gate at a commit in a throwaway worktree. `lead tour` and `session` flag a close note that quotes a lot already done (L347). `cadence release` now prints the whole plugin update (L359), the test suite no longer depends on `TMPDIR` (L360), and the measured walk of a new user (L357) is in `docs/mesures/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.14.1",
3
+ "version": "1.15.1",
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",
@@ -44,6 +44,9 @@
44
44
  "docs/fonctionnement-lead-dark.svg",
45
45
  "docs/lead-workflow-light.svg",
46
46
  "docs/lead-workflow-dark.svg",
47
+ "docs/quickstart.md",
48
+ "docs/quickstart-light.svg",
49
+ "docs/quickstart-dark.svg",
47
50
  "docs/cycle-vague-light.svg",
48
51
  "docs/cycle-vague-dark.svg",
49
52
  "LICENSE"
@@ -107,7 +107,7 @@ handed back too. `cadence orchestrate --stop-after-current` lets the sessions ru
107
107
  ends `interrupted`, and `--resume --wave <wave>` takes it up again (the command printed names the wave). To take a lot out of a wave that is already stopped, use
108
108
  `--resume --wave <wave> --drop <project>:<lot>` (the lot is handed back instead of being replayed). Neither kills a session.
109
109
 
110
- **One worktree per lot** (`orchestrate.worktrees`, L279) is on by default in cadence's own `cadence.yaml` since 2026-10-25 (L320) and off in a project that does not set the key (the key is the whole switch: no environment variable). Two independent lots of one repository then play at once, each in its own worktree, and the program integrates them. Its options belong to that mode: `--resume --wave <wave> --integrate <project:lot>` replays a lot handed back at the integration step (the wave's output names that exact command), `--replay <project:lot>` also after a refused plan write, `--remove-worktree <project:lot> [--abandon-commits]` removes a kept worktree and branch, and `cadence orchestrate --status` and `cadence lead tour` list the worktrees kept by waves (nothing to list in the default mode).
110
+ **One worktree per lot** (`orchestrate.worktrees`, L279) is on by default in cadence's own `cadence.yaml` since 2026-10-25 (L320) and off in a project that does not set the key (the key is the whole switch: no environment variable). Two independent lots of one repository then play at once, each in its own worktree, and the program integrates them. Its options belong to that mode: `--resume --wave <wave> --integrate <project:lot>` replays a lot handed back at the integration step (the wave's output names that exact command) and, since L370, a lot handed back for its budget or a non-compliant review that you had corrected and reviewed outside the wave (the gesture is under « A lot handed back despite the escalation », section 3), `--replay <project:lot>` also after a refused plan write, `--remove-worktree <project:lot> [--abandon-commits]` removes a kept worktree and branch, and `cadence orchestrate --status` and `cadence lead tour` list the worktrees kept by waves (nothing to list in the default mode).
111
111
 
112
112
  **Delivering while the wave runs.** A lot that turns **ready** in the journal is not left waiting: once the lead has re-verified it (section 3, point 2) and recorded its verdict (`raf done`), it delivers that lot at once, without waiting for the end of the wave. The re-check runs at that sha with `cadence verify --gate --sha <sha>` (a throwaway worktree whose untracked dependencies are linked from the main tree, removed afterwards), never in the shared tree, which holds the lots still running. Take the sha to deliver from `git log`: the last commit of the ready lot such that no commit of a lot that is not ready (running, queued, in question, handed back) sits beneath it — the same « livrable jusqu'à » the final table prints. Push that sha alone (`git push origin <sha>:main`, never HEAD, which holds the code of lots still running; the wave tolerates exactly this push — the journal says « push du lead (livraison de <lot>) toléré » — because the sha is an ancestor of HEAD holding no commit of a lot of the wave that is not ready (a commit that cites no lot passes), whereas any other push still stops the wave as an incident), then `cadence deliver --dry-run --sha <sha>` and `cadence deliver --sha <sha>`: `deliver` accepts a `--sha` during a wave of the same repository when none of its commits belongs to a lot that is not ready, and refuses otherwise, naming the lots. Without `--sha` it stays refused until every lot of the repository is ready or handed back. The wave's sessions keep committing in the working tree: `--sha` never reads it. What the log keeps is `<sha>`, unless the delivery script declares another deployed commit in `CADENCE_DEPLOYED_FILE`; if HEAD moved during the delivery without that declaration, `deliver` warns (« la tête a avancé pendant la livraison ») and the log keeps `<sha>`: it is not a failure. This is the measured gain: a ready lot used to wait for the slowest lot of its wave (1,554 lot-minutes between « ready » and the end of the wave over 28 waves).
113
113
 
@@ -141,7 +141,7 @@ of the plan itself (section 2b), and the limits of section « Limits that always
141
141
  handed back ready (refused, review not compliant after the correction passes, red delivery, blocking QA finding) is
142
142
  not chained over: it is reported, and the chain goes on with the lots that do not depend on it.
143
143
 
144
- **A lot handed back despite the escalation** (L337). When the project declares `orchestrate.escalation: { model }`, the wave itself plays one correction and one fresh review on that model before handing a lot back for a non-compliant review or red tests: do not take such a lot up by hand on a model of your own choice first, and do not replay the escalation yourself. The table says `escalade <model> : …` under the lot. A lot handed back **despite** the escalation (`rendu malgré l'escalade`) is for the human or for an arbitration (the sheet, the brief or the lot is at fault, not the model): report it with the remaining findings and go on with the other lots. `escalade … non jouée` means the lot's budget did not cover it: the lot is yours, with its findings. `escalade … session escaladée sans résultat` means the escalated session itself failed (often a model the account does not have): the model never judged the lot, fix the key or take the lot up, do not read it as the lot's fault. A line `en cours` is a lot still running; `engagée, lot rendu avant le verdict du modèle` is a lot withdrawn, failed by an error or stopped by an incident during the escalation: the model did not judge it, read the lot line for the reason. Without the key, nothing changes; the model has no default, as the account may not have the strongest one.
144
+ **A lot handed back despite the escalation** (L337). When the project declares `orchestrate.escalation: { model }`, the wave itself plays one correction and one fresh review on that model before handing a lot back for a non-compliant review or red tests: do not take such a lot up by hand on a model of your own choice first, and do not replay the escalation yourself. The table says `escalade <model> : …` under the lot. A lot handed back **despite** the escalation (`rendu malgré l'escalade`) is for the human or for an arbitration (the sheet, the brief or the lot is at fault, not the model): report it with the remaining findings and go on with the other lots. `escalade … non jouée` means the lot's budget did not cover it: the lot is yours, with its findings. **Taking such a lot up** (handed back for its budget or a non-compliant review, L370): fix it in its kept worktree (the final table gives the path; a fix subagent there, under `cadence lead hold`), then run `raf review <lot> "conforme : …"` **in that worktree** and commit its plan there — the review is read from the worktree's plan, not from the main repository's, and it must come after the last work commit of the lot —, then `cadence orchestrate --resume --wave <wave> --integrate <project>:<lot>`: the program rebases, replays the checks, keeps your verdict (or makes a short review of the rebased diff) and advances the repository's branch. Do not integrate it by hand any more (L367, 26-10), and do not record that review in the main repository's plan: `--integrate` would not see it and would keep refusing (« pas rendu à l'étape d'intégration »). `escalade … session escaladée sans résultat` means the escalated session itself failed (often a model the account does not have): the model never judged the lot, fix the key or take the lot up, do not read it as the lot's fault. A line `en cours` is a lot still running; `engagée, lot rendu avant le verdict du modèle` is a lot withdrawn, failed by an error or stopped by an incident during the escalation: the model did not judge it, read the lot line for the reason. Without the key, nothing changes; the model has no default, as the account may not have the strongest one.
145
145
 
146
146
  **The threshold.** Before every new wave, read the context of the lead session with `cadence session context`: the
147
147
  `cadence-hud` mod publishes the context of each session, one file per session named after its id, and the command reads the