@sylad/cadence 1.38.0 → 1.39.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/dist/cli.js +14 -11
- package/dist/cost.js +40 -0
- package/dist/orchestrate/accept.js +22 -7
- package/dist/orchestrate/ajout.js +87 -2
- package/dist/orchestrate/command.js +44 -49
- package/dist/orchestrate/continue.js +33 -0
- package/dist/orchestrate/cout.js +46 -0
- package/dist/orchestrate/pool.js +1 -1
- package/dist/orchestrate/table.js +6 -3
- package/dist/perf.js +2 -15
- package/dist/plan-commit.js +2 -0
- package/dist/release.js +18 -3
- package/dist/repo-busy.js +6 -2
- package/docs/orchestrate.md +19 -6
- package/docs/perf.md +2 -0
- package/docs/raf.md +2 -2
- package/docs/releasing.md +4 -0
- package/docs/spec.md +1 -1
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
|
@@ -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.39.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.39.0",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/dist/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
|
|
|
33
33
|
import { renderTestsSansCode, testsSansCode } from './corrections-sans-code.js';
|
|
34
34
|
import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
|
|
35
35
|
import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
|
|
36
|
-
import { findAcceptation, parseAcceptation, redAcceptations, resealAcceptation, resealForGreen, runAcceptation, setVert } from './orchestrate/accept.js';
|
|
36
|
+
import { findAcceptation, parseAcceptation, redAcceptations, resealAcceptation, resealForGreen, runAcceptation, modeSpecGreen, setVert } from './orchestrate/accept.js';
|
|
37
37
|
import { refCommand } from './ref/command.js';
|
|
38
38
|
import { coverageCommand } from './ref/coverage.js';
|
|
39
39
|
import { release } from './release.js';
|
|
@@ -558,7 +558,7 @@ function dispatch(argv, io) {
|
|
|
558
558
|
const plan = loadPlan();
|
|
559
559
|
plan.move(rest[0], place, values[place]); // en mémoire : les refus de la commande (plan en lecture seule, parent, archive) passent avant celui de la vague
|
|
560
560
|
// Un lot de la vague s'appuie sur l'ordre du plan qu'il a lu : le réordonner sous ses pieds est refusé tant que la vague tient le dépôt.
|
|
561
|
-
const holding = waveHolding(root);
|
|
561
|
+
const holding = waveHolding(root, 'move');
|
|
562
562
|
if (holding)
|
|
563
563
|
throw new RafError(`move : orchestration en cours (vague ${holding.wave}, pid ${holding.pid}) — attendre que les lots de ce dépôt soient finis (le verrou tombe dès que tous sont prêts ou rendus), puis relancer`);
|
|
564
564
|
plan.save();
|
|
@@ -791,7 +791,7 @@ function dispatch(argv, io) {
|
|
|
791
791
|
case 'deliver': {
|
|
792
792
|
if (!gitRoot(io.cwd))
|
|
793
793
|
throw new RafError('deliver : à lancer dans un dépôt git');
|
|
794
|
-
const orchestrating = waveHolding(root);
|
|
794
|
+
const orchestrating = waveHolding(root, 'deliver');
|
|
795
795
|
if (orchestrating) {
|
|
796
796
|
const refusal = `deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid})`;
|
|
797
797
|
if (values.sha === undefined)
|
|
@@ -1268,7 +1268,9 @@ function specGreen(args, ctx, io) {
|
|
|
1268
1268
|
const doc = parseAcceptation(readFileSync(join(repo, json), 'utf8'));
|
|
1269
1269
|
if (typeof doc === 'string')
|
|
1270
1270
|
throw new RafError(`${json} : ${doc}`);
|
|
1271
|
-
|
|
1271
|
+
// Vert mais sans empreintes (L526, issue #20) : clos avant le scellement, il tient le verrou total sans qu'aucun chemin ne le scelle. On rejoue l'acceptation et, verte, on pose les empreintes.
|
|
1272
|
+
const mode = modeSpecGreen(doc);
|
|
1273
|
+
if (mode.deja) {
|
|
1272
1274
|
io.out(`${lotId} : ${json} est déjà vert, rien à faire`);
|
|
1273
1275
|
return 0;
|
|
1274
1276
|
}
|
|
@@ -1283,25 +1285,26 @@ function specGreen(args, ctx, io) {
|
|
|
1283
1285
|
throw new RafError(`fichiers listés dans ${json} sous la racine d'aucune commande de orchestrate.accept : ${orphans.join(', ')}`);
|
|
1284
1286
|
const failed = runs.filter((r) => r.code !== 0);
|
|
1285
1287
|
if (failed.length) {
|
|
1286
|
-
io.err(`${lotId} : tests d'acceptation encore rouges (${failed.map((r) => `${r.command}, code ${r.code}`).join(' ; ')}), ${json} reste
|
|
1288
|
+
io.err(`${lotId} : tests d'acceptation encore rouges (${failed.map((r) => `${r.command}, code ${r.code}`).join(' ; ')}), ${json} ${mode.reste}`);
|
|
1287
1289
|
for (const r of failed)
|
|
1288
1290
|
io.err(r.output.trim().split('\n').slice(-40).join('\n'));
|
|
1289
1291
|
return 1;
|
|
1290
1292
|
}
|
|
1291
1293
|
const before = readFileSync(join(repo, json), 'utf8');
|
|
1292
|
-
let
|
|
1294
|
+
let sealed;
|
|
1293
1295
|
try {
|
|
1294
|
-
|
|
1296
|
+
sealed = resealForGreen(repo, json, ctx.today);
|
|
1295
1297
|
}
|
|
1296
1298
|
catch (e) {
|
|
1297
1299
|
throw new RafError(`${lotId} : ${e.message}`);
|
|
1298
1300
|
}
|
|
1299
|
-
|
|
1301
|
+
const resceles = sealed.refaits;
|
|
1302
|
+
setVert(repo, json); // déjà vrai pour un lot vert sans empreintes : le fichier garde ce qu'il avait
|
|
1300
1303
|
try {
|
|
1301
1304
|
git(repo, ['add', '--', json]);
|
|
1302
1305
|
// Le sujet reste court et fixe (le gabarit `plan.commit` du projet s'il y en a un, comme tout commit écrit par cadence, L343) ; les tests rescellés, sans borne, vont dans le corps, une ligne chacun.
|
|
1303
1306
|
const template = readPlanConfig(join(repo, 'cadence.yaml'), repo)?.commit;
|
|
1304
|
-
const subject = template ? planCommitMessage(template, { lot: lotId, action:
|
|
1307
|
+
const subject = template ? planCommitMessage(template, { lot: lotId, action: mode.action }) : `chore(${lotId}): ${mode.action}`;
|
|
1305
1308
|
const body = resceles.length ? ['empreintes rescellées (tests mis au point depuis le commit rouge) :', ...resceles.map((f) => `- ${f}`)].join('\n') : null;
|
|
1306
1309
|
git(repo, ['commit', '-q', '-m', subject, ...(body ? ['-m', body] : []), '--', json]);
|
|
1307
1310
|
}
|
|
@@ -1317,14 +1320,14 @@ function specGreen(args, ctx, io) {
|
|
|
1317
1320
|
const out = e;
|
|
1318
1321
|
throw new RafError(`spec green : le commit a échoué, ${json} est revenu tel qu'avant la commande — ${(out.stderr || out.stdout || out.message).trim().split('\n').slice(-5).join(' | ')}`);
|
|
1319
1322
|
}
|
|
1320
|
-
io.out(`${lotId} : ${json} →
|
|
1323
|
+
io.out(`${lotId} : ${json} → ${mode.fait}${sealed.scelles.length ? ` (${sealed.scelles.join(', ')})` : ''}, commit ${git(repo, ['rev-parse', '--short', 'HEAD']).trim()}${resceles.length ? ` ; empreintes rescellées (tests mis au point depuis le commit rouge) : ${resceles.join(', ')}` : ''}`);
|
|
1321
1324
|
return 0;
|
|
1322
1325
|
}
|
|
1323
1326
|
/** Un commit du lead en pleine passe ferait signaler au lot suivant un écart qui n'est pas le sien : tant qu'une vague tient le verrou du dépôt, `spec reseal` et `spec green` refusent. */
|
|
1324
1327
|
function refuseWhileWaveHolds(ctx, command) {
|
|
1325
1328
|
// Comme `deliver` : la vague tient le verrou du dépôt tant qu'un de ses lots y travaille (état illisible : on refuse).
|
|
1326
1329
|
// Le refus est UN message : la vague vivante qui tient le verrou (à attendre) et les vagues arrêtées du dépôt, avec leurs dossiers et leurs commandes (L238).
|
|
1327
|
-
const orchestrating = waveHolding(ctx.root);
|
|
1330
|
+
const orchestrating = waveHolding(ctx.root, command);
|
|
1328
1331
|
if (orchestrating) {
|
|
1329
1332
|
const repo = gitRoot(ctx.root) ?? ctx.root;
|
|
1330
1333
|
let open = [];
|
package/dist/cost.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Part de `counted` rangée en entrée quand une étape ne porte que ce compteur (journal ancien, ou budget d'un lot annoncé en euros) : le reste est en sortie (R8 b de L226). */
|
|
2
|
+
export const COUNTED_INPUT_SHARE = 0.9;
|
|
3
|
+
/** Coût en euros d'une étape (R8 de L226) ; le nom du modèle sans prix quand il en faudrait un. */
|
|
4
|
+
export function stepEuros(step, prices) {
|
|
5
|
+
if (step.model === 'local')
|
|
6
|
+
return 0;
|
|
7
|
+
const usage = step.usage ?? (step.counted === undefined ? null : { input: COUNTED_INPUT_SHARE * step.counted, cacheWrite: 0, cacheRead: 0, output: (1 - COUNTED_INPUT_SHARE) * step.counted });
|
|
8
|
+
if (!usage)
|
|
9
|
+
return 0;
|
|
10
|
+
const price = step.model === 'sonnet' || step.model === 'opus' || step.model === 'haiku' ? prices[step.model] : undefined;
|
|
11
|
+
if (!price)
|
|
12
|
+
return { missing: step.model === 'sonnet' || step.model === 'opus' || step.model === 'haiku' ? step.model : 'inconnu' };
|
|
13
|
+
const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
|
|
14
|
+
return ((n(usage.input) + n(usage.cacheWrite)) * price.input + n(usage.output) * price.output + n(usage.cacheRead) * price.cacheRead) / 1_000_000;
|
|
15
|
+
}
|
|
16
|
+
/** Un montant en euros, deux décimales et virgule : `4,80 €`. */
|
|
17
|
+
export const euros = (v) => `${v.toFixed(2).replace('.', ',')} €`;
|
|
18
|
+
/** Coût en euros d'un lot : la somme de ses étapes ; null dès qu'une étape tourne sur un modèle sans prix dans la grille (un coût partiel serait pris pour le vrai). */
|
|
19
|
+
export function sumEuros(steps, prices) {
|
|
20
|
+
let total = 0;
|
|
21
|
+
for (const s of steps) {
|
|
22
|
+
const e = stepEuros(s, prices);
|
|
23
|
+
if (typeof e !== 'number')
|
|
24
|
+
return null;
|
|
25
|
+
total += e;
|
|
26
|
+
}
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
/** Les montants d'un ensemble de lots : `total` des lots chiffrés, `unknown` = nombre de lots dont un modèle n'a pas de prix. */
|
|
30
|
+
export function waveEuros(perLot) {
|
|
31
|
+
let total = 0;
|
|
32
|
+
let unknown = 0;
|
|
33
|
+
for (const e of perLot) {
|
|
34
|
+
if (e === null)
|
|
35
|
+
unknown++;
|
|
36
|
+
else
|
|
37
|
+
total += e;
|
|
38
|
+
}
|
|
39
|
+
return { total, unknown };
|
|
40
|
+
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { isAbsolute, join, posix } from 'node:path';
|
|
4
4
|
import { parse } from 'yaml';
|
|
5
5
|
import { citedRefs } from '../link.js';
|
|
6
|
+
import { PLAN_ACTIONS } from '../plan-commit.js';
|
|
6
7
|
import { isParts } from './gate-parts.js';
|
|
7
8
|
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
8
9
|
function git(repo, ...args) {
|
|
@@ -215,7 +216,8 @@ export function runAcceptation(repo, json, accept) {
|
|
|
215
216
|
/**
|
|
216
217
|
* `cadence spec green` (L366) : la commande d'acceptation vient de passer au vert sur les tests tels qu'ils sont. Ceux qu'un lot mené à la main a mis au point
|
|
217
218
|
* après le commit rouge ont une empreinte scellée périmée : le verrou les signalerait encore à chaque lot suivant. Chaque empreinte différente est refaite et
|
|
218
|
-
* journalisée dans `resceaux` (comme `spec reseal`, sans fiche amendée) ; le commit qui pose `vert: true` devient ainsi le commit rouge.
|
|
219
|
+
* journalisée dans `resceaux` (comme `spec reseal`, sans fiche amendée) ; le commit qui pose `vert: true` devient ainsi le commit rouge. Un test SANS empreinte (lot clos avant le scellement, L526)
|
|
220
|
+
* n'a rien de périmé : il est scellé (`sealAcceptation`, sans entrée de journal). Rend les tests rescellés (`refaits`) et ceux qui viennent d'être scellés (`scelles`).
|
|
219
221
|
* L'appelant s'est assuré que les tests listés sont commités (l'empreinte rescellée est celle de l'arbre de travail) ; un test absent est refusé.
|
|
220
222
|
*/
|
|
221
223
|
export function resealForGreen(repo, path, today) {
|
|
@@ -223,25 +225,38 @@ export function resealForGreen(repo, path, today) {
|
|
|
223
225
|
const a = parseAcceptation(readFileSync(file, 'utf8'));
|
|
224
226
|
if (typeof a === 'string')
|
|
225
227
|
throw new Error(`${path} : ${a}`);
|
|
226
|
-
const fichiers = a.fichiers.map((f) => posix.normalize(f));
|
|
228
|
+
const fichiers = [...new Set(a.fichiers.map((f) => posix.normalize(f)))];
|
|
227
229
|
const absents = fichiers.filter((f) => !existsSync(join(repo, f)));
|
|
228
230
|
if (absents.length)
|
|
229
231
|
throw new Error(`test(s) listé(s) absent(s) de ${path} : ${absents.join(', ')}`);
|
|
230
232
|
const doc = JSON.parse(readFileSync(file, 'utf8'));
|
|
231
233
|
const empreintes = isObj(doc.empreintes) ? doc.empreintes : {};
|
|
232
234
|
const journal = Array.isArray(doc.resceaux) ? doc.resceaux : [];
|
|
233
|
-
const
|
|
235
|
+
const scelles = fichiers.filter((f) => !empreintes[f]);
|
|
236
|
+
const refaits = fichiers.filter((f) => empreintes[f] && empreintes[f] !== git(repo, 'hash-object', '--', f).trim());
|
|
237
|
+
for (const f of scelles)
|
|
238
|
+
empreintes[f] = git(repo, 'hash-object', '--', f).trim();
|
|
234
239
|
for (const f of refaits) {
|
|
235
240
|
const apres = git(repo, 'hash-object', '--', f).trim();
|
|
236
|
-
journal.push({ test: f, raison: 'cadence spec green : tests mis au point après le commit rouge, acceptation verte sur ces tests', date: today, avant: empreintes[f]
|
|
241
|
+
journal.push({ test: f, raison: 'cadence spec green : tests mis au point après le commit rouge, acceptation verte sur ces tests', date: today, avant: empreintes[f], apres });
|
|
237
242
|
empreintes[f] = apres;
|
|
238
243
|
}
|
|
239
|
-
if (refaits.length) {
|
|
244
|
+
if (refaits.length || scelles.length) {
|
|
240
245
|
doc.empreintes = empreintes;
|
|
241
|
-
|
|
246
|
+
if (refaits.length)
|
|
247
|
+
doc.resceaux = journal;
|
|
242
248
|
writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
243
249
|
}
|
|
244
|
-
return refaits;
|
|
250
|
+
return { refaits, scelles };
|
|
251
|
+
}
|
|
252
|
+
/** Les fichiers listés qui n'ont pas d'empreinte scellée (L526 : lot clos à `vert: true` avant le scellement, ou scellé en partie). */
|
|
253
|
+
export function sansEmpreinte(a) {
|
|
254
|
+
return a.fichiers.map((f) => posix.normalize(f)).filter((f) => !a.empreintes?.[f]);
|
|
255
|
+
}
|
|
256
|
+
/** Ce que `cadence spec green` fait d'une acceptation (L526) : rien si elle est verte et scellée ; sinon rejouer, puis poser `vert: true` (rouge) ou seulement les empreintes (verte, clos avant le scellement). */
|
|
257
|
+
export function modeSpecGreen(a) {
|
|
258
|
+
const sansSceau = a.vert && sansEmpreinte(a).length > 0;
|
|
259
|
+
return { deja: a.vert && !sansSceau, action: sansSceau ? PLAN_ACTIONS.specGreenScelle : PLAN_ACTIONS.specGreen, reste: sansSceau ? 'reste sans empreintes' : 'reste à vert: false', fait: sansSceau ? 'empreintes posées' : 'vert: true' };
|
|
245
260
|
}
|
|
246
261
|
/** Pose `vert: true` dans le fichier (le cycle, jamais une session) ; le commit est à faire. */
|
|
247
262
|
export function setVert(repo, path) {
|
|
@@ -77,6 +77,12 @@ export async function addToLive(args, run, store, checks) {
|
|
|
77
77
|
export class AddFeed {
|
|
78
78
|
e;
|
|
79
79
|
waiting = new Map();
|
|
80
|
+
/** Les lots tirés du plan (L527) dont le dépôt était pris quand ils ont été contrôlés : gardés pour ne pas refaire le contrôle à chaque lecture. */
|
|
81
|
+
drawn = new Map();
|
|
82
|
+
/** Les lots tirés du plan (L527) que le contrôle a refusés pour eux-mêmes : écartés pour la vague, comme un lot sauté au tirage. */
|
|
83
|
+
refused = new Set();
|
|
84
|
+
/** Les causes déjà dites au journal (L527) : un refus passager (arbre sale, dépôt tenu) est relu à chaque lecture de la file mais dit une fois. */
|
|
85
|
+
said = new Set();
|
|
80
86
|
constructor(e) {
|
|
81
87
|
this.e = e;
|
|
82
88
|
}
|
|
@@ -148,8 +154,10 @@ export class AddFeed {
|
|
|
148
154
|
e.wave.lots.push(lotKey(c.lot.project, c.lot.lot));
|
|
149
155
|
e.store.writeLot(c.lot);
|
|
150
156
|
e.saveWave();
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
if (entry) {
|
|
158
|
+
e.store.markAddTaken(entry.id);
|
|
159
|
+
this.waiting.delete(entry.id);
|
|
160
|
+
}
|
|
153
161
|
e.wctx.log(`${lotKey(c.lot.project, c.lot.lot)} : ajouté à la vague ${e.wave.id}`);
|
|
154
162
|
}
|
|
155
163
|
/** Les lots ajoutés à ce tour, prêts à être joués ; `canRun` dit si le dépôt du lot est libre (jamais deux sessions dans un même dépôt hors worktree). */
|
|
@@ -176,9 +184,86 @@ export class AddFeed {
|
|
|
176
184
|
this.e.announce();
|
|
177
185
|
return added;
|
|
178
186
|
}
|
|
187
|
+
say(line) {
|
|
188
|
+
if (this.said.has(line))
|
|
189
|
+
return;
|
|
190
|
+
this.said.add(line);
|
|
191
|
+
this.e.wctx.log(line);
|
|
192
|
+
}
|
|
193
|
+
/** Le contexte d'un lot tiré du plan : celui gardé (recontrôlé : arrêt, budget, lot fini) ou celui que le contrôle préalable construit ; null quand il est refusé ou ne tient pas (L527). */
|
|
194
|
+
async control(t, key, badRepos, room) {
|
|
195
|
+
const e = this.e;
|
|
196
|
+
const kept = this.drawn.get(key);
|
|
197
|
+
if (kept) {
|
|
198
|
+
if (kept.lot.budget <= Math.min(e.budgetLeft(), room) && !e.planState(kept))
|
|
199
|
+
return kept;
|
|
200
|
+
this.drawn.delete(key);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const known = [...e.ctxs, ...this.waiting.values()].map((x) => ({ project: x.lot.project, lot: x.lot.lot }));
|
|
204
|
+
const built = await e.build(t, known);
|
|
205
|
+
if ('refusals' in built) {
|
|
206
|
+
// Un refus propre au lot l'écarte pour la vague ; celui du dépôt (arbre sale, orchestration en cours) est passager : le lot est relu à la lecture suivante.
|
|
207
|
+
if (built.refusals.some((r) => r.startsWith(`${key} : `)))
|
|
208
|
+
this.refused.add(key);
|
|
209
|
+
else
|
|
210
|
+
badRepos.add(t.repo); // les autres lots de ce dépôt buteraient sur la même cause, ce tour-ci
|
|
211
|
+
for (const r of built.refusals)
|
|
212
|
+
this.say(`continue : lot sauté — ${r}`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return built.ctx.lot.budget <= Math.min(e.budgetLeft(), room) ? built.ctx : null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* L527 : remplit au plus `free` créneaux libres avec les lots prêts du plan (`plan.targets`, déjà dans l'ordre de priorité et filtrés sur les ressources libres), chacun contrôlé comme un lot ajouté.
|
|
219
|
+
* Ce qu'un lot pris engage est décompté avant le suivant : son budget du `plan.room`, ses ressources `uses` (un lot qui veut une ressource que ce remplissage vient de prendre attend la lecture suivante).
|
|
220
|
+
* Un lot dont le dépôt est pris par un lot qui joue (`canRun`) est gardé pour la lecture suivante, le lot d'un autre dépôt passe devant ; un lot refusé est sauté pour la vague, dit au journal ;
|
|
221
|
+
* un lot que le budget restant ne tient pas n'est ni pris ni écarté (un moins cher peut passer, ou le tour suivant).
|
|
222
|
+
*/
|
|
223
|
+
async fill(plan, free, canRun) {
|
|
224
|
+
const e = this.e;
|
|
225
|
+
const added = [];
|
|
226
|
+
const badRepos = new Set();
|
|
227
|
+
const taken = new Set();
|
|
228
|
+
let room = plan.room;
|
|
229
|
+
for (const t of plan.targets) {
|
|
230
|
+
if (added.length >= free || e.stopCause())
|
|
231
|
+
break;
|
|
232
|
+
const key = lotKey(t.project, t.lot);
|
|
233
|
+
if (this.refused.has(key) || badRepos.has(t.repo) || t.uses.some((r) => taken.has(r)))
|
|
234
|
+
continue;
|
|
235
|
+
const c = await this.control(t, key, badRepos, room);
|
|
236
|
+
if (!c || !this.start(key, c, canRun))
|
|
237
|
+
continue;
|
|
238
|
+
added.push(c);
|
|
239
|
+
room -= c.lot.budget;
|
|
240
|
+
for (const r of t.uses)
|
|
241
|
+
taken.add(r);
|
|
242
|
+
}
|
|
243
|
+
if (added.length)
|
|
244
|
+
e.announce();
|
|
245
|
+
return added;
|
|
246
|
+
}
|
|
247
|
+
/** Prend le lot contrôlé `c` : faux quand le dépôt est pris (gardé pour la lecture suivante) ou que la prise est refusée (sauté, dit au journal). */
|
|
248
|
+
start(key, c, canRun) {
|
|
249
|
+
if (!canRun(c)) {
|
|
250
|
+
this.drawn.set(key, c);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
this.drawn.delete(key);
|
|
254
|
+
const refused = this.holdAll(c);
|
|
255
|
+
if (refused) {
|
|
256
|
+
this.say(`continue : lot sauté — ${key} : ${refused}`);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
this.e.wctx.log(`continue : tire ${key} (créneau libre)`);
|
|
260
|
+
this.admit(null, c);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
179
263
|
/** Les demandes encore en file quand plus rien ne joue ne seront plus lues : écartées et dites. */
|
|
180
264
|
drain(why) {
|
|
181
265
|
this.waiting.clear();
|
|
266
|
+
this.drawn.clear();
|
|
182
267
|
for (const entry of this.e.store.addQueue())
|
|
183
268
|
this.discard(entry, why);
|
|
184
269
|
}
|
|
@@ -26,7 +26,7 @@ import { ficheReviewText } from './fiche.js';
|
|
|
26
26
|
import { maquetteText } from './maquette.js';
|
|
27
27
|
import { costlyHypotheses, specScreen, specText } from './spec.js';
|
|
28
28
|
import { fichesDuDossier } from '../spec.js';
|
|
29
|
-
import { candidates, parsePriority, parseUntil, drawSettingsReport, readPriority, stopReason } from './continue.js';
|
|
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
32
|
import { buildArgs, buildLocalArgs, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude, enabledPlugins, settingsFiles } from './launch.js';
|
|
@@ -42,7 +42,7 @@ import { resolveLotRepos } from '../repos.js';
|
|
|
42
42
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
43
43
|
import { isExpress } from './express.js';
|
|
44
44
|
import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
45
|
-
import { quotaText, renderDetail, renderTable, resumeCommand } from './table.js';
|
|
45
|
+
import { estimateLines, launchPrices, quotaText, renderDetail, renderTable, resumeCommand } from './table.js';
|
|
46
46
|
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, repairSnapshotModules, snapshotExists, waveReadsAnswerQueue, waveReadsControl, waveReadsIntegrate, waveReadsLotBudget, waveReadsReplay, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
47
47
|
const REPO_WAIT_MS = 3 * 60 * 60_000;
|
|
48
48
|
const REPO_WAIT_POLL_MS = 5_000;
|
|
@@ -424,7 +424,8 @@ function planMessageLines(repo, l, wave, dir) {
|
|
|
424
424
|
` ✗ le lot échouerait au premier commit de plan refusé, avant ou pendant ses sessions : régler plan.commit dans cadence.yaml (gabarit avec {lot}, {action}, {vague})`,
|
|
425
425
|
];
|
|
426
426
|
}
|
|
427
|
-
|
|
427
|
+
/** `wave` : la vague simulée — son identifiant, son budget et si elle a plus d'un créneau (L527 : tout lot d'un dépôt en worktrees y joue alors dans le sien). */
|
|
428
|
+
function dryRun(lots, io, deps, { budget, id, multiSlot }) {
|
|
428
429
|
const tmp = mkdtempSync(join(tmpdir(), 'cadence-orchestrate-'));
|
|
429
430
|
const agents = agentsOf(deps);
|
|
430
431
|
io.out(`vague ${id} — simulation, rien n'est lancé (budget ${budget} tokens comptés)`);
|
|
@@ -443,7 +444,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
443
444
|
io.out(` objectif donné : ${l.goal} (à la place du titre dans les briefs)`);
|
|
444
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)})`);
|
|
445
446
|
const mine = lots.filter((o) => o.repo === l.repo);
|
|
446
|
-
const together = env.config.worktrees && mine.length > 1 && !mine.some((o) => o.repos?.length) && !!repoBranch(l.repo);
|
|
447
|
+
const together = env.config.worktrees && (mine.length > 1 || multiSlot) && !mine.some((o) => o.repos?.length) && !!repoBranch(l.repo);
|
|
447
448
|
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'}`);
|
|
448
449
|
if (l.repos?.length)
|
|
449
450
|
io.out(` dépôts voisins : ${l.repos.map((r) => r.rel).join(', ')} (verrou, garde de push et --add-dir ; leurs commits citent ${l.lot})`);
|
|
@@ -615,7 +616,7 @@ function statusView(args, launch, cap, now) {
|
|
|
615
616
|
throw new RafError(e.message);
|
|
616
617
|
}
|
|
617
618
|
}
|
|
618
|
-
return { live, table: [...renderTable(store.readWave(), lots, { compact: true }), ...keptLines(lots.map((l) => l.repo), now.getTime())], running };
|
|
619
|
+
return { live, table: [...renderTable(store.readWave(), lots, { compact: true, prices: launchPrices(launch, (w) => live.push(`⚠ ${w}`)) }), ...keptLines(lots.map((l) => l.repo), now.getTime())], running };
|
|
619
620
|
}
|
|
620
621
|
/** La vague que `--status` suit (L395) : `--status <id>` ou `--status --wave <id>` ; aucune : la dernière du dossier. Deux ids différents : refus. */
|
|
621
622
|
function statusWave(args) {
|
|
@@ -766,10 +767,9 @@ export async function orchestrate(argv, io, deps) {
|
|
|
766
767
|
const id = waveId(io, args.wave, launch);
|
|
767
768
|
if (args.wave && existsSync(join(RunStore.runsDir(launch), id)))
|
|
768
769
|
return waveExists(args.wave, io);
|
|
769
|
-
dryRun(pre.lots, io, deps, budget, id);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
io.out(l);
|
|
770
|
+
dryRun(pre.lots, io, deps, { budget, id, multiSlot: maxSessions(args, io) > 1 });
|
|
771
|
+
for (const l of [...estimateLines(pre.lots, launch, (w) => io.err(`orchestrate : ${w}`)), ...(args.continue ? freeLines(launch, pre.lots) : [])])
|
|
772
|
+
io.out(l);
|
|
773
773
|
if (args.continue)
|
|
774
774
|
await continueDryRun(args, io, deps, launch, pre.lots, budget, firstSkipped);
|
|
775
775
|
return 0;
|
|
@@ -831,7 +831,7 @@ export async function orchestrate(argv, io, deps) {
|
|
|
831
831
|
l.node.link = (deps.linkNode ?? linkNodeBin)(store.dir, l.node.version, l.node.bin);
|
|
832
832
|
store.writeLot(l);
|
|
833
833
|
}
|
|
834
|
-
const code = await continueRounds(await execute(wave, pre.lots, store, io, deps, today,
|
|
834
|
+
const code = await continueRounds(await execute(wave, pre.lots, store, io, deps, today, args), wave, pre.lots, store, io, deps, today, args, launch);
|
|
835
835
|
abandon();
|
|
836
836
|
return code;
|
|
837
837
|
}
|
|
@@ -899,11 +899,13 @@ async function draw(args, io, deps, launch, exclude, remaining, n) {
|
|
|
899
899
|
return { lots, skipped };
|
|
900
900
|
}
|
|
901
901
|
/** Nombre de tours que la simulation de `--dry-run --continue` déroule au plus. */
|
|
902
|
-
|
|
902
|
+
/** Le plus de lots que `--dry-run --continue` liste après le premier tirage. */
|
|
903
|
+
const DRY_RUN_LOTS = 10;
|
|
903
904
|
/**
|
|
904
|
-
* `--dry-run --continue` : ce que
|
|
905
|
-
*
|
|
906
|
-
* (la consommation réelle n'est pas connue)
|
|
905
|
+
* `--dry-run --continue` : ce que le remplissage des créneaux libres tirerait ensuite (L527), avec la même fonction `draw` que la vague, un lot à la fois
|
|
906
|
+
* (`n = 1` : le premier lot prêt dans l'ordre de priorité, contrôle préalable, budget), comme la vague remplit chaque créneau libre ; les lots sautés disent pourquoi.
|
|
907
|
+
* Le budget est décompté sur l'estimation des lots tirés (la consommation réelle n'est pas connue) et la simulation ne sait pas quel dépôt sera occupé à chaque
|
|
908
|
+
* regard : elle liste les lots dans l'ordre où le remplissage les prendrait si chaque dépôt était libre. `firstSkipped` : causes écartées au premier tirage, quand il a fourni les lots donnés.
|
|
907
909
|
*/
|
|
908
910
|
async function continueDryRun(args, io, deps, launch, given, budget, firstSkipped) {
|
|
909
911
|
io.out(`--continue : priorité ${priorityOf(args, launch).join(' > ') || '(aucune déclarée : ordre alphabétique)'}${args.until ? ` · jusqu'à ${args.until}` : ''}`);
|
|
@@ -921,28 +923,17 @@ async function continueDryRun(args, io, deps, launch, given, budget, firstSkippe
|
|
|
921
923
|
for (const line of drawSettingsReport(launch, exclude))
|
|
922
924
|
io.out(` ${line}`);
|
|
923
925
|
let remaining = budget - given.reduce((sum, l) => sum + (l.budget ?? 0), 0);
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const drawn = await draw(args, io, deps, launch, exclude, remaining, cap);
|
|
926
|
+
const next = [];
|
|
927
|
+
while (next.length < DRY_RUN_LOTS && remaining > 0) {
|
|
928
|
+
const drawn = await draw(args, io, deps, launch, exclude, remaining, 1);
|
|
928
929
|
skip(drawn.skipped);
|
|
929
|
-
|
|
930
|
+
const lot = drawn.lots[0];
|
|
931
|
+
if (!lot)
|
|
930
932
|
break;
|
|
931
|
-
|
|
932
|
-
remaining -=
|
|
933
|
+
next.push(lotKey(lot.project, lot.lot));
|
|
934
|
+
remaining -= lot.budget ?? 0;
|
|
933
935
|
}
|
|
934
|
-
io.out(
|
|
935
|
-
}
|
|
936
|
-
/** Lots rendus ou en échec de suite, en comptant depuis le dernier lot de la vague. */
|
|
937
|
-
function handedBackStreak(all) {
|
|
938
|
-
let n = 0;
|
|
939
|
-
for (const l of [...all].reverse()) {
|
|
940
|
-
if (l.status === 'handed-back' || l.status === 'failed')
|
|
941
|
-
n++;
|
|
942
|
-
else
|
|
943
|
-
break;
|
|
944
|
-
}
|
|
945
|
-
return n;
|
|
936
|
+
io.out(next.length ? ` tirés ensuite, un par un au fil des créneaux libres, dans l'ordre de priorité (budget décompté sur les estimations) : ${next.join(', ')}${next.length === DRY_RUN_LOTS ? ' · …' : ''}` : ' aucun autre lot prêt à tirer');
|
|
946
937
|
}
|
|
947
938
|
/**
|
|
948
939
|
* `--continue` (L147) : tant qu'aucune borne ne joue, tire jusqu'à `cap` lots prêts du plan et les joue dans la même vague
|
|
@@ -985,7 +976,7 @@ async function continueRounds(first, wave, all, store, io, deps, today, args, la
|
|
|
985
976
|
store.writeWave(wave);
|
|
986
977
|
all.push(...drawn.lots);
|
|
987
978
|
say(`continue : tire ${drawn.lots.map((l) => lotKey(l.project, l.lot)).join(', ')}`);
|
|
988
|
-
code = await execute(wave, drawn.lots, store, io, deps, today,
|
|
979
|
+
code = await execute(wave, drawn.lots, store, io, deps, today, args, all);
|
|
989
980
|
if (code === 2)
|
|
990
981
|
return code;
|
|
991
982
|
}
|
|
@@ -994,16 +985,16 @@ function contexts(lots, envs, wctx) {
|
|
|
994
985
|
return lots.map((lot, i) => ({ wave: wctx, lot, config: envs[i].config, loadPlan: envs[i].loadPlan }));
|
|
995
986
|
}
|
|
996
987
|
/**
|
|
997
|
-
* Quels lots jouent dans un worktree (L279). Un groupe de lots du même dépôt (`groupLots`) y joue si `orchestrate.worktrees` est vrai, s'il a au moins deux lots
|
|
998
|
-
* si le dépôt est sur une branche et si aucun de ses lots n'a de dépôt voisin (`repos:`, R2) ; sinon les lots restent à la suite, avec un avertissement qui dit pourquoi.
|
|
988
|
+
* Quels lots jouent dans un worktree (L279). Un groupe de lots du même dépôt (`groupLots`) y joue si `orchestrate.worktrees` est vrai, s'il a au moins deux lots
|
|
989
|
+
* ou si la vague a plus d'un créneau (`multiSlot`, L527 : tout lot d'un tel dépôt reçoit son worktree — donné seul, tiré par le remplissage ou apporté par `--add`, avec ou sans `--continue` — pour qu'un créneau libre reçoive le lot suivant du même dépôt), si le dépôt est sur une branche et si aucun de ses lots n'a de dépôt voisin (`repos:`, R2) ; sinon les lots restent à la suite, avec un avertissement qui dit pourquoi.
|
|
999
990
|
* Un lot dont la branche existe déjà (reprise, R11) retrouve son worktree quel que soit le groupe ; un lot rendu à l'intégration dont la branche a disparu n'en reçoit pas. La branche de départ est celle du dépôt du lead à cet instant,
|
|
1000
991
|
* ou celle que la branche du lot a notée à sa création.
|
|
1001
992
|
*/
|
|
1002
|
-
function planTrees(ctxs, store, wave, log) {
|
|
993
|
+
function planTrees(ctxs, store, wave, log, multiSlot) {
|
|
1003
994
|
for (const group of groupLots(ctxs)) {
|
|
1004
995
|
const repo = group[0].lot.repo;
|
|
1005
996
|
const branch = repoBranch(repo);
|
|
1006
|
-
const requested = group.length > 1 && group.every((c) => c.config.worktrees);
|
|
997
|
+
const requested = (group.length > 1 || multiSlot) && group.every((c) => c.config.worktrees);
|
|
1007
998
|
const why = !requested ? null : group.some((c) => c.lot.repos?.length) ? 'des lots du groupe ont des dépôts voisins (repos:)' : !branch ? 'le dépôt est en HEAD détaché' : null;
|
|
1008
999
|
if (why)
|
|
1009
1000
|
log(`orchestrate : ${basename(repo)} : worktrees demandés mais ${why} : lots joués à la suite`);
|
|
@@ -1035,7 +1026,8 @@ function planTrees(ctxs, store, wave, log) {
|
|
|
1035
1026
|
}
|
|
1036
1027
|
}
|
|
1037
1028
|
/** Pose les verrous et les hooks, joue la vague, range l'état, rend le tableau et le code de sortie. */
|
|
1038
|
-
async function execute(wave, lots, store, io, deps, today,
|
|
1029
|
+
async function execute(wave, lots, store, io, deps, today, args, all = lots) {
|
|
1030
|
+
const cap = maxSessions(args, io);
|
|
1039
1031
|
const launch = store.launchDir;
|
|
1040
1032
|
// La configuration des projets est lue avant de poser quoi que ce soit : un cadence.yaml illisible ne laisse ni verrou ni hook.
|
|
1041
1033
|
const envs = lots.map((lot) => projectEnv(lot.repo));
|
|
@@ -1168,7 +1160,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1168
1160
|
wave.pid = process.pid;
|
|
1169
1161
|
saveWave();
|
|
1170
1162
|
const ctxs = contexts(lots, envs, wctx);
|
|
1171
|
-
planTrees(ctxs, store, wave.id, wctx.log);
|
|
1163
|
+
planTrees(ctxs, store, wave.id, wctx.log, cap > 1);
|
|
1172
1164
|
try {
|
|
1173
1165
|
updateWaveRepos(home, process.pid, repos, lanes());
|
|
1174
1166
|
}
|
|
@@ -1189,7 +1181,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1189
1181
|
}
|
|
1190
1182
|
},
|
|
1191
1183
|
announce: () => updateWaveReposSafe(home, repos.filter((x) => !freed.has(x)), lanes()),
|
|
1192
|
-
build: (t, known) => buildAdd(t, known, { wave: wave.id, ctxs, wctx, store, io, deps, launch }),
|
|
1184
|
+
build: (t, known) => buildAdd(t, known, { wave: wave.id, ctxs, wctx, store, io, deps, launch, multiSlot: cap > 1 }),
|
|
1193
1185
|
});
|
|
1194
1186
|
// Ctrl-C, SIGTERM : les sessions sont tuées en bloc, les étapes marquées interrompues, les verrous et hooks libérés.
|
|
1195
1187
|
const forget = onTermination(async () => {
|
|
@@ -1219,6 +1211,8 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1219
1211
|
release();
|
|
1220
1212
|
});
|
|
1221
1213
|
const finished = lotFinished;
|
|
1214
|
+
/** L527 : les lots prêts du plan que le créneau libre peut recevoir (rien sans `--continue`) ; `adds.fill` les contrôle et les prend. */
|
|
1215
|
+
const refill = async (canRun, free) => args.continue && free > 0 ? adds.fill(fillTargets({ args, io, launch, home, store, all, budget, quotaHit: wctx.quota.hit, incident: !!wctx.incident }), free, canRun) : [];
|
|
1222
1216
|
try {
|
|
1223
1217
|
await runPool(ctxs, cap, all, (c) => {
|
|
1224
1218
|
// Un lot rendu ou en échec garde son worktree et sa branche (R12) : le journal en donne le chemin.
|
|
@@ -1244,7 +1238,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1244
1238
|
}, (lots) => lots.every((c) => c.tree), {
|
|
1245
1239
|
pollMs: deps.slotPollMs ?? ANSWER_POLL_MS,
|
|
1246
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.
|
|
1247
|
-
take: async (canRun) => {
|
|
1241
|
+
take: async (canRun, free) => {
|
|
1248
1242
|
const taken = [];
|
|
1249
1243
|
for (const e of store.answerQueue()) {
|
|
1250
1244
|
const c = ctxs.find((x) => lotKey(x.lot.project, x.lot.lot) === e.lot);
|
|
@@ -1273,7 +1267,8 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1273
1267
|
// registre illisible : le décompte reste celui du dernier écrit
|
|
1274
1268
|
}
|
|
1275
1269
|
}
|
|
1276
|
-
|
|
1270
|
+
const added = await adds.take(canRun);
|
|
1271
|
+
return [...taken, ...added, ...(await refill(canRun, free - taken.length - added.length))];
|
|
1277
1272
|
},
|
|
1278
1273
|
});
|
|
1279
1274
|
// L421 : un ajout encore en file quand plus rien ne joue ne sera plus lu : écarté et dit.
|
|
@@ -1291,7 +1286,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1291
1286
|
io.err(`orchestrate : incident — ${wctx.incident}`);
|
|
1292
1287
|
if (wctx.quota.hit)
|
|
1293
1288
|
io.err(`orchestrate : quota atteint — ${quotaText(wctx.quota.message ?? '')}`.trim());
|
|
1294
|
-
for (const line of renderTable(wave, all))
|
|
1289
|
+
for (const line of renderTable(wave, all, { prices: launchPrices(launch, (w) => io.err(`orchestrate : ${w}`)) }))
|
|
1295
1290
|
io.out(line);
|
|
1296
1291
|
for (const line of keptLines(all.map((l) => l.repo)))
|
|
1297
1292
|
io.out(line); // les worktrees conservés, avec leur chemin (R19)
|
|
@@ -1344,8 +1339,8 @@ async function addOnlyLive(args, io, deps, launch) {
|
|
|
1344
1339
|
return typeof store === 'number' ? store : addToLive(args, { io, deps, launch }, store, LAUNCH_CHECKS);
|
|
1345
1340
|
}
|
|
1346
1341
|
/**
|
|
1347
|
-
* 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,
|
|
1348
|
-
* puis un worktree
|
|
1342
|
+
* Le contexte d'un lot ajouté en cours de vague (L421) ou tiré par le remplissage d'un créneau libre (L527) : le contrôle préalable d'un lot de lancement (les lots de la vague comptent pour les dépendances), le lien du Node,
|
|
1343
|
+
* puis un worktree si son dépôt est en `worktrees: true` et la vague a plus d'un créneau (`multiSlot`), ou si tous les lots encore à jouer de son dépôt y sont ; sinon il attend la fin de ceux qui jouent dans l'arbre du dépôt.
|
|
1349
1344
|
*/
|
|
1350
1345
|
async function buildAdd(t, known, w) {
|
|
1351
1346
|
const pre = await LAUNCH_CHECKS.preflight([t], w, { wave: w.wave, lots: known });
|
|
@@ -1356,7 +1351,7 @@ async function buildAdd(t, known, w) {
|
|
|
1356
1351
|
lot.node.link = (w.deps.linkNode ?? linkNodeBin)(w.store.dir, lot.node.version, lot.node.bin);
|
|
1357
1352
|
const ctx = contexts([lot], [projectEnv(lot.repo)], w.wctx)[0];
|
|
1358
1353
|
const open = openInRepo(w.ctxs, ctx);
|
|
1359
|
-
planTrees(open.every((o) => o.tree) ? [...open, ctx] : [ctx], w.store, w.wave, w.wctx.log);
|
|
1354
|
+
planTrees(open.every((o) => o.tree) ? [...open, ctx] : [ctx], w.store, w.wave, w.wctx.log, w.multiSlot);
|
|
1360
1355
|
return { ctx };
|
|
1361
1356
|
}
|
|
1362
1357
|
/** Le registre des vagues vivantes : dépôts tenus et créneaux occupés ; illisible, le décompte reste celui du dernier écrit. */
|
|
@@ -1797,14 +1792,14 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1797
1792
|
wave.status = 'done';
|
|
1798
1793
|
store.writeWave(wave);
|
|
1799
1794
|
}
|
|
1800
|
-
for (const line of renderTable(wave, lots))
|
|
1795
|
+
for (const line of renderTable(wave, lots, { prices: launchPrices(launch, (w) => io.err(`orchestrate : ${w}`)) }))
|
|
1801
1796
|
io.out(line);
|
|
1802
1797
|
for (const line of keptLines(lots.map((l) => l.repo)))
|
|
1803
1798
|
io.out(line);
|
|
1804
1799
|
return removed.length && !lots.every((l) => l.status === 'ready') ? 1 : 0;
|
|
1805
1800
|
}
|
|
1806
1801
|
played = true;
|
|
1807
|
-
return continueRounds(await execute(wave, live, store, io, deps, today,
|
|
1802
|
+
return continueRounds(await execute(wave, live, store, io, deps, today, args, lots), wave, lots, store, io, deps, today, args, launch);
|
|
1808
1803
|
}
|
|
1809
1804
|
finally {
|
|
1810
1805
|
if (!played)
|
|
@@ -7,6 +7,8 @@ import { gitRoot } from '../git.js';
|
|
|
7
7
|
import { findProjects, isProject } from '../lead.js';
|
|
8
8
|
import { RafError } from '../plan.js';
|
|
9
9
|
import { lotBudget, projectEnv } from './command.js';
|
|
10
|
+
import { readyFree } from '../ready-free.js';
|
|
11
|
+
import { lotFinished, lotKey } from './state.js';
|
|
10
12
|
import { specScreen } from './spec.js';
|
|
11
13
|
import { STOP_REQUESTED } from './cycle.js';
|
|
12
14
|
/** `--until 18:00` : l'heure d'aujourd'hui (date de `now`) à partir de laquelle la vague ne tire plus de lot. */
|
|
@@ -153,3 +155,34 @@ export function stopReason(f) {
|
|
|
153
155
|
return 'budget épuisé';
|
|
154
156
|
return null;
|
|
155
157
|
}
|
|
158
|
+
/** Lots rendus ou en échec de suite, en comptant depuis le dernier lot de la vague. */
|
|
159
|
+
export function handedBackStreak(all) {
|
|
160
|
+
let n = 0;
|
|
161
|
+
for (const l of [...all].reverse()) {
|
|
162
|
+
if (l.status === 'handed-back' || l.status === 'failed')
|
|
163
|
+
n++;
|
|
164
|
+
else
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
return n;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* L527 : les lots que le créneau libre d'une vague `--continue` peut recevoir, dans l'ordre de priorité : ceux du plan que `candidates` tire (hors lots de la vague) et dont aucune ressource
|
|
171
|
+
* n'est prise (`readyFree` : lots en cours, vagues vivantes dont celle-ci). Vide quand une borne de `stopReason` joue (arrêt demandé, limite d'usage, question, deux lots rendus de suite, `--until`, budget).
|
|
172
|
+
* Le budget que les lots qui jouent encore n'ont pas dépensé reste réservé : le lot suivant doit tenir dans le reste (`room`, que `AddFeed.fill` décompte de chaque lot qu'il prend, comme les ressources `uses`).
|
|
173
|
+
*/
|
|
174
|
+
export function fillTargets(f) {
|
|
175
|
+
const reserved = f.all.filter((l) => !lotFinished(l)).reduce((n, l) => n + Math.max(0, (l.budget ?? 0) - l.steps.reduce((t, s) => t + (s.tokens?.counted ?? 0), 0)), 0);
|
|
176
|
+
const remaining = f.budget.limit - f.budget.consumed - reserved;
|
|
177
|
+
const why = stopReason({ now: f.io.now(), until: f.args.untilAt, quotaHit: f.quotaHit, questions: f.all.filter((l) => l.status === 'question').length, streak: handedBackStreak(f.all), interrupted: f.incident, stopRequested: f.store.control().stopAfterCurrent, remaining });
|
|
178
|
+
if (why)
|
|
179
|
+
return { targets: [], room: 0 };
|
|
180
|
+
const dirs = isProject(f.launch) ? [f.launch] : findProjects(f.launch);
|
|
181
|
+
const libres = readyFree(dirs, f.home);
|
|
182
|
+
const exclude = new Set(f.all.map((l) => lotKey(l.project, l.lot)));
|
|
183
|
+
const targets = candidates(f.launch, { priority: f.args.priority ?? readPriority(f.launch), exclude, remaining }).flatMap((c) => {
|
|
184
|
+
const repo = gitRoot(c.dir);
|
|
185
|
+
return repo && libres.get(c.dir)?.some((l) => l.id === c.lot.id) ? [{ project: c.project, projectDir: c.dir, repo, lot: c.lot.id, model: 'sonnet', uses: c.lot.uses ?? [] }] : [];
|
|
186
|
+
});
|
|
187
|
+
return { targets, room: remaining };
|
|
188
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readPerfConfig } from '../config.js';
|
|
3
|
+
import { COUNTED_INPUT_SHARE, euros, stepEuros, sumEuros, waveEuros } from '../cost.js';
|
|
4
|
+
import { RafError } from '../plan.js';
|
|
5
|
+
/**
|
|
6
|
+
* La grille `perf.prices` du cadence.yaml du dossier de lancement (L529), la même que lit `cadence perf`. Une grille mal formée n'interdit ni une simulation
|
|
7
|
+
* ni un tableau (perdre le tableau d'une vague payée serait pire que d'en retirer les euros) : `warn` reçoit pourquoi, et la grille est absente.
|
|
8
|
+
*/
|
|
9
|
+
export function launchPrices(launch, warn) {
|
|
10
|
+
try {
|
|
11
|
+
return readPerfConfig(join(launch, 'cadence.yaml'));
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (!(e instanceof RafError))
|
|
15
|
+
throw e;
|
|
16
|
+
warn(`coût en euros n/d : ${e.message}`);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const unknownText = (n) => `n/d pour ${n} lot${n > 1 ? 's' : ''}, modèle sans prix`;
|
|
21
|
+
/**
|
|
22
|
+
* L'estimation en euros de `--dry-run` : une ligne par lot puis une pour la vague. Le budget du lot (entrée + écriture de cache + sortie, mesuré au p90)
|
|
23
|
+
* est chiffré au modèle d'écriture du lot, `COUNTED_INPUT_SHARE` en entrée : la lecture de cache et les étapes d'un autre modèle (revue) n'y sont pas.
|
|
24
|
+
* Vide sans grille.
|
|
25
|
+
*/
|
|
26
|
+
export function estimateLines(lots, launch, warn) {
|
|
27
|
+
const prices = launchPrices(launch, warn);
|
|
28
|
+
if (!prices)
|
|
29
|
+
return [];
|
|
30
|
+
const per = lots.map((l) => {
|
|
31
|
+
const e = stepEuros({ model: l.model, counted: l.budget ?? 0 }, prices);
|
|
32
|
+
return typeof e === 'number' ? e : null;
|
|
33
|
+
});
|
|
34
|
+
const out = lots.map((l, i) => `${l.project}:${l.lot} — estimation du lot : ${per[i] === null ? `n/d (${l.model} sans prix dans perf.prices)` : `${euros(per[i])} (budget de ${l.budget ?? 0} tokens au prix ${l.model}, ${Math.round(COUNTED_INPUT_SHARE * 100)} % en entrée)`}`);
|
|
35
|
+
const sum = waveEuros(per);
|
|
36
|
+
out.push(`estimation de la vague : ${euros(sum.total)}${sum.unknown ? ` (${unknownText(sum.unknown)})` : ''} — base : le budget de chaque lot, mesuré au p90 comme lui`);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Le coût réel de `renderTable` (L529) : l'en-tête et les cellules de la colonne `€` (une par lot) et la fin de la ligne de la vague ; tout est vide sans grille. */
|
|
40
|
+
export function costView(lots, prices) {
|
|
41
|
+
if (!prices)
|
|
42
|
+
return { head: [], cells: lots.map(() => []), total: '' };
|
|
43
|
+
const per = lots.map((l) => sumEuros(l.steps.map((s) => ({ model: s.model, ...(s.tokens ? { usage: s.tokens } : {}) })), prices));
|
|
44
|
+
const sum = waveEuros(per);
|
|
45
|
+
return { head: ['€'], cells: per.map((e) => [e === null ? 'n/d' : euros(e)]), total: ` — ${euros(sum.total)}${sum.unknown ? ` (${unknownText(sum.unknown)})` : ''}` };
|
|
46
|
+
}
|
package/dist/orchestrate/pool.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () =>
|
|
|
93
93
|
};
|
|
94
94
|
for (;;) {
|
|
95
95
|
admitted.length = 0;
|
|
96
|
-
for (const c of (await answers?.take(canRun)) ?? []) {
|
|
96
|
+
for (const c of (await answers?.take(canRun, Math.max(0, slots - running.size - pending.filter((q) => !blocked(q)).length))) ?? []) {
|
|
97
97
|
live.add(c);
|
|
98
98
|
pending.push([c]);
|
|
99
99
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { lotWork } from '../audit.js';
|
|
2
2
|
import { planLoader } from '../config.js';
|
|
3
|
+
import { costView } from './cout.js';
|
|
4
|
+
export { estimateLines, launchPrices } from './cout.js';
|
|
3
5
|
import { gitRoot, isAncestor } from '../git.js';
|
|
4
6
|
import { repoWork } from '../repos.js';
|
|
5
7
|
import { quotaReset } from './result.js';
|
|
@@ -248,14 +250,15 @@ export function renderDetail(lots, sel) {
|
|
|
248
250
|
}
|
|
249
251
|
/** Le tableau de fin de vague (ou de `--status`), une ligne par lot puis le total et les questions. `compact` (L441, `--status`) : sans le détail des lots, une seule ligne renvoie à `--status --detail`. */
|
|
250
252
|
export function renderTable(wave, lots, deps = {}) {
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
+
const cost = costView(lots, deps.prices);
|
|
254
|
+
const rows = lots.map((l, i) => [`${l.project}:${l.lot}`, stateText(l, wave, deps.now ?? Date.now()), String(l.pass), review(l), ux(l), String(commits(l)), k(counted(l)), `${minutes(l)} min`, ...cost.cells[i], String(l.suiteRuns ?? '—')]);
|
|
255
|
+
const head = ['lot', 'état', 'passes', 'revue', 'UX', 'commits', 'tokens', 'durée', ...cost.head, 'suites'];
|
|
253
256
|
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
254
257
|
const line = (r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
|
|
255
258
|
const out = [line(head), ...rows.map(line)];
|
|
256
259
|
const by = (s) => lots.filter((l) => l.status === s).length;
|
|
257
260
|
const tail = [`${by('ready')} prêt(s)`, by('question') ? `${by('question')} question(s)` : '', by('handed-back') + by('failed') ? `${by('handed-back') + by('failed')} rendu(s) au lead` : '', by('suspended') ? `${by('suspended')} suspendu(s)` : ''].filter(Boolean);
|
|
258
|
-
out.push(`vague ${wave.id} : ${k(wave.consumed)} / ${m(wave.budget)} comptés (lecture de cache ${m(wave.cacheRead)}) — ${tail.join(', ')} — ${wave.status}`);
|
|
261
|
+
out.push(`vague ${wave.id} : ${k(wave.consumed)} / ${m(wave.budget)} comptés (lecture de cache ${m(wave.cacheRead)})${cost.total} — ${tail.join(', ')} — ${wave.status}`);
|
|
259
262
|
for (const l of lots.filter((x) => x.status === 'question')) {
|
|
260
263
|
out.push(`question : ${l.project}:${l.lot} — « ${l.questions.join(' / ')} » (${resumeAnswerHint(wave.id, l)})`);
|
|
261
264
|
}
|
package/dist/perf.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, relative, sep } from 'node:path';
|
|
2
2
|
import { planLoader } from './config.js';
|
|
3
|
+
import { euros, stepEuros } from './cost.js';
|
|
3
4
|
import { bugCodeOf, CAUSES, isReview as isReviewKind, sousCausesRanked } from './corrections.js';
|
|
4
5
|
import { addDays, diffDays, isDay, toDay } from './dates.js';
|
|
5
6
|
import { gitCommonDir } from './git.js';
|
|
@@ -50,20 +51,6 @@ function planLot(plans, project, lot) {
|
|
|
50
51
|
/** Rang d'affichage d'une sorte d'étape : l'ordre du cycle, les sortes inconnues après. */
|
|
51
52
|
const KIND_ORDER = ['spec', 'maquette', 'fiche', 'precheck', 'implement', 'probe', 'review', 'review-small', 'ux', 'fix', 'accept'];
|
|
52
53
|
const kindRank = (kind) => (KIND_ORDER.includes(kind) ? KIND_ORDER.indexOf(kind) : KIND_ORDER.length);
|
|
53
|
-
/** Part de `counted` rangée en entrée quand une étape ne porte que ce compteur (journal ancien) : le reste est en sortie (R8 b). */
|
|
54
|
-
const COUNTED_INPUT_SHARE = 0.9;
|
|
55
|
-
/** Coût en euros d'une étape (R8) ; le nom du modèle sans prix quand il en faudrait un. */
|
|
56
|
-
function stepEuros(step, prices) {
|
|
57
|
-
if (step.model === 'local')
|
|
58
|
-
return 0;
|
|
59
|
-
const usage = step.usage ?? (step.counted === undefined ? null : { input: COUNTED_INPUT_SHARE * step.counted, cacheWrite: 0, cacheRead: 0, output: (1 - COUNTED_INPUT_SHARE) * step.counted });
|
|
60
|
-
if (!usage)
|
|
61
|
-
return 0;
|
|
62
|
-
const price = step.model === 'sonnet' || step.model === 'opus' || step.model === 'haiku' ? prices[step.model] : undefined;
|
|
63
|
-
if (!price)
|
|
64
|
-
return { missing: step.model === 'sonnet' || step.model === 'opus' || step.model === 'haiku' ? step.model : 'inconnu' };
|
|
65
|
-
return ((usage.input + usage.cacheWrite) * price.input + usage.output * price.output + usage.cacheRead * price.cacheRead) / 1_000_000;
|
|
66
|
-
}
|
|
67
54
|
const median = (xs) => {
|
|
68
55
|
if (xs.length === 0)
|
|
69
56
|
return null;
|
|
@@ -569,7 +556,7 @@ function value(v, kind) {
|
|
|
569
556
|
if (kind === 'min')
|
|
570
557
|
return `${num(v)} min`;
|
|
571
558
|
if (kind === 'eur')
|
|
572
|
-
return
|
|
559
|
+
return euros(v);
|
|
573
560
|
return num(v);
|
|
574
561
|
}
|
|
575
562
|
const signed = (c) => `${c >= 0 ? '+' : '−'}${Math.round(100 * Math.abs(c))} %`;
|
package/dist/plan-commit.js
CHANGED
|
@@ -29,6 +29,8 @@ export const PLAN_ACTIONS = {
|
|
|
29
29
|
articleNoted: 'article à rafraîchir : titres livrés notés',
|
|
30
30
|
/** `spec green` (L366) : jamais la liste des tests rescellés, elle va dans le corps du commit (header-max-length de commitlint). */
|
|
31
31
|
specGreen: 'acceptation verte, vert: true posé par cadence spec green',
|
|
32
|
+
/** `cadence spec green` sur un lot déjà `vert: true` mais sans empreintes (L526). */
|
|
33
|
+
specGreenScelle: 'empreintes de l\'acceptation posées par cadence spec green',
|
|
32
34
|
resealed: (norm) => `test d'acceptation rescellé (${norm})`,
|
|
33
35
|
/** En minuscule : `subject-case` de config-conventional refuse une majuscule initiale (L343). */
|
|
34
36
|
leadAnswer: (ids) => `${ids.length > 1 ? 'règles' : 'règle'} ${ids.join(', ')} ${ids.length > 1 ? 'sourcées' : 'sourcée'} par la réponse du lead`,
|
package/dist/release.js
CHANGED
|
@@ -184,15 +184,30 @@ function whatsNewIssues(text, version) {
|
|
|
184
184
|
const at = hits.findIndex((h) => h.v === version);
|
|
185
185
|
if (at < 0) {
|
|
186
186
|
issues.push(`${WHATS_NEW} : ${form} ${absent} — l'écrire ${where}`);
|
|
187
|
-
return;
|
|
187
|
+
return -1;
|
|
188
188
|
}
|
|
189
189
|
if (at > 0)
|
|
190
190
|
issues.push(`${WHATS_NEW} : ${form} ${misplaced} (après la version ${hits[at - 1].v} ; la plus récente va en premier) — la déplacer ${where}`);
|
|
191
191
|
if (max !== undefined && hits[at].l.length > max)
|
|
192
192
|
issues.push(`${WHATS_NEW} : ${form} trop longue (${hits[at].l.length} caractères, ${max} au plus)`);
|
|
193
|
+
return hits[at].i;
|
|
193
194
|
};
|
|
194
|
-
check('ligne de « In two lines »', 'manquante', 'mal placée', /^- \*\*(\d+\.\d+\.\d+)\*\*:/, 0, split, 'en tête de la liste', SHORT_LINE_MAX);
|
|
195
|
+
const own = check('ligne de « In two lines »', 'manquante', 'mal placée', /^- \*\*(\d+\.\d+\.\d+)\*\*:/, 0, split, 'en tête de la liste', SHORT_LINE_MAX);
|
|
195
196
|
check('paragraphe long', 'manquant', 'mal placé', /^\*\*(\d+\.\d+\.\d+)\*\*:/, split, lines.length, 'juste après l’introduction de « Version by version »');
|
|
197
|
+
// Le test « deux lignes » du dépôt lit toute la section « In two lines », jusqu'au prochain titre « ## », et toute ligne `- **x.y.z**` (deux-points ou non) :
|
|
198
|
+
// les autres lignes que celle de la version qui sort comptent (1.22.0, 28-10 : un paragraphe long y était tombé, la publication a échoué).
|
|
199
|
+
const start = Math.max(0, lines.findIndex((l) => /^##\s+In two lines\s*$/.test(l)));
|
|
200
|
+
const next = lines.findIndex((l, i) => i > start && l.startsWith('## '));
|
|
201
|
+
const end = next < 0 ? lines.length : next;
|
|
202
|
+
lines.slice(start, end).forEach((l, k) => {
|
|
203
|
+
const i = start + k;
|
|
204
|
+
const bullet = /^- \*\*(\d+\.\d+\.\d+)\*\*/.exec(l);
|
|
205
|
+
if (bullet && i !== own && l.length > SHORT_LINE_MAX)
|
|
206
|
+
issues.push(`${WHATS_NEW} : « In two lines » : la ligne de la version ${bullet[1]} fait ${l.length} caractères, ${SHORT_LINE_MAX} au plus — la raccourcir (le détail va dans « Version by version »)`);
|
|
207
|
+
const long = /^\*\*(\d+\.\d+\.\d+)\*\*:/.exec(l);
|
|
208
|
+
if (long)
|
|
209
|
+
issues.push(`${WHATS_NEW} : « In two lines » : le paragraphe long de la version ${long[1]} n’y a pas sa place (ligne ${i + 1}) — le mettre dans « Version by version »`);
|
|
210
|
+
});
|
|
196
211
|
return issues;
|
|
197
212
|
}
|
|
198
213
|
/**
|
|
@@ -256,7 +271,7 @@ export function release(rest, dryRun, root, today, out) {
|
|
|
256
271
|
: ' le commit de version n\'est pas prêt : écrire ces formes (à la main ou par une session), puis suivre les étapes ci-dessous sans relancer la commande (elle changerait encore la version)');
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
|
-
out('étapes du lead, dans l\'ordre (la commande n\'en joue aucune) :');
|
|
274
|
+
out('étapes du lead, dans l\'ordre (la commande n\'en joue aucune ; chacune se lit par son code de sortie : jamais de pipe (`| tail`, `| grep`) derrière une commande dont le code de sortie compte, il lui substitue celui du filtre) :');
|
|
260
275
|
for (const s of steps(p, files, branch))
|
|
261
276
|
out(s);
|
|
262
277
|
return ready ? 0 : 1;
|
package/dist/repo-busy.js
CHANGED
|
@@ -6,6 +6,8 @@ import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
|
|
|
6
6
|
import { mainTreeWorkers } from './orchestrate/main-tree.js';
|
|
7
7
|
import { cadenceHome, openLotsIn } from './orchestrate/registry.js';
|
|
8
8
|
import { sharedStateDir } from './state.js';
|
|
9
|
+
import { heldRefusal, localLotOf } from './local-task.js';
|
|
10
|
+
import { RafError } from './plan.js';
|
|
9
11
|
// Ce que `raf`, `deliver` et `spec` lisent du verrou du dépôt : une orchestration (ou une tâche locale du lead, L519) qui le tient, ou une intégration en cours.
|
|
10
12
|
/**
|
|
11
13
|
* Avertit (stderr, sans refuser) quand une intégration du dépôt est en cours (L425) : le plan écrit dans l'arbre principal est un fichier suivi modifié, et
|
|
@@ -37,11 +39,13 @@ export function warnWaveWorking(root, env, err) {
|
|
|
37
39
|
if (working?.length)
|
|
38
40
|
err(`avertissement : la vague ${holder.wave} (pid ${holder.pid}) travaille dans l'arbre principal de ce dépôt (${working.join(', ')}) — un commit de plan maintenant rendrait ces lots à la revue ; attendre la fin de leur étape, ou écrire le plan sans le committer`);
|
|
39
41
|
}
|
|
40
|
-
/** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec
|
|
41
|
-
export function waveHolding(root) {
|
|
42
|
+
/** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Une tâche locale du lead (L519) la tient toujours (elle n'a pas de lots de vague à lire). Avec `refusing` (le nom de la commande), une tâche locale refuse la commande d'un `RafError` qui la nomme et dit comment la finir, sans lire de vague ni conseiller `--sha` : `deliver`, `raf move`, `spec reseal` et `spec green`. Partagé par `deliver`, `raf move`, le hook pre-push, `spec reseal` et `spec green`. */
|
|
43
|
+
export function waveHolding(root, refusing) {
|
|
42
44
|
const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
43
45
|
if (!orchestrating)
|
|
44
46
|
return null;
|
|
47
|
+
if (refusing && localLotOf(orchestrating.wave))
|
|
48
|
+
throw new RafError(`${refusing} : ${heldRefusal(root, orchestrating)}`);
|
|
45
49
|
const stillOpen = openLotsIn(cadenceHome(), orchestrating.pid, root);
|
|
46
50
|
return stillOpen === null || stillOpen.length > 0 ? orchestrating : null;
|
|
47
51
|
}
|
package/docs/orchestrate.md
CHANGED
|
@@ -64,7 +64,7 @@ refuse the resume, and a refusal writes nothing to `control.log`.
|
|
|
64
64
|
|
|
65
65
|
**Answering on a live wave (L407).** `--answer <project:lot> "…"` (from the folder that launched the wave, `--wave` when several are live) no longer waits for the wave to end: on 28-10 a lot waited about 15 minutes for its answer because another lot of the wave was still running, and `--resume` refuses a wave that is running. The answer is appended to the wave's queue (`answers.log` in `.cadence/runs/<wave>/`, one JSON line per answer and per answer taken), the command prints `<lot> : réponse déposée` and returns at once. The live wave reads the queue every 2 seconds and each time a lot finishes, applies the answer to the lot (the same state as `--resume --answer`: the answer joins the brief of its next session) and replays it as one more line of the pool, as soon as a session slot is free; a lot with no worktree never plays together with another lot of its repository. The documented form `--resume --wave <wave> --answer …` does the same when that wave is still running (it used to be refused with `tourne encore`); combined with another resume option (`--budget`, `--drop`, `--integrate`, `--continue`) it is still refused. Refused (exit 2) before anything is written: a lot that is unknown, that several projects carry, or that is not waiting for an answer (`question`); no live wave in that folder (the message gives the `--resume --wave <wave> --answer` form for a stopped wave); a wave started before L407, whose snapshot does not read the queue. `--answer` alone does not combine with `--drop`, `--stop-after-current`, `--budget`, `--dry-run`, `--continue` or lots to launch. An answer whose lot is no longer waiting when the wave reads it (dropped, or already answered by an earlier line) is discarded and the journal says so (`réponse écartée`), it is never played twice; if the wave finishes between the check and the write, the answer is withdrawn and the command says to use `--resume`. Not covered: a lot that depends (`after`) on the answered lot and was left waiting in the pool is not started by this wave when the answer makes the lot ready; `--resume` plays it.
|
|
66
66
|
|
|
67
|
-
**Adding a lot to a live wave (L421).** A second `cadence orchestrate <project:lot>` in a repository that a live wave holds is refused (`une orchestration y est déjà en cours (<wave>, pid …)`), and the lead used to wait for the whole wave to end. The refusal now names the way out when that wave was launched from the same folder: `… ; pour y ajouter ce lot : cadence orchestrate --add <project>:<lot> --wave <wave>`. `--add <project:lot> [--wave <wave>]` (repeatable; from the folder that launched the wave, `--wave` when several are live) checks each lot like a lot given at launch (known, open, dependencies, Node, `repos:`), with three differences: a repository held **by that wave** is accepted (its dirty tree and its push guard are the wave's own), a dependency counts when it is a lot of the wave or an earlier `--add` of the same call, and the remaining budget of the wave (`budget − consumed`) must hold the budget of the lot. Then it appends the lot to the wave's queue (`adds.log` in `.cadence/runs/<wave>/`, one JSON line per request and per request taken), prints `<lot> : ajout déposé` and returns at once. The live wave reads the queue with the answers (every 2 seconds and each time a lot finishes), checks the lot again at that moment, and adds it: the lot is written in the wave state and in `wave.json`, the repository lock and push guard are taken if the wave does not hold the repository (any more), and the journal says `<lot> : ajouté à la vague <wave>`. The lot then plays as one more line of the pool as soon as a session slot is free; a lot of a repository where another lot of the wave is playing waits for it (never two sessions in one repository), and plays in its own worktree only when every lot still to play in that repository does. Refused (exit 2) before anything is written, nothing queued for any lot as soon as one is refused: a lot unknown or already finished, a lot already in the wave or already queued, a dependency neither done nor in the wave, a repository held by another wave, a budget that does not fit, no live wave in that folder, several live waves without `--wave`, a wave started before L421 (its snapshot does not read the queue). `--add` combines with nothing but `--wave`. A request that no longer holds when the wave reads it (lot done meanwhile, repository taken by another wave, budget spent, `--stop-after-current` requested, usage limit, incident) is discarded and the journal says so (`ajout écarté (<cause>)`) — a request kept waiting for its repository is checked again at the moment it is taken, so a stop requested or a budget spent while it waited discards it too, and the lot never enters `wave.json`; the same goes for a request still queued when no session is left to play (`la vague a fini de jouer`) — if the wave ends between the check and the write, the command withdraws the request and says to launch the lot in a new wave. A legitimate commit of an added lot on the main branch of its repository is not an incident for a session already running in a sibling repository (L456). With `--continue`, the lots added by hand count as lots of the wave and are not drawn a second time. The case of `--resume --integrate` refused while another wave holds the repository is not part of `--add`: the resume waits for that wave (L443).
|
|
67
|
+
**Adding a lot to a live wave (L421).** A second `cadence orchestrate <project:lot>` in a repository that a live wave holds is refused (`une orchestration y est déjà en cours (<wave>, pid …)`), and the lead used to wait for the whole wave to end. The refusal now names the way out when that wave was launched from the same folder: `… ; pour y ajouter ce lot : cadence orchestrate --add <project>:<lot> --wave <wave>`. `--add <project:lot> [--wave <wave>]` (repeatable; from the folder that launched the wave, `--wave` when several are live) checks each lot like a lot given at launch (known, open, dependencies, Node, `repos:`), with three differences: a repository held **by that wave** is accepted (its dirty tree and its push guard are the wave's own), a dependency counts when it is a lot of the wave or an earlier `--add` of the same call, and the remaining budget of the wave (`budget − consumed`) must hold the budget of the lot. Then it appends the lot to the wave's queue (`adds.log` in `.cadence/runs/<wave>/`, one JSON line per request and per request taken), prints `<lot> : ajout déposé` and returns at once. The live wave reads the queue with the answers (every 2 seconds and each time a lot finishes), checks the lot again at that moment, and adds it: the lot is written in the wave state and in `wave.json`, the repository lock and push guard are taken if the wave does not hold the repository (any more), and the journal says `<lot> : ajouté à la vague <wave>`. The lot then plays as one more line of the pool as soon as a session slot is free; in a repository in `orchestrate.worktrees: true` mode with a cap above one it plays in its own worktree at once, in parallel with the lot already playing there (L527, every lot of such a repository has its own); otherwise a lot of a repository where another lot of the wave is playing waits for it (never two sessions in one repository), and plays in its own worktree only when every lot still to play in that repository does. Refused (exit 2) before anything is written, nothing queued for any lot as soon as one is refused: a lot unknown or already finished, a lot already in the wave or already queued, a dependency neither done nor in the wave, a repository held by another wave, a budget that does not fit, no live wave in that folder, several live waves without `--wave`, a wave started before L421 (its snapshot does not read the queue). `--add` combines with nothing but `--wave`. A request that no longer holds when the wave reads it (lot done meanwhile, repository taken by another wave, budget spent, `--stop-after-current` requested, usage limit, incident) is discarded and the journal says so (`ajout écarté (<cause>)`) — a request kept waiting for its repository is checked again at the moment it is taken, so a stop requested or a budget spent while it waited discards it too, and the lot never enters `wave.json`; the same goes for a request still queued when no session is left to play (`la vague a fini de jouer`) — if the wave ends between the check and the write, the command withdraws the request and says to launch the lot in a new wave. A legitimate commit of an added lot on the main branch of its repository is not an incident for a session already running in a sibling repository (L456). With `--continue`, the lots added by hand count as lots of the wave and are not drawn a second time. The case of `--resume --integrate` refused while another wave holds the repository is not part of `--add`: the resume waits for that wave (L443).
|
|
68
68
|
|
|
69
69
|
**`--goal "…"` and `goal:` — the objective of the briefs when the title is stale (L258)**: the first line of the `{{objectif}}` of every
|
|
70
70
|
brief of a lot (and the title quoted in their first sentence) is the lot's title; when a change of scope made it stale, the lead no longer
|
|
@@ -121,9 +121,11 @@ journal (`continue : lot sauté — <cause>`; for the first draw also on the err
|
|
|
121
121
|
time window (`--until HH:MM`, today's clock: nothing new is drawn from then on, a running lot is not cut), the usage limit (« usage limit reached », « rate limit », « You've hit your weekly limit »),
|
|
122
122
|
a question asked (the wave stays resumable with `--answer`), two lots handed back in a row, an interrupted wave, a `--stop-after-current` request (`arrêt demandé`, even when the last running
|
|
123
123
|
lots finished cleanly), or no ready
|
|
124
|
-
lot whose estimate fits the budget left. `--resume … --continue` draws again after the resumed lots. When the wave starts, `--continue` (and its `--dry-run`) prints the line `prêts, ressources libres : <project>:<lot> <title> (<resources>) · …` that `cadence lead tour` prints (L252): the ready lots, from every project of the folder, whose `uses:` resources (`raf uses`, `resources:` of the parent `cadence.yaml`) are held neither by a lot `doing` (of the folder's projects, and of every project that shares their resources: those under the folder whose `cadence.yaml` declares `resources:`, walked with its `depth:`, one level by default, so launched from a group or from inside one project it still sees the others) nor by an unfinished lot of a live wave nor by the lots of the first draw of the wave that starts (a lot of a wave is recognised by its repository and its id, not by the project name; those lots, and the ready lots that compete with them for a resource, are not listed); at most 3 lots per project, then `<project> +K`; nothing is held back by it, the draw below is unchanged. `--dry-run --continue`
|
|
125
|
-
|
|
126
|
-
names the candidates skipped with their cause: `
|
|
124
|
+
lot whose estimate fits the budget left. `--resume … --continue` draws again after the resumed lots. When the wave starts, `--continue` (and its `--dry-run`) prints the line `prêts, ressources libres : <project>:<lot> <title> (<resources>) · …` that `cadence lead tour` prints (L252): the ready lots, from every project of the folder, whose `uses:` resources (`raf uses`, `resources:` of the parent `cadence.yaml`) are held neither by a lot `doing` (of the folder's projects, and of every project that shares their resources: those under the folder whose `cadence.yaml` declares `resources:`, walked with its `depth:`, one level by default, so launched from a group or from inside one project it still sees the others) nor by an unfinished lot of a live wave nor by the lots of the first draw of the wave that starts (a lot of a wave is recognised by its repository and its id, not by the project name; those lots, and the ready lots that compete with them for a resource, are not listed); at most 3 lots per project, then `<project> +K`; nothing is held back by it, the draw below is unchanged. `--dry-run --continue` simulates the
|
|
125
|
+
free-slot fill below with the same function as the wave, one lot at a time (the first ready lot in the priority order, pre-check, budget counted on the estimates), and
|
|
126
|
+
names the candidates skipped with their cause: `tirés ensuite, un par un au fil des créneaux libres, dans l'ordre de priorité (budget décompté sur les estimations) : b:L2, a:L2, a:L3` (ten at most, then `…`). The simulation cannot know which repository will be busy at each look: it lists the lots in the order the fill would take them if every repository were free. The round draw happens between rounds: a round of up to `--max-sessions` lots must finish before the next round draw; a free slot is filled in the meantime (below).
|
|
127
|
+
|
|
128
|
+
**A free slot is filled at once (L527, GitHub issue #21).** Between rounds is not the only moment of the draw: while the pool plays, each time a session slot is free — no queue playing, none waiting for its dependency — the wave draws the next ready lot of the plan, in the priority order above, and plays it without waiting for the lots still playing (on 02-11 a lot added to a wave waited behind the other one, played alone in the main tree, while a slot stayed empty; a lot handed back at the pre-check frees its slot within seconds the same way). The wave looks at the slots at the end of each lot and every 2 seconds. The draw keeps every rule of the round: at most `--max-sessions` sessions across all the waves; never two sessions in one repository (a candidate whose repository is playing is kept for the next look, the next lot of another repository goes first; with `orchestrate.worktrees: true` and more than one slot every lot of the repository plays in its own worktree — given alone, drawn by the fill or added by `--add`, with or without `--continue` — so the next lot of its repository can fill the free slot); a lot whose `uses:` resource is held by a lot `doing` or by an unfinished lot of a live wave (this one included) is not drawn; the project's `continue:` setting; a lot refused by the pre-check is skipped, with its cause in the journal (`continue : lot sauté — <cause>`, said once; a refusal of the repository, such as a dirty tree, is looked at again at the next look). The stops are those of the round (`continue : arrêt`): `--stop-after-current`, the usage limit, a question asked, two lots handed back in a row, `--until`, the budget. The budget a lot still playing has not spent yet stays reserved (its budget minus the tokens counted for it): the next lot must fit in the rest. When several slots are free at once, each lot taken counts before the next: its budget comes off the rest, and a lot that wants a `uses:` resource the fill has just taken waits for the next look. Each start is told in the journal: `continue : tire <project>:<lot> (créneau libre)`, then `<project>:<lot> : ajouté à la vague <wave>` as for `--add`; the lot is in `wave.json`. Without `--continue` nothing is drawn: lots given on the command line are the only ones played. The round draw stays for the first lots and for what the free-slot fill did not take (a refused lot that is clean again).
|
|
127
129
|
|
|
128
130
|
**Keeping a lot out of the draw (L390, `orchestrate.continue`)**: `--continue` draws the first ready lot of the plan, even a big one with no spec and no sub-task (maritime B6 « Profils verticaux, coupes (10) », effort L, drawn on 27-10 then withdrawn by the lead). A project says what its draw may not take, in its own `cadence.yaml`:
|
|
129
131
|
|
|
@@ -407,6 +409,7 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
|
|
|
407
409
|
file, deletes it, creates one outside the accept step, or modifies a locked test hands the lot back (`verrou des tests d'acceptation : …`) — for a modified test the message ends with `; premier commit fautif : <sha7> <subject>`, the oldest commit after the reference (red commit, or the head before the lot) that touched the sealed path (a deletion or a rename included, renames not followed), and names no commit when the deviation is only in the working tree (L231) — a useful change is a question to the lead or a review finding, never a silent gesture. The lock does not depend on `orchestrate.accept` being declared. **Exception — a delivered lot (`vert: true`):** a later lot that makes the rule evolve may modify the listed tests of that acceptation when **a commit that cites the lot in progress amends the sheet of the screen and comes after the red commit** (a strict descendant of it; the commit may have been made before the wave, and once one exists the exception covers the whole acceptation: the tests may change in the same commit as the sheet, in a later one or in a following session, a `fix` after the `implement` for instance, L231): the sheet is what evolves the rules, and tests and sheet change together. A sheet amended by another lot, by a commit that cites no lot, by a commit that is not after the red commit, or only in the working tree (not committed) opens nothing. The exception never covers the acceptation of the lot in progress (its `vert: true` is set right after the implement): that one keeps the whole lock. Without a sheet change the difference is still a deviation — except one narrow case (L184): a listed file whose content is identical to what it was at the head before the lot's first **work** commit (commits found by the plan's refs) is not a deviation, the lot having taken its own addition back (a lot handed back for writing in a locked file and resumed in a new wave, which then removes that addition). This applies to every acceptation, `vert: false` included, but only to a file that **exists** at that head and now: a file absent at that head (e.g. created by the lot's own red commit) or deleted since is never exempted, so deleting a locked test stays a deviation; nor when the sheet of the screen has changed since that head (an evolution admitted for another lot: going back to the old content would cancel it, not the lot's addition); apart from that, with `vert: false` the whole lock applies, sheet change or not; `acceptation.json` itself stays locked (only its `vert` key moves).
|
|
408
410
|
- **A sealed test that contradicts the sheet: `cadence spec reseal` (L200).** The lock is total for the lot in progress (the delivered-lot exception above never covers it; for a delivered lot, see the end of this item), so a test that is wrong against its sheet — e.g. the regex `/\d{1,2} /` of an acceptance test rejecting « T3 2027 annoncée », which the sheet allows — would leave the lot handed back with no way out. The way out is the lead's, outside any wave: correct the test in the working tree (the sheet is left as it is: unchanged since the red commit, or amended by commits of the lot and committed, see « A sheet amended in the same lot » below), then `cadence spec reseal <lot> <test> --reason "why the test contradicts the sheet"`. The program checks that the lot is in progress (`doing`; for a delivered lot, see the end of this item), that the test is listed in the lot's sealed `acceptation-<lot>.json`, that the sheet is unchanged since the red commit or amended by commits of the lot (below), that **no other listed test** changed (the lock keeps everything else) and that the test did change; it then redoes the fingerprint of that one test, appends `{ test, raison, date, avant, apres }` (plus `ficheAmendee` for an amended sheet, below) to the `resceaux` key of `acceptation.json`, commits the test and `acceptation.json` (`test(L35): acceptation rescellée, <test> : <reason>`, the new red commit) and the lot's note with the reason (`plan: …`). It is refused during an orchestrated wave (`CADENCE_ORCHESTRATED`, or the repository's wave lock while one of its lots still works, like `cadence deliver`), **while a wave that is not `done` (interrupted, suspended, or still alive with a lot waiting on a question) keeps open any lot of this repository that is still `doing` in this repository's plan (L238)** — the resealed lot, the `--porteur` or any other lot of the repository: the lock hands back every open lot of the repository, so a reseal committed in the meantime was handed back to any of them as the « first offending commit » of the lock (ol-companion L172, then L173). Open means any non-final state in the wave (a question waiting for its `--answer`, a lot suspended by the quota or the budget, a queued lot, a session running): a stopped wave holds no lock, but its `--resume` replays those lots. The filter is by repository (the repository of the lot's project, not its neighbour repositories: lot ids belong to each plan, so a finance-tracker L172 never blocks an ol-companion L172) and by plan: a lot that is `done` or absent in the plan never refuses, so an old interrupted wave whose lots are long delivered blocks nothing, and neither does a `done` wave. The error is a single message that names **every** wave concerned, with its launch folder and its open lots, the wave that holds the repository lock included (alive: while it has an open lot the lock refuses first, so the same message lists it together with the stopped waves of the repository, not the bare « orchestration en cours » line `cadence deliver` gives), and gives for each stopped wave ONE command, runnable as it stands, with a `--drop` per open lot of the repository in that wave, the queued ones that are not `doing` in the plan included (L239: `--resume` would relaunch them, and the lot would take the lock back and find the tree dirty with the corrected test): `cd <launch folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> --drop <project>:<lot>` (`--resume` replays the whole wave except the dropped lots, so one command per lot would start sessions on the others; it looks for the wave in the folder it is run from only); a wave that is still alive is not called suspended, the error says it is running and is to be waited for. A repository reached by another path (a worktree, a symbolic link) is the same repository: the `.git` directories are compared by their real path. The waves read are those of `.cadence/runs/` of the repository root, of the plan's folder and of every folder above them (a wave keeps its state where `cadence orchestrate` was launched, usually the parent folder of the projects), with no `--reason`, and on a lot that is neither in progress nor delivered with a green acceptation. Then, depending on how the lot stands: a lot waiting on a `question` sits in a wave that is not `done`, which refuses the reseal (L238): answer it first (`--resume --wave <wave> --answer`, the lot then goes on or is handed back) or close it (`--resume --wave <wave> --drop`), reseal afterwards if the lot is handed back (a rewrite of `acceptation.json` without a `resceaux` entry stays a violation of the lock); a lot handed back with `verrou des tests d'acceptation` is finished for `--resume`, so it is replayed by a new wave `cadence orchestrate <project>:<lot>`, which starts again from the red commit. A session never does it, three guards: (1) `cadence spec reseal` is in the sessions' `--disallowedTools` (like `raf done`/`review`/`ux`); (2) a resealed commit is adopted as the new red commit only if it belongs to **no session of the wave** (commits listed in each `steps[].commits`, or in `headBefore..headAfter` of any session of any lot of the wave) — otherwise it stays a violation of the lock; (3) the lock is checked at the end of **every** work session, a session that ends with a question included, so a session that replaces the test, forges the fingerprint, commits and asks a question is handed back with `verrou des tests d'acceptation` without waiting for the lead's `--answer`. The reseal is atomic: if the commit fails (a `pre-commit` hook exiting 1, say) `acceptation.json` and the test are put back as they were before the command (the lead's correction in the working tree is kept, nothing staged) and a readable error says nothing was resealed. With a plan kept read-only by another tool, the reseal itself succeeds but the plan is not touched: the note of the lot is left to the lead (the reason stays in `acceptation.json` and in the commit message; the command only says a note is to be added). With a plan outside the repository or untracked, the note **is** written in the plan file but not committed (the command says `plan à commiter`); do not add it a second time. If the commit of the note is refused (a hook), the reseal stays done: the error gives its commit and says the note, already written, is to be committed by hand. **A sheet amended in the same lot (L217):** a sheet amended since the red commit is accepted, for a lot in progress as for a delivered one (the sheet is then not put back to its red state to be restored afterwards). When its net difference with the red commit (`git diff <red> -- <sheet>`) is not empty, the command reads `git log <red>..HEAD -- <sheet>`: every commit that touched it must cite the lot (read like `raf commits`, i.e. `citedRefs`: the scope of the subject first, the body otherwise; for a delivered lot, the lot, the `--porteur` or any delivered lot of the plan, status `done`, L229: the living sheet is the reference; one cited delivered lot among several is enough), and the sheet must be committed. The fingerprint of the test is then redone against the current sheet and the `resceaux` entry gains the optional key `ficheAmendee` (short sha of the last commit that touched the sheet); the lot's note and the commit message say `fiche amendée en <sha>` instead of `fiche inchangée`. Still refused, with nothing written: a sheet modified by a commit that cites neither the lot nor (delivered lot) the `--porteur` or a delivered lot (`la fiche … a été modifiée depuis le commit rouge … par un commit qui ne cite pas <lot> : <sha>`; for a delivered lot `… ne cite ni <lot>, ni le porteur <porteur>, ni un lot livré : <sha> (cite : <lots ou aucun lot>) — faire citer le porteur ou un lot livré par ce commit (le réécrire avant qu'il soit poussé) ou, sinon, rescelle à la main avec le lead`), a sheet deleted or renamed since the red commit, a sheet with uncommitted changes (working tree or index: `a des modifications non commitées : la commiter (en citant <lot>) avant le rescellé`), as before any other listed test touched, a test left unchanged, an orchestrated wave and a missing `--reason`; the atomicity is unchanged. A sheet amended then brought back to the identical content is treated as unchanged, whoever committed in between. After the reseal the new red commit is the reseal commit, so a second reseal sees the sheet as unchanged. **A delivered lot (L212):** a lot whose acceptation is green (`vert: true`) and whose sealed test turns out unstable while the contract does not change (seen: a tab read as « blank » in CI) had no way out but the « sheet modified in the same lot » exception. `cadence spec reseal <lot> <test> --reason "why" --porteur <lot in progress>` now covers it, with the same guards (the sheet unchanged since the red commit, or amended as above by commits that cite the delivered lot, the carrier or another delivered lot; no other listed test touched, no wave, `--reason`) and three differences: `--porteur` is required and names the lot in progress that carries the fix (refused if it is the delivered lot itself, unknown or not `doing`); the commit cites the **carrier** (`test(L113): acceptation rescellée du lot livré L47, <test> : <reason>`) and so does the plan note and its commit, the delivered lot being closed; the dated `resceaux` entry also records `porteur`, and `vert` stays `true`. `--porteur` on a lot in progress, or `spec reseal` on a delivered lot without `--porteur`, is refused.
|
|
409
411
|
- **A closed lot left at `vert: false`: `cadence spec green` (L366).** The cycle sets `vert: true` itself; a lot driven and closed by hand (`raf done`) never gets it, and its `acceptation-<lot>.json` then keeps the lock total on its tests: every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them. Three guards:
|
|
412
|
+
- A lot already at `vert: true` but with no fingerprint for one of its listed tests (closed before the seal, L526, issue #20) is not « already green »: `cadence spec green <lot>` replays the command and, green, seals the missing fingerprints (the others are kept, no `resceaux` entry) in a commit of the file alone citing the lot (`chore(L12): empreintes de l'acceptation posées par cadence spec green`, or the `plan.commit` template); red: nothing changes, exit 1. A lot green and fully sealed still prints « déjà vert » and runs nothing.
|
|
410
413
|
- `cadence spec green <lot>` runs the dedicated command (`orchestrate.accept`, same rules as the cycle: the commands whose root holds a listed file, `{acceptation}` and `CADENCE_ACCEPTATION` set; a listed file under no command's root is refused) and, when it is green, sets `vert: true` and commits the file alone, citing the lot (`chore(L12): acceptation verte, vert: true posé par cadence spec green`, or the `plan.commit` template of the project when it has one). The subject stays short and fixed: the resealed tests are listed in the commit body, one `- <path>` line each, so a `commit-msg` hook bounding the header (commitlint `header-max-length` 100) accepts the commit. Tests refined by the lot after the red commit (the case of issue #18: its own fix commits changed them) would still be reported by the lock, whose reference is the red commit: the command therefore reseals every listed test whose fingerprint changed (the `empreintes` are redone, one `resceaux` entry each) in that same commit, which becomes the red commit; a listed test changed but not committed is refused before anything runs. Red: the tail of the output on stderr, exit 1, nothing changed. Already green: nothing to do. Refused for a `todo` lot, with `CADENCE_ORCHESTRATED` set, while a wave holds the repository lock and while an unfinished wave keeps a lot of the repository open (the same refusals as `spec reseal`, worded for `spec green`: « spec green : refusé … puis relancer spec green »).
|
|
411
414
|
- `raf done <lot>` refuses a lot whose acceptance is `vert: false` (the message names the file and the command above); `--force` closes it anyway and warns on stderr.
|
|
412
415
|
- `cadence orchestrate … --dry-run` prints once per repository `⚠ <repo> · acceptation rouge d'un lot clos : L12 (<file>, vert: false)` for each `done` lot left that way, before the wave starts.
|
|
@@ -569,6 +572,16 @@ A lot of **at most 0.1 day** (`estimate: 0.1` or less, no `screen:`, no `spec: t
|
|
|
569
572
|
|
|
570
573
|
A lot above 0.1 day, or with a screen or a spec sheet, keeps the full cycle. The threshold is `EXPRESS_MAX_ESTIMATE` in `src/orchestrate/express.ts`.
|
|
571
574
|
|
|
575
|
+
|
|
576
|
+
## Cost in euros, before and after a wave (L529)
|
|
577
|
+
|
|
578
|
+
With a `perf.prices` grid in the `cadence.yaml` of the launch folder (the one `cadence perf` reads, see [perf](perf.md)), the wave is priced twice:
|
|
579
|
+
|
|
580
|
+
- **before**: `--dry-run` prints, after the simulation of the lots (their `budget du lot` lines), one line per lot `a:L1 — estimation du lot : 3,15 € (budget de 750000 tokens au prix sonnet, 90 % en entrée)` and then `estimation de la vague : …`. The estimate is the lot's token budget (the p90 measured for the budget, see **Budget** below) priced at the model of the lot's writing step, split 90 % input, 10 % output (the split of a journal that only carries `counted`). The budget counts input, cache writes and output: **cache reads and the steps of another model (reviews) are not in the estimate**, so the real cost, which counts them, can exceed it.
|
|
581
|
+
- **after**: the final table and `--status` gain a `€` column, the real cost of each lot (every step at the price of its own model, the four counters of the step, as `cadence perf`), and the `vague …` line ends its token count with the total in euros. A lot with a step on a model without price in the grid reads `n/d`; the wave total then adds only the priced lots and says `(n/d pour N lots, modèle sans prix)`. A `local` step costs 0 €.
|
|
582
|
+
|
|
583
|
+
Without `perf.prices` nothing changes: no estimate, no column, no total. A malformed grid does not stop a simulation, a wave or the table (a wave already paid must still print its table): the command prints `orchestrate : coût en euros n/d : …` on stderr (`--status`: a `⚠` line) and goes on without euros.
|
|
584
|
+
|
|
572
585
|
## The edge-case probe (`orchestrate.probe`, L497)
|
|
573
586
|
|
|
574
587
|
Code bugs are the first cause of correction passes, and an edge case is the first sub-cause of those (64 % of the `bug-code` passes since the instruction of L447, 69 % before: writing it in the brief changed nothing). `orchestrate.probe: true` adds a step between the writing and the review: a **fresh Sonnet session** (`probe`, brief `templates/orchestrate/probe.md`, `effort.probe` medium, the delay of a write step `timeouts.work`) that has not written the code, reads the lot's diff, and tries to break it with tests at the edges: empty, absent, twice, wrong type, refused, the real shape of the data, the other callers. The diff is given as a command (`git diff <start sha>..HEAD`), never pasted; the sheet of the lot is named when there is one. Like `implement` and `fix`, the brief gives the program's exact gate (L399: the probe commits tests the gate replays, `tsc` and the quality script included) and the session's machine share (L350). The session writes its tests in new files and commits them all, the passing and the failing, with `probe(L<n>): …`; it does not touch the application code nor the sealed acceptance tests.
|
|
@@ -617,9 +630,9 @@ nothing below applies and the wave behaves as before; any other value is refused
|
|
|
617
630
|
(`orchestrate.worktrees doit être true ou false`).
|
|
618
631
|
|
|
619
632
|
- **Which lots.** The lots of one repository (as project; two lots that share a neighbour `repos:` are one group) go to worktrees when the
|
|
620
|
-
group has at least two lots, the repository is on a branch (not a detached `HEAD`) and no lot of the group declares `repos:`. Otherwise they
|
|
633
|
+
group has at least two lots or the session cap is above one (`--max-sessions`, L527), the repository is on a branch (not a detached `HEAD`) and no lot of the group declares `repos:`. Otherwise they
|
|
621
634
|
stay one after the other in the repository, with `orchestrate : <repository> : worktrees demandés mais <reason> : lots joués à la suite` in the
|
|
622
|
-
journal.
|
|
635
|
+
journal. With a cap above one, every lot of the repository has its own worktree — given alone, drawn by the free-slot fill of `--continue` or added by `--add`, with or without `--continue` — so a second lot of the repository can take a free slot while the first plays; with a cap of 1 a lot alone in its repository plays in it. A lot that already has its branch (a resume) keeps its worktree whatever the group.
|
|
623
636
|
- **When.** At the moment the lot takes its place under the cap, after its `raf start` — not at launch: a lot waiting for a place has neither a
|
|
624
637
|
worktree nor a branch. A lot that depends on another lot of the repository (`after`) waits until that one is `ready`, which in this mode only
|
|
625
638
|
happens **after** its integration, then starts from the tip of the repository's branch, which contains it. If the worktree or the branch cannot be
|
package/docs/perf.md
CHANGED
|
@@ -71,6 +71,8 @@ perf:
|
|
|
71
71
|
An unknown key (under `perf`, under a model, or a model other than `sonnet`, `opus`, `haiku`), a negative or non-numeric price, or an unreadable file refuses the command before any journal is read (`raf: …`, exit 1, no output).
|
|
72
72
|
Without `perf.prices` (absent or empty) the cost is `n/d` and a warning says where to declare it; the other indicators are served.
|
|
73
73
|
|
|
74
|
+
The same grid prices the waves (L529, [orchestrate](orchestrate.md#cost-in-euros-before-and-after-a-wave-l529)): `orchestrate --dry-run` announces an estimate in euros per lot and for the wave, the final table and `--status` print the real cost.
|
|
75
|
+
|
|
74
76
|
**Cost of a step** = (`input` + `cacheWrite`) × input price + `output` × output price + `cacheRead` × cacheRead price, per million tokens, at the price of the step's own model. Assumptions written in the sheet (`docs/specs/perf-tokens-delais-cout/fiche.yaml`, R8):
|
|
75
77
|
cache writes are billed at the input price (the grid has no write price, so the cost is slightly underestimated if they are billed more); a step that only carries `counted` (an old journal) is split 90 % input, 10 % output, no cache read;
|
|
76
78
|
a step without any readable token costs 0 € (the lot stays in the population); a `local` step costs 0 € whatever its counters (outside quota and budget) and needs no price.
|
package/docs/raf.md
CHANGED
|
@@ -63,7 +63,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
|
|
|
63
63
|
| `raf add`, `raf note`, `raf done` while a wave works in the main tree (L475) | when the repository's orchestration lock is held by a live wave and one of its lots is in `implementing`, `reviewing` or `fixing` **without a worktree** (a wave without worktrees, or a lot whose worktree folder does not exist), these commands write the plan as usual and print on stderr `avertissement : la vague … travaille dans l'arbre principal de ce dépôt (projet:lot) — …`: a plan commit of the lead then hands the lot back at review (2026-10-30: cadence L458 handed back by a plan commit during its review). Write the plan without committing it, or commit once the step ends. Exit code 0; nothing is printed for lots that are queued, in question, ready, handed back, failed or suspended, for lots playing in their worktree, from a worktree of the repository, from a session of the wave itself (`CADENCE_ORCHESTRATED` set), when the lock belongs to a dead process, or when the wave's state is unreadable |
|
|
64
64
|
| `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
|
|
65
65
|
| `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, with the code-review gate active (`raf review enable`) a lot whose commits its review does not cover, **and a lot whose recorded review verdict says `non conforme` / `not compliant` even when it covers every commit** (L476: `non conforme : 1 bloquant` at the last commit no longer closes; `conforme après 1 correction` is compliant; redo the review with `raf review <id> "verdict"` after the fix, or `--force`), and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr. Like every `raf` command it only writes the plan file and **never commits** it: when the plan or a file `plan.onWrite` produces is left with changes, `done`, `ux <lot>` and `review <lot>` print the `git add` / `git commit` command that takes them, the subject from the `plan.commit` template, L371) |
|
|
66
|
-
| `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
|
|
66
|
+
| `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. A lot already `vert: true` whose tests lack fingerprints (L526) is replayed and, green, sealed (subject `chore(<lot>): empreintes de l'acceptation posées par cadence spec green`). Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
|
|
67
67
|
| `raf todo <id> [--reason text]` | put a started lot back to `todo` (`doing` → `todo`, `started:` removed, dated note « remis en attente : reason »): refused when work commits cite the lot (plan-only and version commits do not count, as `raf commits`), or on a sub-task; sub-tasks and other keys are left as they are. **Reopening** (L396): on a `done` or `dropped` lot, `raf todo` is THE command — never the YAML — that reopens it (`→ todo`, `finished:` removed, `started:` kept so its commits stay its own, dated note « rouvert (était done) : reason »); `raf start`/`raf done`/`raf drop` on a closed lot say so (`L3 est done ; le rouvrir si c'est voulu : raf todo L3`). An archived lot reopened this way comes back into the plan file at that write |
|
|
68
68
|
| `raf migrate [--dry-run]` | move `docs/plan/raf.yaml` to `.cadence/raf.yaml` and rewrite the readers of the old path in the repository, see [Moving the plan: `raf migrate`](#moving-the-plan-raf-migrate) |
|
|
69
69
|
| `raf note <id> "text"` | dated note — keep decisions next to the work |
|
|
@@ -114,7 +114,7 @@ the date. `cadence raf …` is the same as `raf …`.
|
|
|
114
114
|
|
|
115
115
|
| Command | What it does |
|
|
116
116
|
|---|---|
|
|
117
|
-
| `cadence local start <lot> [--ttl minutes]` | locks the repository as a wave does (`orchestrate.lock`, under the name `local:<lot>`): no wave starts in it, `orchestrate --resume` and `orchestrate --add` are refused with « une tâche locale du lead y est en cours (<lot>, depuis …) », and `deliver`
|
|
117
|
+
| `cadence local start <lot> [--ttl minutes]` | locks the repository as a wave does (`orchestrate.lock`, under the name `local:<lot>`): no wave starts in it, `orchestrate --resume` and `orchestrate --add` are refused with « une tâche locale du lead y est en cours (<lot>, depuis …) », and `deliver` and `raf move` are refused with the same message (the task is named with its exit `local done` / `local abort`; no `--sha` advice, a local task has no wave lots). The lot goes to `doing`. The lock is **declared**, like `cadence lead hold`: no process carries it (the shell that runs the command dies with the Bash tool call, and the lead's session has no stable process name), it holds until `local done` or `local abort`, and `--ttl` (60 min by default) is only the announced duration — past it, the lock still holds and `cadence lead tour` and `orchestrate --status` print « ⚠ tâche locale échue : <lot> … » until the lead finishes or aborts it; it never lapses silently. Refused, with nothing locked or written, when the lot is unknown, not marked `local`, `done` or `dropped`, when a wave or another local task holds the repository, or when another lot of the plan is `doing` (it is named). |
|
|
118
118
|
| `cadence local done <lot>` | plays the project's gate **once** (`orchestrate.build`, then `orchestrate.test`, at `HEAD`, as `cadence verify --gate`) and, if it is green, frees the repository. Red (code 1) or nothing to replay (code 2): the lock stays, fix and run it again. Refused when this lot's local task does not hold the lock. |
|
|
119
119
|
| `cadence local abort <lot>` | frees the repository without playing the gate (the task is given up or will be resumed later); the plan is not touched — the lot stays `doing`, `raf todo <lot>` puts it back. Refused when this lot's local task does not hold the lock. |
|
|
120
120
|
|
package/docs/releasing.md
CHANGED
|
@@ -86,6 +86,10 @@ It refuses, with exit code 2 and nothing written:
|
|
|
86
86
|
|
|
87
87
|
Write the two forms of the new version in `docs/whats-new.md` **before** running the command: the file may be modified (it is the one tracked file allowed to be, since the version commit holds it) and the command then reads the forms it finds. Run after the forms are written, it exits 0; if it exits 1 the version files are already written, so fix `docs/whats-new.md` by hand and do not run the command again (it would bump once more).
|
|
88
88
|
|
|
89
|
+
The check reads the whole « In two lines » list, as `test/readme-accueil.test.ts` (R5) does at the gate, and not only the line of the new version (L418): a bullet of any other version over 240 characters, or a long paragraph (`**x.y.z**: …`, no leading `- `) that fell among the short lines, is a finding too (exit 1). That is the 1.22.0 publication of 28-10, stopped by that very test after a long form had been committed in the list; the command now says it before the version commit and the tag.
|
|
90
|
+
|
|
91
|
+
The steps it prints are read by their exit code, never through a pipe: a `| tail` or a trailing `| grep` after the suite hands the shell the filter's code, and a red suite looks green (28-10, 29-10). The same holds for the workflows: `publish.yml` and `tests.yml` read the output of `npm publish` (and the diagnostics) with `grep … <<< "$out"`, no pipe (`test/publish-workflow.test.ts`), because under `set -o pipefail` a `printf | grep -q` over the long output of the suite would fail on a `SIGPIPE`.
|
|
92
|
+
|
|
89
93
|
`docs/whats-new.md` is **not written by the command** (the two forms are prose, written by hand or by a session): it checks that
|
|
90
94
|
both are there and in their place, and says where to write what is missing. The « In two lines » line of the version is the
|
|
91
95
|
first of the list and at most 240 characters; the long paragraph is the first of « Version by version », right after its
|
package/docs/spec.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
`cadence spec reseal <lot> <test> --reason texte` is the lead's way to correct one sealed acceptance test of the lot in progress that contradicts its sheet (unchanged, or amended by commits of the lot, L217; of a delivered lot, also by commits of another delivered lot, L229) (of a delivered lot: add `--porteur <lot in progress>`); refused while a wave of the repository is running, or a wave that is not `done` keeps open (question waiting, suspended, queued) any lot of this repository still `doing` in its plan, the resealed lot or another (L238: one error, also when a live wave holds the lock, names each wave (live ones to wait for), its launch folder and, for each stopped wave, the single `cd <folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> --drop …` that closes all its open lots of the repository, the queued ones that are not `doing` in the plan included (L239); a lot `done` or absent in the plan never refuses), see [The accept step](orchestrate.md#the-accept-step-red-acceptance-tests-and-their-lock).
|
|
30
30
|
|
|
31
|
-
`cadence spec green <lot>` (L366, GitHub issue #18) is the lead's way to put back to green a lot driven and closed by hand (`raf done`) whose `acceptation-<lot>.json` stayed at `vert: false`: closed like that, its tests keep the lock total and every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them, and `spec reseal` does not cover it (it takes a lot `doing`, or a delivered lot at `vert: true` with `--porteur`). The program runs the lot's acceptance command (`orchestrate.accept`, same rules as the cycle) and, green, sets `vert: true`, reseals the listed tests refined since the red commit (one `resceaux` entry each) and commits the file alone, citing the lot (`chore(<lot>): acceptation verte, vert: true posé par cadence spec green`, or the project's `plan.commit` template when it has one; the subject stays short and fixed, the tests resealed are listed in the commit body, one `- <path>` line each, so a `header-max-length` hook accepts it); red, the tail of the output goes to stderr, exit 1, nothing changed; already green, nothing to do. Refused for a `todo` lot, from a session (`CADENCE_ORCHESTRATED`), during a wave of the repository and while an unfinished wave keeps a lot of the repository open (the same refusals as `spec reseal`, worded for `spec green`); a listed test changed but not committed is refused before anything runs. `raf done` refuses such a lot without `--force`, and `cadence orchestrate --dry-run` prints `acceptation rouge d'un lot clos` once per repository, see [A closed lot left at vert: false](orchestrate.md#the-accept-step-red-acceptance-tests-and-their-lock).
|
|
31
|
+
`cadence spec green <lot>` (L366, GitHub issue #18) is the lead's way to put back to green a lot driven and closed by hand (`raf done`) whose `acceptation-<lot>.json` stayed at `vert: false`: closed like that, its tests keep the lock total and every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them, and `spec reseal` does not cover it (it takes a lot `doing`, or a delivered lot at `vert: true` with `--porteur`). The program runs the lot's acceptance command (`orchestrate.accept`, same rules as the cycle) and, green, sets `vert: true`, reseals the listed tests refined since the red commit (one `resceaux` entry each) and commits the file alone, citing the lot (`chore(<lot>): acceptation verte, vert: true posé par cadence spec green`, or the project's `plan.commit` template when it has one; the subject stays short and fixed, the tests resealed are listed in the commit body, one `- <path>` line each, so a `header-max-length` hook accepts it); red, the tail of the output goes to stderr, exit 1, nothing changed; already green and sealed, nothing to do; already `vert: true` but with a listed test lacking its fingerprint (closed before the seal, L526, issue #20), the command is replayed too and, green, only the missing fingerprints are sealed in a commit citing the lot (`chore(<lot>): empreintes de l'acceptation posées par cadence spec green`), red nothing changes. Refused for a `todo` lot, from a session (`CADENCE_ORCHESTRATED`), during a wave of the repository and while an unfinished wave keeps a lot of the repository open (the same refusals as `spec reseal`, worded for `spec green`); a listed test changed but not committed is refused before anything runs. `raf done` refuses such a lot without `--force`, and `cadence orchestrate --dry-run` prints `acceptation rouge d'un lot clos` once per repository, see [A closed lot left at vert: false](orchestrate.md#the-accept-step-red-acceptance-tests-and-their-lock).
|
|
32
32
|
|
|
33
33
|
`cadence spec qa <fiche.yaml>` derives from a valid sheet the section of `docs/qa/expectations.md` for its screen and prints it on the standard output
|
|
34
34
|
(it never writes the file: you paste the section, and the lead commits it like the rest of the plan files). A sheet with violations prints them on the
|
package/docs/whats-new.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## In two lines
|
|
6
6
|
|
|
7
|
+
- **1.39.0**: `orchestrate --continue` fills free slots by priority, one worktree per lot when the cap allows (issue #21); waves show their cost in euros before and after.
|
|
8
|
+
- **1.38.1**: `spec green` seals a lot closed at `vert: true` without fingerprints (issue #20); `deliver`, `raf move`, `spec reseal` and `spec green` refuse cleanly during a local task.
|
|
7
9
|
- **1.38.0**: an express lane plays a touch-up of at most 0.1 day in one session; `raf add --local` and `cadence local start|done|abort` lock a repository while the lead edits it by hand.
|
|
8
10
|
- **1.37.0**: one full test suite per lot, played by the gate (`suites` column); no correction pass without a finding; a richer implementation brief; `scripts/temps-d-un-lot.mjs` measures where a lot's time goes.
|
|
9
11
|
- **1.36.0**: `cadence deliver` no longer waits for the CI of a sha whose commits only touch the plan, with timed steps and a `deliveries-steps.log` journal; `orchestrate --add` puts a lot in a live wave.
|
|
@@ -66,6 +68,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
66
68
|
|
|
67
69
|
The long form of « In two lines » above (the README no longer carries a « What's new » section since L326: it links here). The full history, with the date of each version, is in [CHANGELOG.md](../CHANGELOG.md).
|
|
68
70
|
|
|
71
|
+
**1.39.0**: free slots no longer stay empty (GitHub issue #21, L527, `docs/orchestrate.md`). With `--continue`, once the order is agreed (`--priority` or `priority:` of the parent `cadence.yaml`), the wave starts the next ready lot in each slot as soon as one frees up — a lot handed back at its pre-check frees its slot in seconds —, in priority order, within the session cap, the shared resources and one session per repository or one worktree per lot, and the journal says so. In a repository with `orchestrate.worktrees: true` and a cap above 1, every lot gets its own worktree, a lot launched alone or brought by `--add` included, so a second lot of the same repository plays in parallel instead of waiting; `--dry-run --continue` now simulates the draws one by one, as the wave plays them. `cadence orchestrate --dry-run` also announces the cost in euros of each lot and of the wave beside its token budget, and the final table and `--status` show the real cost (L529, price grid `perf.prices`). `cadence release` reads the « In two lines » section of `docs/whats-new.md` the way the test does, so a line too long or a duplicate is caught before the tag (L418), and a timing test that turned red under load in a fresh clone has a wide margin (L528).
|
|
72
|
+
|
|
73
|
+
**1.38.1**: `cadence spec green <lot>` now handles a lot closed at `vert: true` with no fingerprints (closed before the seal, GitHub issue #20): it replays the lot's acceptance command and, green, writes the fingerprints in a commit that cites the lot, so a later lot that amends one of its tests no longer has to be finished outside a wave (L526, `docs/orchestrate.md`). During a local task of the lead (1.38.0), `cadence deliver`, `raf move`, `cadence spec reseal` and `cadence spec green` refuse with a message that names the task and how to end it, instead of « état illisible » or an impossible `--sha` hint (L525, `docs/raf.md`).
|
|
74
|
+
|
|
69
75
|
**1.38.0**: two ways to make a small change cheaper (L519, decided on 01-11 after a one-line UI separator cost 245 s of implementation). The **express lane**: a lot of at most 0.1 day with no screen sheet nor spec is played by `cadence orchestrate` in a single writing session, without precheck, edge-case probe, separate code review or « code sans test » gate, and without a News entry per lot; the lead reads the diff and the capture, groups the entries at delivery, and the program's gate still plays once (`docs/orchestrate.md`). The **local task**: `raf add --local` marks a lot the lead does by hand, and `cadence local start <lot>` locks the repository like a wave (no wave starts there, `--add` and `--continue` refuse it, and the task itself is refused while a wave or another lot of the repository runs). The lock is declared with a `--ttl` (60 min by default, like `lead hold`) and carried by no process; past its TTL it still holds and `lead tour` and `orchestrate --status` show a ⚠ until `cadence local done` (gate green) or `cadence local abort`. A wave refuses a `local` lot, and the pre-push hook recognises the task instead of reporting an unreadable wave (`docs/raf.md`).
|
|
70
76
|
|
|
71
77
|
**1.37.0**: the first release of the efficiency work, sized by a measure of 721 lots (L517, `docs/mesures/temps-d-un-lot.md`, script `node scripts/temps-d-un-lot.mjs`): a lot took 25 minutes and 265k tokens, tests were 62 % of the writing sessions' wall time and a correction pass cost about 12 minutes. The writing sessions of `cadence orchestrate` now play only the targeted tests of the code they touch; the full suite is played once by the program as the gate, `cadence verify --gate` takes its verdict back, and the final table and `--status` count the full suites of each lot in a `suites` column (L518, `docs/orchestrate.md`). A non-compliant review that lists no blocking or major finding no longer opens a correction pass: the lot is handed back with the announced totals as the cause; a test red in the suite and green alone is proposed as a lot to stabilise, and a green program gate always outweighs a session's claim of red tests (L522). The `implement` brief carries the code reviewer's grid, « edge-case tests first, seen failing, then code » and a re-reading of the diff before the last commit; the pre-check passes what it read (`precheckLu`) to the first implementation so it does not read the repository again (L523). The tests workflow tolerates the npm « previously published » refusal of its publish dry-run on a non-release commit, and nothing else (L515).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sylvain Ladoire",
|