@sylad/cadence 1.1.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 +114 -21
- package/agents/acceptance-writer.md +3 -1
- package/agents/code-reviewer.md +6 -0
- package/agents/spec-writer.md +3 -1
- package/bin/cadence.js +4 -1
- package/dist/cli.js +145 -15
- package/dist/config.js +16 -4
- package/dist/lead.js +72 -19
- package/dist/orchestrate/accept.js +90 -4
- package/dist/orchestrate/aval.js +25 -6
- package/dist/orchestrate/briefs.js +3 -0
- package/dist/orchestrate/command.js +59 -26
- package/dist/orchestrate/continue.js +1 -1
- package/dist/orchestrate/cycle.js +260 -53
- package/dist/orchestrate/fiche.js +22 -0
- package/dist/orchestrate/launch.js +7 -7
- package/dist/orchestrate/spec.js +71 -14
- package/dist/orchestrate/state.js +2 -2
- package/dist/orchestrate/table.js +2 -0
- package/dist/plan.js +58 -1
- package/dist/spec.js +304 -77
- package/package.json +4 -3
- package/skills/lead/SKILL.md +33 -8
- package/templates/acceptation-runner.mjs +13 -5
- package/templates/fiche-back.yaml +75 -0
- package/templates/orchestrate/accept-back.md +16 -0
- package/templates/orchestrate/fiche.md +14 -0
- package/templates/orchestrate/spec-back.md +25 -0
package/dist/orchestrate/aval.js
CHANGED
|
@@ -1,30 +1,46 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import { LARGEURS } from '../spec.js';
|
|
3
3
|
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
4
|
-
/** Une ligne par règle de la fiche : id, [état, statut], énoncé, message exact, tests prévus. Vide quand le document n'est pas une fiche. */
|
|
4
|
+
/** Une ligne par règle de la fiche : id, [état ou cas, statut], énoncé, message exact, tests prévus. Vide quand le document n'est pas une fiche. */
|
|
5
5
|
export function ficheRules(doc) {
|
|
6
6
|
if (!isObj(doc) || !Array.isArray(doc.regles))
|
|
7
7
|
return [];
|
|
8
8
|
return doc.regles.filter(isObj).map((r) => {
|
|
9
9
|
const message = typeof r.message === 'string' ? ` Message: « ${r.message} ».` : '';
|
|
10
10
|
const tests = Array.isArray(r.tests) ? ` Tests: ${r.tests.map((t) => `« ${String(t)} »`).join(', ')}.` : '';
|
|
11
|
-
|
|
11
|
+
const where = typeof r.etat === 'string' ? r.etat : Array.isArray(r.cas) ? r.cas.join(', ') : '';
|
|
12
|
+
return `- ${String(r.id)} [${where ? `${where}, ` : ''}${String(r.statut)}] ${String(r.enonce ?? '').trim()}${message}${tests}`;
|
|
12
13
|
});
|
|
13
14
|
}
|
|
15
|
+
/** Une ligne par contrat de la fiche back : id, appel, entrée, sortie, erreurs (code et message exact). Vide sans contrats. */
|
|
16
|
+
export function ficheContracts(doc) {
|
|
17
|
+
if (!isObj(doc) || !Array.isArray(doc.contrats))
|
|
18
|
+
return [];
|
|
19
|
+
return doc.contrats.filter(isObj).map((c) => {
|
|
20
|
+
const erreurs = Array.isArray(c.erreurs) && c.erreurs.length ? ` Errors: ${c.erreurs.filter(isObj).map((e) => `${String(e.code)} « ${String(e.message)} »`).join(', ')}.` : '';
|
|
21
|
+
return `- ${String(c.id)} ${String(c.appel)} — input: ${String(c.entree ?? '').trim()}; output: ${String(c.sortie ?? '').trim()}.${erreurs}`;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Le document a-t-il une partie écran (`ecran`) ? Une fiche back seule n'en a pas : ni maquette, ni captures. */
|
|
25
|
+
export const hasScreenPart = (doc) => isObj(doc) && isObj(doc.ecran);
|
|
14
26
|
/** Consigne de la fiche pour une étape en aval : `{{fiche}}` des gabarits implement, fix, review et ux. */
|
|
15
27
|
export function avalFicheText(kind, o) {
|
|
16
28
|
const folder = posix.dirname(o.fiche);
|
|
17
29
|
const maquette = `${folder}/maquette.html`;
|
|
18
|
-
const
|
|
30
|
+
const screen = o.screen !== false;
|
|
31
|
+
const contracts = o.contracts?.length ? `\nIts contracts (the calls the code must hold, with the exact codes and messages of their errors) are:\n${o.contracts.join('\n')}` : '';
|
|
32
|
+
const head = screen
|
|
33
|
+
? `This lot has a screen sheet: \`${o.fiche}\` (mockup \`${maquette}\`). It is the reference of the screen; its rules are:\n${o.rules.join('\n')}${contracts}`
|
|
34
|
+
: `This lot has a spec sheet: \`${o.fiche}\` (no screen: use cases, rules, contracts and data). It is the reference of the behaviour; its rules are:\n${o.rules.join('\n')}${contracts}`;
|
|
19
35
|
if (kind === 'implement' || kind === 'fix') {
|
|
20
36
|
return [
|
|
21
37
|
head,
|
|
22
|
-
kind === 'implement' ?
|
|
38
|
+
kind === 'implement' ? `Read the sheet${screen ? ' and the mockup' : ''} before the first test.` : 'The sheet stays the reference of the fix: a finding that contradicts it is contested under "choix" with the rule.',
|
|
23
39
|
o.acceptation
|
|
24
40
|
? `The acceptance tests are already written and committed red, from the sheet (files listed in \`${o.acceptation}\`): make them green without modifying them — the program compares them to the red commit, and a modified file hands the lot back. A change one of them needs is a question or a finding, never a silent edit.`
|
|
25
41
|
: '',
|
|
26
42
|
'The tests you write yourself cite the rule they cover in their name (`R2 — …`); do not invent a rule: a case the sheet does not cover is noted in your report (under "nonVerifie" or "choix"), not coded.',
|
|
27
|
-
kind === 'implement' ? 'The News entry ends with a `## Hypothèses` section: one line per rule whose status is `hypothese` (`- R3 — what the screen does, and what changes if it is wrong`, taken from its `si_faux`); leave it out when the sheet has no hypothesis.' : '',
|
|
43
|
+
kind === 'implement' && screen ? 'The News entry ends with a `## Hypothèses` section: one line per rule whose status is `hypothese` (`- R3 — what the screen does, and what changes if it is wrong`, taken from its `si_faux`); leave it out when the sheet has no hypothesis.' : '',
|
|
28
44
|
]
|
|
29
45
|
.filter(Boolean)
|
|
30
46
|
.join('\n');
|
|
@@ -36,8 +52,11 @@ export function avalFicheText(kind, o) {
|
|
|
36
52
|
o.acceptation
|
|
37
53
|
? `The acceptance tests are listed in \`${o.acceptation}\`${o.rouge ? ` and were committed red at ${o.rouge.slice(0, 7)}` : ''}: each file must be unchanged since the red commit (\`git diff ${o.rouge ? o.rouge.slice(0, 7) : '<red commit>'} -- <file>\` is empty).`
|
|
38
54
|
: 'No acceptance tests were written for this lot: the tests of the rules are the ones the implementation wrote, named after the rule (`R2 — …`).',
|
|
55
|
+
o.contracts?.length ? 'Check each contract too: the code returns the output of the sheet and the exact codes and messages of its errors, and every datum the sheet announces without modification (`donnees`, `modification: aucune`) is intact. A contract that departs from the sheet is a major finding.' : '',
|
|
39
56
|
'A rule without a test, an acceptance test modified since the red commit, a rule coded that is not in the sheet, and a test that is green without exercising its rule are major findings (name the rule and the file).',
|
|
40
|
-
]
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join('\n');
|
|
41
60
|
}
|
|
42
61
|
return [
|
|
43
62
|
`This lot has a screen sheet (\`${o.fiche}\`) and a mockup (\`${maquette}\`) that was already reviewed: this pass is a conformity pass, not the full audit. Compare the running app to the mockup, state by state (the states of the sheet), at ${LARGEURS.join(' and ')} px, against the captures \`${folder}/maquette-<état>-<largeur>.png\`.`,
|
|
@@ -42,6 +42,9 @@ export function capturesText(captureDir) {
|
|
|
42
42
|
const FILES = {
|
|
43
43
|
precheck: 'precheck.md',
|
|
44
44
|
spec: 'spec.md',
|
|
45
|
+
'spec-back': 'spec-back.md',
|
|
46
|
+
'accept-back': 'accept-back.md',
|
|
47
|
+
fiche: 'fiche.md',
|
|
45
48
|
accept: 'accept.md',
|
|
46
49
|
maquette: 'maquette.md',
|
|
47
50
|
implement: 'implement.md',
|
|
@@ -13,10 +13,11 @@ 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
15
|
import { acceptationFile, acceptText } from './accept.js';
|
|
16
|
+
import { ficheReviewText } from './fiche.js';
|
|
16
17
|
import { maquetteText } from './maquette.js';
|
|
17
18
|
import { specText } from './spec.js';
|
|
18
19
|
import { candidates, parsePriority, parseUntil, readPriority, stopReason } from './continue.js';
|
|
19
|
-
import { Budget, DROPPED, MAX_PASSES, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
|
|
20
|
+
import { Budget, DROPPED, MAX_PASSES, briefName, countInterrupted, handBackDroppedQuestions, needsPrecheck } from './cycle.js';
|
|
20
21
|
import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
|
|
21
22
|
import { buildArgs, buildLocalArgs, killSessions, mcpServersFor, readAgents, realClaude } from './launch.js';
|
|
22
23
|
import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
@@ -28,18 +29,45 @@ import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
|
28
29
|
import { quotaText, renderTable } from './table.js';
|
|
29
30
|
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, snapshotExists, waveReadsControl, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
30
31
|
export const DEFAULT_BUDGET = 2_000_000;
|
|
31
|
-
/** 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. */
|
|
32
|
-
const LOT_BUDGET_PER_DAY = 400_000;
|
|
33
32
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* 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).
|
|
39
37
|
*/
|
|
40
38
|
const LOT_BUDGET_FLOOR = 450_000;
|
|
41
|
-
/**
|
|
42
|
-
|
|
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
|
+
};
|
|
43
71
|
/** Sessions simultanées, toutes vagues confondues (`--max-sessions`, ou CADENCE_MAX_SESSIONS). */
|
|
44
72
|
export const DEFAULT_MAX_SESSIONS = 2;
|
|
45
73
|
/** `1500000`, `1.5M`, `800k`. */
|
|
@@ -58,11 +86,11 @@ export function parseMaxSessions(text, what = '--max-sessions') {
|
|
|
58
86
|
return Number(text);
|
|
59
87
|
}
|
|
60
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)
|
|
61
|
-
const LOT_LIKE = /^(?:[\w.-]
|
|
89
|
+
const LOT_LIKE = /^(?:[\w.-]+(?:\/[\w.-]+)*:[\w.-][\w./-]*|[\w.-]+\/[\w./-]*|[A-Za-z]+\d+)(?:@\w+)?$/;
|
|
62
90
|
const MODELS = ['sonnet', 'opus', 'haiku'];
|
|
63
91
|
function lotArg(text) {
|
|
64
|
-
// le premier « : » sépare le projet du lot, un « @ » final le modèle ; le lot garde ses « / » (L120)
|
|
65
|
-
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);
|
|
66
94
|
if (!m)
|
|
67
95
|
throw new RafError(`lot invalide : ${text} (projet:lot, lot, projet:lot@haiku)`);
|
|
68
96
|
if (m[3] && !MODELS.includes(m[3]))
|
|
@@ -243,11 +271,11 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
243
271
|
continue;
|
|
244
272
|
}
|
|
245
273
|
const small = lot.estimate <= 0.5 || lot.quickwin;
|
|
246
|
-
const state = newLot({ project: t.project, repo: t.repo, lot: t.lot, title: lot.title, visible: lot.visible, small, ...(lot.screen ? { screen: lot.screen } : {}), 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 });
|
|
247
275
|
if (neighbours.repos.length)
|
|
248
276
|
state.repos = neighbours.repos;
|
|
249
277
|
state.light = lot.estimate <= env.config.review.threshold;
|
|
250
|
-
state.budget = lotBudget(lot.estimate);
|
|
278
|
+
state.budget = lotBudget(lot.estimate, lot.screen ? 'screen' : lot.spec ? 'spec' : undefined);
|
|
251
279
|
state.dependsOn = lot.after.filter((d) => earlier.includes(d));
|
|
252
280
|
if (node.kind === 'ok')
|
|
253
281
|
state.node = { version: node.version, wanted: node.wanted, bin: node.bin, ...(node.skipped ? { skipped: node.skipped } : {}) };
|
|
@@ -284,7 +312,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
284
312
|
const lot = env.loadPlan().lot(l.lot);
|
|
285
313
|
const slot = repos.indexOf(l.repo);
|
|
286
314
|
io.out(`${lotKey(l.project, l.lot)} — ${l.title}`);
|
|
287
|
-
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)})`);
|
|
288
316
|
io.out(` file ${basename(l.repo)} · ${slot < 2 ? `créneau ${slot + 1}` : 'en attente d\'un créneau'}`);
|
|
289
317
|
if (l.repos?.length)
|
|
290
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})`);
|
|
@@ -294,11 +322,14 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
294
322
|
io.out(` node : ${l.node.version} (.nvmrc ${l.node.wanted}${skipped})`);
|
|
295
323
|
}
|
|
296
324
|
const steps = [{ kind: 'implement', model: l.model }];
|
|
297
|
-
|
|
325
|
+
const specStage = !!(l.screen || l.specLot);
|
|
326
|
+
if (specStage && env.config.accept)
|
|
298
327
|
steps.unshift({ kind: 'accept', model: 'sonnet' });
|
|
299
328
|
if (l.screen && env.config.maquette)
|
|
300
329
|
steps.unshift({ kind: 'maquette', model: env.config.review.full });
|
|
301
|
-
if (l.
|
|
330
|
+
if (l.specLot && env.config.fiche)
|
|
331
|
+
steps.unshift({ kind: 'fiche', model: 'sonnet' });
|
|
332
|
+
if (specStage)
|
|
302
333
|
steps.unshift({ kind: 'spec', model: 'sonnet' });
|
|
303
334
|
if (needsPrecheck(env.config, env.loadPlan(), l.repo, l.lot, l.repos))
|
|
304
335
|
steps.unshift({ kind: 'precheck', model: env.config.precheckLocal ? 'local' : 'sonnet' });
|
|
@@ -316,24 +347,26 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
316
347
|
const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id, lotSlug(l.project, l.lot), 'playwright');
|
|
317
348
|
// brief écrit par --dry-run = base d'une délégation à la main : aucun dossier de vague (il n'existe pas)
|
|
318
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: '' };
|
|
319
|
-
if (
|
|
320
|
-
vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen });
|
|
350
|
+
if (specStage)
|
|
351
|
+
vars.spec = specText({ dir: env.config.spec.dir, render: env.config.spec.render, screen: l.screen, back: !!l.specLot });
|
|
321
352
|
if (l.screen && env.config.maquette)
|
|
322
353
|
vars.maquette = maquetteText({ fiche: `${env.config.spec.dir}/<écran>/fiche.yaml`, passes: 0 });
|
|
323
|
-
if (l.
|
|
324
|
-
|
|
325
|
-
|
|
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 });
|
|
326
359
|
}
|
|
327
360
|
for (const s of steps) {
|
|
328
361
|
const file = join(tmp, `${lotSlug(l.project, l.lot)}--${s.kind}.md`);
|
|
329
|
-
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
|
|
330
363
|
writeFileSync(file, brief);
|
|
331
|
-
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small).playwright;
|
|
364
|
+
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen).playwright;
|
|
332
365
|
const local = s.model === 'local';
|
|
333
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));
|
|
334
367
|
io.out(` ${s.kind} : ${local ? 'claude-local' : 'claude'} ${args.join(' ')}`);
|
|
335
368
|
io.out(` brief : ${file}`);
|
|
336
|
-
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));
|
|
337
370
|
io.out(` mcp : ${servers.length ? `${servers.join(', ')} (captures dans le dossier de la vague : <vague>/${lotSlug(l.project, l.lot)}/playwright)` : 'aucun'}`);
|
|
338
371
|
}
|
|
339
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
|
}
|