@sylad/cadence 0.6.0 → 0.8.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 +143 -11
- package/bin/cadence.js +6 -2
- package/dist/audit.js +36 -14
- package/dist/cli.js +76 -9
- package/dist/deliver.js +90 -32
- package/dist/git.js +12 -2
- package/dist/link.js +15 -1
- package/dist/news.js +58 -13
- package/dist/plan.js +17 -3
- package/dist/proc.js +161 -0
- package/dist/session.js +1 -0
- package/dist/state.js +10 -7
- package/dist/verify.js +114 -0
- package/package.json +13 -3
- package/skills/deliver/SKILL.md +3 -1
- package/skills/session-close/SKILL.md +5 -0
- package/skills/session-start/SKILL.md +3 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"name": "cadence",
|
|
8
8
|
"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).",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.8.0",
|
|
10
10
|
"source": "./",
|
|
11
11
|
"author": { "name": "Sylvain Ladoire" }
|
|
12
12
|
}
|
|
@@ -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.8.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
|
@@ -25,8 +25,23 @@ reports a page left empty or in error.
|
|
|
25
25
|
Your comments and hand edits are preserved.
|
|
26
26
|
- A commit belongs to a lot when its message cites the id: `feat(L3): …`,
|
|
27
27
|
`fix: L3/t1 …`. The link is **computed from `git log`**, never stored, so
|
|
28
|
-
committing never dirties the plan.
|
|
28
|
+
committing never dirties the plan. The id is read as a whole word: `XL3`,
|
|
29
|
+
`L3x` and `L3.4` do not cite `L3`, while `L3.` at the end of a sentence does.
|
|
30
|
+
When the subject has a scope that cites lots (`feat(L3): …`,
|
|
31
|
+
`chore(L31,L32): …`), **the scope alone decides**: a passing mention
|
|
32
|
+
(`page équipe (L27)`), a range (`L28–L31`, `L45 à L48`) or the body does not
|
|
33
|
+
count. Without a scope citing a lot, the whole message is read as before.
|
|
29
34
|
- `raf check` audits drift between the plan and the history.
|
|
35
|
+
- Plan upkeep needs no lot. A commit is plan upkeep when **every file it
|
|
36
|
+
touches is a plan file**: the plan itself, its Gantt page (only at its default place, `gantt.html` next to the plan — written elsewhere with `raf gantt -o`, declare it under `plan.files`), the `plan:` key of the config file in use (`cadence.yaml`, or
|
|
37
|
+
the `--config` file; a change to `deliver:`/`session:` is work), or a file the
|
|
38
|
+
project lists under `plan.files` in `cadence.yaml` (a page it generates from
|
|
39
|
+
the plan, a journal). Such a commit is never a "commit without a lot", and it
|
|
40
|
+
does not count as work on the lots it cites: it is absent from `raf commits`,
|
|
41
|
+
does not start a `todo` lot, does not make a code review stale, and
|
|
42
|
+
`cadence deliver` does not announce the lots it cites as delivered. The files
|
|
43
|
+
decide, never the subject: a `chore(plan): …` commit that touches a source
|
|
44
|
+
file is a commit like any other.
|
|
30
45
|
- `raf gantt` writes a single self-contained HTML page (no server, no CDN).
|
|
31
46
|
|
|
32
47
|
```sh
|
|
@@ -56,7 +71,7 @@ raf gantt # docs/plan/gantt.html
|
|
|
56
71
|
| `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first |
|
|
57
72
|
| `raf now` | what to do next |
|
|
58
73
|
| `raf list [--status s]` | flat list |
|
|
59
|
-
| `raf check [--since date] [--idle 7]` | since the plan's adoption date by default: commits without a lot (commits touching only
|
|
74
|
+
| `raf check [--since date] [--idle 7]` | since the plan's adoption date by default: commits without a lot (commits touching only plan files are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
|
|
60
75
|
| `raf gantt [-o file]` | standalone Gantt page |
|
|
61
76
|
| `raf hook install` | add the (non-blocking, read-only) post-commit hook |
|
|
62
77
|
|
|
@@ -96,6 +111,18 @@ format, and the plan stays writable:
|
|
|
96
111
|
plan: planning/todo.yaml
|
|
97
112
|
```
|
|
98
113
|
|
|
114
|
+
A project that publishes its plan (a JSON generated from it and committed with it)
|
|
115
|
+
declares that file, so a commit touching only the plan and its published copy is
|
|
116
|
+
plan upkeep; the plan keeps raf's format and stays writable:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
plan:
|
|
120
|
+
files: [frontend/public/plan-data/plan.json]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Until the file is declared, such a commit is reported as a "commit without a
|
|
124
|
+
lot" when it cites none, and counts as work on the lots it cites.
|
|
125
|
+
|
|
99
126
|
A project that already keeps its plan with its own tool is read **without migrating it**: describe
|
|
100
127
|
the file, and `raf now`, `raf list`, `raf commits`, `raf check`, `raf gantt` and
|
|
101
128
|
`cadence session start|close` work on it. Such a plan is **read-only** —
|
|
@@ -177,6 +204,15 @@ captures: [captures/l8.png]
|
|
|
177
204
|
Imported statements now read **3.000** as three thousand, not three.
|
|
178
205
|
```
|
|
179
206
|
|
|
207
|
+
A screenshot can say what it shows: write it as `{ file, alt }` instead of a
|
|
208
|
+
bare path, one text per screenshot.
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
captures:
|
|
212
|
+
- { file: captures/l8-before.png, alt: "Statement total read as 3 instead of 3,000" }
|
|
213
|
+
- captures/l8-after.png # a bare path still works: no alternative text
|
|
214
|
+
```
|
|
215
|
+
|
|
180
216
|
| Command | Effect |
|
|
181
217
|
|---|---|
|
|
182
218
|
| `cadence news new <lot…> [--title t]` | entry skeleton, dated and timed now (`date`, `created`), titled after the lot |
|
|
@@ -189,7 +225,11 @@ Imported statements now read **3.000** as three thousand, not three.
|
|
|
189
225
|
The Markdown is deliberately small: paragraphs, `-` lists, `**bold**`,
|
|
190
226
|
`` `code` ``, `[links](url)`; everything else is escaped text. The JSON holds
|
|
191
227
|
`{ project, generated, entries: [{ slug, title, date, lots, captures, html }] }`,
|
|
192
|
-
with screenshot paths relative to the JSON file.
|
|
228
|
+
with screenshot paths relative to the JSON file. `captures` is always a list of
|
|
229
|
+
paths; an entry that gives at least one alternative text also carries `alts`,
|
|
230
|
+
the texts in the same order (`""` for a screenshot without one) — an entry
|
|
231
|
+
without any keeps exactly the shape above. The built page puts the text in the
|
|
232
|
+
image's `alt`, and falls back to "Capture : <title>".
|
|
193
233
|
|
|
194
234
|
**Order.** Everywhere (`list`, `build`, the JSON), entries are strictly newest
|
|
195
235
|
first: by `date`, then, on the same day, by creation time. Every entry carries
|
|
@@ -216,8 +256,8 @@ raf ux L8 "no screen: calculation fix"
|
|
|
216
256
|
|
|
217
257
|
With the rule on, `raf done` refuses a visible lot without a review (`--force`
|
|
218
258
|
to override) and `raf check` reports visible lots finished after the `uxSince`
|
|
219
|
-
day without one. An empty verdict is refused
|
|
220
|
-
affected.
|
|
259
|
+
day without one. An empty verdict is refused, and one left empty or blank by hand
|
|
260
|
+
in the YAML counts as no review. Plans without `uxSince` are not affected.
|
|
221
261
|
|
|
222
262
|
### Code review
|
|
223
263
|
|
|
@@ -231,8 +271,8 @@ The counterpart of the UX review, off by default. With the rule on, `raf done`
|
|
|
231
271
|
refuses a lot that has at least one commit citing it and no recorded verdict
|
|
232
272
|
(`--force` to override), and `raf check` reports such lots finished after the
|
|
233
273
|
`reviewSince` day. A lot with no commit has nothing to review; neither does a
|
|
234
|
-
lot whose only commits touch
|
|
235
|
-
match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
|
|
274
|
+
lot whose only commits touch plan files alone (the plan, or a file listed under
|
|
275
|
+
`plan.files`), predate the plan's `since` or match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
|
|
236
276
|
|
|
237
277
|
The verdict is tied to what was reviewed: `raf review` stores it on the lot with
|
|
238
278
|
the sha of the lot's latest counted commit (`review: { date, verdict, commit }`,
|
|
@@ -241,7 +281,8 @@ makes the review stale: `raf done` refuses (`--force` to override), and
|
|
|
241
281
|
`raf check` reports a finished lot, until the lot is reviewed again and
|
|
242
282
|
`raf review` is rerun. A verdict
|
|
243
283
|
written by hand without a `commit` field is not checked for staleness. An empty
|
|
244
|
-
verdict is refused
|
|
284
|
+
verdict is refused, and one left empty or blank by hand in the YAML counts as no
|
|
285
|
+
review. Plans without `reviewSince` are not affected.
|
|
245
286
|
|
|
246
287
|
### QA review
|
|
247
288
|
|
|
@@ -319,9 +360,13 @@ cadence session start --since "3 days ago" --idle 2
|
|
|
319
360
|
cadence session close # today's commits by lot, commits without a lot, lots in progress
|
|
320
361
|
# with no commit today, drift, uncommitted / unpushed work
|
|
321
362
|
# exit 1 while something is still open
|
|
322
|
-
cadence session next "finish L3" "review L4" # shown by the next session start
|
|
363
|
+
cadence session next "finish L3" "review L4" # shown by the next session start; replaces the previous notes
|
|
364
|
+
cadence session next --clear # erase those notes, on purpose
|
|
323
365
|
```
|
|
324
366
|
|
|
367
|
+
`cadence session next` without a line refuses (exit 2) and leaves the notes of the
|
|
368
|
+
last close as they are — it used to erase them silently; erasing is `--clear`.
|
|
369
|
+
|
|
325
370
|
Proposals come from the plan only: lots in progress, then ready lots (dependencies
|
|
326
371
|
done), quick wins first. Local state lives in the git directory, never committed:
|
|
327
372
|
the close notes per worktree, the delivery lock and log in `.git/cadence/`, shared
|
|
@@ -372,7 +417,35 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
372
417
|
process died is removed with a warning); with `ci: github`, `gh` installed and
|
|
373
418
|
logged in.
|
|
374
419
|
- Every command is killed when it exceeds its budget (CI, `deployTimeout`, what is
|
|
375
|
-
left of `verifyTimeout`) and reported as "délai dépassé".
|
|
420
|
+
left of `verifyTimeout`) and reported as "délai dépassé". Killed means the
|
|
421
|
+
whole tree, followed while the command runs: every 200 ms cadence lists the
|
|
422
|
+
command's descendants and remembers each process it has seen with its start
|
|
423
|
+
time, so a process whose parent exits (re-parented to init — a background
|
|
424
|
+
child of a script, `sh`'s fork for each command) stays known. To kill, cadence
|
|
425
|
+
takes every remembered process still alive with the same start time (never a
|
|
426
|
+
recycled pid) plus all its current descendants, stops each one first
|
|
427
|
+
(`SIGSTOP`, the tree is re-read until nothing new appears), then kills them
|
|
428
|
+
(`SIGKILL`). This happens on a delay overrun, and at once when cadence
|
|
429
|
+
receives `SIGTERM`. On Ctrl-C or a hangup the tree has already received the
|
|
430
|
+
signal from the terminal: cadence gives the whole followed set, not just the
|
|
431
|
+
command, up to 2 seconds to finish (a script's `trap`, git removing its
|
|
432
|
+
`index.lock`), carries on the moment none of it is alive, and kills what is
|
|
433
|
+
left after the 2 seconds, a background child that ignores Ctrl-C included.
|
|
434
|
+
A delay overrun and `SIGTERM` give no grace: a script's own `trap` does not
|
|
435
|
+
get to finish there. The lock is held until the followed set is empty: when
|
|
436
|
+
cadence dies of the signal it dies after the tree, and its stale lock, which
|
|
437
|
+
the next delivery removes, leaves nothing of the first delivery running.
|
|
438
|
+
What can still outlive cadence and run concurrently with the next delivery:
|
|
439
|
+
a process that leaves the tree before cadence first sees it (a daemon's
|
|
440
|
+
double fork, `setsid`, `nohup … &` from a shell that exits, all within less
|
|
441
|
+
than 200 ms), a process of another user that cadence may not signal (`sudo`),
|
|
442
|
+
a background process still running when the command returns normally (it is
|
|
443
|
+
neither waited for nor killed), and everything if cadence itself is killed
|
|
444
|
+
with `SIGKILL` (`kill -9`, the OOM killer).
|
|
445
|
+
- Commands run in cadence's own process group and session, attached to the
|
|
446
|
+
terminal: `ssh`, `sudo` or `pinentry` can prompt on `/dev/tty`, and Ctrl-C or
|
|
447
|
+
closing the terminal stops the running command together with cadence (within
|
|
448
|
+
the limits above).
|
|
376
449
|
- **CI** `github`: polls `gh run list --commit <sha>` every 15 s; no run after
|
|
377
450
|
5 minutes is a failure (you probably pushed another commit than the one you
|
|
378
451
|
deliver); every run must end `success`, `skipped` or `neutral`. `gh` errors
|
|
@@ -380,7 +453,12 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
380
453
|
- Commands get `CADENCE_SHA`, `CADENCE_SHORT` (7 characters) and `CADENCE_BRANCH`;
|
|
381
454
|
`${SHA}` and `${SHORT}` are replaced in `url` and `contains`.
|
|
382
455
|
- On success the lots cited by the commits since the previous delivery are
|
|
383
|
-
listed, so you can `raf done` those whose effect you have seen.
|
|
456
|
+
listed, so you can `raf done` those whose effect you have seen. With a
|
|
457
|
+
read-only plan, only the lots that were in progress when the delivery started
|
|
458
|
+
are listed: an id quoted in a message for context (a finished lot, a
|
|
459
|
+
reservation number that looks like one) is not a delivered lot. Plan upkeep
|
|
460
|
+
commits (see "Plan upkeep needs no lot") are skipped in that list: they cite
|
|
461
|
+
lots without delivering anything.
|
|
384
462
|
|
|
385
463
|
### A project with its own delivery script
|
|
386
464
|
|
|
@@ -413,6 +491,60 @@ intact. If the script commits and pushes during the delivery (stamping a
|
|
|
413
491
|
changelog entry, say), the new `HEAD` is the sha recorded as delivered. Exit code
|
|
414
492
|
0 of the script means delivered; `verify` checks, if any, run after it.
|
|
415
493
|
|
|
494
|
+
### verify: replay the effect checks, outside a delivery
|
|
495
|
+
|
|
496
|
+
A green delivery says the effect was right *then*. `cadence verify` replays the
|
|
497
|
+
`deliver.verify` checks of `cadence.yaml` at any time — no CI, no deploy, no lock,
|
|
498
|
+
no journal entry, nothing written.
|
|
499
|
+
|
|
500
|
+
```sh
|
|
501
|
+
cadence verify # one pass, one line per check, then a one-line summary
|
|
502
|
+
cadence verify --retry 60 # retry each failing check for up to 60 s (deliver's 300 s is not applied)
|
|
503
|
+
cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default: last delivery, else HEAD)
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
✓ GET https://ol.example/api/health → 200
|
|
508
|
+
✗ GET https://ol.example/api/lineup → 200, contient « "starters" » — « "starters" » absent de la réponse
|
|
509
|
+
verify : 1 effet rouge sur 2 vérifications
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Exit code: **0** every check green · **1** at least one red effect · **2** nothing
|
|
513
|
+
to verify or invalid configuration.
|
|
514
|
+
|
|
515
|
+
Time limits differ from deliver's. `verify` runs all the checks **in parallel**,
|
|
516
|
+
each try with the whole 120 s limit (a `url` request gives up after 20 s; a slow
|
|
517
|
+
check is killed at its limit, "délai dépassé", and takes nothing from the others); results are printed in the order
|
|
518
|
+
of `cadence.yaml`. Retries repeat every 10 s (deliver's interval) up to `--retry`
|
|
519
|
+
seconds for each check on its own, so a run lasts at most `--retry` + 120 s.
|
|
520
|
+
deliver, by contrast, runs its checks one after the other, retries each until
|
|
521
|
+
`verifyTimeout` (300 s by default) is spent, and stops at the first check that
|
|
522
|
+
never turns green. The check code is deliver's own (`url` / `status` /
|
|
523
|
+
`contains` / `command`, same messages). A project with
|
|
524
|
+
a delivery script (`deliver.script`) and no `verify` declares no effect checks —
|
|
525
|
+
`verify` says so and exits 2 (its script's own checks stay its business); add
|
|
526
|
+
`deliver.verify` to replay some.
|
|
527
|
+
|
|
528
|
+
Each `command` check of `verify` (and of `session start`) runs in a process group
|
|
529
|
+
of its own, detached from the terminal (no `/dev/tty`: a check must not prompt).
|
|
530
|
+
At its time limit the whole group is killed, so no child survives it; Ctrl-C,
|
|
531
|
+
`SIGTERM` or a closed terminal is passed on to the running checks before cadence
|
|
532
|
+
exits. As the checks run together, the output of their commands may interleave;
|
|
533
|
+
the result lines come after it, in order.
|
|
534
|
+
|
|
535
|
+
Make `verify` count: a health endpoint stays green while the data is wrong (a
|
|
536
|
+
lineup served empty for 38 h behind a green `/api/health`). Add a check on the
|
|
537
|
+
content that matters, e.g. `url: …/api/lineup` with `contains: '"starters"'`.
|
|
538
|
+
|
|
539
|
+
`cadence session start` runs the same checks in parallel (one try each, command
|
|
540
|
+
output muted), each with a 10 s limit, so the whole step takes about 10 s at worst;
|
|
541
|
+
it is not deliver's `verifyTimeout`, and there is no retry. It adds an
|
|
542
|
+
**"Effets en production"** section to the morning report: a single `✓` line when
|
|
543
|
+
everything is green, the red effects and the summary otherwise. It is a fact like
|
|
544
|
+
the others: it never changes the exit code, and an unreachable network shows up
|
|
545
|
+
as red lines ("erreur réseau") without blocking the session. No section for a
|
|
546
|
+
project without `deliver.verify`.
|
|
547
|
+
|
|
416
548
|
## Claude Code skills
|
|
417
549
|
|
|
418
550
|
As a plugin:
|
package/bin/cadence.js
CHANGED
|
@@ -13,7 +13,7 @@ const io = {
|
|
|
13
13
|
|
|
14
14
|
if (tool === 'raf') {
|
|
15
15
|
process.exitCode = await run(args, io);
|
|
16
|
-
} else if (['news', 'session', 'deliver', 'skills'].includes(tool)) {
|
|
16
|
+
} else if (['news', 'session', 'deliver', 'verify', 'skills'].includes(tool)) {
|
|
17
17
|
process.exitCode = await run([tool, ...args], io);
|
|
18
18
|
} else if (tool === '--version' || tool === '-v') {
|
|
19
19
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -26,10 +26,14 @@ if (tool === 'raf') {
|
|
|
26
26
|
cadence session start [--since "24 hours ago"] [--idle 2]
|
|
27
27
|
faits de reprise : notes de la veille, en cours, fait depuis, écarts, propositions
|
|
28
28
|
cadence session close [--since …] faits de clôture ; code 1 tant que ce n'est pas fermé
|
|
29
|
-
cadence session next "ligne" … notes pour la prochaine session (
|
|
29
|
+
cadence session next "ligne" … notes pour la prochaine session (remplacent les précédentes)
|
|
30
|
+
cadence session next --clear efface ces notes ; sans ligne ni --clear, la commande refuse
|
|
30
31
|
cadence deliver [--dry-run] [--sha rév] [--config cadence.yaml] [-- arguments du script du projet]
|
|
31
32
|
CI du sha poussé → déploiement → vérifications de l'effet ;
|
|
32
33
|
ou le script de livraison du projet (deliver.script), sous verrou et journal
|
|
34
|
+
cadence verify [--retry secondes] [--sha rév]
|
|
35
|
+
rejoue les vérifications d'effet (deliver.verify) hors livraison, une passe, en parallèle ;
|
|
36
|
+
code 0 tout vert, 1 un effet rouge, 2 rien à vérifier ; « session start » la lance aussi
|
|
33
37
|
cadence skills install [--dir .claude] [--force]
|
|
34
38
|
installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
|
|
35
39
|
process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
|
package/dist/audit.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import { dirname, join, relative } from 'node:path';
|
|
2
2
|
import { check } from './check.js';
|
|
3
|
-
import {
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { changedFiles, fileAt, readCommits } from './git.js';
|
|
4
5
|
import { linkCommits } from './link.js';
|
|
5
6
|
import { loadEntries, newsIssues } from './news.js';
|
|
6
7
|
import { isOpen } from './plan.js';
|
|
7
|
-
/**
|
|
8
|
+
/** Chemin de la configuration lue, relatif à la racine : celui de --config, sinon cadence.yaml. */
|
|
9
|
+
function configRel(plan, root) {
|
|
10
|
+
return plan.configFile ? relative(root, plan.configFile) : 'cadence.yaml';
|
|
11
|
+
}
|
|
12
|
+
/** Le plan, sa page Gantt, la configuration lue (cadence.yaml) et les fichiers tenus avec lui (plan.files). */
|
|
8
13
|
function ownFiles(plan, root) {
|
|
9
|
-
return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), ...plan.files]);
|
|
14
|
+
return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), configRel(plan, root), ...plan.files]);
|
|
15
|
+
}
|
|
16
|
+
function withoutPlanKey(text) {
|
|
17
|
+
if (text === null)
|
|
18
|
+
return '{}';
|
|
19
|
+
try {
|
|
20
|
+
const { plan: _plan, ...rest } = (parse(text) ?? {});
|
|
21
|
+
return JSON.stringify(rest, (_k, v) => (v && typeof v === 'object' && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([x], [y]) => x.localeCompare(y))) : v));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Le commit ne change de la configuration que la clé `plan:` ? Un fichier illisible avant ou après compte comme du travail. */
|
|
28
|
+
function onlyPlanKeyChanged(root, sha, file) {
|
|
29
|
+
const before = withoutPlanKey(fileAt(root, `${sha}^`, file));
|
|
30
|
+
const after = withoutPlanKey(fileAt(root, sha, file));
|
|
31
|
+
return before !== null && before === after;
|
|
10
32
|
}
|
|
11
33
|
/** Commits du dépôt sans les commits automatiques (motifs `ignore`) : ni audités ni comptés pour un lot. */
|
|
12
34
|
export function planCommits(plan, root, opts = {}) {
|
|
@@ -14,25 +36,25 @@ export function planCommits(plan, root, opts = {}) {
|
|
|
14
36
|
const commits = readCommits(root, opts);
|
|
15
37
|
return patterns.length ? commits.filter((c) => !patterns.some((re) => re.test(c.subject))) : commits;
|
|
16
38
|
}
|
|
17
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Commit d'entretien du plan : TOUS ses fichiers sont des fichiers du plan (le plan, sa page Gantt,
|
|
41
|
+
* plan.files — son plan publié par exemple — et la configuration lue, mais celle-ci seulement quand la
|
|
42
|
+
* clé `plan:` est la seule à changer : deliver/session/… sont du travail). Les fichiers décident,
|
|
43
|
+
* jamais le sujet : « chore(plan): … » qui touche un fichier source est un commit comme un autre.
|
|
44
|
+
*/
|
|
18
45
|
export function isPlanOnly(sha, plan, root) {
|
|
19
46
|
const own = ownFiles(plan, root);
|
|
47
|
+
const config = configRel(plan, root);
|
|
20
48
|
const files = changedFiles(root, sha);
|
|
21
|
-
return files.length > 0 && files.every((f) => own.has(f));
|
|
49
|
+
return files.length > 0 && files.every((f) => own.has(f) && (f !== config || onlyPlanKeyChanged(root, sha, f)));
|
|
22
50
|
}
|
|
23
51
|
/**
|
|
24
|
-
* N'ont pas besoin de citer un lot : un commit
|
|
25
|
-
*
|
|
52
|
+
* N'ont pas besoin de citer un lot : un commit d'entretien du plan (cf. isPlanOnly) et un commit
|
|
53
|
+
* automatique dont le sujet correspond à un motif `ignore:` du plan.
|
|
26
54
|
*/
|
|
27
55
|
export function exemptPlanOnly(linked, plan, root) {
|
|
28
|
-
const own = ownFiles(plan, root);
|
|
29
56
|
const { patterns } = plan.ignore;
|
|
30
|
-
const orphans = linked.orphans.filter((c) =>
|
|
31
|
-
if (patterns.some((re) => re.test(c.subject)))
|
|
32
|
-
return false;
|
|
33
|
-
const files = changedFiles(root, c.sha);
|
|
34
|
-
return files.length === 0 || !files.every((f) => own.has(f));
|
|
35
|
-
});
|
|
57
|
+
const orphans = linked.orphans.filter((c) => !patterns.some((re) => re.test(c.subject)) && !isPlanOnly(c.sha, plan, root));
|
|
36
58
|
return { ...linked, orphans };
|
|
37
59
|
}
|
|
38
60
|
/** Fenêtre de l'audit : --since, sinon la date d'adoption du plan, sinon 30 jours. */
|
package/dist/cli.js
CHANGED
|
@@ -6,16 +6,17 @@ import { audit, exemptPlanOnly, isPlanOnly, lotWork, nextUp, planCommits, unrevi
|
|
|
6
6
|
import { short } from './check.js';
|
|
7
7
|
import { isDay, toDay } from './dates.js';
|
|
8
8
|
import { deliver, parseDeliverConfig, realDeps } from './deliver.js';
|
|
9
|
+
import { effectLines, realCheckDeps, verifyCommand } from './verify.js';
|
|
9
10
|
import { ganttData, renderGantt } from './gantt.js';
|
|
10
|
-
import { gitRoot, readCommits } from './git.js';
|
|
11
|
+
import { gitRoot, headSha, readCommits, resolveCommit } from './git.js';
|
|
11
12
|
import { installHook } from './hook.js';
|
|
12
|
-
import { linkCommits } from './link.js';
|
|
13
|
+
import { citedRefs, linkCommits } from './link.js';
|
|
13
14
|
import { buildNews, loadEntries, newEntry, newsData, newsIssues, stampEntries } from './news.js';
|
|
14
15
|
import { Plan, RafError, STATUSES } from './plan.js';
|
|
15
16
|
import { schedule } from './schedule.js';
|
|
16
17
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
17
18
|
import { sessionClose, sessionStart } from './session.js';
|
|
18
|
-
import { sharedStateDir, stateDir, writeNext } from './state.js';
|
|
19
|
+
import { clearNext, lastDelivery, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
|
|
19
20
|
const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
|
|
20
21
|
|
|
21
22
|
raf init [--project nom] [--prefix L] [--no-hook]
|
|
@@ -32,9 +33,15 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
32
33
|
raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
|
|
33
34
|
raf gantt [-o docs/plan/gantt.html]
|
|
34
35
|
raf hook install
|
|
36
|
+
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
35
37
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
36
38
|
|
|
37
39
|
Un commit appartient à un lot quand son message cite l'identifiant : « feat(L3): … », « L3/t1 ».
|
|
40
|
+
Quand la portée du sujet cite des lots — « feat(L3): … », « chore(L31,L32): … » — elle seule décide : une
|
|
41
|
+
mention en passage (« page équipe (L27) »), une plage (« L28–L31 », « L45 à L48 ») ou le corps n'y comptent pas.
|
|
42
|
+
Sans portée citant un lot, tout le message est lu : « fix: L3 corrigé », « L3/t1 ».
|
|
43
|
+
Un commit qui ne touche que le plan, la clé plan: de cadence.yaml (deliver/session sont du travail) (et les fichiers déclarés sous plan.files, un plan
|
|
44
|
+
publié par exemple) n'a pas à en citer, et ne compte pas pour les lots qu'il cite ; le sujet n'y change rien.
|
|
38
45
|
Un lot --visible attend une entrée Nouveautés (docs/nouveautes/, --dir) avec capture ; raf check le vérifie.
|
|
39
46
|
Un texte qui commence par « - » se passe après « -- » : raf note L1 -- "-5 %".
|
|
40
47
|
Le plan est docs/plan/raf.yaml, ou celui que nomme « plan: » dans cadence.yaml ; un plan tenu par un
|
|
@@ -85,6 +92,8 @@ function dispatch(argv, io) {
|
|
|
85
92
|
config: { type: 'string' },
|
|
86
93
|
'dry-run': { type: 'boolean' },
|
|
87
94
|
sha: { type: 'string' },
|
|
95
|
+
retry: { type: 'string' },
|
|
96
|
+
clear: { type: 'boolean' },
|
|
88
97
|
help: { type: 'boolean', short: 'h' },
|
|
89
98
|
},
|
|
90
99
|
});
|
|
@@ -103,7 +112,7 @@ function dispatch(argv, io) {
|
|
|
103
112
|
const installing = command === 'skills' || (command === 'hook' && rest[0] === 'install');
|
|
104
113
|
const planConfig = installing ? null : readPlanConfig(configPath);
|
|
105
114
|
const planPath = resolve(io.cwd, values.file ?? io.env.RAF_FILE ?? resolve(root, planConfig?.path ?? 'docs/plan/raf.yaml'));
|
|
106
|
-
const loadPlan = () => Plan.load(planPath, planConfig?.settings);
|
|
115
|
+
const loadPlan = () => Plan.load(planPath, { ...planConfig?.settings, config: configPath });
|
|
107
116
|
const newsDir = resolve(io.cwd, values.dir ?? join(root, 'docs/nouveautes'));
|
|
108
117
|
const need = (n, usage) => {
|
|
109
118
|
if (rest.length < n)
|
|
@@ -228,7 +237,14 @@ function dispatch(argv, io) {
|
|
|
228
237
|
throw new RafError('session : à lancer dans un dépôt git');
|
|
229
238
|
// « next » n'écrit que les notes : la commande du projet ne se joue qu'à la reprise et à la clôture.
|
|
230
239
|
const facts = rest[0] === 'start' || rest[0] === 'close' ? readSessionConfig(configPath)[rest[0]] : undefined;
|
|
231
|
-
|
|
240
|
+
const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, out: io.out, facts };
|
|
241
|
+
if (rest[0] !== 'start')
|
|
242
|
+
return session(rest, sctx, values);
|
|
243
|
+
// La reprise rejoue les vérifications d'effet (un essai, borné) : un effet rouge d'hier se lit avec les faits du matin.
|
|
244
|
+
const effects = morningEffects(configPath, root);
|
|
245
|
+
if (effects === null)
|
|
246
|
+
return session(rest, sctx, values);
|
|
247
|
+
return Promise.resolve(effects).then((lines) => session(rest, { ...sctx, effects: lines }, values));
|
|
232
248
|
}
|
|
233
249
|
case 'deliver': {
|
|
234
250
|
if (!gitRoot(io.cwd))
|
|
@@ -240,6 +256,20 @@ function dispatch(argv, io) {
|
|
|
240
256
|
const ctx = { root, state: sharedStateDir(root), plan, config, today, dryRun: !!values['dry-run'], sha: values.sha, args: rest, out: io.out, err: io.err };
|
|
241
257
|
return deliver(ctx, realDeps(root));
|
|
242
258
|
}
|
|
259
|
+
case 'verify': {
|
|
260
|
+
if (!gitRoot(io.cwd))
|
|
261
|
+
throw new RafError('verify : à lancer dans un dépôt git');
|
|
262
|
+
if (!existsSync(configPath))
|
|
263
|
+
throw new RafError(`pas de configuration : ${configPath} (voir « cadence.yaml » dans le README)`);
|
|
264
|
+
const retry = values.retry === undefined ? 0 : Number(values.retry);
|
|
265
|
+
if (!Number.isFinite(retry) || retry < 0)
|
|
266
|
+
throw new RafError(`--retry invalide : ${values.retry} (secondes, 0 ou plus)`);
|
|
267
|
+
const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
|
|
268
|
+
const sha = values.sha ? (resolveCommit(root, values.sha) ?? '') : (lastDelivery(sharedStateDir(root)) ?? headSha(root) ?? '');
|
|
269
|
+
if (values.sha && !sha)
|
|
270
|
+
throw new RafError(`--sha ${values.sha} : commit introuvable`);
|
|
271
|
+
return verifyCommand({ config, sha, retry, out: io.out }, realCheckDeps(root));
|
|
272
|
+
}
|
|
243
273
|
case 'skills': {
|
|
244
274
|
if (rest[0] !== 'install')
|
|
245
275
|
throw new RafError('usage : cadence skills install [--dir .claude] [--force]');
|
|
@@ -342,7 +372,7 @@ function postCommit(load, newsDir, root, io) {
|
|
|
342
372
|
const head = readCommits(root, { range: '-1' })[0];
|
|
343
373
|
if (!head)
|
|
344
374
|
return 0;
|
|
345
|
-
const refs = plan.refs
|
|
375
|
+
const refs = citedRefs(head, plan.refs);
|
|
346
376
|
if (refs.length === 0) {
|
|
347
377
|
const planOnly = exemptPlanOnly({ byLot: new Map(), orphans: [head], unknown: [] }, plan, root).orphans.length === 0;
|
|
348
378
|
if (!planOnly && !/^Merge\b/.test(head.subject)) {
|
|
@@ -419,6 +449,27 @@ function news([sub, ...args], plan, dir, today, values, io) {
|
|
|
419
449
|
throw new RafError('usage : cadence news new|list|check|stamp|build');
|
|
420
450
|
}
|
|
421
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Reprise : lignes d'effets à rejouer, ou null quand il n'y a rien à vérifier (sans cadence.yaml, sans deliver,
|
|
454
|
+
* script sans verify) — la reprise reste alors synchrone. Ne lève jamais.
|
|
455
|
+
*/
|
|
456
|
+
function morningEffects(configPath, root) {
|
|
457
|
+
if (!existsSync(configPath))
|
|
458
|
+
return null;
|
|
459
|
+
const text = readFileSync(configPath, 'utf8');
|
|
460
|
+
if (!/^deliver\s*:/m.test(text))
|
|
461
|
+
return null;
|
|
462
|
+
try {
|
|
463
|
+
const config = parseDeliverConfig(text, configPath);
|
|
464
|
+
if (config.verify.length === 0)
|
|
465
|
+
return null;
|
|
466
|
+
const sha = lastDelivery(sharedStateDir(root)) ?? headSha(root) ?? '';
|
|
467
|
+
return effectLines(config, sha, realCheckDeps(root, { quiet: true }));
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
return [`✗ ${e.message}`];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
422
473
|
function session([sub, ...args], ctx, values) {
|
|
423
474
|
switch (sub) {
|
|
424
475
|
case 'start': {
|
|
@@ -429,10 +480,26 @@ function session([sub, ...args], ctx, values) {
|
|
|
429
480
|
}
|
|
430
481
|
case 'close':
|
|
431
482
|
return sessionClose(ctx, { since: values.since ?? `${ctx.today} 00:00` });
|
|
432
|
-
case 'next':
|
|
433
|
-
|
|
483
|
+
case 'next': {
|
|
484
|
+
const lines = args.filter((l) => l.trim() !== '');
|
|
485
|
+
if (values.clear) {
|
|
486
|
+
if (lines.length)
|
|
487
|
+
throw new RafError('session next --clear efface les notes : ne pas lui passer de ligne');
|
|
488
|
+
const gone = clearNext(ctx.state);
|
|
489
|
+
ctx.out(gone ? `notes effacées (${gone.lines.length} ligne(s) du ${gone.date})` : 'aucune note à effacer');
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
// Lancée sans ligne (variable vide dans un script, agent pressé), la commande effaçait en silence
|
|
493
|
+
// les notes de la dernière clôture : effacer se demande exprès.
|
|
494
|
+
if (lines.length === 0) {
|
|
495
|
+
const kept = readNext(ctx.state);
|
|
496
|
+
throw new RafError(`session next : aucune ligne — ${kept ? `${kept.lines.length} ligne(s) du ${kept.date} conservée(s)` : "rien n'est écrit"} ; ` +
|
|
497
|
+
'usage : cadence session next "ligne" … (pour effacer les notes exprès : cadence session next --clear)');
|
|
498
|
+
}
|
|
499
|
+
writeNext(ctx.state, ctx.today, lines);
|
|
434
500
|
return 0;
|
|
501
|
+
}
|
|
435
502
|
default:
|
|
436
|
-
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" …');
|
|
503
|
+
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" … | next --clear');
|
|
437
504
|
}
|
|
438
505
|
}
|