@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -67
- package/agents/doc-keeper.md +2 -2
- package/agents/precheck-reader.md +1 -1
- package/agents/reference-analyst.md +25 -0
- package/agents/reference-verifier.md +19 -0
- package/dist/audit.js +3 -1
- package/dist/check.js +19 -3
- package/dist/cli.js +28 -5
- package/dist/corrections.js +2 -1
- package/dist/deliver.js +47 -16
- package/dist/lead.js +8 -0
- package/dist/orchestrate/briefs.js +19 -1
- package/dist/orchestrate/changelog-merge.js +94 -0
- package/dist/orchestrate/command.js +108 -60
- package/dist/orchestrate/cycle.js +226 -36
- package/dist/orchestrate/integrate.js +52 -8
- package/dist/orchestrate/launch.js +14 -4
- package/dist/orchestrate/no-test.js +41 -0
- package/dist/orchestrate/registry.js +23 -0
- package/dist/orchestrate/snapshot.js +29 -1
- package/dist/orchestrate/state.js +14 -9
- package/dist/orchestrate/table.js +13 -1
- package/dist/perf.js +109 -7
- package/dist/plan.js +20 -1
- package/dist/proc.js +6 -0
- package/dist/ref/add.js +24 -11
- package/dist/ref/analyse.js +220 -0
- package/dist/ref/command.js +15 -1
- package/dist/ref/diff.js +262 -0
- package/dist/ref/evaluate.js +2 -2
- package/dist/ref/figures.js +86 -13
- package/dist/ref/pdf.js +20 -8
- package/dist/ref/regard.js +102 -0
- package/dist/ref/session.js +34 -0
- package/dist/state.js +17 -2
- package/docs/deliver.md +20 -4
- package/docs/fonctionnement.md +3 -3
- package/docs/lead.md +8 -4
- package/docs/news.md +6 -0
- package/docs/orchestrate.md +71 -40
- package/docs/overview.md +65 -1
- package/docs/perf.md +3 -2
- package/docs/raf.md +2 -1
- package/docs/ref.md +102 -3
- package/docs/releasing.md +3 -3
- package/docs/skills.md +14 -1
- package/docs/spec.md +1 -1
- package/docs/whats-new.md +31 -1
- package/package.json +1 -1
- package/skills/deliver/SKILL.md +25 -2
- package/skills/lead/SKILL.md +6 -6
- package/skills/orchestrate/SKILL.md +39 -0
- package/templates/orchestrate/fix-docs.md +2 -2
- package/templates/orchestrate/implement.md +1 -1
- 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.
|
|
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.
|
|
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,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
18
|
-
raf init
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
## Install as a Claude Code plugin
|
|
28
24
|
|
|
29
25
|
```
|
|
30
26
|
/plugin marketplace add Sylad/cadence
|
|
31
|
-
/plugin install cadence@cadence # skills
|
|
32
|
-
/plugin install cadence-hud@cadence # the status band
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
package/agents/doc-keeper.md
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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)
|
|
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)} &&
|
|
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,
|
package/dist/corrections.js
CHANGED
|
@@ -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
|
-
/**
|
|
106
|
-
|
|
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', () =>
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
178
|
+
tracker.kill();
|
|
160
179
|
}, Math.max(1_000, timeoutMs));
|
|
161
180
|
child.once('exit', (code, signal) => {
|
|
162
181
|
clearTimeout(timer);
|
|
163
|
-
|
|
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
|
-
|
|
185
|
+
tracker.stop();
|
|
167
186
|
forget();
|
|
168
187
|
resolve(result);
|
|
169
188
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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;
|