@sylad/cadence 1.35.0 → 1.37.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/dist/deliver-ci.js +105 -0
- package/dist/deliver.js +57 -75
- package/dist/orchestrate/ajout.js +186 -0
- package/dist/orchestrate/briefs.js +23 -3
- package/dist/orchestrate/command.js +118 -140
- package/dist/orchestrate/cycle.js +23 -18
- package/dist/orchestrate/depot.js +33 -0
- package/dist/orchestrate/gate-notes.js +7 -1
- package/dist/orchestrate/lot-budget.js +50 -0
- package/dist/orchestrate/no-finding.js +19 -0
- package/dist/orchestrate/pool.js +1 -1
- package/dist/orchestrate/precheck.js +34 -0
- package/dist/orchestrate/retrait.js +37 -0
- package/dist/orchestrate/schemas.js +2 -2
- package/dist/orchestrate/snapshot.js +13 -0
- package/dist/orchestrate/state.js +47 -9
- package/dist/orchestrate/table.js +2 -2
- package/dist/orchestrate/tiers.js +9 -0
- package/dist/remote-gate.js +2 -2
- package/dist/state.js +4 -0
- package/docs/corrections.md +2 -0
- package/docs/deliver.md +18 -0
- package/docs/lead.md +3 -1
- package/docs/mesures/brief-implementation-enrichi-avant-apres.md +36 -0
- package/docs/orchestrate.md +11 -3
- package/docs/releasing.md +1 -1
- package/docs/whats-new.md +6 -0
- package/package.json +2 -1
- package/templates/orchestrate/fix-docs.md +1 -1
- package/templates/orchestrate/fix-minors.md +1 -1
- package/templates/orchestrate/fix.md +5 -4
- package/templates/orchestrate/implement.md +5 -4
- package/templates/orchestrate/precheck.md +1 -0
|
@@ -27,12 +27,15 @@ import { costlyHypotheses, specScreen, specText } from './spec.js';
|
|
|
27
27
|
import { fichesDuDossier } from '../spec.js';
|
|
28
28
|
import { candidates, parsePriority, parseUntil, drawSettingsReport, readPriority, stopReason } from './continue.js';
|
|
29
29
|
import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, leadReviewedHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
|
|
30
|
-
import {
|
|
30
|
+
import { installPrePush, keepGateGuard, longDuration, removePrePush } from './guard.js';
|
|
31
31
|
import { buildArgs, buildLocalArgs, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude, enabledPlugins, settingsFiles } from './launch.js';
|
|
32
32
|
import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
33
33
|
import { groupLots, runPool } from './pool.js';
|
|
34
|
+
import { AddFeed, addNotAlone, addToLive, openInRepo, projectName } from './ajout.js';
|
|
35
|
+
import { repoRefusals, trackedDirty } from './depot.js';
|
|
36
|
+
import { removeWorktree } from './retrait.js';
|
|
34
37
|
import { repoBranch } from './integrate.js';
|
|
35
|
-
import { baseBranchOf, keptLines,
|
|
38
|
+
import { baseBranchOf, keptLines, lotBranch, lotTreeDir, planFileSet, recoverTree } from './worktree.js';
|
|
36
39
|
import { agentOf, schemaFor } from './schemas.js';
|
|
37
40
|
import { resolveLotRepos } from '../repos.js';
|
|
38
41
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
@@ -42,55 +45,8 @@ import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveMo
|
|
|
42
45
|
const REPO_WAIT_MS = 3 * 60 * 60_000;
|
|
43
46
|
const REPO_WAIT_POLL_MS = 5_000;
|
|
44
47
|
export const DEFAULT_BUDGET = 2_000_000;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* où il est passé, 250 lots, 11-10), rangé par estimate : jusqu'à 0,5 j p90 374 k (152 lots), de 0,5 à 1 j p90 732 k (88 lots, max
|
|
48
|
-
* 1,09 M), au-delà d'un jour p90 1,14 M pour les 10 lots de 1,25 à 2 j (570 k par jour d'estimate), max 1,7 M (L188). Le plafond
|
|
49
|
-
* de L78 (400 k par jour) rendait au lead, au deuxième tour, un lot de 1 j (L188 : 483 k puis 495 k).
|
|
50
|
-
* Plancher relevé de 450 k à 540 k (L218) avec la réserve de la passe fix (155 k) : 14 k + 282 k + 80 k + 155 k = 531 k arrondis, la marge avant refus du fix reste 385 k.
|
|
51
|
-
* Relevé à 600 k (L247), mesuré sur 16–22-10 : 145 lots à 540 k, p90 451 k, 3 au-dessus (tous maritime-atlas : 12 lots, p90 556 k, max 609 k) ; la marge
|
|
52
|
-
* avant refus du fix passe à 445 k. Les lots à 750 k (60 lots, p90 571 k, 0 dépassement) et à 1,3 M (20 lots, p90 904 k, 1 dépassement) tiennent : inchangés.
|
|
53
|
-
* Remesuré sur 17–23-10 (L280, `docs/perf/recalage-2026-10-24.md`) : 0 lot sur 309 au-dessus de son budget, p90 452 k à 600 k, 563 k à 750 k, 875 k à 1,3 M : tous inchangés, avec au moins 10 % de marge.
|
|
54
|
-
*/
|
|
55
|
-
const LOT_BUDGET_FLOOR = 600_000;
|
|
56
|
-
/** Lot de plus de 0,5 j et d'au plus 1 j : p90 mesuré du total, 732 k arrondis à 750 k. */
|
|
57
|
-
const LOT_BUDGET_ONE_DAY = 750_000;
|
|
58
|
-
/** Tokens par jour d'estimate au-delà d'un jour : p90 1,14 M pour 2 j. */
|
|
59
|
-
const LOT_BUDGET_PER_DAY = 570_000;
|
|
60
|
-
/**
|
|
61
|
-
* Total d'un lot à `screen:` (L194), étapes spec, maquette et accept comprises : p90 1,28 M sur 5 lots (L113 1,12 M, L25 1,28 M, L50 877 k,
|
|
62
|
-
* L85 758 k, L43 238 k), arrondi à 1,3 M. Remplace l'allocation de L191 (+350 k au budget d'estimate), qui ne payait ni l'UX de
|
|
63
|
-
* conformité ni les passes de correction.
|
|
64
|
-
*/
|
|
65
|
-
const SCREEN_LOT_BUDGET = 1_300_000;
|
|
66
|
-
/**
|
|
67
|
-
* Allocation d'un lot à fiche `spec: true` (L188/L191), en plus du budget dérivé de l'estimate : sessions spec, fiche et accept hors de
|
|
68
|
-
* l'estimate d'écriture. Relevée de 350 k à 450 k (L218) : la chaîne avant implement du lot spec warhammer L120 a coûté 388 k (350 k : pire cas
|
|
69
|
-
* mesuré des étapes d'un lot à écran, ol L113 : 349 k) ; cadence L217 n'en a pris que 176 k.
|
|
70
|
-
*/
|
|
71
|
-
export const SPEC_STAGE_ALLOCATION = 450_000;
|
|
72
|
-
/** Base mesurée d'un budget, pour `--dry-run` : l'estimate (et l'étape spec) d'où il sort. */
|
|
73
|
-
export function lotBudgetBasis(estimate, stage, escalationModel) {
|
|
74
|
-
const base = ordinaryBasis(estimate, stage);
|
|
75
|
-
return escalationModel ? `${base}, plus ${ESCALATION_RESERVE} réservés à l'escalade ${escalationModel}` : base;
|
|
76
|
-
}
|
|
77
|
-
function ordinaryBasis(estimate, stage) {
|
|
78
|
-
if (stage === 'screen' && lotBudget(estimate, stage) === SCREEN_LOT_BUDGET)
|
|
79
|
-
return "p90 mesuré du total d'un lot à écran, étapes spec, maquette et accept comprises";
|
|
80
|
-
const ordinary = lotBudget(estimate);
|
|
81
|
-
const basis = estimate <= 0.5 ? "plancher d'un lot jusqu'à 0,5 j" : estimate <= 1 ? "p90 mesuré du total d'un lot de 0,5 à 1 j" : Math.round(estimate * LOT_BUDGET_PER_DAY) <= LOT_BUDGET_ONE_DAY ? `plancher de ${LOT_BUDGET_ONE_DAY} au-delà d'un jour, l'estimate × ${LOT_BUDGET_PER_DAY} ne le dépasse pas encore` : `${LOT_BUDGET_PER_DAY} par jour, p90 mesuré du total au-delà d'un jour`;
|
|
82
|
-
if (!stage)
|
|
83
|
-
return basis;
|
|
84
|
-
return `${ordinary} dérivés de l'estimate (${basis}), plus ${SPEC_STAGE_ALLOCATION} pour les étapes spec, ${stage === 'screen' ? 'maquette' : 'fiche'} et accept`;
|
|
85
|
-
}
|
|
86
|
-
/** Budget d'un lot, dérivé de son estimate et de sa catégorie : empêche quelques lots d'avaler le budget de la vague au détriment des autres. Un lot à `screen:` vaut le total mesuré d'un lot à écran (L194) ; un lot à `spec: true` y ajoute l'allocation de son étape (L191) ; `escalation` (orchestrate.escalation déclarée) y ajoute la part de l'escalade (L349). */
|
|
87
|
-
export const lotBudget = (estimate, stage, escalation = false) => lotBaseBudget(estimate, stage) + (escalation ? ESCALATION_RESERVE : 0);
|
|
88
|
-
const lotBaseBudget = (estimate, stage) => {
|
|
89
|
-
const ordinary = estimate <= 0.5 ? LOT_BUDGET_FLOOR : Math.max(LOT_BUDGET_ONE_DAY, Math.round(estimate * LOT_BUDGET_PER_DAY));
|
|
90
|
-
if (stage === 'screen')
|
|
91
|
-
return Math.max(SCREEN_LOT_BUDGET, ordinary + SPEC_STAGE_ALLOCATION);
|
|
92
|
-
return ordinary + (stage === 'spec' ? SPEC_STAGE_ALLOCATION : 0);
|
|
93
|
-
};
|
|
48
|
+
export { lotBudget, lotBudgetBasis, SPEC_STAGE_ALLOCATION } from './lot-budget.js';
|
|
49
|
+
import { lotBudget, lotBudgetBasis } from './lot-budget.js';
|
|
94
50
|
/** Sessions simultanées, toutes vagues confondues (`--max-sessions`, ou CADENCE_MAX_SESSIONS). */
|
|
95
51
|
export const DEFAULT_MAX_SESSIONS = 2;
|
|
96
52
|
/** `1500000`, `1.5M`, `800k`. */
|
|
@@ -137,7 +93,7 @@ function lotBudgetArg(text) {
|
|
|
137
93
|
return { project: target.project, lot: target.lot, raise, text: m[2] };
|
|
138
94
|
}
|
|
139
95
|
export function parseOrchestrateArgs(argv) {
|
|
140
|
-
const a = { lots: [], lotBudgets: [], dryRun: false, waits: false, watch: false, continue: false, answers: [], drop: [], stopAfterCurrent: false, integrate: [], abandonCommits: false };
|
|
96
|
+
const a = { lots: [], lotBudgets: [], dryRun: false, waits: false, watch: false, continue: false, answers: [], drop: [], stopAfterCurrent: false, add: [], integrate: [], abandonCommits: false };
|
|
141
97
|
const optional = (i) => (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-') && !LOT_LIKE.test(argv[i + 1]) ? argv[i + 1] : undefined);
|
|
142
98
|
for (let i = 0; i < argv.length; i++) {
|
|
143
99
|
const t = argv[i];
|
|
@@ -191,6 +147,8 @@ export function parseOrchestrateArgs(argv) {
|
|
|
191
147
|
a.drop.push(lotArg(value(t)));
|
|
192
148
|
else if (t === '--stop-after-current')
|
|
193
149
|
a.stopAfterCurrent = true;
|
|
150
|
+
else if (t === '--add')
|
|
151
|
+
a.add.push(lotArg(value(t)));
|
|
194
152
|
else if (t === '--integrate')
|
|
195
153
|
a.integrate.push(lotArg(value(t)));
|
|
196
154
|
else if (t === '--replay')
|
|
@@ -230,12 +188,12 @@ export function projectEnv(repo) {
|
|
|
230
188
|
return { config: readOrchestrateConfig(configPath), loadPlan, configPath };
|
|
231
189
|
}
|
|
232
190
|
const isProject = (dir) => existsSync(join(dir, PLAN_DEFAULT)) || existsSync(join(dir, PLAN_LEGACY)) || (existsSync(join(dir, 'cadence.yaml')) && /^plan\s*:/m.test(readFileSync(join(dir, 'cadence.yaml'), 'utf8')));
|
|
233
|
-
function resolveTargets(args, io, refusals) {
|
|
191
|
+
export function resolveTargets(args, io, refusals) {
|
|
234
192
|
const out = [];
|
|
235
193
|
const seen = new Set();
|
|
236
194
|
for (const l of args.lots) {
|
|
237
195
|
const projectDir = l.project ? resolve(io.cwd, l.project) : io.cwd;
|
|
238
|
-
const project =
|
|
196
|
+
const project = projectName(io.cwd, l.project); // L421 : `.:L2` depuis le dépôt et `L2` désignent le même projet que les lots de la vague
|
|
239
197
|
const key = lotKey(project, l.lot);
|
|
240
198
|
if (seen.has(key)) {
|
|
241
199
|
refusals.push(`${key} : lot donné deux fois`);
|
|
@@ -259,9 +217,6 @@ function resolveTargets(args, io, refusals) {
|
|
|
259
217
|
}
|
|
260
218
|
return out;
|
|
261
219
|
}
|
|
262
|
-
async function trackedDirty(repo) {
|
|
263
|
-
return (await snapshot(repo, { remote: false })).tracked.map((l) => l.slice(3));
|
|
264
|
-
}
|
|
265
220
|
/** `--effort` date de claude 2.1.284 (L137). Une version illisible n'est pas refusée. */
|
|
266
221
|
function olderThanEffort(version) {
|
|
267
222
|
const m = /(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
@@ -275,7 +230,7 @@ function notLoggedIn(deps) {
|
|
|
275
230
|
return deps.claudeAuth?.()?.loggedIn === false ? "claude n'est pas connecté : lancer `claude` puis /login (état : `claude auth status`)" : null;
|
|
276
231
|
}
|
|
277
232
|
/** Préconditions : tout ce qui peut être refusé l'est ici, avant d'agir, sans verrou ni écriture. */
|
|
278
|
-
async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
233
|
+
export async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
279
234
|
const refusals = [];
|
|
280
235
|
if (io.env.CADENCE_ORCHESTRATED)
|
|
281
236
|
refusals.push(`orchestrate ne se lance pas depuis une session orchestrée (vague ${io.env.CADENCE_ORCHESTRATED})`);
|
|
@@ -290,8 +245,16 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
290
245
|
const lots = [];
|
|
291
246
|
const repoChecked = new Set();
|
|
292
247
|
const byProject = new Map();
|
|
248
|
+
// L421 : les lots déjà portés par la vague qui reçoit l'ajout comptent comme « plus tôt dans la vague » pour les dépendances.
|
|
249
|
+
for (const l of opts.addTo?.lots ?? [])
|
|
250
|
+
byProject.set(l.project, [...(byProject.get(l.project) ?? []), l.lot]);
|
|
293
251
|
for (const t of targets) {
|
|
294
252
|
const key = lotKey(t.project, t.lot);
|
|
253
|
+
const already = opts.addTo?.lots.find((l) => l.project === t.project && l.lot === t.lot);
|
|
254
|
+
if (already) {
|
|
255
|
+
refusals.push(`${key} : lot déjà ${already.queued ? "en file d'ajout dans" : 'dans'} la vague ${opts.addTo.wave}`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
295
258
|
let env;
|
|
296
259
|
let plan;
|
|
297
260
|
try {
|
|
@@ -345,18 +308,7 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
345
308
|
if (repoChecked.has(repo))
|
|
346
309
|
continue;
|
|
347
310
|
repoChecked.add(repo);
|
|
348
|
-
|
|
349
|
-
if (dirty.length)
|
|
350
|
-
refusals.push(`${basename(repo)} : arbre sale, ${dirty.length} fichier(s) suivi(s) modifié(s) : ${dirty.join(', ')}`);
|
|
351
|
-
const locked = indexLockRefusal(repo, io.now(), deps.gitRunning ?? gitProcessRunning);
|
|
352
|
-
if (locked)
|
|
353
|
-
refusals.push(`${basename(repo)} : ${locked}`);
|
|
354
|
-
const hook = canInstallPrePush(repo);
|
|
355
|
-
if (hook)
|
|
356
|
-
refusals.push(`${basename(repo)} : ${hook}`);
|
|
357
|
-
const busy = activeLock(join(sharedStateDir(repo), REPO_LOCK));
|
|
358
|
-
if (busy && !opts.resume)
|
|
359
|
-
refusals.push(`${basename(repo)} : une orchestration y est déjà en cours (${busy.wave}, pid ${busy.pid})`);
|
|
311
|
+
refusals.push(...(await repoRefusals(repo, t, { io, deps, launch }, opts)));
|
|
360
312
|
}
|
|
361
313
|
const node = resolveNode(t.repo, nvmVersionsDir(io.env));
|
|
362
314
|
if (node.kind === 'missing') {
|
|
@@ -670,39 +622,6 @@ function waveExists(wave, io) {
|
|
|
670
622
|
io.err(`orchestrate : --wave ${wave} : cette vague existe déjà`);
|
|
671
623
|
return 2;
|
|
672
624
|
}
|
|
673
|
-
/**
|
|
674
|
-
* `--remove-worktree projet:lot` (L279, R20) : seule commande qui retire un worktree conservé. Refus (code 2, rien retiré) pour le worktree d'une vague vivante (même avec `--abandon-commits`), et tant qu'un commit du lot n'est ni
|
|
675
|
-
* intégré à la branche du dépôt ni confirmé abandonné par `--abandon-commits` ; un worktree entièrement intégré se retire sans option. `--wave` désigne la vague
|
|
676
|
-
* quand plusieurs ont laissé un worktree pour ce lot.
|
|
677
|
-
*/
|
|
678
|
-
function removeWorktree(args, io) {
|
|
679
|
-
const t = args.removeWorktree;
|
|
680
|
-
const key = `${t.project ?? basename(io.cwd)}:${t.lot}`;
|
|
681
|
-
const projectDir = t.project ? resolve(io.cwd, t.project) : io.cwd;
|
|
682
|
-
const repo = existsSync(projectDir) ? gitRoot(projectDir) : null;
|
|
683
|
-
const kept = repo ? keptTrees(repo, { includeActive: true }).filter((k) => k.lot === key && (args.wave === undefined || k.wave === args.wave)) : [];
|
|
684
|
-
if (!repo || kept.length === 0) {
|
|
685
|
-
io.err(`orchestrate : ${key} : aucun worktree conservé`);
|
|
686
|
-
return 2;
|
|
687
|
-
}
|
|
688
|
-
if (kept.length > 1) {
|
|
689
|
-
io.err(`orchestrate : ${key} : plusieurs worktrees conservés (vagues ${kept.map((k) => k.wave).join(', ')}) : précisez --wave`);
|
|
690
|
-
return 2;
|
|
691
|
-
}
|
|
692
|
-
const k = kept[0];
|
|
693
|
-
if (k.active) {
|
|
694
|
-
// une vague vivante joue encore ce lot (ses sessions travaillent dans ce dossier) : refus sans rien retirer, même avec --abandon-commits
|
|
695
|
-
io.err(`orchestrate : ${key} : la vague ${k.wave} est vivante et joue ce lot : worktree non retirable (arrêter la vague d'abord)`);
|
|
696
|
-
return 2;
|
|
697
|
-
}
|
|
698
|
-
const gone = removeTree({ repo, dir: k.dir ?? '', branch: k.branch, base: k.base }, { abandon: args.abandonCommits });
|
|
699
|
-
if (!gone.ok) {
|
|
700
|
-
io.err(gone.dirty ? `orchestrate : ${key} : fichiers suivis modifiés dans le worktree (${gone.dirty.join(', ')}), --abandon-commits pour les abandonner` : gone.untracked ? `orchestrate : ${key} : fichiers non suivis dans le worktree (${gone.untracked.join(', ')}), --abandon-commits pour les abandonner` : `orchestrate : ${key} : ${gone.unintegrated} commit(s) non intégré(s), --abandon-commits pour les abandonner`);
|
|
701
|
-
return 2;
|
|
702
|
-
}
|
|
703
|
-
io.out(`orchestrate : ${key} : worktree retiré (${gone.abandoned} commit(s) abandonné(s))`);
|
|
704
|
-
return 0;
|
|
705
|
-
}
|
|
706
625
|
/** L382 : `claude auth status` peut durer jusqu'à 20 s ; en --continue, preflight() le relancerait pour chaque lot candidat de chaque tour. Une seule lecture par lancement (une vague). */
|
|
707
626
|
function onceAuth(deps) {
|
|
708
627
|
const read = deps.claudeAuth;
|
|
@@ -711,11 +630,47 @@ function onceAuth(deps) {
|
|
|
711
630
|
let cached;
|
|
712
631
|
return { ...deps, claudeAuth: () => (cached ??= { value: read() }).value };
|
|
713
632
|
}
|
|
633
|
+
/** `--status` (L395) : le tableau des vagues vivantes et de la vague suivie, une fois ou rafraîchi avec `--watch` jusqu'à ce que plus rien ne tourne. */
|
|
634
|
+
async function statusCommand(args, io, deps, launch) {
|
|
635
|
+
maxSessions(args, io); // une valeur invalide est refusée ici aussi
|
|
636
|
+
const view = () => statusView(args, launch, configuredCap(io.env, args.maxSessions), io.now());
|
|
637
|
+
if (!args.watch) {
|
|
638
|
+
const { live, table } = view();
|
|
639
|
+
printStatus(io, live, table); // sans identifiant (pas de tableau) : les vagues vivantes suffisent
|
|
640
|
+
return 0;
|
|
641
|
+
}
|
|
642
|
+
// --watch : le même tableau, rafraîchi ; il s'arrête seul quand plus aucune vague ne tourne (le dernier rendu reste affiché)
|
|
643
|
+
const sleep = deps.watchSleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
|
|
644
|
+
const seconds = args.interval ?? DEFAULT_WATCH_INTERVAL;
|
|
645
|
+
for (;;) {
|
|
646
|
+
const { live, table, running } = view();
|
|
647
|
+
io.out(CLEAR_SCREEN);
|
|
648
|
+
printStatus(io, live, table);
|
|
649
|
+
if (!running)
|
|
650
|
+
return 0;
|
|
651
|
+
io.out('');
|
|
652
|
+
io.out(`rafraîchi toutes les ${seconds} s — Ctrl-C pour quitter`);
|
|
653
|
+
await sleep(seconds * 1000);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/** Les vagues vivantes, puis (s'il y en a un) le tableau de la vague suivie, séparés d'une ligne vide. */
|
|
657
|
+
function printStatus(io, live, table) {
|
|
658
|
+
for (const line of live)
|
|
659
|
+
io.out(line);
|
|
660
|
+
if (!table)
|
|
661
|
+
return;
|
|
662
|
+
if (live.length)
|
|
663
|
+
io.out('');
|
|
664
|
+
for (const line of table)
|
|
665
|
+
io.out(line);
|
|
666
|
+
}
|
|
714
667
|
/** Point d'entrée de `cadence orchestrate`. 0 prêts · 1 rendus au lead · 2 refus avant d'agir · 3 suspendue (budget, quota). */
|
|
715
668
|
export async function orchestrate(argv, io, deps) {
|
|
716
669
|
deps = onceAuth(deps);
|
|
717
670
|
const args = parseOrchestrateArgs(argv);
|
|
718
671
|
const launch = io.cwd;
|
|
672
|
+
if (args.add.length && addNotAlone(args))
|
|
673
|
+
throw new RafError('--add ne se combine avec aucune autre action (seul --wave désigne la vague qui reçoit les lots)');
|
|
719
674
|
const today = io.env.RAF_TODAY && isDay(io.env.RAF_TODAY) ? io.env.RAF_TODAY : toDay(io.now());
|
|
720
675
|
if ((args.until !== undefined || args.priority !== undefined) && !args.continue)
|
|
721
676
|
throw new RafError('--until et --priority s\'utilisent avec --continue');
|
|
@@ -767,50 +722,20 @@ export async function orchestrate(argv, io, deps) {
|
|
|
767
722
|
io.out(line);
|
|
768
723
|
return 0;
|
|
769
724
|
}
|
|
770
|
-
if (args.status !== undefined)
|
|
771
|
-
|
|
772
|
-
if (!args.watch) {
|
|
773
|
-
const { live, table } = statusView(args, launch, configuredCap(io.env, args.maxSessions), io.now());
|
|
774
|
-
for (const line of live)
|
|
775
|
-
io.out(line);
|
|
776
|
-
if (!table)
|
|
777
|
-
return 0; // sans identifiant : les vagues vivantes suffisent
|
|
778
|
-
if (live.length)
|
|
779
|
-
io.out('');
|
|
780
|
-
for (const line of table)
|
|
781
|
-
io.out(line);
|
|
782
|
-
return 0;
|
|
783
|
-
}
|
|
784
|
-
// --watch : le même tableau, rafraîchi ; il s'arrête seul quand plus aucune vague ne tourne (le dernier rendu reste affiché)
|
|
785
|
-
const sleep = deps.watchSleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
|
|
786
|
-
for (;;) {
|
|
787
|
-
const { live, table, running } = statusView(args, launch, configuredCap(io.env, args.maxSessions), io.now());
|
|
788
|
-
io.out(CLEAR_SCREEN);
|
|
789
|
-
for (const line of live)
|
|
790
|
-
io.out(line);
|
|
791
|
-
if (table) {
|
|
792
|
-
if (live.length)
|
|
793
|
-
io.out('');
|
|
794
|
-
for (const line of table)
|
|
795
|
-
io.out(line);
|
|
796
|
-
}
|
|
797
|
-
if (!running)
|
|
798
|
-
return 0;
|
|
799
|
-
io.out('');
|
|
800
|
-
io.out(`rafraîchi toutes les ${args.interval ?? DEFAULT_WATCH_INTERVAL} s — Ctrl-C pour quitter`);
|
|
801
|
-
await sleep((args.interval ?? DEFAULT_WATCH_INTERVAL) * 1000);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
725
|
+
if (args.status !== undefined)
|
|
726
|
+
return statusCommand(args, io, deps, launch);
|
|
804
727
|
if (args.removeWorktree)
|
|
805
728
|
return removeWorktree(args, io);
|
|
806
729
|
if (args.resume !== undefined)
|
|
807
730
|
return resume(args, argv, io, deps, launch, today);
|
|
731
|
+
if (args.add.length)
|
|
732
|
+
return addOnlyLive(args, io, deps, launch);
|
|
808
733
|
if (args.drop.length || args.stopAfterCurrent)
|
|
809
734
|
return control(args, io);
|
|
810
735
|
if (args.answers.length)
|
|
811
736
|
return answerOnly(args, io);
|
|
812
737
|
if (args.lots.length === 0 && !args.continue)
|
|
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]');
|
|
738
|
+
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] | --add projet:lot [--wave vague] | --drop projet:lot | --stop-after-current | --remove-worktree projet:lot [--abandon-commits]');
|
|
814
739
|
const refusals = [];
|
|
815
740
|
const targets = resolveTargets(args, io, refusals);
|
|
816
741
|
const pre = await preflight(args, targets, io, deps, launch);
|
|
@@ -1029,6 +954,8 @@ async function continueRounds(first, wave, all, store, io, deps, today, args, la
|
|
|
1029
954
|
const cap = maxSessions(args, io);
|
|
1030
955
|
let code = first;
|
|
1031
956
|
for (;;) {
|
|
957
|
+
for (const l of all)
|
|
958
|
+
exclude.add(lotKey(l.project, l.lot)); // L421 : les lots ajoutés à la main pendant la vague ne sont pas tirés une seconde fois
|
|
1032
959
|
const questions = all.filter((l) => l.status === 'question').length;
|
|
1033
960
|
const remaining = wave.status === 'suspended-budget' ? 0 : wave.budget - wave.consumed;
|
|
1034
961
|
const why = stopReason({ now: io.now(), until: args.untilAt, quotaHit: wave.status === 'suspended-quota', questions, streak: handedBackStreak(all), interrupted: wave.status === 'interrupted' && questions === 0, stopRequested: store.control().stopAfterCurrent, remaining });
|
|
@@ -1075,6 +1002,8 @@ function planTrees(ctxs, store, wave, log) {
|
|
|
1075
1002
|
if (why)
|
|
1076
1003
|
log(`orchestrate : ${basename(repo)} : worktrees demandés mais ${why} : lots joués à la suite`);
|
|
1077
1004
|
for (const c of group) {
|
|
1005
|
+
if (c.tree)
|
|
1006
|
+
continue; // L421 : un lot ajouté en cours de vague replanifie son groupe ; les arbres déjà posés (et leur état actif) ne sont pas refaits
|
|
1078
1007
|
const lotBr = lotBranch(wave, c.lot.project, c.lot.lot);
|
|
1079
1008
|
// Chemin de reprise d'un lot rendu hors intégration : sa branche d'une vague passée devient celle de cette vague (recoverTree), puis se retrouve comme une reprise (R11).
|
|
1080
1009
|
const dir = lotTreeDir(store.dir, c.lot.project, c.lot.lot, repo);
|
|
@@ -1240,6 +1169,22 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1240
1169
|
catch {
|
|
1241
1170
|
// registre illisible : le décompte des créneaux libres reste celui des dépôts
|
|
1242
1171
|
}
|
|
1172
|
+
const adds = new AddFeed({
|
|
1173
|
+
store, wave, wctx, io, deps, launch, ctxs, all, lots, held, freed, repos, freeRepo, saveWave, checks: LAUNCH_CHECKS,
|
|
1174
|
+
stopCause: () => (wctx.incident ? 'incident' : wctx.quota.hit ? "limite d'usage atteinte" : budget.exhausted ? 'budget de la vague épuisé' : store.control().stopAfterCurrent ? "la vague s'arrête" : null),
|
|
1175
|
+
budgetLeft: () => budget.limit - budget.consumed,
|
|
1176
|
+
planState: (c) => {
|
|
1177
|
+
try {
|
|
1178
|
+
const lot = projectEnv(c.lot.repo).loadPlan().lots().find((l) => l.id === c.lot.lot);
|
|
1179
|
+
return !lot ? 'lot inconnu' : !isOpen(lot.status) ? `le lot est ${lot.status}` : null;
|
|
1180
|
+
}
|
|
1181
|
+
catch (e) {
|
|
1182
|
+
return e.message;
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
announce: () => updateWaveReposSafe(home, repos.filter((x) => !freed.has(x)), lanes()),
|
|
1186
|
+
build: (t, known) => buildAdd(t, known, { wave: wave.id, ctxs, wctx, store, io, deps, launch }),
|
|
1187
|
+
});
|
|
1243
1188
|
// Ctrl-C, SIGTERM : les sessions sont tuées en bloc, les étapes marquées interrompues, les verrous et hooks libérés.
|
|
1244
1189
|
const forget = onTermination(async () => {
|
|
1245
1190
|
killSessions();
|
|
@@ -1293,7 +1238,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1293
1238
|
}, (lots) => lots.every((c) => c.tree), {
|
|
1294
1239
|
pollMs: deps.slotPollMs ?? ANSWER_POLL_MS,
|
|
1295
1240
|
// 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) => {
|
|
1241
|
+
take: async (canRun) => {
|
|
1297
1242
|
const taken = [];
|
|
1298
1243
|
for (const e of store.answerQueue()) {
|
|
1299
1244
|
const c = ctxs.find((x) => lotKey(x.lot.project, x.lot.lot) === e.lot);
|
|
@@ -1322,9 +1267,11 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1322
1267
|
// registre illisible : le décompte reste celui du dernier écrit
|
|
1323
1268
|
}
|
|
1324
1269
|
}
|
|
1325
|
-
return taken;
|
|
1270
|
+
return [...taken, ...(await adds.take(canRun))];
|
|
1326
1271
|
},
|
|
1327
1272
|
});
|
|
1273
|
+
// L421 : un ajout encore en file quand plus rien ne joue ne sera plus lu : écarté et dit.
|
|
1274
|
+
adds.drain('la vague a fini de jouer');
|
|
1328
1275
|
handBackDroppedQuestions(ctxs); // un lot en question retiré pendant la vague : rendu avant de fixer le statut de la vague
|
|
1329
1276
|
}
|
|
1330
1277
|
finally {
|
|
@@ -1384,6 +1331,37 @@ function answerLive(args, io, store) {
|
|
|
1384
1331
|
}
|
|
1385
1332
|
return 0;
|
|
1386
1333
|
}
|
|
1334
|
+
const LAUNCH_CHECKS = { resolveTargets, preflight: (targets, run, addTo) => preflight({ dryRun: false }, targets, run.io, run.deps, run.launch, { addTo }) };
|
|
1335
|
+
/** `--add` (L421) : la vague vivante de ce dossier reçoit les lots (le travail est dans `ajout.ts`). */
|
|
1336
|
+
async function addOnlyLive(args, io, deps, launch) {
|
|
1337
|
+
const store = liveStore('--add', args, io);
|
|
1338
|
+
return typeof store === 'number' ? store : addToLive(args, { io, deps, launch }, store, LAUNCH_CHECKS);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Le contexte d'un lot ajouté en cours de vague (L421) : le contrôle préalable d'un lot de lancement (les lots de la vague comptent pour les dépendances), le lien du Node,
|
|
1342
|
+
* puis un worktree seulement si tous les lots encore à jouer de son dépôt y sont aussi ; sinon il attend la fin de ceux qui jouent dans l'arbre du dépôt.
|
|
1343
|
+
*/
|
|
1344
|
+
async function buildAdd(t, known, w) {
|
|
1345
|
+
const pre = await LAUNCH_CHECKS.preflight([t], w, { wave: w.wave, lots: known });
|
|
1346
|
+
const lot = pre.lots[0];
|
|
1347
|
+
if (pre.refusals.length || !lot)
|
|
1348
|
+
return { refusals: pre.refusals };
|
|
1349
|
+
if (lot.node)
|
|
1350
|
+
lot.node.link = (w.deps.linkNode ?? linkNodeBin)(w.store.dir, lot.node.version, lot.node.bin);
|
|
1351
|
+
const ctx = contexts([lot], [projectEnv(lot.repo)], w.wctx)[0];
|
|
1352
|
+
const open = openInRepo(w.ctxs, ctx);
|
|
1353
|
+
planTrees(open.every((o) => o.tree) ? [...open, ctx] : [ctx], w.store, w.wave, w.wctx.log);
|
|
1354
|
+
return { ctx };
|
|
1355
|
+
}
|
|
1356
|
+
/** Le registre des vagues vivantes : dépôts tenus et créneaux occupés ; illisible, le décompte reste celui du dernier écrit. */
|
|
1357
|
+
function updateWaveReposSafe(home, repos, lanes) {
|
|
1358
|
+
try {
|
|
1359
|
+
updateWaveRepos(home, process.pid, repos, lanes);
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
// registre illisible : le décompte reste celui du dernier écrit
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1387
1365
|
/** La vague vivante, pour `--answer` seul (sans --resume) : la même recherche que `--drop`. */
|
|
1388
1366
|
function answerOnly(args, io) {
|
|
1389
1367
|
if (args.drop.length || args.stopAfterCurrent || args.budget !== undefined || args.dryRun || args.continue || args.lots.length)
|
|
@@ -10,7 +10,7 @@ import { planLoader, readDocsConfig, readPlanConfig } from '../config.js';
|
|
|
10
10
|
import { PLAN_ACTIONS, leadAnswerMessage, planCommitMessage } from '../plan-commit.js';
|
|
11
11
|
import { articleRepoDir, articlesBrief, docSyncBrief, docSyncGaps, filesOf, gapMessage } from '../docsync.js';
|
|
12
12
|
import { testGateMissing } from '../testgate.js';
|
|
13
|
-
import { citesPriorWork, precheckNothingToFind } from './precheck.js';
|
|
13
|
+
import { citesPriorWork, keepPrecheckReading, localYesNote, precheckNothingToFind } from './precheck.js';
|
|
14
14
|
import { PROBE_FAILED, PROBE_RESERVE, probeStep, probeVars, scheduleProbe } from './probe.js';
|
|
15
15
|
import { changedFiles, dirtyPaths, gitCommonDir, isAncestor, readCommits, resolveCommit, restoreWorktree, tryGit } from '../git.js';
|
|
16
16
|
import { blockingLots, gitCommitsIn } from './table.js';
|
|
@@ -24,7 +24,7 @@ import { TEST_WORKERS_ENV, testWorkersText } from './workers.js';
|
|
|
24
24
|
import { baselineCandidates, gateLabel, gateReplayable, gateUnnamed, redAloneGate, redCandidates, replayGate, runGate } from './gate-parts.js';
|
|
25
25
|
import { withPlace } from './gate-place.js';
|
|
26
26
|
import { APP_TIMEOUT_S, startApp } from './app.js';
|
|
27
|
-
import { capturesText, gateText, headline, integrationText, localReading, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tiersText, tmpText } from './briefs.js';
|
|
27
|
+
import { capturesText, gateText, headline, integrationText, localReading, precheckBrief, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tiersText, tmpText } from './briefs.js';
|
|
28
28
|
import { SlotAbandoned } from './registry.js';
|
|
29
29
|
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig, enabledPlugins, settingsFiles } from './launch.js';
|
|
30
30
|
import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
|
|
@@ -37,11 +37,12 @@ import { avalFicheText, ficheContracts, ficheRules, hasScreenPart } from './aval
|
|
|
37
37
|
import { ficheReviewText, reviewedSheetText } from './fiche.js';
|
|
38
38
|
import { bulletLines, maquetteText, reviewedText } from './maquette.js';
|
|
39
39
|
import { answeredText, checkSpec, costlyHypotheses, ficheHasBack, ficheReady, findFiche, hypothesisLines, answerAltersRule, hypothesisNote, LEAD_SOURCE, ruleIdsSourcedByLead, sourceRules, specText } from './spec.js';
|
|
40
|
+
import { handBackWithoutFinding } from './no-finding.js';
|
|
40
41
|
import { decisionsText, humanDecisions } from './decisions.js';
|
|
41
42
|
import { LOCAL_PRECHECK_UNREADABLE, PRECHECK_UNREADABLE, lotKey, lotRepoPaths, neighbourDirs } from './state.js';
|
|
42
43
|
import { advance, baseMoved, INTEGRATE_WAIT_MS, MAX_REPLAYS, mergeBase, patchId, rebaseOnto, repoBranch, takeRepoWrite } from './integrate.js';
|
|
43
44
|
import { mutexFor } from './mutex.js';
|
|
44
|
-
import { readTier, takeTiers, tierMoved, tierRepos, wipBranch } from './tiers.js';
|
|
45
|
+
import { readTier, stillThird, takeTiers, tierMoved, tierRepos, wipBranch } from './tiers.js';
|
|
45
46
|
import { branchExists, git, gitFailure, integrated, linkIgnored, lotBranch, planFileSet, planSeenFrom, prepareTree, removeTree, trackedDirty } from './worktree.js';
|
|
46
47
|
/** Passes de correction au plus, puis la main est rendue au lead. */
|
|
47
48
|
export const MAX_PASSES = 2;
|
|
@@ -593,8 +594,10 @@ function briefFor(c, kind, local = false) {
|
|
|
593
594
|
? `A previous session was interrupted; resume from these commits of the lot already present:\n${mine}`
|
|
594
595
|
: 'A previous session was interrupted before any commit of the lot; start again from the current state of the repository.';
|
|
595
596
|
}
|
|
596
|
-
|
|
597
|
-
|
|
597
|
+
// La lecture ne va qu'à la première passe implement : la session en cours (« running ») ne compte pas, toute autre passe implement déjà jouée l'exclut.
|
|
598
|
+
const firstPass = !l.steps.some((s) => s.kind === 'implement' && s.status !== 'running');
|
|
599
|
+
if (kind === 'implement')
|
|
600
|
+
vars.commits = precheckBrief(vars.commits, l.precheck, firstPass ? l.precheckLu : undefined);
|
|
598
601
|
if (l.pendingAnswer)
|
|
599
602
|
vars.reponse = `Answer from the human to your earlier question: ${l.pendingAnswer}`;
|
|
600
603
|
}
|
|
@@ -1085,7 +1088,7 @@ function checkTiers(c, kind, step, before) {
|
|
|
1085
1088
|
const w = c.wave;
|
|
1086
1089
|
const key = lotKey(l.project, l.lot);
|
|
1087
1090
|
const wip = wipBranch(l.project, l.lot);
|
|
1088
|
-
for (const [repo, b] of before) {
|
|
1091
|
+
for (const [repo, b] of stillThird(before, w.store.lots(), key)) {
|
|
1089
1092
|
const name = basename(repo);
|
|
1090
1093
|
let a;
|
|
1091
1094
|
try {
|
|
@@ -1367,6 +1370,8 @@ async function precheck(c) {
|
|
|
1367
1370
|
localYes = true;
|
|
1368
1371
|
rep = null;
|
|
1369
1372
|
}
|
|
1373
|
+
else if (rep)
|
|
1374
|
+
rep.lu = undefined; // « non » ou « partiel » du local, accepté sans relecture par Sonnet : sa lecture n'est pas gardée (L523)
|
|
1370
1375
|
}
|
|
1371
1376
|
}
|
|
1372
1377
|
if (!rep) {
|
|
@@ -1383,11 +1388,8 @@ async function precheck(c) {
|
|
|
1383
1388
|
return;
|
|
1384
1389
|
}
|
|
1385
1390
|
}
|
|
1386
|
-
if (localYes)
|
|
1387
|
-
l.warnings.push(rep.dejaPresent
|
|
1388
|
-
? 'contrôle préalable : « oui » du modèle local, confirmé par Sonnet'
|
|
1389
|
-
: `contrôle préalable : le modèle local a dit « oui », Sonnet dit « ${rep.dejaPresent} » — le local s'est trompé, repasser orchestrate.precheck à true`);
|
|
1390
|
-
}
|
|
1391
|
+
if (localYes)
|
|
1392
|
+
l.warnings.push(localYesNote(rep.dejaPresent));
|
|
1391
1393
|
const proofs = rep.preuves.length ? ` (${rep.preuves.join(' ; ')})` : '';
|
|
1392
1394
|
// « oui » sans preuve ne se vérifie pas : il vaut « partiel », l'implémentation part avec le constat.
|
|
1393
1395
|
const unproven = rep.dejaPresent === 'oui' && rep.preuves.length === 0;
|
|
@@ -1402,6 +1404,7 @@ async function precheck(c) {
|
|
|
1402
1404
|
l.precheck = `A pre-check found part of the deliverable already in the repository (do not redo it): ${rep.resume.trim()}${proofs}`;
|
|
1403
1405
|
l.warnings.push(`contrôle préalable : livrable en partie présent — ${rep.resume.trim()}${proofs}`);
|
|
1404
1406
|
}
|
|
1407
|
+
keepPrecheckReading(l, rep.lu);
|
|
1405
1408
|
l.next = firstWrite(c);
|
|
1406
1409
|
save(c);
|
|
1407
1410
|
}
|
|
@@ -2015,6 +2018,8 @@ async function runChecks(c, red, runs, place) {
|
|
|
2015
2018
|
if (!command)
|
|
2016
2019
|
continue;
|
|
2017
2020
|
l.gate = { label, started: new Date().toISOString() };
|
|
2021
|
+
if (label === 'tests')
|
|
2022
|
+
l.suiteRuns = (l.suiteRuns ?? 0) + 1;
|
|
2018
2023
|
save(c);
|
|
2019
2024
|
const r = await runGate(place.run, command); // porte en parties (L512) : toutes jouées, chacune dans son dossier ; le CheckRun porte le libellé « dir : run ; … »
|
|
2020
2025
|
const run = { label, command: gateLabel(command), code: r.code, ...place.stamp() };
|
|
@@ -2246,7 +2251,7 @@ async function work(c, kind) {
|
|
|
2246
2251
|
else if (made === 0)
|
|
2247
2252
|
l.warnings.push(kind === 'fix' ? 'fix sans nouveau commit : revue lancée sur les commits du lot, avec la raison de la session (choix)' : `${kind} sans nouveau commit : revue lancée sur les commits déjà faits du lot`);
|
|
2248
2253
|
const red = [];
|
|
2249
|
-
// Tests annoncés rouges (L213) : avec orchestrate.test, le programme rejoue et compare à la ligne de base, la session n'est crue que si les tests y sont rouges autrement que par des échecs de départ ; sans, la liste des échecs de base de la session, non vérifiée.
|
|
2254
|
+
// Tests annoncés rouges (L213) : avec orchestrate.test, le programme rejoue et compare à la ligne de base, la session n'est crue que si les tests y sont rouges autrement que par des échecs de départ, et jamais quand sa porte est verte (L522, quels que soient echecsDeBase et nouveauxEchecs) ; sans, la liste des échecs de base de la session, non vérifiée.
|
|
2250
2255
|
const claimed = { source: 'tests', gravite: 'bloquant', texte: `tests annoncés rouges par la session : ${rep.tests.commande} — ${rep.tests.resultat}` };
|
|
2251
2256
|
let deferred = false;
|
|
2252
2257
|
const declared = (rep.echecsDeBase ?? []).map((t) => t.trim()).filter(Boolean);
|
|
@@ -2278,12 +2283,8 @@ async function work(c, kind) {
|
|
|
2278
2283
|
}
|
|
2279
2284
|
if (red.length === 0)
|
|
2280
2285
|
await programChecks(c, red, runs);
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
const gateGreen = runs.some((r) => r.label === 'tests' && r.code === 0);
|
|
2284
|
-
const baseOnly = declared.length > 0 && !!newFailures && !newFailures.length;
|
|
2285
|
-
if (deferred && red.length === 0 && gateGreen && baseOnly)
|
|
2286
|
-
l.warnings.push(`tests annoncés rouges par la session, mais la porte du programme verte (${gateLabel(c.config.test ?? '')}, code 0) et aucun nouvel échec déclaré (nouveauxEchecs vide) : les échecs sont ceux de la base qu'elle déclare (${declared.length}) ; pas de correction`);
|
|
2286
|
+
if (deferred && red.length === 0 && runs.some((r) => r.label === 'tests' && r.code === 0 && !r.instables?.length))
|
|
2287
|
+
l.warnings.push(`tests annoncés rouges par la session, mais la porte du programme verte (${gateLabel(c.config.test ?? '')}, code 0) : l'annonce n'est pas crue, pas de correction`);
|
|
2287
2288
|
else if (deferred && red.length === 0 && !runs.some((r) => r.label === 'tests' && (r.dejaRouges || r.instables?.length)))
|
|
2288
2289
|
red.push(claimed);
|
|
2289
2290
|
if (red.length) {
|
|
@@ -2518,6 +2519,8 @@ async function reviewStep(c, kind) {
|
|
|
2518
2519
|
// L'UX est la dernière étape (L221) : conforme, le lot conclut avec la revue de code ; sinon une correction (jamais une revue de code d'office), la revue de code et l'UX rejouée suivent.
|
|
2519
2520
|
if (summary.conforme)
|
|
2520
2521
|
return () => conclude(c, l.code, noCommitNote(l));
|
|
2522
|
+
if (handBackWithoutFinding(c, l.constats, summary, stop))
|
|
2523
|
+
return;
|
|
2521
2524
|
toFix(c, l.constats, 'UX non conforme');
|
|
2522
2525
|
return;
|
|
2523
2526
|
}
|
|
@@ -2586,6 +2589,8 @@ async function reviewStep(c, kind) {
|
|
|
2586
2589
|
return;
|
|
2587
2590
|
}
|
|
2588
2591
|
const constats = blockingConstats(rep, 'code');
|
|
2592
|
+
if (handBackWithoutFinding(c, constats, summary, stop))
|
|
2593
|
+
return;
|
|
2589
2594
|
(l.reviewCounts ??= []).push(constats.length);
|
|
2590
2595
|
toFix(c, constats, 'revue non conforme');
|
|
2591
2596
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { basename, join } from 'node:path';
|
|
2
|
+
import { sharedStateDir } from '../state.js';
|
|
3
|
+
import { addHint } from './ajout.js';
|
|
4
|
+
import { canInstallPrePush, gitProcessRunning, indexLockRefusal, snapshot } from './guard.js';
|
|
5
|
+
import { activeLock, REPO_LOCK } from './lock.js';
|
|
6
|
+
/** Les fichiers suivis modifiés d'un dépôt. */
|
|
7
|
+
export async function trackedDirty(repo) {
|
|
8
|
+
return (await snapshot(repo, { remote: false })).tracked.map((l) => l.slice(3));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Les refus propres à un dépôt avant qu'une vague y joue (arbre sale, `index.lock`, hook de push, orchestration en cours), les mêmes pour un lot lancé, repris ou ajouté.
|
|
12
|
+
* L421 : le dépôt que tient la vague qui reçoit l'ajout (`addTo`) est le sien : son arbre porte le travail de ses lots et son hook de push est le sien. `resume` : un dépôt tenu n'est pas refusé.
|
|
13
|
+
*/
|
|
14
|
+
export async function repoRefusals(repo, t, run, opts) {
|
|
15
|
+
const busy = activeLock(join(sharedStateDir(repo), REPO_LOCK));
|
|
16
|
+
const ours = !!opts.addTo && busy?.wave === opts.addTo.wave;
|
|
17
|
+
const name = basename(repo);
|
|
18
|
+
const out = [];
|
|
19
|
+
if (!ours) {
|
|
20
|
+
const dirty = await trackedDirty(repo);
|
|
21
|
+
if (dirty.length)
|
|
22
|
+
out.push(`${name} : arbre sale, ${dirty.length} fichier(s) suivi(s) modifié(s) : ${dirty.join(', ')}`);
|
|
23
|
+
const locked = indexLockRefusal(repo, run.io.now(), run.deps.gitRunning ?? gitProcessRunning);
|
|
24
|
+
if (locked)
|
|
25
|
+
out.push(`${name} : ${locked}`);
|
|
26
|
+
const hook = canInstallPrePush(repo);
|
|
27
|
+
if (hook)
|
|
28
|
+
out.push(`${name} : ${hook}`);
|
|
29
|
+
}
|
|
30
|
+
if (busy && !opts.resume && !ours)
|
|
31
|
+
out.push(`${name} : une orchestration y est déjà en cours (${busy.wave}, pid ${busy.pid})${addHint(t, busy.wave, run.launch)}`);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
@@ -3,13 +3,19 @@ import { lotKey } from './state.js';
|
|
|
3
3
|
/** Trace des tests instables sous charge (L354) : le lot (`instables`, journal et `cadence perf`), un avertissement du lot (tableau de fin de vague) et une ligne du journal de la vague. */
|
|
4
4
|
export function noteUnstable(c, tests) {
|
|
5
5
|
const l = c.lot;
|
|
6
|
-
const fresh = tests.filter((t) => !(l.instables ?? []).includes(t));
|
|
6
|
+
const fresh = [...new Set(tests)].filter((t) => !(l.instables ?? []).includes(t));
|
|
7
7
|
if (!fresh.length)
|
|
8
8
|
return;
|
|
9
9
|
l.instables = [...(l.instables ?? []), ...fresh];
|
|
10
10
|
const text = `${fresh.length} test(s) instable(s) sous charge (rouge(s) dans la suite, vert(s) rejoué(s) seul(s)), pas de passe de correction : ${fresh.join(' ; ')}`;
|
|
11
11
|
l.warnings.push(text);
|
|
12
12
|
c.wave.log(`${lotKey(l.project, l.lot)} · ${text}`);
|
|
13
|
+
// Le rouge de charge ne coûte plus de passe, mais la dette reste au lead : un lot à stabiliser (L522).
|
|
14
|
+
for (const t of fresh) {
|
|
15
|
+
const proposal = `[instable] ${t} — rouge dans la suite, vert rejoué seul : à stabiliser`;
|
|
16
|
+
if (!l.proposals.includes(proposal))
|
|
17
|
+
l.proposals.push(proposal);
|
|
18
|
+
}
|
|
13
19
|
c.wave.store.writeLot(l);
|
|
14
20
|
}
|
|
15
21
|
const seconds = (ms) => `${Math.round(ms / 100) / 10} s`.replace(/\.0 s$/, ' s');
|