@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/doc-keeper.md +17 -0
- package/agents/spec-writer.md +1 -0
- package/agents/ux-reviewer.md +48 -1
- package/dist/cli.js +90 -15
- package/dist/config-set.js +2 -1
- package/dist/config.js +16 -1
- package/dist/corrections.js +164 -1
- package/dist/lead.js +24 -7
- package/dist/orchestrate/cycle.js +29 -2
- package/dist/perf.js +18 -4
- package/dist/priority.js +77 -0
- package/dist/verify-gate.js +3 -2
- package/docs/cadence-hud.md +2 -0
- package/docs/corrections.md +11 -1
- package/docs/deliver.md +2 -0
- package/docs/fonctionnement-lead-dark.svg +2 -2
- package/docs/fonctionnement-lead-light.svg +2 -2
- package/docs/lead-workflow-dark.svg +2 -2
- package/docs/lead-workflow-light.svg +2 -2
- package/docs/lead.md +12 -4
- package/docs/orchestrate.md +7 -2
- package/docs/perf.md +1 -1
- package/docs/raf.md +1 -0
- package/docs/session.md +7 -0
- package/docs/skills.md +4 -0
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +8 -5
- package/templates/orchestrate/implement.md +1 -1
- package/templates/orchestrate/maquette.md +1 -1
- package/templates/orchestrate/spec.md +2 -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.
|
|
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.
|
|
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",
|
package/agents/doc-keeper.md
CHANGED
|
@@ -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.
|
package/agents/spec-writer.md
CHANGED
|
@@ -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.
|
package/agents/ux-reviewer.md
CHANGED
|
@@ -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
|
-
**
|
|
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 {
|
|
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
|
|
1523
|
-
await classifyAll(data.passes, cache,
|
|
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
|
-
|
|
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) {
|
package/dist/config-set.js
CHANGED
|
@@ -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');
|