@sylad/cadence 1.29.0 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "cadence",
10
10
  "description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
11
- "version": "1.29.0",
11
+ "version": "1.31.0",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
3
  "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
- "version": "1.29.0",
4
+ "version": "1.31.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/dist/audit.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { dirname, join, relative } from 'node:path';
3
3
  import { check } from './check.js';
4
4
  import { parse } from 'yaml';
5
- import { changedFiles, fileAt, preloadChangedFiles, readCommits } from './git.js';
5
+ import { changedFiles, fileAt, isAncestor, preloadChangedFiles, readCommits } from './git.js';
6
6
  import { citedRefs, linkCommits, scopeOf } from './link.js';
7
7
  import { PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readNewsConfig, readOrchestrateConfig } from './config.js';
8
8
  import { ficheHypotheses } from './spec.js';
@@ -303,6 +303,42 @@ function unreviewed(plan, root, review, linked) {
303
303
  // Seuls les commits postérieurs sont examinés : un lot relu à son dernier commit ne coûte aucun appel git.
304
304
  return workCommits(plan, root, at < 0 ? linked : linked.slice(0, at));
305
305
  }
306
+ /** Un texte de verdict de revue dit « non conforme » / « not compliant » (le mot « conforme » dedans ne compte pas) ; « conforme après 1 correction » reste conforme. */
307
+ export function isNonConformingVerdict(text) {
308
+ return /\bnon[\s-]*conforme|\bnot\s+compliant|\bnon[\s-]*compliant/i.test(text);
309
+ }
310
+ /**
311
+ * Les lots dont les commits de travail sont dans l'histoire de `sha` après `base` (toute l'histoire sans `base`), avec leur état de livraison (L474, hors vague) :
312
+ * `done` et abandonné ne bloquent pas ; un lot « prêt » (son verdict de revue, conforme, couvre tous ses commits) non plus ; un lot à faire ou en cours, non relu
313
+ * ou relu avant des commits postérieurs, bloque. Un commit qui cite un lot que le plan ne connaît pas (clos et archivé, ou inconnu) n'en fait pas un lot.
314
+ */
315
+ export function deliveryLots(plan, root, sha, base) {
316
+ const inRange = planCommits(plan, root, { range: base ? `${base}..${sha}` : sha });
317
+ const { byLot } = linkCommits(plan.lots(), inRange, plan.refs);
318
+ const lots = [];
319
+ for (const l of plan.lots()) {
320
+ if (!workCommits(plan, root, byLot.get(l.id) ?? []).length)
321
+ continue;
322
+ const entry = (blocking, state) => lots.push({ id: l.id, title: l.title, blocking, state });
323
+ if (l.status === 'done')
324
+ entry(false, 'terminé');
325
+ else if (l.status === 'dropped')
326
+ entry(false, 'abandonné');
327
+ else {
328
+ // Couverture jugée sur les commits du lot jusqu'à `sha` seulement : un `fix` posé au-dessus du sha livré n'est pas livré.
329
+ const late = l.review ? unreviewed(plan, root, l.review, lotCommits(plan, root, l.id)).filter((c) => isAncestor(root, c.sha, sha)).length : 0;
330
+ const state = l.status === 'todo' ? 'à faire' : 'en cours';
331
+ // Prêt = revue CONFORME couvrant les commits (côté vague, un lot n'est ready qu'après une revue conforme).
332
+ if (l.review && late === 0 && !isNonConformingVerdict(l.review.verdict))
333
+ entry(false, 'prêt');
334
+ else if (l.review && late === 0)
335
+ entry(true, `${state}, relu non conforme`);
336
+ else
337
+ entry(true, `${state}, ${l.review ? `relu avant ${late} commit(s)` : 'non relu'}`);
338
+ }
339
+ }
340
+ return lots;
341
+ }
306
342
  /** Nombre de commits d'un lot que sa revue de code ne couvre pas — ce que `raf done` refuse quand la porte est active. */
307
343
  export function unreviewedWork(plan, root, lotId) {
308
344
  const lot = plan.lots().find((l) => l.id === lotId);
package/dist/cli.js CHANGED
@@ -23,11 +23,12 @@ import { schedule } from './schedule.js';
23
23
  import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
24
24
  import { configSet } from './config-set.js';
25
25
  import { movePriority } from './priority.js';
26
- import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
26
+ import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
27
27
  import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
28
28
  import { addHold, releaseHold } from './orchestrate/registry.js';
29
29
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
30
30
  import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
31
+ import { mainTreeWorkers } from './orchestrate/main-tree.js';
31
32
  import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
32
33
  import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
33
34
  import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
@@ -423,6 +424,7 @@ function dispatch(argv, io) {
423
424
  plan.save();
424
425
  io.out(id);
425
426
  warnIntegrating(root, io.err);
427
+ warnWaveWorking(root, io.env, io.err);
426
428
  return 0;
427
429
  }
428
430
  case 'start':
@@ -464,8 +466,10 @@ function dispatch(argv, io) {
464
466
  plan.note(rest[0], `abandonné : ${values.reason}`, today);
465
467
  plan.save();
466
468
  io.out(`${rest[0]} → ${status}`);
467
- if (status === 'done')
469
+ if (status === 'done') {
468
470
  warnIntegrating(root, io.err);
471
+ warnWaveWorking(root, io.env, io.err);
472
+ }
469
473
  touchedLot(plan, rest[0], values.verbose, io);
470
474
  if (status === 'done' && !rest[0].includes('/')) {
471
475
  const lot = plan.lot(rest[0]);
@@ -522,6 +526,7 @@ function dispatch(argv, io) {
522
526
  plan.note(rest[0], rest.slice(1).join(' '), today);
523
527
  plan.save();
524
528
  warnIntegrating(root, io.err);
529
+ warnWaveWorking(root, io.env, io.err);
525
530
  touchedLot(plan, rest[0], values.verbose, io);
526
531
  return 0;
527
532
  }
@@ -1219,6 +1224,23 @@ function warnIntegrating(root, err) {
1219
1224
  if (holder)
1220
1225
  err(`avertissement : intégration en cours (vague ${holder.wave}, pid ${holder.pid}) — ce plan est un fichier suivi modifié de l'arbre principal, l'intégration en cours peut refuser son lot (arbre du dépôt modifié) : committer le plan sans attendre`);
1221
1226
  }
1227
+ /**
1228
+ * Avertit (stderr, sans refuser) quand une session de vague écrit dans l'arbre principal du dépôt (L475) : un lot en implémentation, revue ou correction sans worktree.
1229
+ * Un commit de plan du lead à ce moment-là rend le lot (cas du 30-10 : cadence L458 rendu par le commit du plan). Sans effet dans le worktree d'un lot, ou depuis une session de la vague (c'est elle qui tient l'arbre).
1230
+ */
1231
+ function warnWaveWorking(root, env, err) {
1232
+ if (env.CADENCE_ORCHESTRATED)
1233
+ return;
1234
+ const top = gitRoot(root);
1235
+ if (!top || !statSync(join(top, '.git')).isDirectory())
1236
+ return;
1237
+ const holder = activeLock(join(sharedStateDir(root), REPO_LOCK));
1238
+ if (!holder)
1239
+ return;
1240
+ const working = mainTreeWorkers(cadenceHome(), holder.pid, top);
1241
+ if (working?.length)
1242
+ err(`avertissement : la vague ${holder.wave} (pid ${holder.pid}) travaille dans l'arbre principal de ce dépôt (${working.join(', ')}) — un commit de plan maintenant rendrait ces lots à la revue ; attendre la fin de leur étape, ou écrire le plan sans le committer`);
1243
+ }
1222
1244
  /** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec reseal`. */
1223
1245
  function waveHolding(root) {
1224
1246
  const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
@@ -1746,8 +1768,10 @@ function sessionContext(io, session) {
1746
1768
  if (!r.ok)
1747
1769
  throw new RafError(`session context : contexte illisible — ${r.reason} ; à traiter comme au seuil`);
1748
1770
  io.out(`ctx ${Math.round(r.ctx.percent)} % (${r.ctx.tokens}/${r.ctx.window})`);
1749
- const threshold = readLeadContextThreshold(io.cwd);
1750
- io.out(`seuil ${threshold} tokens : ${r.ctx.tokens >= threshold ? 'atteint' : 'sous le seuil'}`);
1771
+ // Lancée d'un sous-projet, la commande lit le cadence.yaml du dossier du lead (L457) et nomme le dossier lu.
1772
+ const leadDir = findLeadDir(io.cwd);
1773
+ const threshold = readLeadContextThreshold(leadDir);
1774
+ io.out(`seuil ${threshold} tokens : ${r.ctx.tokens >= threshold ? 'atteint' : 'sous le seuil'}${leadDir === io.cwd ? '' : ` (lu dans ${join(leadDir, 'cadence.yaml')})`}`);
1751
1775
  return 0;
1752
1776
  }
1753
1777
  function session([sub, ...args], ctx, values) {
package/dist/deliver.js CHANGED
@@ -4,7 +4,7 @@ import { constants } from 'node:os';
4
4
  import { join, relative } from 'node:path';
5
5
  import { parse } from 'yaml';
6
6
  import { onTermination, readProcs, SIGNAL_GRACE_MS, SIGNAL_PENDING_MS, TreeTracker } from './proc.js';
7
- import { isPlanOnly } from './audit.js';
7
+ import { deliveryLots, isPlanOnly } from './audit.js';
8
8
  import { commitPaths, dirtyPaths, headSha, isAncestor, onRemote, readCommits, repoStatus, resolveCommit } from './git.js';
9
9
  import { PLAN_ACTIONS, planCommitMessage } from './plan-commit.js';
10
10
  import { citedRefs } from './link.js';
@@ -235,6 +235,7 @@ export function describeCheck(c, sha) {
235
235
  return c.command;
236
236
  return `GET ${substitute(c.url, sha)} → ${c.status ?? 200}${c.contains === undefined ? '' : `, contient « ${substitute(c.contains, sha)} »`}`;
237
237
  }
238
+ const guardMessage = (sha, lots) => `${sha.slice(0, 7)} contient des commits de lots ni terminés ni prêts : ${lots.map((l) => `${l.id} (${l.state})`).join(', ')} ; livrer un sha antérieur, ou relire ces lots (raf review) avant`;
238
239
  /** Livre HEAD. 0 livré, 1 étape en échec, 2 refus avant d'agir. */
239
240
  export async function deliver(ctx, deps) {
240
241
  const { config, out, err } = ctx;
@@ -259,6 +260,24 @@ export async function deliver(ctx, deps) {
259
260
  err(`deliver : ${dirty} fichier(s) suivi(s) modifié(s) : non livré(s), seul ${sha.slice(0, 7)} l'est (allowDirty)`);
260
261
  if (!onRemote(ctx.root, sha))
261
262
  return refuse(`${sha.slice(0, 7)} non poussé : la CI n'a rien construit (git push)`);
263
+ // Hors vague, un sha choisi emporte les commits de TOUS les lots sous lui (« L49 seul » a livré L50 et L51 non relus, 30-10) : on les nomme, avec leur état.
264
+ // Pendant une vague, la garde de la vague (cli, `blockingLots`) en tient lieu.
265
+ let undelivered = [];
266
+ if (ctx.sha !== undefined && !ctx.duringWave && ctx.plan) {
267
+ const last = lastDelivery(ctx.state);
268
+ const from = last && isAncestor(ctx.root, last, sha) ? last : null;
269
+ const lots = deliveryLots(ctx.plan, ctx.root, sha, from);
270
+ if (ctx.dryRun) {
271
+ out(`Lots de l'intervalle ${from ? `${from.slice(0, 7)}..` : 'jusqu\'à '}${sha.slice(0, 7)} :`);
272
+ if (lots.length === 0)
273
+ out(' (aucun)');
274
+ for (const l of lots)
275
+ out(` ${l.id} « ${l.title} » — ${l.state}`);
276
+ }
277
+ undelivered = lots.filter((l) => l.blocking);
278
+ if (undelivered.length && !ctx.dryRun)
279
+ return refuse(guardMessage(sha, undelivered));
280
+ }
262
281
  const lock = readLock(ctx.state);
263
282
  if (lock && lockAlive(lock)) {
264
283
  return refuse(`livraison déjà en cours (${lock.sha.slice(0, 7)}, pid ${lock.pid}, depuis ${lock.started}) — jamais deux à la fois ; ` +
@@ -292,6 +311,8 @@ export async function deliver(ctx, deps) {
292
311
  out(` Vérifications (réessayées pendant ${config.verifyTimeout} s) :`);
293
312
  config.verify.forEach((c, i) => out(` ${i + 1}. ${describeCheck(c, sha)}`));
294
313
  out(` Variables : ${Object.entries(env).map(([k, v]) => `${k}=${v}`).join(' ')}`);
314
+ if (undelivered.length)
315
+ return refuse(guardMessage(sha, undelivered));
295
316
  return 0;
296
317
  }
297
318
  if (lock && removeStaleLock(ctx.state, lock)) {
package/dist/lead.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
- import { basename, join, relative, sep } from 'node:path';
2
+ import { basename, dirname, join, relative, sep } from 'node:path';
3
3
  import { audit, gateWaiting, nextUp, planCommits, workByLot } from './audit.js';
4
4
  import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readPlanConfig } from './config.js';
5
5
  import { diffDays } from './dates.js';
@@ -45,6 +45,24 @@ export function readLeadUx(parent) {
45
45
  throw new RafError(`${found.file} : lead.ux doit être « lead » ou « human »`);
46
46
  return ux;
47
47
  }
48
+ /** Le dossier du lead pour `start` (L457) : `start` ou le premier parent dont le cadence.yaml porte la clé `lead:` ou `priority:` ; aucun ne la porte (ou un yaml illisible en chemin) = `start` lui-même. */
49
+ export function findLeadDir(start) {
50
+ for (let dir = start;; dir = dirname(dir)) {
51
+ const file = join(dir, 'cadence.yaml');
52
+ if (existsSync(file)) {
53
+ try {
54
+ const raw = parse(readFileSync(file, 'utf8'));
55
+ if (raw && typeof raw === 'object' && ('lead' in raw || 'priority' in raw))
56
+ return dir;
57
+ }
58
+ catch {
59
+ // un yaml illisible n'est pas celui d'un lead qu'on puisse lire : on continue de remonter
60
+ }
61
+ }
62
+ if (dirname(dir) === dir)
63
+ return start;
64
+ }
65
+ }
48
66
  /** Seuil de contexte du lead quand `lead.contextThreshold` n'est pas posé (L437). */
49
67
  export const CONTEXT_THRESHOLD_DEFAULT = 150_000;
50
68
  /** Clé `lead.contextThreshold` du cadence.yaml du dossier (L437) : le nombre de tokens de contexte à partir duquel le lead ne lance plus de vague. En tokens et non en % : 60 % d'une fenêtre de 1M valait 600k. Un entier ≥ 1, sinon refus. */
@@ -38,7 +38,7 @@ import { resolveLotRepos } from '../repos.js';
38
38
  import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
39
39
  import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
40
40
  import { quotaText, renderDetail, renderTable, resumeCommand } from './table.js';
41
- import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, repairSnapshotModules, snapshotExists, waveReadsControl, waveReadsIntegrate, waveReadsLotBudget, waveReadsReplay, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
41
+ import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, repairSnapshotModules, snapshotExists, waveReadsAnswerQueue, waveReadsControl, waveReadsIntegrate, waveReadsLotBudget, waveReadsReplay, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
42
42
  const REPO_WAIT_MS = 3 * 60 * 60_000;
43
43
  const REPO_WAIT_POLL_MS = 5_000;
44
44
  export const DEFAULT_BUDGET = 2_000_000;
@@ -634,6 +634,8 @@ function freeLines(launch, drawn) {
634
634
  /** Efface l'écran et replace le curseur en haut : l'entrée d'un rendu de `--status --watch`. */
635
635
  const CLEAR_SCREEN = '\x1b[H\x1b[2J';
636
636
  const DEFAULT_WATCH_INTERVAL = 10;
637
+ /** Attente (ms) entre deux lectures de la file de réponses d'une vague vivante (L407) quand aucune file de lots ne finit. */
638
+ const ANSWER_POLL_MS = 2000;
637
639
  /** Ce que `--status` montre à un instant : les vagues vivantes, puis le tableau (absent si seules les vagues vivantes comptent). */
638
640
  function statusView(args, launch, cap, now) {
639
641
  const id = statusWave(args);
@@ -805,6 +807,8 @@ export async function orchestrate(argv, io, deps) {
805
807
  return resume(args, argv, io, deps, launch, today);
806
808
  if (args.drop.length || args.stopAfterCurrent)
807
809
  return control(args, io);
810
+ if (args.answers.length)
811
+ return answerOnly(args, io);
808
812
  if (args.lots.length === 0 && !args.continue)
809
813
  throw new RafError('usage : cadence orchestrate <projet>:<lot>… [--budget 2M] [--max-sessions 2] [--dry-run] [--continue [--until 18:00] [--priority a,b]] | --waits [--since AAAA-MM-JJ] | --status [vague] [--detail projet:lot | --watch [--interval 10]] | --resume [vague] [--answer projet:lot "réponse"] [--integrate projet:lot] [--replay projet:lot] | --drop projet:lot | --stop-after-current | --remove-worktree projet:lot [--abandon-commits]');
810
814
  const refusals = [];
@@ -1286,7 +1290,41 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
1286
1290
  if (held.includes(join(sharedStateDir(r), REPO_LOCK)) && all.filter((l) => lotRepoPaths(l).includes(r)).every(finished))
1287
1291
  freeRepo(r);
1288
1292
  }
1289
- }, (lots) => lots.every((c) => c.tree));
1293
+ }, (lots) => lots.every((c) => c.tree), {
1294
+ pollMs: deps.slotPollMs ?? ANSWER_POLL_MS,
1295
+ // L407 : les réponses déposées par `--answer` pendant la vague sont prises entre deux étapes ; un lot qui n'est plus en question ne rejoue rien.
1296
+ take: (canRun) => {
1297
+ const taken = [];
1298
+ for (const e of store.answerQueue()) {
1299
+ const c = ctxs.find((x) => lotKey(x.lot.project, x.lot.lot) === e.lot);
1300
+ const why = !c ? 'lot inconnu' : c.lot.status !== 'question' ? `le lot n'attend plus de réponse (${c.lot.status})` : c.lot.pendingAnswer ? 'une réponse est déjà en cours de jeu' : null;
1301
+ if (why) {
1302
+ wctx.log(`${e.lot} : réponse écartée (${why})`);
1303
+ store.markAnswerTaken(e.id);
1304
+ }
1305
+ else if (c && canRun(c)) {
1306
+ c.lot.pendingAnswer = e.text;
1307
+ c.lot.answers.push(e.text);
1308
+ c.lot.questions = [];
1309
+ store.writeLot(c.lot);
1310
+ store.markAnswerTaken(e.id);
1311
+ wctx.log(`${e.lot} : réponse du lead prise, le lot repart`);
1312
+ // Le lot avait fini de jouer à sa question (`settled`) : il rejoue, il tient de nouveau un créneau — sans cela `lanes()` le sous-compte et le registre annonce un créneau libre de trop (L433).
1313
+ settled.delete(c);
1314
+ taken.push(c);
1315
+ }
1316
+ }
1317
+ if (taken.length) {
1318
+ try {
1319
+ updateWaveRepos(home, process.pid, repos.filter((x) => !freed.has(x)), lanes());
1320
+ }
1321
+ catch {
1322
+ // registre illisible : le décompte reste celui du dernier écrit
1323
+ }
1324
+ }
1325
+ return taken;
1326
+ },
1327
+ });
1290
1328
  handBackDroppedQuestions(ctxs); // un lot en question retiré pendant la vague : rendu avant de fixer le statut de la vague
1291
1329
  }
1292
1330
  finally {
@@ -1308,11 +1346,57 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
1308
1346
  return 3;
1309
1347
  return all.every((l) => l.status === 'ready') ? 0 : 1;
1310
1348
  }
1311
- /** `--drop` / `--stop-after-current` (L79) : une demande écrite dans le dossier d'état de la vague vivante ; c'est elle qui la lit avant sa prochaine session. */
1312
- function control(args, io) {
1313
- const flags = '--drop / --stop-after-current';
1314
- if (args.lots.length || args.continue || args.dryRun || args.budget !== undefined || args.answers.length)
1315
- throw new RafError(`${flags} ne se combinent pas avec des lots à lancer, --continue, --dry-run, --budget ou --answer`);
1349
+ /**
1350
+ * `--answer` sur une vague VIVANTE (L407) : la réponse est déposée dans la file de la vague (`answers.log`), qui la prend entre deux étapes et rejoue le lot dès qu'un créneau est libre.
1351
+ * Sans cela le lead attendait la fin de la vague (15 min d'un lot bloqué le 28-10). Les lots visés sont contrôlés comme à la reprise : connus, en question.
1352
+ */
1353
+ function answerLive(args, io, store) {
1354
+ const lots = store.lots();
1355
+ const refusals = [];
1356
+ const keys = [];
1357
+ for (const a of args.answers) {
1358
+ const matches = lots.filter((l) => l.lot === a.lot && (a.project ? l.project === a.project : true));
1359
+ const where = `${a.project ? `${a.project}:` : ''}${a.lot}`;
1360
+ if (matches.length !== 1)
1361
+ refusals.push(matches.length ? `--answer ${where} : plusieurs projets portent ce lot, précisez projet:lot` : `--answer ${where} : lot inconnu dans la vague ${store.id}`);
1362
+ else if (matches[0].status !== 'question')
1363
+ refusals.push(`--answer ${lotKey(matches[0].project, matches[0].lot)} : le lot n'attend pas de réponse (${matches[0].status})`);
1364
+ else
1365
+ keys.push({ key: lotKey(matches[0].project, matches[0].lot), text: a.text });
1366
+ }
1367
+ if (!waveReadsAnswerQueue(store.dir))
1368
+ refusals.push(`la vague ${store.id} ne lit pas la file de réponses (lancée avec une version de cadence d'avant L407) : attendre sa fin, puis ${resumeCommand(store.id, '--answer', '<projet>:<lot>', '"…"')}`);
1369
+ if (refusals.length) {
1370
+ for (const r of refusals)
1371
+ io.err(`orchestrate : ${r}`);
1372
+ return 2;
1373
+ }
1374
+ for (const { key, text } of keys) {
1375
+ const id = store.requestAnswer(key, text);
1376
+ // La vague a pu finir entre le contrôle et le dépôt : sa file ne sera plus lue, la réponse est retirée et le lead reprend à la main.
1377
+ if (!liveWaves(cadenceHome()).some((w) => w.wave === store.id)) {
1378
+ store.markAnswerTaken(id);
1379
+ io.err(`orchestrate : ${key} : la vague ${store.id} vient de finir, réponse non prise : ${resumeCommand(store.id, '--answer', key, '"…"')}`);
1380
+ return 2;
1381
+ }
1382
+ store.journal(`demande : réponse du lead pour ${key}`);
1383
+ io.out(`${key} : réponse déposée — la vague ${store.id} la prend entre deux étapes et rejoue le lot dès qu'un créneau est libre`);
1384
+ }
1385
+ return 0;
1386
+ }
1387
+ /** La vague vivante, pour `--answer` seul (sans --resume) : la même recherche que `--drop`. */
1388
+ function answerOnly(args, io) {
1389
+ if (args.drop.length || args.stopAfterCurrent || args.budget !== undefined || args.dryRun || args.continue || args.lots.length)
1390
+ throw new RafError("--answer ne se combine pas avec --drop, --stop-after-current, --budget, --dry-run, --continue ni des lots à lancer (sur une vague arrêtée : --resume --answer)");
1391
+ const store = liveStore('--answer', args, io);
1392
+ if (typeof store === 'number') {
1393
+ io.err(`orchestrate : --answer : une vague arrêtée se reprend avec ${resumeCommand('<vague>', '--answer', '<projet>:<lot>', '"…"')}`);
1394
+ return store;
1395
+ }
1396
+ return answerLive(args, io, store);
1397
+ }
1398
+ /** La vague vivante lancée depuis ce dossier que vise `--wave` (ou la seule) ; sinon le refus (code 2) déjà écrit. */
1399
+ function liveStore(flags, args, io) {
1316
1400
  const live = liveWaves(cadenceHome()).filter((w) => w.cwd === io.cwd && (args.wave === undefined || w.wave === args.wave));
1317
1401
  if (live.length === 0) {
1318
1402
  io.err(`orchestrate : ${flags} : aucune vague en cours${args.wave ? ` (${args.wave})` : ''} lancée depuis ce dossier`);
@@ -1327,6 +1411,16 @@ function control(args, io) {
1327
1411
  io.err(`orchestrate : ${flags} : dossier d'état de la vague ${live[0].wave} introuvable`);
1328
1412
  return 2;
1329
1413
  }
1414
+ return store;
1415
+ }
1416
+ /** `--drop` / `--stop-after-current` (L79) : une demande écrite dans le dossier d'état de la vague vivante ; c'est elle qui la lit avant sa prochaine session. */
1417
+ function control(args, io) {
1418
+ const flags = '--drop / --stop-after-current';
1419
+ if (args.lots.length || args.continue || args.dryRun || args.budget !== undefined || args.answers.length)
1420
+ throw new RafError(`${flags} ne se combinent pas avec des lots à lancer, --continue, --dry-run, --budget ou --answer`);
1421
+ const store = liveStore(flags, args, io);
1422
+ if (typeof store === 'number')
1423
+ return store;
1330
1424
  const lots = store.lots();
1331
1425
  const refusals = [];
1332
1426
  const keys = [];
@@ -1427,6 +1521,9 @@ async function resume(args, argv, io, deps, launch, today) {
1427
1521
  io.err(`orchestrate : vague ${store.id} sans instantané (antérieure à L61) : reprise avec le dist/ et les gabarits courants`);
1428
1522
  }
1429
1523
  }
1524
+ // L407 : la forme documentée sur une vague qui tourne encore dépose la réponse dans sa file au lieu de refuser ; avec une autre option de reprise, le refus ci-dessous reste.
1525
+ if (args.answers.length && !args.lotBudgets.length && !args.drop.length && !args.integrate.length && args.budget === undefined && !args.continue && liveWaves(cadenceHome()).some((w) => w.wave === store.id && w.cwd === launch && w.pid !== process.pid))
1526
+ return answerLive(args, io, store);
1430
1527
  const wave = store.readWave();
1431
1528
  const refusals = [];
1432
1529
  const info = deps.claudeInfo();
@@ -1030,7 +1030,7 @@ async function session(c, kind, local = false) {
1030
1030
  save(c);
1031
1031
  return { step, report: res.structured, before, after, others };
1032
1032
  }
1033
- /** Les dépôts tiers d'un lot (L456) : frères du dépôt du lot, hors voisins déclarés et dépôts des autres lots de la vague, ou d'une autre vague en cours du même dossier (leur branche principale bouge légitimement, R14). */
1033
+ /** Les dépôts tiers d'un lot (L456) : frères du dépôt du lot, hors voisins déclarés et dépôts des autres lots de la vague (leur branche principale bouge légitimement, R14). Une vague lancée après ce relevé, comme le lead ou une livraison, n'est pas dans la liste : seuls les commits qui citent un lot de cette vague sont constatés (L458) ; exclure les vagues vivantes déjà connues évite qu'un identifiant de lot commun (deux projets, chacun son L1) fasse passer leurs commits pour les nôtres. */
1034
1034
  function thirdRepos(c) {
1035
1035
  const mine = lotKey(c.lot.project, c.lot.lot);
1036
1036
  const others = [...c.wave.store.lots().filter((x) => lotKey(x.project, x.lot) !== mine), ...c.wave.store.liveWaveLots()].flatMap(lotRepoPaths);
@@ -1060,7 +1060,7 @@ function checkTiers(c, kind, step, before) {
1060
1060
  w.log(`${key} · dépôt tiers ${name} ignoré : aucune branche principale`);
1061
1061
  }
1062
1062
  else {
1063
- const moved = tierMoved(repo, b, a, relative(l.repo, c.loadPlan().path));
1063
+ const moved = tierMoved(repo, b, a, w.store.lots().map((x) => x.lot));
1064
1064
  if (moved) {
1065
1065
  step.status = 'failed';
1066
1066
  step.cause = `commit sur la branche principale d'un autre dépôt (${name}, ${moved.branch})`;
@@ -0,0 +1,34 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { liveWaves } from './registry.js';
3
+ import { lotKey, lotRepoPaths, RunStore } from './state.js';
4
+ import { lotTreeDir } from './worktree.js';
5
+ /** Les étapes d'un lot où une session de la vague écrit dans le dépôt (L475) : le lot en file, en question, prêt, rendu, en échec ou suspendu n'y touche pas. */
6
+ const WRITING = new Set(['implementing', 'reviewing', 'fixing']);
7
+ const real = (p) => {
8
+ try {
9
+ return realpathSync(p);
10
+ }
11
+ catch {
12
+ return p;
13
+ }
14
+ };
15
+ /**
16
+ * Lots de la vague vivante `pid` dont une session travaille dans l'ARBRE PRINCIPAL de `repo` (L475) : un lot en implémentation, revue ou correction qui ne joue pas dans un worktree
17
+ * (vague sans worktree, ou lot dont le dossier de worktree n'existe pas). Un commit de plan du lead y est un commit de plus dans l'historique que la revue ou l'intégration compare
18
+ * à celui du lot : le lot est rendu (cas du 30-10). Clés `projet:lot` ; liste vide si aucun ; null si l'état de la vague est illisible.
19
+ */
20
+ export function mainTreeWorkers(home, pid, repo) {
21
+ try {
22
+ const w = liveWaves(home).find((x) => x.pid === pid);
23
+ const store = w && RunStore.find(w.cwd, w.wave);
24
+ if (!store)
25
+ return null;
26
+ return store
27
+ .lots()
28
+ .filter((l) => WRITING.has(l.status) && lotRepoPaths(l).some((r) => real(r) === real(repo)) && !existsSync(lotTreeDir(store.dir, l.project, l.lot, l.repo)))
29
+ .map((l) => lotKey(l.project, l.lot));
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
@@ -25,14 +25,7 @@ export function groupLots(ctxs) {
25
25
  }
26
26
  return groups.map((g) => g.lots);
27
27
  }
28
- /**
29
- * Ordonnanceur : une file par dépôt, les lots d'un dépôt l'un après l'autre dans l'ordre donné, `slots` files au plus en parallèle. Deux lots qui partagent un dépôt
30
- * sont dans la même file : jamais deux sessions dans un même dépôt (le plafond global des sessions de toutes les vagues est tenu par command.ts).
31
- * `parallel(groupe)` (L279) : les lots de ce groupe sont chacun leur propre file — ils jouent ensemble, chacun dans son worktree, dans la limite de `slots` ;
32
- * un lot qui dépend d'un lot du groupe attend qu'il soit fini (ready après son intégration) avant de partir, et le lot en attente d'une place n'est pas commencé
33
- * (son worktree n'existe pas encore, R3). `onSettled` est appelé quand un lot a fini de jouer (L132 : command.ts libère alors les dépôts que plus aucun lot ne tient). Le budget, le quota et les incidents sont vérifiés par le cycle avant chaque session.
34
- */
35
- export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () => { }, parallel = () => false) {
28
+ export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () => { }, parallel = () => false, answers) {
36
29
  const pending = groupLots(ctxs).flatMap((g) => (parallel(g) ? g.map((c) => [c]) : [g]));
37
30
  // Les lots pas encore finis de jouer (en file ou en cours) : un lot ne part pas tant que l'un d'eux est sa dépendance.
38
31
  const live = new Set(ctxs);
@@ -82,12 +75,28 @@ export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () =>
82
75
  const dep = ctxs.find((o) => o.lot.project === queue[0].lot.project && o.lot.lot === id);
83
76
  return !!dep && live.has(dep) && dep.lot.status !== 'ready';
84
77
  });
85
- const running = new Set();
78
+ const running = new Map();
86
79
  const start = (queue) => {
87
80
  const p = runQueue(queue).finally(() => running.delete(p));
88
- running.add(p);
81
+ running.set(p, queue);
82
+ };
83
+ // Sans worktree, un lot repris ne joue pas pendant qu'un autre lot du même dépôt joue, attend un créneau ou vient d'être accepté dans ce même tour (jamais deux sessions dans un dépôt).
84
+ const admitted = [];
85
+ const sameRepo = (a, b) => lotRepoPaths(a.lot).some((r) => lotRepoPaths(b.lot).includes(r));
86
+ const canRun = (c) => {
87
+ if (c.tree)
88
+ return true;
89
+ if ([...running.values(), ...pending].some((q) => q.some((o) => sameRepo(c, o))) || admitted.some((o) => sameRepo(c, o)))
90
+ return false;
91
+ admitted.push(c);
92
+ return true;
89
93
  };
90
94
  for (;;) {
95
+ admitted.length = 0;
96
+ for (const c of answers?.take(canRun) ?? []) {
97
+ live.add(c);
98
+ pending.push([c]);
99
+ }
91
100
  while (running.size < slots) {
92
101
  const next = pending.findIndex((q) => !blocked(q));
93
102
  if (next < 0)
@@ -101,6 +110,12 @@ export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () =>
101
110
  start(pending.shift());
102
111
  continue;
103
112
  }
104
- await Promise.race(running);
113
+ if (!answers)
114
+ await Promise.race(running.keys());
115
+ else {
116
+ let timer;
117
+ await Promise.race([...running.keys(), new Promise((res) => (timer = setTimeout(res, answers.pollMs)))]);
118
+ clearTimeout(timer);
119
+ }
105
120
  }
106
121
  }
@@ -117,6 +117,19 @@ export function waveReadsControl(waveDir) {
117
117
  return false;
118
118
  }
119
119
  }
120
+ /**
121
+ * La vague lit-elle la file de réponses `answers.log` (L407) ? Une vague lancée avant L407 ne la lirait jamais : la réponse y dormirait. Sans instantané : on suppose que oui.
122
+ */
123
+ export function waveReadsAnswerQueue(waveDir) {
124
+ if (!snapshotExists(waveDir))
125
+ return true;
126
+ try {
127
+ return readFileSync(join(toolDirOf(waveDir), 'dist', 'orchestrate', 'state.js'), 'utf8').includes('answers.log');
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
120
133
  /**
121
134
  * L'instantané de la vague connaît-il `--budget projet:lot=+N` (L223) ? Un instantané plus ancien relit l'argument avec son `parseBudget` et le refuse
122
135
  * comme budget invalide. Sans instantané : rien à relire, on suppose que oui.
@@ -167,6 +167,42 @@ export class RunStore {
167
167
  const kept = readFileSync(f, 'utf8').split('\n').filter((l) => l.trim() !== '' && l.trim() !== 'stop-after-current');
168
168
  writeFileSync(f, kept.map((l) => `${l}\n`).join(''));
169
169
  }
170
+ /**
171
+ * File des réponses du lead à une vague vivante (L407) : `answers.log`, en ajout seul, une ligne JSON par réponse déposée (`{id, lot, text}`) ou par réponse prise (`{taken: id}`).
172
+ * La vague la relit entre deux étapes ; une réponse prise ou écartée n'y figure plus. Une ligne illisible est ignorée. Pas de `.json` : `lots()` le prendrait pour un lot.
173
+ */
174
+ answerQueue() {
175
+ const f = join(this.dir, 'answers.log');
176
+ if (!existsSync(f))
177
+ return [];
178
+ const entries = [];
179
+ const taken = new Set();
180
+ for (const line of readFileSync(f, 'utf8').split('\n')) {
181
+ try {
182
+ const o = JSON.parse(line);
183
+ if (typeof o.taken === 'number')
184
+ taken.add(o.taken);
185
+ else if (typeof o.id === 'number' && typeof o.lot === 'string' && typeof o.text === 'string')
186
+ entries.push({ id: o.id, lot: o.lot, text: o.text });
187
+ }
188
+ catch {
189
+ // ligne vide ou illisible : ignorée
190
+ }
191
+ }
192
+ return entries.filter((e) => !taken.has(e.id));
193
+ }
194
+ /** Dépose une réponse dans la file ; rend son numéro. */
195
+ requestAnswer(lot, text) {
196
+ const f = join(this.dir, 'answers.log');
197
+ const last = existsSync(f) ? [...readFileSync(f, 'utf8').matchAll(/^\{"id":(\d+),/gm)].map((m) => Number(m[1])) : [];
198
+ const id = Math.max(0, ...last) + 1;
199
+ appendFileSync(join(this.dir, 'answers.log'), `${JSON.stringify({ id, lot, text })}\n`);
200
+ return id;
201
+ }
202
+ /** La réponse `id` est prise (jouée, écartée ou retirée par son auteur) : elle ne sera plus relue. */
203
+ markAnswerTaken(id) {
204
+ appendFileSync(join(this.dir, 'answers.log'), `${JSON.stringify({ taken: id })}\n`);
205
+ }
170
206
  journal(line) {
171
207
  appendFileSync(join(this.dir, 'journal.log'), `${new Date().toISOString()} ${line}\n`);
172
208
  }
@@ -87,20 +87,16 @@ export function takeTiers(repos, log) {
87
87
  }
88
88
  /** Plus de commits que cela dans le constat : le reste est compté. */
89
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);
90
+ /** Le sujet d'un commit cite-t-il l'un de ces lots ? Identifiant entier : « L1 » n'est ni dans « L10 » ni dans « L1.2 », mais l'est dans « feat(L1): » ou « demo:L1 ». */
91
+ export function citesLot(subject, lots) {
92
+ return lots.some((id) => new RegExp(`(?<![\\w.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-]|\\.\\w)`).test(subject));
98
93
  }
99
94
  /**
100
95
  * 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é.
96
+ * supprimée ou déplacée sans commit nouveau. Seuls comptent les commits dont le sujet cite un lot de la vague (`lots`, L458) : une livraison (bump de tag de deploy.sh), un commit du lead
97
+ * ou d'une autre vague ne citent aucun d'eux et passent. Une branche absente d'un seul côté a bougé (créée ou supprimée pendant la session). Null : rien n'a bougé.
102
98
  */
103
- export function tierMoved(repo, before, after, planFile) {
99
+ export function tierMoved(repo, before, after, lots) {
104
100
  for (const branch of [...new Set([...Object.keys(before.mains), ...Object.keys(after.mains)])].sort()) {
105
101
  const b = before.mains[branch];
106
102
  const a = after.mains[branch];
@@ -111,9 +107,9 @@ export function tierMoved(repo, before, after, planFile) {
111
107
  // branche créée : seuls comptent les commits que aucune branche principale d'avant ne portait déjà
112
108
  const range = b ? [`${b}..${a}`] : [a, '--not', ...Object.values(before.mains)];
113
109
  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));
110
+ const lines = all.filter((l) => citesLot(l.slice(l.indexOf(' ') + 1), lots));
115
111
  if (all.length && !lines.length)
116
- continue; // que des commits du lead
112
+ continue; // aucun commit ne cite un lot de la vague
117
113
  if (!lines.length && !b)
118
114
  return { branch, detail: `branche créée (${a.slice(0, 7)})` };
119
115
  if (!lines.length)
@@ -93,7 +93,7 @@ Before the first reading the panel says `Lecture des plans…`; if `cadence lead
93
93
 
94
94
  **Last delivery of each project (L415).** Under the lots in progress, `Dernière livraison par projet (N)` shows one line per project that has delivered: `✔ finance 3f9a1c2 · il y a 2 j · verte · 2 lots clos non livrés : L12, L13`, or `✖ … · rouge` in red when the last delivery failed; `rien à livrer` when every closed lot was delivered, and nothing after `verte` when the plan is not known. It reads the journal `cadence deliver` writes (`.git/cadence/deliveries.log`, one `day sha` line per green delivery, and `deliveries-failed.log`) through the `LAST_DELIVERIES` script (python3, the folder and each direct sub-folder whose `.git` is a folder, like the failed-delivery script; run at each 5 s refresh **while the panel is open**): the last delivery is the red one when `deliveries-failed.log` was modified after `deliveries.log` (the files' modification date decides, as for the failed deliveries), the green one otherwise; a repository with no readable line is absent. The age counts days from the journal's day (the journal has no hour): `aujourd’hui`, `hier`, `il y a N j`. **Closed lots not delivered**: `cadence lead tour --json` gives each project row `closed`, the closed lots (`done`, archived ones included, lots with an `every:` left out) as `{ id, finished }`; a lot is delivered when a green delivery happened on the day it was closed or after (the rule of the delivery delay in `cadence perf`), so it is not delivered when closed **after the day of the last green delivery** — a red delivery delivers nothing, and a project that never delivered green counts all its closed lots. A project absent from the tour, in error, or read by a `cadence` too old to give `closed` shows no count (not « rien à livrer »: we do not know). Order: red first, then the most undelivered lots, then the name. Height (`deliveriesHeight`): the title and one line per project, within what the decisions and the lots in progress leave of the half of the body, `… +N projets` when they do not all fit, nothing under two lines; the tree keeps the rest. Calculation: `hooks/deliveries.ts` (`deliveriesOf`, `buildDeliveries`, `parseLastDeliveries`). Tested by `tests/deliveries.test.ts` (L415 R4 to R11), `tests/lots-pane.test.ts` (R12 to R14, panel mounted), `test/hud-livraisons-l415.test.ts` (R2, R3: the real script on real journals) and `test/lead-tour-lots.test.ts` (R1: the `closed` field).
95
95
 
96
- **What is waiting for a decision (L412).** Above the tree, a block `Ce qui attend une décision (N)` gathers what the person has to settle; it exists only when there is something to settle. It reads the band's waves (live ones, and the last finished one only when none is live), the finished waves of the folder that the collector returns apart in `ended` (whether a wave is live or not, without the band's 30-minute delay: **all** the finished waves of the last 7 days, with no cap on their number (a cost guard of 500 waves, far above the 162 waves per 7 days measured, is not a rule), newest first by the date of their `wave.json` — a question or a handed-back lot waits as long as the plan keeps the lot open and its wave is less than 7 days old (beyond that, the decision leaves the block even if the lot is still open in the plan), a newer wave finishing without that lot does not erase it; a live wave is not among them: its `wave.json`, always the most recent of the folder since orchestrate rewrites `consumed` in it, is skipped by the collector; the band and the tree keep reading `waves` only, their drawing does not change) and the failed deliveries; a lot counts once, in the first wave carrying it, live waves first then the finished ones newest first (taken over in a newer wave, that wave decides). Four kinds: `? project:lot <questions>` (a lot in the `question` state; the short text is the first line of each question, joined by ` / `) with, below it in grey, the command that answers it (`cd <launch folder of the wave> && cadence orchestrate --resume --wave <wave> --answer project:lot "…"`, the one the end-of-wave table prints, preceded by a `cd` to the wave's `cwd`: orchestrate finds the wave only from there) — while the wave is live, no command but `réponse possible à la fin de la vague`, since orchestrate refuses to resume a wave that is still running; `↩ project:lot <cause>` (a `handed-back` or `failed` lot: the first line of its outcome, the status in words when empty; a `suspended` lot waits for nothing from the person), in red; `◐ project:lot verdict UX : <verdict>` (a `ready`, visible lot whose review returned a verdict) with `cd <repository of the lot> && raf ux <lot> "verdict"` below (the `repo` of the lot state: from the parent folder raf answers « pas de plan », from another project it would record the verdict on that project's lot of the same id) — the commands to copy start with `cd` and run as they are from any folder, a path with a space or a quote is quoted for sh; `✖ project livraison : <short sha> · <reason> (<day>)` in red (7 characters: deliver writes the full 40-character sha in the log). The block never takes more than half of the body (3 lines at least): past that, the first decisions that fit, then `… +N décisions de plus`. Failed deliveries come from the `DELIVERIES` script (collect.ts, python3), run at every 5 s refresh **while the panel is open**: for the folder and each direct sub-folder whose `.git` is a directory, the last line of `.git/cadence/deliveries-failed.log`, unless `deliveries.log` was modified after it (a green delivery clears the failure; file modification times decide, the day on the line cannot order two writes of the same day). The plan decides: the panel's board (read from `cadence lead tour --json`) drops any decision whose lot is no longer open in a project the tour read, so a UX verdict recorded then `raf done`, or a handed-back lot taken over and closed, disappears at the next plan read; a project missing from the board, in error or without lots keeps its decisions. The command to copy is wrapped over several lines at the panel's width, never cut, every line but the last ending with the shell continuation ` \` (`\` glued to the piece when a word longer than the line is cut): pasted as they are into a shell, the lines run one command, the whole one (without it, a first line `cadence orchestrate --resume --wave <wave>` ran alone, without its `--answer`); the wrapped lines count in the block's ceiling. Details in the plugin's README.
96
+ **What is waiting for a decision (L412).** Above the tree, a block `Ce qui attend une décision (N)` gathers what the person has to settle; it exists only when there is something to settle. It reads the band's waves (live ones, and the last finished one only when none is live), the finished waves of the folder that the collector returns apart in `ended` (whether a wave is live or not, without the band's 30-minute delay: **all** the finished waves of the last 7 days, with no cap on their number (a cost guard of 500 waves, far above the 162 waves per 7 days measured, is not a rule), newest first by the date of their `wave.json` — a question or a handed-back lot waits as long as the plan keeps the lot open and its wave is less than 7 days old (beyond that, the decision leaves the block even if the lot is still open in the plan), a newer wave finishing without that lot does not erase it; a live wave is not among them: its `wave.json`, always the most recent of the folder since orchestrate rewrites `consumed` in it, is skipped by the collector; the band and the tree keep reading `waves` only, their drawing does not change) and the failed deliveries; a lot counts once, in the first wave carrying it, live waves first then the finished ones newest first (taken over in a newer wave, that wave decides). Four kinds: `? project:lot <questions>` (a lot in the `question` state; the short text is the first line of each question, joined by ` / `) with, below it in grey, the command that answers it (`cd <launch folder of the wave> && cadence orchestrate --resume --wave <wave> --answer project:lot "…"`, the one the end-of-wave table prints, preceded by a `cd` to the wave's `cwd`: orchestrate finds the wave only from there) — the same command while the wave is live (L407: orchestrate then drops the answer in the wave's queue, which the live wave reads between two steps, instead of refusing « la vague tourne encore »); `↩ project:lot <cause>` (a `handed-back` or `failed` lot: the first line of its outcome, the status in words when empty; a `suspended` lot waits for nothing from the person), in red; `◐ project:lot verdict UX : <verdict>` (a `ready`, visible lot whose review returned a verdict) with `cd <repository of the lot> && raf ux <lot> "verdict"` below (the `repo` of the lot state: from the parent folder raf answers « pas de plan », from another project it would record the verdict on that project's lot of the same id) — the commands to copy start with `cd` and run as they are from any folder, a path with a space or a quote is quoted for sh; `✖ project livraison : <short sha> · <reason> (<day>)` in red (7 characters: deliver writes the full 40-character sha in the log). The block never takes more than half of the body (3 lines at least): past that, the first decisions that fit, then `… +N décisions de plus`. Failed deliveries come from the `DELIVERIES` script (collect.ts, python3), run at every 5 s refresh **while the panel is open**: for the folder and each direct sub-folder whose `.git` is a directory, the last line of `.git/cadence/deliveries-failed.log`, unless `deliveries.log` was modified after it (a green delivery clears the failure; file modification times decide, the day on the line cannot order two writes of the same day). The plan decides: the panel's board (read from `cadence lead tour --json`) drops any decision whose lot is no longer open in a project the tour read, so a UX verdict recorded then `raf done`, or a handed-back lot taken over and closed, disappears at the next plan read; a project missing from the board, in error or without lots keeps its decisions. The command to copy is wrapped over several lines at the panel's width, never cut, every line but the last ending with the shell continuation ` \` (`\` glued to the piece when a word longer than the line is cut): pasted as they are into a shell, the lines run one command, the whole one (without it, a first line `cadence orchestrate --resume --wave <wave>` ran alone, without its `--answer`); the wrapped lines count in the block's ceiling. Details in the plugin's README.
97
97
 
98
98
  The data is the `lots` field of `cadence lead tour --json` ([lead tour](lead.md#lead-tour)), read together with the band's `progress` (same call, same folder). While the panel is open, the modification date of each plan (`.cadence/raf.yaml` and `cadence.yaml` of the folder and of its direct sub-folders) is read every 5 s, one `stat` each: a changed plan, like a wave transition, triggers a new reading before the minute is over. Cost of the `lots` field (R18, measured on 2026-10-28 with `cadence lead tour --json` on the parent folder of ten projects, three passes each): before L401 (cadence 1.19.1, no `lots`) 8.0 / 9.1 / 7.3 s and 86 KB of output; after (the L401 code) 7.5 / 7.0 / 8.4 s and 188 KB (192,503 bytes). The output more than doubles in size but the duration stays in the same order, under the 20 s refresh delay of the band, so the rule holds.
99
99
 
package/docs/deliver.md CHANGED
@@ -172,6 +172,15 @@ itself never reads the working tree: a tracked file modified outside the deliver
172
172
  sha (the plan after `raf done`, say) neither refuses nor is reported, with or
173
173
  without `allowDirty`.
174
174
 
175
+ **Outside a wave: the lots under the sha (L474).** `--sha` also refuses, with no wave running, a sha whose history carries work commits of a lot of the
176
+ plan that is neither done nor ready — a commit cited « L49 » that sits above commits of L50 and L51 delivers them too (30-10: `09217b0` « L49 alone »
177
+ shipped two lots nobody had reviewed). The interval checked runs from the last delivery of the journal (when it is in the history of `<sha>`) up to
178
+ `<sha>`; with no such delivery, the whole history, the plan's adoption day and the plan-only, acknowledged and release commits left out as everywhere. A lot passes
179
+ when it is `done` or dropped, or **ready**: its code review verdict is compliant and covers all its commits (a verdict reading `non conforme` / `not compliant` keeps the lot back, state `relu non conforme`). A lot to do or in progress, not reviewed or reviewed before later commits, holds the sha back; the
180
+ refusal (exit 2, before anything runs) names each such lot with its state. `--dry-run` prints every lot of the interval with its state (`terminé`, `prêt`,
181
+ `abandonné`, `en cours, non relu`, `à faire, non relu`, `en cours, relu avant N commit(s)`, `en cours, relu non conforme`) after the same refusal checks, and refuses the same way when one blocks. A plain `cadence deliver` (HEAD) and a delivery during a wave (the
182
+ wave's own guard, below) do not run this check; a commit that cites a lot the plan does not know (archived, unknown) makes no lot.
183
+
175
184
  **While a wave runs.** `cadence orchestrate` holds the repository: a plain `cadence deliver` is refused until every lot of the
176
185
  repository in the wave is ready or handed back. `--sha` is the exception (L277): it is accepted while the wave runs when no
177
186
  commit in the history of `<sha>` belongs to a lot of the wave that is not ready (running, queued, in question, suspended,
package/docs/lead.md CHANGED
@@ -55,7 +55,9 @@ picture, not its source.
55
55
  question from a session comes back to the human at once; the answer goes in with
56
56
  `cadence orchestrate --resume --wave <wave> --answer <project>:<lot> "…"` (the wave is named: the
57
57
  command is printed under each question of the table, ready to run; without a wave, `--resume` is refused as
58
- soon as the folder keeps several unfinished waves). The lead never answers in the
58
+ soon as the folder keeps several unfinished waves). While the wave is still running the same command (or
59
+ `cadence orchestrate --answer <project>:<lot> "…"`) queues the answer and the live wave replays the lot at
60
+ once, whatever the other lots do (L407). The lead never answers in the
59
61
  human's place. `--drop` and `--stop-after-current` steer a wave that is running.
60
62
  4. **Review.** The final table of the wave lists the lots ready, handed back, and the line
61
63
  « livrable jusqu'à <sha> » for a ready lot stacked under a handed-back one. Minor findings come back in
@@ -35,6 +35,7 @@ cadence orchestrate --resume … --dry-run # refused: a
35
35
  # a resume running on the wave's snapshot receives `--resume <wave>` in place of `--wave <wave>`, so an older snapshot cannot pick another wave; a wave whose id looks like a lot (`suite2`) cannot be named that way, so it is resumed with the current `dist/` and templates
36
36
  cadence orchestrate --drop ccc:L29 [--wave <wave>] # from another terminal: take a lot out of the running wave
37
37
  cadence orchestrate --stop-after-current [--wave <wave>] # from another terminal: let the running sessions finish, start nothing else
38
+ cadence orchestrate --answer ol-companion:L22 "reply" [--wave <wave>] # from another terminal: answer a lot of the running wave that is waiting for it (below)
38
39
  ```
39
40
 
40
41
  **Claude Code must be installed and logged in** (L378). Before starting a lot, `cadence orchestrate` runs `claude auth status` (no model, no cost); when it says `loggedIn: false` the command is refused (exit 2) before anything is written: no `raf start`, no plan commit, the lot stays `todo`. The message says what to do: « claude n'est pas connecté : lancer `claude` puis /login (état : `claude auth status`) ». `--dry-run` starts no session, so it is not refused: it prints the same message after its first line, as a `⚠`. `claude auth status` is read once per launch, so with `--continue` the rounds of a wave do not run it again for every candidate lot. An unreadable answer (a `claude` without `auth status`) is not a refusal.
@@ -60,6 +61,8 @@ this way is left out of the resume checks (repository lock, `.nvmrc` Node, dirty
60
61
  refuse the resume, and a refusal writes nothing to `control.log`.
61
62
  `--resume --wave <wave> --stop-after-current` is refused (exit 2): resuming contradicts stopping.
62
63
 
64
+ **Answering on a live wave (L407).** `--answer <project:lot> "…"` (from the folder that launched the wave, `--wave` when several are live) no longer waits for the wave to end: on 28-10 a lot waited about 15 minutes for its answer because another lot of the wave was still running, and `--resume` refuses a wave that is running. The answer is appended to the wave's queue (`answers.log` in `.cadence/runs/<wave>/`, one JSON line per answer and per answer taken), the command prints `<lot> : réponse déposée` and returns at once. The live wave reads the queue every 2 seconds and each time a lot finishes, applies the answer to the lot (the same state as `--resume --answer`: the answer joins the brief of its next session) and replays it as one more line of the pool, as soon as a session slot is free; a lot with no worktree never plays together with another lot of its repository. The documented form `--resume --wave <wave> --answer …` does the same when that wave is still running (it used to be refused with `tourne encore`); combined with another resume option (`--budget`, `--drop`, `--integrate`, `--continue`) it is still refused. Refused (exit 2) before anything is written: a lot that is unknown, that several projects carry, or that is not waiting for an answer (`question`); no live wave in that folder (the message gives the `--resume --wave <wave> --answer` form for a stopped wave); a wave started before L407, whose snapshot does not read the queue. `--answer` alone does not combine with `--drop`, `--stop-after-current`, `--budget`, `--dry-run`, `--continue` or lots to launch. An answer whose lot is no longer waiting when the wave reads it (dropped, or already answered by an earlier line) is discarded and the journal says so (`réponse écartée`), it is never played twice; if the wave finishes between the check and the write, the answer is withdrawn and the command says to use `--resume`. Not covered: a lot that depends (`after`) on the answered lot and was left waiting in the pool is not started by this wave when the answer makes the lot ready; `--resume` plays it.
65
+
63
66
  **`--goal "…"` and `goal:` — the objective of the briefs when the title is stale (L258)**: the first line of the `{{objectif}}` of every
64
67
  brief of a lot (and the title quoted in their first sentence) is the lot's title; when a change of scope made it stale, the lead no longer
65
68
  has to delegate by hand. Three ways to give the goal, in this order of precedence (`--goal`, then the plan's key, then the title):
@@ -999,10 +1002,13 @@ main branch of any other repository**, whether the lot's text cites it or not: o
999
1002
  `wip/<project>-<lot>` in that repository, commit on it, never push, and say it under « choix ».
1000
1003
 
1001
1004
  **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
1005
+ the sha of the main branches of the other repositories (the commits it reports are filtered, see below). The main branches of a repository are, among its local branches, the one `origin/HEAD` names, `main` and
1003
1006
  `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
1007
  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
1008
+ est la branche wip/<project>-<lot>`. **Only a commit whose subject cites a lot of the wave is reported** (`feat(L1): chart cluster-sante`; the whole identifier counts, `L10` and `L1.2` do not cite `L1`; the
1009
+ body of the message is not read). Every other commit on that main branch passes: a delivery (the tag bump of a `deploy.sh` in a GitOps repository), a commit of the lead
1010
+ (`plan: …`), a commit of another wave, whenever it was launched, a commit that cites no lot. The rule needs no registry of the waves, so it holds
1011
+ for a wave started after the session began. The repository is marked dirty for the wave. A branch created or deleted during the session counts as a moved sha. The comparison
1006
1012
  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
1013
  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
1014
  successful one: otherwise the next attempt would take the moved sha as its reference and the commit would never be named.
@@ -1014,10 +1020,12 @@ That makes every sibling repository, cited by the lot or not.
1014
1020
 
1015
1021
  - 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
1022
  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);
1023
+ - the repositories of the **other lots of the same wave**, and their neighbours: their main branch moves legitimately through their own sessions and their integration, and
1024
+ those commits cite the lots of the wave; likewise those of the lots of a wave **running at the same time** that the registry shows when the session starts (two projects
1025
+ can both have a `L1`: the citation alone would take the other wave's commits for the lot's own). A wave launched later is not in that list: its commits pass because
1026
+ they cite no lot of this wave;
1027
+ - the commits that **cite no lot of the wave** (see above): deliveries, the lead's `plan: ` commits, another wave's commits. A branch deleted, created or moved with no new
1028
+ commit is still reported;
1021
1029
  - 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
1030
  - 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
1031
  - 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
package/docs/overview.md CHANGED
@@ -80,7 +80,7 @@ Also: [news](news.md) (changelog, UX, code and QA reviews, documentation that fo
80
80
  - **raf** (French *reste à faire*, "what is left to do"): the plan, linked to your commits by the lot id in the message, computed from `git log` and never stored.
81
81
  - **news**: a user-facing changelog with screenshots, tied to the plan; a lot is not done until its entry is written.
82
82
  - **session**: `session start` and `session close` gather the facts of a work session (each lot in progress shows its commit count, `0 commit` included, next to its last activity).
83
- - **deliver**: wait for the CI of the pushed commit, run the deploy commands, then **verify the effect**; a delivery is done only when its checks pass. A failed delivery is logged as a *red delivery* with its class (refusal, CI, deploy, check) and the last lines of the failing command's output. While a wave runs, `cadence deliver --sha <sha>` is accepted when no commit of the sha belongs to a lot that is not ready, so a ready lot ships while the wave runs.
83
+ - **deliver**: wait for the CI of the pushed commit, run the deploy commands, then **verify the effect**; a delivery is done only when its checks pass. A failed delivery is logged as a *red delivery* with its class (refusal, CI, deploy, check) and the last lines of the failing command's output. While a wave runs, `cadence deliver --sha <sha>` is accepted when no commit of the sha belongs to a lot that is not ready, so a ready lot ships while the wave runs. Outside a wave, `--sha` refuses a range that holds commits of a lot that is neither done nor ready (its code review covering its commits up to the sha, not above), and `--dry-run` lists the lots of the range with their state (L474).
84
84
  - **auto-verification** (L236): before it reports, an `implement` or `fix` session re-reads the complete diff of its lot (of its fix commits, in `fix`) against the grid of the `code-reviewer` (code bug, architecture drift, architecture documentation, edge cases, one test per changed behaviour), fixes what it finds and writes an « auto-vérification » paragraph in its report; the measure is `cadence corrections --classify` and the share right the first time of `cadence perf`.
85
85
  - **real walkthrough and decisions** (L344): once the tests are green an `implement` session plays what it delivered as a user (the command in a throwaway repository following its own documentation, the screen in each state with the failure in view) and reports it under « parcours réel »; the notes of the lot and of its parent that record a decision of the human reach the `implement` and `fix` briefs under « Decisions to respect », a departure being a question, never a choice (`docs/orchestrate.md`).
86
86
  - **perf**: `cadence perf` is the dashboard of the workflow (speed next to quality, `⚠` on a trend), including the red deliveries and the blocking QA findings; `cadence lead tour` opens with its *perf header*, one line per `⚠`, nothing when stable; a rise of the tokens per lot names the step that carries it, and when the share right the first time rose too it reads as quality paid for, not a wrong estimate ([docs/perf.md](perf.md)).
package/docs/raf.md CHANGED
@@ -60,6 +60,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
60
60
  | `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) |
61
61
  | `raf move <id> --before\|--after <id>` | reorder a lot in the plan file (L400): `<id>` is placed right before or right after the other lot, the others keep their order, and the lot travels with all its keys, its sub-tasks and its comments. Only among siblings: a lot among lots, a sub-task (`L1/t1`) among the sub-tasks of its own lot (`move : L2 et L1/t1 n'ont pas le même parent …`, plan unchanged). Exactly one of `--before` / `--after`, one id (else `usage : raf move <id> --before <id> \| raf move <id> --after <id>`); a lot placed where it already is, or an unknown id (`lot inconnu : <id>`), changes nothing. **Refused** on a read-only plan, plan kept by another tool (`plan en lecture seule : … — le modifier avec l'outil du projet`: reorder it with the project's own tool), on an archived lot (`move : L1 est archivé …`: the archive is presented in its former order and the order of the plan file never moves, see `raf archive`) and while a wave holds the repository (`move : orchestration en cours (vague …, pid …) — attendre …`: the lots of the wave rely on the order they read; the lock falls as soon as all of them are ready or handed back). The order is the order of the file, so it is the order `raf list`, `raf now`, the Gantt page and the draw of `cadence orchestrate` follow; the command only writes the plan, it does not commit it (`plan.commit` prints the command, as for every plan write) |
62
62
  | `raf add`, `raf note`, `raf done` during an integration (L425) | while a wave integrates a lot of the repository (the integration lock is held by a live process), these three commands write the plan as usual and print on stderr `avertissement : intégration en cours (vague …, pid …) — …`: the plan is a tracked file of the main tree, and an integration that sees it modified refuses its lot (`arbre du dépôt modifié`, 2026-10-29: a `raf add` of the lead during the integration of L416). Commit the plan right away. Exit code 0; nothing is printed in a lot's worktree (its plan is not the tree the integration checks) nor when the lock belongs to a dead process |
63
+ | `raf add`, `raf note`, `raf done` while a wave works in the main tree (L475) | when the repository's orchestration lock is held by a live wave and one of its lots is in `implementing`, `reviewing` or `fixing` **without a worktree** (a wave without worktrees, or a lot whose worktree folder does not exist), these commands write the plan as usual and print on stderr `avertissement : la vague … travaille dans l'arbre principal de ce dépôt (projet:lot) — …`: a plan commit of the lead then hands the lot back at review (2026-10-30: cadence L458 handed back by a plan commit during its review). Write the plan without committing it, or commit once the step ends. Exit code 0; nothing is printed for lots that are queued, in question, ready, handed back, failed or suspended, for lots playing in their worktree, from a worktree of the repository, from a session of the wave itself (`CADENCE_ORCHESTRATED` set), when the lock belongs to a dead process, or when the wave's state is unreadable |
63
64
  | `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
64
65
  | `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) |
65
66
  | `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) |
package/docs/session.md CHANGED
@@ -24,7 +24,10 @@ cadence session context # `ctx 42 % (84000/200000)`: the context of Y
24
24
  # `seuil 150000 tokens : sous le seuil` or `… : atteint` (tokens ≥ threshold)
25
25
  ```
26
26
 
27
- `lead.contextThreshold` is read in the `cadence.yaml` of the folder where `session context` runs (the lead's folder): an integer ≥ 1,
27
+ `lead.contextThreshold` is read in the `cadence.yaml` of the lead's folder: the folder where `session context` runs, or else the first
28
+ parent whose `cadence.yaml` carries a `lead:` or `priority:` key (run from a sub-project it no longer falls back on the default). When it
29
+ is read in a parent, the verdict line names the file: `seuil 500000 tokens : sous le seuil (lu dans /…/cadence.yaml)`. With no such
30
+ parent, the current folder and the default apply. The value is an integer ≥ 1,
28
31
  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
32
  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
33
  smaller than the threshold the verdict never reads `atteint`, so set a lower one there.
package/docs/whats-new.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.31.0**: `deliver --sha` refuses, outside a wave too, a sha that carries commits of a lot neither done nor ready (a `non conforme` verdict is not ready); `raf add/note/done` warn when a wave session writes in the main tree.
8
+ - **1.30.0**: a question can be answered while its wave still runs; the sibling-repository check of 1.29.0 only flags commits that cite a lot of the wave; `session context` reads the lead's threshold from the lead's folder.
7
9
  - **1.29.0**: a wave session can no longer commit on the main branch of another repository than its lot's (the lot is handed back with the finding); `verify --gate` no longer links data folders into its throwaway copy.
8
10
  - **1.28.0**: projects are reordered from the lots panel or `cadence lead priority`; the lead's context threshold is in tokens; reviewers answer the lead in a few lines; the gate replays under plan commits when tests read the plan.
9
11
  - **1.27.0**: `cadence corrections --classify` names the sub-cause of each code-bug pass and the dominant link of the chain, `cadence perf` shows it; the mockup review checks hierarchy, tokens, texts and extra accessibility.
@@ -56,6 +58,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
56
58
 
57
59
  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).
58
60
 
61
+ **1.31.0**: `cadence deliver --sha <sha>` refuses, outside a wave too, a sha whose history since the last delivery carries work commits of a lot that is neither done nor ready — ready meaning a compliant code review that covers the lot's commits up to that sha, so a fix stacked above a reviewed sha does not block it and a `non conforme` verdict does — and `--dry-run` lists the lots of the interval with their state (L474, `docs/deliver.md`). Before, the check only ran during a wave of the repository: on 30-10 a sha delivered as « L49 alone » took two unreviewed lots whose commits sat beneath it. `raf add`, `raf note` and `raf done` warn on stderr when a session of a live wave writes in the main tree of the repository (L475, `docs/raf.md`): a plan commit of the lead at that moment hands the lot back, as it did on 30-10.
62
+
63
+ **1.30.0**: `cadence orchestrate --answer <project>:<lot> "…"` (or `--resume --wave <wave> --answer`) is accepted while the wave is still running (L407, `docs/orchestrate.md`): the answer goes into a queue the live wave reads, the lot takes a slot again under the wave's cap, plays in its own worktree when the mode is on, and never runs beside another session of the same repository; the registry counts it at once, so « créneaux libres » stays right. Before, the lead had to wait for the end of the wave. The check of 1.29.0 against commits on the main branch of a sibling repository (L456) now flags only the commits that cite a lot of the wave (L458): a delivery's tag bump in a GitOps repository, the lead's plan commits and the lots of another live wave launched later no longer hand a lot back — three false positives on 30-10. `cadence session context` reads `lead.contextThreshold` from the lead's folder, the first parent holding `lead:` or `priority:`, instead of the current folder (L457).
64
+
59
65
  **1.29.0**: two incidents of 30-10 turned into mechanisms. A writing session of a wave no longer commits on the main branch of a repository its lot does not declare (L456, `docs/orchestrate.md`): the brief forbids it and names the way, a branch `wip/<project>-<lot>`, and at the end of each session the program compares the main branches of the sibling repositories of the folder; a commit found there hands the lot back with the repository, the branch and the shas, and never moves or reverts anything — the lead moves the commits. Commits of the lead (subject `plan:`, plan file only) and commits of lots of another live wave of the folder are tolerated; repositories declared by the lot (`repos:`) keep their behaviour. The case was a session that committed a chart on the production branch of a GitOps repository. `cadence verify --gate` no longer links the ignored folders of the main tree that are not dependencies (`data/`, `data-local/`, `.env`) into its throwaway copy (L455, `docs/deliver.md`): a test comparing paths by `realpath` went red in the copy and green in the main tree at the same sha, and stopped a delivery.
60
66
 
61
67
  **1.28.0**: the lead spends less of its context, and the projects are ordered where they are read. `cadence lead priority <project> --before|--after <project>` rewrites the `priority:` list of the folder's `cadence.yaml`, comments kept, and the lots panel does the same by drag and drop or Alt+↑/↓ in a region « ordre des projets » above the tree (L449, `docs/lead.md`, `docs/cadence-hud.md`); a move it cannot write back faithfully (a name that prefixes another) is refused with its reason. The lead's context threshold is a number of tokens, `lead.contextThreshold` (150000 by default), read by `cadence session context` (L437). `ux-reviewer`, `doc-keeper` and the reviewers answer the lead in a few lines and keep the full report in the file named by `Report file:` (L452), and `raf` cuts the titles it echoes to 60 characters (L453). `orchestrate.gate.readsPlan` lets a project whose tests read the plan say so: `cadence verify --gate` then replays the suite under plan-only commits instead of taking over the wave's verdict (L450). A lot made `visible` by its own session during a wave now goes through the UX step instead of coming out ready without it (L454).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvain Ladoire",