@sylad/cadence 1.40.0 → 1.42.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.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/spec-writer.md +1 -0
  4. package/agents/ux-reviewer.md +2 -0
  5. package/dist/archive.js +103 -6
  6. package/dist/audit.js +2 -2
  7. package/dist/clean.js +0 -4
  8. package/dist/cli/context.js +129 -0
  9. package/dist/cli/deliver-commands.js +114 -0
  10. package/dist/cli/gates.js +234 -0
  11. package/dist/cli/lead-commands.js +161 -0
  12. package/dist/cli/lot-commands.js +257 -0
  13. package/dist/cli/read-commands.js +227 -0
  14. package/dist/cli/session-commands.js +152 -0
  15. package/dist/cli/setup-commands.js +72 -0
  16. package/dist/cli/spec-commands.js +298 -0
  17. package/dist/cli/stats-commands.js +138 -0
  18. package/dist/cli-add.js +37 -0
  19. package/dist/cli.js +85 -1669
  20. package/dist/config.js +21 -108
  21. package/dist/corrections.js +28 -42
  22. package/dist/deliver-lots.js +37 -0
  23. package/dist/deliver.js +15 -39
  24. package/dist/docs-ok-file.js +25 -0
  25. package/dist/docsync.js +15 -3
  26. package/dist/lead.js +6 -24
  27. package/dist/lot-lines.js +51 -0
  28. package/dist/orchestrate/attente.js +82 -0
  29. package/dist/orchestrate/command.js +26 -25
  30. package/dist/orchestrate/continue.js +6 -14
  31. package/dist/orchestrate/cycle.js +18 -27
  32. package/dist/orchestrate/integrate.js +7 -2
  33. package/dist/orchestrate/integration-vivante.js +138 -0
  34. package/dist/orchestrate/launch.js +6 -18
  35. package/dist/orchestrate/lock.js +3 -20
  36. package/dist/orchestrate/lot-budget.js +2 -2
  37. package/dist/orchestrate/maquette.js +3 -0
  38. package/dist/orchestrate/registry.js +8 -1
  39. package/dist/orchestrate/schemas.js +5 -1
  40. package/dist/orchestrate/snapshot.js +13 -0
  41. package/dist/orchestrate/spec.js +51 -6
  42. package/dist/orchestrate/state.js +17 -0
  43. package/dist/plan-extras.js +119 -0
  44. package/dist/plan-ids.js +95 -0
  45. package/dist/plan-read.js +146 -0
  46. package/dist/plan-states.js +119 -0
  47. package/dist/plan.js +119 -190
  48. package/dist/raf-close-checks.js +25 -0
  49. package/dist/raf-status.js +28 -0
  50. package/dist/ref/command.js +2 -3
  51. package/dist/resources.js +3 -9
  52. package/dist/state.js +31 -8
  53. package/dist/verify-gate.js +33 -2
  54. package/dist/verify.js +20 -3
  55. package/dist/yaml-keys.js +21 -0
  56. package/docs/cadence-hud.md +1 -1
  57. package/docs/deliver.md +25 -3
  58. package/docs/fonctionnement.md +9 -5
  59. package/docs/lead.md +9 -7
  60. package/docs/news.md +2 -0
  61. package/docs/orchestrate.md +24 -7
  62. package/docs/perf.md +1 -0
  63. package/docs/qualite.md +14 -4
  64. package/docs/raf.md +62 -7
  65. package/docs/spec.md +5 -2
  66. package/docs/whats-new.md +9 -0
  67. package/package.json +1 -1
  68. package/skills/lead/SKILL.md +1 -1
  69. package/templates/orchestrate/spec.md +2 -2
@@ -0,0 +1,82 @@
1
+ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { processStart } from '../proc.js';
4
+ import { holderAlive } from './lock.js';
5
+ import { lotKey } from './state.js';
6
+ import { priorityRank, readPriority } from './continue.js';
7
+ const waitersDir = (home) => join(home, 'waiters');
8
+ let waiterSeq = 0;
9
+ /** Les attentes vivantes, toutes vagues confondues, de la première servie à la dernière : étape en cours avant nouveau tirage, puis rang de priorité, puis ancienneté. */
10
+ function liveWaiters(home) {
11
+ let names;
12
+ try {
13
+ names = readdirSync(waitersDir(home)).filter((n) => n.endsWith('.json'));
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ const out = [];
19
+ for (const n of names) {
20
+ const file = join(waitersDir(home), n);
21
+ try {
22
+ const r = JSON.parse(readFileSync(file, 'utf8'));
23
+ const w = {
24
+ pid: Number(r.pid),
25
+ start: r.start === undefined ? undefined : String(r.start),
26
+ wave: String(r.wave),
27
+ since: Number(r.since),
28
+ seq: Number(r.seq),
29
+ fresh: !!r.fresh,
30
+ rank: Number.isFinite(Number(r.rank)) ? Number(r.rank) : Number.MAX_SAFE_INTEGER,
31
+ file,
32
+ };
33
+ if (holderAlive(w.pid, w.start))
34
+ out.push(w);
35
+ else
36
+ rmSync(file, { force: true }); // le processus est mort sans désinscrire son attente
37
+ }
38
+ catch {
39
+ // inscription illisible (en cours d'écriture) : ignorée à ce tour
40
+ }
41
+ }
42
+ return out.sort((a, b) => Number(a.fresh) - Number(b.fresh) || a.rank - b.rank || a.since - b.since || a.pid - b.pid || a.seq - b.seq);
43
+ }
44
+ /** Inscrit une attente de créneau le temps de `run`, qui reçoit la question « est-ce mon tour ? » ; désinscrite à la fin, obtenue ou abandonnée (L531). */
45
+ export async function whileWaiting(home, o, run) {
46
+ const me = {
47
+ pid: process.pid,
48
+ start: processStart(process.pid) ?? undefined,
49
+ wave: o.wave,
50
+ since: Date.now(),
51
+ seq: waiterSeq++,
52
+ fresh: o.fresh ?? false,
53
+ rank: o.rank ?? Number.MAX_SAFE_INTEGER,
54
+ };
55
+ const file = join(waitersDir(home), `${me.pid}-${me.seq}.json`);
56
+ mkdirSync(waitersDir(home), { recursive: true });
57
+ writeFileSync(file, JSON.stringify(me));
58
+ try {
59
+ return await run(() => liveWaiters(home)[0]?.file === file);
60
+ }
61
+ finally {
62
+ rmSync(file, { force: true });
63
+ }
64
+ }
65
+ /** Un créneau est à prendre quand le total est sous le plafond et qu'aucune attente vivante ne passe avant la nôtre. */
66
+ export const hasRoom = (taken, limit, myTurn) => taken() < limit && myTurn();
67
+ /**
68
+ * La place dans la file des créneaux de la session que le lot `key` (`projet:lot`) lance : un lot qui n'a qu'un pas (celui qui part, déjà inscrit) n'a rien joué (`fresh`) ;
69
+ * le rang est celui de son projet dans `priority:` (`--priority`, sinon le cadence.yaml du dossier de lancement ; illisible : rang neutre). Lot inconnu : place neutre.
70
+ */
71
+ export function slotPlace(all, launch, given, key) {
72
+ const l = key ? all.find((x) => lotKey(x.project, x.lot) === key) : undefined;
73
+ if (!l)
74
+ return {};
75
+ const fresh = l.steps.length <= 1;
76
+ try {
77
+ return { fresh, rank: priorityRank(l.project, given ?? readPriority(launch)) };
78
+ }
79
+ catch {
80
+ return { fresh }; // priority: illisible : rang neutre (après tous les déclarés), le lancement n'en dépend pas
81
+ }
82
+ }
@@ -1,7 +1,7 @@
1
1
  import { gateProgress, gateText } from '../audit.js';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
- import { availableParallelism, homedir, tmpdir } from 'node:os';
4
+ import { availableParallelism, tmpdir } from 'node:os';
5
5
  import { testWorkersFrom } from './workers.js';
6
6
  import { basename, join, resolve } from 'node:path';
7
7
  import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readOrchestrateConfig, readPlanConfig } from '../config.js';
@@ -24,20 +24,22 @@ import { acceptationFile, acceptText, redAcceptations } from './accept.js';
24
24
  import { noTestGate, noTestText } from './no-test.js';
25
25
  import { ficheReviewText } from './fiche.js';
26
26
  import { maquetteText } from './maquette.js';
27
- import { costlyHypotheses, specScreen, specText } from './spec.js';
27
+ import { budgetStage, costlyHypotheses, lightLot, specText, stageScreen } from './spec.js';
28
28
  import { fichesDuDossier } from '../spec.js';
29
29
  import { candidates, fillTargets, handedBackStreak, parsePriority, parseUntil, drawSettingsReport, readPriority, stopReason } from './continue.js';
30
30
  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';
31
31
  import { installPrePush, keepGateGuard, longDuration, removePrePush } from './guard.js';
32
- import { buildArgs, buildLocalArgs, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude, enabledPlugins, settingsFiles } from './launch.js';
32
+ import { buildArgs, buildLocalArgs, claudeConfigDir, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude, enabledPlugins, settingsFiles } from './launch.js';
33
33
  import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
34
34
  import { groupLots, runPool } from './pool.js';
35
+ import { slotPlace } from './attente.js';
35
36
  import { AddFeed, addNotAlone, addToLive, openInRepo, projectName } from './ajout.js';
36
37
  import { repoRefusals, trackedDirty } from './depot.js';
38
+ import { adoptLeadReview, depositIntegrations, depositsOnly, integrationText, runsElsewhere, takeIntegrations } from './integration-vivante.js';
37
39
  import { removeWorktree } from './retrait.js';
38
40
  import { repoBranch } from './integrate.js';
39
41
  import { baseBranchOf, keptLines, lotBranch, lotTreeDir, planFileSet, recoverTree } from './worktree.js';
40
- import { agentOf, schemaFor } from './schemas.js';
42
+ import { effortKey, schemaFor, stepAgent } from './schemas.js';
41
43
  import { resolveLotRepos } from '../repos.js';
42
44
  import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
43
45
  import { isExpress } from './express.js';
@@ -320,7 +322,7 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
320
322
  }
321
323
  const small = lot.estimate <= 0.5 || lot.quickwin;
322
324
  const goal = args.goal ?? lot.goal;
323
- const screen = specScreen(t.repo, env.config.spec.dir, lot);
325
+ const screen = stageScreen(t.repo, env.config.spec.dir, lot, env.config.maquette);
324
326
  const state = newLot({ project: t.project, repo: t.repo, lot: t.lot, title: lot.title, ...(goal ? { goal } : {}), visible: lot.visible, small, ...(screen ? { screen } : {}), ...(lot.spec && !screen ? { specLot: true } : {}), model: t.model, readOnlyPlan: plan.readonly });
325
327
  if (neighbours.repos.length)
326
328
  state.repos = neighbours.repos;
@@ -329,7 +331,7 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
329
331
  state.express = true;
330
332
  state.light = lot.estimate <= env.config.review.threshold;
331
333
  const escalating = !!env.config.escalation;
332
- state.budget = lotBudget(lot.estimate, screen ? 'screen' : lot.spec ? 'spec' : undefined, escalating);
334
+ state.budget = lotBudget(lot.estimate, budgetStage(lot, screen, env.config.maquette, t.repo, env.config.spec.dir), escalating);
333
335
  if (escalating)
334
336
  state.escalationReserve = ESCALATION_RESERVE;
335
337
  state.dependsOn = lot.after.filter((d) => earlier.includes(d));
@@ -442,7 +444,7 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
442
444
  io.out(`${lotKey(l.project, l.lot)} — ${l.title}`);
443
445
  if (l.goal)
444
446
  io.out(` objectif donné : ${l.goal} (à la place du titre dans les briefs)`);
445
- io.out(` budget du lot : ${l.budget} tokens comptés (base : ${lotBudgetBasis(lot.estimate, l.screen ? 'screen' : l.specLot ? 'spec' : undefined, l.escalationReserve ? env.config.escalation?.model : undefined)})`);
447
+ io.out(` budget du lot : ${l.budget} tokens comptés (base : ${lotBudgetBasis(lot.estimate, budgetStage(lot, l.screen, env.config.maquette, l.repo, env.config.spec.dir), l.escalationReserve ? env.config.escalation?.model : undefined)})`);
446
448
  const mine = lots.filter((o) => o.repo === l.repo);
447
449
  const together = env.config.worktrees && (mine.length > 1 || multiSlot) && !mine.some((o) => o.repos?.length) && !!repoBranch(l.repo);
448
450
  io.out(together ? ` file ${basename(l.repo)} · en parallèle des autres lots du dépôt, dans son worktree (cadence/${id}/${lotSlug(l.project, l.lot)}, créé à la prise du créneau)` : ` file ${basename(l.repo)} · ${slot < 2 ? `créneau ${slot + 1}` : 'en attente d\'un créneau'}`);
@@ -459,7 +461,7 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
459
461
  }
460
462
  const steps = [{ kind: 'implement', model: l.model }];
461
463
  const specStage = !!(l.screen || l.specLot);
462
- if (specStage && env.config.accept)
464
+ if (specStage && env.config.accept && !lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir))
463
465
  steps.unshift({ kind: 'accept', model: 'sonnet' });
464
466
  if (l.screen && env.config.maquette)
465
467
  steps.unshift({ kind: 'maquette', model: env.config.review.full });
@@ -513,12 +515,12 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
513
515
  const vars = { chemin: l.repo, lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '', diff: 'git diff <sha de départ>..HEAD' };
514
516
  vars.decisions = decisionsText(humanDecisions(lot, lot.parent ? env.loadPlan().lots().find((x) => x.id === lot.parent) : undefined));
515
517
  if (specStage)
516
- vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, back: !!l.specLot });
518
+ vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, light: lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir), back: !!l.specLot });
517
519
  if (l.screen && env.config.maquette)
518
- vars.maquette = maquetteText({ fiche: `${env.config.spec.dir}/<écran>/fiche.yaml`, passes: 0, max: env.config.maquetteReviews });
520
+ vars.maquette = maquetteText({ fiche: `${env.config.spec.dir}/<écran>/fiche.yaml`, passes: 0, max: env.config.maquetteReviews, light: lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir) });
519
521
  if (l.specLot && env.config.fiche)
520
522
  vars.fiche = ficheReviewText({ fiche: `${env.config.spec.dir}/<sujet>/fiche.yaml`, passes: 0 });
521
- if (specStage && env.config.accept) {
523
+ if (specStage && env.config.accept && !lightLot(l, lot, env.config.maquette, l.repo, env.config.spec.dir)) {
522
524
  const folder = `${env.config.spec.dir}/${l.screen ? '<écran>' : '<sujet>'}`;
523
525
  vars.accept = acceptText({ fiche: `${folder}/fiche.yaml`, maquette: `${folder}/maquette.html`, screen: !!l.screen, back: false, acceptation: acceptationFile(`${folder}/fiche.yaml`, l.lot), command: env.config.accept ?? '', test: env.config.test });
524
526
  }
@@ -527,7 +529,7 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
527
529
  writeFileSync(file, renderBrief(briefName(l, s.kind), s.kind === 'fiche' || vars.fiche === undefined ? vars : { ...vars, fiche: '' }, deps.templatesDir)); // la consigne de revue de fiche ne sert qu'au brief de la fiche : implement et review en sont la base d'une délégation à la main
528
530
  const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen).playwright;
529
531
  const local = s.model === 'local';
530
- const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: agentOf(s.kind), cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && (s.kind === 'implement' || s.kind === 'spec') ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright, disablePlugins: enabledPlugins(settingsFiles(deps.claudeHome, l.repo)) }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
532
+ const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[effortKey(s.kind)], schema: schemaFor(s.kind), agent: stepAgent(s.kind), cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && (s.kind === 'implement' || s.kind === 'spec') ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright, disablePlugins: enabledPlugins(settingsFiles(deps.claudeHome, l.repo)) }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
531
533
  io.out(` ${s.kind} : ${local ? 'claude-local' : 'claude'} ${args.join(' ')}`);
532
534
  io.out(` brief : ${file}`);
533
535
  const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen));
@@ -1102,9 +1104,8 @@ async function execute(wave, lots, store, io, deps, today, args, all = lots) {
1102
1104
  throw e;
1103
1105
  }
1104
1106
  // Plafond de sessions simultanées, toutes vagues confondues : chaque session attend un créneau libre avant de partir.
1105
- const claude = async (args, o) => {
1106
- const free = await acquireSlot(home, cap, {
1107
- wave: wave.id,
1107
+ const claude = async (argv, o) => {
1108
+ const free = await acquireSlot(home, cap, { wave: wave.id, ...slotPlace(all, launch, args.priority, o.lot), // L531 : place dans la file des créneaux, commune aux vagues
1108
1109
  pollMs: deps.slotPollMs,
1109
1110
  // L332 : une demande écrite pendant l'attente (`--drop` de ce lot, `--stop-after-current`) l'interrompt ; la session ne part pas.
1110
1111
  abandon: () => {
@@ -1120,7 +1121,7 @@ async function execute(wave, lots, store, io, deps, today, args, all = lots) {
1120
1121
  onGot: (ms) => wctx.log(`${o.lot ? `${o.lot} · ` : ''}créneau de session obtenu après ${duration(ms)} d'attente`),
1121
1122
  });
1122
1123
  try {
1123
- return await deps.claude(args, o);
1124
+ return await deps.claude(argv, o);
1124
1125
  }
1125
1126
  finally {
1126
1127
  free();
@@ -1259,6 +1260,8 @@ async function execute(wave, lots, store, io, deps, today, args, all = lots) {
1259
1260
  taken.push(c);
1260
1261
  }
1261
1262
  }
1263
+ // L532 : les intégrations déposées par `--resume --integrate` pendant la vague sont reprises de la même façon, au tour où le lot peut jouer.
1264
+ taken.push(...(await takeIntegrations({ store, ctxs, wctx, settled }, canRun)));
1262
1265
  if (taken.length) {
1263
1266
  try {
1264
1267
  updateWaveRepos(home, process.pid, repos.filter((x) => !freed.has(x)), lanes());
@@ -1469,6 +1472,9 @@ async function resume(args, argv, io, deps, launch, today) {
1469
1472
  const store = typeof args.resume === 'string' ? RunStore.find(launch, args.resume) : singleUnfinished(launch);
1470
1473
  if (!store)
1471
1474
  throw new RafError(`vague inconnue : ${args.resume}`);
1475
+ // L532 : `--integrate` seul sur une vague qui tourne encore dépose l'intégration dans sa file au lieu de refuser ; dit avant l'instantané (le dépôt ne demande rien à son code) ; une reprise qui attend le dépôt (L443) est inscrite vivante mais ne joue pas encore : la vague doit être « running ». Avec une autre option de reprise ou --replay, le refus ci-dessous reste.
1476
+ if (depositsOnly(args, runsElsewhere(store, launch)))
1477
+ return depositIntegrations(args.integrate, io, store);
1472
1478
  if (deps.snapshot) {
1473
1479
  // La reprise tourne sur l'instantané de la vague, jamais sur le dist/ courant ; une vague d'avant L61 n'en a pas.
1474
1480
  if (LOT_LIKE.test(store.id) && snapshotExists(store.dir)) {
@@ -1743,19 +1749,14 @@ async function resume(args, argv, io, deps, launch, today) {
1743
1749
  for (const l of integ) {
1744
1750
  const k = lotKey(l.project, l.lot);
1745
1751
  const lead = leadReviewed.get(l);
1746
- if (lead) {
1747
- // 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é.
1748
- l.next = 'integrate';
1749
- l.code = { conforme: true, bloquants: 0, majeurs: 0, mineurs: 0, verdict: lead.verdict, sousTaches: [], nonVerifie: [], head: lead.head };
1750
- l.verdict = lead.verdict;
1751
- l.constats = [];
1752
- }
1752
+ if (lead)
1753
+ adoptLeadReview(l, lead);
1753
1754
  const planNote = l.next !== 'integrate'; // rendu sur une écriture de plan refusée (--replay) : le lot repart de l'étape gardée
1754
1755
  if (planNote && l.outcome?.startsWith(HYPOTHESES_HANDBACK))
1755
1756
  l.hypotheses = undefined; // les notes d'hypothèses se posent de nouveau (sans doublon) à l'entrée de l'étape
1756
1757
  l.status = 'suspended';
1757
1758
  l.outcome = null;
1758
- 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)';
1759
+ const text = planNote ? `écriture du plan reprise à l'étape ${l.next} (--resume --replay)` : integrationText(lead ?? null);
1759
1760
  store.journal(`${k} → suspended — ${text}`);
1760
1761
  io.out(`${k} → suspended — ${text}`);
1761
1762
  }
@@ -1847,6 +1848,6 @@ export function realOrchestrateDeps(env) {
1847
1848
  },
1848
1849
  agentsDir: AGENTS_DIR,
1849
1850
  snapshot: child ? undefined : { packageRoot: PACKAGE_ROOT, reexec: spawnReexec },
1850
- claudeHome: env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'),
1851
+ claudeHome: claudeConfigDir(env),
1851
1852
  };
1852
1853
  }
@@ -1,15 +1,14 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
1
  import { basename, join } from 'node:path';
3
- import { parse } from 'yaml';
4
2
  import { nextUp } from '../audit.js';
5
3
  import { planLoader } from '../config.js';
6
4
  import { gitRoot } from '../git.js';
7
5
  import { findProjects, isProject } from '../lead.js';
8
6
  import { RafError } from '../plan.js';
7
+ import { readYamlKey } from '../yaml-keys.js';
9
8
  import { lotBudget, projectEnv } from './command.js';
10
9
  import { readyFree } from '../ready-free.js';
11
10
  import { lotFinished, lotKey } from './state.js';
12
- import { specScreen } from './spec.js';
11
+ import { budgetStage, stageScreen } from './spec.js';
13
12
  import { STOP_REQUESTED } from './cycle.js';
14
13
  /** `--until 18:00` : l'heure d'aujourd'hui (date de `now`) à partir de laquelle la vague ne tire plus de lot. */
15
14
  export function parseUntil(text, now) {
@@ -32,16 +31,7 @@ export function parsePriority(text) {
32
31
  /** Clé `priority:` du cadence.yaml du dossier de lancement (le dossier parent des projets) ; absente : aucune priorité déclarée. */
33
32
  export function readPriority(launchDir) {
34
33
  const file = join(launchDir, 'cadence.yaml');
35
- if (!existsSync(file))
36
- return [];
37
- let raw;
38
- try {
39
- raw = parse(readFileSync(file, 'utf8'));
40
- }
41
- catch (e) {
42
- throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
43
- }
44
- const p = raw?.priority;
34
+ const p = readYamlKey(file, 'priority');
45
35
  if (p == null)
46
36
  return [];
47
37
  if (!Array.isArray(p) || p.length === 0 || p.some((n) => typeof n !== 'string' || !n.trim()))
@@ -90,11 +80,13 @@ export function candidates(launch, o) {
90
80
  }
91
81
  let specDir = 'docs/specs';
92
82
  let escalating = false;
83
+ let maquette = true;
93
84
  let setting;
94
85
  try {
95
86
  const config = projectEnv(dir).config;
96
87
  specDir = config.spec.dir;
97
88
  escalating = !!config.escalation;
89
+ maquette = config.maquette;
98
90
  setting = config.continue;
99
91
  }
100
92
  catch {
@@ -108,7 +100,7 @@ export function candidates(launch, o) {
108
100
  o.skipped?.push(`${project}:${lot.id} écarté du tirage : ${why}`);
109
101
  continue;
110
102
  }
111
- if (lotBudget(lot.estimate, specScreen(dir, specDir, lot) ? 'screen' : lot.spec ? 'spec' : undefined, escalating) > o.remaining)
103
+ if (lotBudget(lot.estimate, budgetStage(lot, stageScreen(dir, specDir, lot, maquette), maquette, dir, specDir), escalating) > o.remaining)
112
104
  continue;
113
105
  out.push({ project, dir, lot });
114
106
  }
@@ -8,7 +8,7 @@ import { tmpdir } from 'node:os';
8
8
  import { coveredTasks, lotCommits, lotWork } from '../audit.js';
9
9
  import { planLoader, readDocsConfig, readPlanConfig } from '../config.js';
10
10
  import { PLAN_ACTIONS, leadAnswerMessage, planCommitMessage } from '../plan-commit.js';
11
- import { articleRepoDir, articlesBrief, docSyncBrief, docSyncGaps, filesOf, gapMessage } from '../docsync.js';
11
+ import { articleRepoDir, articlesBrief, docSyncBrief, filesOf, gapMessage, lotDocSyncGaps } from '../docsync.js';
12
12
  import { testGateMissing } from '../testgate.js';
13
13
  import { citesPriorWork, keepPrecheckReading, localYesNote, precheckNothingToFind } from './precheck.js';
14
14
  import { PROBE_FAILED, PROBE_RESERVE, probeStep, probeVars, scheduleProbe } from './probe.js';
@@ -30,13 +30,13 @@ import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, t
30
30
  import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
31
31
  import { sumTokens } from './result.js';
32
32
  import { cutReport, EXTENSION_MS, MAX_EXTENSIONS, workTimeoutMs } from './timeouts.js';
33
- import { ACCEPT_SCHEMA, agentOf, checkShape, PRECHECK_SCHEMA, REVIEW_SCHEMA, schemaFor, SPEC_SCHEMA, WORK_SCHEMA } from './schemas.js';
33
+ import { ACCEPT_SCHEMA, checkShape, effortKey, PRECHECK_SCHEMA, REVIEW_SCHEMA, schemaFor, SPEC_SCHEMA, stepAgent, WORK_SCHEMA } from './schemas.js';
34
34
  import { acceptationFile, acceptCommands, isAcceptList, acceptText, checkAcceptation, findAcceptation, isAcceptationFile, lockViolations, lotStartHead, parseAcceptation, planAcceptRuns, redSha, resealedRouge, sealAcceptation, setVert } from './accept.js';
35
35
  import { lotIds, referenceIdsText } from '../ref/coverage.js';
36
36
  import { avalFicheText, ficheContracts, ficheRules, hasScreenPart } from './aval.js';
37
37
  import { ficheReviewText, reviewedSheetText } from './fiche.js';
38
38
  import { bulletLines, maquetteText, reviewedText } from './maquette.js';
39
- import { answeredText, checkSpec, costlyHypotheses, ficheHasBack, ficheReady, findFiche, hypothesisLines, answerAltersRule, hypothesisNote, LEAD_SOURCE, ruleIdsSourcedByLead, sourceRules, specText } from './spec.js';
39
+ import { answeredText, checkSpec, costlyHypotheses, ficheHasBack, ficheReady, findFiche, hypothesisLines, answerAltersRule, hypothesisNote, lightLot, LEAD_SOURCE, ruleIdsSourcedByLead, sourceRules, specText } from './spec.js';
40
40
  import { handBackWithoutFinding } from './no-finding.js';
41
41
  import { decisionsText, humanDecisions } from './decisions.js';
42
42
  import { LOCAL_PRECHECK_UNREADABLE, PRECHECK_UNREADABLE, expressSummary, lotKey, lotRepoPaths, neighbourDirs } from './state.js';
@@ -413,7 +413,7 @@ function docGaps(c) {
413
413
  const { sync } = docsConfig(c);
414
414
  const plan = histPlan(c);
415
415
  const acknowledged = !!plan.lots().find((x) => x.id === c.lot.lot)?.docsOk;
416
- return { sync, gaps: sync.length && !acknowledged ? docSyncGaps(sync, filesOf(wd(c), lotWork(plan, wd(c), c.lot.lot))) : [] };
416
+ return { sync, gaps: sync.length && !acknowledged ? lotDocSyncGaps(sync, wd(c), lotWork(plan, wd(c), c.lot.lot)) : [] };
417
417
  }
418
418
  /** Cause du renvoi au lead d'un lot dont l'écart docs.sync est ouvert (L317), null sans écart. */
419
419
  function openDocGap(c) {
@@ -439,24 +439,19 @@ export function briefName(l, kind, integration = false) {
439
439
  return 'review-recheck'; // la revue d'intégration (R17) garde le gabarit qui porte {{integration}}
440
440
  return kind;
441
441
  }
442
- /** Gabarit de fiche livré avec le paquet, joint au brief de la spec (la session ne lit pas le dossier du paquet). */
443
- function ficheTemplate() {
442
+ /** Fichier livré avec le paquet à côté des gabarits, joint à un brief (la session ne lit pas le dossier du paquet) ; undefined s'il manque. */
443
+ function packageFile(name) {
444
444
  try {
445
- return readFileSync(join(TEMPLATES_DIR, '..', 'fiche.yaml'), 'utf8');
445
+ return readFileSync(join(TEMPLATES_DIR, '..', name), 'utf8');
446
446
  }
447
447
  catch {
448
448
  return undefined;
449
449
  }
450
450
  }
451
+ /** Gabarit de fiche livré avec le paquet, joint au brief de la spec. */
452
+ const ficheTemplate = () => packageFile('fiche.yaml');
451
453
  /** Gabarit de la partie back de la fiche (L187), joint au brief de la spec d'un lot `spec: true`. */
452
- function ficheBackTemplate() {
453
- try {
454
- return readFileSync(join(TEMPLATES_DIR, '..', 'fiche-back.yaml'), 'utf8');
455
- }
456
- catch {
457
- return undefined;
458
- }
459
- }
454
+ const ficheBackTemplate = () => packageFile('fiche-back.yaml');
460
455
  /** Le lot passe par l'étape spec : il déclare `screen:` (écran) ou `spec: true` (comportement sans écran, L188). */
461
456
  const hasSpecStage = (l) => !!(l.screen || l.specLot);
462
457
  /**
@@ -465,14 +460,7 @@ const hasSpecStage = (l) => !!(l.screen || l.specLot);
465
460
  */
466
461
  const partsOf = (c, fiche) => ({ screen: !!c.lot.screen, back: !!c.lot.specLot || (!!c.lot.screen && !!fiche && ficheHasBack(wd(c), fiche)) });
467
462
  /** Gabarit du lanceur de tests livré avec le paquet, joint au brief de l'étape accept. */
468
- function acceptRunner() {
469
- try {
470
- return readFileSync(join(TEMPLATES_DIR, '..', 'acceptation-runner.mjs'), 'utf8');
471
- }
472
- catch {
473
- return undefined;
474
- }
475
- }
463
+ const acceptRunner = () => packageFile('acceptation-runner.mjs');
476
464
  /** Fiche du lot : celle de l'étape spec, ou la fiche retrouvée dans le dépôt (reprise). */
477
465
  function ficheOf(c) {
478
466
  const l = c.lot;
@@ -543,7 +531,7 @@ function briefFor(c, kind, local = false) {
543
531
  vars.spec = specText({
544
532
  dir: c.config.spec.dir,
545
533
  render: c.config.spec.render,
546
- screen: l.screen,
534
+ screen: l.screen, light: lightLot(l, lot, c.config.maquette, wd(c), c.config.spec.dir),
547
535
  back,
548
536
  problems: l.specProblems,
549
537
  review: !reviewed.length ? undefined : back ? reviewedSheetText(reviewed, !!l.screen, session) : reviewedText(reviewed, session),
@@ -562,7 +550,7 @@ function briefFor(c, kind, local = false) {
562
550
  vars.reponse = '';
563
551
  }
564
552
  if (kind === 'maquette') {
565
- vars.maquette = maquetteText({ fiche: ficheOf(c) ?? '', passes: l.maquette?.passes ?? 0, max: c.config.maquetteReviews, previous: l.maquetteConstats });
553
+ vars.maquette = maquetteText({ fiche: ficheOf(c) ?? '', passes: l.maquette?.passes ?? 0, max: c.config.maquetteReviews, previous: l.maquetteConstats, light: lightLot(l, lot, c.config.maquette, wd(c), c.config.spec.dir) });
566
554
  vars.reponse = '';
567
555
  }
568
556
  if (kind === 'accept') {
@@ -711,7 +699,7 @@ async function session(c, kind, local = false) {
711
699
  if (WRITE_STEPS.includes(kind) && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
712
700
  return overBudget(c, kind === 'fix' && !lotOver(l));
713
701
  const model = ['spec', 'accept', 'fiche', 'probe'].includes(kind) ? 'sonnet' : write ? (kind === 'fix' && l.escalation?.stage === 'fix' ? l.escalation.model : l.model) : local ? 'local' : kind === 'precheck' ? 'sonnet' : reviewModel(c, kind); // le contrôle préalable ne fait que lire : pas d'Opus
714
- const effort = c.config.effort[kind === 'review-small' ? 'review' : kind];
702
+ const effort = c.config.effort[effortKey(kind)];
715
703
  const before = await snapshot(wd(c));
716
704
  const neighbours = l.repos ?? [];
717
705
  const beforeOthers = await Promise.all(neighbours.map((r) => snapshot(r.path)));
@@ -771,7 +759,7 @@ async function session(c, kind, local = false) {
771
759
  effort,
772
760
  local,
773
761
  schema: schemaFor(kind),
774
- agent: kind === 'fix' && l.docsFix ? 'doc-keeper' : agentOf(kind),
762
+ agent: stepAgent(kind, l.docsFix),
775
763
  cwd: wd(c),
776
764
  wave: w.id,
777
765
  lot: lotKey(l.project, l.lot),
@@ -1455,6 +1443,9 @@ function afterMaquette(c, fiche) {
1455
1443
  // Des constats de la revue de fiche attendent la revue de la maquette (lot mixte) : les deux partent ensemble à la seconde passe `spec`.
1456
1444
  if (l.ficheConstats?.length)
1457
1445
  return 'spec';
1446
+ // Une maquette légère n'a pas de règles de comportement à verrouiller : pas d'accept, même si orchestrate.accept est déclaré.
1447
+ if (lightLot(l, c.loadPlan().lot(l.lot), c.config.maquette, wd(c), c.config.spec.dir))
1448
+ return 'implement';
1458
1449
  if (!c.config.accept) {
1459
1450
  if (!l.warnings.includes(NO_ACCEPT))
1460
1451
  l.warnings.push(NO_ACCEPT);
@@ -6,6 +6,7 @@ import { sharedStateDir } from '../state.js';
6
6
  import { releaseLock, takeLock } from './lock.js';
7
7
  import { mergeUnreleased, moveToUnreleased, settleLotEntries, unreleasedSections } from './changelog-merge.js';
8
8
  import { mutexFor } from './mutex.js';
9
+ import { readPlanConfig } from '../config.js';
9
10
  import { Plan } from '../plan.js';
10
11
  import { parseDocument } from 'yaml';
11
12
  import { git, gitFailure, tryGit } from './worktree.js';
@@ -91,11 +92,15 @@ function resolvePlan(work) {
91
92
  return false;
92
93
  const file = files[0];
93
94
  try {
95
+ // Les champs d'extension déclarés (L464) sont des propriétés des lots : sans les réglages, `bare` ne verrait pas un champ changé.
96
+ // Seuls les champs d'extension sont passés : le `plan.onWrite` du projet ne doit pas se lancer pendant un rebase.
97
+ const extras = readPlanConfig(join(work, 'cadence.yaml'), work)?.settings.extras;
98
+ const settings = extras ? { extras } : {};
94
99
  const stage = (n) => {
95
100
  const text = run(work, 'show', `:${n}:${file}`);
96
101
  writeFileSync(join(work, file), text);
97
102
  const head = parseDocument(text).toJSON();
98
- return { lots: Plan.load(join(work, file)).lots(), head: JSON.stringify({ ...(head ?? {}), lots: undefined }) };
103
+ return { lots: Plan.load(join(work, file), settings).lots(), head: JSON.stringify({ ...(head ?? {}), lots: undefined }) };
99
104
  };
100
105
  const [b, o, t] = [stage(1), stage(2), stage(3)];
101
106
  const [base, ours, theirs] = [b.lots, o.lots, t.lots];
@@ -109,7 +114,7 @@ function resolvePlan(work) {
109
114
  if (added.some((t) => review(ours.find((o) => o.id === t.id)) !== review(base.find((b) => b.id === t.id))))
110
115
  return false;
111
116
  writeFileSync(join(work, file), run(work, 'show', `:2:${file}`));
112
- const plan = Plan.load(join(work, file));
117
+ const plan = Plan.load(join(work, file), settings);
113
118
  for (const t of added)
114
119
  plan.recordReview(t.id, t.review.verdict, t.review.date, t.review.commit ?? null, t.review.repos, t.review.reporte);
115
120
  plan.save();
@@ -0,0 +1,138 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { planLoader } from '../config.js';
4
+ import { leadReviewedHandBack } from './cycle.js';
5
+ import { trackedDirty } from './depot.js';
6
+ import { cadenceHome, liveWaves } from './registry.js';
7
+ import { waveReadsIntegrateQueue } from './snapshot.js';
8
+ import { lotKey } from './state.js';
9
+ import { resumeCommand } from './table.js';
10
+ import { lotTreeDir } from './worktree.js';
11
+ /**
12
+ * Un lot rendu que `--integrate` peut reprendre (L279, L370) : rendu à l'étape d'intégration (`next: integrate`), ou rendu ailleurs puis corrigé et revu conforme par le lead dans son worktree (la revue porte le dernier commit).
13
+ * Rend le verdict du lead dans ce second cas (`lead`), `{ lead: null }` dans le premier, null si le lot n'est pas reprenable.
14
+ */
15
+ export function integrationHandBack(store, l) {
16
+ if (l.status !== 'handed-back')
17
+ return null;
18
+ if (l.next === 'integrate')
19
+ return { lead: null };
20
+ // 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.
21
+ const tree = lotTreeDir(store.dir, l.project, l.lot, l.repo);
22
+ const lead = existsSync(join(tree, '.git')) ? leadReviewedHandBack(l, planLoader(tree)(), tree) : null;
23
+ return lead ? { lead } : null;
24
+ }
25
+ /** 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é. */
26
+ export function adoptLeadReview(l, lead) {
27
+ l.next = 'integrate';
28
+ l.code = { conforme: true, bloquants: 0, majeurs: 0, mineurs: 0, verdict: lead.verdict, sousTaches: [], nonVerifie: [], head: lead.head };
29
+ l.verdict = lead.verdict;
30
+ l.constats = [];
31
+ }
32
+ /** La ligne de journal d'une intégration reprise (à la reprise comme sur la vague vivante). */
33
+ export const integrationText = (lead) => (lead ? `intégration reprise (--resume --integrate) : revue conforme du lead hors vague, ${lead.head.slice(0, 7)}` : 'intégration reprise (--resume --integrate)');
34
+ /** Le contrôle d'une cible de `--integrate` : sa clé `projet:lot`, ou le refus. */
35
+ function checkTarget(store, t, seen) {
36
+ const matches = store.lots().filter((l) => l.lot === t.lot && (t.project ? l.project === t.project : true));
37
+ const where = `${t.project ? `${t.project}:` : ''}${t.lot}`;
38
+ if (matches.length !== 1)
39
+ return { refusal: matches.length ? `--integrate ${where} : plusieurs projets portent ce lot, précisez projet:lot` : `--integrate ${where} : lot inconnu dans la vague ${store.id}` };
40
+ const l = matches[0];
41
+ const key = lotKey(l.project, l.lot);
42
+ if (seen.includes(key))
43
+ return { refusal: `--integrate ${key} : lot visé deux fois` };
44
+ if (!integrationHandBack(store, l))
45
+ return { refusal: `--integrate ${key} : le lot n'est pas rendu à l'étape d'intégration (${l.status}, étape ${l.next ?? 'aucune'} : ${l.outcome ?? 'sans cause'})` };
46
+ return { key };
47
+ }
48
+ /**
49
+ * `--resume --integrate` sur une vague VIVANTE (L532) : l'intégration est déposée dans la file de la vague (`integrates.log`), qui la reprend entre deux étapes, comme un `--add`.
50
+ * Sans cela le lead attendait la fin de la vague (qui dure des heures sous `--continue`). Les lots visés sont contrôlés comme à la reprise : connus, rendus à l'intégration ou revus par le lead.
51
+ */
52
+ export function depositIntegrations(targets, io, store) {
53
+ const refusals = [];
54
+ const keys = [];
55
+ for (const t of targets) {
56
+ const r = checkTarget(store, t, keys);
57
+ if ('key' in r)
58
+ keys.push(r.key);
59
+ else
60
+ refusals.push(r.refusal);
61
+ }
62
+ if (!waveReadsIntegrateQueue(store.dir))
63
+ refusals.push(`la vague ${store.id} ne lit pas la file d'intégrations (lancée avec une version de cadence d'avant L532) : attendre sa fin, puis ${resumeCommand(store.id, '--integrate', '<projet>:<lot>')}`);
64
+ if (refusals.length) {
65
+ for (const r of refusals)
66
+ io.err(`orchestrate : ${r}`);
67
+ return 2;
68
+ }
69
+ for (const key of keys) {
70
+ const id = store.requestIntegrate(key);
71
+ // La vague a pu finir entre le contrôle et le dépôt : sa file ne sera plus lue, la demande est retirée et le lead reprend à la main.
72
+ if (!liveWaves(cadenceHome()).some((w) => w.wave === store.id)) {
73
+ store.markIntegrateTaken(id);
74
+ io.err(`orchestrate : ${key} : la vague ${store.id} vient de finir, intégration non prise : ${resumeCommand(store.id, '--integrate', key)}`);
75
+ return 2;
76
+ }
77
+ store.journal(`demande : intégration du lot ${key}`);
78
+ io.out(`${key} : intégration déposée — la vague ${store.id} la reprend entre deux étapes dès qu'un créneau est libre`);
79
+ }
80
+ return 0;
81
+ }
82
+ /** Pourquoi la demande ne peut plus être jouée (lot inconnu, déjà repris, revue du lead perdue), ou le lot et ce qui le reprend. */
83
+ function eligible(store, ctxs, lot) {
84
+ const c = ctxs.find((x) => lotKey(x.lot.project, x.lot.lot) === lot);
85
+ if (!c)
86
+ return { why: 'lot inconnu' };
87
+ if (c.lot.status !== 'handed-back')
88
+ return { why: `le lot n'est plus rendu (${c.lot.status})` };
89
+ const back = integrationHandBack(store, c.lot);
90
+ return back ? { c, back } : { why: "le lot n'est pas rendu à l'étape d'intégration ni revu par le lead au dernier commit de son worktree" };
91
+ }
92
+ /**
93
+ * La vague vivante prend les intégrations déposées (L532) : le lot rendu repart à l'étape `integrate` comme sous `--resume --integrate`, quand `canRun` l'accepte ; une demande dont le lot ne qualifie plus
94
+ * (déjà repris, commit après la revue du lead, fichier suivi modifié) est écartée et dite au journal. Rend les lots à jouer ; `settled` est la liste des lots qui ont fini de jouer.
95
+ */
96
+ export async function takeIntegrations(w, canRun) {
97
+ const { store, wctx } = w;
98
+ const taken = [];
99
+ const drop = (id, lot, why) => {
100
+ wctx.log(`${lot} : intégration écartée (${why})`);
101
+ store.markIntegrateTaken(id);
102
+ };
103
+ for (const e of store.integrateQueue()) {
104
+ const r = eligible(store, w.ctxs, e.lot);
105
+ if ('why' in r) {
106
+ drop(e.id, e.lot, r.why);
107
+ continue;
108
+ }
109
+ const { c, back } = r;
110
+ if (!canRun(c))
111
+ continue;
112
+ const dirty = await trackedDirty(lotTreeDir(store.dir, c.lot.project, c.lot.lot, c.lot.repo));
113
+ if (dirty.length) {
114
+ drop(e.id, e.lot, `arbre sale, ${dirty.length} fichier(s) suivi(s) modifié(s) : ${dirty.join(', ')}`);
115
+ continue;
116
+ }
117
+ if (back.lead)
118
+ adoptLeadReview(c.lot, back.lead);
119
+ c.lot.next = 'integrate';
120
+ c.lot.status = 'suspended';
121
+ c.lot.outcome = null;
122
+ store.writeLot(c.lot);
123
+ store.markIntegrateTaken(e.id);
124
+ wctx.log(`${e.lot} → suspended — ${integrationText(back.lead)}`);
125
+ // Le lot avait fini de jouer (`settled`) : il rejoue, il tient de nouveau un créneau (L433).
126
+ w.settled.delete(c);
127
+ taken.push(c);
128
+ }
129
+ return taken;
130
+ }
131
+ /** `--resume --integrate` seul (sans --replay, --answer, --budget, --drop ni --continue) sur une vague qui tourne encore : l'intégration est déposée, pas refusée. */
132
+ export function depositsOnly(a, live) {
133
+ return live && a.integrate.length > 0 && a.integrate.every((t) => !t.replay) && !a.answers.length && !a.lotBudgets.length && !a.drop.length && a.budget === undefined && !a.continue;
134
+ }
135
+ /** La vague tourne dans un autre processus de ce dossier, et joue (« running ») : une reprise qui attend le dépôt (L443) est inscrite vivante sans rien jouer encore, elle ne lit pas la file. */
136
+ export function runsElsewhere(store, launch) {
137
+ return store.readWave()?.status === 'running' && liveWaves(cadenceHome()).some((w) => w.wave === store.id && w.cwd === launch && w.pid !== process.pid);
138
+ }