@sylad/cadence 1.12.1 → 1.13.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/bin/cadence.js +8 -1
- package/dist/cli.js +23 -2
- package/dist/config-set.js +249 -0
- package/dist/config.js +4 -2
- package/dist/corrections.js +1 -0
- package/dist/git.js +31 -0
- package/dist/orchestrate/briefs.js +1 -1
- package/dist/orchestrate/command.js +23 -9
- package/dist/orchestrate/continue.js +5 -2
- package/dist/orchestrate/cycle.js +79 -14
- package/dist/orchestrate/decisions.js +31 -0
- package/dist/orchestrate/guard.js +16 -0
- package/dist/orchestrate/launch.js +2 -1
- package/dist/orchestrate/workers.js +20 -0
- package/dist/perf.js +3 -1
- package/dist/release.js +245 -0
- package/docs/fonctionnement.md +1 -0
- package/docs/lead.md +16 -0
- package/docs/orchestrate.md +15 -3
- package/docs/overview.md +2 -0
- package/docs/perf.md +1 -0
- package/docs/raf.md +1 -0
- package/docs/releasing.md +40 -1
- package/docs/whats-new.md +3 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
- package/templates/orchestrate/fix-docs.md +1 -0
- package/templates/orchestrate/fix-minors.md +1 -0
- package/templates/orchestrate/fix.md +2 -0
- package/templates/orchestrate/implement.md +4 -0
|
@@ -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.
|
|
11
|
+
"version": "1.13.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.
|
|
4
|
+
"version": "1.13.0",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/bin/cadence.js
CHANGED
|
@@ -13,7 +13,7 @@ const io = {
|
|
|
13
13
|
|
|
14
14
|
if (tool === 'raf') {
|
|
15
15
|
process.exitCode = await run(args, io);
|
|
16
|
-
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref'].includes(tool)) {
|
|
16
|
+
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref', 'config', 'release'].includes(tool)) {
|
|
17
17
|
process.exitCode = await run([tool, ...args], io);
|
|
18
18
|
} else if (tool === '--version' || tool === '-v') {
|
|
19
19
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -39,6 +39,9 @@ if (tool === 'raf') {
|
|
|
39
39
|
cadence ref measure <slug> <page> <page | x,y | x,y,l,h> [--quoi couleurs|contraste|boites|texte] [--largeur 1440] [--json]
|
|
40
40
|
mesure une zone d'une page rendue : couleurs, contraste WCAG 2.2, boîtes, mots OCR et taille de police estimée,
|
|
41
41
|
ramenés à 1440 px de large, tolérance déclarée
|
|
42
|
+
cadence release patch|minor|major [--dry-run]
|
|
43
|
+
prépare le commit de version (fichiers de version, CHANGELOG) sans rien commiter ni publier ;
|
|
44
|
+
imprime les étapes du lead dans l'ordre et vérifie docs/whats-new.md
|
|
42
45
|
cadence verify [--retry secondes] [--sha rév]
|
|
43
46
|
rejoue les vérifications d'effet (deliver.verify) hors livraison, une passe, en parallèle ;
|
|
44
47
|
code 0 tout vert, 1 un effet rouge, 2 rien à vérifier ; « session start » la lance aussi
|
|
@@ -52,6 +55,10 @@ if (tool === 'raf') {
|
|
|
52
55
|
cadence lead tour [dossier] [--idle 3] [--json]
|
|
53
56
|
le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier courant
|
|
54
57
|
s'il est lui-même un projet ; une première ligne « cadence.yaml · ✗ erreur » si priority: est illisible
|
|
58
|
+
cadence config set <clé> <valeur> --all [--dry-run] [--depth n]
|
|
59
|
+
pose la même clé dans le cadence.yaml de chaque projet du dossier (ceux de lead tour), commentaires et ordre gardés,
|
|
60
|
+
avec un lot de config qui la cite et un commit à chemin explicite par projet ; ne pousse pas ;
|
|
61
|
+
s'arrête sur une config qui ne se relit pas
|
|
55
62
|
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--since date] [--json]
|
|
56
63
|
la mesure des passes de correction des vagues (dossier/.cadence/runs) : lots à ≥1 et ≥2 corrections,
|
|
57
64
|
par projet, visibles ou non, passes par cause ; --classify les classe par Ollama (hors quota)
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,8 @@ import { invalidResource, Plan, RafError, SCREENS, STATUSES } from './plan.js';
|
|
|
18
18
|
import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
19
19
|
import { schedule } from './schedule.js';
|
|
20
20
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
21
|
-
import {
|
|
21
|
+
import { configSet } from './config-set.js';
|
|
22
|
+
import { findProjects, readDepth, leadTour, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
22
23
|
import { configuredCap, expiredHoldLines, liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
23
24
|
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
24
25
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
@@ -28,6 +29,7 @@ import { classifyAll, measure, ollamaClassifier, readCache, readJournals, readRu
|
|
|
28
29
|
import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
|
|
29
30
|
import { findAcceptation, parseAcceptation, resealAcceptation } from './orchestrate/accept.js';
|
|
30
31
|
import { refCommand } from './ref/command.js';
|
|
32
|
+
import { release } from './release.js';
|
|
31
33
|
import { applyMigration, describeMigration, planMigration, unhandledError } from './migrate.js';
|
|
32
34
|
import { checkFiche, deriveQa, readFiche, summary } from './spec.js';
|
|
33
35
|
import { readHudContext } from './hud-context.js';
|
|
@@ -59,6 +61,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
59
61
|
raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot (ou citant un id inconnu) sans réécrire l'historique ; raf check --ignored les liste
|
|
60
62
|
raf docs-ok <lot> --reason texte acquitte l'écart docs.sync d'un lot (documentation en retard) avec la raison pour laquelle la documentation reste telle quelle ; lu par raf check et par orchestrate, qui ne rend jamais prêt un lot avec un écart ouvert
|
|
61
63
|
raf gantt [-o .cadence/gantt.html]
|
|
64
|
+
cadence config set <clé> <valeur> --all [--dry-run] [--depth n] pose la même clé dans le cadence.yaml de chaque projet du dossier (ceux de lead tour), commentaires et ordre gardés, un lot de config qui la cite (raf add + start) et un commit à chemin explicite par projet ; ne pousse pas ; liste ce qui est fait et sauté (dépôt sale, déjà posée, plan en lecture seule) ; s'arrête sur une config qui ne se relit pas
|
|
62
65
|
raf migrate [--dry-run] déplace docs/plan/raf.yaml vers .cadence/raf.yaml (git mv) et réécrit les lecteurs du chemin dans le dépôt ; --dry-run les liste sans rien toucher ; refus tant qu'un lecteur n'est pas traité (migrate.keep de cadence.yaml pour ceux qui ne lisent pas le plan) ; rejouable
|
|
63
66
|
raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
|
|
64
67
|
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--goal "…"] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague | --wave vague] [--budget 1M] [--budget projet:lot=+300k] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague] ; --waits [--since AAAA-MM-JJ]
|
|
@@ -74,6 +77,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
74
77
|
cadence ref check <inventaire.json | lecture.json> valide un inventaire ou une lecture (schéma version 1, modèle templates/inventaire.json) : une ligne « ✗ chemin : raison » par écart puis « N écart(s) » sur la sortie standard (code 1), le résumé par type sinon (code 0) ; fichier absent, vide ou illisible : code 2 ; n'appelle aucun outil système
|
|
75
78
|
cadence ref eval <attendu.json | dossier> <lecture.json | dossier> [--json] note une lecture contre l'inventaire attendu (vérité connue) : rappel et précision par type, faux lu (éléments sûrs et faux), pièges trouvés, tokens par page (usage.tokens de la lecture) ; lecture d'un autre document refusée (code 2) ; sur dossiers, un cas par sous-dossier (attendu.json / lecture.json)
|
|
76
79
|
cadence ref corpus <dossier> [--sans-rendu] écrit le corpus synthétique à vérité connue (7 pièges : texte ambigu levé par une figure, figure qui contredit, table en image, gabarit BUFR, contrôle translucide, page Proposal, scan bruité) : par cas document.html et attendu.json, puis document.pdf et page-<n>.png (1700×2200) rendus par Playwright (CADENCE_PLAYWRIGHT = dossier du paquet) ; dossier existant ou Playwright introuvable : code 2
|
|
80
|
+
cadence release patch|minor|major [--dry-run] prépare le commit de version (package.json, package-lock.json, plugin.json, marketplace.json, CHANGELOG.md) sans commiter ni publier ; imprime les étapes du lead et vérifie docs/whats-new.md : refus sur un arbre sale, un [Unreleased] vide ou une version déjà taguée
|
|
77
81
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
78
82
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
79
83
|
|
|
@@ -194,7 +198,7 @@ function dispatch(argv, io) {
|
|
|
194
198
|
io.out(HELP);
|
|
195
199
|
return 0;
|
|
196
200
|
}
|
|
197
|
-
if (command !== 'session' && !(command === 'lead' && !values.all) && (values.all || values.depth !== undefined))
|
|
201
|
+
if (command !== 'session' && command !== 'config' && !(command === 'lead' && !values.all) && (values.all || values.depth !== undefined))
|
|
198
202
|
throw new RafError(`${values.all ? '--all' : '--depth'} ne va qu'avec session start ou session close`);
|
|
199
203
|
const today = io.env.RAF_TODAY ?? toDay(io.now());
|
|
200
204
|
if (!isDay(today))
|
|
@@ -263,12 +267,29 @@ function dispatch(argv, io) {
|
|
|
263
267
|
}
|
|
264
268
|
return 0;
|
|
265
269
|
}
|
|
270
|
+
// Poser une clé dans le cadence.yaml de chaque projet du dossier (L342) : ne lit ni le plan ni le cadence.yaml du dossier courant.
|
|
271
|
+
if (command === 'config') {
|
|
272
|
+
if (rest[0] !== 'set' || rest.length !== 3)
|
|
273
|
+
throw new RafError('usage : cadence config set <clé> <valeur> --all [--dry-run] [--depth n]');
|
|
274
|
+
if (!values.all)
|
|
275
|
+
throw new RafError('config set demande --all : la commande s\'applique aux projets du dossier courant (lancer la commande dans le projet visé pour un seul)');
|
|
276
|
+
if (values.depth !== undefined && !(Number.isInteger(Number(values.depth)) && Number(values.depth) >= 1))
|
|
277
|
+
throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
|
|
278
|
+
const parent = resolve(io.cwd);
|
|
279
|
+
const result = configSet(parent, rest[1], rest[2], { dryRun: values['dry-run'], depth: values.depth === undefined ? readDepth(parent) : Number(values.depth) }, today);
|
|
280
|
+
for (const l of result.lines)
|
|
281
|
+
io.out(l);
|
|
282
|
+
return result.code;
|
|
283
|
+
}
|
|
266
284
|
if (command === 'corrections')
|
|
267
285
|
return corrections(rest, values, io);
|
|
268
286
|
if (command === 'perf')
|
|
269
287
|
return perf(rest, values, today, io);
|
|
270
288
|
if (command === 'ref')
|
|
271
289
|
return refCommand(rest, values, root, io);
|
|
290
|
+
// Prépare le commit de version : écrit les fichiers de version, ne commite ni ne publie rien (L341).
|
|
291
|
+
if (command === 'release')
|
|
292
|
+
return release(rest, !!values['dry-run'], root, today, io.out);
|
|
272
293
|
// Déplacer le plan ne lit pas le plan : pas d'avertissement « ancien emplacement » (L284).
|
|
273
294
|
if (command === 'migrate') {
|
|
274
295
|
if (rest.length > 0)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { isNode, isScalar, parse, parseDocument } from 'yaml';
|
|
6
|
+
import { defaultPlan, readDocsConfig, readHookConfig, readMigrateConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readReferencesConfig, readSessionConfig } from './config.js';
|
|
7
|
+
import { parseDeliverConfig } from './deliver.js';
|
|
8
|
+
import { commitPaths, dirtyPaths, gitRoot } from './git.js';
|
|
9
|
+
import { findProjects, isProject, readDepth, readLeadUx } from './lead.js';
|
|
10
|
+
import { readPriority } from './orchestrate/continue.js';
|
|
11
|
+
import { Plan, RafError } from './plan.js';
|
|
12
|
+
const KEY = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
|
|
13
|
+
/** Un argument de shell entre apostrophes. */
|
|
14
|
+
const shellQuote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
15
|
+
/** Estimation du lot de config : une ligne de configuration, pas une journée. */
|
|
16
|
+
const ESTIMATE = 0.05;
|
|
17
|
+
/** Relit un cadence.yaml avec chacun des chargeurs de config de cadence : la première erreur sort (RafError). */
|
|
18
|
+
export function validateConfig(file, root) {
|
|
19
|
+
readPlanConfig(file, root);
|
|
20
|
+
readNewsConfig(file);
|
|
21
|
+
readHookConfig(file);
|
|
22
|
+
readPerfConfig(file);
|
|
23
|
+
readSessionConfig(file);
|
|
24
|
+
readOrchestrateConfig(file);
|
|
25
|
+
readDocsConfig(file);
|
|
26
|
+
readReferencesConfig(file);
|
|
27
|
+
readMigrateConfig(file);
|
|
28
|
+
readLeadUx(dirname(file));
|
|
29
|
+
readDepth(dirname(file));
|
|
30
|
+
readPriority(dirname(file));
|
|
31
|
+
const text = readFileSync(file, 'utf8');
|
|
32
|
+
if (parse(text)?.deliver != null)
|
|
33
|
+
parseDeliverConfig(text, file);
|
|
34
|
+
}
|
|
35
|
+
/** Les chargeurs lisent un chemin : le texte est posé dans un dossier jetable, le cadence.yaml du projet ne change pas avant d'avoir été relu. */
|
|
36
|
+
function inTemp(text, use) {
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), 'cadence-config-'));
|
|
38
|
+
try {
|
|
39
|
+
const file = join(dir, 'cadence.yaml');
|
|
40
|
+
writeFileSync(file, text);
|
|
41
|
+
try {
|
|
42
|
+
return use(file);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
throw new RafError((e instanceof Error ? e.message : String(e)).split('\n')[0].replaceAll(file, 'cadence.yaml'));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Le message de la première erreur des chargeurs de config sur ce texte, null quand il se relit. */
|
|
53
|
+
function readsBack(text, root) {
|
|
54
|
+
try {
|
|
55
|
+
inTemp(text, (file) => validateConfig(file, root));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return e.message;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Le texte de cadence.yaml avec `key` posée à `value` (le reste du fichier est gardé tel quel : seules les lignes de la clé changent) ; null quand la clé a déjà cette valeur. */
|
|
63
|
+
export function setKey(text, key, value) {
|
|
64
|
+
const doc = parseDocument(text);
|
|
65
|
+
if (doc.errors.length > 0)
|
|
66
|
+
throw new RafError(doc.errors[0].message.split('\n')[0]);
|
|
67
|
+
const baseline = doc.toString({ lineWidth: 0 });
|
|
68
|
+
const path = key.split('.');
|
|
69
|
+
const old = doc.getIn(path, true);
|
|
70
|
+
const next = value.clone().contents; // une copie par projet : le commentaire de l'ancienne valeur ne passe pas au projet suivant
|
|
71
|
+
if (JSON.stringify(doc.getIn(path)) === JSON.stringify(value.toJS()))
|
|
72
|
+
return null;
|
|
73
|
+
if (isScalar(old) && isScalar(next))
|
|
74
|
+
old.value = next.value; // la même case : son commentaire de fin de ligne reste
|
|
75
|
+
else {
|
|
76
|
+
if (isNode(old) && isNode(next))
|
|
77
|
+
next.comment = old.comment;
|
|
78
|
+
doc.setIn(path, next);
|
|
79
|
+
}
|
|
80
|
+
const full = doc.toString({ lineWidth: 0 });
|
|
81
|
+
const kept = onlyChangedLines(text, baseline, full);
|
|
82
|
+
return readsAs(kept, full) ? kept : full; // un report mal aligné change le sens du fichier : l'écriture entière de yaml
|
|
83
|
+
}
|
|
84
|
+
/** Les deux textes sont le même document YAML ; un texte qui ne se lit pas (clés en double d'un report mal aligné) n'est pas le même. */
|
|
85
|
+
function readsAs(text, expected) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.stringify(parse(text)) === JSON.stringify(parse(expected));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* La réécriture de yaml normalise tout le fichier (`[a, b]` devient `[ a, b ]`, les espaces avant un commentaire de fin de ligne) :
|
|
95
|
+
* seules les lignes que la pose de la clé a changées (`baseline` → `changed`, deux écritures de yaml) sont reportées dans le texte d'origine.
|
|
96
|
+
* Quand yaml a aussi changé le nombre de lignes du fichier non modifié, le report n'est pas sûr : l'écriture entière de yaml est rendue (`setKey` la rend aussi quand le report ne relit pas le même document).
|
|
97
|
+
*/
|
|
98
|
+
function onlyChangedLines(original, baseline, changed) {
|
|
99
|
+
const o = original.split('\n');
|
|
100
|
+
const b = baseline.split('\n');
|
|
101
|
+
const c = changed.split('\n');
|
|
102
|
+
if (o.length !== b.length)
|
|
103
|
+
return changed;
|
|
104
|
+
let head = 0;
|
|
105
|
+
while (head < b.length && head < c.length && b[head] === c[head])
|
|
106
|
+
head++;
|
|
107
|
+
let tail = 0;
|
|
108
|
+
while (tail < b.length - head && tail < c.length - head && b[b.length - 1 - tail] === c[c.length - 1 - tail])
|
|
109
|
+
tail++;
|
|
110
|
+
// Les lignes reportées doivent être celles que yaml a écrites au même endroit : un commentaire que yaml a déplacé d'une ligne (compensé par une liste regroupée) ne se reporte pas.
|
|
111
|
+
const sameComments = (from, n, ofB) => Array.from({ length: n }, (_, k) => commentOf(o[from + k]) === commentOf(b[ofB + k])).every(Boolean);
|
|
112
|
+
if (!sameComments(0, head, 0) || !sameComments(o.length - tail, tail, b.length - tail))
|
|
113
|
+
return changed;
|
|
114
|
+
return [...o.slice(0, head), ...c.slice(head, c.length - tail), ...o.slice(o.length - tail)].join('\n');
|
|
115
|
+
}
|
|
116
|
+
/** Le commentaire d'une ligne (à partir du premier `#` en début de ligne ou après un blanc), '' sans commentaire ; une erreur sur un `#` de chaîne ne coûte que l'écriture entière. */
|
|
117
|
+
const commentOf = (line) => /(^|\s)#.*$/.exec(line)?.[0].trim() ?? '';
|
|
118
|
+
/**
|
|
119
|
+
* `cadence config set <clé> <valeur> --all` (L342) : pose la même clé dans le cadence.yaml de chaque projet de `parent` (ceux que `lead tour` connaît),
|
|
120
|
+
* crée dans chacun un lot de config qui la cite (en cours, à fermer par le lead) et commite à chemin explicite le cadence.yaml et le plan, sans pousser.
|
|
121
|
+
* Un projet que la commande ne peut pas traiter sans risque est sauté et dit pourquoi ; une config qui ne se relit pas arrête le passage.
|
|
122
|
+
*/
|
|
123
|
+
export function configSet(parent, key, valueText, opts, today) {
|
|
124
|
+
if (!KEY.test(key))
|
|
125
|
+
throw new RafError(`clé invalide : « ${key} » (segments de lettres, chiffres, - ou _ séparés par des points : orchestrate.escalation)`);
|
|
126
|
+
if (valueText.trim() === '')
|
|
127
|
+
throw new RafError('valeur vide : cadence config set <clé> <valeur> --all');
|
|
128
|
+
const value = parseDocument(valueText);
|
|
129
|
+
if (value.errors.length > 0 || value.contents == null)
|
|
130
|
+
throw new RafError(`valeur illisible en YAML : ${valueText} — ${value.errors[0]?.message.split('\n')[0] ?? 'vide'}`);
|
|
131
|
+
const shown = valueText.trim().replace(/\s+/g, ' ');
|
|
132
|
+
let projects = findProjects(parent, opts.depth);
|
|
133
|
+
if (projects.length === 0 && isProject(parent))
|
|
134
|
+
projects = [parent]; // comme lead tour : sans sous-projet, le dossier lui-même s'il en est un
|
|
135
|
+
if (projects.length === 0)
|
|
136
|
+
return { lines: [`aucun projet sous ${parent} (.cadence/raf.yaml, docs/plan/raf.yaml ou cadence.yaml avec plan:)`], code: 0 };
|
|
137
|
+
const lines = [];
|
|
138
|
+
let done = 0;
|
|
139
|
+
let skipped = 0;
|
|
140
|
+
let stopped = false;
|
|
141
|
+
for (const [i, dir] of projects.entries()) {
|
|
142
|
+
const name = relative(parent, dir).split('\\').join('/') || basename(dir);
|
|
143
|
+
const skip = (why) => {
|
|
144
|
+
skipped++;
|
|
145
|
+
lines.push(`– ${name} · sauté : ${why}`);
|
|
146
|
+
};
|
|
147
|
+
const stop = (why) => {
|
|
148
|
+
stopped = true;
|
|
149
|
+
lines.push(`✗ ${name} · arrêt : ${why}`);
|
|
150
|
+
const rest = projects.slice(i + 1).map((p) => relative(parent, p).split('\\').join('/'));
|
|
151
|
+
if (rest.length)
|
|
152
|
+
lines.push(`non traités : ${rest.join(', ')}`);
|
|
153
|
+
};
|
|
154
|
+
const top = gitRoot(dir);
|
|
155
|
+
if (!top || realpathSync(top) !== realpathSync(dir)) {
|
|
156
|
+
skip(top ? `pas de dépôt git propre au projet (dépôt parent : ${top})` : 'pas un dépôt git');
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const configFile = join(dir, 'cadence.yaml');
|
|
160
|
+
const before = existsSync(configFile) ? readFileSync(configFile, 'utf8') : null;
|
|
161
|
+
if (before !== null) {
|
|
162
|
+
const unreadable = readsBack(before, dir);
|
|
163
|
+
if (unreadable) {
|
|
164
|
+
stop(`la config ne se relit pas, avant toute modification : ${unreadable}`);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let text;
|
|
169
|
+
try {
|
|
170
|
+
text = setKey(before ?? '', key, value);
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
skip(`la clé ne se pose pas dans ce cadence.yaml (${e.message.split('\n')[0]})`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (text === null) {
|
|
177
|
+
skip(`${key} a déjà cette valeur`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const failure = readsBack(text, dir);
|
|
181
|
+
if (failure) {
|
|
182
|
+
stop(`la config modifiée ne se relit pas (${failure}) — rien n'est écrit ici`);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
// Le plan : lu avec la config modifiée (une clé plan: posée peut le déplacer) ; un plan tenu par un autre outil ne se modifie pas d'ici.
|
|
186
|
+
const found = planOf(text, dir);
|
|
187
|
+
if (found.readonly && !found.add) {
|
|
188
|
+
skip(`plan en lecture seule (tenu par un autre outil) sans commande de création : nommer plan.add dans son cadence.yaml, ou créer le lot de config avec l'outil du projet puis poser ${key}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const planPath = found.path ?? defaultPlan(dir).path;
|
|
192
|
+
const planRel = relative(dir, planPath).split('\\').join('/');
|
|
193
|
+
if (!existsSync(planPath)) {
|
|
194
|
+
skip(`pas de plan à ${planRel}`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const dirty = dirtyPaths(dir, ['cadence.yaml', planRel]);
|
|
198
|
+
if (dirty.length) {
|
|
199
|
+
skip(`${dirty.join(', ')} non commité : le commit à chemin explicite l'emporterait`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (opts.dryRun) {
|
|
203
|
+
lines.push(`+ ${name} · serait fait : ${key} = ${shown}, lot de config créé et démarré dans ${planRel}, commit de cadence.yaml et ${planRel}`);
|
|
204
|
+
done++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const planBefore = readFileSync(planPath, 'utf8');
|
|
208
|
+
const restore = () => {
|
|
209
|
+
if (before === null) {
|
|
210
|
+
if (existsSync(configFile))
|
|
211
|
+
unlinkSync(configFile);
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
writeFileSync(configFile, before);
|
|
215
|
+
writeFileSync(planPath, planBefore);
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
writeFileSync(configFile, text);
|
|
219
|
+
const title = `config cadence.yaml : ${key} = ${shown}`;
|
|
220
|
+
let id;
|
|
221
|
+
if (found.readonly && found.add) {
|
|
222
|
+
// La commande du plan crée le lot (et l'ouvre, si son outil le veut) : l'identifiant est le premier mot de sa sortie qui ressemble à un id de lot.
|
|
223
|
+
const out = execSync(found.add.replaceAll('{title}', shellQuote(title)), { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
224
|
+
id = /\b[A-Za-z]+-?\d+(?:\.\d+)?\b/.exec(out)?.[0] ?? '';
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const plan = Plan.load(planPath, { ...found.settings, config: configFile });
|
|
228
|
+
id = plan.add(title, today, { estimate: ESTIMATE });
|
|
229
|
+
plan.setStatus(id, 'doing', today);
|
|
230
|
+
plan.save();
|
|
231
|
+
}
|
|
232
|
+
const sha = commitPaths(dir, ['cadence.yaml', planRel], `chore${id ? `(${id})` : ''}: cadence.yaml — ${key} = ${shown}`);
|
|
233
|
+
done++;
|
|
234
|
+
lines.push(`✓ ${name} · ${id || 'lot de config'} · ${key} = ${shown} · commit ${sha}`);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
restore();
|
|
238
|
+
stop(`lot ou commit impossible, fichiers remis en l'état (${e.message.split('\n')[0]})`);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
lines.push(`${done} fait${done > 1 ? 's' : ''}${opts.dryRun ? ' (simulation, rien écrit)' : ''}, ${skipped} sauté${skipped > 1 ? 's' : ''}${stopped ? ', arrêt sur un projet' : ''}`);
|
|
243
|
+
return { lines, code: stopped ? 1 : 0 };
|
|
244
|
+
}
|
|
245
|
+
/** Où est le plan et est-il en lecture seule, d'après le texte de config modifié. */
|
|
246
|
+
function planOf(text, root) {
|
|
247
|
+
const c = inTemp(text, (file) => readPlanConfig(file, root));
|
|
248
|
+
return { readonly: !!c?.settings.format, ...(c?.path ? { path: resolve(root, c.path) } : {}), ...(c?.add ? { add: c.add } : {}), settings: c?.settings ?? {} };
|
|
249
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isDay } from './dates.js';
|
|
|
5
5
|
import { DEFAULT_TEST_PATTERNS } from './testgate.js';
|
|
6
6
|
import { gitRoot } from './git.js';
|
|
7
7
|
import { FIELDS, Plan, RafError, STATUSES } from './plan.js';
|
|
8
|
-
const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates'];
|
|
8
|
+
const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add'];
|
|
9
9
|
const FORMAT_KEYS = ['lots', 'fields', 'statuses', 'estimates'];
|
|
10
10
|
const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
11
11
|
const list = (v) => (v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)]);
|
|
@@ -88,7 +88,9 @@ export function readPlanConfig(file, root) {
|
|
|
88
88
|
settings.format = parseFormat(p, bad);
|
|
89
89
|
if (p.path != null && String(p.path).trim() === '')
|
|
90
90
|
throw bad('path est vide');
|
|
91
|
-
|
|
91
|
+
if (p.add != null && (typeof p.add !== 'string' || !p.add.includes('{title}')))
|
|
92
|
+
throw bad('add doit être une commande qui contient {title}');
|
|
93
|
+
return { ...(p.path != null ? { path: String(p.path) } : {}), ...(p.add != null ? { add: String(p.add) } : {}), settings };
|
|
92
94
|
}
|
|
93
95
|
export const PLAN_DEFAULT = '.cadence/raf.yaml';
|
|
94
96
|
export const PLAN_LEGACY = 'docs/plan/raf.yaml';
|
package/dist/corrections.js
CHANGED
|
@@ -118,6 +118,7 @@ export function readJournals(runsDir, until, since) {
|
|
|
118
118
|
small: !!rec.small,
|
|
119
119
|
status: String(rec.status),
|
|
120
120
|
...(typeof rec.budget === 'number' ? { budget: rec.budget } : {}),
|
|
121
|
+
...(typeof rec.escalationReserve === 'number' ? { escalationReserve: rec.escalationReserve } : {}),
|
|
121
122
|
...(precheck ? { precheck } : {}),
|
|
122
123
|
...(escalation ? { escalation } : {}),
|
|
123
124
|
...(Array.isArray(rec.warnings) && rec.warnings.some((w) => typeof w === 'string' && w.startsWith(LOCAL_PRECHECK_UNREADABLE)) ? { localUnreadable: true } : {}),
|
package/dist/git.js
CHANGED
|
@@ -212,3 +212,34 @@ export function addedTimes(dir) {
|
|
|
212
212
|
}
|
|
213
213
|
return times;
|
|
214
214
|
}
|
|
215
|
+
/** Chemins parmi `paths` (relatifs à la racine) que git voit modifiés, stagés ou non suivis. */
|
|
216
|
+
export function dirtyPaths(root, paths) {
|
|
217
|
+
const out = git(root, ['-c', 'core.quotepath=off', 'status', '--porcelain', '-z', '--', ...paths]);
|
|
218
|
+
return out.split('\0').filter(Boolean).map((l) => l.slice(3));
|
|
219
|
+
}
|
|
220
|
+
/** Commit de `paths` seuls (à chemin explicite : le reste de l'index n'y entre pas). Rend le sha court. Lève l'erreur de git (hook refusé compris). */
|
|
221
|
+
export function commitPaths(root, paths, message) {
|
|
222
|
+
git(root, ['add', '--', ...paths]);
|
|
223
|
+
try {
|
|
224
|
+
git(root, ['commit', '-q', '-m', message, '--', ...paths]);
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
tryGit(root, ['reset', '-q', '--', ...paths]);
|
|
228
|
+
const err = e;
|
|
229
|
+
throw new Error((err.stderr || err.stdout || err.message).trim().split('\n')[0]);
|
|
230
|
+
}
|
|
231
|
+
return tryGit(root, ['rev-parse', '--short', 'HEAD']) ?? '';
|
|
232
|
+
}
|
|
233
|
+
/** Un fichier SUIVI est-il modifié (indexé ou non) ? Les fichiers non suivis ne comptent pas : un commit à chemins explicites ne les embarque pas. `except` : chemins qu'on ne compte pas. */
|
|
234
|
+
export function hasTrackedChanges(cwd, except = []) {
|
|
235
|
+
return git(cwd, ['status', '--porcelain', '--untracked-files=no', '--', '.', ...except.map((f) => `:(exclude)${f}`)]).trim() !== '';
|
|
236
|
+
}
|
|
237
|
+
/** Le tag existe-t-il (local) ? */
|
|
238
|
+
export function tagExists(cwd, tag) {
|
|
239
|
+
return git(cwd, ['tag', '--list', tag]).trim() !== '';
|
|
240
|
+
}
|
|
241
|
+
/** Branche courante, ou null sur un HEAD détaché. */
|
|
242
|
+
export function currentBranch(cwd) {
|
|
243
|
+
const name = git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
244
|
+
return name === 'HEAD' ? null : name;
|
|
245
|
+
}
|
|
@@ -109,7 +109,7 @@ export function loadTemplates(dir = TEMPLATES_DIR) {
|
|
|
109
109
|
export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
|
|
110
110
|
const text = typeof source === 'string' ? loadTemplates(source)[kind] : source[kind];
|
|
111
111
|
const out = text.replace(/\{\{(\w+)\}\}/g, (_m, name) => {
|
|
112
|
-
const v = { ...vars, repos: vars.repos ?? '', integration: vars.integration ?? '', docs: vars.docs ?? '', elsewhere: vars.elsewhere ?? '', ack: vars.ack ?? '', spec: vars.spec ?? '', accept: vars.accept ?? '', maquette: vars.maquette ?? '', tmp: vars.tmp ?? '', shots: vars.shots ?? '', fiche: vars.fiche ?? '', lecture: vars.lecture ?? PLAN_READING }[name];
|
|
112
|
+
const v = { ...vars, repos: vars.repos ?? '', integration: vars.integration ?? '', docs: vars.docs ?? '', elsewhere: vars.elsewhere ?? '', ack: vars.ack ?? '', spec: vars.spec ?? '', decisions: vars.decisions ?? '', accept: vars.accept ?? '', maquette: vars.maquette ?? '', tmp: vars.tmp ?? '', workers: vars.workers ?? '', shots: vars.shots ?? '', fiche: vars.fiche ?? '', lecture: vars.lecture ?? PLAN_READING }[name];
|
|
113
113
|
if (v === undefined)
|
|
114
114
|
throw new RafError(`gabarit ${FILES[kind]} : valeur manquante pour {{${name}}}`);
|
|
115
115
|
return v;
|
|
@@ -1,7 +1,8 @@
|
|
|
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 { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { availableParallelism, homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { testWorkersFrom } from './workers.js';
|
|
5
6
|
import { basename, join, resolve } from 'node:path';
|
|
6
7
|
import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readOrchestrateConfig } from '../config.js';
|
|
7
8
|
import { isDay, toDay } from '../dates.js';
|
|
@@ -14,6 +15,7 @@ import { pidAlive, sharedStateDir } from '../state.js';
|
|
|
14
15
|
import { findProjects, isProject as isLeadProject } from '../lead.js';
|
|
15
16
|
import { freeLine, readyFree } from '../ready-free.js';
|
|
16
17
|
import { acquireSlot, cadenceHome, expiredHolds, freeSlots, liveHolds, liveSlots, liveWaves, registerWave, unregisterWave, updateWaveRepos } from './registry.js';
|
|
18
|
+
import { decisionsText, humanDecisions } from './decisions.js';
|
|
17
19
|
import { headline, loadTemplates, newsText, objective, renderBrief } from './briefs.js';
|
|
18
20
|
import { readWaits, renderWaits } from './waits.js';
|
|
19
21
|
import { acceptationFile, acceptText } from './accept.js';
|
|
@@ -22,7 +24,7 @@ import { ficheReviewText } from './fiche.js';
|
|
|
22
24
|
import { maquetteText } from './maquette.js';
|
|
23
25
|
import { specScreen, specText } from './spec.js';
|
|
24
26
|
import { candidates, parsePriority, parseUntil, readPriority, stopReason } from './continue.js';
|
|
25
|
-
import { precheckNothingToFind, Budget, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
|
|
27
|
+
import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
|
|
26
28
|
import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
|
|
27
29
|
import { buildArgs, buildLocalArgs, killSessions, mcpServersFor, readAgents, realClaude } from './launch.js';
|
|
28
30
|
import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
@@ -64,7 +66,11 @@ const SCREEN_LOT_BUDGET = 1_300_000;
|
|
|
64
66
|
*/
|
|
65
67
|
export const SPEC_STAGE_ALLOCATION = 450_000;
|
|
66
68
|
/** Base mesurée d'un budget, pour `--dry-run` : l'estimate (et l'étape spec) d'où il sort. */
|
|
67
|
-
export function lotBudgetBasis(estimate, stage) {
|
|
69
|
+
export function lotBudgetBasis(estimate, stage, escalationModel) {
|
|
70
|
+
const base = ordinaryBasis(estimate, stage);
|
|
71
|
+
return escalationModel ? `${base}, plus ${ESCALATION_RESERVE} réservés à l'escalade ${escalationModel}` : base;
|
|
72
|
+
}
|
|
73
|
+
function ordinaryBasis(estimate, stage) {
|
|
68
74
|
if (stage === 'screen' && lotBudget(estimate, stage) === SCREEN_LOT_BUDGET)
|
|
69
75
|
return "p90 mesuré du total d'un lot à écran, étapes spec, maquette et accept comprises";
|
|
70
76
|
const ordinary = lotBudget(estimate);
|
|
@@ -73,8 +79,9 @@ export function lotBudgetBasis(estimate, stage) {
|
|
|
73
79
|
return basis;
|
|
74
80
|
return `${ordinary} dérivés de l'estimate (${basis}), plus ${SPEC_STAGE_ALLOCATION} pour les étapes spec, ${stage === 'screen' ? 'maquette' : 'fiche'} et accept`;
|
|
75
81
|
}
|
|
76
|
-
/** 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). */
|
|
77
|
-
export const lotBudget = (estimate, stage) =>
|
|
82
|
+
/** 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). */
|
|
83
|
+
export const lotBudget = (estimate, stage, escalation = false) => lotBaseBudget(estimate, stage) + (escalation ? ESCALATION_RESERVE : 0);
|
|
84
|
+
const lotBaseBudget = (estimate, stage) => {
|
|
78
85
|
const ordinary = estimate <= 0.5 ? LOT_BUDGET_FLOOR : Math.max(LOT_BUDGET_ONE_DAY, Math.round(estimate * LOT_BUDGET_PER_DAY));
|
|
79
86
|
if (stage === 'screen')
|
|
80
87
|
return Math.max(SCREEN_LOT_BUDGET, ordinary + SPEC_STAGE_ALLOCATION);
|
|
@@ -351,7 +358,10 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
351
358
|
state.repos = neighbours.repos;
|
|
352
359
|
state.estimate = lot.estimate;
|
|
353
360
|
state.light = lot.estimate <= env.config.review.threshold;
|
|
354
|
-
|
|
361
|
+
const escalating = !!env.config.escalation;
|
|
362
|
+
state.budget = lotBudget(lot.estimate, screen ? 'screen' : lot.spec ? 'spec' : undefined, escalating);
|
|
363
|
+
if (escalating)
|
|
364
|
+
state.escalationReserve = ESCALATION_RESERVE;
|
|
355
365
|
state.dependsOn = lot.after.filter((d) => earlier.includes(d));
|
|
356
366
|
if (node.kind === 'ok')
|
|
357
367
|
state.node = { version: node.version, wanted: node.wanted, bin: node.bin, ...(node.skipped ? { skipped: node.skipped } : {}) };
|
|
@@ -391,7 +401,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
391
401
|
io.out(`${lotKey(l.project, l.lot)} — ${l.title}`);
|
|
392
402
|
if (l.goal)
|
|
393
403
|
io.out(` objectif donné : ${l.goal} (à la place du titre dans les briefs)`);
|
|
394
|
-
io.out(` budget du lot : ${l.budget} tokens comptés (base : ${lotBudgetBasis(lot.estimate, l.screen ? 'screen' : l.specLot ? 'spec' : undefined)})`);
|
|
404
|
+
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)})`);
|
|
395
405
|
const mine = lots.filter((o) => o.repo === l.repo);
|
|
396
406
|
const together = env.config.worktrees && mine.length > 1 && !mine.some((o) => o.repos?.length) && !!repoBranch(l.repo);
|
|
397
407
|
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'}`);
|
|
@@ -437,6 +447,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
437
447
|
const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id, lotSlug(l.project, l.lot), 'playwright');
|
|
438
448
|
// brief écrit par --dry-run = base d'une délégation à la main : aucun dossier de vague (il n'existe pas)
|
|
439
449
|
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: '' };
|
|
450
|
+
vars.decisions = decisionsText(humanDecisions(lot, lot.parent ? env.loadPlan().lots().find((x) => x.id === lot.parent) : undefined));
|
|
440
451
|
if (specStage)
|
|
441
452
|
vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, back: !!l.specLot });
|
|
442
453
|
if (l.screen && env.config.maquette)
|
|
@@ -1054,6 +1065,8 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1054
1065
|
wave.budget = budget.limit;
|
|
1055
1066
|
store.writeWave(wave);
|
|
1056
1067
|
};
|
|
1068
|
+
// Part de la machine de chaque session (L350) : recalculée à chaque (re)prise, le plafond peut avoir changé.
|
|
1069
|
+
wave.testWorkers = testWorkersFrom(io.env, (deps.cores ?? availableParallelism)(), cap);
|
|
1057
1070
|
const wctx = {
|
|
1058
1071
|
id: wave.id,
|
|
1059
1072
|
store,
|
|
@@ -1063,6 +1076,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1063
1076
|
today,
|
|
1064
1077
|
claudeHome: deps.claudeHome,
|
|
1065
1078
|
templates,
|
|
1079
|
+
testWorkers: wave.testWorkers,
|
|
1066
1080
|
quota: { hit: false },
|
|
1067
1081
|
incident: null,
|
|
1068
1082
|
log: (line) => {
|
|
@@ -1303,7 +1317,7 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1303
1317
|
else
|
|
1304
1318
|
drops.push(lotKey(matches[0].project, matches[0].lot));
|
|
1305
1319
|
}
|
|
1306
|
-
// --resume --budget projet:lot=+N (L223) : un lot rendu sur budget est remis en jeu avec son budget
|
|
1320
|
+
// --resume --budget projet:lot=+N (L223) : un lot rendu sur budget est remis en jeu avec son budget relevé (N reste la marge des passes ordinaires : la part de l'escalade encore retenue, L349, s'y ajoute). Il subit ensuite les gardes d'un lot vivant.
|
|
1307
1321
|
const raised = new Map();
|
|
1308
1322
|
const answered = args.answers.map((a) => lots.filter((x) => x.lot === a.lot && (a.project ? x.project === a.project : true))).filter((m) => m.length === 1).map((m) => m[0]);
|
|
1309
1323
|
const dropped = args.drop.map((d) => lots.filter((x) => x.lot === d.lot && (d.project ? x.project === d.project : true))).filter((m) => m.length === 1).map((m) => m[0]);
|
|
@@ -1327,7 +1341,7 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1327
1341
|
else if (budgetStep(l) === 'fix' && b.raise <= REVIEW_RESERVE)
|
|
1328
1342
|
refusals.push(`--budget ${key}=+${b.text} : un fix ne part qu'au-dessus de ${REVIEW_RESERVE} tokens de réserve`);
|
|
1329
1343
|
else
|
|
1330
|
-
raised.set(l, { from: l.budget ?? lotSpent(l), to: lotSpent(l) + b.raise, step: budgetStep(l) });
|
|
1344
|
+
raised.set(l, { from: l.budget ?? lotSpent(l), to: lotSpent(l) + b.raise + heldForEscalation(l), step: budgetStep(l) });
|
|
1331
1345
|
}
|
|
1332
1346
|
// --resume --integrate projet:lot (L279, R27) : un lot rendu à l'étape d'intégration (next: integrate) n'est rejoué que si le lead le nomme. Ni --resume seul, ni --drop, --answer ou --budget ne le rejouent.
|
|
1333
1347
|
const integ = [];
|
|
@@ -73,14 +73,17 @@ export function candidates(launch, o) {
|
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
let specDir = 'docs/specs';
|
|
76
|
+
let escalating = false;
|
|
76
77
|
try {
|
|
77
|
-
|
|
78
|
+
const config = projectEnv(dir).config;
|
|
79
|
+
specDir = config.spec.dir;
|
|
80
|
+
escalating = !!config.escalation;
|
|
78
81
|
}
|
|
79
82
|
catch {
|
|
80
83
|
// configuration illisible : le dossier par défaut
|
|
81
84
|
}
|
|
82
85
|
for (const lot of ready) {
|
|
83
|
-
if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot) || lotBudget(lot.estimate, specScreen(dir, specDir, lot) ? 'screen' : lot.spec ? 'spec' : undefined) > o.remaining)
|
|
86
|
+
if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot) || lotBudget(lot.estimate, specScreen(dir, specDir, lot) ? 'screen' : lot.spec ? 'spec' : undefined, escalating) > o.remaining)
|
|
84
87
|
continue;
|
|
85
88
|
out.push({ project, dir, lot });
|
|
86
89
|
}
|