@sylad/cadence 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +286 -22
- package/agents/acceptance-writer.md +21 -0
- package/agents/code-reviewer.md +8 -1
- package/agents/spec-writer.md +23 -0
- package/agents/ux-reviewer.md +20 -1
- package/bin/cadence.js +11 -1
- package/dist/audit.js +14 -2
- package/dist/cli.js +233 -18
- package/dist/config.js +46 -5
- package/dist/corrections.js +228 -0
- package/dist/lead.js +72 -19
- package/dist/news.js +54 -7
- package/dist/orchestrate/accept.js +389 -0
- package/dist/orchestrate/aval.js +65 -0
- package/dist/orchestrate/briefs.js +7 -1
- package/dist/orchestrate/command.js +70 -20
- package/dist/orchestrate/continue.js +1 -1
- package/dist/orchestrate/cycle.js +662 -24
- package/dist/orchestrate/fiche.js +22 -0
- package/dist/orchestrate/launch.js +17 -10
- package/dist/orchestrate/maquette.js +27 -0
- package/dist/orchestrate/sandbox.js +38 -0
- package/dist/orchestrate/schemas.js +17 -1
- package/dist/orchestrate/spec.js +197 -0
- package/dist/orchestrate/state.js +2 -2
- package/dist/orchestrate/table.js +2 -0
- package/dist/plan.js +79 -1
- package/dist/spec.js +457 -0
- package/package.json +4 -3
- package/skills/lead/SKILL.md +33 -8
- package/templates/acceptation-runner.mjs +61 -0
- package/templates/fiche-back.yaml +75 -0
- package/templates/fiche.yaml +41 -0
- package/templates/orchestrate/accept-back.md +16 -0
- package/templates/orchestrate/accept.md +16 -0
- package/templates/orchestrate/fiche.md +14 -0
- package/templates/orchestrate/fix-minors.md +1 -0
- package/templates/orchestrate/fix.md +1 -0
- package/templates/orchestrate/implement.md +1 -0
- package/templates/orchestrate/maquette.md +12 -0
- package/templates/orchestrate/review-small.md +1 -0
- package/templates/orchestrate/review.md +1 -0
- package/templates/orchestrate/spec-back.md +25 -0
- package/templates/orchestrate/spec.md +28 -0
- package/templates/orchestrate/ux.md +1 -0
|
@@ -12,8 +12,12 @@ import { AGENTS_DIR } from '../skills.js';
|
|
|
12
12
|
import { pidAlive, sharedStateDir } from '../state.js';
|
|
13
13
|
import { acquireSlot, cadenceHome, freeSlots, liveSlots, liveWaves, registerWave, unregisterWave, updateWaveRepos } from './registry.js';
|
|
14
14
|
import { loadTemplates, newsText, objective, renderBrief } from './briefs.js';
|
|
15
|
+
import { acceptationFile, acceptText } from './accept.js';
|
|
16
|
+
import { ficheReviewText } from './fiche.js';
|
|
17
|
+
import { maquetteText } from './maquette.js';
|
|
18
|
+
import { specText } from './spec.js';
|
|
15
19
|
import { candidates, parsePriority, parseUntil, readPriority, stopReason } from './continue.js';
|
|
16
|
-
import { Budget, DROPPED, MAX_PASSES, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
|
|
20
|
+
import { Budget, DROPPED, MAX_PASSES, briefName, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
|
|
17
21
|
import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
|
|
18
22
|
import { buildArgs, buildLocalArgs, killSessions, mcpServersFor, readAgents, realClaude } from './launch.js';
|
|
19
23
|
import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
@@ -25,18 +29,45 @@ import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
|
25
29
|
import { quotaText, renderTable } from './table.js';
|
|
26
30
|
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, snapshotExists, waveReadsControl, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
27
31
|
export const DEFAULT_BUDGET = 2_000_000;
|
|
28
|
-
/** Tokens comptés par jour d'estimate (L78) ; mesuré le 06-10 : des lots de 0,5 j ont mangé 192 à 531 k, des lots de 1 j 455 à 468 k (2 M pour 6 lots). Depuis le plancher de L128 (450 k), ce plafond ne borne plus que les lots de plus de 1,125 j : les 5 lots du 06-10 passent tous. */
|
|
29
|
-
const LOT_BUDGET_PER_DAY = 400_000;
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* L62 (estimate 1,5, donc 600 k) n'est pas changé par le plancher, la réserve de L145 couvre déjà sa correction et sa revue.
|
|
33
|
+
* Budget d'un lot ordinaire (L78, L128, recalibré L194). Mesuré sur le TOTAL de chaque lot dans `.cadence/runs` (toutes les vagues
|
|
34
|
+
* où il est passé, 250 lots, 11-10), rangé par estimate : jusqu'à 0,5 j p90 374 k (152 lots), de 0,5 à 1 j p90 732 k (88 lots, max
|
|
35
|
+
* 1,09 M), au-delà d'un jour p90 1,14 M pour les 10 lots de 1,25 à 2 j (570 k par jour d'estimate), max 1,7 M (L188). Le plafond
|
|
36
|
+
* de L78 (400 k par jour) rendait au lead, au deuxième tour, un lot de 1 j (L188 : 483 k puis 495 k).
|
|
36
37
|
*/
|
|
37
38
|
const LOT_BUDGET_FLOOR = 450_000;
|
|
38
|
-
/**
|
|
39
|
-
|
|
39
|
+
/** Lot de plus de 0,5 j et d'au plus 1 j : p90 mesuré du total, 732 k arrondis à 750 k. */
|
|
40
|
+
const LOT_BUDGET_ONE_DAY = 750_000;
|
|
41
|
+
/** Tokens par jour d'estimate au-delà d'un jour : p90 1,14 M pour 2 j. */
|
|
42
|
+
const LOT_BUDGET_PER_DAY = 570_000;
|
|
43
|
+
/**
|
|
44
|
+
* Total d'un lot à `screen:` (L194), étapes spec, maquette et accept comprises : p90 1,28 M sur 5 lots (L113 1,12 M, L25 1,28 M, L50 877 k,
|
|
45
|
+
* L85 758 k, L43 238 k), arrondi à 1,3 M. Remplace l'allocation de L191 (+350 k au budget d'estimate), qui ne payait ni l'UX de
|
|
46
|
+
* conformité ni les passes de correction.
|
|
47
|
+
*/
|
|
48
|
+
const SCREEN_LOT_BUDGET = 1_300_000;
|
|
49
|
+
/**
|
|
50
|
+
* Allocation d'un lot à fiche `spec: true` (L188/L191), en plus du budget dérivé de l'estimate : sessions spec, fiche et accept hors de
|
|
51
|
+
* l'estimate d'écriture. Aucun lot `spec:` seul n'a encore été mesuré ; 350 k est le pire cas mesuré des étapes d'un lot à écran (ol L113 : 349 k).
|
|
52
|
+
*/
|
|
53
|
+
export const SPEC_STAGE_ALLOCATION = 350_000;
|
|
54
|
+
/** Base mesurée d'un budget, pour `--dry-run` : l'estimate (et l'étape spec) d'où il sort. */
|
|
55
|
+
export function lotBudgetBasis(estimate, stage) {
|
|
56
|
+
if (stage === 'screen' && lotBudget(estimate, stage) === SCREEN_LOT_BUDGET)
|
|
57
|
+
return "p90 mesuré du total d'un lot à écran, étapes spec, maquette et accept comprises";
|
|
58
|
+
const ordinary = lotBudget(estimate);
|
|
59
|
+
const basis = estimate <= 0.5 ? "plancher d'un lot jusqu'à 0,5 j" : estimate <= 1 ? "p90 mesuré du total d'un lot de 0,5 à 1 j" : Math.round(estimate * LOT_BUDGET_PER_DAY) <= LOT_BUDGET_ONE_DAY ? `plancher de ${LOT_BUDGET_ONE_DAY} au-delà d'un jour, l'estimate × ${LOT_BUDGET_PER_DAY} ne le dépasse pas encore` : `${LOT_BUDGET_PER_DAY} par jour, p90 mesuré du total au-delà d'un jour`;
|
|
60
|
+
if (!stage)
|
|
61
|
+
return basis;
|
|
62
|
+
return `${ordinary} dérivés de l'estimate (${basis}), plus ${SPEC_STAGE_ALLOCATION} pour les étapes spec, ${stage === 'screen' ? 'maquette' : 'fiche'} et accept`;
|
|
63
|
+
}
|
|
64
|
+
/** Budget d'un lot, dérivé de son estimate et de sa catégorie : empêche quelques lots d'avaler le budget de la vague au détriment des autres. Un lot à `screen:` vaut le total mesuré d'un lot à écran (L194) ; un lot à `spec: true` y ajoute l'allocation de son étape (L191). */
|
|
65
|
+
export const lotBudget = (estimate, stage) => {
|
|
66
|
+
const ordinary = estimate <= 0.5 ? LOT_BUDGET_FLOOR : Math.max(LOT_BUDGET_ONE_DAY, Math.round(estimate * LOT_BUDGET_PER_DAY));
|
|
67
|
+
if (stage === 'screen')
|
|
68
|
+
return Math.max(SCREEN_LOT_BUDGET, ordinary + SPEC_STAGE_ALLOCATION);
|
|
69
|
+
return ordinary + (stage === 'spec' ? SPEC_STAGE_ALLOCATION : 0);
|
|
70
|
+
};
|
|
40
71
|
/** Sessions simultanées, toutes vagues confondues (`--max-sessions`, ou CADENCE_MAX_SESSIONS). */
|
|
41
72
|
export const DEFAULT_MAX_SESSIONS = 2;
|
|
42
73
|
/** `1500000`, `1.5M`, `800k`. */
|
|
@@ -55,11 +86,11 @@ export function parseMaxSessions(text, what = '--max-sessions') {
|
|
|
55
86
|
return Number(text);
|
|
56
87
|
}
|
|
57
88
|
// une vague (2026-10-04-1412) n'a ni « : » ni « / » : leur présence désigne un lot, quel que soit son motif (R-M7/ux-1)
|
|
58
|
-
const LOT_LIKE = /^(?:[\w.-]
|
|
89
|
+
const LOT_LIKE = /^(?:[\w.-]+(?:\/[\w.-]+)*:[\w.-][\w./-]*|[\w.-]+\/[\w./-]*|[A-Za-z]+\d+)(?:@\w+)?$/;
|
|
59
90
|
const MODELS = ['sonnet', 'opus', 'haiku'];
|
|
60
91
|
function lotArg(text) {
|
|
61
|
-
// le premier « : » sépare le projet du lot, un « @ » final le modèle ; le lot garde ses « / » (L120)
|
|
62
|
-
const m = /^(?:([\w.-]+):)?([\w.-][\w./-]*?)(?:@(\w+))?$/.exec(text);
|
|
92
|
+
// le premier « : » sépare le projet du lot, un « @ » final le modèle ; le lot garde ses « / » (L120), le projet peut être un chemin depuis le dossier (`groupe/gamma`, projet imbriqué de lead tour --depth, L201)
|
|
93
|
+
const m = /^(?:([\w.-]+(?:\/[\w.-]+)*):)?([\w.-][\w./-]*?)(?:@(\w+))?$/.exec(text);
|
|
63
94
|
if (!m)
|
|
64
95
|
throw new RafError(`lot invalide : ${text} (projet:lot, lot, projet:lot@haiku)`);
|
|
65
96
|
if (m[3] && !MODELS.includes(m[3]))
|
|
@@ -240,11 +271,11 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
240
271
|
continue;
|
|
241
272
|
}
|
|
242
273
|
const small = lot.estimate <= 0.5 || lot.quickwin;
|
|
243
|
-
const state = newLot({ project: t.project, repo: t.repo, lot: t.lot, title: lot.title, visible: lot.visible, small, model: t.model, readOnlyPlan: plan.readonly });
|
|
274
|
+
const state = newLot({ project: t.project, repo: t.repo, lot: t.lot, title: lot.title, visible: lot.visible, small, ...(lot.screen ? { screen: lot.screen } : {}), ...(lot.spec && !lot.screen ? { specLot: true } : {}), model: t.model, readOnlyPlan: plan.readonly });
|
|
244
275
|
if (neighbours.repos.length)
|
|
245
276
|
state.repos = neighbours.repos;
|
|
246
277
|
state.light = lot.estimate <= env.config.review.threshold;
|
|
247
|
-
state.budget = lotBudget(lot.estimate);
|
|
278
|
+
state.budget = lotBudget(lot.estimate, lot.screen ? 'screen' : lot.spec ? 'spec' : undefined);
|
|
248
279
|
state.dependsOn = lot.after.filter((d) => earlier.includes(d));
|
|
249
280
|
if (node.kind === 'ok')
|
|
250
281
|
state.node = { version: node.version, wanted: node.wanted, bin: node.bin, ...(node.skipped ? { skipped: node.skipped } : {}) };
|
|
@@ -281,7 +312,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
281
312
|
const lot = env.loadPlan().lot(l.lot);
|
|
282
313
|
const slot = repos.indexOf(l.repo);
|
|
283
314
|
io.out(`${lotKey(l.project, l.lot)} — ${l.title}`);
|
|
284
|
-
io.out(` budget du lot : ${l.budget} tokens comptés (
|
|
315
|
+
io.out(` budget du lot : ${l.budget} tokens comptés (base : ${lotBudgetBasis(lot.estimate, l.screen ? 'screen' : l.specLot ? 'spec' : undefined)})`);
|
|
285
316
|
io.out(` file ${basename(l.repo)} · ${slot < 2 ? `créneau ${slot + 1}` : 'en attente d\'un créneau'}`);
|
|
286
317
|
if (l.repos?.length)
|
|
287
318
|
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})`);
|
|
@@ -291,6 +322,15 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
291
322
|
io.out(` node : ${l.node.version} (.nvmrc ${l.node.wanted}${skipped})`);
|
|
292
323
|
}
|
|
293
324
|
const steps = [{ kind: 'implement', model: l.model }];
|
|
325
|
+
const specStage = !!(l.screen || l.specLot);
|
|
326
|
+
if (specStage && env.config.accept)
|
|
327
|
+
steps.unshift({ kind: 'accept', model: 'sonnet' });
|
|
328
|
+
if (l.screen && env.config.maquette)
|
|
329
|
+
steps.unshift({ kind: 'maquette', model: env.config.review.full });
|
|
330
|
+
if (l.specLot && env.config.fiche)
|
|
331
|
+
steps.unshift({ kind: 'fiche', model: 'sonnet' });
|
|
332
|
+
if (specStage)
|
|
333
|
+
steps.unshift({ kind: 'spec', model: 'sonnet' });
|
|
294
334
|
if (needsPrecheck(env.config, env.loadPlan(), l.repo, l.lot, l.repos))
|
|
295
335
|
steps.unshift({ kind: 'precheck', model: env.config.precheckLocal ? 'local' : 'sonnet' });
|
|
296
336
|
const { light, full } = env.config.review;
|
|
@@ -307,16 +347,26 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
307
347
|
const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id, lotSlug(l.project, l.lot), 'playwright');
|
|
308
348
|
// brief écrit par --dry-run = base d'une délégation à la main : aucun dossier de vague (il n'existe pas)
|
|
309
349
|
const vars = { chemin: l.repo, lot: l.lot, titre: lot.title, objectif: objective(lot), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '' };
|
|
350
|
+
if (specStage)
|
|
351
|
+
vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, back: !!l.specLot });
|
|
352
|
+
if (l.screen && env.config.maquette)
|
|
353
|
+
vars.maquette = maquetteText({ fiche: `${env.config.spec.dir}/<écran>/fiche.yaml`, passes: 0 });
|
|
354
|
+
if (l.specLot && env.config.fiche)
|
|
355
|
+
vars.fiche = ficheReviewText({ fiche: `${env.config.spec.dir}/<sujet>/fiche.yaml`, passes: 0 });
|
|
356
|
+
if (specStage && env.config.accept) {
|
|
357
|
+
const folder = `${env.config.spec.dir}/${l.screen ? '<écran>' : '<sujet>'}`;
|
|
358
|
+
vars.accept = acceptText({ fiche: `${folder}/fiche.yaml`, maquette: `${folder}/maquette.html`, screen: !!l.screen, back: false, acceptation: acceptationFile(`${folder}/fiche.yaml`, l.lot), command: env.config.accept ?? '', test: env.config.test });
|
|
359
|
+
}
|
|
310
360
|
for (const s of steps) {
|
|
311
361
|
const file = join(tmp, `${lotSlug(l.project, l.lot)}--${s.kind}.md`);
|
|
312
|
-
const brief = renderBrief(s.kind, vars, deps.templatesDir);
|
|
362
|
+
const brief = renderBrief(briefName(l, s.kind), s.kind === 'fiche' || vars.fiche === undefined ? vars : { ...vars, fiche: '' }, deps.templatesDir); // la consigne de revue de fiche ne sert qu'au brief de la fiche : implement et review en sont la base d'une délégation à la main
|
|
313
363
|
writeFileSync(file, brief);
|
|
314
|
-
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small).playwright;
|
|
364
|
+
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen).playwright;
|
|
315
365
|
const local = s.model === 'local';
|
|
316
|
-
const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: s.kind === 'implement' ? undefined : s.kind === 'ux' ? 'ux-reviewer' : s.kind === 'precheck' ? 'precheck-reader' : 'code-reviewer', cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && s.kind === 'implement' ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
|
|
366
|
+
const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: s.kind === 'implement' ? undefined : s.kind === 'spec' ? 'spec-writer' : s.kind === 'accept' ? 'acceptance-writer' : s.kind === 'ux' || s.kind === 'maquette' ? 'ux-reviewer' : s.kind === 'precheck' ? 'precheck-reader' : 'code-reviewer', cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && (s.kind === 'implement' || s.kind === 'spec') ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
|
|
317
367
|
io.out(` ${s.kind} : ${local ? 'claude-local' : 'claude'} ${args.join(' ')}`);
|
|
318
368
|
io.out(` brief : ${file}`);
|
|
319
|
-
const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small));
|
|
369
|
+
const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen));
|
|
320
370
|
io.out(` mcp : ${servers.length ? `${servers.join(', ')} (captures dans le dossier de la vague : <vague>/${lotSlug(l.project, l.lot)}/playwright)` : 'aucun'}`);
|
|
321
371
|
}
|
|
322
372
|
}
|
|
@@ -72,7 +72,7 @@ export function candidates(launch, o) {
|
|
|
72
72
|
continue;
|
|
73
73
|
}
|
|
74
74
|
for (const lot of ready) {
|
|
75
|
-
if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot) || lotBudget(lot.estimate) > o.remaining)
|
|
75
|
+
if (o.exclude.has(`${project}:${lot.id}`) || decisionPending(lot) || lotBudget(lot.estimate, lot.screen ? 'screen' : lot.spec ? 'spec' : undefined) > o.remaining)
|
|
76
76
|
continue;
|
|
77
77
|
out.push({ project, dir, lot });
|
|
78
78
|
}
|