@sylad/cadence 1.8.0 → 1.10.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +27 -54
- package/agents/code-reviewer.md +2 -2
- package/agents/doc-keeper.md +19 -0
- package/agents/precheck-reader.md +1 -1
- package/agents/qa-reviewer.md +2 -2
- package/agents/reference-analyst.md +25 -0
- package/agents/reference-verifier.md +19 -0
- package/agents/spec-writer.md +1 -1
- package/agents/ux-reviewer.md +1 -1
- package/bin/cadence.js +9 -1
- package/dist/audit.js +16 -4
- package/dist/check.js +24 -2
- package/dist/cli.js +258 -30
- package/dist/config.js +101 -5
- package/dist/corrections.js +24 -2
- package/dist/deliver.js +125 -37
- package/dist/docsync.js +19 -3
- package/dist/git.js +16 -3
- package/dist/lead.js +45 -12
- package/dist/migrate.js +166 -0
- package/dist/orchestrate/briefs.js +46 -6
- package/dist/orchestrate/changelog-merge.js +94 -0
- package/dist/orchestrate/command.js +358 -41
- package/dist/orchestrate/cycle.js +922 -285
- package/dist/orchestrate/guard.js +2 -2
- package/dist/orchestrate/integrate.js +129 -0
- package/dist/orchestrate/launch.js +48 -6
- package/dist/orchestrate/mutex.js +30 -0
- package/dist/orchestrate/no-test.js +41 -0
- package/dist/orchestrate/pool.js +74 -36
- package/dist/orchestrate/registry.js +146 -14
- package/dist/orchestrate/result.js +1 -1
- package/dist/orchestrate/snapshot.js +54 -1
- package/dist/orchestrate/spec.js +89 -5
- package/dist/orchestrate/state.js +56 -15
- package/dist/orchestrate/table.js +74 -3
- package/dist/orchestrate/timeouts.js +40 -0
- package/dist/orchestrate/worktree.js +368 -0
- package/dist/perf.js +188 -25
- package/dist/plan.js +112 -7
- package/dist/proc.js +6 -0
- package/dist/ready-free.js +109 -0
- package/dist/ref/add.js +129 -0
- package/dist/ref/analyse.js +220 -0
- package/dist/ref/check.js +338 -0
- package/dist/ref/command.js +92 -0
- package/dist/ref/corpus-render.js +83 -0
- package/dist/ref/corpus.js +201 -0
- package/dist/ref/diff.js +262 -0
- package/dist/ref/evaluate.js +241 -0
- package/dist/ref/figures.js +252 -0
- package/dist/ref/measure.js +330 -0
- package/dist/ref/pdf.js +116 -0
- package/dist/ref/png.js +125 -0
- package/dist/ref/regard.js +102 -0
- package/dist/ref/session.js +34 -0
- package/dist/ref/store.js +40 -0
- package/dist/ref/tools.js +51 -0
- package/dist/resources.js +51 -0
- package/dist/session.js +5 -2
- package/dist/state.js +75 -7
- package/dist/testgate.js +15 -0
- package/docs/cadence-hud.md +12 -0
- package/docs/cycle-vague-dark.svg +1 -1
- package/docs/cycle-vague-light.svg +1 -1
- package/docs/deliver.md +76 -5
- package/docs/fonctionnement-wave-dark.svg +1 -1
- package/docs/fonctionnement-wave-light.svg +1 -1
- package/docs/fonctionnement.md +6 -6
- package/docs/lead.md +40 -13
- package/docs/news.md +30 -1
- package/docs/orchestrate.md +275 -33
- package/docs/overview.md +65 -1
- package/docs/perf.md +14 -8
- package/docs/raf.md +90 -11
- package/docs/ref.md +415 -0
- package/docs/releasing.md +3 -3
- package/docs/session.md +2 -2
- package/docs/skills.md +21 -2
- package/docs/spec.md +1 -1
- package/docs/stabilite.md +4 -4
- package/docs/whats-new.md +30 -1
- package/package.json +2 -1
- package/skills/deliver/SKILL.md +6 -1
- package/skills/lead/SKILL.md +28 -10
- package/skills/session-close/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +2 -2
- package/templates/inventaire.json +96 -0
- package/templates/orchestrate/accept-back.md +1 -0
- package/templates/orchestrate/accept.md +1 -0
- package/templates/orchestrate/fiche.md +1 -0
- package/templates/orchestrate/fix-docs.md +18 -0
- package/templates/orchestrate/fix-minors.md +2 -1
- package/templates/orchestrate/fix.md +5 -2
- package/templates/orchestrate/implement.md +7 -4
- package/templates/orchestrate/maquette.md +2 -1
- package/templates/orchestrate/precheck.md +2 -1
- package/templates/orchestrate/review-recheck.md +2 -0
- package/templates/orchestrate/review-small.md +5 -1
- package/templates/orchestrate/review.md +2 -0
- package/templates/orchestrate/spec-back.md +2 -1
- package/templates/orchestrate/spec.md +4 -3
- package/templates/orchestrate/ux.md +3 -1
package/dist/cli.js
CHANGED
|
@@ -2,29 +2,33 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { basename, dirname, join, posix, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import { readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
|
+
import { defaultPlan, PLAN_DEFAULT, PLAN_LEGACY, readDocsConfig, readHookConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
6
6
|
import { repoSections, repoShas, resolveLotRepos } from './repos.js';
|
|
7
7
|
import { audit, exemptPlanOnly, gateProgress, gateText, gateWaiting, hypothesesOf, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
8
8
|
import { short } from './check.js';
|
|
9
9
|
import { isDay, toDay } from './dates.js';
|
|
10
|
-
import { deliver, parseDeliverConfig, realDeps } from './deliver.js';
|
|
10
|
+
import { deliver, parseDeliverConfig, realDeps, shellQuote } from './deliver.js';
|
|
11
11
|
import { defaultTarget, effectLines, realCheckDeps, verifyCommand } from './verify.js';
|
|
12
12
|
import { ganttData, renderGantt } from './gantt.js';
|
|
13
|
-
import { gitRoot, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
|
|
13
|
+
import { gitRoot, isTracked, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
|
|
14
14
|
import { installHook, pendingCommitMessage, REFUSED } from './hook.js';
|
|
15
15
|
import { citedRefs, linkCommits } from './link.js';
|
|
16
16
|
import { buildNews, loadEntries, newEntry, newsData, newsIssues, publicTitleTooLong, stampEntries } from './news.js';
|
|
17
|
-
import { Plan, RafError, SCREENS, STATUSES } from './plan.js';
|
|
17
|
+
import { invalidResource, Plan, RafError, SCREENS, STATUSES } from './plan.js';
|
|
18
18
|
import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
19
19
|
import { schedule } from './schedule.js';
|
|
20
20
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
21
|
-
import { findProjects, leadTour, readLeadUx, tourLine } from './lead.js';
|
|
22
|
-
import { configuredCap, liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
21
|
+
import { findProjects, leadTour, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
22
|
+
import { configuredCap, expiredHoldLines, liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
23
|
+
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
23
24
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
24
|
-
import { cadenceHome, openLotsIn } from './orchestrate/registry.js';
|
|
25
|
+
import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
|
|
26
|
+
import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
|
|
25
27
|
import { classifyAll, measure, ollamaClassifier, readCache, readJournals, readRuns, renderReport, sampleKeys, serializeCache } from './corrections.js';
|
|
26
|
-
import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries,
|
|
28
|
+
import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
|
|
27
29
|
import { findAcceptation, parseAcceptation, resealAcceptation } from './orchestrate/accept.js';
|
|
30
|
+
import { refCommand } from './ref/command.js';
|
|
31
|
+
import { applyMigration, describeMigration, planMigration, unhandledError } from './migrate.js';
|
|
28
32
|
import { checkFiche, deriveQa, readFiche, summary } from './spec.js';
|
|
29
33
|
import { readHudContext } from './hud-context.js';
|
|
30
34
|
import { sessionClose, sessionStart } from './session.js';
|
|
@@ -32,13 +36,16 @@ import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './stat
|
|
|
32
36
|
const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
|
|
33
37
|
|
|
34
38
|
raf init [--project nom] [--prefix L] [--no-hook]
|
|
35
|
-
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--public "titre public"] [--every jours] [--after L2,L4] [--parent L3]
|
|
39
|
+
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--parent L3]
|
|
36
40
|
raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
|
|
37
41
|
raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (seule la valeur page adopte l'étape spec d'orchestrate)
|
|
42
|
+
raf visible <id> on|off pose ou retire visible: d'un lot existant (changement visible : entrée Nouveautés et étape UX d'orchestrate) ; off est refusé tant que le lot a un screen:
|
|
38
43
|
raf spec <id> on|off pose ou retire spec: d'un lot existant (le lot change un comportement : étape spec back d'orchestrate)
|
|
44
|
+
raf uses <lot> [<ressource>…] remplace les ressources partagées du lot (uses:, déclarées par resources: dans le cadence.yaml du dossier parent ; raf check refuse un nom non déclaré) ; sans nom, retire la clé
|
|
39
45
|
raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
|
|
40
46
|
raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits
|
|
41
47
|
raf note <id> "texte"
|
|
48
|
+
raf title <id> "nouveau titre" remplace le titre d'un lot, de tout statut (done compris), et note l'ancien dans une note datée
|
|
42
49
|
raf did <id> ["texte"] un lot récurrent (--every) a été refait : le « dû depuis N j » repart de aujourd'hui
|
|
43
50
|
raf ux enable revue UX obligatoire avant « done » pour les lots --visible
|
|
44
51
|
raf ux <id> "verdict" enregistre la revue d'ergonomie du lot (agent ux-reviewer)
|
|
@@ -50,12 +57,23 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
50
57
|
raf list [--status todo|doing|done|dropped]
|
|
51
58
|
raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
|
|
52
59
|
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
|
|
53
|
-
raf
|
|
60
|
+
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
|
+
raf gantt [-o .cadence/gantt.html]
|
|
62
|
+
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
|
|
54
63
|
raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
|
|
55
|
-
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [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]
|
|
64
|
+
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]
|
|
65
|
+
mode worktree par lot (orchestrate.worktrees: true) (activé par défaut dans cadence, désactivé ailleurs tant que la clé est absente) ; ses options : --resume --wave <vague> --integrate projet:lot (rejoue un lot rendu à l'intégration) · --resume --wave <vague> --replay projet:lot (idem, et après une écriture de plan refusée) · --remove-worktree projet:lot [--abandon-commits] (retire le worktree et la branche conservés) ; la liste des worktrees conservés : orchestrate --status et lead tour
|
|
56
66
|
cadence lead tour [dossier] [--idle 3] [--depth n] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier seul s'il est lui-même un projet (en cours, dérive, notes de clôture, prochain lot prêt, dépôt), précédées de l'en-tête perf : une ligne par ⚠ de cadence perf, rien si tout est stable
|
|
67
|
+
cadence lead hold <étiquette> [--ttl minutes] | lead release <étiquette> déclare (60 min par défaut), puis rend, une session lancée à la main par le lead : elle est soustraite des créneaux libres de orchestrate --status et de lead tour ; au TTL échu, les deux la signalent en ⚠ jusqu'au release
|
|
57
68
|
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--since date] [--json] mesure des passes de correction des journaux dossier/.cadence/runs : lots à ≥1 et ≥2 corrections, par projet, visibles ou non, passes par cause (classées par Ollama avec --classify, hors quota)
|
|
58
69
|
cadence perf [dossier] [--since date] [--json] tableau de bord du workflow, sans modèle : deux fenêtres (7 derniers jours contre les 7 précédents) lues dans dossier/.cadence/runs et les plans, chaque vitesse à côté de sa qualité, ⚠ au-delà de 25 % de dégradation ; tokens par lot selon l'estimate, part de la revue, délais création → done → livraison, durée de mur par étape, coût en euros par lot livré (grille cadence.yaml perf.prices) ; --since est un jour qui tronque les fenêtres
|
|
70
|
+
cadence ref add <document.pdf> [--sections 3-5,9] [--slug nom] enregistre un document de référence hors du dépôt (references.dir de cadence.yaml, sinon $XDG_DATA_HOME/cadence/references) : pages rendues à 200 dpi, texte positionné, images incluses, index des figures et des passages qui les citent ; exige poppler, ImageMagick et tesseract (outil absent nommé avec sa commande, code 2)
|
|
71
|
+
cadence ref analyse <document.pdf> [--sections 3-5,9] [--slug nom] [--model sonnet|opus|haiku] [--model-b sonnet|opus|haiku] [--seuil 30] [--double] la chaîne : add, puis UNE lecture (une session de l'agent reference-analyst, lectures/a.json, le programme relit le fichier : ref check, document et genre) ; --double : deux lectures indépendantes (A sur --model, B sur --model-b, lectures/a.json et b.json), ref diff, le troisième regard (agent reference-verifier : confirme ou écarte ce qu'une seule lecture porte, d'après une découpe) et l'inventaire réconcilié (inventaire.json, usage.tokens = les trois sessions) — option dont la mesure n'a pas montré de gain (docs/mesures/ref-double-lecture-2026-10-25.md) ; --model-b et --seuil n'ont de sens qu'avec --double ; plafond déclaré : 20 pages par lecture ; code 1 lecture refusée ou (--double) trop d'éléments non appariés (--seuil, 30 %), 2 refus (plafond, modèle, session en échec), 3 quota
|
|
72
|
+
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
|
+
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
|
+
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
|
|
75
|
+
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
|
+
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
|
|
59
77
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
60
78
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
61
79
|
|
|
@@ -67,7 +85,7 @@ Un commit qui ne touche que le plan, la clé plan: de cadence.yaml (deliver/sess
|
|
|
67
85
|
publié par exemple, ainsi que le fichier d'attendus QA : docs/qa/expectations.md ou qa.expectations) n'a pas à en citer, et ne compte pas pour les lots qu'il cite ; le sujet n'y change rien.
|
|
68
86
|
Un lot --visible attend une entrée Nouveautés (docs/nouveautes/, --dir) avec capture ; raf check le vérifie.
|
|
69
87
|
Un texte qui commence par « - » se passe après « -- » : raf note L1 -- "-5 %".
|
|
70
|
-
Le plan est docs/plan/raf.yaml, ou celui que nomme « plan: » dans cadence.yaml ; un plan tenu par un
|
|
88
|
+
Le plan est .cadence/raf.yaml (versionné, jamais publié avec docs/ ; un docs/plan/raf.yaml hérité reste lu, raf propose le git mv (mv si git ne le suit pas), avec cd <racine> pour qu'il s'exécute de n'importe quel dossier), ou celui que nomme « plan: » dans cadence.yaml ; un plan tenu par un
|
|
71
89
|
autre outil se lit sans migration (correspondance des champs, voir docs/raf.md du paquet @sylad/cadence) et reste en lecture seule.
|
|
72
90
|
Options communes : --file chemin (ou RAF_FILE), RAF_TODAY=AAAA-MM-JJ pour figer la date.`;
|
|
73
91
|
/** Ajoute `line` au .gitignore de `root` s'il n'y est pas déjà (avec ou sans `/` initial ou final). Rend vrai si écrit. */
|
|
@@ -80,6 +98,16 @@ export function ensureGitignore(root, line) {
|
|
|
80
98
|
writeFileSync(file, `${text}${text === '' || text.endsWith('\n') ? '' : '\n'}${line}\n`);
|
|
81
99
|
return true;
|
|
82
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* La ligne stderr d'un docs/plan/raf.yaml hérité : le déplacement proposé (jamais fait), ou l'avertissement quand .cadence/raf.yaml existe aussi.
|
|
103
|
+
* La commande proposée commence par `cd <racine>` : elle s'exécute telle quelle depuis n'importe quel dossier (et non depuis la racine
|
|
104
|
+
* seulement) et, sous `session --all`, elle nomme le projet dont il s'agit.
|
|
105
|
+
*/
|
|
106
|
+
function legacyPlanLine(root, legacy) {
|
|
107
|
+
if (legacy === 'ignored')
|
|
108
|
+
return `raf: ${PLAN_LEGACY} est ignoré : ${PLAN_DEFAULT} existe aussi et fait foi (supprimer ou fusionner l'ancien)`;
|
|
109
|
+
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
|
+
}
|
|
83
111
|
export function run(argv, io) {
|
|
84
112
|
try {
|
|
85
113
|
// orchestrate a ses propres options (--status et --resume prennent une valeur facultative, --answer en prend deux).
|
|
@@ -123,6 +151,7 @@ function dispatch(argv, io) {
|
|
|
123
151
|
dir: { type: 'string' },
|
|
124
152
|
title: { type: 'string' },
|
|
125
153
|
after: { type: 'string' },
|
|
154
|
+
uses: { type: 'string' },
|
|
126
155
|
parent: { type: 'string' },
|
|
127
156
|
force: { type: 'boolean' },
|
|
128
157
|
reason: { type: 'string' },
|
|
@@ -130,6 +159,7 @@ function dispatch(argv, io) {
|
|
|
130
159
|
status: { type: 'string' },
|
|
131
160
|
since: { type: 'string' },
|
|
132
161
|
idle: { type: 'string' },
|
|
162
|
+
ttl: { type: 'string' },
|
|
133
163
|
session: { type: 'string' },
|
|
134
164
|
all: { type: 'boolean' },
|
|
135
165
|
depth: { type: 'string' },
|
|
@@ -146,6 +176,15 @@ function dispatch(argv, io) {
|
|
|
146
176
|
model: { type: 'string' },
|
|
147
177
|
cache: { type: 'string' },
|
|
148
178
|
sample: { type: 'string' },
|
|
179
|
+
sections: { type: 'string' },
|
|
180
|
+
slug: { type: 'string' },
|
|
181
|
+
quoi: { type: 'string' },
|
|
182
|
+
'sans-rendu': { type: 'boolean' },
|
|
183
|
+
out: { type: 'string' },
|
|
184
|
+
seuil: { type: 'string' },
|
|
185
|
+
'model-b': { type: 'string' },
|
|
186
|
+
double: { type: 'boolean' },
|
|
187
|
+
largeur: { type: 'string' },
|
|
149
188
|
until: { type: 'string' },
|
|
150
189
|
help: { type: 'boolean', short: 'h' },
|
|
151
190
|
},
|
|
@@ -163,8 +202,12 @@ function dispatch(argv, io) {
|
|
|
163
202
|
const root = gitRoot(io.cwd) ?? io.cwd;
|
|
164
203
|
// Le tour parcourt les sous-dossiers (ou, sans sous-projet, prend le dossier lui-même s'il est un projet) : il ne lit pas le plan du dossier courant.
|
|
165
204
|
if (command === 'lead') {
|
|
205
|
+
if (rest[0] === 'hold' || rest[0] === 'release')
|
|
206
|
+
return leadHold(rest, values.ttl, io);
|
|
207
|
+
if (values.ttl !== undefined)
|
|
208
|
+
throw new RafError('--ttl ne va qu\'avec lead hold');
|
|
166
209
|
if (rest[0] !== 'tour' || rest.length > 2)
|
|
167
|
-
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json]');
|
|
210
|
+
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
|
|
168
211
|
const idle = values.idle === undefined ? undefined : Number(values.idle);
|
|
169
212
|
if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
|
|
170
213
|
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
@@ -178,7 +221,7 @@ function dispatch(argv, io) {
|
|
|
178
221
|
if (values.json)
|
|
179
222
|
io.out(JSON.stringify(rows, null, 2));
|
|
180
223
|
else if (rows.length === 0)
|
|
181
|
-
io.out(`aucun projet sous ${parent} (docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
224
|
+
io.out(`aucun projet sous ${parent} (.cadence/raf.yaml, docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
182
225
|
else {
|
|
183
226
|
// L'en-tête perf (L227) : un ⚠ par ligne avant les projets ; un dossier que perf ne lit pas n'en donne pas, et ses avertissements ne sont pas repris.
|
|
184
227
|
try {
|
|
@@ -190,10 +233,23 @@ function dispatch(argv, io) {
|
|
|
190
233
|
}
|
|
191
234
|
for (const r of rows)
|
|
192
235
|
io.out(tourLine(r));
|
|
236
|
+
// les worktrees de lot que des vagues ont conservés : aucun n'est oublié en silence (L279, R19)
|
|
237
|
+
for (const l of tourWorktreeLines(rows, io.now().getTime()))
|
|
238
|
+
io.out(l);
|
|
239
|
+
// un projet sans orchestrate.test : la vague ne lancera pas la suite (L327)
|
|
240
|
+
for (const l of tourNoTestLines(rows))
|
|
241
|
+
io.out(l);
|
|
193
242
|
// les créneaux libres viennent du registre, le lead les lit au lieu de les calculer (L141)
|
|
194
|
-
const [slots] = liveLines(configuredCap(io.env));
|
|
243
|
+
const [slots] = liveLines(configuredCap(io.env), io.now());
|
|
195
244
|
if (slots)
|
|
196
245
|
io.out(slots);
|
|
246
|
+
// en face des créneaux libres, les lots prêts dont aucune ressource n'est prise (L252) ; sans vague en cours ni session tenue par le lead (L255), pas de créneau à remplir : seul --json les donne
|
|
247
|
+
const free = slots ? tourFreeLine(rows) : null;
|
|
248
|
+
if (free)
|
|
249
|
+
io.out(free);
|
|
250
|
+
// une session du lead dont le TTL est échu (L278) : rien ne l'arrête, le tour la signale (hors --json, comme les créneaux)
|
|
251
|
+
for (const l of expiredHoldLines(io.now()))
|
|
252
|
+
io.out(l);
|
|
197
253
|
// le réglage lead.ux du cadence.yaml du dossier (L197) : le lead lit ici qui pose le verdict UX, au lieu d'ouvrir le fichier
|
|
198
254
|
try {
|
|
199
255
|
if (readLeadUx(parent) === 'lead')
|
|
@@ -211,6 +267,23 @@ function dispatch(argv, io) {
|
|
|
211
267
|
return corrections(rest, values, io);
|
|
212
268
|
if (command === 'perf')
|
|
213
269
|
return perf(rest, values, today, io);
|
|
270
|
+
if (command === 'ref')
|
|
271
|
+
return refCommand(rest, values, root, io);
|
|
272
|
+
// Déplacer le plan ne lit pas le plan : pas d'avertissement « ancien emplacement » (L284).
|
|
273
|
+
if (command === 'migrate') {
|
|
274
|
+
if (rest.length > 0)
|
|
275
|
+
throw new RafError('usage : raf migrate [--dry-run]');
|
|
276
|
+
const migration = planMigration(root);
|
|
277
|
+
// --dry-run décrit ce qui serait fait avant de refuser ; sans lui, un refus n'annonce rien de fait.
|
|
278
|
+
if (values['dry-run'] || migration.unhandled.length === 0)
|
|
279
|
+
for (const l of describeMigration(migration, !!values['dry-run']))
|
|
280
|
+
io.out(l);
|
|
281
|
+
if (migration.unhandled.length > 0)
|
|
282
|
+
throw unhandledError(migration);
|
|
283
|
+
if (!values['dry-run'])
|
|
284
|
+
applyMigration(root, migration);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
214
287
|
// `spec` est aussi un verbe de raf (raf spec <lot> on|off, L186) : seuls check et qa vont à la fiche.
|
|
215
288
|
if (command === 'spec' && (rest[0] === 'check' || rest[0] === 'qa'))
|
|
216
289
|
return spec(rest, io);
|
|
@@ -223,7 +296,13 @@ function dispatch(argv, io) {
|
|
|
223
296
|
// Installer les skills ou le hook ne lit pas le plan : un cadence.yaml fautif ne doit pas l'empêcher.
|
|
224
297
|
const installing = command === 'skills' || (command === 'hook' && rest[0] === 'install');
|
|
225
298
|
const planConfig = installing ? null : readPlanConfig(configPath, root);
|
|
226
|
-
|
|
299
|
+
// Le défaut (ni --file, ni RAF_FILE, ni plan:) : .cadence/raf.yaml, ou le docs/plan/raf.yaml hérité, dont le déplacement est proposé sur stderr (L246).
|
|
300
|
+
// --file et RAF_FILE se lisent depuis le dossier courant ; le plan: de cadence.yaml, depuis la racine du dépôt où est ce fichier.
|
|
301
|
+
const chosen = values.file ?? io.env.RAF_FILE ?? planConfig?.path;
|
|
302
|
+
const fallback = chosen === undefined ? defaultPlan(root) : null;
|
|
303
|
+
const planPath = resolve(io.cwd, values.file ?? io.env.RAF_FILE ?? (planConfig?.path !== undefined ? resolve(root, planConfig.path) : fallback.path));
|
|
304
|
+
if (fallback?.legacy && !installing)
|
|
305
|
+
(io.notice ?? io.err)(legacyPlanLine(root, fallback.legacy));
|
|
227
306
|
const loadPlan = () => Plan.load(planPath, { ...planConfig?.settings, config: configPath });
|
|
228
307
|
const newsDir = resolve(io.cwd, values.dir ?? join(root, 'docs/nouveautes'));
|
|
229
308
|
// Limite DÉCLARÉE (news.publicTitleMax) : sans clé, seul « raf check » avertit ; avec, la pose d'un titre public est refusée.
|
|
@@ -247,6 +326,10 @@ function dispatch(argv, io) {
|
|
|
247
326
|
throw new RafError(`plan en lecture seule : ${configPath} décrit un plan tenu par un autre outil`);
|
|
248
327
|
const plan = Plan.create(planPath, values.project ?? basename(root), values.prefix ?? 'L', today);
|
|
249
328
|
io.out(`plan créé : ${plan.path}`);
|
|
329
|
+
if (chosen === undefined) {
|
|
330
|
+
mkdirSync(join(root, '.cadence/notes'), { recursive: true });
|
|
331
|
+
writeFileSync(join(root, '.cadence/notes/.gitkeep'), '');
|
|
332
|
+
}
|
|
250
333
|
if (ensureGitignore(root, '.playwright-mcp/'))
|
|
251
334
|
io.out('.gitignore : .playwright-mcp/');
|
|
252
335
|
if (!values['no-hook'] && gitRoot(io.cwd))
|
|
@@ -266,6 +349,8 @@ function dispatch(argv, io) {
|
|
|
266
349
|
throw new RafError('--public se pose sur un lot, pas sur une sous-tâche');
|
|
267
350
|
if (values.every !== undefined)
|
|
268
351
|
throw new RafError('--every se pose sur un lot, pas sur une sous-tâche');
|
|
352
|
+
if (values.uses !== undefined)
|
|
353
|
+
throw new RafError(`uses se pose sur un lot, pas sur une sous-tâche : ${values.parent}`);
|
|
269
354
|
id = plan.addTask(values.parent, rest.join(' '));
|
|
270
355
|
}
|
|
271
356
|
else {
|
|
@@ -278,7 +363,10 @@ function dispatch(argv, io) {
|
|
|
278
363
|
throw new RafError(`périodicité invalide : ${values.every} (un nombre entier de jours)`);
|
|
279
364
|
if (values.public !== undefined)
|
|
280
365
|
refuseLongTitle(values.public);
|
|
281
|
-
|
|
366
|
+
const uses = values.uses?.split(',').map((n) => n.trim()).filter(Boolean);
|
|
367
|
+
if (uses?.length === 0)
|
|
368
|
+
throw new RafError(invalidResource(''));
|
|
369
|
+
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, public: values.public, every, after, uses });
|
|
282
370
|
}
|
|
283
371
|
plan.save();
|
|
284
372
|
io.out(id);
|
|
@@ -336,6 +424,20 @@ function dispatch(argv, io) {
|
|
|
336
424
|
io.out(`${rest[0]} refait le ${today}`);
|
|
337
425
|
return 0;
|
|
338
426
|
}
|
|
427
|
+
case 'title': {
|
|
428
|
+
need(2, 'title <id> "nouveau titre"');
|
|
429
|
+
const plan = loadPlan();
|
|
430
|
+
plan.setTitle(rest[0], rest.slice(1).join(' '), today);
|
|
431
|
+
plan.save();
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
case 'uses': {
|
|
435
|
+
need(1, 'uses <lot> [<ressource>…]');
|
|
436
|
+
const plan = loadPlan();
|
|
437
|
+
plan.setUses(rest[0], rest.slice(1));
|
|
438
|
+
plan.save();
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
339
441
|
case 'public': {
|
|
340
442
|
need(values.clear ? 1 : 2, 'public <id> "titre public" | public <id> --clear');
|
|
341
443
|
if (values.clear && rest.length > 1)
|
|
@@ -358,6 +460,14 @@ function dispatch(argv, io) {
|
|
|
358
460
|
plan.save();
|
|
359
461
|
return 0;
|
|
360
462
|
}
|
|
463
|
+
case 'visible': {
|
|
464
|
+
if (rest.length !== 2 || (rest[1] !== 'on' && rest[1] !== 'off'))
|
|
465
|
+
throw new RafError('usage : raf visible <id> on|off');
|
|
466
|
+
const plan = loadPlan();
|
|
467
|
+
plan.setVisible(rest[0], rest[1] === 'on');
|
|
468
|
+
plan.save();
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
361
471
|
case 'spec': {
|
|
362
472
|
if (reseal)
|
|
363
473
|
return specReseal(rest.slice(1), values.reason, values.porteur, { root, loadPlan, configPath, today }, io);
|
|
@@ -397,6 +507,8 @@ function dispatch(argv, io) {
|
|
|
397
507
|
return 0;
|
|
398
508
|
}
|
|
399
509
|
io.out(`${lot.id} ${lot.status} ${lot.title}`);
|
|
510
|
+
if (lot.goal)
|
|
511
|
+
io.out(`goal : ${lot.goal}`);
|
|
400
512
|
const dates = [['créé', lot.created], ['démarré', lot.started], [lot.status === 'dropped' ? 'abandonné' : 'terminé', lot.finished]].filter(([, d]) => d);
|
|
401
513
|
if (dates.length)
|
|
402
514
|
io.out(dates.map(([k, d]) => `${k} : ${d}`).join(' '));
|
|
@@ -408,6 +520,10 @@ function dispatch(argv, io) {
|
|
|
408
520
|
io.out(`screen : ${lot.screen}`);
|
|
409
521
|
if (lot.spec)
|
|
410
522
|
io.out('spec : true');
|
|
523
|
+
if (lot.uses)
|
|
524
|
+
io.out(`uses : ${lot.uses.join(', ')}`);
|
|
525
|
+
if (lot.docsOk)
|
|
526
|
+
io.out(`docs-ok : ${lot.docsOk.date} — ${lot.docsOk.reason}`);
|
|
411
527
|
const gate = gateProgress(plan.lots(), lot);
|
|
412
528
|
if (lot.gate && gate)
|
|
413
529
|
io.out(lot.gate.invalid ? `gate : ${gateText(gate)}` : `gate : ${gate.n} lots ${gate.label} faits`);
|
|
@@ -438,21 +554,34 @@ function dispatch(argv, io) {
|
|
|
438
554
|
return now(loadPlan(), root, newsDir, today, io);
|
|
439
555
|
case 'ignore': {
|
|
440
556
|
need(1, 'ignore <sha> | "<sujet exact>" [--reason texte]');
|
|
557
|
+
if (rest.length > 1)
|
|
558
|
+
throw new RafError('un seul commit attendu ; la raison se passe par --reason "texte"');
|
|
441
559
|
const plan = loadPlan();
|
|
442
|
-
const arg = rest
|
|
560
|
+
const arg = rest[0];
|
|
443
561
|
const commits = planCommits(plan, root);
|
|
444
562
|
// Un sujet exact l'emporte sur un nom de ref homonyme (`other` à la fois sujet et branche).
|
|
445
|
-
const sha =
|
|
446
|
-
const found =
|
|
563
|
+
const sha = commits.some((c) => c.subject === arg) ? null : resolveCommit(root, arg);
|
|
564
|
+
const found = sha ? commits.find((c) => c.sha === sha) : commits.find((c) => c.subject === arg);
|
|
447
565
|
if (!found)
|
|
448
566
|
throw new RafError(`aucun commit ne correspond à « ${arg} » (sha ou sujet exact)`);
|
|
449
567
|
// Un sha est exact ; un sujet vaut pour tous les commits qui le portent.
|
|
450
|
-
const bySha =
|
|
568
|
+
const bySha = sha !== null;
|
|
451
569
|
plan.acknowledge({ ...(bySha ? { sha: found.sha, subject: found.subject } : { subject: found.subject }), date: today, ...(values.reason ? { reason: values.reason } : {}) });
|
|
452
570
|
plan.save();
|
|
453
571
|
io.out(`${bySha ? short(found) : found.subject} acquitté`);
|
|
454
572
|
return 0;
|
|
455
573
|
}
|
|
574
|
+
case 'docs-ok': {
|
|
575
|
+
need(1, 'docs-ok <lot> --reason texte');
|
|
576
|
+
if (rest.length > 1)
|
|
577
|
+
throw new RafError('un seul lot attendu ; la raison se passe par --reason "texte"');
|
|
578
|
+
const plan = loadPlan();
|
|
579
|
+
knownLot(plan, rest[0], 'docs-ok se pose sur un lot, pas sur une sous-tâche');
|
|
580
|
+
plan.setDocsOk(rest[0], values.reason ?? '', today);
|
|
581
|
+
plan.save();
|
|
582
|
+
io.out(`${rest[0]} : écart docs.sync acquitté`);
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
456
585
|
case 'check': {
|
|
457
586
|
if (values.ignored) {
|
|
458
587
|
const list = loadPlan().acknowledged;
|
|
@@ -521,8 +650,25 @@ function dispatch(argv, io) {
|
|
|
521
650
|
if (!gitRoot(io.cwd))
|
|
522
651
|
throw new RafError('deliver : à lancer dans un dépôt git');
|
|
523
652
|
const orchestrating = waveHolding(root);
|
|
524
|
-
if (orchestrating)
|
|
525
|
-
|
|
653
|
+
if (orchestrating) {
|
|
654
|
+
const refusal = `deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid})`;
|
|
655
|
+
if (values.sha === undefined)
|
|
656
|
+
throw new RafError(`${refusal} — livrer une fois les lots de ce dépôt finis (le verrou tombe dès que tous sont prêts ou rendus), ou un sha « livrable jusqu'à » de la vague (--sha)`);
|
|
657
|
+
// L277 : un sha dont tous les commits de la vague citent des lots prêts se livre au fil de l'eau ; l'état de la vague illisible ou un sha inconnu : refus.
|
|
658
|
+
const lots = waveLotsIn(cadenceHome(), orchestrating.pid, root);
|
|
659
|
+
const target = resolveCommit(root, values.sha);
|
|
660
|
+
if (lots === null)
|
|
661
|
+
throw new RafError(`${refusal} — état de la vague illisible : --sha ${values.sha} ne peut pas être vérifié`);
|
|
662
|
+
if (!target)
|
|
663
|
+
throw new RafError(`${refusal} — --sha ${values.sha} : commit introuvable`);
|
|
664
|
+
const { blocking, unreadable } = blockingLots(lots, root, target);
|
|
665
|
+
if (blocking.length > 0) {
|
|
666
|
+
throw new RafError(`${refusal} — ${target.slice(0, 7)} contient des commits de lots pas prêts : ${blocking.map((l) => `${l.project}:${l.lot} (${LABEL[l.status]})`).join(', ')} ; livrer un sha antérieur (« livrable jusqu'à » du tableau de la vague) ou attendre leur fin`);
|
|
667
|
+
}
|
|
668
|
+
if (unreadable.length > 0) {
|
|
669
|
+
throw new RafError(`${refusal} — commits illisibles (plan du projet ou dépôt) des lots pas prêts ${unreadable.map((l) => `${l.project}:${l.lot} (${LABEL[l.status]})`).join(', ')} : --sha ${values.sha} ne peut pas être vérifié ; rétablir le plan ou attendre la fin de ces lots`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
526
672
|
if (!existsSync(configPath))
|
|
527
673
|
throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans docs/deliver.md du paquet @sylad/cadence)`);
|
|
528
674
|
const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
|
|
@@ -530,7 +676,7 @@ function dispatch(argv, io) {
|
|
|
530
676
|
const articles = readDocsConfig(configPath).articles;
|
|
531
677
|
// Sans public: d'un lot visible, le site reprend le titre de sa Nouveauté la plus récente.
|
|
532
678
|
const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
|
|
533
|
-
const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, args: rest, articles, publicTitle, out: io.out, err: io.err };
|
|
679
|
+
const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, duringWave: !!orchestrating, args: rest, articles, publicTitle, out: io.out, err: io.err };
|
|
534
680
|
return deliver(ctx, realDeps(root));
|
|
535
681
|
}
|
|
536
682
|
case 'verify': {
|
|
@@ -842,15 +988,65 @@ function waveHolding(root) {
|
|
|
842
988
|
* acceptation.json, commite test + acceptation (le nouveau commit rouge), et note la raison au lot. Le verrou garde tout le reste.
|
|
843
989
|
*/
|
|
844
990
|
const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
991
|
+
/** Chemin à coller dans un shell : entre apostrophes dès qu'il contient autre chose que des caractères sûrs. */
|
|
992
|
+
const shellPath = (p) => (/^[\w@%+=:,./-]+$/.test(p) ? p : `'${p.replace(/'/g, "'\\''")}'`);
|
|
993
|
+
/** 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) {
|
|
995
|
+
const lines = open.map((w) => {
|
|
996
|
+
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
|
+
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'}`;
|
|
998
|
+
});
|
|
999
|
+
const how = open.map((w) => w.live
|
|
1000
|
+
? ` 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
|
+
: ` cd ${shellPath(w.launchDir)} && ${resumeCommand(w.wave, ...[...w.lots, ...w.others].flatMap((l) => ['--drop', l.key]))}`);
|
|
1002
|
+
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) :`,
|
|
1004
|
+
...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é :',
|
|
1006
|
+
...how,
|
|
1007
|
+
].join('\n');
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Les vagues non terminées qui gardent ouvert un lot de `repo` encore en cours au plan (L238). Une vague range son état dans le dossier d'où elle a été lancée (`RunStore.runsDir(cwd)`),
|
|
1011
|
+
* souvent un dossier PARENT du dépôt (le dossier des projets) : on remonte donc de la racine du plan jusqu'à la racine du disque.
|
|
1012
|
+
*/
|
|
1013
|
+
function unfinishedWaves(root, repo, plan) {
|
|
1014
|
+
const launchDirs = new Set();
|
|
1015
|
+
for (let d = resolve(root); !launchDirs.has(d); d = dirname(d))
|
|
1016
|
+
launchDirs.add(d);
|
|
1017
|
+
launchDirs.add(repo);
|
|
1018
|
+
const doing = new Set(plan.lots().filter((l) => l.status === 'doing').map((l) => l.id));
|
|
1019
|
+
const alive = liveWaves(cadenceHome());
|
|
1020
|
+
return [...launchDirs].flatMap((dir) => openWavesIn(dir, repo, (l) => doing.has(l.lot), alive));
|
|
1021
|
+
}
|
|
1022
|
+
/** La vague qui tient le verrou du dépôt rejoint la liste des vagues en cause, même quand l'état de ses lots ne dit rien (illisible, ou lots absents du plan) : le refus est unique et la nomme toujours. */
|
|
1023
|
+
function withHolder(open, holder, repo) {
|
|
1024
|
+
const known = open.find((w) => w.wave === holder.wave);
|
|
1025
|
+
if (known)
|
|
1026
|
+
return open.map((w) => (w === known ? { ...w, live: true, pid: holder.pid } : w));
|
|
1027
|
+
const reg = liveWaves(cadenceHome()).find((w) => w.pid === holder.pid);
|
|
1028
|
+
const keys = openLotsIn(cadenceHome(), holder.pid, repo) ?? [];
|
|
1029
|
+
return [{ wave: holder.wave, launchDir: reg?.cwd ?? '', live: true, pid: holder.pid, lots: keys.map((key) => ({ key, status: 'ouvert' })), others: [] }, ...open];
|
|
1030
|
+
}
|
|
845
1031
|
function specReseal(args, reason, porteur, ctx, io) {
|
|
846
1032
|
if (args.length !== 2 || !reason?.trim())
|
|
847
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é]');
|
|
848
1034
|
if (io.env.CADENCE_ORCHESTRATED)
|
|
849
1035
|
throw new RafError(`spec reseal refusé pendant une vague orchestrée (${io.env.CADENCE_ORCHESTRATED}) : c'est le travail du lead, hors session`);
|
|
850
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.
|
|
1037
|
+
// 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).
|
|
851
1038
|
const orchestrating = waveHolding(ctx.root);
|
|
852
|
-
if (orchestrating)
|
|
853
|
-
|
|
1039
|
+
if (orchestrating) {
|
|
1040
|
+
const repo = gitRoot(ctx.root) ?? ctx.root;
|
|
1041
|
+
let open = [];
|
|
1042
|
+
try {
|
|
1043
|
+
open = unfinishedWaves(ctx.root, repo, ctx.loadPlan());
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
// plan illisible : le verrou seul motive le refus, la vague qui le tient est nommée
|
|
1047
|
+
}
|
|
1048
|
+
throw new RafError(openWavesRefusal(withHolder(open, orchestrating, repo)));
|
|
1049
|
+
}
|
|
854
1050
|
const [lotId, test] = args;
|
|
855
1051
|
const plan = ctx.loadPlan();
|
|
856
1052
|
const lot = plan.lot(lotId);
|
|
@@ -875,6 +1071,11 @@ function specReseal(args, reason, porteur, ctx, io) {
|
|
|
875
1071
|
const cite = livre ? porteur : lotId;
|
|
876
1072
|
if (!json)
|
|
877
1073
|
throw new RafError(`${lotId} : aucune acceptation sous ${dir} (le lot n'a pas passé l'étape accept)`);
|
|
1074
|
+
// Une vague non terminée (arrêtée sur une question ou le quota) garde ouverts des lots que son `--resume` rejouera : un rescellé commité entre-temps serait rendu à l'un d'eux comme « premier commit fautif » (L238).
|
|
1075
|
+
// 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
|
+
const open = unfinishedWaves(ctx.root, repo, plan);
|
|
1077
|
+
if (open.length)
|
|
1078
|
+
throw new RafError(openWavesRefusal(open));
|
|
878
1079
|
const before = { json: readFileSync(join(repo, json), 'utf8'), test: existsSync(join(repo, test)) ? readFileSync(join(repo, test), 'utf8') : '' };
|
|
879
1080
|
let r;
|
|
880
1081
|
try {
|
|
@@ -926,6 +1127,32 @@ function specReseal(args, reason, porteur, ctx, io) {
|
|
|
926
1127
|
io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha}${r.ficheAmendee ? `, fiche amendée en ${r.ficheAmendee}` : ''} ; raison dans ${json} et en note du lot ${cite}${tracked ? '' : ' (plan à commiter)'}`);
|
|
927
1128
|
return 0;
|
|
928
1129
|
}
|
|
1130
|
+
/** Durée par défaut (minutes) d'une session du lead inscrite par `lead hold` : de quoi finir un correctif ou une revue, pas de quoi oublier la réservation toute la journée. */
|
|
1131
|
+
const HOLD_TTL_MINUTES = 60;
|
|
1132
|
+
/** Longueur maximale de l'étiquette encodée : un nom de fichier tient en 255 octets, `.json.tmp` en prend 9. */
|
|
1133
|
+
const HOLD_LABEL_MAX = 200;
|
|
1134
|
+
/** `cadence lead hold <étiquette> [--ttl minutes]` / `lead release <étiquette>` (L255) : inscrit, ou rend, une session lancée à la main par le lead, qui compte dans les créneaux libres. */
|
|
1135
|
+
function leadHold(rest, ttl, io) {
|
|
1136
|
+
const [verb, label] = rest;
|
|
1137
|
+
const usage = verb === 'hold' ? 'usage : cadence lead hold <étiquette> [--ttl minutes]' : 'usage : cadence lead release <étiquette>';
|
|
1138
|
+
if (rest.length !== 2 || !label.trim())
|
|
1139
|
+
throw new RafError(usage);
|
|
1140
|
+
if (verb === 'release') {
|
|
1141
|
+
if (ttl !== undefined)
|
|
1142
|
+
throw new RafError(`--ttl ne va qu'avec lead hold (${usage})`);
|
|
1143
|
+
io.out(releaseHold(cadenceHome(), label) ? `session « ${label} » rendue` : `aucune session « ${label} » inscrite`);
|
|
1144
|
+
return 0;
|
|
1145
|
+
}
|
|
1146
|
+
// le nom de fichier encode l'étiquette (6 octets par lettre accentuée) : au-delà, le système de fichiers refuse (ENAMETOOLONG)
|
|
1147
|
+
if (encodeURIComponent(label).length > HOLD_LABEL_MAX)
|
|
1148
|
+
throw new RafError(`étiquette trop longue (${usage})`);
|
|
1149
|
+
const minutes = ttl === undefined ? HOLD_TTL_MINUTES : Number(ttl);
|
|
1150
|
+
if (!(Number.isInteger(minutes) && minutes >= 1))
|
|
1151
|
+
throw new RafError(`--ttl invalide : ${ttl} (un nombre entier de minutes ≥ 1)`);
|
|
1152
|
+
addHold(cadenceHome(), label, minutes, io.now());
|
|
1153
|
+
io.out(`session « ${label} » inscrite pour ${minutes} min : elle compte dans les créneaux libres (cadence lead release « ${label} » à sa fin)`);
|
|
1154
|
+
return 0;
|
|
1155
|
+
}
|
|
929
1156
|
/** `cadence spec check|qa <fiche>` (L170) : code 0 valide, 1 écarts, 2 usage ou fichier illisible. `qa` imprime la section expectations.md de l'écran. */
|
|
930
1157
|
function spec(rest, io) {
|
|
931
1158
|
const sub = rest[0];
|
|
@@ -1014,8 +1241,9 @@ function readPerf(parent, today, since, warn) {
|
|
|
1014
1241
|
if (plans.size === 0 && warnings.length === 0)
|
|
1015
1242
|
warnings.push(`perf : aucun plan sous ${parent}, indicateurs du plan n/d`);
|
|
1016
1243
|
const deliveries = readProjectDeliveries(parent, [...plans.keys()]);
|
|
1017
|
-
const
|
|
1018
|
-
const
|
|
1244
|
+
const failures = readProjectFailedEntries(parent, [...plans.keys()]);
|
|
1245
|
+
const failedDeliveries = new Map([...failures].map(([name, entries]) => [name, entries.map((e) => e.day)]));
|
|
1246
|
+
const result = computePerf({ journals, plans, cache, deliveries, failedDeliveries, failures, prices, windows: perfWindows(today, since), today, since });
|
|
1019
1247
|
if (!prices)
|
|
1020
1248
|
warnings.push('perf : aucune grille de prix (cadence.yaml perf.prices), coût en euros n/d');
|
|
1021
1249
|
for (const [model, lots] of Object.entries(result.cost.withoutPrice))
|
|
@@ -1063,7 +1291,7 @@ async function sessionAll(rest, values, io) {
|
|
|
1063
1291
|
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
1064
1292
|
const projects = findProjects(io.cwd, depth);
|
|
1065
1293
|
if (projects.length === 0) {
|
|
1066
|
-
io.out(`aucun projet sous ${io.cwd} (docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
1294
|
+
io.out(`aucun projet sous ${io.cwd} (.cadence/raf.yaml, docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
1067
1295
|
return 0;
|
|
1068
1296
|
}
|
|
1069
1297
|
const argv = ['session', sub, ...(values.since === undefined ? [] : [`--since=${values.since}`]), ...(values.idle === undefined ? [] : ['--idle', values.idle])];
|
|
@@ -1079,7 +1307,7 @@ async function sessionAll(rest, values, io) {
|
|
|
1079
1307
|
continue;
|
|
1080
1308
|
}
|
|
1081
1309
|
try {
|
|
1082
|
-
code = await run(argv, { ...io, cwd: dir, err: (l) => io.out(`✗ ${l}`) });
|
|
1310
|
+
code = await run(argv, { ...io, cwd: dir, err: (l) => io.out(`✗ ${l}`), notice: io.notice ?? io.err });
|
|
1083
1311
|
}
|
|
1084
1312
|
catch (e) {
|
|
1085
1313
|
io.out(`✗ ${e instanceof Error ? e.message : String(e)}`);
|