@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
|
@@ -17,10 +17,11 @@ import { capturesText, newsText, objective, renderBrief, reposText, TEMPLATES_DI
|
|
|
17
17
|
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig } from './launch.js';
|
|
18
18
|
import { cleanPlaywrightOutput, pushed, snapshot } from './guard.js';
|
|
19
19
|
import { ACCEPT_SCHEMA, checkShape, PRECHECK_SCHEMA, REVIEW_SCHEMA, schemaFor, SPEC_SCHEMA, WORK_SCHEMA } from './schemas.js';
|
|
20
|
-
import { acceptationFile, acceptText, checkAcceptation, findAcceptation, isAcceptationFile, lockViolations, lotStartHead, parseAcceptation, redSha, sealAcceptation, setVert } from './accept.js';
|
|
21
|
-
import { avalFicheText, ficheRules } from './aval.js';
|
|
20
|
+
import { acceptationFile, acceptCommands, isAcceptList, acceptText, checkAcceptation, findAcceptation, isAcceptationFile, lockViolations, lotStartHead, parseAcceptation, planAcceptRuns, redSha, resealedRouge, sealAcceptation, setVert } from './accept.js';
|
|
21
|
+
import { avalFicheText, ficheContracts, ficheRules, hasScreenPart } from './aval.js';
|
|
22
|
+
import { ficheReviewText, reviewedSheetText } from './fiche.js';
|
|
22
23
|
import { bulletLines, maquetteText, reviewedText } from './maquette.js';
|
|
23
|
-
import { answeredText, checkSpec, costlyHypotheses, ficheReady, findFiche, specText } from './spec.js';
|
|
24
|
+
import { answeredText, checkSpec, costlyHypotheses, ficheHasBack, ficheReady, findFiche, hypothesisLines, hypothesisNote, specText } from './spec.js';
|
|
24
25
|
import { lotKey, lotRepoPaths, neighbourDirs } from './state.js';
|
|
25
26
|
/** Passes de correction au plus, puis la main est rendue au lead. */
|
|
26
27
|
export const MAX_PASSES = 2;
|
|
@@ -217,7 +218,9 @@ function docsText(c) {
|
|
|
217
218
|
return docSyncBrief(sync, docSyncGaps(sync, filesOf(c.lot.repo, lotWork(plan, c.lot.repo, c.lot.lot))));
|
|
218
219
|
}
|
|
219
220
|
/** Gabarit d'une étape : la passe des mineurs et la revue courte qui la suit ont chacune le leur. */
|
|
220
|
-
function briefName(l, kind) {
|
|
221
|
+
export function briefName(l, kind) {
|
|
222
|
+
if (l.specLot && !l.screen && (kind === 'spec' || kind === 'accept'))
|
|
223
|
+
return `${kind}-back`;
|
|
221
224
|
if (kind === 'fix' && l.minorFix)
|
|
222
225
|
return 'fix-minors';
|
|
223
226
|
if (kind === 'review-small' && l.minorPass && !(l.small && l.visible))
|
|
@@ -233,6 +236,22 @@ function ficheTemplate() {
|
|
|
233
236
|
return undefined;
|
|
234
237
|
}
|
|
235
238
|
}
|
|
239
|
+
/** Gabarit de la partie back de la fiche (L187), joint au brief de la spec d'un lot `spec: true`. */
|
|
240
|
+
function ficheBackTemplate() {
|
|
241
|
+
try {
|
|
242
|
+
return readFileSync(join(TEMPLATES_DIR, '..', 'fiche-back.yaml'), 'utf8');
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Le lot passe par l'étape spec : il déclare `screen:` (écran) ou `spec: true` (comportement sans écran, L188). */
|
|
249
|
+
const hasSpecStage = (l) => !!(l.screen || l.specLot);
|
|
250
|
+
/**
|
|
251
|
+
* Les parties que la fiche du lot doit avoir : l'écran d'un lot à `screen:`, le sujet d'un lot à `spec: true` (sans écran). Un lot mixte est un lot
|
|
252
|
+
* `screen:` dont la FICHE a une partie back (bloc `sujet`, décision du lead) : la fiche écrite le dit, pas un champ du plan.
|
|
253
|
+
*/
|
|
254
|
+
const partsOf = (l, fiche) => ({ screen: !!l.screen, back: !!l.specLot || (!!l.screen && !!fiche && ficheHasBack(l.repo, fiche)) });
|
|
236
255
|
/** Gabarit du lanceur de tests livré avec le paquet, joint au brief de l'étape accept. */
|
|
237
256
|
function acceptRunner() {
|
|
238
257
|
try {
|
|
@@ -258,19 +277,24 @@ function avalOf(c, kind) {
|
|
|
258
277
|
if (!step || !fiche)
|
|
259
278
|
return '';
|
|
260
279
|
let rules;
|
|
280
|
+
let contracts;
|
|
281
|
+
let screen;
|
|
261
282
|
try {
|
|
262
|
-
|
|
283
|
+
const doc = parse(readFileSync(join(l.repo, fiche), 'utf8'));
|
|
284
|
+
rules = ficheRules(doc);
|
|
285
|
+
contracts = ficheContracts(doc);
|
|
286
|
+
screen = hasScreenPart(doc);
|
|
263
287
|
}
|
|
264
288
|
catch {
|
|
265
289
|
return '';
|
|
266
290
|
}
|
|
267
|
-
if (!rules.length)
|
|
291
|
+
if (!rules.length || (step === 'ux' && !screen))
|
|
268
292
|
return '';
|
|
269
293
|
const acceptation = l.accept?.acceptation ?? findAcceptation(l.repo, c.config.spec.dir, l.lot) ?? undefined;
|
|
270
|
-
const o = { fiche, rules, acceptation, rouge: l.accept?.rouge };
|
|
294
|
+
const o = { fiche, rules, acceptation, rouge: l.accept?.rouge, contracts, screen };
|
|
271
295
|
const text = avalFicheText(step, o);
|
|
272
296
|
// la revue courte d'un petit lot visible est sa seule passe : elle porte aussi la conformité à la maquette
|
|
273
|
-
return kind === 'review-small' && l.small && l.visible ? `${text}\n${avalFicheText('ux', o)}` : text;
|
|
297
|
+
return kind === 'review-small' && l.small && l.visible && screen ? `${text}\n${avalFicheText('ux', o)}` : text;
|
|
274
298
|
}
|
|
275
299
|
function briefFor(c, kind) {
|
|
276
300
|
const plan = c.loadPlan();
|
|
@@ -282,15 +306,31 @@ function briefFor(c, kind) {
|
|
|
282
306
|
vars.news = newsText(l.lot, pwDir);
|
|
283
307
|
vars.captures = capturesText(pwDir);
|
|
284
308
|
}
|
|
285
|
-
vars.fiche = l
|
|
309
|
+
vars.fiche = hasSpecStage(l) ? avalOf(c, kind) : '';
|
|
286
310
|
if (kind === 'spec') {
|
|
287
|
-
|
|
311
|
+
const reviewed = [...(l.ficheConstats ?? []), ...(l.maquetteConstats ?? [])];
|
|
312
|
+
const written = ficheOf(c);
|
|
313
|
+
const back = partsOf(l, written).back;
|
|
314
|
+
vars.spec = specText({
|
|
315
|
+
dir: c.config.spec.dir,
|
|
316
|
+
render: c.config.spec.render,
|
|
317
|
+
screen: l.screen,
|
|
318
|
+
back,
|
|
319
|
+
problems: l.specProblems,
|
|
320
|
+
review: !reviewed.length ? undefined : back ? reviewedSheetText(reviewed, !!l.screen) : reviewedText(reviewed),
|
|
321
|
+
template: l.screen ? ficheTemplate() : undefined,
|
|
322
|
+
templateBack: back || l.screen ? ficheBackTemplate() : undefined,
|
|
323
|
+
});
|
|
288
324
|
const mine = lotCommitLines(c);
|
|
289
325
|
if (mine)
|
|
290
326
|
vars.commits = `Commits of the lot already present (do not redo them; fix or complete the spec from them):\n${mine}`;
|
|
291
327
|
if (l.pendingAnswer)
|
|
292
328
|
vars.reponse = l.specAsked?.length ? answeredText(l.specAsked, l.pendingAnswer) : `Answer from the human to your earlier question: ${l.pendingAnswer}`;
|
|
293
329
|
}
|
|
330
|
+
if (kind === 'fiche') {
|
|
331
|
+
vars.fiche = ficheReviewText({ fiche: ficheOf(c) ?? '', passes: l.fiche?.passes ?? 0, previous: l.ficheConstats });
|
|
332
|
+
vars.reponse = '';
|
|
333
|
+
}
|
|
294
334
|
if (kind === 'maquette') {
|
|
295
335
|
vars.maquette = maquetteText({ fiche: ficheOf(c) ?? '', passes: l.maquette?.passes ?? 0, previous: l.maquetteConstats });
|
|
296
336
|
vars.reponse = '';
|
|
@@ -298,7 +338,7 @@ function briefFor(c, kind) {
|
|
|
298
338
|
if (kind === 'accept') {
|
|
299
339
|
const fiche = ficheOf(c) ?? '';
|
|
300
340
|
const folder = posix.dirname(fiche);
|
|
301
|
-
vars.accept = acceptText({ fiche, maquette: `${folder}/maquette.html`, acceptation: acceptationFile(fiche, l.lot), command: c.config.accept ?? '', test: c.config.test, runner: acceptRunner(), problems: l.acceptProblems });
|
|
341
|
+
vars.accept = acceptText({ fiche, maquette: `${folder}/maquette.html`, screen: !!l.screen, back: !!l.screen && partsOf(l, fiche).back, acceptation: acceptationFile(fiche, l.lot), command: c.config.accept ?? '', test: c.config.test, runner: acceptRunner(), problems: l.acceptProblems });
|
|
302
342
|
vars.objectif = ''; // l'oracle est écrit depuis la fiche et la maquette seules, pas depuis le texte du lot
|
|
303
343
|
vars.commits = '';
|
|
304
344
|
if (l.pendingAnswer)
|
|
@@ -385,7 +425,7 @@ async function session(c, kind, local = false) {
|
|
|
385
425
|
// Le budget du lot borne l'écriture, jamais la revue (L145) : une revue est jouée dès que le code est à relire (sauf tests rouges après une écriture, cf. REVIEW_RESERVE), la passe fix réserve son coût.
|
|
386
426
|
if (write && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
|
|
387
427
|
return overBudget(c, kind === 'fix' && !lotOver(l));
|
|
388
|
-
const model = kind === 'spec' || kind === 'accept' ? 'sonnet' : write ? l.model : local ? 'local' : kind === 'precheck' ? 'sonnet' : reviewModel(c, kind); // le contrôle préalable ne fait que lire : pas d'Opus
|
|
428
|
+
const model = kind === 'spec' || kind === 'accept' || kind === 'fiche' ? 'sonnet' : write ? l.model : local ? 'local' : kind === 'precheck' ? 'sonnet' : reviewModel(c, kind); // le contrôle préalable ne fait que lire : pas d'Opus
|
|
389
429
|
const effort = c.config.effort[kind === 'review-small' ? 'review' : kind];
|
|
390
430
|
const before = await snapshot(l.repo);
|
|
391
431
|
const neighbours = l.repos ?? [];
|
|
@@ -394,7 +434,7 @@ async function session(c, kind, local = false) {
|
|
|
394
434
|
const sessionId = randomUUID();
|
|
395
435
|
const step = { n, kind, model, ...(effort !== 'default' ? { effort } : {}), status: 'running', sessionId, started: new Date().toISOString(), headBefore: before.head ?? undefined };
|
|
396
436
|
l.steps.push(step);
|
|
397
|
-
const label = { implement: 'implementing', fix: 'fixing', review: 'reviewing', ux: 'reviewing', 'review-small': 'reviewing', precheck: 'implementing', spec: 'implementing', accept: 'implementing', maquette: 'reviewing' };
|
|
437
|
+
const label = { implement: 'implementing', fix: 'fixing', review: 'reviewing', ux: 'reviewing', 'review-small': 'reviewing', precheck: 'implementing', spec: 'implementing', accept: 'implementing', maquette: 'reviewing', fiche: 'reviewing' };
|
|
398
438
|
transition(c, label[kind]);
|
|
399
439
|
w.log(`${lotKey(l.project, l.lot)} · session ${n} ${kind} (${model})`);
|
|
400
440
|
let brief;
|
|
@@ -406,8 +446,8 @@ async function session(c, kind, local = false) {
|
|
|
406
446
|
step.cause = e.message;
|
|
407
447
|
return stop(c, 'failed', `brief de ${kind} : ${e.message}`);
|
|
408
448
|
}
|
|
409
|
-
const mcpConfig = writeMcpConfig(w.store.lotDir(l.project, l.lot), kind, l.visible, l.small);
|
|
410
|
-
const playwright = !!mcpServersFor(kind, l.visible, '', l.small).playwright;
|
|
449
|
+
const mcpConfig = writeMcpConfig(w.store.lotDir(l.project, l.lot), kind, l.visible, l.small, !!l.screen);
|
|
450
|
+
const playwright = !!mcpServersFor(kind, l.visible, '', l.small, !!l.screen).playwright;
|
|
411
451
|
if (playwright)
|
|
412
452
|
l.uxCaptures = join(dirname(mcpConfig), 'playwright');
|
|
413
453
|
l.pendingAnswer = null; // la réponse est dans le brief : elle ne repart pas avec la session suivante
|
|
@@ -635,7 +675,8 @@ function overBudget(c, reserve = false) {
|
|
|
635
675
|
const l = c.lot;
|
|
636
676
|
// Une réponse du lead encore en attente n'a pas été consommée : elle ne doit pas se perdre sans qu'on le dise.
|
|
637
677
|
const answer = l.pendingAnswer ? ` ; la réponse du lead (« ${l.pendingAnswer} ») n'a pas été jouée` : '';
|
|
638
|
-
const
|
|
678
|
+
const basis = hasSpecStage(l) ? 'estimate et étape spec' : "dérivé de l'estimate";
|
|
679
|
+
const why = reserve ? `budget du lot : ${lotSpent(l)} / ${l.budget} tokens comptés (${basis}), il ne reste pas de quoi payer la revue qui suivrait la correction (${REVIEW_RESERVE} réservés)` : `budget du lot atteint (${lotSpent(l)} / ${l.budget} tokens comptés, ${basis})`;
|
|
639
680
|
// Tests rouges après une écriture : la passe fix refusée aurait suivi, le lot revient sans revue (L145) — la cause le dit.
|
|
640
681
|
const red = l.next === 'fix' && l.constats.some((k) => k.source === 'tests');
|
|
641
682
|
// Passe d'écriture qui vient d'être jouée, nommée d'après le compteur : l.pass - 1 passes de correction l'ont précédée (minorPass reste vrai après la passe des mineurs).
|
|
@@ -746,13 +787,13 @@ async function precheck(c) {
|
|
|
746
787
|
l.next = firstWrite(c);
|
|
747
788
|
save(c);
|
|
748
789
|
}
|
|
749
|
-
/** Première session d'écriture d'un lot : la spec
|
|
790
|
+
/** Première session d'écriture d'un lot : la spec quand il déclare `screen:` ou `spec: true` et qu'aucune fiche n'est écrite, sinon l'implémentation. */
|
|
750
791
|
function firstWrite(c) {
|
|
751
792
|
const l = c.lot;
|
|
752
|
-
if (!l
|
|
793
|
+
if (!hasSpecStage(l) || l.spec)
|
|
753
794
|
return 'implement';
|
|
754
795
|
const fiche = findFiche(l.repo, c.config.spec.dir, l.lot);
|
|
755
|
-
return fiche && ficheReady(l.repo, fiche, l.lot) ? afterSpec(c, fiche) : 'spec';
|
|
796
|
+
return fiche && ficheReady(l.repo, fiche, l.lot, partsOf(l, fiche)) ? afterSpec(c, fiche) : 'spec';
|
|
756
797
|
}
|
|
757
798
|
const NO_TEST_ACCEPT = 'étape accept : orchestrate.test absent, rien ne prouve que les tests rouges sont exclus du lanceur de tests et de la CI (vérifier à la main la config du lanceur avant de livrer)';
|
|
758
799
|
const NO_ACCEPT = 'étape accept ignorée : orchestrate.accept absent (commande dédiée aux tests d\'acceptation), le lot passe de spec à implement sans tests rouges';
|
|
@@ -762,13 +803,24 @@ const NO_ACCEPT = 'étape accept ignorée : orchestrate.accept absent (commande
|
|
|
762
803
|
*/
|
|
763
804
|
function afterSpec(c, fiche) {
|
|
764
805
|
const l = c.lot;
|
|
765
|
-
|
|
806
|
+
// Revue de la fiche back (L188) d'abord, puis celle de la maquette d'un lot mixte ; la même preuve (acceptation scellée, code déjà commité) dispense des deux.
|
|
807
|
+
if (c.config.fiche && partsOf(l, fiche).back && !l.fiche?.conforme && !acceptationProof(c, fiche) && !codeAfterFiche(c, fiche))
|
|
808
|
+
return 'fiche';
|
|
809
|
+
return afterFiche(c, fiche);
|
|
810
|
+
}
|
|
811
|
+
/** Étape qui suit une fiche relue : la revue UX de la maquette d'un lot à écran (S4), sinon `afterMaquette`. */
|
|
812
|
+
function afterFiche(c, fiche) {
|
|
813
|
+
const l = c.lot;
|
|
814
|
+
if (l.screen && c.config.maquette && !l.maquette?.conforme && !acceptationProof(c, fiche) && !codeAfterFiche(c, fiche))
|
|
766
815
|
return 'maquette';
|
|
767
816
|
return afterMaquette(c, fiche);
|
|
768
817
|
}
|
|
769
|
-
/** Étape qui suit une maquette relue : les tests d'acceptation rouges (S5) quand le projet déclare sa commande dédiée et qu'aucune acceptation conforme n'est déjà commitée, sinon l'implémentation. */
|
|
818
|
+
/** Étape qui suit une maquette relue (ou une fiche, pour un lot sans écran) : les tests d'acceptation rouges (S5) quand le projet déclare sa commande dédiée et qu'aucune acceptation conforme n'est déjà commitée, sinon l'implémentation. */
|
|
770
819
|
function afterMaquette(c, fiche) {
|
|
771
820
|
const l = c.lot;
|
|
821
|
+
// Des constats de la revue de fiche attendent la revue de la maquette (lot mixte) : les deux partent ensemble à la seconde passe `spec`.
|
|
822
|
+
if (l.ficheConstats?.length)
|
|
823
|
+
return 'spec';
|
|
772
824
|
if (!c.config.accept) {
|
|
773
825
|
if (!l.warnings.includes(NO_ACCEPT))
|
|
774
826
|
l.warnings.push(NO_ACCEPT);
|
|
@@ -868,6 +920,7 @@ async function specStep(c) {
|
|
|
868
920
|
const done = await session(c, 'spec');
|
|
869
921
|
if (!done)
|
|
870
922
|
return;
|
|
923
|
+
l.specRuns = (l.specRuns ?? 0) + 1;
|
|
871
924
|
let rep;
|
|
872
925
|
try {
|
|
873
926
|
rep = checkShape(done.report, SPEC_SCHEMA);
|
|
@@ -879,6 +932,7 @@ async function specStep(c) {
|
|
|
879
932
|
const questions = takeReport(c, done, rep);
|
|
880
933
|
if (questions.length) {
|
|
881
934
|
l.questions = questions;
|
|
935
|
+
l.specRuns = undefined; // la réponse ouvre un nouveau budget de deux sessions
|
|
882
936
|
l.specAsked = undefined; // la prochaine réponse porte sur la question de la session, pas sur d'anciennes hypothèses coûteuses
|
|
883
937
|
l.next = 'spec';
|
|
884
938
|
transition(c, 'question', questions[0]);
|
|
@@ -893,7 +947,7 @@ async function specStep(c) {
|
|
|
893
947
|
if (reason) {
|
|
894
948
|
l.spec = { sousSeuil: reason };
|
|
895
949
|
l.specProblems = undefined;
|
|
896
|
-
l.warnings.push(`étape spec : sous le seuil, aucune maquette — ${reason}`);
|
|
950
|
+
l.warnings.push(`étape spec : sous le seuil, ${l.screen ? 'aucune maquette' : 'aucune fiche'} — ${reason}`);
|
|
897
951
|
l.next = 'implement';
|
|
898
952
|
save(c);
|
|
899
953
|
return;
|
|
@@ -903,10 +957,11 @@ async function specStep(c) {
|
|
|
903
957
|
const asked = reported ? relative(l.repo, resolve(l.repo, reported)) : '';
|
|
904
958
|
const outside = asked === '..' || asked.startsWith('../') || isAbsolute(asked);
|
|
905
959
|
const fiche = outside ? '' : asked || findFiche(l.repo, c.config.spec.dir, l.lot) || '';
|
|
960
|
+
const what = l.screen ? '<écran>' : '<sujet>';
|
|
906
961
|
const inDir = fiche === '' || fiche.startsWith(`${c.config.spec.dir.replace(/\/+$/, '')}/`);
|
|
907
|
-
const problems = !outside && fiche && !inDir ? [`${fiche} : fiche hors de ${c.config.spec.dir} (la spec vit dans ${c.config.spec.dir}
|
|
962
|
+
const problems = !outside && fiche && !inDir ? [`${fiche} : fiche hors de ${c.config.spec.dir} (la spec vit dans ${c.config.spec.dir}/${what}/fiche.yaml, où l'étape la retrouve)`] : outside ? [`${reported} : fiche hors du dépôt (le chemin de « fiche » est relatif au dépôt : ${c.config.spec.dir}/${what}/fiche.yaml)`] : fiche ? checkSpec(l.repo, fiche, l.lot, partsOf(l, fiche)) : [`aucune fiche écrite : ${c.config.spec.dir}/${what}/fiche.yaml est attendue (ou une raison dans « sousSeuil » si le lot ne change ${l.screen ? 'aucun écran' : 'aucun comportement'})`];
|
|
908
963
|
if (problems.length) {
|
|
909
|
-
if (l.
|
|
964
|
+
if ((l.specRuns ?? 0) >= 2) {
|
|
910
965
|
stop(c, 'handed-back', `spec non conforme après 2 passes : ${problems.join(' ; ')}`);
|
|
911
966
|
return;
|
|
912
967
|
}
|
|
@@ -922,6 +977,7 @@ async function specStep(c) {
|
|
|
922
977
|
if (costly.length) {
|
|
923
978
|
l.questions = costly.map((x) => `${x.id} est posée comme hypothèse mais touche un choix coûteux à défaire (${x.categorie}) : « ${x.siFaux} » — la confirmer, ou la corriger, avant que la fiche serve de référence.`);
|
|
924
979
|
l.specAsked = [...l.questions];
|
|
980
|
+
l.specRuns = undefined; // la réponse du lead ouvre un nouveau budget de deux sessions, comme celle d'une question de la session
|
|
925
981
|
l.next = 'spec';
|
|
926
982
|
transition(c, 'question', l.questions[0]);
|
|
927
983
|
return;
|
|
@@ -929,24 +985,113 @@ async function specStep(c) {
|
|
|
929
985
|
l.specAsked = undefined;
|
|
930
986
|
l.spec = { fiche, hypotheses: (doc.regles ?? []).filter((r) => r.statut === 'hypothese').length };
|
|
931
987
|
l.specProblems = undefined;
|
|
988
|
+
l.hypotheses = undefined; // la fiche vient d'être (ré)écrite : ses hypothèses se posent une fois la fiche relue, pas avant (settleHypotheses)
|
|
989
|
+
// Les constats de la revue de fiche ont servi à cette passe : sans nouvelle revue de fiche derrière (partie back retirée, preuve d'acceptation), ils ne renvoient pas encore à la spec.
|
|
990
|
+
const kept = l.ficheConstats;
|
|
991
|
+
l.ficheConstats = undefined;
|
|
932
992
|
l.next = afterSpec(c, fiche);
|
|
993
|
+
if (l.next === 'fiche')
|
|
994
|
+
l.ficheConstats = kept; // la seconde revue de fiche les relit
|
|
933
995
|
save(c);
|
|
934
996
|
}
|
|
935
997
|
/**
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
998
|
+
* Un lot sans écran n'a pas de Nouveauté : les hypothèses de sa fiche vont en notes du lot (une par règle) et au tableau de fin de vague (spec
|
|
999
|
+
* etape-spec-back § 7). Elles se lisent sur la fiche FINALE, juste avant `accept` ou `implement` : après la revue de la fiche et sa seconde passe
|
|
1000
|
+
* (une hypothèse changée ou retirée en passe 2 ne laisse pas la note de la passe 1), et aussi pour une fiche déjà présente à la reprise.
|
|
1001
|
+
* Rend vrai quand le lot est rendu.
|
|
939
1002
|
*/
|
|
940
|
-
async function
|
|
1003
|
+
async function settleHypotheses(c) {
|
|
1004
|
+
const l = c.lot;
|
|
1005
|
+
if (l.screen || !l.specLot || l.hypotheses !== undefined || (l.spec && !('fiche' in l.spec)))
|
|
1006
|
+
return false;
|
|
1007
|
+
const fiche = ficheOf(c);
|
|
1008
|
+
if (!fiche)
|
|
1009
|
+
return false;
|
|
1010
|
+
let doc;
|
|
1011
|
+
try {
|
|
1012
|
+
doc = parse(readFileSync(join(l.repo, fiche), 'utf8'));
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
return noteHypotheses(c, hypothesisLines(doc), typeof doc.sujet?.id === 'string' ? doc.sujet.id : posix.basename(posix.dirname(fiche)));
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Pose en note du lot une ligne par hypothèse de la fiche (rien de plus que ce que la note dit déjà : une reprise ne duplique pas) et la commite avec le
|
|
1021
|
+
* plan. Un plan tenu par un autre outil (lecture seule) ne reçoit rien : la ligne devient une proposition rendue au lead. Rend vrai quand le lot est rendu.
|
|
1022
|
+
*/
|
|
1023
|
+
async function noteHypotheses(c, lines, sujet) {
|
|
1024
|
+
const l = c.lot;
|
|
1025
|
+
l.hypotheses = lines;
|
|
1026
|
+
if (!lines.length)
|
|
1027
|
+
return false;
|
|
1028
|
+
const plan = c.loadPlan();
|
|
1029
|
+
const texts = lines.map((x) => hypothesisNote(sujet, x));
|
|
1030
|
+
if (plan.readonly) {
|
|
1031
|
+
for (const t of texts)
|
|
1032
|
+
propose(c, `[note à poser] ${t}`);
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
const have = new Set(plan.lot(l.lot).notes.map((n) => n.text));
|
|
1036
|
+
const fresh = texts.filter((t) => !have.has(t));
|
|
1037
|
+
if (!fresh.length)
|
|
1038
|
+
return false;
|
|
1039
|
+
for (const t of fresh)
|
|
1040
|
+
plan.note(l.lot, t, c.wave.today);
|
|
1041
|
+
plan.save();
|
|
1042
|
+
const dirty = await commitPlan(c, `plan: ${l.lot} hypothèses de la fiche ${sujet} notées (orchestrate ${c.wave.id})`);
|
|
1043
|
+
if (dirty) {
|
|
1044
|
+
stop(c, 'handed-back', `notes des hypothèses de la fiche : ${dirty}`);
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Les mineurs de la revue de la fiche vont en notes du lot (spec etape-spec-back § 4), pas au tableau de fin de vague : une note par constat, sans doublon
|
|
1051
|
+
* à la reprise, commitée avec le plan. Un plan en lecture seule ne reçoit rien : la ligne devient une proposition `[note à poser]` rendue au lead.
|
|
1052
|
+
* Rend vrai quand le lot est rendu (arbre sale).
|
|
1053
|
+
*/
|
|
1054
|
+
async function noteMinors(c, rep, label) {
|
|
1055
|
+
const texts = rep.constats.filter((k) => k.gravite === 'mineur').map((k) => `Revue de ${label} — mineur : ${k.fichier ? `${k.fichier}${k.ligne ? `:${k.ligne}` : ''} — ` : ''}${k.texte}`);
|
|
1056
|
+
if (!texts.length)
|
|
1057
|
+
return false;
|
|
1058
|
+
const plan = c.loadPlan();
|
|
1059
|
+
if (plan.readonly) {
|
|
1060
|
+
for (const t of texts)
|
|
1061
|
+
propose(c, `[note à poser] ${t}`);
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
const have = new Set(plan.lot(c.lot.lot).notes.map((n) => n.text));
|
|
1065
|
+
const fresh = [...new Set(texts)].filter((t) => !have.has(t));
|
|
1066
|
+
if (!fresh.length)
|
|
1067
|
+
return false;
|
|
1068
|
+
for (const t of fresh)
|
|
1069
|
+
plan.note(c.lot.lot, t, c.wave.today);
|
|
1070
|
+
plan.save();
|
|
1071
|
+
const dirty = await commitPlan(c, `plan: ${c.lot.lot} mineurs de la revue de ${label} notés (orchestrate ${c.wave.id})`);
|
|
1072
|
+
if (dirty) {
|
|
1073
|
+
stop(c, 'handed-back', `notes des mineurs de la revue de ${label} : ${dirty}`);
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
const FICHE_REVIEW = { kind: 'fiche', label: 'fiche', source: 'code', after: afterFiche, retry: afterFiche };
|
|
1079
|
+
const MAQUETTE_REVIEW = { kind: 'maquette', label: 'maquette', source: 'ux', after: afterMaquette, retry: () => 'spec' };
|
|
1080
|
+
/**
|
|
1081
|
+
* Revue de la spec (fiche ou maquette) : une session de lecture seule. Un bloquant ou un majeur renvoie à la spec (une seule seconde passe, avec
|
|
1082
|
+
* les constats ; pour un lot mixte, ceux des deux revues ensemble), puis la spec est relue ; ce qui reste après la seconde revue est rendu au lead.
|
|
1083
|
+
* Les mineurs de la fiche vont en notes du lot (ceux de la maquette restent des propositions). Ce verdict n'est pas `raf review` ni `raf ux` : la revue après code garde ce rôle.
|
|
1084
|
+
*/
|
|
1085
|
+
async function reviewSpecStep(c, r) {
|
|
941
1086
|
const l = c.lot;
|
|
942
1087
|
const fiche = ficheOf(c);
|
|
943
1088
|
if (!fiche) {
|
|
944
|
-
l.warnings.push(
|
|
1089
|
+
l.warnings.push(`revue de la ${r.label} ignorée : aucune fiche trouvée pour ce lot`);
|
|
945
1090
|
l.next = 'implement';
|
|
946
1091
|
save(c);
|
|
947
1092
|
return;
|
|
948
1093
|
}
|
|
949
|
-
const done = await session(c,
|
|
1094
|
+
const done = await session(c, r.kind);
|
|
950
1095
|
if (!done)
|
|
951
1096
|
return;
|
|
952
1097
|
let rep;
|
|
@@ -958,28 +1103,50 @@ async function maquetteStep(c) {
|
|
|
958
1103
|
return;
|
|
959
1104
|
}
|
|
960
1105
|
const summary = summarize(rep, done.after.head ?? '');
|
|
961
|
-
const passes = (l.
|
|
962
|
-
l.
|
|
963
|
-
|
|
964
|
-
|
|
1106
|
+
const passes = (l[r.kind]?.passes ?? 0) + 1;
|
|
1107
|
+
l[r.kind] = { passes, conforme: summary.conforme, verdict: summary.verdict };
|
|
1108
|
+
// Fiche : les mineurs sont des notes du lot (design § 4). Maquette : inchangé (propositions du tableau final).
|
|
1109
|
+
addProposals(c, rep, r.kind, r.kind !== 'fiche');
|
|
1110
|
+
if (r.kind === 'fiche' && (await noteMinors(c, rep, `la ${r.label}`)))
|
|
1111
|
+
return;
|
|
1112
|
+
const constats = blockingConstats(rep, r.source);
|
|
1113
|
+
const key = r.kind === 'fiche' ? 'ficheConstats' : 'maquetteConstats';
|
|
965
1114
|
if (summary.conforme) {
|
|
966
|
-
l
|
|
967
|
-
l.next =
|
|
1115
|
+
l[key] = undefined;
|
|
1116
|
+
l.next = r.after(c, fiche);
|
|
968
1117
|
save(c);
|
|
969
1118
|
return;
|
|
970
1119
|
}
|
|
971
1120
|
if (passes >= 2) {
|
|
972
|
-
stop(c, 'handed-back',
|
|
1121
|
+
stop(c, 'handed-back', `${r.label} non conforme après 2 revues : ${summary.verdict}\n${bulletLines(constats).join('\n')}`);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
// Le budget de deux sessions spec est épuisé (écarts du programme compris) : un renvoi à la spec serait une 3ᵉ session.
|
|
1125
|
+
if ((l.specRuns ?? 0) >= 2) {
|
|
1126
|
+
stop(c, 'handed-back', `${r.label} non conforme et la spec a déjà joué 2 sessions : ${summary.verdict}\n${bulletLines(constats).join('\n')}`);
|
|
973
1127
|
return;
|
|
974
1128
|
}
|
|
975
|
-
l
|
|
976
|
-
l.next =
|
|
1129
|
+
l[key] = constats.length ? constats : [{ source: r.source, gravite: 'majeur', texte: summary.verdict }];
|
|
1130
|
+
l.next = r.retry(c, fiche);
|
|
977
1131
|
save(c);
|
|
978
1132
|
}
|
|
979
|
-
|
|
980
|
-
|
|
1133
|
+
const ficheStep = (c) => reviewSpecStep(c, FICHE_REVIEW);
|
|
1134
|
+
const maquetteStep = (c) => reviewSpecStep(c, MAQUETTE_REVIEW);
|
|
1135
|
+
/**
|
|
1136
|
+
* Commandes dédiées aux tests d'acceptation du lot (une chaîne ou une liste, L189) : `{acceptation}` y est remplacé, `CADENCE_ACCEPTATION` (chemin absolu
|
|
1137
|
+
* dans les deux cas) et `CADENCE_ACCEPTATION_ROOT` (sa racine) posés. Une commande sans fichier listé sous sa racine n'est pas lancée ; les fichiers
|
|
1138
|
+
* listés sous aucune racine sont rendus (`orphans`) sans rien lancer.
|
|
1139
|
+
*/
|
|
1140
|
+
async function runAccept(c, json) {
|
|
981
1141
|
const abs = join(c.lot.repo, json);
|
|
982
|
-
|
|
1142
|
+
const doc = parseAcceptation(readFileSync(abs, 'utf8'));
|
|
1143
|
+
const plan = planAcceptRuns(acceptCommands(c.config.accept), typeof doc === 'string' ? [] : doc.fichiers, !isAcceptList(c.config.accept));
|
|
1144
|
+
if (plan.orphans.length)
|
|
1145
|
+
return { runs: [], orphans: plan.orphans };
|
|
1146
|
+
const runs = [];
|
|
1147
|
+
for (const r of plan.runs)
|
|
1148
|
+
runs.push({ command: r.command, ...(await sh(c, r.command.replaceAll('{acceptation}', abs), 30 * 60_000, { CADENCE_ACCEPTATION: abs, CADENCE_ACCEPTATION_ROOT: r.root || '.' })) });
|
|
1149
|
+
return { runs, orphans: [] };
|
|
983
1150
|
}
|
|
984
1151
|
const tail = (output, n = 40) => output.trim().split('\n').slice(-n).join('\n');
|
|
985
1152
|
/** Écarts des tests d'acceptation écrits par la session (vide = acceptés) : autres lots intacts, acceptation.json conforme, tests rouges, config.test toujours verte. */
|
|
@@ -990,8 +1157,11 @@ async function acceptProblems(c, fiche, json, headBefore) {
|
|
|
990
1157
|
if (out.length)
|
|
991
1158
|
return out;
|
|
992
1159
|
const red = await runAccept(c, json);
|
|
993
|
-
if (red.
|
|
994
|
-
out.push(`
|
|
1160
|
+
if (red.orphans.length)
|
|
1161
|
+
out.push(`fichiers listés dans ${json} sous la racine d'aucune commande de orchestrate.accept (le dossier de son « cd <dossier> && » initial, sinon le dépôt) : ${red.orphans.join(', ')} — aucun lanceur ne les prendrait`);
|
|
1162
|
+
for (const r of red.runs)
|
|
1163
|
+
if (r.code === 0)
|
|
1164
|
+
out.push(`la commande dédiée « ${r.command} » (orchestrate.accept) passe alors que le code n'existe pas : des tests verts avant le code ne prouvent rien — chaque test doit échouer parce que la fonctionnalité manque\n${tail(r.output, 15)}`);
|
|
995
1165
|
if (!c.config.test) {
|
|
996
1166
|
if (!l.warnings.includes(NO_TEST_ACCEPT))
|
|
997
1167
|
l.warnings.push(NO_TEST_ACCEPT);
|
|
@@ -1076,9 +1246,12 @@ async function acceptGreen(c) {
|
|
|
1076
1246
|
const a = parseAcceptation(readFileSync(join(l.repo, json), 'utf8'));
|
|
1077
1247
|
if (typeof a === 'string' || a.vert)
|
|
1078
1248
|
return null;
|
|
1079
|
-
const
|
|
1080
|
-
if (
|
|
1081
|
-
return { source: 'tests', gravite: 'bloquant', texte: `
|
|
1249
|
+
const { runs, orphans } = await runAccept(c, json);
|
|
1250
|
+
if (orphans.length)
|
|
1251
|
+
return { source: 'tests', gravite: 'bloquant', texte: `fichiers listés dans ${json} sous la racine d'aucune commande de orchestrate.accept : ${orphans.join(', ')}` };
|
|
1252
|
+
const failed = runs.filter((r) => r.code !== 0);
|
|
1253
|
+
if (failed.length)
|
|
1254
|
+
return { source: 'tests', gravite: 'bloquant', texte: `tests d'acceptation encore rouges (${failed.map((r) => `${r.command}, code ${r.code}`).join(' ; ')}) : à rendre verts sans modifier les fichiers listés dans ${json} :\n${failed.map((r) => (failed.length > 1 ? `$ ${r.command}\n` : '') + tail(r.output)).join('\n')}` };
|
|
1082
1255
|
setVert(l.repo, json);
|
|
1083
1256
|
const dirty = await commitPlan(c, `chore(${l.lot}): acceptation verte, vert: true posé par le cycle (orchestrate ${c.wave.id})`);
|
|
1084
1257
|
if (dirty)
|
|
@@ -1135,6 +1308,13 @@ async function work(c, kind) {
|
|
|
1135
1308
|
if (!(l.choix ??= []).includes(x))
|
|
1136
1309
|
l.choix.push(x);
|
|
1137
1310
|
save(c);
|
|
1311
|
+
// Verrou des tests d'acceptation (L173) : ceux de TOUS les lots du dépôt, pas seulement du lot courant ; une modification est une question au lead. Contrôlé à CHAQUE fin de session, une question comprise (L200) :
|
|
1312
|
+
// une session qui remplace un test scellé puis pose une question ne passe pas par la reprise du lead pour se faire adopter.
|
|
1313
|
+
const locked = done.step.headBefore ? lockViolations(l.repo, c.config.spec.dir, done.step.headBefore, { rouge: l.accept?.rouge ? { [l.accept.acceptation]: l.accept.rouge } : {}, lot: l.lot, lotBase: l.steps.find((s) => s.headBefore)?.headBefore, lotStart: lotStartHead(l.repo, lotWork(c.loadPlan(), l.repo, l.lot).map((k) => k.sha)), lotCommits: lotCommits(c.loadPlan(), l.repo, l.lot).map((k) => k.sha) }) : [];
|
|
1314
|
+
if (locked.length) {
|
|
1315
|
+
stop(c, 'handed-back', `verrou des tests d'acceptation : ${locked.join(' ; ')}`);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1138
1318
|
// La passe des mineurs ne bloque jamais le lot (revue conforme déjà acquise) : une question est rendue en proposition.
|
|
1139
1319
|
const questions = rep.questions.filter((q) => !isNonQuestion(q));
|
|
1140
1320
|
// Une entrée filtrée reste visible (avertissement du lot, donc tableau de fin de vague) ; l'entrée vide n'a rien à montrer.
|
|
@@ -1155,12 +1335,6 @@ async function work(c, kind) {
|
|
|
1155
1335
|
stop(c, 'handed-back', `dépôt sale après ${kind} : ${dirt.join(', ')}`);
|
|
1156
1336
|
return;
|
|
1157
1337
|
}
|
|
1158
|
-
// Verrou des tests d'acceptation (L173) : ceux de TOUS les lots du dépôt, pas seulement du lot courant ; une modification est une question au lead.
|
|
1159
|
-
const locked = done.step.headBefore ? lockViolations(l.repo, c.config.spec.dir, done.step.headBefore, { rouge: l.accept?.rouge ? { [l.accept.acceptation]: l.accept.rouge } : {}, lot: l.lot, lotBase: l.steps.find((s) => s.headBefore)?.headBefore, lotStart: lotStartHead(l.repo, lotWork(c.loadPlan(), l.repo, l.lot).map((k) => k.sha)), lotCommits: lotCommits(c.loadPlan(), l.repo, l.lot).map((k) => k.sha) }) : [];
|
|
1160
|
-
if (locked.length) {
|
|
1161
|
-
stop(c, 'handed-back', `verrou des tests d'acceptation : ${locked.join(' ; ')}`);
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
1338
|
// Reprise : une session précédente de l'étape (correction ordinaire ou passe des mineurs) a pu commiter avant d'être coupée ; la revue d'origine ne vaut alors plus.
|
|
1165
1339
|
// Référence : le HEAD de départ de la première session coupée de la série qui précède celle-ci (pas la revue, qui peut ne pas exister encore).
|
|
1166
1340
|
let resumedFrom;
|
|
@@ -1472,6 +1646,32 @@ export function handBackDroppedQuestions(ctxs) {
|
|
|
1472
1646
|
}
|
|
1473
1647
|
}
|
|
1474
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Prédicat « ce commit est l'œuvre d'une session de la vague » (L200) : les commits connus des étapes de TOUS les lots de la vague
|
|
1651
|
+
* (`steps[].commits`) et ceux de `headBefore..headAfter` de chaque session terminée. Un rescellé du lead se fait entre deux sessions, hors de ces plages.
|
|
1652
|
+
*/
|
|
1653
|
+
function waveCommits(c, repo) {
|
|
1654
|
+
const known = new Set();
|
|
1655
|
+
const prefixes = [];
|
|
1656
|
+
for (const l of [c.lot, ...c.wave.store.lots().filter((x) => lotKey(x.project, x.lot) !== lotKey(c.lot.project, c.lot.lot))]) {
|
|
1657
|
+
for (const s of l.steps) {
|
|
1658
|
+
for (const k of s.commits ?? [])
|
|
1659
|
+
if (/^[0-9a-f]{7,40}\b/i.test(k))
|
|
1660
|
+
prefixes.push(k.split(' ')[0].toLowerCase());
|
|
1661
|
+
if (!s.headBefore || !s.headAfter || l.repo !== repo)
|
|
1662
|
+
continue;
|
|
1663
|
+
try {
|
|
1664
|
+
for (const sha of git(repo, 'rev-list', `${s.headBefore}..${s.headAfter}`).split('\n'))
|
|
1665
|
+
if (sha)
|
|
1666
|
+
known.add(sha);
|
|
1667
|
+
}
|
|
1668
|
+
catch {
|
|
1669
|
+
// plage illisible (historique réécrit) : les commits connus de l'étape restent comptés
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return (sha) => known.has(sha) || prefixes.some((p) => sha.startsWith(p));
|
|
1674
|
+
}
|
|
1475
1675
|
/** Joue le cycle d'un lot jusqu'à son terme, ou jusqu'à l'arrêt (question, budget, quota). Ne lève jamais. */
|
|
1476
1676
|
export async function runLot(c) {
|
|
1477
1677
|
const l = c.lot;
|
|
@@ -1485,6 +1685,9 @@ export async function runLot(c) {
|
|
|
1485
1685
|
}
|
|
1486
1686
|
if (l.status === 'question' && !l.pendingAnswer)
|
|
1487
1687
|
return;
|
|
1688
|
+
// Un test rescellé par le lead pendant la question (L200) a déplacé le commit rouge : l'ancre suit, le verrou garde le reste.
|
|
1689
|
+
if (l.status === 'question' && l.accept?.rouge)
|
|
1690
|
+
l.accept.rouge = resealedRouge(l.repo, l.accept.acceptation, l.accept.rouge, waveCommits(c, l.repo)) ?? l.accept.rouge;
|
|
1488
1691
|
if (l.next === null && l.steps.length === 0) {
|
|
1489
1692
|
if (dropped(c)) {
|
|
1490
1693
|
stop(c, 'handed-back', DROPPED);
|
|
@@ -1512,10 +1715,14 @@ export async function runLot(c) {
|
|
|
1512
1715
|
}
|
|
1513
1716
|
while (l.next && !TERMINAL.has(l.status)) {
|
|
1514
1717
|
const kind = l.next;
|
|
1718
|
+
if ((kind === 'accept' || kind === 'implement') && (await settleHypotheses(c)))
|
|
1719
|
+
return;
|
|
1515
1720
|
if (kind === 'precheck')
|
|
1516
1721
|
await precheck(c);
|
|
1517
1722
|
else if (kind === 'spec')
|
|
1518
1723
|
await specStep(c);
|
|
1724
|
+
else if (kind === 'fiche')
|
|
1725
|
+
await ficheStep(c);
|
|
1519
1726
|
else if (kind === 'maquette')
|
|
1520
1727
|
await maquetteStep(c);
|
|
1521
1728
|
else if (kind === 'accept')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { bulletLines } from './maquette.js';
|
|
2
|
+
// Revue de la fiche back (L188, spec etape-spec-back B3) : ce que le programme dit à l'agent code-reviewer en mode « fiche », et à l'agent spec à la seconde passe.
|
|
3
|
+
/** Consigne de la revue de fiche dans le brief : la fiche, la passe en cours, les points de la première revue à la seconde. */
|
|
4
|
+
export function ficheReviewText(o) {
|
|
5
|
+
const second = o.passes >= 1;
|
|
6
|
+
const lines = [
|
|
7
|
+
`Sheet mode: review the sheet \`${o.fiche}\` (back part: \`sujet\`, \`cas\`, \`regles\`, \`contrats\`, \`donnees\`). It is a document, not code: read it, then open the code, the tests and the sources it cites.`,
|
|
8
|
+
second
|
|
9
|
+
? 'This is the second and last review: the spec writer fixed the points of the first one (below). Check them first, then review the whole sheet again.'
|
|
10
|
+
: 'This is the first review of this sheet.',
|
|
11
|
+
];
|
|
12
|
+
if (second && o.previous?.length)
|
|
13
|
+
lines.push('', 'Points of the first review that the spec writer had to fix:', ...bulletLines(o.previous));
|
|
14
|
+
return lines.join('\n');
|
|
15
|
+
}
|
|
16
|
+
/** Consigne de la seconde passe `spec` : les constats bloquants et majeurs des revues de la fiche (et de la maquette d'un lot mixte), ensemble. */
|
|
17
|
+
export function reviewedSheetText(constats, mockup) {
|
|
18
|
+
return [
|
|
19
|
+
`The review of your ${mockup ? 'sheet and mockup' : 'sheet'} found the blocking and major points below. Fix exactly these in the sheet${mockup ? ', the mockup and the captures' : ''} (one more pass only: it is reviewed again, and what remains goes back to the lead). Keep the sheet consistent: every rule sourced or a hypothesis with its \`si_faux\`, every rule attached to a case and tested, every contract with its errors:`,
|
|
20
|
+
...bulletLines(constats),
|
|
21
|
+
].join('\n');
|
|
22
|
+
}
|
|
@@ -8,12 +8,12 @@ import { sandboxEnv } from './sandbox.js';
|
|
|
8
8
|
import { killMarked, SESSION_MARK_VAR, TreeTracker } from '../proc.js';
|
|
9
9
|
import { isQuotaMessage, lacksStructuredOutput, parseSession, salvageUsage, sumTokens, tokensOf } from './result.js';
|
|
10
10
|
/**
|
|
11
|
-
* Serveurs MCP d'une étape : Playwright pour `ux` et `maquette` (revue des captures et du HTML local, L172) et, sur un lot `visible`, pour `spec` (rendu 1440/390 de la maquette, L171), `implement` et `fix` (fix-minors compris) ;
|
|
11
|
+
* Serveurs MCP d'une étape : Playwright pour `ux` et `maquette` (revue des captures et du HTML local, L172) et, sur un lot `visible`, pour `spec` quand il a un écran `screen:` (rendu 1440/390 de la maquette, L171), `implement` et `fix` (fix-minors compris) ;
|
|
12
12
|
* et `review-small` d'un petit lot visible (sa seule revue d'ergonomie) ; rien pour `review`, ni pour un lot sans écran.
|
|
13
13
|
* Sorties dans `outputDir`, hors du dépôt.
|
|
14
14
|
*/
|
|
15
|
-
export function mcpServersFor(kind, visible, outputDir, small = false) {
|
|
16
|
-
if (!(kind === 'ux' || kind === 'maquette' || (visible && (kind === 'spec' || kind === 'implement' || kind === 'fix' || (kind === 'review-small' && small)))))
|
|
15
|
+
export function mcpServersFor(kind, visible, outputDir, small = false, screen = true) {
|
|
16
|
+
if (!(kind === 'ux' || kind === 'maquette' || (visible && ((kind === 'spec' && screen) || kind === 'implement' || kind === 'fix' || (kind === 'review-small' && small)))))
|
|
17
17
|
return {};
|
|
18
18
|
// La maquette est un fichier local : sans cette option le serveur bloque toute URL file:// (« Access to "file:" protocol is blocked »).
|
|
19
19
|
const files = kind === 'maquette' || kind === 'spec' ? ['--allow-unrestricted-file-access'] : [];
|
|
@@ -24,18 +24,18 @@ export function playwrightDir(lotDir) {
|
|
|
24
24
|
return join(resolve(lotDir), 'playwright');
|
|
25
25
|
}
|
|
26
26
|
/** Écrit `<lotDir>/mcp-<étape>.json` (et, si Playwright est chargé, le dossier `playwright/`) ; rend le chemin absolu du fichier. */
|
|
27
|
-
export function writeMcpConfig(lotDir, kind, visible, small = false) {
|
|
27
|
+
export function writeMcpConfig(lotDir, kind, visible, small = false, screen = true) {
|
|
28
28
|
const dir = resolve(lotDir);
|
|
29
29
|
const out = playwrightDir(dir);
|
|
30
|
-
const servers = mcpServersFor(kind, visible, out, small);
|
|
30
|
+
const servers = mcpServersFor(kind, visible, out, small, screen);
|
|
31
31
|
if (servers.playwright)
|
|
32
32
|
mkdirSync(out, { recursive: true });
|
|
33
33
|
const file = join(dir, `mcp-${kind}.json`);
|
|
34
34
|
writeFileSync(file, `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`);
|
|
35
35
|
return file;
|
|
36
36
|
}
|
|
37
|
-
/** Outils interdits à toute session : pas de sous-agent, pas de push, de livraison ni de
|
|
38
|
-
export const DISALLOWED = ['Agent', 'Bash(git push:*)', 'Bash(cadence deliver:*)', 'Bash(raf done:*)', 'Bash(raf review:*)', 'Bash(raf ux:*)'];
|
|
37
|
+
/** Outils interdits à toute session : pas de sous-agent, pas de push, de livraison, de verdict ni de rescellé d'un test d'acceptation (le chemin du lead, L200). */
|
|
38
|
+
export const DISALLOWED = ['Agent', 'Bash(git push:*)', 'Bash(cadence deliver:*)', 'Bash(raf done:*)', 'Bash(raf review:*)', 'Bash(raf ux:*)', 'Bash(cadence spec reseal:*)', 'Bash(raf spec reseal:*)'];
|
|
39
39
|
/** Outils MCP du serveur Playwright, par le préfixe du serveur (sonde réelle du 07-10 : `mcp__playwright` expose tous les `browser_*`). */
|
|
40
40
|
export const PLAYWRIGHT_TOOLS = 'mcp__playwright';
|
|
41
41
|
/** Agents passés à `--agents` : l'agent d'une étape qui charge Playwright, s'il a une liste d'outils, y gagne les outils du serveur (sinon il ne peut pas l'appeler). */
|