@sylad/cadence 1.9.0 → 1.11.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.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +24 -67
  4. package/agents/doc-keeper.md +2 -2
  5. package/agents/precheck-reader.md +1 -1
  6. package/agents/reference-analyst.md +25 -0
  7. package/agents/reference-verifier.md +19 -0
  8. package/dist/audit.js +3 -1
  9. package/dist/check.js +19 -3
  10. package/dist/cli.js +28 -5
  11. package/dist/corrections.js +2 -1
  12. package/dist/deliver.js +47 -16
  13. package/dist/lead.js +8 -0
  14. package/dist/orchestrate/briefs.js +19 -1
  15. package/dist/orchestrate/changelog-merge.js +94 -0
  16. package/dist/orchestrate/command.js +108 -60
  17. package/dist/orchestrate/cycle.js +226 -36
  18. package/dist/orchestrate/integrate.js +52 -8
  19. package/dist/orchestrate/launch.js +14 -4
  20. package/dist/orchestrate/no-test.js +41 -0
  21. package/dist/orchestrate/registry.js +23 -0
  22. package/dist/orchestrate/snapshot.js +29 -1
  23. package/dist/orchestrate/state.js +14 -9
  24. package/dist/orchestrate/table.js +13 -1
  25. package/dist/perf.js +109 -7
  26. package/dist/plan.js +20 -1
  27. package/dist/proc.js +6 -0
  28. package/dist/ref/add.js +24 -11
  29. package/dist/ref/analyse.js +220 -0
  30. package/dist/ref/command.js +15 -1
  31. package/dist/ref/diff.js +262 -0
  32. package/dist/ref/evaluate.js +2 -2
  33. package/dist/ref/figures.js +86 -13
  34. package/dist/ref/pdf.js +20 -8
  35. package/dist/ref/regard.js +102 -0
  36. package/dist/ref/session.js +34 -0
  37. package/dist/state.js +17 -2
  38. package/docs/deliver.md +20 -4
  39. package/docs/fonctionnement.md +3 -3
  40. package/docs/lead.md +8 -4
  41. package/docs/news.md +6 -0
  42. package/docs/orchestrate.md +71 -40
  43. package/docs/overview.md +65 -1
  44. package/docs/perf.md +3 -2
  45. package/docs/raf.md +2 -1
  46. package/docs/ref.md +102 -3
  47. package/docs/releasing.md +3 -3
  48. package/docs/skills.md +14 -1
  49. package/docs/spec.md +1 -1
  50. package/docs/whats-new.md +31 -1
  51. package/package.json +1 -1
  52. package/skills/deliver/SKILL.md +25 -2
  53. package/skills/lead/SKILL.md +6 -6
  54. package/skills/orchestrate/SKILL.md +39 -0
  55. package/templates/orchestrate/fix-docs.md +2 -2
  56. package/templates/orchestrate/implement.md +1 -1
  57. package/templates/orchestrate/precheck.md +1 -1
@@ -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.9.0",
11
+ "version": "1.11.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.9.0",
4
+ "version": "1.11.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -1,89 +1,46 @@
1
1
  # cadence
2
2
 
3
- A small working method that lives in your repository: a versioned plan linked to your commits, waves of fresh AI sessions driven by a program, and deliveries proved by their effect.
4
- Solo developers and AI-assisted sessions lose track of *what is left to do* and *what each commit was for*; cadence keeps both in plain files next to the code.
5
- **raf** is the plan, **news** the changelog with screenshots, **session** the facts to open and close a day, **deliver** waits for the CI, deploys, then checks the effect.
6
- **orchestrate** runs a lot through spec, review, code and UX passes, each in its own session (with a short `TMPDIR` of its own, `<os tmpdir>/cadence-<hash>`, outside the repository and the wave's state folder, removed at the end of each pass of the lot; short because Chrome's Unix sockets live there and are limited to 107 characters); the **lead** decides, re-verifies and delivers. An **experimental** worktree mode (`orchestrate.worktrees: true` in `cadence.yaml`: independent lots of one repository in parallel, each in its own git worktree) is built but **refused**, on start and on `--resume`, without `CADENCE_EXPERIMENTAL_WORKTREES=1`; lots L299–L301 must be done before it is enabled ([docs/orchestrate.md](docs/orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279)).
7
- Four [Claude Code](https://claude.com/claude-code) skills, three reviewer agents and a status-band plugin (`cadence-hud`) turn it into rituals.
8
-
9
- <picture>
10
- <source media="(prefers-color-scheme: dark)" srcset="docs/cycle-vague-dark.svg">
11
- <img alt="A lot goes through spec, mockup, accept, implement, review, ux and deliver; the lead picks the lots, answers the questions, re-verifies and delivers" src="docs/cycle-vague-light.svg">
12
- </picture>
3
+ A small working method that lives in your repository: a versioned plan linked to your commits,
4
+ waves of fresh AI sessions driven by a program, and deliveries proved by their effect.
5
+ It is for solo developers and AI-assisted sessions who lose track of *what is left to do* and *what each commit was for*:
6
+ cadence keeps both in plain files next to the code, and [Claude Code](https://claude.com/claude-code) skills turn it into rituals.
13
7
 
14
8
  ## Start
15
9
 
16
10
  ```sh
17
- npm install -g @sylad/cadence # or: npx -p @sylad/cadence raf …
18
- raf init # .cadence/raf.yaml + .cadence/notes/ + a post-commit hook
19
- # (a legacy docs/plan/raf.yaml is still read until moved: `raf migrate` moves it, see docs/raf.md)
11
+ npm install -g @sylad/cadence
12
+ raf init
20
13
  raf add "My first lot" --estimate 1
21
14
  ```
22
15
 
23
- Then `raf start L1`, commit with `feat(L1): …`, `raf now` to see where you are, `raf done L1` when it is finished. A title gone stale after a change of scope is replaced with `raf title L1 "new title"` (any status, the old one is kept in a dated note). A lot that occupies a resource shared between projects says so with `raf add --uses banc` or `raf uses L1 banc`, the folder's `cadence.yaml` lists the resources (`resources: [banc]`), and `raf check` refuses a name that is not declared ([docs/raf.md](docs/raf.md#shared-resources-uses-and-resources)); `cadence lead hold <label>` / `lead release <label>` declare the sessions the lead starts by hand so that they are subtracted from the free slots of `orchestrate --status` and `lead tour` ([docs/lead.md](docs/lead.md#lead-hold-and-release)); `cadence lead tour` and `orchestrate --continue` list, next to the free slots, the ready lots whose resources no `doing` lot (in any project sharing them, at every `depth:` of the declaring `cadence.yaml`) or live wave holds (`--json`: `free`); the `lead` skill reads that list again each time an agent comes back or a lot is blocked, before waiting).
16
+ Then `raf start L1`, commit with `feat(L1): …`, `raf now` to see where you are, `raf done L1` when it is finished.
24
17
 
25
- ## Install as a Claude Code plugin
18
+ <picture>
19
+ <source media="(prefers-color-scheme: dark)" srcset="docs/cycle-vague-dark.svg">
20
+ <img alt="A lot goes through spec, mockup, accept, implement, review, ux and deliver; the lead picks the lots, answers the questions, re-verifies and delivers" src="docs/cycle-vague-light.svg">
21
+ </picture>
26
22
 
27
- The repository is its own [Claude Code](https://claude.com/claude-code) plugin marketplace. In a Claude Code session:
23
+ ## Install as a Claude Code plugin
28
24
 
29
25
  ```
30
26
  /plugin marketplace add Sylad/cadence
31
- /plugin install cadence@cadence # skills /cadence:session-start, session-close, deliver, lead + the three reviewer agents
32
- /plugin install cadence-hud@cadence # the status band above the prompt
27
+ /plugin install cadence@cadence # skills and reviewer agents; needs @sylad/cadence above
28
+ /plugin install cadence-hud@cadence # the status band, works without the CLI
33
29
  ```
34
30
 
35
- The two plugins are independent. `cadence@cadence` carries the rituals, which call the **`cadence` CLI** (`raf`, `session`, `deliver`, …): install it too with `npm install -g @sylad/cadence`, the plugin does not bring it. `cadence-hud` works without the CLI; only its per-project plan progress needs `cadence` on the `PATH`. Without the plugin, `cadence skills install` copies the skills and agents into the repository. Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence-hud.md).
31
+ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence-hud.md).
36
32
 
37
33
  ## Where to read more
38
34
 
39
- | Page | What it covers |
40
- |---|---|
41
- | [docs/raf.md](docs/raf.md) | the plan: commands, plan file, foreign plan formats, Gantt |
42
- | [docs/orchestrate.md](docs/orchestrate.md) | waves of sessions: budgets, spec, accept and review steps, neighbouring repositories, one worktree per lot (experimental, not enabled: `orchestrate.worktrees: true` is refused until L299–L301 are done; `--integrate`, `--replay`, `--remove-worktree` belong to it, and a new wave of a lot handed back recovers its branch) |
43
- | [docs/deliver.md](docs/deliver.md) | delivery: wait for CI, deploy, verify the effect |
44
- | [docs/spec.md](docs/spec.md) | the screen and back-end sheets, `cadence spec check\|qa\|reseal` |
45
- | [docs/lead.md](docs/lead.md) | the lead workflow and `cadence lead tour` |
46
- | [docs/ref.md](docs/ref.md) | reference documents: `cadence ref add` (rendered pages, positioned text, images, figure index), `cadence ref measure` (colours, WCAG contrast, boxes, OCR, font size), `cadence ref check` (the inventory schema, `templates/inventaire.json`), `cadence ref eval` (recall, precision, false « read », tokens per page against a known inventory) and `cadence ref corpus` (the synthetic corpus with a known truth, rendered by Playwright) |
47
- | [docs/stabilite.md](docs/stabilite.md) | what is guaranteed from 1.0.0 and what is not |
48
-
49
- Also: [news](docs/news.md) (changelog, UX, code and QA reviews, documentation that follows the code), [session](docs/session.md), [corrections](docs/corrections.md), [perf](docs/perf.md), [skills and agents](docs/skills.md), [cadence-hud](docs/cadence-hud.md), [releasing](docs/releasing.md), [overview](docs/overview.md), [how it all works](docs/fonctionnement.md) (four figures).
50
-
51
- <details>
52
- <summary>The four tools in short</summary>
53
-
54
- - **raf** (French *reste à faire*, "what is left to do"): the plan, linked to your commits by the lot id in the message, computed from `git log` and never stored.
55
- - **news**: a user-facing changelog with screenshots, tied to the plan; a lot is not done until its entry is written.
56
- - **session**: `session start` and `session close` gather the facts of a work session (each lot in progress shows its commit count, `0 commit` included, next to its last activity).
57
- - **deliver**: wait for the CI of the pushed commit, run the deploy commands, then **verify the effect**; a delivery is done only when its checks pass. A failed delivery is logged as a *red delivery* with its class (refusal, CI, deploy, check) and the last lines of the failing command's output. While a wave runs, `cadence deliver --sha <sha>` is accepted when no commit of the sha belongs to a lot that is not ready, so a ready lot ships while the wave runs.
58
- - **auto-verification** (L236): before it reports, an `implement` or `fix` session re-reads the complete diff of its lot (of its fix commits, in `fix`) against the grid of the `code-reviewer` (code bug, architecture drift, architecture documentation, edge cases, one test per changed behaviour), fixes what it finds and writes an « auto-vérification » paragraph in its report; the measure is `cadence corrections --classify` and the share right the first time of `cadence perf`.
59
- - **perf**: `cadence perf` is the dashboard of the workflow (speed next to quality, `⚠` on a trend), including the red deliveries and the blocking QA findings; `cadence lead tour` opens with its *perf header*, one line per `⚠`, nothing when stable ([docs/perf.md](docs/perf.md)).
60
- - **doc-keeper** (L306): a generic agent that reads the diff of a lot and the project's map of documents (`docs.sync`, README, `docs/`, CHANGELOG, news), says which documents must change and updates them (or only lists them, read-only); `cadence orchestrate` calls it before the review when `docs.sync` reports a document behind the lot, the lead by hand; a document outside the repository only through the `docs.articles` key of the project's `cadence.yaml` (it then proposes a lot, never writes it) ([docs/news.md](docs/news.md#the-librarian-doc-keeper)).
61
- Reviews are opt-in gates: `ux-reviewer` (no user-facing change is done before its usability review), `code-reviewer` (no lot with commits is done before its code review) and `qa-reviewer`, which walks the delivered app in a real browser.
62
-
63
- </details>
64
-
65
- ## What's new
66
-
67
- Two lines per version; the long form is in [docs/whats-new.md](docs/whats-new.md) and the full history, with dates, in [CHANGELOG.md](CHANGELOG.md).
68
-
69
- - **1.9.0**: the plan lives at `.cadence/raf.yaml` (`raf migrate`); `cadence ref` reads reference documents; `doc-keeper` agent, `orchestrate.testGate`, `cadence lead hold` shown by `cadence-hud`.
70
- - **1.8.0**: `cadence perf` shows the speed of the workflow next to its quality (cost in euros per delivered lot, red deliveries, blocking QA findings), and `cadence lead tour` opens with a perf header.
71
- - **1.7.0**: `cadence orchestrate` no longer replays `accept` on a green sealed acceptation and refuses `--resume --dry-run`; `spec reseal --porteur` accepts a sheet amended by a delivered lot; the README becomes a short home page.
72
- - **1.6.0**: the mockup review sweeps everything in its first report and runs up to `orchestrate.maquetteReviews` times (3 by default); the acceptance lock names the first offending commit.
73
- - **1.5.1**: publishes 1.5.0 (tagged, never published): one UX review per lot, replay of a lot handed back for its budget, a self-check list in the briefs, a `tests.yml` workflow.
74
- - **1.5.0**: `gate: { count: N, … }` makes a lot wait for N done lots of a kind; `raf todo <id>` puts a started lot without work commits back to `todo`.
75
- - **1.4.0**: `cadence spec reseal` accepts a sheet amended by the lot's commits; wave budgets recalibrated; a `git checkout` in a session no longer stops the wave.
76
- - **1.3.0**: the spec step narrows to `screen: page`; `cadence spec reseal … --porteur` re-seals an unstable test; `cadence orchestrate --waits`; a third correction pass when the review converges.
77
- - **1.2.0**: the spec step reaches lots without a screen (`spec: true`), `orchestrate.accept` may be a list of runners, `lead.ux: lead`, and the code in service is a separate worktree.
78
- - **1.1.0**: the spec step of `cadence orchestrate`: screen sheet, reviewed mockup, red locked acceptance tests, and briefs that carry rule → test → green.
79
- - **1.0.0**: the first major release, a stable contract that follows semantic versioning; the [stability page](docs/stabilite.md) lists what is guaranteed.
80
-
81
- <details>
82
- <summary>Older versions (0.10.0 to 0.28.0)</summary>
83
-
84
- Each is a paragraph in [docs/whats-new.md](docs/whats-new.md) and a full section in [CHANGELOG.md](CHANGELOG.md): `cadence-hud` (0.12.0 to 0.25.0), `cadence orchestrate --continue` and `--status --watch`, `cadence lead tour`, `hook.autostart`, `docs.sync` and `docs.articles`, neighbouring repositories, `raf ignore`.
85
-
86
- </details>
35
+ - [docs/raf.md](docs/raf.md): the plan, its commands and its file.
36
+ - [docs/orchestrate.md](docs/orchestrate.md): waves of sessions and their budgets.
37
+ - [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect.
38
+ - [docs/spec.md](docs/spec.md): screen and back-end sheets.
39
+ - [docs/lead.md](docs/lead.md): the lead workflow.
40
+ - [docs/overview.md](docs/overview.md): the longer introduction and the map of every page.
41
+ - [docs/fonctionnement.md](docs/fonctionnement.md): how it all works, in four figures.
42
+ - [docs/stabilite.md](docs/stabilite.md): what is guaranteed from 1.0.0.
43
+ - [docs/whats-new.md](docs/whats-new.md) and [CHANGELOG.md](CHANGELOG.md): what changed.
87
44
 
88
45
  ## License
89
46
 
@@ -10,9 +10,9 @@ Follow the brief you are given. The method:
10
10
 
11
11
  1. **Read the diff of the lot.** The commits that cite the lot (`git log`, then `git show <sha>` for each; never a `git diff` range, which would take in the commits of other lots). Note what the change makes true or false for a reader: a behaviour, a command, a key, a default, a name, a file.
12
12
  2. **Draw the map of the documents.** The `docs.sync` rules of the project's `cadence.yaml` (the code paths and the documents they call for), then what the project keeps besides: the README, the pages of `docs/`, the CHANGELOG, the Nouveautés / What's new entries, the architecture document. Read the project's CLAUDE.md first for its own rules on documentation. Nothing about a folder or a machine comes from you: only this project's files.
13
- 3. **Say which documents must change, and why.** For each: the file, the passage, the sentence that the diff makes false or leaves out. A document the diff does not touch the truth of is not listed.
13
+ 3. **Say which documents must change, and why.** A lot that changes how the tool works (a behaviour, a command, a key, a default) is written in the pages of `docs/`, in full; the README is a short home page and receives only a link to that page, never the detail (an option, a key, a path, a flag). A README that grows by a paragraph is a finding, not a document brought up to date. For each: the file, the passage, the sentence that the diff makes false or leaves out. A document the diff does not touch the truth of is not listed.
14
14
  4. **Act according to the session.**
15
- - Writing session: update each document, in the language and the style it already has. Test the project's documentation checks if it has any (links, doc tests). Commit as soon as a document is right, with explicit paths (never `git add -A` or `commit -a`) and a message that cites the lot (`docs(<lot>): …`). A CHANGELOG or Nouveautés entry only if the project keeps one for this kind of change.
15
+ - Writing session: update each document (the detail in `docs/`, a link in the README), in the language and the style it already has. Test the project's documentation checks if it has any (links, doc tests). Commit as soon as a document is right, with explicit paths (never `git add -A` or `commit -a`) and a message that cites the lot (`docs(<lot>): …`). A CHANGELOG or Nouveautés entry only if the project keeps one for this kind of change.
16
16
  - Read-only session (no write tool, or a brief that says read-only): change nothing; list the documents of step 3 with the passage to write, ready to be applied by someone else.
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
 
@@ -6,7 +6,7 @@ tools: Read, Grep, Glob, Bash
6
6
 
7
7
  You check, quickly, whether the deliverable of one lot of a plan is already in the repository. You report; you never edit code.
8
8
 
9
- Follow the brief you are given: read the lot in the plan, then look for each part of its deliverable in the code, the tests and `git log`. A lot with no commit of its own is the normal case here — do not stop for that, it is the reason for the check.
9
+ Follow the brief you are given: the brief tells you how to get the lot (it carries it, or tells you where to read it in the plan), then look for each part of its deliverable in the code, the tests and `git log`. A lot with no commit of its own is the normal case here — do not stop for that, it is the reason for the check.
10
10
 
11
11
  - Do not run the test suite, the type-check, the linter or the build: this is a quick look.
12
12
  - Do not modify, commit, push, or run `raf start|done|note|review|ux`. Do not launch subagents.
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: reference-analyst
3
+ description: Reference document analyst. Reads the pages that `cadence ref add` rendered for one document (a screen mock-up PDF, a spec with figures, a norm), page by page and then zone by zone at full resolution, and writes one reading — an inventory of what the document shows (screens, components, states, verbatim texts, design tokens, tables, message templates, schemas, text-figure links, status badges, silences) — in the JSON format of `templates/inventaire.json`. Everything measurable goes through `cadence ref measure`; what cannot be read for sure is marked incertain with its cause. Writes only its reading, validates it with `cadence ref check`. Used by `cadence ref analyse` and by the orchestrator, not by hand.
4
+ tools: Read, Write, Bash, Grep, Glob
5
+ ---
6
+
7
+ You read one reference document and write down what it shows. You write one file, your reading; you never write code, specs or the plan, and you never decide what the project should do with what you read.
8
+
9
+ Follow the brief you are given. It names the document folder, the pages that were rendered (`pages/page-<n>.png`, 200 dpi), the file to write, the model to copy and the exact commands to measure and to validate. The folder also holds `document.json`: the positioned text of every page (each line with its box `x, y, l, h` in pixels of the rendered page), the images, the figure index with the passages that cite each figure, and the unresolved links. The method:
10
+
11
+ 1. **Look at every page whole first.** Open each rendered page with the Read tool and say to yourself what it is: a mock-up, a requirements page, a figure, a table, a scan. A page you did not open is a page you did not read.
12
+ 2. **Then every zone at full resolution.** A whole page is too small to read a label, a threshold or a table cell. Crop the zone (`cadence ref measure` gives the boxes of the shapes of a zone: `--quoi boites`) and look at the crop, or at the embedded image under `images/`, before writing anything about it.
13
+ 3. **Measure whatever is measurable, never estimate it by eye.** A colour, a contrast, a size, a box, a font size: `cadence ref measure <slug> <page> <zone> --quoi couleurs|contraste|boites|texte`, with the commands of the brief. An element whose numbers come from that command is `confiance: mesure` and its `point` or zone says where it was sampled. Text read by tesseract on a whole page is `ocr: true` and never `lu`.
14
+ 4. **Copy every visible text verbatim**: same characters, same case, same units (`120 KT` is not `120 kt`). One convention for labels: every visible text outside a table or a message template is a `texte` with its role from the closed list of `templates/inventaire.json`, including the label of a component (the button `Layers` is a `composant` and a `texte`). The text of a table or a template belongs to it, not to a `texte`.
15
+ 5. **Link every figure to the passages that cite it.** For each figure of `document.json`, read the passages that cite it, then the figure, and write a `lien`: `confirme` when the figure says what the text says, `precise` when the figure settles an ambiguity of the text (say the ambiguous reading, the one you keep and why, from what the figure draws), `contredit` when the two disagree (give the two statements, never pick one). A figure with no caption, or a citation with no figure: say so, or declare it not found.
16
+ 6. **What the document does not show is a `silence`**, written as a question that ends with `?`: a state, a message, a rule a developer will have to decide. Do not answer it.
17
+ 7. **Never guess.** An element you cannot read for sure (blurred, cut, too small, two readings possible) is `confiance: incertain` with its `cause`, never `lu`. Never invent an element to complete a list, never leave one out because it looks like another. A text that is too small to measure (under about 8 px at the render) is `incertain` with that cause.
18
+ 8. **Write the reading** to the file of the brief, in the format of `templates/inventaire.json` but with `genre: "lecture"`, the `document` `slug` and `sha256` of `document.json` (copy them, never retype them), and no `pages` key. Every id is `<slug>#<type>-<n>`. Then run `cadence ref check` on it, with the command of the brief, until it passes: a reading that does not pass is not a result.
19
+
20
+ - Never write `usage`, tokens or costs in the reading: the program that launched you counts them. Never write the `sha256` of another file than `document.json`.
21
+ - Never open another file of `lectures/` than the one you write: another reader may have written its own reading of the same document, and a reading made with the other in hand is not independent, so it measures nothing.
22
+ - Read only the document folder named in the brief and the model. Never look for, open or use an expected inventory, an answer key or a test corpus: the reading is yours alone, and a reading made with the answer in hand measures nothing.
23
+ - You write only the reading file: no other file, no commit, no change in the repository. Do not push, deliver, run `raf start|done|note|review|ux`, or launch subagents.
24
+ - A question a human must settle (a figure that contradicts its text, an element you cannot read) goes in `questions` of your report, as well as in the reading (`contredit`, `incertain`, `silence`).
25
+ - Give the report in the structured output the brief asks for.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: reference-verifier
3
+ description: Third look at a reference document. Receives only the elements that ONE of two independent readings carried and the other did not (each with the rendered page and a crop of its zone), looks at the crop and says for each one whether the document really shows it (confirme) or not (ecarte), with the reason. Writes nothing. Used by `cadence ref analyse` after `cadence ref diff`, not by hand.
4
+ tools: Read, Bash
5
+ ---
6
+
7
+ You are the third look on a reference document. Two readers read the same pages separately; they agree on most elements, and disagree on a few. The brief lists only the elements that **one** reader wrote and the other did not, each with a number, what was written, the rendered page and a **crop** of the zone it names. You never see the readings themselves, nor which reader wrote what, and you do not read the rest of the document: only the elements of the brief and their crops.
8
+
9
+ For each numbered element:
10
+
11
+ 1. **Open the crop** with the Read tool, and say to yourself what is drawn there. If the brief says there is no crop (the page was not rendered, or the zone is outside it), open the whole page, and say so in the reason.
12
+ 2. **Compare what was written with what is drawn**, character by character for a text (same characters, same case, same units: `120 KT` is not `120 kt`), value by value for a colour, a size or a cell. A number or a colour that can be measured is measured, never estimated by eye: `cadence ref measure` with the command of the brief.
13
+ 3. **Rule**: `confirme` when the crop shows exactly what was written, `ecarte` when it does not show it, or shows something else, or the element is a duplicate of another written differently. Never guess: if you cannot tell (blurred, cut, too small), the answer is `ecarte` with that reason, never `confirme`. Never rewrite an element, never correct it, never add one.
14
+
15
+ Every verdict carries a `raison`: one sentence that says what the crop shows. A verdict without a reason, or with a verdict other than `confirme` or `ecarte`, is counted as no answer.
16
+
17
+ - You write no file, you make no commit, you change nothing in the repository. Do not push, deliver, run `raf start|done|note|review|ux`, or launch subagents.
18
+ - Never look for, open or use an expected inventory, an answer key or a test corpus, nor any file of `lectures/`.
19
+ - Give the report in the structured output the brief asks for: `decisions` (one object per numbered element: `numero`, `verdict`, `raison`) and `questions` (what a human must settle).
package/dist/audit.js CHANGED
@@ -170,7 +170,7 @@ export function audit(plan, root, newsDir, today, opts = {}) {
170
170
  const m = l.public ? publicTitleTooLong(l.public, max) : reused ? publicTitleTooLong(reused, max) : null;
171
171
  return m ? [{ message: `${l.id} : ${reused && !l.public ? m.replace('titre public', 'titre public repris de la Nouveauté') : m}` }] : [];
172
172
  });
173
- const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, entries, newsDir, hypothesesOf(root, plan.configFile ?? join(root, 'cadence.yaml'))), ...titles, ...gates, ...docSyncIssues(plan, root, all.byLot), ...resourceIssues(lots, root), ...(plan.hasPublicField ? missingPublicTitles(lots, entries) : []),
173
+ const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7, { readonly: plan.readonly, hasVisibleField: plan.hasVisibleField }), ...newsIssues(lots, entries, newsDir, hypothesesOf(root, plan.configFile ?? join(root, 'cadence.yaml'))), ...titles, ...gates, ...docSyncIssues(plan, root, all.byLot), ...resourceIssues(lots, root), ...(plan.hasPublicField ? missingPublicTitles(lots, entries) : []),
174
174
  ...plan.ignore.invalid.map((src) => ({ message: `ignore : motif invalide « ${src} »` }))];
175
175
  // Un plan en lecture seule se corrige avec l'outil du projet : ne pas conseiller une commande raf qui refuserait.
176
176
  return plan.readonly ? issues.map((i) => ({ ...i, message: i.message.replace(/ — raf (start|public) .*$/, '') })) : issues;
@@ -189,6 +189,7 @@ export function hypothesesOf(root, configFile) {
189
189
  /**
190
190
  * Documentation en retard (L143, clé `docs.sync`) : un lot en cours — ou terminé à partir de `docs.since` — dont les commits de
191
191
  * travail touchent des chemins sans toucher le document que la règle désigne. Les commits de plan ne comptent pas.
192
+ * Un lot dont l'écart est acquitté et motivé (`raf docs-ok`, L317) n'est pas compté.
192
193
  */
193
194
  export function docSyncIssues(plan, root, byLot) {
194
195
  const { sync, since } = readDocsConfig(plan.configFile ?? join(root, 'cadence.yaml'));
@@ -196,6 +197,7 @@ export function docSyncIssues(plan, root, byLot) {
196
197
  return [];
197
198
  return plan
198
199
  .lots()
200
+ .filter((l) => !l.docsOk)
199
201
  .filter((l) => l.status === 'doing' || (l.status === 'done' && !!since && !!l.finished && l.finished >= since))
200
202
  .flatMap((l) => docSyncGaps(sync, filesOf(root, workCommits(plan, root, byLot.get(l.id) ?? []))).map((g) => ({ message: gapMessage(l.id, g) })));
201
203
  }
package/dist/check.js CHANGED
@@ -5,7 +5,16 @@ import { isRecurring } from './recurring.js';
5
5
  export const short = (c) => `${c.sha.slice(0, 7)} ${c.subject}`;
6
6
  /** Titre d'un lot de revue UX planifiée : « Revue UX — <écran> ». */
7
7
  const UX_REVIEW_TITLE = /^revue ux\s+—/i;
8
- export function check(lots, linked, today, idleDays = 7) {
8
+ /** Message de l'écart d'une revue UX sans `visible:` ; la correction dépend de qui tient le plan (L321). */
9
+ function uxReviewMessage(lot, plan) {
10
+ const head = `${lot.id} est une revue UX (« ${lot.title.slice(0, 60)} ») sans visible: — l'étape UX n'aurait pas lieu`;
11
+ if (!plan.readonly)
12
+ return `${head} : poser visible: true dans le plan (raf visible ${lot.id} on, ou raf add --visible pour un nouveau lot)`;
13
+ if (plan.hasVisibleField === false)
14
+ return `${head} : ce plan ne porte pas visible : mapper le champ dans cadence.yaml (plan.fields.visible) si l'outil du projet l'écrit ; sans ce champ, la revue d'ergonomie de cet écran se fait hors orchestrate`;
15
+ return `${head} : poser visible: true dans le plan avec l'outil du projet (plan en lecture seule)`;
16
+ }
17
+ export function check(lots, linked, today, idleDays = 7, plan = {}) {
9
18
  const issues = [];
10
19
  const ids = new Set(lots.map((l) => l.id));
11
20
  for (const c of linked.orphans)
@@ -21,7 +30,14 @@ export function check(lots, linked, today, idleDays = 7) {
21
30
  issues.push({ kind: 'todo-with-commits', message: `${lot.id} a ${commits.length} commit(s) mais est encore todo — raf start ${lot.id}` });
22
31
  }
23
32
  if (lot.status === 'doing' && !recurring) {
24
- const last = commits[0]?.day ?? lot.started;
33
+ // Un lot parent travaille par ses sous-lots (champ parent:, L321) : leur dernier commit, ou leur démarrage s'ils sont en cours, compte comme le sien.
34
+ const own = [commits[0]?.day ?? lot.started];
35
+ for (const sub of lots) {
36
+ if (sub.parent !== lot.id)
37
+ continue;
38
+ own.push(linked.byLot.get(sub.id)?.[0]?.day ?? (sub.status === 'doing' ? sub.started : undefined));
39
+ }
40
+ const last = own.filter((d) => !!d).sort().at(-1);
25
41
  if (last && diffDays(last, today) > idleDays) {
26
42
  issues.push({ kind: 'idle', message: `${lot.id} en cours sans commit depuis ${diffDays(last, today)} j (${lot.title})` });
27
43
  }
@@ -34,7 +50,7 @@ export function check(lots, linked, today, idleDays = 7) {
34
50
  }
35
51
  // L271 : sans visible:, le lot ne passe par aucune étape UX d'orchestrate (precheck → implement → review) et la revue de l'écran n'a pas lieu.
36
52
  if (isOpen(lot.status) && UX_REVIEW_TITLE.test(lot.title) && !lot.visible) {
37
- issues.push({ kind: 'ux-review-not-visible', message: `${lot.id} est une revue UX (« ${lot.title.slice(0, 60)} ») sans visible: — l'étape UX n'aurait pas lieu : poser visible: true dans le plan (raf visible ${lot.id} on, ou raf add --visible pour un nouveau lot)` });
53
+ issues.push({ kind: 'ux-review-not-visible', message: uxReviewMessage(lot, plan) });
38
54
  }
39
55
  for (const p of lot.problems)
40
56
  issues.push({ kind: 'bad-field', message: `${lot.id} : ${p}` });
package/dist/cli.js CHANGED
@@ -18,12 +18,12 @@ 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, tourFreeLine, tourLine, tourWorktreeLines } from './lead.js';
21
+ import { findProjects, leadTour, readLeadUx, tourFreeLine, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
22
22
  import { configuredCap, expiredHoldLines, liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
23
23
  import { addHold, releaseHold } from './orchestrate/registry.js';
24
24
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
25
25
  import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
26
- import { blockingLots, LABEL } from './orchestrate/table.js';
26
+ import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
27
27
  import { classifyAll, measure, ollamaClassifier, readCache, readJournals, readRuns, renderReport, sampleKeys, serializeCache } from './corrections.js';
28
28
  import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
29
29
  import { findAcceptation, parseAcceptation, resealAcceptation } from './orchestrate/accept.js';
@@ -57,16 +57,19 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
57
57
  raf list [--status todo|doing|done|dropped]
58
58
  raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
59
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
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
60
61
  raf gantt [-o .cadence/gantt.html]
61
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
62
63
  raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
63
- 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] [--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
- mode worktree par lot (orchestrate.worktrees: true) : EXPÉRIMENTAL, NON ACTIVABLE (refusé, code 2) tant que L299, L300 et L301 ne sont pas faits, sauf CADENCE_EXPERIMENTAL_WORKTREES=1 (tests) ; ses options : --resume <vague> --integrate projet:lot (rejoue un lot rendu à l'intégration) · --resume <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
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
65
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
66
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
67
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)
68
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
69
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
70
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
71
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
72
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)
@@ -177,6 +180,10 @@ function dispatch(argv, io) {
177
180
  slug: { type: 'string' },
178
181
  quoi: { type: 'string' },
179
182
  'sans-rendu': { type: 'boolean' },
183
+ out: { type: 'string' },
184
+ seuil: { type: 'string' },
185
+ 'model-b': { type: 'string' },
186
+ double: { type: 'boolean' },
180
187
  largeur: { type: 'string' },
181
188
  until: { type: 'string' },
182
189
  help: { type: 'boolean', short: 'h' },
@@ -229,6 +236,9 @@ function dispatch(argv, io) {
229
236
  // les worktrees de lot que des vagues ont conservés : aucun n'est oublié en silence (L279, R19)
230
237
  for (const l of tourWorktreeLines(rows, io.now().getTime()))
231
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);
232
242
  // les créneaux libres viennent du registre, le lead les lit au lieu de les calculer (L141)
233
243
  const [slots] = liveLines(configuredCap(io.env), io.now());
234
244
  if (slots)
@@ -512,6 +522,8 @@ function dispatch(argv, io) {
512
522
  io.out('spec : true');
513
523
  if (lot.uses)
514
524
  io.out(`uses : ${lot.uses.join(', ')}`);
525
+ if (lot.docsOk)
526
+ io.out(`docs-ok : ${lot.docsOk.date} — ${lot.docsOk.reason}`);
515
527
  const gate = gateProgress(plan.lots(), lot);
516
528
  if (lot.gate && gate)
517
529
  io.out(lot.gate.invalid ? `gate : ${gateText(gate)}` : `gate : ${gate.n} lots ${gate.label} faits`);
@@ -559,6 +571,17 @@ function dispatch(argv, io) {
559
571
  io.out(`${bySha ? short(found) : found.subject} acquitté`);
560
572
  return 0;
561
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
+ }
562
585
  case 'check': {
563
586
  if (values.ignored) {
564
587
  const list = loadPlan().acknowledged;
@@ -975,7 +998,7 @@ function openWavesRefusal(open) {
975
998
  });
976
999
  const how = open.map((w) => w.live
977
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)`
978
- : ` cd ${shellPath(w.launchDir)} && cadence orchestrate --resume ${w.wave}${[...w.lots, ...w.others].map((l) => ` --drop ${l.key}`).join('')}`);
1001
+ : ` cd ${shellPath(w.launchDir)} && ${resumeCommand(w.wave, ...[...w.lots, ...w.others].flatMap((l) => ['--drop', l.key]))}`);
979
1002
  return [
980
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) :`,
981
1004
  ...lines,
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
- import { PRECHECK_UNREADABLE } from './orchestrate/state.js';
3
+ import { LOCAL_PRECHECK_UNREADABLE, PRECHECK_UNREADABLE } from './orchestrate/state.js';
4
4
  import { RafError } from './plan.js';
5
5
  /** Les causes d'une passe de correction (L165), dans l'ordre où le rapport les liste. */
6
6
  export const CAUSES = ['spec-floue', 'regle-inventee', 'derive-architecture', 'donnees-sql', 'bug-code', 'test-manquant', 'faux-constat'];
@@ -117,6 +117,7 @@ export function readJournals(runsDir, until, since) {
117
117
  status: String(rec.status),
118
118
  ...(typeof rec.budget === 'number' ? { budget: rec.budget } : {}),
119
119
  ...(precheck ? { precheck } : {}),
120
+ ...(Array.isArray(rec.warnings) && rec.warnings.some((w) => typeof w === 'string' && w.startsWith(LOCAL_PRECHECK_UNREADABLE)) ? { localUnreadable: true } : {}),
120
121
  steps: steps.map((s) => {
121
122
  const usage = usageOf(s.tokens);
122
123
  return { n: s.n, kind: s.kind, status: s.status, ...(typeof s.model === 'string' ? { model: s.model } : {}), ...(s.started ? { started: s.started } : {}), ...(s.ended ? { ended: s.ended } : {}), ...(typeof s.tokens?.counted === 'number' ? { counted: s.tokens.counted } : {}), ...(usage ? { usage } : {}) };
package/dist/deliver.js CHANGED
@@ -3,7 +3,7 @@ import { readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { constants } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { parse } from 'yaml';
6
- import { onTermination, readProcs, SIGNAL_GRACE_MS, TreeTracker } from './proc.js';
6
+ import { onTermination, readProcs, SIGNAL_GRACE_MS, SIGNAL_PENDING_MS, TreeTracker } from './proc.js';
7
7
  import { isPlanOnly } from './audit.js';
8
8
  import { headSha, isAncestor, onRemote, readCommits, repoStatus, resolveCommit } from './git.js';
9
9
  import { citedRefs } from './link.js';
@@ -19,6 +19,8 @@ const GH_TIMEOUT = 60_000;
19
19
  export const TIMED_OUT = 124;
20
20
  /** Sortie gardée d'une commande : de quoi retrouver ses dernières lignes. */
21
21
  const OUTPUT_KEPT = 8_192;
22
+ /** Signaux que cadence écoute (proc.ts) : une racine qu'ils ont tuée laisse présager le même signal vers cadence. */
23
+ const TERMINATION_SIGNALS = [constants.signals.SIGHUP, constants.signals.SIGINT, constants.signals.SIGTERM];
22
24
  /** Les lignes de la sortie ; une ligne réécrite par \r (barre de progression) ne garde que sa dernière image non vide. */
23
25
  const outputTail = (text) => text.split('\n').flatMap((l) => l.split('\r').filter((f) => f.trim() !== '').slice(-1));
24
26
  const codeText = (code) => (code === TIMED_OUT ? 'délai dépassé' : `code ${code}`);
@@ -102,8 +104,12 @@ export function parseDeliverConfig(text, file) {
102
104
  deployTimeout: seconds('deployTimeout', 1800),
103
105
  };
104
106
  }
105
- /** Dépendances réelles : sh, gh, fetch, horloge. `read` : le relevé des processus du suivi (injectable pour les tests). */
106
- export function realDeps(root, read = readProcs) {
107
+ /**
108
+ * Dépendances réelles : sh, gh, fetch, horloge. `read` : le relevé des processus du suivi, `graceMs` : le délai de grâce
109
+ * après Ctrl-C ou raccrochage, `pendingMs` : l'attente d'un signal encore en route après la sortie de la racine
110
+ * (injectables pour les tests : sous charge, une grâce de 2 s se mesure mal).
111
+ */
112
+ export function realDeps(root, read = readProcs, graceMs = SIGNAL_GRACE_MS, pendingMs = SIGNAL_PENDING_MS) {
107
113
  const gh = (args) => {
108
114
  try {
109
115
  return execFileSync('gh', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: GH_TIMEOUT });
@@ -122,6 +128,19 @@ export function realDeps(root, read = readProcs) {
122
128
  lastOutput: () => outputTail(output),
123
129
  exec: (cmd, env, timeoutMs) => new Promise((resolve) => {
124
130
  output = '';
131
+ // Écoute posée AVANT spawn : un Ctrl-C reçu pendant la première lecture du suivi (synchrone, dans le
132
+ // constructeur du TreeTracker) serait sinon traité par l'action par défaut, qui tue cadence sans nettoyer
133
+ // l'arbre. Le nettoyage ne tourne qu'au tour de boucle suivant, quand `tree` existe.
134
+ let tree = null;
135
+ // Terminaison en cours : exec ne rend pas la main avant qu'elle ne soit finie — sinon deliver libère
136
+ // le verrou pendant la grâce, alors que des descendants tournent encore.
137
+ let ending = null;
138
+ const forget = onTermination((sig) => {
139
+ if (!tree)
140
+ return; // commande jamais lancée : rien à nettoyer
141
+ ending = sig === 'SIGTERM' ? Promise.resolve(tree.kill()) : tree.end(graceMs);
142
+ return ending;
143
+ });
125
144
  const child = spawn('sh', ['-c', cmd], {
126
145
  cwd: root,
127
146
  env: { ...process.env, ...env },
@@ -138,7 +157,10 @@ export function realDeps(root, read = readProcs) {
138
157
  relay(child.stderr, process.stderr);
139
158
  const pid = child.pid;
140
159
  if (pid === undefined) {
141
- child.once('error', () => resolve(127));
160
+ child.once('error', () => {
161
+ forget();
162
+ resolve(127);
163
+ });
142
164
  return;
143
165
  }
144
166
  // L'arbre de la commande est suivi pendant toute son exécution (relevé périodique, pid + heure de
@@ -148,29 +170,38 @@ export function realDeps(root, read = readProcs) {
148
170
  // qu'une seconde livraison prend le verrou libéré ou périmé. Ctrl-C et raccrochage : l'arbre a déjà
149
171
  // reçu le signal du terminal, un court délai de grâce laisse finir ses trap (et git son index.lock)
150
172
  // avant le kill ; SIGTERM (à cadence seul) et le délai : kill immédiat.
151
- const tree = new TreeTracker(pid, read, () => process.stderr.write('deliver : relevé des processus indisponible — le kill n\'est dégradé que tant que le relevé échoue : seule la commande racine sera tuée, ses descendants survivront\n'), () => process.stderr.write('deliver : kill replié sur la commande racine faute de relevé des processus — des descendants ont pu survivre\n'));
152
- // Terminaison en cours : exec ne rend pas la main avant qu'elle ne soit finie — sinon deliver libère
153
- // le verrou pendant la grâce, alors que des descendants tournent encore.
154
- let ending = null;
155
- const forget = onTermination((sig) => (ending = sig === 'SIGTERM' ? Promise.resolve(tree.kill()) : tree.end(SIGNAL_GRACE_MS)));
173
+ tree = new TreeTracker(pid, read, () => process.stderr.write('deliver : relevé des processus indisponible — le kill n\'est dégradé que tant que le relevé échoue : seule la commande racine sera tuée, ses descendants survivront\n'), () => process.stderr.write('deliver : kill replié sur la commande racine faute de relevé des processus — des descendants ont pu survivre\n'));
174
+ const tracker = tree;
156
175
  let timedOut = false;
157
176
  const timer = setTimeout(() => {
158
177
  timedOut = true;
159
- tree.kill();
178
+ tracker.kill();
160
179
  }, Math.max(1_000, timeoutMs));
161
180
  child.once('exit', (code, signal) => {
162
181
  clearTimeout(timer);
163
- tree.rootExited(); // son pid peut être repris : plus d'adoption tardive de la racine pendant la grâce
182
+ tracker.rootExited(); // son pid peut être repris : plus d'adoption tardive de la racine pendant la grâce
164
183
  const result = timedOut ? TIMED_OUT : signal ? 128 + (constants.signals[signal] ?? 0) : (code ?? 1);
165
184
  const settle = () => {
166
- tree.stop();
185
+ tracker.stop();
167
186
  forget();
168
187
  resolve(result);
169
188
  };
170
- if (ending)
171
- void ending.then(settle);
172
- else
173
- settle();
189
+ // Un Ctrl-C part au groupe entier : la racine peut mourir et sa sortie être traitée avant que le SIGINT
190
+ // adressé à cadence ne le soit. Retirer l'écoute à cet instant le perdrait (tracker.end jamais joué, un
191
+ // descendant qui ignore SIGINT survivrait, un second Ctrl-C tuerait cadence sans libérer le verrou) : on
192
+ // attend le signal en route, au plus pendantMs, tant que des processus suivis vivent OU que la racine est
193
+ // elle-même morte d'un signal de terminaison (ou sortie 128 + ce signal) — curl tué par le Ctrl-C.
194
+ const terminated = !timedOut && TERMINATION_SIGNALS.includes(result - 128);
195
+ const deadline = Date.now() + pendingMs;
196
+ const next = () => {
197
+ if (ending)
198
+ void ending.then(settle);
199
+ else if ((!terminated && tracker.alive().length === 0) || Date.now() >= deadline)
200
+ settle();
201
+ else
202
+ setTimeout(next, 20);
203
+ };
204
+ next();
174
205
  });
175
206
  }),
176
207
  gh: (sha) => JSON.parse(gh(['run', 'list', '--commit', sha, '--json', 'name,status,conclusion'])),
package/dist/lead.js CHANGED
@@ -12,6 +12,7 @@ import { priorityRank, readPriority } from './orchestrate/continue.js';
12
12
  import { readNext, sharedStateDir, stateDir } from './state.js';
13
13
  import { freeLine, readyFree } from './ready-free.js';
14
14
  import { cadenceHome } from './orchestrate/registry.js';
15
+ import { noTestGate, noTestText } from './orchestrate/no-test.js';
15
16
  import { KEPT_TITLE, keptLine, keptTrees } from './orchestrate/worktree.js';
16
17
  /** Clé `lead.ux` du cadence.yaml du dossier parent (L197) : qui pose le verdict UX (`raf ux`) d'un lot visible. `human` (défaut) : l'humain ; `lead` : le lead, l'humain n'étant consulté qu'en cas de doute. */
17
18
  export function readLeadUx(parent) {
@@ -143,6 +144,9 @@ export function tourRow(dir, today, idle = TOUR_IDLE, name = basename(dir)) {
143
144
  const kept = keptTrees(root);
144
145
  if (kept.length)
145
146
  row.worktrees = kept;
147
+ const noTest = noTestGate(root);
148
+ if (noTest)
149
+ row.noTest = noTest;
146
150
  }
147
151
  catch (e) {
148
152
  return { ...empty(row.project), error: (e instanceof Error ? e.message : String(e)).split('\n')[0] };
@@ -196,6 +200,10 @@ export function tourWorktreeLines(rows, now = Date.now()) {
196
200
  const lines = rows.flatMap((r) => (r.worktrees ?? []).map((t) => keptLine(r.project, t, now)));
197
201
  return lines.length ? [KEPT_TITLE, ...lines] : [];
198
202
  }
203
+ /** Une ligne par projet sans `orchestrate.test` (L327) : la vague ne lancera pas la suite ; rien quand tous le déclarent. */
204
+ export function tourNoTestLines(rows) {
205
+ return rows.filter((r) => r.noTest).map((r) => `${r.project} · ${noTestText(r.noTest)}`);
206
+ }
199
207
  const DRIFT_SHOWN = 2;
200
208
  /** Une ligne par projet : projet · en cours · dérive · notes · prochain lot prêt · dépôt. */
201
209
  export function tourLine(r) {
@@ -54,6 +54,24 @@ export function capturesText(captureDir) {
54
54
  export function shotsText(captureDir) {
55
55
  return `The Playwright server resolves a file name you give it against the repository, not against its output directory: a bare name such as page-home.png lands at the repository root. Give every screenshot, snapshot and trace the absolute path of the wave's capture directory, \`${captureDir}/\`, followed by the name (e.g. \`${captureDir}/page-home.png\`): the server allows it, and the repository stays clean. Never the \`--output-dir\` option (the server's launch argument, which a session cannot set).`;
56
56
  }
57
+ /** Précheck : la session lit le lot dans le plan (Sonnet, qui a la place pour un plan de plusieurs centaines de ko). */
58
+ export const PLAN_READING = 'Read the lot, its notes and sub-tasks in the plan (`.cadence/raf.yaml`, the legacy `docs/plan/raf.yaml`, or the file named by `plan:` in `cadence.yaml`),';
59
+ /**
60
+ * Précheck sur le modèle local (L322) : le lot est dans le brief et la session n'ouvre pas le plan. Un plan de 474 ko lu en entier remplissait la fenêtre de 64 k
61
+ * de qwen3-coder:30b (7 « Autocompact is thrashing » sur 32 lots, 23 au 25-10) ; les lectures du dépôt restent courtes pour la même raison.
62
+ */
63
+ export function localReading(lot) {
64
+ const notes = lot.notes.slice(-5).map((n) => `- ${n.date ? `${n.date} : ` : ''}${n.text.length > 1500 ? `${n.text.slice(0, 1500)} […]` : n.text}`);
65
+ const tasks = lot.tasks.map((t) => `- ${t.id} [${t.status}] ${t.title}`);
66
+ return [
67
+ 'Here is the lot as written in the plan; do not open the plan file (hundreds of kB: it would fill your context window).',
68
+ `Title: ${lot.title}`,
69
+ ...(lot.goal ? [`Goal: ${lot.goal}`] : []),
70
+ ...(notes.length ? ['Notes:', ...notes] : []),
71
+ ...(tasks.length ? ['Sub-tasks:', ...tasks] : []),
72
+ 'Keep every read small (`grep -n`, `git log --oneline -S`, a few dozen lines at a time, never a whole large file),',
73
+ ].join('\n');
74
+ }
57
75
  const FILES = {
58
76
  precheck: 'precheck.md',
59
77
  spec: 'spec.md',
@@ -91,7 +109,7 @@ export function loadTemplates(dir = TEMPLATES_DIR) {
91
109
  export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
92
110
  const text = typeof source === 'string' ? loadTemplates(source)[kind] : source[kind];
93
111
  const out = text.replace(/\{\{(\w+)\}\}/g, (_m, name) => {
94
- const v = { ...vars, repos: vars.repos ?? '', integration: vars.integration ?? '', docs: vars.docs ?? '', elsewhere: vars.elsewhere ?? '', spec: vars.spec ?? '', accept: vars.accept ?? '', maquette: vars.maquette ?? '', tmp: vars.tmp ?? '', shots: vars.shots ?? '', fiche: vars.fiche ?? '' }[name];
112
+ const v = { ...vars, repos: vars.repos ?? '', integration: vars.integration ?? '', docs: vars.docs ?? '', elsewhere: vars.elsewhere ?? '', ack: vars.ack ?? '', spec: vars.spec ?? '', accept: vars.accept ?? '', maquette: vars.maquette ?? '', tmp: vars.tmp ?? '', shots: vars.shots ?? '', fiche: vars.fiche ?? '', lecture: vars.lecture ?? PLAN_READING }[name];
95
113
  if (v === undefined)
96
114
  throw new RafError(`gabarit ${FILES[kind]} : valeur manquante pour {{${name}}}`);
97
115
  return v;