@sylad/cadence 1.38.1 → 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/cost.js +40 -0
- 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/release.js +18 -3
- package/docs/orchestrate.md +18 -6
- package/docs/perf.md +2 -0
- package/docs/releasing.md +4 -0
- package/docs/whats-new.md +3 -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/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
|
+
}
|
|
@@ -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/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/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
|
|
|
@@ -570,6 +572,16 @@ A lot of **at most 0.1 day** (`estimate: 0.1` or less, no `screen:`, no `spec: t
|
|
|
570
572
|
|
|
571
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`.
|
|
572
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
|
+
|
|
573
585
|
## The edge-case probe (`orchestrate.probe`, L497)
|
|
574
586
|
|
|
575
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.
|
|
@@ -618,9 +630,9 @@ nothing below applies and the wave behaves as before; any other value is refused
|
|
|
618
630
|
(`orchestrate.worktrees doit être true ou false`).
|
|
619
631
|
|
|
620
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
|
|
621
|
-
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
|
|
622
634
|
stay one after the other in the repository, with `orchestrate : <repository> : worktrees demandés mais <reason> : lots joués à la suite` in the
|
|
623
|
-
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.
|
|
624
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
|
|
625
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
|
|
626
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/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/whats-new.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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.
|
|
7
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.
|
|
8
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.
|
|
9
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.
|
|
@@ -67,6 +68,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
67
68
|
|
|
68
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).
|
|
69
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
|
+
|
|
70
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`).
|
|
71
74
|
|
|
72
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`).
|
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",
|