@sylad/cadence 1.26.0 → 1.28.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.
@@ -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.26.0",
11
+ "version": "1.28.0",
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.26.0",
4
+ "version": "1.28.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
@@ -17,3 +17,20 @@ Follow the brief you are given. The method:
17
17
  5. **A document outside the repository** is yours only when the configuration says so: the brief then lists the entries of `docs.articles` (the cadence.yaml of the project: « this project is also described by this file of that repository », with the absolute path of the repository, open to you for reading). Never guess such a file, never go looking for one. Do not edit it, and never write to the plan of that repository (no `raf add`, no edit of its plan file): write a proposed lot instead — a title and a body that name the repository, the file, the passage and the sentence to change — and put it in the `resume` of your report, starting with « lot proposé ». The lead decides whether it enters that repository's plan.
18
18
 
19
19
  Never push, deliver, or run `raf done|start|note|review|ux`. Never run `git checkout` or `git switch` to another commit or branch in this repository. Do not launch subagents. Do not stop to ask: an interpretation you settle yourself goes under "choix" with the alternative you did not take. A document you could not check against the code goes under "nonVerifie". Give the report in the structured output the brief asks for.
20
+
21
+ ## Short reply
22
+
23
+ Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
24
+ small by reading a report only when it needs to). That line is the only trigger; an orchestrated
25
+ session and a call by hand without it have none.
26
+
27
+ - The full report, with its measurements, is written to that file with Bash. Use the path the brief gives
28
+ and no other: it is outside the repository, so the working tree stays as you found it. The
29
+ verdict is the last line of the file. Write the file before you reply.
30
+ - Reply to the caller with 15 lines at most, and nothing before or after them:
31
+ `Verdict: <the one-line verdict>`; `Findings:` one line per finding, most severe first, with its rank and the rule or the
32
+ measure it rests on (e.g. `README.md:12 — still says the old flag, `git show <sha>` line 40`); `Not verified: <what could not be run or seen>`
33
+ (`Not verified: none` when nothing); `Sub-tasks: <n> — <the lines to add to the plan, by their first words>`; `Report: <the path>`. A « lot proposé » for a document outside the repository goes in `Sub-tasks:` (title first), its body in the file.
34
+ - If you cannot write the file, say so on the first line of the reply and give the whole report in the reply, as if
35
+ the brief had no report file: a verdict whose evidence nobody can read is lost.
36
+ - With no `Report file:` line in the brief, give the complete report in your reply.
@@ -10,6 +10,7 @@ Follow the brief you are given. The method:
10
10
 
11
11
  1. **Read the charter first.** The project's CLAUDE.md, its CSS and variables, the neighbouring components and pages, `docs/qa/expectations.md`, the routes and API contracts already present. The mockup must look like the product: reuse what exists, invent nothing when an equivalent exists.
12
12
  2. **List the states before drawing**: empty, loading, error, and loaded when there is data. Each is an addressable section `#etat-<état>` of one static `maquette.html` (HTML + CSS, no business JavaScript). Each field of the sheet carries `data-testid="<field id>"`.
13
+ **Write the texts before drawing and render them in the mockup**, with the structure the reviewer will check: an error message says what, why, how to fix; an empty or loading state says what it is, why it is empty, how to start; an action label is a verb that names the result, never “OK” or “Validate” alone; the confirmation of a destructive action names the object and the consequence and its buttons are named by the action; one word per notion, in the sheet and in the mockup. Draw one primary element per state, one visible label per field, and take every colour, size and spacing from the product’s CSS variables (a product with no CSS variables has none to take: follow its existing values). A word or a message already in the product is kept as it is (reuse what exists).
13
14
  3. **Render and look.** Capture each state at **1440 px** and **390 px** wide with the browser tool (Playwright), named `maquette-<état>-<largeur>.png`, with the absolute capture-directory path your brief gives in front of the name (a bare name lands in the repository), and look at every capture before keeping it: a mockup you did not look at is not checked. Copy the kept captures into the spec folder.
14
15
  Before keeping a state, check its sample data: compute the weekday of every date shown (`date -d`), never write it from memory.
15
16
  4. **Write the sheet** in the format of the template of the brief, and run `cadence spec check` on it until it passes. Every rule is `sourcee` (say where it comes from) or `hypothese` (say what would change if it were wrong, in `si_faux`). Never dress a guess as a source.
@@ -24,7 +24,36 @@ On top of the usability and accessibility checks, check the **consistency mockup
24
24
  - every state of `etats` has its section `#etat-<état>`; the `masque` of an empty-state rule really hides the fields it names;
25
25
  - the units, formats and wording the sheet declares are those shown.
26
26
 
27
- **Sweep everything in the first review**, and render it as a checklist in the text of your report (one line per item, ticked or not): every state of the sheet at 1440 px and at 390 px; every rule of the sheet; contrast (WCAG 2.2 AA, measured); the real envelope of the app (usable width, side bar, header — measured in the neighbouring pages, not assumed); gutters; empty or stretched panels; holes in the layout. What you cannot verify (a state you cannot capture, an ambiguous rule) goes in `nonVerifie`, not ticked. Report ALL the blocking and major findings in the first report: the mockup is reviewed up to `orchestrate.maquetteReviews` times (3 by default), but a later review that finds a major on a point nobody changed counts as a review defect in the wave journal. From the second review on, check the points of the previous one first, run the sweep again, and set `inchange: true` on each blocking or major finding about a point the spec writer did not change and the previous review had not raised (leave the field out otherwise).
27
+ **Criteria added in mockup mode** — in this mode only (conformity mode and the review of an existing screen keep their text). Each one carries its named rule or the value to count; the checks of the method below (contrast, targets, keyboard, focus, alternatives, colour, reflow, data display) are already written there and are not repeated here.
28
+
29
+ **Family: visual hierarchy**
30
+
31
+ - one primary element per state: count the primary actions of each state, more than one is a finding (Nielsen 8);
32
+
33
+ **Family: consistency with the product tokens**
34
+
35
+ - colour, size and spacing values of `maquette.html` that do not come from the product’s CSS variables: report the count as one finding, not one per value (Nielsen 4); a product with no CSS variables gets no finding, write it in `nonVerifie`;
36
+
37
+ **Family: quality of the texts**
38
+
39
+ - an error message says what, why, how to fix (Nielsen 9);
40
+ - an empty or loading state says what it is, why it is empty, how to start (Nielsen 1);
41
+ - an action label is a verb that names the result, never “OK” or “Validate” alone (Nielsen 2 and 4);
42
+ - the confirmation of a destructive action names the object and the consequence, and its buttons are named by the action (Nielsen 5);
43
+ - one word per notion: count of distinct labels used for one notion across the states and the sheet (Nielsen 4);
44
+
45
+ **Family: complementary accessibility**
46
+
47
+ - focus order follows the reading order (WCAG 2.4.3);
48
+ - name, role, value and a visible label for every field (WCAG 4.1.2, 3.3.2, 1.3.1);
49
+ - an error is identified by text, not by colour or an icon alone (WCAG 3.3.1);
50
+ - text enlarged to 200 % loses no content or function (WCAG 1.4.4);
51
+
52
+ What a static mockup cannot show, such as what a real screen reader announces, goes in `nonVerifie`, not ticked, and is never replaced by a finding.
53
+
54
+ Severity keeps the scale below: an accessibility (WCAG) gap is blocking or major as before; a text, hierarchy or token defect is major only if it misleads or slows the task (an error message without the fix, two primary actions, an off-token value that changes the identity), otherwise minor.
55
+
56
+ **Sweep everything in the first review**, and render it as a checklist in the text of your report (one line per item, ticked or not): every state of the sheet at 1440 px and at 390 px; one line per family of the added criteria, per state at 1440 px and 390 px; every rule of the sheet; contrast (WCAG 2.2 AA, measured); the real envelope of the app (usable width, side bar, header — measured in the neighbouring pages, not assumed); gutters; empty or stretched panels; holes in the layout. What you cannot verify (a state you cannot capture, an ambiguous rule) goes in `nonVerifie`, not ticked. Report ALL the blocking and major findings in the first report: the mockup is reviewed up to `orchestrate.maquetteReviews` times (3 by default), but a later review that finds a major on a point nobody changed counts as a review defect in the wave journal. From the second review on, check the points of the previous one first, run the sweep again, and set `inchange: true` on each blocking or major finding about a point the spec writer did not change and the previous review had not raised (leave the field out otherwise).
28
57
 
29
58
  Check the **sample data** too: recompute the weekday of every date shown (a command such as `date -d`), do not trust the one written. A wrong sample datum (weekday, date, figure that does not add up) is **minor**, never major: it changes no field, rule or state of the sheet, so it does not send the mockup back to the spec writer.
30
59
 
@@ -69,3 +98,21 @@ A short report:
69
98
  - Judge on taste, or propose a new visual identity.
70
99
  - Report a finding you have not seen in a capture or measured.
71
100
  - Edit code, commit, or record `raf ux` yourself: the session that owns the lot does it.
101
+
102
+ ## Short reply
103
+
104
+ Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
105
+ small by reading a report only when it needs to). That line is the only trigger; an orchestrated
106
+ session and a call by hand without it have none.
107
+
108
+ - The full report, with its measurements, is written to that file with Bash. Use the path the brief gives
109
+ and no other: it is outside the repository, so the working tree stays as you found it. The
110
+ verdict is the last line of the file. Write the file before you reply.
111
+ - Reply to the caller with 15 lines at most, and nothing before or after them:
112
+ `Verdict: <the one-line verdict>`; `Findings:` one line per finding, most severe first, with its rank and the rule or the
113
+ measure it rests on (e.g. `major — /players: target 18×18 px, WCAG 2.5.8`); `Not verified: <what could not be run or seen>`
114
+ (`Not verified: none` when nothing); `Sub-tasks: <n> — <the lines to add to the plan, by their first words>`; `Report: <the path>`.
115
+ Add a last line `Captures: <the absolute path of the capture folder>` (the folder the brief names): the report file does not replace the captures, which the lead opens itself.
116
+ - If you cannot write the file, say so on the first line of the reply and give the whole report in the reply, as if
117
+ the brief had no report file: a verdict whose evidence nobody can read is lost.
118
+ - With no `Report file:` line in the brief, give the complete report in your reply.
package/dist/cli.js CHANGED
@@ -22,14 +22,15 @@ import { dueLine, isRecurring, recurringByDue } from './recurring.js';
22
22
  import { schedule } from './schedule.js';
23
23
  import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
24
24
  import { configSet } from './config-set.js';
25
- import { findProjects, readDepth, leadTour, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
25
+ import { movePriority } from './priority.js';
26
+ import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
26
27
  import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
27
28
  import { addHold, releaseHold } from './orchestrate/registry.js';
28
29
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
29
30
  import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
30
31
  import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
31
32
  import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
32
- import { classifyAll, measure, ollamaClassifier, readCache, readJournals, readRuns, renderReport, sampleKeys, serializeCache } from './corrections.js';
33
+ import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
33
34
  import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
34
35
  import { findAcceptation, parseAcceptation, redAcceptations, resealAcceptation, resealForGreen, runAcceptation, setVert } from './orchestrate/accept.js';
35
36
  import { refCommand } from './ref/command.js';
@@ -53,6 +54,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
53
54
  raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
54
55
  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 ; rouvre un lot done ou dropped (→ todo, finished retiré, note « rouvert ») — la seule façon de rouvrir, archivé compris (il revient dans le fichier du plan)
55
56
  raf note <id> "texte"
57
+ --verbose avec done, ux, review, note, move ou public : imprime la ligne « id état titre entier » du lot touché ; avec hook post-commit : le titre entier (sinon 60 caractères au plus)
56
58
  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
57
59
  raf did <id> ["texte"] un lot récurrent (--every) a été refait : le « dû depuis N j » repart de aujourd'hui
58
60
  raf ux enable revue UX obligatoire avant « done » pour les lots --visible
@@ -74,8 +76,9 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
74
76
  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]
75
77
  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
76
78
  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
79
+ cadence lead priority <projet> --before|--after <projet> [dossier] [--depth n] range un projet dans la liste priority: du cadence.yaml du dossier (ordre du tour, de orchestrate --continue et du panneau des lots), commentaires gardés ; ne commite pas
77
80
  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
78
- 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)
81
+ cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--cache-sous-causes 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)
79
82
  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
80
83
  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)
81
84
  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
@@ -197,9 +200,11 @@ function dispatch(argv, io) {
197
200
  ignored: { type: 'boolean' },
198
201
  notes: { type: 'boolean' },
199
202
  json: { type: 'boolean' },
203
+ verbose: { type: 'boolean' },
200
204
  classify: { type: 'boolean' },
201
205
  model: { type: 'string' },
202
206
  cache: { type: 'string' },
207
+ 'cache-sous-causes': { type: 'string' },
203
208
  sample: { type: 'string' },
204
209
  sections: { type: 'string' },
205
210
  slug: { type: 'string' },
@@ -229,10 +234,12 @@ function dispatch(argv, io) {
229
234
  if (command === 'lead') {
230
235
  if (rest[0] === 'hold' || rest[0] === 'release')
231
236
  return leadHold(rest, values.ttl, io, today);
237
+ if (rest[0] === 'priority')
238
+ return leadPriority(rest, values.before, values.after, values.depth, io);
232
239
  if (values.ttl !== undefined)
233
240
  throw new RafError('--ttl ne va qu\'avec lead hold');
234
241
  if (rest[0] !== 'tour' || rest.length > 2)
235
- throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
242
+ throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead priority <projet> --before|--after <projet> [dossier] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
236
243
  const idle = values.idle === undefined ? undefined : Number(values.idle);
237
244
  if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
238
245
  throw new RafError(`--idle invalide : ${values.idle}`);
@@ -459,6 +466,7 @@ function dispatch(argv, io) {
459
466
  io.out(`${rest[0]} → ${status}`);
460
467
  if (status === 'done')
461
468
  warnIntegrating(root, io.err);
469
+ touchedLot(plan, rest[0], values.verbose, io);
462
470
  if (status === 'done' && !rest[0].includes('/')) {
463
471
  const lot = plan.lot(rest[0]);
464
472
  if (lot.visible && !loadEntries(newsDir).some((e) => e.lots.includes(lot.id))) {
@@ -514,6 +522,7 @@ function dispatch(argv, io) {
514
522
  plan.note(rest[0], rest.slice(1).join(' '), today);
515
523
  plan.save();
516
524
  warnIntegrating(root, io.err);
525
+ touchedLot(plan, rest[0], values.verbose, io);
517
526
  return 0;
518
527
  }
519
528
  case 'did': {
@@ -550,6 +559,7 @@ function dispatch(argv, io) {
550
559
  if (holding)
551
560
  throw new RafError(`move : orchestration en cours (vague ${holding.wave}, pid ${holding.pid}) — attendre que les lots de ce dépôt soient finis (le verrou tombe dès que tous sont prêts ou rendus), puis relancer`);
552
561
  plan.save();
562
+ touchedLot(plan, rest[0], values.verbose, io);
553
563
  return 0;
554
564
  }
555
565
  case 'public': {
@@ -561,6 +571,7 @@ function dispatch(argv, io) {
561
571
  refuseLongTitle(rest.slice(1).join(' '));
562
572
  plan.setPublic(rest[0], values.clear ? null : rest.slice(1).join(' '));
563
573
  plan.save();
574
+ touchedLot(plan, rest[0], values.verbose, io);
564
575
  return 0;
565
576
  }
566
577
  case 'screen': {
@@ -599,7 +610,7 @@ function dispatch(argv, io) {
599
610
  }
600
611
  case 'ux':
601
612
  case 'review':
602
- return gate(command, rest, loadPlan, root, today, io);
613
+ return gate(command, rest, loadPlan, root, today, io, !!values.verbose);
603
614
  case 'commits': {
604
615
  need(1, 'commits <lot>');
605
616
  const plan = loadPlan();
@@ -748,7 +759,7 @@ function dispatch(argv, io) {
748
759
  return 0;
749
760
  }
750
761
  if (rest[0] === 'post-commit')
751
- return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart);
762
+ return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart, !!values.verbose);
752
763
  if (rest[0] === 'pre-commit')
753
764
  return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
754
765
  if (rest[0] === 'pre-push')
@@ -853,7 +864,7 @@ const GATES = {
853
864
  review: { on: 'revue de code obligatoire pour les lots à commits', already: 'revue de code déjà active' },
854
865
  };
855
866
  /** « raf ux … » et « raf review … » ont la même forme : activer la porte, ou noter le verdict d'un lot. */
856
- function gate(kind, rest, loadPlan, root, today, io) {
867
+ function gate(kind, rest, loadPlan, root, today, io, verbose) {
857
868
  if (rest.length < 1)
858
869
  throw new RafError(`usage : raf ${kind} enable | ${kind} <lot> "verdict"`);
859
870
  const plan = loadPlan();
@@ -878,8 +889,19 @@ function gate(kind, rest, loadPlan, root, today, io) {
878
889
  plan.recordReview(rest[0], verdict, today, plan.readonly ? null : (lotWork(plan, root, rest[0])[0]?.sha ?? null), neighbours?.repos.length ? repoShas(plan, neighbours.repos, rest[0]) : undefined);
879
890
  }
880
891
  plan.save();
892
+ touchedLot(plan, rest[0], verbose, io);
881
893
  return 0;
882
894
  }
895
+ /** Un titre de lot n'est jamais rendu en entier par défaut (L453) : ~300 caractères répétés à chaque commande, chaque commit de plan. */
896
+ function shortTitle(title, verbose) {
897
+ return verbose ? title : truncate(title, TITLE_MAX);
898
+ }
899
+ /** `--verbose` (L453) : la ligne `id état titre entier` du lot touché par `done`, `ux`, `review`, `note`, `move`, `public` ; sans lui, rien de plus (une sous-tâche n'a pas de ligne). */
900
+ function touchedLot(plan, id, verbose, io) {
901
+ const lot = verbose ? plan.lots().find((l) => l.id === id) : undefined;
902
+ if (lot)
903
+ io.out(`${lot.id} ${lot.status} ${lot.title}`);
904
+ }
883
905
  function knownLot(plan, id, subtask) {
884
906
  const lot = plan.lots().find((l) => l.id === id);
885
907
  if (!lot)
@@ -1020,7 +1042,7 @@ function preCommit(load, mode, root, today, io) {
1020
1042
  }
1021
1043
  return 0;
1022
1044
  }
1023
- function postCommit(load, newsDir, root, today, io, mode) {
1045
+ function postCommit(load, newsDir, root, today, io, mode, verbose) {
1024
1046
  if (!load)
1025
1047
  return 0;
1026
1048
  try {
@@ -1049,7 +1071,7 @@ function postCommit(load, newsDir, root, today, io, mode) {
1049
1071
  else if (lot.status === 'todo' && !planning)
1050
1072
  io.err(`raf: ${r.lot} est encore todo${plan.readonly ? '' : ` — raf start ${r.lot}`}`);
1051
1073
  else
1052
- io.err(`raf: ${r.lot} (${lot.status}) ${lot.title}`);
1074
+ io.err(`raf: ${r.lot} (${lot.status}) ${shortTitle(lot.title, verbose)}`);
1053
1075
  }
1054
1076
  const entries = loadEntries(newsDir);
1055
1077
  for (const id of new Set(refs.map((r) => r.lot))) {
@@ -1444,6 +1466,23 @@ function specReseal(args, reason, porteur, ctx, io) {
1444
1466
  const HOLD_TTL_MINUTES = 60;
1445
1467
  /** Longueur maximale de l'étiquette encodée : un nom de fichier tient en 255 octets, `.json.tmp` en prend 9. */
1446
1468
  const HOLD_LABEL_MAX = 200;
1469
+ /** `cadence lead priority <projet> --before|--after <projet> [dossier]` (L449) : range un projet dans la liste `priority:` du cadence.yaml du dossier (celui du tour), sans modèle ni commit. */
1470
+ function leadPriority(rest, before, after, depth, io) {
1471
+ const usage = 'usage : cadence lead priority <projet> --before|--after <projet> [dossier] [--depth n]';
1472
+ if (rest.length < 2 || rest.length > 3 || (before === undefined) === (after === undefined))
1473
+ throw new RafError(usage);
1474
+ const place = before !== undefined ? 'before' : 'after';
1475
+ const levels = depth === undefined ? undefined : Number(depth);
1476
+ if (levels !== undefined && !(Number.isInteger(levels) && levels >= 1))
1477
+ throw new RafError(`--depth invalide : ${depth} (un entier ≥ 1)`);
1478
+ const parent = resolve(io.cwd, rest[2] ?? '.');
1479
+ if (!existsSync(parent))
1480
+ throw new RafError(`lead priority : dossier introuvable : ${parent}`);
1481
+ const target = (before ?? after);
1482
+ const moved = movePriority(parent, rest[1], place, target, levels);
1483
+ io.out(moved.changed ? `${rest[1]} placé ${place === 'before' ? 'avant' : 'après'} ${target} dans priority: de ${join(parent, 'cadence.yaml')} (ordre : ${moved.order.join(' > ')}) — le fichier est modifié, à commiter` : `${rest[1]} est déjà ${place === 'before' ? 'avant' : 'après'} ${target} : rien à changer`);
1484
+ return 0;
1485
+ }
1447
1486
  /** `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. */
1448
1487
  function leadHold(rest, ttl, io, today) {
1449
1488
  const [verb, label] = rest;
@@ -1502,7 +1541,7 @@ function spec(rest, io) {
1502
1541
  /** `cadence corrections` (L165) : la mesure de référence des passes de correction d'une vague, lue dans <dossier>/.cadence/runs. */
1503
1542
  async function corrections(rest, values, io) {
1504
1543
  if (rest.length > 1)
1505
- throw new RafError('usage : cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--sample n] [--until date] [--since date] [--json]');
1544
+ throw new RafError('usage : cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--cache-sous-causes fichier] [--sample n] [--until date] [--since date] [--json]');
1506
1545
  const parent = resolve(io.cwd, rest[0] ?? '.');
1507
1546
  const runsDir = join(parent, '.cadence', 'runs');
1508
1547
  if (!existsSync(runsDir))
@@ -1517,14 +1556,33 @@ async function corrections(rest, values, io) {
1517
1556
  const model = values.model ?? 'mistral-small3.2:24b';
1518
1557
  const cacheFile = resolve(io.cwd, values.cache ?? join(parent, '.cadence', 'corrections.json'));
1519
1558
  const data = readRuns(runsDir, values.until, values.since);
1559
+ const sousFile = resolve(io.cwd, values['cache-sous-causes'] ?? join(parent, '.cadence', 'corrections-sous-causes.json'));
1520
1560
  const cache = readCache(cacheFile);
1561
+ // R5 (L447) : le cache des sous-causes illisible sort avec le code 1 de la fiche (corrections.json illisible sort avec 2, par `failure`).
1562
+ let sousCache;
1563
+ try {
1564
+ sousCache = readSousCache(sousFile);
1565
+ }
1566
+ catch (e) {
1567
+ if (!(e instanceof RafError))
1568
+ throw e;
1569
+ io.err(`raf: ${e.message}`);
1570
+ return 1;
1571
+ }
1521
1572
  if (values.classify) {
1522
- const classify = ollamaClassifier({ url: io.env.OLLAMA_HOST ?? 'http://localhost:11434', model });
1523
- await classifyAll(data.passes, cache, classify, model, (done, total) => {
1573
+ const url = io.env.OLLAMA_HOST ?? 'http://localhost:11434';
1574
+ await classifyAll(data.passes, cache, ollamaClassifier({ url, model }), model, (done, total) => {
1524
1575
  if (done % 25 === 0 || done === total)
1525
1576
  io.err(`classement : ${done}/${total}`);
1526
1577
  }, { every: 10, save: () => writeFileSync(cacheFile, serializeCache(cache)) });
1527
1578
  writeFileSync(cacheFile, serializeCache(cache));
1579
+ const saveSous = () => writeFileSync(sousFile, serializeSousCache(sousCache));
1580
+ const added = await classifySous(data.passes, cache, sousCache, ollamaSousClassifier({ url, model }), model, (done, total) => {
1581
+ if (done % 25 === 0 || done === total)
1582
+ io.err(`sous-causes : ${done}/${total}`);
1583
+ }, { every: 10, save: saveSous });
1584
+ if (added > 0)
1585
+ saveSous();
1528
1586
  }
1529
1587
  if (sample !== undefined) {
1530
1588
  const byKey = new Map(data.passes.map((p) => [p.key, p]));
@@ -1541,7 +1599,8 @@ async function corrections(rest, values, io) {
1541
1599
  return 0;
1542
1600
  }
1543
1601
  const m = measure(data, cache);
1544
- io.out(values.json ? JSON.stringify(m, null, 2) : renderReport(m));
1602
+ const bugCode = bugCodeOf(bugCodePasses(data.passes, cache), sousCache);
1603
+ io.out(values.json ? JSON.stringify(bugCode ? { ...m, bugCode } : m, null, 2) : renderReport(m, bugCode));
1545
1604
  return 0;
1546
1605
  }
1547
1606
  /**
@@ -1558,17 +1617,31 @@ function readPerf(parent, today, since, warn) {
1558
1617
  const cache = readCache(join(parent, '.cadence', 'corrections.json'));
1559
1618
  const journals = readJournals(runsDir);
1560
1619
  const warnings = [];
1620
+ // Les sous-causes (L447) n'arrêtent pas perf : un cache illisible avertit et la ligne est omise.
1621
+ const sousFile = join(parent, '.cadence', 'corrections-sous-causes.json');
1622
+ let sousCache;
1623
+ let sousWarning;
1624
+ try {
1625
+ sousCache = readSousCache(sousFile);
1626
+ }
1627
+ catch (e) {
1628
+ if (!(e instanceof RafError))
1629
+ throw e;
1630
+ sousWarning = `perf : ${sousFile} illisible, sous-causes n/d`;
1631
+ }
1561
1632
  const plans = readPlans(parent, (l) => warnings.push(l));
1562
1633
  if (plans.size === 0 && warnings.length === 0)
1563
1634
  warnings.push(`perf : aucun plan sous ${parent}, indicateurs du plan n/d`);
1564
1635
  const deliveries = readProjectDeliveries(parent, [...plans.keys()]);
1565
1636
  const failures = readProjectFailedEntries(parent, [...plans.keys()]);
1566
1637
  const failedDeliveries = new Map([...failures].map(([name, entries]) => [name, entries.map((e) => e.day)]));
1567
- const result = computePerf({ journals, plans, cache, deliveries, failedDeliveries, failures, prices, windows: perfWindows(today, since), today, since });
1638
+ const result = computePerf({ journals, plans, cache, ...(sousCache ? { sousCache } : {}), deliveries, failedDeliveries, failures, prices, windows: perfWindows(today, since), today, since });
1568
1639
  if (!prices)
1569
1640
  warnings.push('perf : aucune grille de prix (cadence.yaml perf.prices), coût en euros n/d');
1570
1641
  for (const [model, lots] of Object.entries(result.cost.withoutPrice))
1571
1642
  warnings.push(`perf : modèle sans prix dans perf.prices : ${model}, coût n/d pour ${lots} lot(s)`);
1643
+ if (sousWarning)
1644
+ warnings.push(sousWarning);
1572
1645
  warnings.forEach(warn);
1573
1646
  return result;
1574
1647
  }
@@ -1667,12 +1740,14 @@ async function sessionAll(rest, values, io) {
1667
1740
  }
1668
1741
  return worst;
1669
1742
  }
1670
- /** Le contexte de la session de `--session <id>`, sinon de `CLAUDE_CODE_SESSION_ID`, sinon (variable absente) de la seule session du dossier courant, tel que la bande cadence-hud le publie ; illisible = code 2, à lire comme « au seuil ». */
1743
+ /** Le contexte (et sa position par rapport à `lead.contextThreshold`, en tokens) de la session de `--session <id>`, sinon de `CLAUDE_CODE_SESSION_ID`, sinon (variable absente) de la seule session du dossier courant, tel que la bande cadence-hud le publie ; illisible = code 2, à lire comme « au seuil ». */
1671
1744
  function sessionContext(io, session) {
1672
1745
  const r = readHudContext(io.env, io.now().getTime(), { cwd: io.cwd, session });
1673
1746
  if (!r.ok)
1674
1747
  throw new RafError(`session context : contexte illisible — ${r.reason} ; à traiter comme au seuil`);
1675
1748
  io.out(`ctx ${Math.round(r.ctx.percent)} % (${r.ctx.tokens}/${r.ctx.window})`);
1749
+ const threshold = readLeadContextThreshold(io.cwd);
1750
+ io.out(`seuil ${threshold} tokens : ${r.ctx.tokens >= threshold ? 'atteint' : 'sous le seuil'}`);
1676
1751
  return 0;
1677
1752
  }
1678
1753
  function session([sub, ...args], ctx, values) {
@@ -6,7 +6,7 @@ import { isNode, isScalar, parse, parseDocument } from 'yaml';
6
6
  import { defaultPlan, readDocsConfig, readHookConfig, readMigrateConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readReferencesConfig, readSessionConfig } from './config.js';
7
7
  import { parseDeliverConfig } from './deliver.js';
8
8
  import { commitPaths, dirtyPaths, gitRoot, restorePaths } from './git.js';
9
- import { findProjects, isProject, readDepth, readLeadUx } from './lead.js';
9
+ import { findProjects, isProject, readDepth, readLeadContextThreshold, readLeadUx } from './lead.js';
10
10
  import { readPriority } from './orchestrate/continue.js';
11
11
  import { Plan, RafError } from './plan.js';
12
12
  const KEY = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
@@ -26,6 +26,7 @@ export function validateConfig(file, root) {
26
26
  readReferencesConfig(file);
27
27
  readMigrateConfig(file);
28
28
  readLeadUx(dirname(file));
29
+ readLeadContextThreshold(dirname(file));
29
30
  readDepth(dirname(file));
30
31
  readPriority(dirname(file));
31
32
  const text = readFileSync(file, 'utf8');
package/dist/config.js CHANGED
@@ -338,7 +338,7 @@ export function readVerifyConfig(file) {
338
338
  return { inPlace: v.inPlace };
339
339
  }
340
340
  const REVIEW_KEYS = ['threshold', 'light', 'full'];
341
- const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
341
+ const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'gate', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
342
342
  /** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
343
343
  export function readOrchestrateConfig(file) {
344
344
  const config = { precheck: true, maquette: true, maquetteReviews: 3, fiche: true, precheckLocal: false, prodAccess: false, worktrees: false, spec: { dir: 'docs/specs' }, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', spec: 'medium', maquette: 'high', fiche: 'medium', accept: 'medium', implement: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
@@ -392,6 +392,21 @@ export function readOrchestrateConfig(file) {
392
392
  throw bad('worktreeDeps : liste de chemins relatifs au dépôt, sans « .. » (node_modules, .venv…)');
393
393
  config.worktreeDeps = deps.map((x) => x.trim().replace(/\/$/, ''));
394
394
  }
395
+ if (o.gate != null) {
396
+ if (!isObject(o.gate))
397
+ throw bad('gate : objet attendu (gate: { readsPlan: true })');
398
+ for (const k of Object.keys(o.gate))
399
+ if (k !== 'readsPlan')
400
+ throw bad(`gate.${k} inconnu (attendu : readsPlan)`);
401
+ const r = o.gate.readsPlan;
402
+ const msg = 'gate.readsPlan : true, false ou liste de chemins de tests relatifs au dépôt, sans « .. »';
403
+ if (r === true)
404
+ config.gate = { readsPlan: true };
405
+ else if (Array.isArray(r) && r.length && r.every((x) => typeof x === 'string' && x.trim() && !isAbsolute(x) && !x.split(/[\\/]/).includes('..')))
406
+ config.gate = { readsPlan: r.map((x) => x.trim()) };
407
+ else if (r !== false)
408
+ throw bad(msg);
409
+ }
395
410
  if (o.maquette != null) {
396
411
  if (typeof o.maquette !== 'boolean')
397
412
  throw bad('maquette : true ou false attendu');