@sylad/cadence 1.12.1 → 1.14.1

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.
Files changed (59) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +3 -0
  4. package/agents/reference-planner.md +26 -0
  5. package/bin/cadence.js +8 -1
  6. package/dist/cli.js +197 -22
  7. package/dist/config-set.js +252 -0
  8. package/dist/config.js +49 -9
  9. package/dist/corrections.js +2 -0
  10. package/dist/git.js +42 -1
  11. package/dist/lead.js +5 -1
  12. package/dist/orchestrate/accept.js +63 -1
  13. package/dist/orchestrate/baseline.js +39 -3
  14. package/dist/orchestrate/briefs.js +1 -1
  15. package/dist/orchestrate/changelog-merge.js +40 -1
  16. package/dist/orchestrate/command.js +120 -19
  17. package/dist/orchestrate/continue.js +5 -2
  18. package/dist/orchestrate/cycle.js +318 -74
  19. package/dist/orchestrate/decisions.js +33 -0
  20. package/dist/orchestrate/guard.js +16 -0
  21. package/dist/orchestrate/integrate.js +26 -3
  22. package/dist/orchestrate/launch.js +4 -1
  23. package/dist/orchestrate/state.js +1 -1
  24. package/dist/orchestrate/workers.js +71 -0
  25. package/dist/orchestrate/worktree.js +15 -4
  26. package/dist/perf.js +12 -2
  27. package/dist/plan-commit.js +88 -0
  28. package/dist/plan.js +41 -3
  29. package/dist/ref/analyse.js +34 -4
  30. package/dist/ref/command.js +2 -1
  31. package/dist/ref/coverage.js +136 -0
  32. package/dist/ref/diff.js +99 -15
  33. package/dist/release.js +249 -0
  34. package/dist/session.js +7 -1
  35. package/dist/stale-notes.js +22 -0
  36. package/dist/verify-gate.js +138 -0
  37. package/docs/deliver.md +37 -0
  38. package/docs/fonctionnement.md +3 -1
  39. package/docs/lead.md +18 -1
  40. package/docs/orchestrate.md +71 -13
  41. package/docs/overview.md +5 -2
  42. package/docs/perf.md +4 -1
  43. package/docs/raf.md +49 -1
  44. package/docs/ref.md +61 -3
  45. package/docs/releasing.md +43 -2
  46. package/docs/session.md +8 -0
  47. package/docs/skills.md +1 -0
  48. package/docs/spec.md +2 -0
  49. package/docs/stabilite.md +2 -0
  50. package/docs/whats-new.md +9 -0
  51. package/package.json +3 -3
  52. package/skills/lead/SKILL.md +6 -4
  53. package/skills/orchestrate/SKILL.md +1 -1
  54. package/skills/session-close/SKILL.md +2 -0
  55. package/skills/session-start/SKILL.md +2 -1
  56. package/templates/orchestrate/fix-docs.md +1 -0
  57. package/templates/orchestrate/fix-minors.md +1 -0
  58. package/templates/orchestrate/fix.md +2 -0
  59. package/templates/orchestrate/implement.md +4 -0
@@ -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.12.1",
11
+ "version": "1.14.1",
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.12.1",
4
+ "version": "1.14.1",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -7,6 +7,8 @@ cadence keeps both in plain files next to the code, and [Claude Code](https://cl
7
7
 
8
8
  ## Start
9
9
 
10
+ Needs Node 22.18 or later (see [docs/stabilite.md](docs/stabilite.md)).
11
+
10
12
  ```sh
11
13
  npm install -g @sylad/cadence
12
14
  raf init
@@ -36,6 +38,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
36
38
  - [docs/orchestrate.md](docs/orchestrate.md): waves of sessions and their budgets.
37
39
  - [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect.
38
40
  - [docs/spec.md](docs/spec.md): screen and back-end sheets.
41
+ - [docs/ref.md](docs/ref.md): reference documents (specs, norms, mock-ups) and the coverage of their elements by the lots.
39
42
  - [docs/lead.md](docs/lead.md): the lead workflow.
40
43
  - [docs/overview.md](docs/overview.md): the longer introduction and the map of every page.
41
44
  - [docs/fonctionnement.md](docs/fonctionnement.md): how it all works, in four figures.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: reference-planner
3
+ description: Reference document coverage planner. Reads an inventory (`inventaire.json` of `ref analyse --double` or `ref diff --out`, or `lectures/a.json`, the reading `ref analyse` writes by default) and the project's plan, and PROPOSES lots that cover the elements nobody covers yet — each proposal cites the ids it covers, says whether it is feasible or blocked (naming the missing datum or service) and what it depends on. It only proposes, it creates no lot, edits no file and writes no exclusion, and the lead decides. Starts from `cadence ref coverage`, which lists the gaps. Used by the lead, not by hand.
4
+ tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ You plan the coverage of one reference document. You read; you never write. The lot is not yours to create, the plan is not yours to edit, and an element is not yours to exclude: you propose, the lead decides.
8
+
9
+ Follow the brief you are given. It names the inventory (`inventaire.json`) and the plan. The method:
10
+
11
+ 1. **Run `cadence ref coverage <inventaire.json>` first.** Its `✗` lines are the work: elements no done lot covers and no written exclusion justifies. A lot (any status but `dropped`) that cites an id already covers it in the plan's eyes; only a **done** lot counts for the command. An element a todo or doing lot already cites is not a gap to plan again: say so, name the lot.
12
+ 2. **Read the inventory and the plan.** The inventory gives, per element, its type, page, zone, text and the elements it refers to (`composant.ecran`, `etat.cible`, `statut.couvre`). The plan (`.cadence/raf.yaml`, the legacy `docs/plan/raf.yaml`, or the file named by `plan:` in `cadence.yaml`) gives the lots that exist, their titles and what they depend on. Read the project's CLAUDE.md and the code of the screens or modules the elements belong to: a proposal that ignores what exists duplicates it.
13
+ 3. **Group the gaps into lots of one session each** (an estimate of 1 or less): one screen, one zone of a screen, one table, one message template. Never one lot per element, never one lot for the whole document. An element goes into exactly one proposed lot.
14
+ 4. **For each proposed lot, say:**
15
+ - `titre`: what the lot delivers, in the project's words;
16
+ - `ids`: the ids it covers, copied from the inventory, never retyped from memory (the lot will quote them, and `cadence ref coverage` finds them by that quotation);
17
+ - `faisable`: `true` when everything the lot needs exists today; `false` when it is **blocked**, and then `bloque_par` names precisely the missing datum or service (« the `GET /api/x` endpoint does not exist », « no data source for the wind barbs », « the licence of the font is unknown »), never « missing information »;
18
+ - `depend_de`: the lots it waits for, an existing id of the plan (`L12`) or the title of another proposal;
19
+ - `raison`: one sentence on why these elements belong together.
20
+ 5. **A `silence` of the inventory is a question for a human**, not a lot: list it under `questions` with its id. Never answer it.
21
+ 6. **An element that should not be built** (a decoration of the document, a text of the mock-up that is not the product) is not excluded by you: list it under `exclusions_proposees` with its id and the reason, the lead writes it in the `exclusions` key of the inventory if they agree.
22
+ 7. **Never invent an id.** An id that is not in the inventory is an error of your proposal. Before you report, check that every `✗` id of `cadence ref coverage` is in a proposal, in `exclusions_proposees`, in `questions`, or named in `deja_cites` as already cited by an existing lot.
23
+
24
+ - You are read-only: no `raf add|start|done|note|review|ux`, no edit of the plan, of the inventory or of any file, no commit, no push, no deliver, no subagent. The only command you run is `cadence ref coverage` (and read-only commands such as `git log`, `grep`).
25
+ - A proposal with no id, or an id twice, is not a proposal.
26
+ - Give the report in the structured output the brief asks for: `lots` (each `titre`, `ids`, `faisable`, `bloque_par`, `depend_de`, `raison`), `exclusions_proposees` (`id`, `raison`), `deja_cites` (`id`, `lot`), `questions`, `nonVerifie`, `resume`.
package/bin/cadence.js CHANGED
@@ -13,7 +13,7 @@ const io = {
13
13
 
14
14
  if (tool === 'raf') {
15
15
  process.exitCode = await run(args, io);
16
- } else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref'].includes(tool)) {
16
+ } else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref', 'config', 'release'].includes(tool)) {
17
17
  process.exitCode = await run([tool, ...args], io);
18
18
  } else if (tool === '--version' || tool === '-v') {
19
19
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -39,6 +39,9 @@ if (tool === 'raf') {
39
39
  cadence ref measure <slug> <page> <page | x,y | x,y,l,h> [--quoi couleurs|contraste|boites|texte] [--largeur 1440] [--json]
40
40
  mesure une zone d'une page rendue : couleurs, contraste WCAG 2.2, boîtes, mots OCR et taille de police estimée,
41
41
  ramenés à 1440 px de large, tolérance déclarée
42
+ cadence release patch|minor|major [--dry-run]
43
+ prépare le commit de version (fichiers de version, CHANGELOG) sans rien commiter ni publier ;
44
+ imprime les étapes du lead dans l'ordre et vérifie docs/whats-new.md
42
45
  cadence verify [--retry secondes] [--sha rév]
43
46
  rejoue les vérifications d'effet (deliver.verify) hors livraison, une passe, en parallèle ;
44
47
  code 0 tout vert, 1 un effet rouge, 2 rien à vérifier ; « session start » la lance aussi
@@ -52,6 +55,10 @@ if (tool === 'raf') {
52
55
  cadence lead tour [dossier] [--idle 3] [--json]
53
56
  le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier courant
54
57
  s'il est lui-même un projet ; une première ligne « cadence.yaml · ✗ erreur » si priority: est illisible
58
+ cadence config set <clé> <valeur> --all [--dry-run] [--depth n]
59
+ pose la même clé dans le cadence.yaml de chaque projet du dossier (ceux de lead tour), commentaires et ordre gardés,
60
+ avec un lot de config qui la cite et un commit à chemin explicite par projet ; ne pousse pas ;
61
+ s'arrête sur une config qui ne se relit pas
55
62
  cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--since date] [--json]
56
63
  la mesure des passes de correction des vagues (dossier/.cadence/runs) : lots à ≥1 et ≥2 corrections,
57
64
  par projet, visibles ou non, passes par cause ; --classify les classe par Ollama (hors quota)
package/dist/cli.js CHANGED
@@ -9,16 +9,19 @@ import { short } from './check.js';
9
9
  import { isDay, toDay } from './dates.js';
10
10
  import { deliver, parseDeliverConfig, realDeps, shellQuote } from './deliver.js';
11
11
  import { defaultTarget, effectLines, realCheckDeps, verifyCommand } from './verify.js';
12
+ import { verifyGate } from './verify-gate.js';
12
13
  import { ganttData, renderGantt } from './gantt.js';
13
- import { gitRoot, isTracked, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
14
+ import { PLAN_ACTIONS, planCommitMessage } from './plan-commit.js';
15
+ import { dirtyPaths, gitRoot, isTracked, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
14
16
  import { installHook, pendingCommitMessage, REFUSED } from './hook.js';
15
17
  import { citedRefs, linkCommits } from './link.js';
16
18
  import { buildNews, loadEntries, newEntry, newsData, newsIssues, publicTitleTooLong, stampEntries } from './news.js';
17
- import { invalidResource, Plan, RafError, SCREENS, STATUSES } from './plan.js';
19
+ import { invalidResource, Plan, RafError, SCREENS, STATUSES, setPlanNotice } from './plan.js';
18
20
  import { dueLine, isRecurring, recurringByDue } from './recurring.js';
19
21
  import { schedule } from './schedule.js';
20
22
  import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
21
- import { findProjects, leadTour, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
23
+ import { configSet } from './config-set.js';
24
+ import { findProjects, readDepth, leadTour, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
22
25
  import { configuredCap, expiredHoldLines, liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
23
26
  import { addHold, releaseHold } from './orchestrate/registry.js';
24
27
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
@@ -26,8 +29,10 @@ import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './o
26
29
  import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
27
30
  import { classifyAll, measure, ollamaClassifier, readCache, readJournals, readRuns, renderReport, sampleKeys, serializeCache } from './corrections.js';
28
31
  import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
29
- import { findAcceptation, parseAcceptation, resealAcceptation } from './orchestrate/accept.js';
32
+ import { findAcceptation, parseAcceptation, redAcceptations, resealAcceptation, resealForGreen, runAcceptation, setVert } from './orchestrate/accept.js';
30
33
  import { refCommand } from './ref/command.js';
34
+ import { coverageCommand } from './ref/coverage.js';
35
+ import { release } from './release.js';
31
36
  import { applyMigration, describeMigration, planMigration, unhandledError } from './migrate.js';
32
37
  import { checkFiche, deriveQa, readFiche, summary } from './spec.js';
33
38
  import { readHudContext } from './hud-context.js';
@@ -59,6 +64,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
59
64
  raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot (ou citant un id inconnu) sans réécrire l'historique ; raf check --ignored les liste
60
65
  raf docs-ok <lot> --reason texte acquitte l'écart docs.sync d'un lot (documentation en retard) avec la raison pour laquelle la documentation reste telle quelle ; lu par raf check et par orchestrate, qui ne rend jamais prêt un lot avec un écart ouvert
61
66
  raf gantt [-o .cadence/gantt.html]
67
+ cadence config set <clé> <valeur> --all [--dry-run] [--depth n] pose la même clé dans le cadence.yaml de chaque projet du dossier (ceux de lead tour), commentaires et ordre gardés, un lot de config qui la cite (raf add + start) et un commit à chemin explicite par projet ; ne pousse pas ; liste ce qui est fait et sauté (dépôt sale, déjà posée, plan en lecture seule) ; s'arrête sur une config qui ne se relit pas
62
68
  raf migrate [--dry-run] déplace docs/plan/raf.yaml vers .cadence/raf.yaml (git mv) et réécrit les lecteurs du chemin dans le dépôt ; --dry-run les liste sans rien toucher ; refus tant qu'un lecteur n'est pas traité (migrate.keep de cadence.yaml pour ceux qui ne lisent pas le plan) ; rejouable
63
69
  raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
64
70
  cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--goal "…"] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague | --wave vague] [--budget 1M] [--budget projet:lot=+300k] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague] ; --waits [--since AAAA-MM-JJ]
@@ -72,9 +78,12 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
72
78
  cadence ref diff <lecture-a.json> <lecture-b.json> [--out inventaire.json] [--seuil 30] réconcilie deux lectures du même document (même slug et même empreinte) : concordant → un élément lu ou mesuré, divergent → incertain avec les deux versions, seul (une lecture) → incertain en attente du troisième regard ; silences réunis ; au-delà de 30 % d'éléments non appariés (--seuil, en pour cent) code 1 et rien d'écrit ; --out écrit l'inventaire
73
79
  cadence ref measure <slug> <page> <page | x,y | x,y,l,h> [--quoi couleurs|contraste|boites|texte] [--largeur 1440] [--json] mesure une zone d'une page rendue (pixels de la page) : couleurs (point, moyenne, dominantes), contraste WCAG 2.2, boîtes, mots OCR tesseract et taille de police estimée ; valeurs ramenées à 1440 px de large, tolérance déclarée
74
80
  cadence ref check <inventaire.json | lecture.json> valide un inventaire ou une lecture (schéma version 1, modèle templates/inventaire.json) : une ligne « ✗ chemin : raison » par écart puis « N écart(s) » sur la sortie standard (code 1), le résumé par type sinon (code 0) ; fichier absent, vide ou illisible : code 2 ; n'appelle aucun outil système
81
+ cadence ref coverage <inventaire.json> couverture d'un document de référence, sans modèle : les éléments qu'aucun lot fait ne couvre (lot qui cite l'id) et qu'aucune exclusion écrite avec sa raison (clé « exclusions » de l'inventaire) ne justifie, une ligne ✗ chacun ; code 1 s'il en reste, 0 sinon, 2 pour un appel mal formé ou un fichier qui n'est pas un inventaire valide
75
82
  cadence ref eval <attendu.json | dossier> <lecture.json | dossier> [--json] note une lecture contre l'inventaire attendu (vérité connue) : rappel et précision par type, faux lu (éléments sûrs et faux), pièges trouvés, tokens par page (usage.tokens de la lecture) ; lecture d'un autre document refusée (code 2) ; sur dossiers, un cas par sous-dossier (attendu.json / lecture.json)
76
83
  cadence ref corpus <dossier> [--sans-rendu] écrit le corpus synthétique à vérité connue (7 pièges : texte ambigu levé par une figure, figure qui contredit, table en image, gabarit BUFR, contrôle translucide, page Proposal, scan bruité) : par cas document.html et attendu.json, puis document.pdf et page-<n>.png (1700×2200) rendus par Playwright (CADENCE_PLAYWRIGHT = dossier du paquet) ; dossier existant ou Playwright introuvable : code 2
84
+ cadence release patch|minor|major [--dry-run] prépare le commit de version (package.json, package-lock.json, plugin.json, marketplace.json, CHANGELOG.md) sans commiter ni publier ; imprime les étapes du lead et vérifie docs/whats-new.md : refus sur un arbre sale, un [Unreleased] vide ou une version déjà taguée
77
85
  cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
86
+ cadence verify --gate --sha rév [--project dossier] rejoue la porte du projet (orchestrate.build puis orchestrate.test) au sha dans un worktree jetable aux dépendances reliées (orchestrate.worktreeDeps), puis le retire : 0 vert, 1 rouge avec les tests en échec, 2 rien à rejouer
78
87
  raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
79
88
 
80
89
  Un commit appartient à un lot quand son message cite l'identifiant : « feat(L3): … », « L3/t1 ».
@@ -109,6 +118,7 @@ function legacyPlanLine(root, legacy) {
109
118
  return `raf: ${PLAN_LEGACY} est l'ancien emplacement du plan ; le déplacer : cd ${shellQuote(root)} && mkdir -p ${dirname(PLAN_DEFAULT)} && ${gitRoot(root) && isTracked(root, PLAN_LEGACY) ? 'git mv' : 'mv'} ${PLAN_LEGACY} ${PLAN_DEFAULT}`;
110
119
  }
111
120
  export function run(argv, io) {
121
+ setPlanNotice(io.err);
112
122
  try {
113
123
  // orchestrate a ses propres options (--status et --resume prennent une valeur facultative, --answer en prend deux).
114
124
  if (argv[0] === 'orchestrate') {
@@ -168,6 +178,7 @@ function dispatch(argv, io) {
168
178
  'dry-run': { type: 'boolean' },
169
179
  sha: { type: 'string' },
170
180
  retry: { type: 'string' },
181
+ gate: { type: 'boolean' },
171
182
  clear: { type: 'boolean' },
172
183
  ignored: { type: 'boolean' },
173
184
  notes: { type: 'boolean' },
@@ -194,7 +205,7 @@ function dispatch(argv, io) {
194
205
  io.out(HELP);
195
206
  return 0;
196
207
  }
197
- if (command !== 'session' && !(command === 'lead' && !values.all) && (values.all || values.depth !== undefined))
208
+ if (command !== 'session' && command !== 'config' && !(command === 'lead' && !values.all) && (values.all || values.depth !== undefined))
198
209
  throw new RafError(`${values.all ? '--all' : '--depth'} ne va qu'avec session start ou session close`);
199
210
  const today = io.env.RAF_TODAY ?? toDay(io.now());
200
211
  if (!isDay(today))
@@ -263,12 +274,29 @@ function dispatch(argv, io) {
263
274
  }
264
275
  return 0;
265
276
  }
277
+ // Poser une clé dans le cadence.yaml de chaque projet du dossier (L342) : ne lit ni le plan ni le cadence.yaml du dossier courant.
278
+ if (command === 'config') {
279
+ if (rest[0] !== 'set' || rest.length !== 3)
280
+ throw new RafError('usage : cadence config set <clé> <valeur> --all [--dry-run] [--depth n]');
281
+ if (!values.all)
282
+ throw new RafError('config set demande --all : la commande s\'applique aux projets du dossier courant (lancer la commande dans le projet visé pour un seul)');
283
+ if (values.depth !== undefined && !(Number.isInteger(Number(values.depth)) && Number(values.depth) >= 1))
284
+ throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
285
+ const parent = resolve(io.cwd);
286
+ const result = configSet(parent, rest[1], rest[2], { dryRun: values['dry-run'], depth: values.depth === undefined ? readDepth(parent) : Number(values.depth) }, today);
287
+ for (const l of result.lines)
288
+ io.out(l);
289
+ return result.code;
290
+ }
266
291
  if (command === 'corrections')
267
292
  return corrections(rest, values, io);
268
293
  if (command === 'perf')
269
294
  return perf(rest, values, today, io);
270
- if (command === 'ref')
295
+ if (command === 'ref' && rest[0] !== 'coverage')
271
296
  return refCommand(rest, values, root, io);
297
+ // Prépare le commit de version : écrit les fichiers de version, ne commite ni ne publie rien (L341).
298
+ if (command === 'release')
299
+ return release(rest, !!values['dry-run'], root, today, io.out);
272
300
  // Déplacer le plan ne lit pas le plan : pas d'avertissement « ancien emplacement » (L284).
273
301
  if (command === 'migrate') {
274
302
  if (rest.length > 0)
@@ -288,6 +316,7 @@ function dispatch(argv, io) {
288
316
  if (command === 'spec' && (rest[0] === 'check' || rest[0] === 'qa'))
289
317
  return spec(rest, io);
290
318
  const reseal = command === 'spec' && rest[0] === 'reseal';
319
+ const green = command === 'spec' && rest[0] === 'green';
291
320
  // --all balaie les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant.
292
321
  if (command === 'session' && (values.all || values.depth !== undefined))
293
322
  return sessionAll(rest, values, io);
@@ -304,6 +333,9 @@ function dispatch(argv, io) {
304
333
  if (fallback?.legacy && !installing)
305
334
  (io.notice ?? io.err)(legacyPlanLine(root, fallback.legacy));
306
335
  const loadPlan = () => Plan.load(planPath, { ...planConfig?.settings, config: configPath });
336
+ // `ref coverage` lit le plan (les lots faits qui citent les éléments) : il passe après le choix du plan, les autres `ref` n'y touchent pas (L367).
337
+ if (command === 'ref')
338
+ return coverageCommand(rest.slice(1), loadPlan, io, { repo: root, specDir: readOrchestrateConfig(configPath).spec.dir });
307
339
  const newsDir = resolve(io.cwd, values.dir ?? join(root, 'docs/nouveautes'));
308
340
  // Limite DÉCLARÉE (news.publicTitleMax) : sans clé, seul « raf check » avertit ; avec, la pose d'un titre public est refusée.
309
341
  const refuseLongTitle = (text, hint = '') => {
@@ -387,6 +419,24 @@ function dispatch(argv, io) {
387
419
  refuseLongTitle(pub, lot?.public ? '' : ' — titre de la Nouveauté reprise par le site : corriger l\'entrée ou poser raf public');
388
420
  }
389
421
  // Le plan ne lit pas git : lui dire combien de commits du lot sa revue ne couvre pas quand elle est exigée.
422
+ // Un lot clos avec une acceptation à vert: false bloque les vagues suivantes du dépôt (verrou des tests d'acceptation, L366).
423
+ if (status === 'done' && !rest[0].includes('/')) {
424
+ const repo = gitRoot(root) ?? root;
425
+ let dir = null;
426
+ try {
427
+ dir = readOrchestrateConfig(configPath).spec.dir;
428
+ }
429
+ catch {
430
+ // un cadence.yaml illisible ne doit pas empêcher de clore un lot : `cadence check` en parle
431
+ }
432
+ const red = dir === null ? undefined : redAcceptations(repo, dir).find((a) => a.lot === rest[0]);
433
+ if (red) {
434
+ const text = `${rest[0]} : ${red.path} est à vert: false — clos ainsi, le lot bloquerait les vagues suivantes du dépôt (verrou des tests d'acceptation) ; cadence spec green ${rest[0]} lance la commande d'acceptation et pose vert: true`;
435
+ if (!values.force)
436
+ throw new RafError(`${text} (--force pour passer outre)`);
437
+ io.err(`avertissement : ${text}`);
438
+ }
439
+ }
390
440
  const unreviewed = status === 'done' && plan.reviewSince && !plan.readonly && !values.force ? unreviewedWork(plan, root, rest[0]) : 0;
391
441
  plan.setStatus(rest[0], status, today, { force: values.force, unreviewed });
392
442
  if (command === 'drop' && values.reason)
@@ -469,6 +519,8 @@ function dispatch(argv, io) {
469
519
  return 0;
470
520
  }
471
521
  case 'spec': {
522
+ if (green)
523
+ return specGreen(rest.slice(1), { root, loadPlan, configPath, today }, io);
472
524
  if (reseal)
473
525
  return specReseal(rest.slice(1), values.reason, values.porteur, { root, loadPlan, configPath, today }, io);
474
526
  if (rest.length !== 2 || (rest[1] !== 'on' && rest[1] !== 'off'))
@@ -680,6 +732,8 @@ function dispatch(argv, io) {
680
732
  return deliver(ctx, realDeps(root));
681
733
  }
682
734
  case 'verify': {
735
+ if (values.gate)
736
+ return verifyGateCommand(values, io);
683
737
  if (!gitRoot(io.cwd))
684
738
  throw new RafError('verify : à lancer dans un dépôt git');
685
739
  if (!existsSync(configPath))
@@ -849,12 +903,23 @@ function preCommit(load, mode, root, today, io) {
849
903
  io.err(`raf: plan modifié non commité : démarrage automatique sauté (hook.autostart: start), raf start ${todo.join(' ')} à la main`);
850
904
  return 0;
851
905
  }
852
- for (const id of todo) {
906
+ for (const id of todo)
853
907
  plan.setStatus(id, 'doing', today, {});
854
- io.err(`raf: ${id} était todo — raf start ${id} fait par le hook, plan inclus dans ce commit`);
908
+ try {
909
+ plan.save();
855
910
  }
856
- plan.save();
911
+ catch (e) {
912
+ if (!(e instanceof RafError))
913
+ throw e;
914
+ // plan.onWrite en échec (L346) : le plan est resté tel quel, le commit part sans lui et le dit.
915
+ io.err(`raf: démarrage automatique refusé : ${e.message} ; raf start ${todo.join(' ')} à la main`);
916
+ return 0;
917
+ }
918
+ for (const id of todo)
919
+ io.err(`raf: ${id} était todo — raf start ${id} fait par le hook, plan inclus dans ce commit`);
857
920
  stage(root, file);
921
+ for (const f of plan.produced)
922
+ stage(root, f);
858
923
  }
859
924
  catch {
860
925
  // Un hook ne doit jamais gêner un commit, hors refus.
@@ -991,7 +1056,7 @@ const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', st
991
1056
  /** Chemin à coller dans un shell : entre apostrophes dès qu'il contient autre chose que des caractères sûrs. */
992
1057
  const shellPath = (p) => (/^[\w@%+=:,./-]+$/.test(p) ? p : `'${p.replace(/'/g, "'\\''")}'`);
993
1058
  /** Le refus de `spec reseal` devant des vagues non terminées (L238) : chaque vague (vivante ou arrêtée), son dossier de lancement, ses lots, et pour chaque vague arrêtée UNE commande à lancer telle quelle (un `--drop` par lot ouvert du dépôt, y compris ceux en file qui ne sont pas en cours au plan : `--resume` relance toute la vague avec les lots non retirés, et un lot en file reprendrait le verrou, L239). */
994
- function openWavesRefusal(open) {
1059
+ function openWavesRefusal(open, command) {
995
1060
  const lines = open.map((w) => {
996
1061
  const where = [w.live ? 'vivante' : 'arrêtée', ...(w.live && w.pid !== undefined ? [`pid ${w.pid}`] : []), ...(w.launchDir ? [`lancée depuis ${w.launchDir}`] : [])].join(', ');
997
1062
  return ` - vague ${w.wave} (${where}) : ${w.lots.length ? [...w.lots.map((l) => `${l.key} (${l.status})`), ...w.others.map((l) => `${l.key} (${l.status}, pas en cours au plan)`)].join(', ') : 'lots illisibles'}`;
@@ -1000,9 +1065,9 @@ function openWavesRefusal(open) {
1000
1065
  ? ` vague ${w.wave} : orchestration en cours, à attendre (le verrou du dépôt tombe dès que tous ses lots y sont prêts ou rendus)`
1001
1066
  : ` cd ${shellPath(w.launchDir)} && ${resumeCommand(w.wave, ...[...w.lots, ...w.others].flatMap((l) => ['--drop', l.key]))}`);
1002
1067
  return [
1003
- `spec reseal : refusé — ${open.length > 1 ? `${open.length} vagues non terminées gardent` : 'une vague non terminée garde'} ouvert au moins un lot en cours de ce dépôt (un rescellé commité maintenant lui serait rendu comme « premier commit fautif » à la reprise de la vague) :`,
1068
+ `${command} : refusé — ${open.length > 1 ? `${open.length} vagues non terminées gardent` : 'une vague non terminée garde'} ouvert au moins un lot en cours de ce dépôt (${command === 'spec green' ? 'le commit de spec green posé' : 'un rescellé commité'} maintenant lui serait rendu comme « premier commit fautif » à la reprise de la vague) :`,
1004
1069
  ...lines,
1005
- 'Marche à suivre : clore chacun de ces lots dans sa vague (attendre les vivantes, retirer les lots des arrêtées), puis relancer le rescellé :',
1070
+ `Marche à suivre : clore chacun de ces lots dans sa vague (attendre les vivantes, retirer les lots des arrêtées), puis relancer ${command === 'spec green' ? 'spec green' : 'le rescellé'} :`,
1006
1071
  ...how,
1007
1072
  ].join('\n');
1008
1073
  }
@@ -1028,12 +1093,86 @@ function withHolder(open, holder, repo) {
1028
1093
  const keys = openLotsIn(cadenceHome(), holder.pid, repo) ?? [];
1029
1094
  return [{ wave: holder.wave, launchDir: reg?.cwd ?? '', live: true, pid: holder.pid, lots: keys.map((key) => ({ key, status: 'ouvert' })), others: [] }, ...open];
1030
1095
  }
1031
- function specReseal(args, reason, porteur, ctx, io) {
1032
- if (args.length !== 2 || !reason?.trim())
1033
- throw new RafError('usage : cadence spec reseal <lot> <test> --reason "pourquoi le test contredit la fiche" [--porteur <lot en cours>, pour un lot livré]');
1096
+ /**
1097
+ * `cadence spec green <lot>` (L366) : un lot mené ou clos à la main (raf done) dont l'acceptation est restée à `vert: false` bloque toutes les vagues suivantes du dépôt.
1098
+ * Lance la commande dédiée (`orchestrate.accept`) comme le cycle ; verte, rescelle les tests mis au point depuis le commit rouge (`resealForGreen`), pose `vert: true` et commite le seul fichier, en citant le lot. Rouge : rien n'est changé, code 1.
1099
+ */
1100
+ function specGreen(args, ctx, io) {
1101
+ if (args.length !== 1)
1102
+ throw new RafError('usage : cadence spec green <lot>');
1034
1103
  if (io.env.CADENCE_ORCHESTRATED)
1035
- throw new RafError(`spec reseal refusé pendant une vague orchestrée (${io.env.CADENCE_ORCHESTRATED}) : c'est le travail du lead, hors session`);
1036
- // Comme `deliver` : la vague tient le verrou du dépôt tant qu'un de ses lots y travaille (état illisible : on refuse) ; un commit du lead en pleine passe ferait signaler au lot suivant un écart qui n'est pas le sien.
1104
+ throw new RafError(`spec green refusé pendant une vague orchestrée (${io.env.CADENCE_ORCHESTRATED}) : c'est le travail du lead, hors session`);
1105
+ refuseWhileWaveHolds(ctx, 'spec green');
1106
+ const lotId = args[0];
1107
+ const plan = ctx.loadPlan();
1108
+ const lot = plan.lot(lotId);
1109
+ if (lot.status === 'todo')
1110
+ throw new RafError(`${lotId} n'est pas commencé (todo) : il n'a rien livré dont l'acceptation puisse passer au vert`);
1111
+ const repo = gitRoot(ctx.root) ?? ctx.root;
1112
+ const config = readOrchestrateConfig(ctx.configPath);
1113
+ if (!config.accept)
1114
+ throw new RafError(`orchestrate.accept absent de ${ctx.configPath} : sans la commande dédiée, il n'y a rien à lancer pour ${lotId}`);
1115
+ const json = findAcceptation(repo, config.spec.dir, lotId);
1116
+ if (!json)
1117
+ throw new RafError(`${lotId} : aucune acceptation sous ${config.spec.dir} (le lot n'a pas passé l'étape accept)`);
1118
+ const doc = parseAcceptation(readFileSync(join(repo, json), 'utf8'));
1119
+ if (typeof doc === 'string')
1120
+ throw new RafError(`${json} : ${doc}`);
1121
+ if (doc.vert) {
1122
+ io.out(`${lotId} : ${json} est déjà vert, rien à faire`);
1123
+ return 0;
1124
+ }
1125
+ const open = unfinishedWaves(ctx.root, repo, plan);
1126
+ if (open.length)
1127
+ throw new RafError(openWavesRefusal(open, 'spec green'));
1128
+ const dirty = git(repo, ['status', '--porcelain', '-z', '--', ...doc.fichiers]).split('\0').filter(Boolean).map((l) => l.slice(3));
1129
+ if (dirty.length)
1130
+ throw new RafError(`${lotId} : test(s) listé(s) modifié(s) sans être commités : ${dirty.join(', ')} — les commiter (en citant ${lotId}) avant de poser vert: true`);
1131
+ const { runs, orphans } = runAcceptation(repo, json, config.accept);
1132
+ if (orphans.length)
1133
+ throw new RafError(`fichiers listés dans ${json} sous la racine d'aucune commande de orchestrate.accept : ${orphans.join(', ')}`);
1134
+ const failed = runs.filter((r) => r.code !== 0);
1135
+ if (failed.length) {
1136
+ io.err(`${lotId} : tests d'acceptation encore rouges (${failed.map((r) => `${r.command}, code ${r.code}`).join(' ; ')}), ${json} reste à vert: false`);
1137
+ for (const r of failed)
1138
+ io.err(r.output.trim().split('\n').slice(-40).join('\n'));
1139
+ return 1;
1140
+ }
1141
+ const before = readFileSync(join(repo, json), 'utf8');
1142
+ let resceles;
1143
+ try {
1144
+ resceles = resealForGreen(repo, json, ctx.today);
1145
+ }
1146
+ catch (e) {
1147
+ throw new RafError(`${lotId} : ${e.message}`);
1148
+ }
1149
+ setVert(repo, json);
1150
+ try {
1151
+ git(repo, ['add', '--', json]);
1152
+ // Le sujet reste court et fixe (le gabarit `plan.commit` du projet s'il y en a un, comme tout commit écrit par cadence, L343) ; les tests rescellés, sans borne, vont dans le corps, une ligne chacun.
1153
+ const template = readPlanConfig(join(repo, 'cadence.yaml'), repo)?.commit;
1154
+ const subject = template ? planCommitMessage(template, { lot: lotId, action: PLAN_ACTIONS.specGreen }) : `chore(${lotId}): ${PLAN_ACTIONS.specGreen}`;
1155
+ const body = resceles.length ? ['empreintes rescellées (tests mis au point depuis le commit rouge) :', ...resceles.map((f) => `- ${f}`)].join('\n') : null;
1156
+ git(repo, ['commit', '-q', '-m', subject, ...(body ? ['-m', body] : []), '--', json]);
1157
+ }
1158
+ catch (e) {
1159
+ // Atomique, comme le rescellé : un commit refusé (hook pre-commit, ...) laisse le fichier tel qu'avant.
1160
+ writeFileSync(join(repo, json), before);
1161
+ try {
1162
+ git(repo, ['reset', '-q', '--', json]);
1163
+ }
1164
+ catch {
1165
+ // rien d'indexé : rien à défaire
1166
+ }
1167
+ const out = e;
1168
+ throw new RafError(`spec green : le commit a échoué, ${json} est revenu tel qu'avant la commande — ${(out.stderr || out.stdout || out.message).trim().split('\n').slice(-5).join(' | ')}`);
1169
+ }
1170
+ io.out(`${lotId} : ${json} → vert: true, commit ${git(repo, ['rev-parse', '--short', 'HEAD']).trim()}${resceles.length ? ` ; empreintes rescellées (tests mis au point depuis le commit rouge) : ${resceles.join(', ')}` : ''}`);
1171
+ return 0;
1172
+ }
1173
+ /** Un commit du lead en pleine passe ferait signaler au lot suivant un écart qui n'est pas le sien : tant qu'une vague tient le verrou du dépôt, `spec reseal` et `spec green` refusent. */
1174
+ function refuseWhileWaveHolds(ctx, command) {
1175
+ // Comme `deliver` : la vague tient le verrou du dépôt tant qu'un de ses lots y travaille (état illisible : on refuse).
1037
1176
  // Le refus est UN message : la vague vivante qui tient le verrou (à attendre) et les vagues arrêtées du dépôt, avec leurs dossiers et leurs commandes (L238).
1038
1177
  const orchestrating = waveHolding(ctx.root);
1039
1178
  if (orchestrating) {
@@ -1045,8 +1184,15 @@ function specReseal(args, reason, porteur, ctx, io) {
1045
1184
  catch {
1046
1185
  // plan illisible : le verrou seul motive le refus, la vague qui le tient est nommée
1047
1186
  }
1048
- throw new RafError(openWavesRefusal(withHolder(open, orchestrating, repo)));
1187
+ throw new RafError(openWavesRefusal(withHolder(open, orchestrating, repo), command));
1049
1188
  }
1189
+ }
1190
+ function specReseal(args, reason, porteur, ctx, io) {
1191
+ if (args.length !== 2 || !reason?.trim())
1192
+ throw new RafError('usage : cadence spec reseal <lot> <test> --reason "pourquoi le test contredit la fiche" [--porteur <lot en cours>, pour un lot livré]');
1193
+ if (io.env.CADENCE_ORCHESTRATED)
1194
+ throw new RafError(`spec reseal refusé pendant une vague orchestrée (${io.env.CADENCE_ORCHESTRATED}) : c'est le travail du lead, hors session`);
1195
+ refuseWhileWaveHolds(ctx, 'spec reseal');
1050
1196
  const [lotId, test] = args;
1051
1197
  const plan = ctx.loadPlan();
1052
1198
  const lot = plan.lot(lotId);
@@ -1075,7 +1221,7 @@ function specReseal(args, reason, porteur, ctx, io) {
1075
1221
  // Le verrou du rescellé couvre tous les lots ouverts du dépôt, pas seulement le porteur : on refuse dès qu'un lot DE CE DÉPÔT, encore en cours au plan, reste ouvert dans une vague non terminée.
1076
1222
  const open = unfinishedWaves(ctx.root, repo, plan);
1077
1223
  if (open.length)
1078
- throw new RafError(openWavesRefusal(open));
1224
+ throw new RafError(openWavesRefusal(open, 'spec reseal'));
1079
1225
  const before = { json: readFileSync(join(repo, json), 'utf8'), test: existsSync(join(repo, test)) ? readFileSync(join(repo, test), 'utf8') : '' };
1080
1226
  let r;
1081
1227
  try {
@@ -1109,14 +1255,23 @@ function specReseal(args, reason, porteur, ctx, io) {
1109
1255
  return 0;
1110
1256
  }
1111
1257
  plan.note(cite, `test d'acceptation rescellé${livre ? ` (lot livré ${lotId}, ` : ' ('}${norm}, ${r.ficheAmendee ? `fiche amendée en ${r.ficheAmendee}` : 'fiche inchangée'}) : ${reason.trim()}`, ctx.today);
1112
- plan.save();
1258
+ try {
1259
+ plan.save();
1260
+ }
1261
+ catch (e) {
1262
+ // Le rescellé est commité (sha) : seul le plan est refusé (plan.onWrite en échec), il reste inchangé et la note n'est pas écrite ; dire « écriture refusée » seul laisserait croire que rien n'a été fait.
1263
+ throw new RafError(`spec reseal : le rescellé a eu lieu (commit ${sha}) mais la note du lot n’a pas été écrite, le plan est inchangé — ${e.message} — relancer la commande dit « rien à resceller » : ajouter la note à la main (raf note ${cite}) une fois le plan réparé`);
1264
+ }
1113
1265
  // La note du lot rejoint le dépôt dans la foulée (un dépôt sale arrêterait la vague suivante) ; un plan hors du dépôt ou non suivi reste à la main du lead.
1114
1266
  const planRel = relative(repo, plan.path);
1267
+ const produced = plan.produced;
1115
1268
  const tracked = !planRel.startsWith('..') && git(repo, ['ls-files', '--', planRel]).trim() !== '';
1116
1269
  if (tracked) {
1117
1270
  try {
1118
- git(repo, ['add', '--', planRel]);
1119
- git(repo, ['commit', '-q', '-m', `plan: ${cite} test d'acceptation rescellé (${norm})`, '--', planRel]);
1271
+ // Les fichiers de plan.onWrite (L346) partent avec la note : la commande a tourné à l'écriture du plan.
1272
+ const withPlan = [planRel, ...(produced.length ? dirtyPaths(repo, produced) : [])];
1273
+ git(repo, ['add', '--', ...withPlan]);
1274
+ git(repo, ['commit', '-q', '-m', planCommitMessage(readPlanConfig(join(repo, 'cadence.yaml'), repo)?.commit, { lot: cite, action: PLAN_ACTIONS.resealed(norm) }), '--', ...withPlan]);
1120
1275
  }
1121
1276
  catch (e) {
1122
1277
  // Le rescellé est fait (commit ${sha}) : on ne le défait pas, on dit où en est la note, au lieu d'une pile d'appels.
@@ -1273,6 +1428,26 @@ function perf(rest, values, today, io) {
1273
1428
  return 1;
1274
1429
  }
1275
1430
  }
1431
+ /** `verify --gate --sha rév [--project dossier]` (L348) : la porte du projet rejouée au sha ; `--project` est un sous-dossier du dossier courant qui a son propre dépôt. */
1432
+ async function verifyGateCommand(values, io) {
1433
+ if (values.sha === undefined)
1434
+ throw new RafError('verify --gate : --sha obligatoire (le sha à re-vérifier, jamais la tête mouvante d\'une vague)');
1435
+ if (values.retry !== undefined)
1436
+ throw new RafError('verify --gate : --retry ne va qu\'à verify seul (deliver.verify)');
1437
+ const dir = values.project === undefined ? io.cwd : resolve(io.cwd, values.project);
1438
+ if (values.project !== undefined && !existsSync(dir))
1439
+ throw new RafError(`verify --gate : --project ${values.project} : dossier introuvable sous ${io.cwd}`);
1440
+ const top = gitRoot(dir);
1441
+ if (!top)
1442
+ throw new RafError('verify --gate : à lancer dans un dépôt git (ou --project vers le dossier d\'un projet)');
1443
+ if (values.project !== undefined && realpathSync(top) !== realpathSync(dir))
1444
+ throw new RafError(`verify --gate : ${values.project} n'a pas son propre dépôt git (dépôt parent : ${top})`);
1445
+ const configPath = resolve(io.cwd, values.config ?? join(top, 'cadence.yaml'));
1446
+ const sha = resolveCommit(top, values.sha);
1447
+ if (!sha)
1448
+ throw new RafError(`verify --gate : --sha ${values.sha} : commit introuvable dans ${top}`);
1449
+ return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), out: io.out });
1450
+ }
1276
1451
  /** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
1277
1452
  async function sessionAll(rest, values, io) {
1278
1453
  const sub = rest[0];