@sylad/cadence 0.20.0 → 0.21.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 +82 -3
- package/dist/articles.js +86 -0
- package/dist/audit.js +16 -2
- package/dist/cli.js +12 -4
- package/dist/config.js +81 -2
- package/dist/deliver.js +56 -9
- package/dist/docsync.js +57 -0
- package/dist/orchestrate/briefs.js +1 -1
- package/dist/orchestrate/command.js +17 -5
- package/dist/orchestrate/cycle.js +14 -2
- package/dist/orchestrate/launch.js +5 -2
- package/dist/orchestrate/registry.js +10 -0
- package/dist/orchestrate/state.js +9 -5
- package/dist/plan.js +11 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +3 -3
- package/templates/orchestrate/review-small.md +1 -0
- package/templates/orchestrate/review.md +1 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "cadence",
|
|
10
10
|
"description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.21.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": "0.
|
|
4
|
+
"version": "0.21.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
|
@@ -24,7 +24,7 @@ Code prompt that shows the session's context, quota, cost and the orchestrate wa
|
|
|
24
24
|
|
|
25
25
|
## What's new
|
|
26
26
|
|
|
27
|
-
**0.20.0**: `cadence-hud` no longer misleads once the work is over: `/hud cmd` lists the background commands it counts (id, tool, origin, age, name), any other argument answers `argument inconnu` instead of hiding the band, commands with no end seen for an hour or no record go to a grey `N cmd sans fin vue`, a finished subagent takes its commands with it, two races that left a `1 cmd` counted forever are closed, and a finished wave shows grey without its budget then disappears after 30 minutes (L142); the `qa-reviewer` and `ux-reviewer` agents are read-only for real, their frontmatter listing Read, Grep, Glob, Bash and the Playwright tools (L140). **0.19.1**: `cadence-hud`'s `N cmd` counter goes back down when a Monitor expires, when the background commands a subagent started end with it, and at session start or plugin reload (L135); its `tool.call` and `prompt.submit` guard hooks carry a `.catch` on registration, which clears the `claude plugin validate` warning (L136). **0.19.0**: `cadence orchestrate` lifts a repository's lock and `pre-push` guard as soon as every lot of the wave that touches it is finished, so `cadence deliver` works there while the wave continues elsewhere (L132); a review that leaves a repository dirty hands back that lot only, suspends the lots still queued in that repository and keeps the other repositories running, its verdict reported instead of lost (L133); `cadence session close` re-run without a `session start` keeps the window since the last opening (L131); `cadence-hud` draws the lot rows of a wave in aligned columns on every surface (L134). **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
27
|
+
**0.21.0**: `cadence orchestrate --status` and `cadence lead tour` print the free session slots, which the `lead` skill reads (L141); `orchestrate.effort` sets the effort level of each pass of a wave (L137); `docs.sync` makes `raf check`, `lead tour` and the review brief report a document that does not follow the code (L143); `docs.articles` opens, after a green `cadence deliver`, a lot `Article <project> à rafraîchir` in the neighbouring repository (L144). **0.20.0**: `cadence-hud` no longer misleads once the work is over: `/hud cmd` lists the background commands it counts (id, tool, origin, age, name), any other argument answers `argument inconnu` instead of hiding the band, commands with no end seen for an hour or no record go to a grey `N cmd sans fin vue`, a finished subagent takes its commands with it, two races that left a `1 cmd` counted forever are closed, and a finished wave shows grey without its budget then disappears after 30 minutes (L142); the `qa-reviewer` and `ux-reviewer` agents are read-only for real, their frontmatter listing Read, Grep, Glob, Bash and the Playwright tools (L140). **0.19.1**: `cadence-hud`'s `N cmd` counter goes back down when a Monitor expires, when the background commands a subagent started end with it, and at session start or plugin reload (L135); its `tool.call` and `prompt.submit` guard hooks carry a `.catch` on registration, which clears the `claude plugin validate` warning (L136). **0.19.0**: `cadence orchestrate` lifts a repository's lock and `pre-push` guard as soon as every lot of the wave that touches it is finished, so `cadence deliver` works there while the wave continues elsewhere (L132); a review that leaves a repository dirty hands back that lot only, suspends the lots still queued in that repository and keeps the other repositories running, its verdict reported instead of lost (L133); `cadence session close` re-run without a `session start` keeps the window since the last opening (L131); `cadence-hud` draws the lot rows of a wave in aligned columns on every surface (L134). **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
28
28
|
stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
|
|
29
29
|
(sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
|
|
30
30
|
terminal cells and kept to ASCII; `publish.yml` on the v5 actions. **0.12.0**: the `cadence-hud` plugin (a status band above the Claude Code prompt),
|
|
@@ -341,6 +341,64 @@ lot in each neighbour (the one `raf commits` lists last: it cites the lot, falls
|
|
|
341
341
|
neighbour has none), and refuses to record when a listed repository cannot be read. `raf check` does not audit the
|
|
342
342
|
neighbours: their staleness is not checked, only the project's own commits are.
|
|
343
343
|
|
|
344
|
+
### Documentation that follows the code
|
|
345
|
+
|
|
346
|
+
A stale README misleads. `cadence.yaml` can declare which documents must follow which code:
|
|
347
|
+
|
|
348
|
+
```yaml
|
|
349
|
+
docs:
|
|
350
|
+
sync:
|
|
351
|
+
- paths: [src/**, bin/*.js] # what changes…
|
|
352
|
+
docs: [README.md, docs/usage.md] # …must change one of these, in the same lot
|
|
353
|
+
- paths: [templates/]
|
|
354
|
+
docs: [CLAUDE.md]
|
|
355
|
+
- paths: [frontend/src/**, '!**/*.test.tsx'] # a pattern starting with ! leaves files out
|
|
356
|
+
docs: [README.md]
|
|
357
|
+
since: 2026-10-09 # optional: also audit lots finished on or after this day
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
A pattern is an exact path, with `*` (inside a folder), `**` (across folders), `?`, or a trailing `/` for
|
|
361
|
+
a whole folder. `raf check` takes the **work commits** of every lot in progress (plan-only commits do
|
|
362
|
+
not count; a lot finished on or after `docs.since` is audited too) and, for each pair, reports the lot when
|
|
363
|
+
those commits touch `paths` without any of them touching a file of `docs`:
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
✗ L7 : documentation en retard — src/a.ts, src/b.ts sans toucher README.md ou docs/usage.md (docs.sync)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
It counts as drift: exit code 1, shown in the `dérive` column of `cadence lead tour`, and the review
|
|
370
|
+
brief of `cadence orchestrate` (`review` and `review-small`) carries the list the program computed,
|
|
371
|
+
to be reported as one **major** finding per line — a stale document is not left to the reviewer's
|
|
372
|
+
memory of a sentence. Touching a document is what the program checks; whether its text is true is
|
|
373
|
+
still the review's reading. The commits of neighbouring repositories (`repos:`) are not audited.
|
|
374
|
+
|
|
375
|
+
#### An article elsewhere that follows the delivery
|
|
376
|
+
|
|
377
|
+
A case study that lives in a neighbouring repository (the project's article on a showcase site) goes stale
|
|
378
|
+
the same way. `docs.articles` names it:
|
|
379
|
+
|
|
380
|
+
```yaml
|
|
381
|
+
docs:
|
|
382
|
+
articles:
|
|
383
|
+
- repo: ../claude-code-codex # neighbouring repository, relative to the project
|
|
384
|
+
file: src/pages/cas/demo.md # the article in it
|
|
385
|
+
name: demo # optional: the name in the lot title (default: the plan's project)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
After a **green** `cadence deliver`, for the lots it lists as delivered **that have a public title** (`public:`, or
|
|
389
|
+
the title of their Nouveautés entry), cadence opens in this repository's plan the lot
|
|
390
|
+
`Article demo à rafraîchir` (todo, estimate 0.5, `repos: [{ path: ../claude-code-codex, cite: demo }]`), with a note
|
|
391
|
+
that quotes the delivery, the public titles delivered, the article and the **sections to read again**: the headings
|
|
392
|
+
(`#` lines, or `<h1>`…`<h6>` of an Astro page) that share a word of five letters or more with a delivered title,
|
|
393
|
+
else all the `##` headings. The lot is then played like any other, in an ordinary wave (`cadence orchestrate`: an
|
|
394
|
+
implementer rewrites the article in the neighbouring repository, the review checks it against the note). If a lot
|
|
395
|
+
of that title is still open, it receives a note instead of a duplicate. Two entries with the same name (the same
|
|
396
|
+
project told in two neighbouring repositories) share one lot, which declares both repositories in `repos:`. Internal lots (no public title) open nothing;
|
|
397
|
+
a red delivery opens nothing; `cadence deliver --dry-run` announces the articles. The plan is written but **not
|
|
398
|
+
committed** (`deliver` never commits): the line `article à rafraîchir : L9 …` says so. With a plan kept by another
|
|
399
|
+
tool (read-only), the same line carries the instruction to open the lot with that tool. This replaces the manual
|
|
400
|
+
`sync-site-docs` skill.
|
|
401
|
+
|
|
344
402
|
### QA review
|
|
345
403
|
|
|
346
404
|
No gate and no command here: the QA review comes **after** a delivery, and
|
|
@@ -525,11 +583,13 @@ beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second
|
|
|
525
583
|
| Column | Content |
|
|
526
584
|
|---|---|
|
|
527
585
|
| `en cours` | ids of the lots in progress; `(aucune activité)` when it has none at all, `(silencieux Nj)` when the lot's last activity (commit, note or start) is more than `--idle` days old (default 3) |
|
|
528
|
-
| `dérive` | the gaps `raf check` reports, counted and the first two shown |
|
|
586
|
+
| `dérive` | the gaps `raf check` reports (a lot whose commits leave a document behind, [`docs.sync`](#documentation-that-follows-the-code), included), counted and the first two shown |
|
|
529
587
|
| `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters |
|
|
530
588
|
| `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters |
|
|
531
589
|
| `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
|
|
532
590
|
|
|
591
|
+
When an orchestrated wave is live, one more line closes the tour (not part of `--json`): `vagues en cours : 1 · sessions en cours : 0 · créneaux libres : 1 sur 2` — the lead reads the free slots there instead of computing them (L141).
|
|
592
|
+
|
|
533
593
|
`--json` prints the same content as an array of objects (`project`, `doing`, `drift`, `notes`,
|
|
534
594
|
`next`, `repo`, and `error` when the project could not be read). The tour is read-only: it changes
|
|
535
595
|
no plan. It exits 0 even when a project is in error — that project's line reads
|
|
@@ -634,6 +694,8 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
634
694
|
reservation number that looks like one) is not a delivered lot. Plan upkeep
|
|
635
695
|
commits (see "Plan upkeep needs no lot") are skipped in that list: they cite
|
|
636
696
|
lots without delivering anything.
|
|
697
|
+
- With [`docs.articles`](#an-article-elsewhere-that-follows-the-delivery), a green delivery of lots with a
|
|
698
|
+
public title also opens the lot `Article <project> à rafraîchir` in the plan.
|
|
637
699
|
|
|
638
700
|
### A project with its own delivery script
|
|
639
701
|
|
|
@@ -796,6 +858,22 @@ orchestrate:
|
|
|
796
858
|
|
|
797
859
|
`full` is also the model of the UX review and of the single pass of a small lot that is not light.
|
|
798
860
|
|
|
861
|
+
**Effort level per pass** (L137). Each session is launched with `claude -p --effort <level>` (Claude Code 2.1.284; an
|
|
862
|
+
older `claude` is refused at launch unless every pass is `default`). Defaults: `precheck` low (it only reads),
|
|
863
|
+
`implement` and `fix` medium, `review` high (`review-small` follows `review`), `ux` high. A project overrides any of them
|
|
864
|
+
in `cadence.yaml`; the levels are `low`, `medium`, `high`, `xhigh`, `max`, and `default` sends no `--effort` (the
|
|
865
|
+
level of the session, as before this key). The format-retry session (`--resume`) keeps the level of its pass, and
|
|
866
|
+
`--dry-run` shows the flag in each command line.
|
|
867
|
+
|
|
868
|
+
```yaml
|
|
869
|
+
orchestrate:
|
|
870
|
+
effort: { precheck: low, implement: medium, fix: medium, review: high, ux: high } # defaults
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
To measure what a level costs, run the same kind of wave before and after changing the key: each step of
|
|
874
|
+
`<wave>/<project>--<lot>.json` records its `effort`, its `tokens` and its `started` / `ended` times (a step from a wave
|
|
875
|
+
older than L137, or with `default`, has no `effort`); compare the totals per `kind` between the two waves.
|
|
876
|
+
|
|
799
877
|
**Pre-check « deliverable already present? »** (L77). Before the first implementation of a lot that has no commit yet, a
|
|
800
878
|
short read-only session (Sonnet, `precheck` step, brief `templates/orchestrate/precheck.md`) looks in the repository for
|
|
801
879
|
what the lot asks for (another lot, or a correction, may have done it already). It answers `oui` (everything is there,
|
|
@@ -919,7 +997,7 @@ A slot or a registry entry is owned by a pid **and** its start time: a reused pi
|
|
|
919
997
|
identifiers are reserved atomically (`-2`, `-3` suffix when two waves start in the same minute; an existing
|
|
920
998
|
`--wave` is refused). The registry of live waves and the slots live under `~/.cadence/orchestrate/` (`CADENCE_HOME`
|
|
921
999
|
to move it): `cadence orchestrate --status` lists, from any folder, the live waves, the repositories each
|
|
922
|
-
still holds (those not yet released), the cap of each wave and the slots
|
|
1000
|
+
still holds (those not yet released), the cap of each wave, the slots in use and the **free slots** (`créneaux libres : N sur 2`: 2 minus, for each live wave, the smaller of its cap and the repositories it still holds — the figure the `lead` skill reads before starting a subagent). Without an identifier it then prints the table of the wave launched most recently from this folder (by launch time, not by alphabetical order of the identifier).
|
|
923
1001
|
|
|
924
1002
|
**Guards, imposed by the code**: a global cap of simultaneous sessions, one wave per repository at
|
|
925
1003
|
a time (above); `Agent`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions; a
|
|
@@ -964,6 +1042,7 @@ orchestrate:
|
|
|
964
1042
|
test: npm test # run by the orchestrator after a work step (optional)
|
|
965
1043
|
build: npm run build # run after the tests; both results go to the reviewer, who does not redo them (optional)
|
|
966
1044
|
precheck: true # default: before the first implementation of a lot with no commit, a read-only Sonnet session checks whether the deliverable is already in the repository (see below); false skips it
|
|
1045
|
+
effort: { review: xhigh } # effort level per pass (see « Effort level per pass »): precheck, implement, fix, review, ux
|
|
967
1046
|
ux: http://localhost:4200 # a URL, a launch command, or { command, url, timeout? } — for the UX review (see below)
|
|
968
1047
|
permissionMode: auto # default
|
|
969
1048
|
addDirs: [/home/me/projects/tmp] # extra directories the sessions may use
|
package/dist/articles.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isOpen } from './plan.js';
|
|
4
|
+
/** Titre du lot ouvert à la livraison. */
|
|
5
|
+
export const articleTitle = (name) => `Article ${name} à rafraîchir`;
|
|
6
|
+
const SECTIONS_SHOWN = 12;
|
|
7
|
+
/** Titres d'un article : lignes `#` du Markdown (hors en-tête YAML) et balises `<h1>`…`<h6>` d'une page Astro ou MDX. */
|
|
8
|
+
export function articleHeadings(text) {
|
|
9
|
+
const body = text.replace(/^/, '').replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
|
10
|
+
const found = [];
|
|
11
|
+
for (const m of body.matchAll(/^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm))
|
|
12
|
+
found.push({ at: m.index, heading: { level: m[1].length, text: m[2].trim() } });
|
|
13
|
+
for (const m of body.matchAll(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi)) {
|
|
14
|
+
const t = m[2].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
15
|
+
if (t)
|
|
16
|
+
found.push({ at: m.index, heading: { level: Number(m[1]), text: t } });
|
|
17
|
+
}
|
|
18
|
+
return found.sort((a, b) => a.at - b.at).map((f) => f.heading);
|
|
19
|
+
}
|
|
20
|
+
const words = (s) => new Set(s
|
|
21
|
+
.normalize('NFD')
|
|
22
|
+
.replace(/[̀-ͯ]/g, '')
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.split(/[^a-z0-9]+/)
|
|
25
|
+
.filter((w) => w.length >= 5));
|
|
26
|
+
/**
|
|
27
|
+
* Sections à relire : celles dont le titre partage un mot (5 lettres au moins, sans accents ni casse) avec un titre public
|
|
28
|
+
* livré ; sans recoupement, toutes les sections de premier niveau (`##`), faute de mieux.
|
|
29
|
+
*/
|
|
30
|
+
export function articleSections(text, titles) {
|
|
31
|
+
const headings = articleHeadings(text);
|
|
32
|
+
const wanted = new Set(titles.flatMap((t) => [...words(t)]));
|
|
33
|
+
const matching = headings.filter((h) => [...words(h.text)].some((w) => wanted.has(w))).map((h) => h.text);
|
|
34
|
+
const picked = matching.length ? matching : headings.filter((h) => h.level === 2).map((h) => h.text);
|
|
35
|
+
return picked.slice(0, SECTIONS_SHOWN);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Après une livraison verte (L144) : pour chaque article déclaré, ouvre dans ce dépôt le lot « Article <projet> à rafraîchir »
|
|
39
|
+
* — ou, s'il en est déjà un d'ouvert, y ajoute une note. La note cite les titres publics livrés et les sections à relire ;
|
|
40
|
+
* le lot déclare le ou les dépôts voisins (`repos`, filtrés par le nom du projet : le dépôt de l'article est partagé entre projets).
|
|
41
|
+
* Les entrées de même nom partagent un seul lot, qui déclare tous leurs dépôts.
|
|
42
|
+
* Ne sauve pas le plan. Aucun titre public livré : rien à rafraîchir.
|
|
43
|
+
*/
|
|
44
|
+
export function openArticleLots(o) {
|
|
45
|
+
const result = { opened: [], noted: [], manual: [], warnings: [] };
|
|
46
|
+
const titles = [...new Set(o.delivered.map(o.publicTitle).filter((t) => !!t))];
|
|
47
|
+
if (!titles.length)
|
|
48
|
+
return result;
|
|
49
|
+
// Un lot par nom de projet : deux articles de même nom (deux dépôts voisins) se jouent ensemble dans le même lot.
|
|
50
|
+
const byName = new Map();
|
|
51
|
+
for (const rule of o.rules) {
|
|
52
|
+
const name = rule.name ?? o.plan.project;
|
|
53
|
+
byName.set(name, [...(byName.get(name) ?? []), rule]);
|
|
54
|
+
}
|
|
55
|
+
for (const [name, rules] of byName) {
|
|
56
|
+
const title = articleTitle(name);
|
|
57
|
+
const repos = [...new Set(rules.map((r) => r.repo))];
|
|
58
|
+
const articles = rules.map((rule) => {
|
|
59
|
+
const path = resolve(o.root, rule.repo, rule.file);
|
|
60
|
+
const sections = existsSync(path) ? articleSections(readFileSync(path, 'utf8'), titles) : null;
|
|
61
|
+
if (sections === null)
|
|
62
|
+
result.warnings.push(`article introuvable : ${path}`);
|
|
63
|
+
return (`Article : ${rule.repo}/${rule.file}. ` +
|
|
64
|
+
(sections === null ? 'Fichier introuvable à l\'ouverture du lot : vérifier le chemin (docs.articles). ' : `Sections à relire : ${sections.length ? sections.join(' ; ') : '(aucun titre trouvé : relire tout l\'article)'}. `));
|
|
65
|
+
});
|
|
66
|
+
const note = `Livraison ${o.sha.slice(0, 7)} : titres publics livrés ${titles.map((t) => `« ${t} »`).join(', ')}. ` +
|
|
67
|
+
articles.join('') +
|
|
68
|
+
`Réécrire ce que ces changements rendent faux ou incomplet, sans rien inventer ; les commits ${repos.length > 1 ? 'des dépôts' : 'du dépôt'} ${repos.join(', ')} citent le lot et « ${name} ».`;
|
|
69
|
+
if (o.plan.readonly) {
|
|
70
|
+
result.manual.push(`${title} — plan tenu par un autre outil, ouvrir le lot avec lui : ${note}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const open = o.plan.lots().find((l) => l.title === title && isOpen(l.status));
|
|
74
|
+
if (open) {
|
|
75
|
+
o.plan.note(open.id, note, o.today);
|
|
76
|
+
result.noted.push(open.id);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const id = o.plan.add(title, o.today, { estimate: 0.5 });
|
|
80
|
+
o.plan.setRepos(id, repos.map((path) => ({ path, cite: name })));
|
|
81
|
+
o.plan.note(id, note, o.today);
|
|
82
|
+
result.opened.push(id);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
export const describeArticle = (rule) => join(rule.repo, rule.file);
|
package/dist/audit.js
CHANGED
|
@@ -4,7 +4,8 @@ import { check } from './check.js';
|
|
|
4
4
|
import { parse } from 'yaml';
|
|
5
5
|
import { changedFiles, fileAt, readCommits } from './git.js';
|
|
6
6
|
import { linkCommits } from './link.js';
|
|
7
|
-
import { readNewsConfig } from './config.js';
|
|
7
|
+
import { readDocsConfig, readNewsConfig } from './config.js';
|
|
8
|
+
import { docSyncGaps, filesOf, gapMessage } from './docsync.js';
|
|
8
9
|
import { loadEntries, newsIssues, PUBLIC_TITLE_DEFAULT, publicTitleTooLong, reusedNewsTitle } from './news.js';
|
|
9
10
|
import { isRecurring } from './recurring.js';
|
|
10
11
|
import { isOpen } from './plan.js';
|
|
@@ -109,11 +110,24 @@ export function audit(plan, root, newsDir, today, opts = {}) {
|
|
|
109
110
|
const m = l.public ? publicTitleTooLong(l.public, max) : reused ? publicTitleTooLong(reused, max) : null;
|
|
110
111
|
return m ? [{ message: `${l.id} : ${reused && !l.public ? m.replace('titre public', 'titre public repris de la Nouveauté') : m}` }] : [];
|
|
111
112
|
});
|
|
112
|
-
const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, entries, newsDir), ...titles, ...gates, ...(plan.hasPublicField ? missingPublicTitles(lots, entries) : []),
|
|
113
|
+
const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, entries, newsDir), ...titles, ...gates, ...docSyncIssues(plan, root, all.byLot), ...(plan.hasPublicField ? missingPublicTitles(lots, entries) : []),
|
|
113
114
|
...plan.ignore.invalid.map((src) => ({ message: `ignore : motif invalide « ${src} »` }))];
|
|
114
115
|
// Un plan en lecture seule se corrige avec l'outil du projet : ne pas conseiller une commande raf qui refuserait.
|
|
115
116
|
return plan.readonly ? issues.map((i) => ({ ...i, message: i.message.replace(/ — raf (start|public) .*$/, '') })) : issues;
|
|
116
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Documentation en retard (L143, clé `docs.sync`) : un lot en cours — ou terminé à partir de `docs.since` — dont les commits de
|
|
120
|
+
* travail touchent des chemins sans toucher le document que la règle désigne. Les commits de plan ne comptent pas.
|
|
121
|
+
*/
|
|
122
|
+
export function docSyncIssues(plan, root, byLot) {
|
|
123
|
+
const { sync, since } = readDocsConfig(plan.configFile ?? join(root, 'cadence.yaml'));
|
|
124
|
+
if (!sync.length)
|
|
125
|
+
return [];
|
|
126
|
+
return plan
|
|
127
|
+
.lots()
|
|
128
|
+
.filter((l) => l.status === 'doing' || (l.status === 'done' && !!since && !!l.finished && l.finished >= since))
|
|
129
|
+
.flatMap((l) => docSyncGaps(sync, filesOf(root, workCommits(plan, root, byLot.get(l.id) ?? []))).map((g) => ({ message: gapMessage(l.id, g) })));
|
|
130
|
+
}
|
|
117
131
|
/** Commits qui portent du travail sur un lot : ni antérieurs à l'adoption du plan, ni réduits au plan. */
|
|
118
132
|
function workCommits(plan, root, commits) {
|
|
119
133
|
const adopted = plan.since;
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { readHookConfig, readNewsConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
4
|
+
import { readDocsConfig, readHookConfig, readNewsConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
5
|
import { repoSections, repoShas, resolveLotRepos } from './repos.js';
|
|
6
6
|
import { audit, exemptPlanOnly, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
7
7
|
import { short } from './check.js';
|
|
@@ -18,7 +18,7 @@ import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
|
18
18
|
import { schedule } from './schedule.js';
|
|
19
19
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
20
20
|
import { findProjects, leadTour, tourLine } from './lead.js';
|
|
21
|
-
import { orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
21
|
+
import { liveLines, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
22
22
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
23
23
|
import { sessionClose, sessionStart } from './session.js';
|
|
24
24
|
import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
|
|
@@ -155,9 +155,14 @@ function dispatch(argv, io) {
|
|
|
155
155
|
io.out(JSON.stringify(rows, null, 2));
|
|
156
156
|
else if (rows.length === 0)
|
|
157
157
|
io.out(`aucun projet sous ${parent} (docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
158
|
-
else
|
|
158
|
+
else {
|
|
159
159
|
for (const r of rows)
|
|
160
160
|
io.out(tourLine(r));
|
|
161
|
+
// les créneaux libres viennent du registre, le lead les lit au lieu de les calculer (L141)
|
|
162
|
+
const [slots] = liveLines();
|
|
163
|
+
if (slots)
|
|
164
|
+
io.out(slots);
|
|
165
|
+
}
|
|
161
166
|
return 0;
|
|
162
167
|
}
|
|
163
168
|
// --all balaie les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant.
|
|
@@ -426,7 +431,10 @@ function dispatch(argv, io) {
|
|
|
426
431
|
throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans le README)`);
|
|
427
432
|
const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
|
|
428
433
|
const plan = existsSync(planPath) ? loadPlan() : null;
|
|
429
|
-
const
|
|
434
|
+
const articles = readDocsConfig(configPath).articles;
|
|
435
|
+
// Sans public: d'un lot visible, le site reprend le titre de sa Nouveauté la plus récente.
|
|
436
|
+
const publicTitle = (lot) => lot.public ?? (lot.visible ? loadEntries(newsDir).find((e) => e.lots.includes(lot.id))?.title : undefined);
|
|
437
|
+
const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, args: rest, articles, publicTitle, out: io.out, err: io.err };
|
|
430
438
|
return deliver(ctx, realDeps(root));
|
|
431
439
|
}
|
|
432
440
|
case 'verify': {
|
package/dist/config.js
CHANGED
|
@@ -209,12 +209,14 @@ export function readSessionConfig(file) {
|
|
|
209
209
|
}
|
|
210
210
|
return config;
|
|
211
211
|
}
|
|
212
|
+
const EFFORTS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
213
|
+
export const EFFORT_STEPS = ['precheck', 'implement', 'fix', 'review', 'ux'];
|
|
212
214
|
const MODELS = ['haiku', 'sonnet', 'opus'];
|
|
213
215
|
const REVIEW_KEYS = ['threshold', 'light', 'full'];
|
|
214
|
-
const ORCH_KEYS = ['start', 'verdict', 'test', 'build', 'ux', 'precheck', 'review', 'permissionMode', 'addDirs', 'timeouts'];
|
|
216
|
+
const ORCH_KEYS = ['start', 'verdict', 'test', 'build', 'ux', 'precheck', 'review', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
|
|
215
217
|
/** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
|
|
216
218
|
export function readOrchestrateConfig(file) {
|
|
217
|
-
const config = { precheck: true, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
|
|
219
|
+
const config = { precheck: true, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', implement: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
|
|
218
220
|
if (!existsSync(file))
|
|
219
221
|
return config;
|
|
220
222
|
let raw;
|
|
@@ -274,6 +276,17 @@ export function readOrchestrateConfig(file) {
|
|
|
274
276
|
config.review[k] = m;
|
|
275
277
|
}
|
|
276
278
|
}
|
|
279
|
+
if (o.effort != null) {
|
|
280
|
+
if (!isObject(o.effort))
|
|
281
|
+
throw bad('effort doit être un objet { precheck, implement, fix, review, ux }');
|
|
282
|
+
for (const [k, v] of Object.entries(o.effort)) {
|
|
283
|
+
if (!EFFORT_STEPS.includes(k))
|
|
284
|
+
throw bad(`effort.${k} inconnu (attendu : ${EFFORT_STEPS.join(', ')})`);
|
|
285
|
+
if (!EFFORTS.includes(v))
|
|
286
|
+
throw bad(`effort.${k} : ${EFFORTS.join(', ')} attendu`);
|
|
287
|
+
config.effort[k] = v;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
277
290
|
if (o.ux != null) {
|
|
278
291
|
if (typeof o.ux === 'string' && o.ux.trim()) {
|
|
279
292
|
const u = o.ux.trim();
|
|
@@ -324,3 +337,69 @@ export function readOrchestrateConfig(file) {
|
|
|
324
337
|
}
|
|
325
338
|
return config;
|
|
326
339
|
}
|
|
340
|
+
const DOCS_KEYS = ['sync', 'since', 'articles'];
|
|
341
|
+
/** Clé `docs:` de cadence.yaml : `docs.sync`, la documentation qui doit suivre le code. */
|
|
342
|
+
export function readDocsConfig(file) {
|
|
343
|
+
const none = { sync: [] };
|
|
344
|
+
if (!existsSync(file))
|
|
345
|
+
return none;
|
|
346
|
+
let raw;
|
|
347
|
+
try {
|
|
348
|
+
raw = parse(readFileSync(file, 'utf8'));
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
352
|
+
}
|
|
353
|
+
const d = raw?.docs;
|
|
354
|
+
if (d == null)
|
|
355
|
+
return none;
|
|
356
|
+
const bad = (what) => new RafError(`${file} : docs.${what}`);
|
|
357
|
+
if (!isObject(d))
|
|
358
|
+
throw new RafError(`${file} : docs doit être un objet { sync, since, articles }`);
|
|
359
|
+
for (const k of Object.keys(d))
|
|
360
|
+
if (!DOCS_KEYS.includes(k))
|
|
361
|
+
throw bad(`${k} inconnu (attendu : ${DOCS_KEYS.join(', ')})`);
|
|
362
|
+
const config = { sync: [] };
|
|
363
|
+
if (d.since != null) {
|
|
364
|
+
if (!isDay(d.since))
|
|
365
|
+
throw bad(`since « ${String(d.since)} » n'est pas une date AAAA-MM-JJ`);
|
|
366
|
+
config.since = d.since;
|
|
367
|
+
}
|
|
368
|
+
if (d.sync != null) {
|
|
369
|
+
if (!Array.isArray(d.sync))
|
|
370
|
+
throw bad('sync doit être une liste de { paths, docs }');
|
|
371
|
+
d.sync.forEach((r, i) => {
|
|
372
|
+
if (!isObject(r))
|
|
373
|
+
throw bad(`sync[${i}] doit être un objet { paths, docs }`);
|
|
374
|
+
for (const k of Object.keys(r))
|
|
375
|
+
if (k !== 'paths' && k !== 'docs')
|
|
376
|
+
throw bad(`sync[${i}].${k} inconnu (attendu : paths, docs)`);
|
|
377
|
+
const entry = (key) => {
|
|
378
|
+
const v = list(r[key]);
|
|
379
|
+
if (!v.length || v.some((x) => !x.trim()))
|
|
380
|
+
throw bad(`sync[${i}].${key} : au moins un chemin non vide attendu`);
|
|
381
|
+
return v.map((x) => x.trim());
|
|
382
|
+
};
|
|
383
|
+
config.sync.push({ paths: entry('paths'), docs: entry('docs') });
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
if (d.articles != null) {
|
|
387
|
+
if (!Array.isArray(d.articles))
|
|
388
|
+
throw bad('articles doit être une liste de { repo, file }');
|
|
389
|
+
config.articles = d.articles.map((r, i) => {
|
|
390
|
+
if (!isObject(r))
|
|
391
|
+
throw bad(`articles[${i}] doit être un objet { repo, file }`);
|
|
392
|
+
for (const k of Object.keys(r))
|
|
393
|
+
if (!['repo', 'file', 'name'].includes(k))
|
|
394
|
+
throw bad(`articles[${i}].${k} inconnu (attendu : repo, file, name)`);
|
|
395
|
+
const text = (key) => {
|
|
396
|
+
const v = r[key];
|
|
397
|
+
if (typeof v !== 'string' || !v.trim())
|
|
398
|
+
throw bad(`articles[${i}].${key} : texte non vide attendu`);
|
|
399
|
+
return v.trim();
|
|
400
|
+
};
|
|
401
|
+
return { repo: text('repo'), file: text('file'), ...(r.name != null ? { name: text('name') } : {}) };
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return config;
|
|
405
|
+
}
|
package/dist/deliver.js
CHANGED
|
@@ -5,6 +5,7 @@ import { onTermination, readProcs, SIGNAL_GRACE_MS, TreeTracker } from './proc.j
|
|
|
5
5
|
import { isPlanOnly } from './audit.js';
|
|
6
6
|
import { headSha, isAncestor, onRemote, readCommits, repoStatus, resolveCommit } from './git.js';
|
|
7
7
|
import { citedRefs } from './link.js';
|
|
8
|
+
import { describeArticle, openArticleLots } from './articles.js';
|
|
8
9
|
import { RafError } from './plan.js';
|
|
9
10
|
import { appendDelivery, lastDelivery, lockAlive, lockFault, lockPath, readLock, releaseLock, removeStaleLock, writeLock } from './state.js';
|
|
10
11
|
const POLL_CI = 15_000;
|
|
@@ -235,6 +236,8 @@ export async function deliver(ctx, deps) {
|
|
|
235
236
|
out(' (aucune commande)');
|
|
236
237
|
config.deploy.forEach((c, i) => out(` ${i + 1}. ${c}`));
|
|
237
238
|
}
|
|
239
|
+
if (ctx.articles?.length)
|
|
240
|
+
out(` Articles rafraîchis après une livraison verte (lot « Article <projet> à rafraîchir ») : ${ctx.articles.map(describeArticle).join(', ')}`);
|
|
238
241
|
if (config.verify.length)
|
|
239
242
|
out(` Vérifications (réessayées pendant ${config.verifyTimeout} s) :`);
|
|
240
243
|
config.verify.forEach((c, i) => out(` ${i + 1}. ${describeCheck(c, sha)}`));
|
|
@@ -285,7 +288,8 @@ export async function deliver(ctx, deps) {
|
|
|
285
288
|
const head = script === null || !atHead ? sha : (headSha(ctx.root) ?? sha);
|
|
286
289
|
const moved = head !== sha && isAncestor(ctx.root, sha, head);
|
|
287
290
|
const final = moved ? head : sha;
|
|
288
|
-
const
|
|
291
|
+
const ids = deliveredIds(ctx, lastDelivery(ctx.state), final);
|
|
292
|
+
const delivered = deliveredLots(ctx, ids, lastDelivery(ctx.state), final);
|
|
289
293
|
appendDelivery(ctx.state, ctx.today, final);
|
|
290
294
|
if (moved)
|
|
291
295
|
out(`✓ livré : ${final.slice(0, 7)} (tête déplacée par le script depuis ${env.CADENCE_SHORT})`);
|
|
@@ -293,6 +297,7 @@ export async function deliver(ctx, deps) {
|
|
|
293
297
|
out(`✓ livré${config.verify.length ? ' et vérifié' : ''} : ${env.CADENCE_SHORT}`);
|
|
294
298
|
if (delivered)
|
|
295
299
|
out(delivered);
|
|
300
|
+
refreshArticles(ctx, ids, final);
|
|
296
301
|
return 0;
|
|
297
302
|
}
|
|
298
303
|
finally {
|
|
@@ -407,12 +412,10 @@ async function verifyAll(ctx, deps, sha, env) {
|
|
|
407
412
|
* nommé pour le contexte — les annoncer « livrés » ferait fermer à tort. L'état est celui du départ de
|
|
408
413
|
* la livraison : le plan a été lu avant que le script du projet ne ferme lui-même les lots qu'il livre.
|
|
409
414
|
*/
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return `livraison précédente (${prev.slice(0, 7)}) hors de l'historique de ${sha.slice(0, 7)} (réécrit ?) : lots livrés non calculés`;
|
|
415
|
-
}
|
|
415
|
+
/** Lots cités par les commits livrés depuis la livraison précédente ; vide quand elle est inconnue ou hors de l'historique. */
|
|
416
|
+
function deliveredIds(ctx, prev, sha) {
|
|
417
|
+
if (!ctx.plan || !prev || prev === sha || !isAncestor(ctx.root, prev, sha))
|
|
418
|
+
return [];
|
|
416
419
|
const lots = ctx.plan.lots();
|
|
417
420
|
const known = new Set((ctx.plan.readonly ? lots.filter((l) => l.status === 'doing') : lots).map((l) => l.id));
|
|
418
421
|
const ids = new Set();
|
|
@@ -423,7 +426,51 @@ function deliveredLots(ctx, prev, sha) {
|
|
|
423
426
|
if (known.has(r.lot))
|
|
424
427
|
ids.add(r.lot);
|
|
425
428
|
}
|
|
426
|
-
|
|
429
|
+
return [...ids].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
430
|
+
}
|
|
431
|
+
function deliveredLots(ctx, ids, prev, sha) {
|
|
432
|
+
if (!ctx.plan || !prev || prev === sha)
|
|
433
|
+
return null;
|
|
434
|
+
if (!isAncestor(ctx.root, prev, sha)) {
|
|
435
|
+
return `livraison précédente (${prev.slice(0, 7)}) hors de l'historique de ${sha.slice(0, 7)} (réécrit ?) : lots livrés non calculés`;
|
|
436
|
+
}
|
|
437
|
+
if (ids.length === 0)
|
|
427
438
|
return null;
|
|
428
|
-
return `livré : ${
|
|
439
|
+
return `livré : ${ids.join(', ')} — ${ctx.plan.readonly ? "à fermer avec l'outil du projet" : 'raf done'} si l'effet est celui attendu`;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Livraison verte (L144) : les lots livrés dont le titre est public changent ce que les articles externes racontent, on ouvre
|
|
443
|
+
* donc le lot qui les rafraîchit. Le plan est écrit mais pas commité (deliver ne commite jamais) ; une erreur ici ne défait pas
|
|
444
|
+
* une livraison déjà faite.
|
|
445
|
+
*/
|
|
446
|
+
function refreshArticles(ctx, ids, sha) {
|
|
447
|
+
if (!ctx.plan || !ctx.articles?.length || !ids.length)
|
|
448
|
+
return;
|
|
449
|
+
try {
|
|
450
|
+
// Relu : le script du projet a pu fermer des lots ou commiter le plan depuis le départ de la livraison, save() ne doit pas l'écraser.
|
|
451
|
+
const plan = ctx.plan.reloaded();
|
|
452
|
+
const wanted = new Set(ids);
|
|
453
|
+
const r = openArticleLots({
|
|
454
|
+
plan,
|
|
455
|
+
root: ctx.root,
|
|
456
|
+
rules: ctx.articles,
|
|
457
|
+
delivered: plan.lots().filter((l) => wanted.has(l.id)),
|
|
458
|
+
publicTitle: ctx.publicTitle ?? ((l) => l.public),
|
|
459
|
+
sha,
|
|
460
|
+
today: ctx.today,
|
|
461
|
+
});
|
|
462
|
+
for (const w of r.warnings)
|
|
463
|
+
ctx.err(`deliver : ${w}`);
|
|
464
|
+
if (r.opened.length || r.noted.length)
|
|
465
|
+
plan.save();
|
|
466
|
+
for (const id of r.opened)
|
|
467
|
+
ctx.out(`article à rafraîchir : ${id} « ${plan.lot(id).title} » ouvert (plan modifié, à commiter) — à jouer dans une vague ordinaire`);
|
|
468
|
+
for (const id of r.noted)
|
|
469
|
+
ctx.out(`article à rafraîchir : ${id} déjà ouvert, titres livrés ajoutés en note (plan modifié, à commiter)`);
|
|
470
|
+
for (const m of r.manual)
|
|
471
|
+
ctx.out(`article à rafraîchir : ${m}`);
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
ctx.err(`deliver : article à rafraîchir non ouvert — ${e.message}`);
|
|
475
|
+
}
|
|
429
476
|
}
|
package/dist/docsync.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { changedFiles } from './git.js';
|
|
2
|
+
/** Motif de chemin : `*` reste dans un dossier, `**` le traverse, `?` un caractère, un `/` final prend tout le dossier ; sinon chemin exact. */
|
|
3
|
+
export function docSyncMatcher(pattern) {
|
|
4
|
+
const p = pattern.trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
5
|
+
const source = (p.endsWith('/') ? `${p}**` : p)
|
|
6
|
+
.split(/(\*\*\/?|\*|\?)/)
|
|
7
|
+
.map((part) => (part === '**/' ? '(?:.*/)?' : part === '**' ? '.*' : part === '*' ? '[^/]*' : part === '?' ? '[^/]' : part.replace(/[.+^${}()|[\]\\]/g, '\\$&')))
|
|
8
|
+
.join('');
|
|
9
|
+
const re = new RegExp(`^${source}$`);
|
|
10
|
+
return (file) => re.test(file);
|
|
11
|
+
}
|
|
12
|
+
/** Liste de motifs : un fichier en fait partie s'il correspond à un motif et à aucun motif « !… » (exclusion, ex. `!**\/*.test.ts`). */
|
|
13
|
+
export function docSyncSet(patterns) {
|
|
14
|
+
const yes = patterns.filter((p) => !p.startsWith('!')).map(docSyncMatcher);
|
|
15
|
+
const no = patterns.filter((p) => p.startsWith('!')).map((p) => docSyncMatcher(p.slice(1)));
|
|
16
|
+
return (file) => yes.some((m) => m(file)) && !no.some((m) => m(file));
|
|
17
|
+
}
|
|
18
|
+
/** Fichiers modifiés par un ensemble de commits, sans doublon. */
|
|
19
|
+
export function filesOf(root, commits) {
|
|
20
|
+
return new Set(commits.flatMap((c) => changedFiles(root, c.sha)));
|
|
21
|
+
}
|
|
22
|
+
/** Les règles que ces fichiers enfreignent : ils touchent `paths` sans qu'aucun `docs` ne soit parmi eux. */
|
|
23
|
+
export function docSyncGaps(rules, files) {
|
|
24
|
+
const gaps = [];
|
|
25
|
+
for (const rule of rules) {
|
|
26
|
+
if ([...files].some(docSyncSet(rule.docs)))
|
|
27
|
+
continue;
|
|
28
|
+
const inPaths = docSyncSet(rule.paths);
|
|
29
|
+
const touched = [...files].filter(inPaths).sort();
|
|
30
|
+
if (touched.length)
|
|
31
|
+
gaps.push({ rule, touched });
|
|
32
|
+
}
|
|
33
|
+
return gaps;
|
|
34
|
+
}
|
|
35
|
+
const SHOWN = 5;
|
|
36
|
+
/** `a, b, c, … (+2)` : les premiers fichiers, le reste compté. */
|
|
37
|
+
function listFiles(files) {
|
|
38
|
+
return files.length <= SHOWN ? files.join(', ') : `${files.slice(0, SHOWN).join(', ')}, … (+${files.length - SHOWN})`;
|
|
39
|
+
}
|
|
40
|
+
/** La ligne de `raf check` et de `cadence lead tour` pour un lot. */
|
|
41
|
+
export function gapMessage(lot, gap) {
|
|
42
|
+
return `${lot} : documentation en retard — ${listFiles(gap.touched)} sans toucher ${gap.rule.docs.join(' ou ')} (docs.sync)`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Consigne du brief de revue (L143) : la liste calculée par le programme, à passer en constat majeur. Vide sans règle
|
|
46
|
+
* déclarée ; sans écart, une ligne qui dit que le programme a vérifié (le relecteur juge encore le contenu).
|
|
47
|
+
*/
|
|
48
|
+
export function docSyncBrief(rules, gaps) {
|
|
49
|
+
if (!rules.length)
|
|
50
|
+
return '';
|
|
51
|
+
if (!gaps.length)
|
|
52
|
+
return 'Documentation (cadence.yaml docs.sync): the program checked that the commits of the lot touch the documents their paths call for. Still read the documents themselves against the change.';
|
|
53
|
+
return [
|
|
54
|
+
'Documentation (cadence.yaml docs.sync): the program computed that the commits of the lot touch code without touching the document that describes it. Report ONE MAJOR finding per line below, quoting the line, unless the change really leaves the document true (say why then):',
|
|
55
|
+
...gaps.map((g) => `- ${listFiles(g.touched)} changed, but none of ${g.rule.docs.join(', ')} was touched`),
|
|
56
|
+
].join('\n');
|
|
57
|
+
}
|
|
@@ -69,7 +69,7 @@ export function loadTemplates(dir = TEMPLATES_DIR) {
|
|
|
69
69
|
export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
|
|
70
70
|
const text = typeof source === 'string' ? loadTemplates(source)[kind] : source[kind];
|
|
71
71
|
const out = text.replace(/\{\{(\w+)\}\}/g, (_m, name) => {
|
|
72
|
-
const v = { ...vars, repos: vars.repos ?? '' }[name];
|
|
72
|
+
const v = { ...vars, repos: vars.repos ?? '', docs: vars.docs ?? '' }[name];
|
|
73
73
|
if (v === undefined)
|
|
74
74
|
throw new RafError(`gabarit ${FILES[kind]} : valeur manquante pour {{${name}}}`);
|
|
75
75
|
return v;
|
|
@@ -10,7 +10,7 @@ import { stopApps } from './app.js';
|
|
|
10
10
|
import { Plan, RafError, isOpen } from '../plan.js';
|
|
11
11
|
import { AGENTS_DIR } from '../skills.js';
|
|
12
12
|
import { pidAlive, sharedStateDir } from '../state.js';
|
|
13
|
-
import { acquireSlot, cadenceHome, liveSlots, liveWaves, registerWave, unregisterWave, updateWaveRepos } from './registry.js';
|
|
13
|
+
import { acquireSlot, cadenceHome, freeSlots, liveSlots, liveWaves, registerWave, unregisterWave, updateWaveRepos } from './registry.js';
|
|
14
14
|
import { loadTemplates, newsText, objective, renderBrief } from './briefs.js';
|
|
15
15
|
import { Budget, MAX_PASSES, countInterrupted, needsPrecheck } from './cycle.js';
|
|
16
16
|
import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
|
|
@@ -137,6 +137,14 @@ function resolveTargets(args, io, refusals) {
|
|
|
137
137
|
async function trackedDirty(repo) {
|
|
138
138
|
return (await snapshot(repo, { remote: false })).tracked.map((l) => l.slice(3));
|
|
139
139
|
}
|
|
140
|
+
/** `--effort` date de claude 2.1.284 (L137). Une version illisible n'est pas refusée. */
|
|
141
|
+
function olderThanEffort(version) {
|
|
142
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
143
|
+
if (!m)
|
|
144
|
+
return false;
|
|
145
|
+
const [a, b, c] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
146
|
+
return a < 2 || (a === 2 && (b < 1 || (b === 1 && c < 284)));
|
|
147
|
+
}
|
|
140
148
|
/** Préconditions : tout ce qui peut être refusé l'est ici, avant d'agir, sans verrou ni écriture. */
|
|
141
149
|
async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
142
150
|
const refusals = [];
|
|
@@ -162,6 +170,10 @@ async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
162
170
|
refusals.push(`${key} : ${e.message}`);
|
|
163
171
|
continue;
|
|
164
172
|
}
|
|
173
|
+
if (info && olderThanEffort(info.version) && Object.values(env.config.effort).some((e) => e !== 'default')) {
|
|
174
|
+
refusals.push(`${key} : claude ${info.version} n'a pas --effort (2.1.284 requise) : mettre claude à jour, ou orchestrate.effort à « default » pour chaque passe`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
165
177
|
const lot = plan.lots().find((l) => l.id === t.lot);
|
|
166
178
|
if (!lot) {
|
|
167
179
|
refusals.push(`${key} : lot inconnu`);
|
|
@@ -283,7 +295,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
283
295
|
const brief = renderBrief(s.kind, vars, deps.templatesDir);
|
|
284
296
|
writeFileSync(file, brief);
|
|
285
297
|
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small).playwright;
|
|
286
|
-
const args = buildArgs({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model, schema: schemaFor(s.kind), agent: s.kind === 'implement' ? undefined : s.kind === 'ux' ? 'ux-reviewer' : s.kind === 'precheck' ? 'precheck-reader' : 'code-reviewer', cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && s.kind === 'implement' ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
|
|
298
|
+
const args = buildArgs({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model, effort: env.config.effort[s.kind === 'review-small' ? 'review' : s.kind], schema: schemaFor(s.kind), agent: s.kind === 'implement' ? undefined : s.kind === 'ux' ? 'ux-reviewer' : s.kind === 'precheck' ? 'precheck-reader' : 'code-reviewer', cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && s.kind === 'implement' ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
|
|
287
299
|
io.out(` ${s.kind} : claude ${args.join(' ')}`);
|
|
288
300
|
io.out(` brief : ${file}`);
|
|
289
301
|
const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small));
|
|
@@ -301,14 +313,14 @@ function duration(ms) {
|
|
|
301
313
|
const s = Math.round(ms / 1000);
|
|
302
314
|
return s < 60 ? `${s} s` : `${Math.floor(s / 60)} min ${String(s % 60).padStart(2, '0')} s`;
|
|
303
315
|
}
|
|
304
|
-
/** Vagues vivantes (toutes, quel que soit leur dossier de départ) et dépôts qu'elles tiennent. */
|
|
305
|
-
function liveLines() {
|
|
316
|
+
/** Vagues vivantes (toutes, quel que soit leur dossier de départ), créneaux libres et dépôts qu'elles tiennent. */
|
|
317
|
+
export function liveLines() {
|
|
306
318
|
const home = cadenceHome();
|
|
307
319
|
const waves = liveWaves(home);
|
|
308
320
|
if (waves.length === 0)
|
|
309
321
|
return [];
|
|
310
322
|
const slots = liveSlots(home).length;
|
|
311
|
-
const lines = [`vagues en cours : ${waves.length} · sessions en cours : ${slots}`];
|
|
323
|
+
const lines = [`vagues en cours : ${waves.length} · sessions en cours : ${slots} · créneaux libres : ${freeSlots(waves, DEFAULT_MAX_SESSIONS)} sur ${DEFAULT_MAX_SESSIONS}`];
|
|
312
324
|
for (const w of waves)
|
|
313
325
|
lines.push(` ${w.wave} (pid ${w.pid}, depuis ${w.started}) lancée depuis ${w.cwd} · plafond ${w.cap ?? '?'} · dépôts : ${w.repos.join(', ')}`);
|
|
314
326
|
return lines;
|
|
@@ -4,6 +4,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { dirname, join, relative } from 'node:path';
|
|
5
5
|
import { writeFileSync } from 'node:fs';
|
|
6
6
|
import { lotCommits, lotWork } from '../audit.js';
|
|
7
|
+
import { readDocsConfig } from '../config.js';
|
|
8
|
+
import { docSyncBrief, docSyncGaps, filesOf } from '../docsync.js';
|
|
7
9
|
import { readCommits, resolveCommit } from '../git.js';
|
|
8
10
|
import { citedRefs } from '../link.js';
|
|
9
11
|
import { repoShas, repoWork } from '../repos.js';
|
|
@@ -198,6 +200,14 @@ function checksText(c) {
|
|
|
198
200
|
const runs = k.runs.map((r) => `- \`${r.command}\` (${r.label}): ${r.code === 0 ? 'green' : `RED, exit code ${r.code}`}`);
|
|
199
201
|
return [`The program already ran these checks at HEAD ${k.head.slice(0, 7)}, just before this review:`, ...runs, 'Take these results as given: do not rebuild, do not rerun the whole suite, and do not wait on them. Run only a check they do not cover (a targeted test of a case you doubt), and list under "nonVerifie" what neither they nor you verified.'].join('\n');
|
|
200
202
|
}
|
|
203
|
+
/** Consigne docs.sync du brief de revue (L143) : calculée sur les commits du lot au moment de la revue, pas écrite d'avance. */
|
|
204
|
+
function docsText(c) {
|
|
205
|
+
const plan = c.loadPlan();
|
|
206
|
+
const { sync } = readDocsConfig(plan.configFile ?? join(c.lot.repo, 'cadence.yaml'));
|
|
207
|
+
if (!sync.length)
|
|
208
|
+
return '';
|
|
209
|
+
return docSyncBrief(sync, docSyncGaps(sync, filesOf(c.lot.repo, lotWork(plan, c.lot.repo, c.lot.lot))));
|
|
210
|
+
}
|
|
201
211
|
/** Gabarit d'une étape : la passe des mineurs et la revue courte qui la suit ont chacune le leur. */
|
|
202
212
|
function briefName(l, kind) {
|
|
203
213
|
if (kind === 'fix' && l.minorFix)
|
|
@@ -210,7 +220,7 @@ function briefFor(c, kind) {
|
|
|
210
220
|
const plan = c.loadPlan();
|
|
211
221
|
const l = c.lot;
|
|
212
222
|
const lot = plan.lot(l.lot);
|
|
213
|
-
const vars = { chemin: l.repo, lot: l.lot, titre: lot.title, objectif: objective(lot), commits: '', reponse: '', constats: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', news: '', captures: '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []) };
|
|
223
|
+
const vars = { chemin: l.repo, lot: l.lot, titre: lot.title, objectif: objective(lot), commits: '', reponse: '', constats: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []) };
|
|
214
224
|
if (l.visible) {
|
|
215
225
|
const pwDir = playwrightDir(c.wave.store.lotDir(l.project, l.lot));
|
|
216
226
|
vars.news = newsText(l.lot, pwDir);
|
|
@@ -272,12 +282,13 @@ async function session(c, kind) {
|
|
|
272
282
|
return overBudget(c);
|
|
273
283
|
const write = kind === 'implement' || kind === 'fix';
|
|
274
284
|
const model = write ? l.model : kind === 'precheck' ? 'sonnet' : reviewModel(c, kind); // le contrôle préalable ne fait que lire : pas d'Opus
|
|
285
|
+
const effort = c.config.effort[kind === 'review-small' ? 'review' : kind];
|
|
275
286
|
const before = await snapshot(l.repo);
|
|
276
287
|
const neighbours = l.repos ?? [];
|
|
277
288
|
const beforeOthers = await Promise.all(neighbours.map((r) => snapshot(r.path)));
|
|
278
289
|
const n = l.steps.length + 1;
|
|
279
290
|
const sessionId = randomUUID();
|
|
280
|
-
const step = { n, kind, model, status: 'running', sessionId, started: new Date().toISOString(), headBefore: before.head ?? undefined };
|
|
291
|
+
const step = { n, kind, model, ...(effort !== 'default' ? { effort } : {}), status: 'running', sessionId, started: new Date().toISOString(), headBefore: before.head ?? undefined };
|
|
281
292
|
l.steps.push(step);
|
|
282
293
|
const label = { implement: 'implementing', fix: 'fixing', review: 'reviewing', ux: 'reviewing', 'review-small': 'reviewing', precheck: 'implementing' };
|
|
283
294
|
transition(c, label[kind]);
|
|
@@ -301,6 +312,7 @@ async function session(c, kind) {
|
|
|
301
312
|
sessionId,
|
|
302
313
|
brief,
|
|
303
314
|
model,
|
|
315
|
+
effort,
|
|
304
316
|
schema: schemaFor(kind),
|
|
305
317
|
agent: kind === 'ux' ? 'ux-reviewer' : kind === 'precheck' ? 'precheck-reader' : write ? undefined : 'code-reviewer',
|
|
306
318
|
cwd: l.repo,
|
|
@@ -40,8 +40,11 @@ function agentsFor(spec, agents) {
|
|
|
40
40
|
return agents;
|
|
41
41
|
return { ...agents, [spec.agent]: { ...a, tools: [...a.tools, ...(a.tools.includes(PLAYWRIGHT_TOOLS) ? [] : [PLAYWRIGHT_TOOLS])] } };
|
|
42
42
|
}
|
|
43
|
+
function effortArgs(spec) {
|
|
44
|
+
return spec.effort && spec.effort !== 'default' ? ['--effort', spec.effort] : [];
|
|
45
|
+
}
|
|
43
46
|
export function buildArgs(spec, agents) {
|
|
44
|
-
const args = ['-p', spec.brief, '--output-format', 'json', '--json-schema', JSON.stringify(spec.schema), '--model', spec.model];
|
|
47
|
+
const args = ['-p', spec.brief, '--output-format', 'json', '--json-schema', JSON.stringify(spec.schema), '--model', spec.model, ...effortArgs(spec)];
|
|
45
48
|
if (spec.agent) {
|
|
46
49
|
if (!agents[spec.agent])
|
|
47
50
|
throw new RafError(`agent introuvable dans le paquet : ${spec.agent}`);
|
|
@@ -81,7 +84,7 @@ export function readAgents(dir) {
|
|
|
81
84
|
export const FORMAT_RETRY_PROMPT = 'Return your report now in the required format (the JSON structure of the schema), exactly as you concluded it. Do not do any further work, do not add anything else.';
|
|
82
85
|
/** Arguments de la relance : même session (--resume), mêmes schéma, modèle, agent (outils restreints), mode de permission, dossiers et interdits. */
|
|
83
86
|
export function buildRetryArgs(spec, sessionId, agents) {
|
|
84
|
-
const args = ['-p', FORMAT_RETRY_PROMPT, '--output-format', 'json', '--json-schema', JSON.stringify(spec.schema), '--model', spec.model];
|
|
87
|
+
const args = ['-p', FORMAT_RETRY_PROMPT, '--output-format', 'json', '--json-schema', JSON.stringify(spec.schema), '--model', spec.model, ...effortArgs(spec)];
|
|
85
88
|
if (spec.agent) {
|
|
86
89
|
if (!agents[spec.agent])
|
|
87
90
|
throw new RafError(`agent introuvable dans le paquet : ${spec.agent}`);
|
|
@@ -44,6 +44,16 @@ export function liveWaves(home) {
|
|
|
44
44
|
}
|
|
45
45
|
return out.sort((a, b) => a.started.localeCompare(b.started));
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Les créneaux que le lead peut encore donner à des sous-agents : `total` moins, pour chaque vague vivante, le
|
|
49
|
+
* plus petit de son plafond et du nombre de dépôts qu'elle tient encore (ses pas prennent un créneau et le
|
|
50
|
+
* rendent entre deux étapes : les sessions vivantes à l'instant ne disent pas ce que la vague va reprendre).
|
|
51
|
+
* Une vague sans plafond connu compte pour `total`.
|
|
52
|
+
*/
|
|
53
|
+
export function freeSlots(waves, total) {
|
|
54
|
+
const held = waves.reduce((n, w) => n + Math.min(w.cap ?? total, w.repos.length), 0);
|
|
55
|
+
return Math.max(0, total - held);
|
|
56
|
+
}
|
|
47
57
|
/** Inscrit la vague ; écarte au passage les entrées de processus morts (les `.tmp` d'une inscription en cours ne sont pas touchés). */
|
|
48
58
|
export function registerWave(home, given) {
|
|
49
59
|
const w = { ...given, start: given.start ?? processStart(given.pid) ?? undefined };
|
|
@@ -53,15 +53,19 @@ export class RunStore {
|
|
|
53
53
|
static runsDir(launchDir) {
|
|
54
54
|
return join(launchDir, '.cadence', 'runs');
|
|
55
55
|
}
|
|
56
|
-
/** La dernière vague (par identifiant) ; avec `unfinished`, la dernière qui n'est pas terminée. */
|
|
56
|
+
/** La dernière vague lancée (par date de lancement : un identifiant libre comme `ol-gains-1` ne se range pas par ordre alphabétique) ; avec `unfinished`, la dernière qui n'est pas terminée. */
|
|
57
57
|
static last(launchDir, opts = {}) {
|
|
58
58
|
const base = RunStore.runsDir(launchDir);
|
|
59
59
|
if (!existsSync(base))
|
|
60
60
|
return null;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const stores = readdirSync(base)
|
|
62
|
+
.filter((n) => existsSync(join(base, n, 'wave.json')))
|
|
63
|
+
.map((id) => new RunStore(launchDir, id))
|
|
64
|
+
.map((store) => ({ store, wave: store.readWave() }))
|
|
65
|
+
// lancement le plus récent d'abord ; à date égale (ou illisible), l'identifiant le plus grand
|
|
66
|
+
.sort((a, b) => (b.wave?.created ?? '').localeCompare(a.wave?.created ?? '') || b.store.id.localeCompare(a.store.id));
|
|
67
|
+
for (const { store, wave } of stores) {
|
|
68
|
+
if (!opts.unfinished || wave?.status !== 'done')
|
|
65
69
|
return store;
|
|
66
70
|
}
|
|
67
71
|
return null;
|
package/dist/plan.js
CHANGED
|
@@ -97,6 +97,10 @@ export class Plan {
|
|
|
97
97
|
lots.flow = false; // « lots: [] » écrit par init : passer en style bloc
|
|
98
98
|
return new Plan(path, doc, !/^lots:[^\n]*\n(?:#[^\n]*\n)*- /m.test(text), settings);
|
|
99
99
|
}
|
|
100
|
+
/** Le plan tel qu'il est maintenant sur le disque (un script a pu l'écrire depuis la lecture), mêmes réglages. */
|
|
101
|
+
reloaded() {
|
|
102
|
+
return Plan.load(this.path, this.settings);
|
|
103
|
+
}
|
|
100
104
|
save() {
|
|
101
105
|
this.writable();
|
|
102
106
|
writeFileSync(this.path, this.doc.toString({ lineWidth: 0, indentSeq: this.indentSeq }));
|
|
@@ -317,6 +321,13 @@ export class Plan {
|
|
|
317
321
|
this.doc.get('lots').add(node);
|
|
318
322
|
return id;
|
|
319
323
|
}
|
|
324
|
+
/** Déclare les dépôts voisins d'un lot (clé `repos:`) : un chemin seul, ou `{ path, cite }` quand le voisin est partagé. */
|
|
325
|
+
setRepos(lotId, repos) {
|
|
326
|
+
this.writable();
|
|
327
|
+
const node = this.doc.createNode(repos.map((r) => (r.cite === undefined ? r.path : { path: r.path, cite: r.cite })));
|
|
328
|
+
node.flow = true;
|
|
329
|
+
this.lotNode(lotId).set('repos', node);
|
|
330
|
+
}
|
|
320
331
|
addTask(lotId, title) {
|
|
321
332
|
this.writable();
|
|
322
333
|
const lot = this.lotNode(lotId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sylvain Ladoire",
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -18,9 +18,9 @@ commands (its CLAUDE.md names them), never with `raf start|done|note|ux|review`.
|
|
|
18
18
|
(section 2b) count toward this limit: orchestrate sessions plus subagents never exceed two. A wave
|
|
19
19
|
already caps itself (`--max-sessions`, 2 by default, shared by every wave); before starting a
|
|
20
20
|
subagent, check `cadence orchestrate --status`, and while a wave runs, start none beyond the free
|
|
21
|
-
slots.
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
slots. Read the free slots, do not compute them: `cadence orchestrate --status` and `cadence lead tour` print
|
|
22
|
+
`créneaux libres : N sur 2` (the registry subtracts, for each live wave, min(cap, repositories it still holds)
|
|
23
|
+
from the two slots; no line from `lead tour` means no live wave, so both are free). Never raise `--max-sessions` above two to speed a wave up.
|
|
24
24
|
- **Never two subagents in the same repository at the same time**, and the lead does not commit in a
|
|
25
25
|
repository where a subagent is working.
|
|
26
26
|
- **Deliveries and `raf ux` / `raf review` verdicts are done by the lead, one project at a time** —
|
|
@@ -5,6 +5,7 @@ cite the lot (`raf commits {{lot}}`, or `git log`), run the checks, report real
|
|
|
5
5
|
{{repos}}
|
|
6
6
|
{{ux}}
|
|
7
7
|
{{checks}}
|
|
8
|
+
{{docs}}
|
|
8
9
|
{{choix}}
|
|
9
10
|
A proposed sub-task describes an observable bug (a wrong output, a crash, a measured regression); any other minor finding stays a note of this lot, not a sub-task.
|
|
10
11
|
You are read-only: do not modify, commit, push or run `raf review|ux|done`. Do not launch subagents.
|
|
@@ -3,6 +3,7 @@ You are given the repository path and the lot id only: read the diff yourself fr
|
|
|
3
3
|
cite the lot (`raf commits {{lot}}`, or `git log`), run the checks, report real defects only.
|
|
4
4
|
{{repos}}
|
|
5
5
|
{{checks}}
|
|
6
|
+
{{docs}}
|
|
6
7
|
{{choix}}
|
|
7
8
|
A proposed sub-task describes an observable bug (a wrong output, a crash, a measured regression); any other minor finding stays a note of this lot, not a sub-task.
|
|
8
9
|
You are read-only: do not modify, commit, push or run `raf review|ux|done`. Do not launch subagents.
|