@sylad/cadence 0.7.0 → 0.9.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 +274 -17
- package/agents/qa-reviewer.md +15 -6
- package/bin/cadence.js +6 -1
- package/dist/audit.js +41 -19
- package/dist/check.js +5 -2
- package/dist/clean.js +225 -0
- package/dist/cli.js +113 -12
- package/dist/config.js +82 -6
- package/dist/deliver.js +83 -32
- package/dist/git.js +12 -2
- package/dist/link.js +15 -1
- package/dist/orchestrate/briefs.js +52 -0
- package/dist/orchestrate/command.js +498 -0
- package/dist/orchestrate/cycle.js +673 -0
- package/dist/orchestrate/guard.js +131 -0
- package/dist/orchestrate/launch.js +221 -0
- package/dist/orchestrate/lock.js +85 -0
- package/dist/orchestrate/pool.js +51 -0
- package/dist/orchestrate/result.js +94 -0
- package/dist/orchestrate/schemas.js +48 -0
- package/dist/orchestrate/state.js +93 -0
- package/dist/orchestrate/table.js +73 -0
- package/dist/plan.js +61 -2
- package/dist/proc.js +260 -0
- package/dist/recurring.js +19 -0
- package/dist/schedule.js +4 -1
- package/dist/session.js +18 -1
- package/dist/verify.js +114 -0
- package/package.json +18 -3
- package/skills/lead/SKILL.md +33 -13
- package/skills/session-close/SKILL.md +25 -4
- package/skills/session-start/SKILL.md +3 -0
- package/templates/orchestrate/fix-minors.md +18 -0
- package/templates/orchestrate/fix.md +16 -0
- package/templates/orchestrate/implement.md +20 -0
- package/templates/orchestrate/review-recheck.md +11 -0
- package/templates/orchestrate/review-small.md +11 -0
- package/templates/orchestrate/review.md +8 -0
- package/templates/orchestrate/ux.md +10 -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.9.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.9.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
|
@@ -27,13 +27,19 @@ reports a page left empty or in error.
|
|
|
27
27
|
`fix: L3/t1 …`. The link is **computed from `git log`**, never stored, so
|
|
28
28
|
committing never dirties the plan. The id is read as a whole word: `XL3`,
|
|
29
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.
|
|
30
34
|
- `raf check` audits drift between the plan and the history.
|
|
31
35
|
- Plan upkeep needs no lot. A commit is plan upkeep when **every file it
|
|
32
|
-
touches is a plan file**: the plan itself, its Gantt page,
|
|
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
|
|
33
38
|
project lists under `plan.files` in `cadence.yaml` (a page it generates from
|
|
34
39
|
the plan, a journal). Such a commit is never a "commit without a lot", and it
|
|
35
40
|
does not count as work on the lots it cites: it is absent from `raf commits`,
|
|
36
|
-
does not start a `todo` lot
|
|
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
|
|
37
43
|
decide, never the subject: a `chore(plan): …` commit that touches a source
|
|
38
44
|
file is a commit like any other.
|
|
39
45
|
- `raf gantt` writes a single self-contained HTML page (no server, no CDN).
|
|
@@ -59,7 +65,9 @@ raf gantt # docs/plan/gantt.html
|
|
|
59
65
|
| Command | Effect |
|
|
60
66
|
|---|---|
|
|
61
67
|
| `raf init [--project name] [--prefix L] [--no-hook]` | create the plan and install the hook |
|
|
62
|
-
| `raf add "title" [--estimate d] [--quickwin] [--visible] [--after L2,L4] [--parent L3]` | add a lot or a sub-task, print its id |
|
|
68
|
+
| `raf add "title" [--estimate d] [--quickwin] [--visible] [--public "title"] [--after L2,L4] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`) |
|
|
69
|
+
| `raf add "title" --every <days>` · `raf did <id> ["text"]` | recurring lot: `every` (days) and `last` (last time done) fields; the lot stays `todo`, `raf now` and `session start` list it under « Récurrent » with « dû depuis N j » / « prochain dans N j »; `raf did` resets the count (spec: `docs/superpowers/specs/2026-10-04-L11-tache-recurrente.md`) |
|
|
70
|
+
| `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
|
|
63
71
|
| `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`) |
|
|
64
72
|
| `raf note <id> "text"` | dated note — keep decisions next to the work |
|
|
65
73
|
| `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first |
|
|
@@ -133,6 +141,7 @@ plan:
|
|
|
133
141
|
lots: taches # root key holding the list (default: lots)
|
|
134
142
|
fields: # raf field: key in the file (a list = first one present)
|
|
135
143
|
title: titre
|
|
144
|
+
public: titre_public # the lot's title for the public; read when it is text, else ignored
|
|
136
145
|
status: etat
|
|
137
146
|
estimate: effort
|
|
138
147
|
created: cree_le
|
|
@@ -148,7 +157,7 @@ plan:
|
|
|
148
157
|
estimates: { S: 0.5, M: 1, L: 3 } # their effort labels, in working days
|
|
149
158
|
```
|
|
150
159
|
|
|
151
|
-
- Fields: `id`, `title`, `status`, `estimate`, `quickwin`, `visible`, `after`, `created`, `started`,
|
|
160
|
+
- Fields: `id`, `title`, `status`, `estimate`, `quickwin`, `visible`, `public`, `every`, `last`, `after`, `created`, `started`,
|
|
152
161
|
`finished`, `notes`, `parent`; one left out is read under its own name. A timestamp counts for
|
|
153
162
|
its day; a note written as plain text is one note.
|
|
154
163
|
- `parent`: an entry `B33/t1-fusion` whose parent is `B33` becomes the sub-task `t1-fusion` of `B33`.
|
|
@@ -376,6 +385,48 @@ session:
|
|
|
376
385
|
close: ./scripts/evening.sh "$CADENCE_SINCE"
|
|
377
386
|
```
|
|
378
387
|
|
|
388
|
+
```yaml
|
|
389
|
+
session:
|
|
390
|
+
clean: [ "~/projects/tmp/*", "tmp/*", "test-output-*" ] # working files to propose for removal at close
|
|
391
|
+
cleanDays: 7 # older than this many days (default 7)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`cadence session close` then lists, under "Nettoyage proposé", the entries matching these
|
|
395
|
+
patterns (`*` stands for part of a name and never crosses a `/`; `~` is the home folder, a
|
|
396
|
+
relative pattern starts at the repo root) that were not modified for more than `cleanDays`
|
|
397
|
+
days, oldest first — a folder's age is that of the most recent entry it contains, at any depth,
|
|
398
|
+
and an entry's age runs from the later of its modification time and its status-change time (ctime):
|
|
399
|
+
what `tar xf`, `cp -a` or `rsync -a` just brought in with an old modification time is not old.
|
|
400
|
+
|
|
401
|
+
An entry is proposed only if it could be measured entirely. Never proposed:
|
|
402
|
+
|
|
403
|
+
- a git repository, a folder that contains one at any depth, and anything under a `.git` folder;
|
|
404
|
+
- a git directory without a `.git` entry — a bare repository (`git init --bare`, `git clone
|
|
405
|
+
--mirror`, `backup.git/`), a `--separate-git-dir`, a worktree's admin folder —, recognised by
|
|
406
|
+
its content as git does (`HEAD` with `objects` and `refs`, or `HEAD` with `commondir`): neither
|
|
407
|
+
it, nor anything inside it, nor a folder that contains one;
|
|
408
|
+
- anything `git` tracks, in any repository that has a `.git` entry above it. One limit, stated
|
|
409
|
+
plainly: a bare repository driven with an external work tree (`git --git-dir=~/.dotfiles
|
|
410
|
+
--work-tree=~`) leaves no `.git` beside the files, so its tracked files cannot be recognised and
|
|
411
|
+
can be proposed — never point a pattern at such a work tree;
|
|
412
|
+
- a name starting with `.` unless the pattern itself starts that name with `.` (as in the
|
|
413
|
+
shell, `tmp/*` does not match `tmp/.env`; `tmp/.cache-*` does);
|
|
414
|
+
- anything reached through a symbolic link matched by a `*` segment: unlike the shell, the walk
|
|
415
|
+
never descends into such a link (`tmp/*/*` lists nothing behind `tmp/link → ../outside`), and a
|
|
416
|
+
folder's age never looks behind a link; the link itself can be proposed, as a link — removing it
|
|
417
|
+
leaves its target alone. Only segments written in full *before* the first `*` follow a link, as
|
|
418
|
+
`cd` would (`~/shared/*` with `~/shared` a link lists the target's entries); once a `*` has been
|
|
419
|
+
crossed no link is followed any more, even on a segment written in full (`tmp/*/out/*` with
|
|
420
|
+
`tmp/run1/out → /elsewhere` lists nothing behind `out`);
|
|
421
|
+
- anything that could not be read entirely: a folder that cannot be listed — nor anything
|
|
422
|
+
inside it, even named in full —, an entry that cannot be examined or vanished during the
|
|
423
|
+
walk, an entry under a `.git` that `git` could not answer for. These are listed apart,
|
|
424
|
+
under "Nettoyage : N élément(s) illisible(s), jamais proposé(s)";
|
|
425
|
+
- the repository itself, or a folder that contains it.
|
|
426
|
+
|
|
427
|
+
The command deletes nothing, never fails on the cleanup and does not change the exit code: the
|
|
428
|
+
`session-close` skill shows the list and removes the entries only after the human agrees.
|
|
429
|
+
|
|
379
430
|
The commands get `CADENCE_SINCE` (the `--since` in effect) and `CADENCE_TODAY`. They
|
|
380
431
|
add facts and decide nothing: a failing command is reported and changes neither
|
|
381
432
|
the exit code nor the verdict.
|
|
@@ -411,7 +462,48 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
411
462
|
process died is removed with a warning); with `ci: github`, `gh` installed and
|
|
412
463
|
logged in.
|
|
413
464
|
- Every command is killed when it exceeds its budget (CI, `deployTimeout`, what is
|
|
414
|
-
left of `verifyTimeout`) and reported as "délai dépassé".
|
|
465
|
+
left of `verifyTimeout`) and reported as "délai dépassé". Killed means the
|
|
466
|
+
whole tree, followed while the command runs: every 200 ms cadence lists the
|
|
467
|
+
command's descendants and remembers each process it has seen with its start
|
|
468
|
+
time, so a process whose parent exits (re-parented to init — a background
|
|
469
|
+
child of a script, `sh`'s fork for each command) stays known. To kill, cadence
|
|
470
|
+
takes every remembered process still alive with the same start time (never a
|
|
471
|
+
recycled pid) plus all its current descendants, stops each one first
|
|
472
|
+
(`SIGSTOP`, the tree is re-read until nothing new appears), then kills them
|
|
473
|
+
(`SIGKILL`). This happens on a delay overrun, and at once when cadence
|
|
474
|
+
receives `SIGTERM`. On Ctrl-C or a hangup the tree has already received the
|
|
475
|
+
signal from the terminal: cadence gives the whole followed set, not just the
|
|
476
|
+
command, up to 2 seconds to finish (a script's `trap`, git removing its
|
|
477
|
+
`index.lock`), carries on the moment none of it is alive, and kills what is
|
|
478
|
+
left after the 2 seconds, a background child that ignores Ctrl-C included. If
|
|
479
|
+
the command was never listed (process list unreadable from the start), the
|
|
480
|
+
grace lasts as long as the command itself is alive, and what remains after
|
|
481
|
+
2 seconds is killed through the same fallback (the command itself only). The
|
|
482
|
+
same fallback applies when the process list fails while the command runs and
|
|
483
|
+
is still failing when the kill comes (the last listing is not trusted after
|
|
484
|
+
1 second): only the command is killed, its descendants survive, and cadence
|
|
485
|
+
says so once on stderr.
|
|
486
|
+
A delay overrun and `SIGTERM` give no grace: a script's own `trap` does not
|
|
487
|
+
get to finish there. The lock is held until the followed set is empty: when
|
|
488
|
+
cadence dies of the signal it dies after the tree, and its stale lock, which
|
|
489
|
+
the next delivery removes, leaves nothing of the first delivery running.
|
|
490
|
+
What can still outlive cadence and run concurrently with the next delivery:
|
|
491
|
+
a process that leaves the tree before cadence first sees it (a daemon's
|
|
492
|
+
double fork, `setsid`, `nohup … &` from a shell that exits, all within less
|
|
493
|
+
than 200 ms), a process of another user that cadence may not signal (`sudo`),
|
|
494
|
+
a background process still running when the command returns normally (it is
|
|
495
|
+
neither waited for nor killed), and everything if cadence itself is killed
|
|
496
|
+
with `SIGKILL` (`kill -9`, the OOM killer).
|
|
497
|
+
Where the process list cannot be read (no `/proc` and `ps` failing), cadence
|
|
498
|
+
can only kill the root command (`sh`): it leads no group of its own, since it
|
|
499
|
+
is deliberately kept in cadence's group, so its descendants are not killed and
|
|
500
|
+
survive it. A list that fails for more than a second is no longer
|
|
501
|
+
trusted (a followed pid may have been recycled): nothing is signalled from it.
|
|
502
|
+
cadence says so once on stderr when the list is unavailable.
|
|
503
|
+
- Commands run in cadence's own process group and session, attached to the
|
|
504
|
+
terminal: `ssh`, `sudo` or `pinentry` can prompt on `/dev/tty`, and Ctrl-C or
|
|
505
|
+
closing the terminal stops the running command together with cadence (within
|
|
506
|
+
the limits above).
|
|
415
507
|
- **CI** `github`: polls `gh run list --commit <sha>` every 15 s; no run after
|
|
416
508
|
5 minutes is a failure (you probably pushed another commit than the one you
|
|
417
509
|
deliver); every run must end `success`, `skipped` or `neutral`. `gh` errors
|
|
@@ -422,7 +514,9 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
|
|
|
422
514
|
listed, so you can `raf done` those whose effect you have seen. With a
|
|
423
515
|
read-only plan, only the lots that were in progress when the delivery started
|
|
424
516
|
are listed: an id quoted in a message for context (a finished lot, a
|
|
425
|
-
reservation number that looks like one) is not a delivered lot.
|
|
517
|
+
reservation number that looks like one) is not a delivered lot. Plan upkeep
|
|
518
|
+
commits (see "Plan upkeep needs no lot") are skipped in that list: they cite
|
|
519
|
+
lots without delivering anything.
|
|
426
520
|
|
|
427
521
|
### A project with its own delivery script
|
|
428
522
|
|
|
@@ -455,6 +549,159 @@ intact. If the script commits and pushes during the delivery (stamping a
|
|
|
455
549
|
changelog entry, say), the new `HEAD` is the sha recorded as delivered. Exit code
|
|
456
550
|
0 of the script means delivered; `verify` checks, if any, run after it.
|
|
457
551
|
|
|
552
|
+
### verify: replay the effect checks, outside a delivery
|
|
553
|
+
|
|
554
|
+
A green delivery says the effect was right *then*. `cadence verify` replays the
|
|
555
|
+
`deliver.verify` checks of `cadence.yaml` at any time — no CI, no deploy, no lock,
|
|
556
|
+
no journal entry, nothing written.
|
|
557
|
+
|
|
558
|
+
```sh
|
|
559
|
+
cadence verify # one pass, one line per check, then a one-line summary
|
|
560
|
+
cadence verify --retry 60 # retry each failing check for up to 60 s (deliver's 300 s is not applied)
|
|
561
|
+
cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default: last delivery, else HEAD)
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
```
|
|
565
|
+
✓ GET https://ol.example/api/health → 200
|
|
566
|
+
✗ GET https://ol.example/api/lineup → 200, contient « "starters" » — « "starters" » absent de la réponse
|
|
567
|
+
verify : 1 effet rouge sur 2 vérifications
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Exit code: **0** every check green · **1** at least one red effect · **2** nothing
|
|
571
|
+
to verify or invalid configuration.
|
|
572
|
+
|
|
573
|
+
Time limits differ from deliver's. `verify` runs all the checks **in parallel**,
|
|
574
|
+
each try with the whole 120 s limit (a `url` request gives up after 20 s; a slow
|
|
575
|
+
check is killed at its limit, "délai dépassé", and takes nothing from the others); results are printed in the order
|
|
576
|
+
of `cadence.yaml`. Retries repeat every 10 s (deliver's interval) up to `--retry`
|
|
577
|
+
seconds for each check on its own, so a run lasts at most `--retry` + 120 s.
|
|
578
|
+
deliver, by contrast, runs its checks one after the other, retries each until
|
|
579
|
+
`verifyTimeout` (300 s by default) is spent, and stops at the first check that
|
|
580
|
+
never turns green. The check code is deliver's own (`url` / `status` /
|
|
581
|
+
`contains` / `command`, same messages). A project with
|
|
582
|
+
a delivery script (`deliver.script`) and no `verify` declares no effect checks —
|
|
583
|
+
`verify` says so and exits 2 (its script's own checks stay its business); add
|
|
584
|
+
`deliver.verify` to replay some.
|
|
585
|
+
|
|
586
|
+
Each `command` check of `verify` (and of `session start`) runs in a process group
|
|
587
|
+
of its own, detached from the terminal (no `/dev/tty`: a check must not prompt).
|
|
588
|
+
At its time limit the whole group is killed, so no child survives it; Ctrl-C,
|
|
589
|
+
`SIGTERM` or a closed terminal is passed on to the running checks before cadence
|
|
590
|
+
exits. As the checks run together, the output of their commands may interleave;
|
|
591
|
+
the result lines come after it, in order.
|
|
592
|
+
|
|
593
|
+
Make `verify` count: a health endpoint stays green while the data is wrong (a
|
|
594
|
+
lineup served empty for 38 h behind a green `/api/health`). Add a check on the
|
|
595
|
+
content that matters, e.g. `url: …/api/lineup` with `contains: '"starters"'`.
|
|
596
|
+
|
|
597
|
+
`cadence session start` runs the same checks in parallel (one try each, command
|
|
598
|
+
output muted), each with a 10 s limit, so the whole step takes about 10 s at worst;
|
|
599
|
+
it is not deliver's `verifyTimeout`, and there is no retry. It adds an
|
|
600
|
+
**"Effets en production"** section to the morning report: a single `✓` line when
|
|
601
|
+
everything is green, the red effects and the summary otherwise. It is a fact like
|
|
602
|
+
the others: it never changes the exit code, and an unreachable network shows up
|
|
603
|
+
as red lines ("erreur réseau") without blocking the session. No section for a
|
|
604
|
+
project without `deliver.verify`.
|
|
605
|
+
|
|
606
|
+
## orchestrate
|
|
607
|
+
|
|
608
|
+
`cadence orchestrate` is a program above `/lead`, not a conversation: for each lot you choose, it runs
|
|
609
|
+
a **fresh** `claude -p` session per step with a short brief, reads the result, writes the state in
|
|
610
|
+
files and moves on. The lead session keeps only the decision (which lots), the final table and the
|
|
611
|
+
questions. Nothing is resumed: a correction is a new session, never the author's reopened. The one
|
|
612
|
+
exception is a **formatting retry** (below).
|
|
613
|
+
|
|
614
|
+
```sh
|
|
615
|
+
cadence orchestrate finance-tracker:L41 ol-companion:L22 cadence:L18@haiku
|
|
616
|
+
cadence orchestrate L18 # from inside a project
|
|
617
|
+
cadence orchestrate … --budget 1.5M # 1500000, 1.5M, 800k; default 2M
|
|
618
|
+
cadence orchestrate … --dry-run # preconditions + the plan of the wave; nothing is started
|
|
619
|
+
cadence orchestrate --status [<wave>] # the table, read back from the state (default: the last wave)
|
|
620
|
+
cadence orchestrate --resume [<wave>] [--budget 1M] [--answer ol-companion:L22 "reply"]
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
You choose the lots; the order is the order given (one queue per repository, two repositories at most
|
|
624
|
+
at the same time). `@haiku|@sonnet|@opus` sets the model of the implementation and corrections of that
|
|
625
|
+
lot (default Sonnet; reviews are always Opus; Haiku only when you write it, for a mechanical lot). Run it
|
|
626
|
+
in the background and read `--status`: it prints one line per transition and the final table.
|
|
627
|
+
|
|
628
|
+
**Cycle of a lot**: preconditions (clean tracked files, lot `todo` or `doing`, dependencies met) →
|
|
629
|
+
`raf start` (committed alone) → implementation → **UX review** if the lot is `visible` and the app is
|
|
630
|
+
declared → **code review**, which always comes last (a UX fix changes code) → compliant (no blocking, no
|
|
631
|
+
major finding) → `raf review` is recorded by the orchestrator with the sha the review read, then
|
|
632
|
+
"ready to deliver". Not compliant → a correction in a new session, then a new review (the UX review of a
|
|
633
|
+
visible lot is replayed too, the code having changed), **two passes at most**, then the lot goes back to
|
|
634
|
+
you with the findings. A small lot (`estimate` ≤ 0.5 or `quickwin`) gets one single Opus pass for code and
|
|
635
|
+
usability (code only when the lot is not `visible`). Failing tests (reported red, or red when
|
|
636
|
+
`orchestrate.test` is run) go straight to a correction.
|
|
637
|
+
|
|
638
|
+
**Compliant with minor findings**: the minors are not left for a follow-up lot. One **minors pass** runs
|
|
639
|
+
before concluding: a new Sonnet session with its own brief (`fix-minors.md`) fixes the minors that are right
|
|
640
|
+
and lists in `choix`, with the reason, the ones it rejects (it never stops to ask); it does not count among the
|
|
641
|
+
two defect passes. A short code re-review follows (for a visible lot, after the UX review replayed), and
|
|
642
|
+
concludes even when it finds new minors, which are returned to you as proposals (no second minors pass).
|
|
643
|
+
When the pass makes no commit (every minor judged wrong) and HEAD has not moved since the compliant review,
|
|
644
|
+
the lot concludes on that original review (if HEAD moved, e.g. a resume after a cut-off session that had
|
|
645
|
+
committed, a short re-review of that commit runs instead): `ready`, verdict recorded with the sha that review read (`… + passe des mineurs sans commit`),
|
|
646
|
+
the untreated minors returned as proposals. Same when the budget is exhausted right after a compliant
|
|
647
|
+
review with minors: it concludes on that review instead of staying suspended.
|
|
648
|
+
|
|
649
|
+
**Choices, not questions**: the author brief tells the session to decide minor interpretation questions itself and to list them under `choix` in its report; the reviewer receives that list to re-read, and the final table prints each one (`choix fait : …`). A session stops with a question only on a real blocker (scope, architecture, costly to undo, not settled by the plan or CLAUDE.md).
|
|
650
|
+
|
|
651
|
+
**What stays with you**: choosing the lots, the questions raised (`--resume --answer`), re-verifying
|
|
652
|
+
after the wave (`git log`, tests, `raf check`), `raf done`, **`raf ux`** (the orchestrator reports the UX
|
|
653
|
+
verdict and screenshots, it does not record it), the push and the deliveries, one project at a time.
|
|
654
|
+
|
|
655
|
+
**Guards, imposed by the code**: at most two sessions, one per repository (a lock in the repository's
|
|
656
|
+
shared state, `orchestrate.lock`, which also makes `cadence deliver` refuse that repository) and one wave
|
|
657
|
+
per folder; `Agent`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions; a
|
|
658
|
+
temporary `pre-push` hook, installed for the duration of the wave and removed at its end, refuses any push
|
|
659
|
+
from a session (`CADENCE_ORCHESTRATED` is in their environment; a repository that already has another
|
|
660
|
+
`pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
|
|
661
|
+
upstream ref and `git ls-remote` are compared with the "before", and a review that changed `HEAD` or the
|
|
662
|
+
tree is an incident that stops the wave. `raf done|ux|review` and `cadence deliver` refuse when
|
|
663
|
+
`CADENCE_ORCHESTRATED` is set.
|
|
664
|
+
|
|
665
|
+
**Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
|
|
666
|
+
shown apart. When the budget (or the usage limit) is reached no new session starts, the running ones
|
|
667
|
+
finish, the wave is *suspended* (exit code 3) and `--resume --budget …` continues. A session that returns
|
|
668
|
+
nothing readable, times out (45 min for work, 25 for a review) or fails is not retried; the lot is handed
|
|
669
|
+
back with the cause. Exit codes: 0 every lot ready · 1 at least one lot handed back (question, failure,
|
|
670
|
+
review still not compliant after two passes) · 2 refused before acting · 3 wave suspended.
|
|
671
|
+
|
|
672
|
+
**Formatting retry** (the only `--resume` of a session): when a session ends successfully but in plain text,
|
|
673
|
+
without the `structured_output` the schema asks for (the verdict is there, not in the required shape), the
|
|
674
|
+
orchestrator resumes **that same session once** (`claude -p --resume <session-id> --json-schema <same schema>`,
|
|
675
|
+
same model, permission mode and denied tools) with a short prompt that only asks for the report in the required
|
|
676
|
+
format. It applies to every step that has a schema (implementation, correction, reviews). The tokens of the
|
|
677
|
+
retry count in the wave budget and in the step (`formatRetry: true`); if the output is still missing after it,
|
|
678
|
+
the step fails as usual — never a second retry.
|
|
679
|
+
|
|
680
|
+
State is in `.cadence/runs/<wave>/` of the folder where the command is run (added to `.git/info/exclude`
|
|
681
|
+
when that folder is in a repository): `wave.json`, one `<project>--<lot>.json` per lot (steps, tokens
|
|
682
|
+
kept apart, session ids, commits, verdicts), the JSON output of every session and a `journal.log`. After a
|
|
683
|
+
cut (Ctrl-C, WSL closed) `--resume` replays an interrupted step entirely in a new session whose brief
|
|
684
|
+
lists the commits already present; finished steps are never replayed.
|
|
685
|
+
|
|
686
|
+
Briefs are the templates of `templates/orchestrate/` (`implement.md` is the `lead` skill's standard
|
|
687
|
+
brief; `--dry-run` writes the rendered ones). A project can declare, in `cadence.yaml`:
|
|
688
|
+
|
|
689
|
+
```yaml
|
|
690
|
+
orchestrate:
|
|
691
|
+
test: npm test # run by the orchestrator after a work step (optional)
|
|
692
|
+
ux: http://localhost:4200 # a URL, a launch command, or { url, command } — for the UX review
|
|
693
|
+
permissionMode: auto # default
|
|
694
|
+
addDirs: [/home/me/projects/tmp] # extra directories the sessions may use
|
|
695
|
+
timeouts: { implement: 45, review: 25 } # minutes
|
|
696
|
+
# a plan kept by the project's own tool is read-only for raf: the orchestrator calls these instead
|
|
697
|
+
start: python3 scripts/raf.py start {lot}
|
|
698
|
+
verdict: python3 scripts/raf.py note {lot} "revue de code : {verdict}"
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Without `start`, a read-only plan's `todo` lot is refused (start it with the project's tool); without
|
|
702
|
+
`verdict`, the review verdict stays in the wave's state and you report it. Only the plan's files
|
|
703
|
+
(`plan.path`, `plan.files`) are committed from those commands; anything else dirty stops the lot.
|
|
704
|
+
|
|
458
705
|
## Claude Code skills
|
|
459
706
|
|
|
460
707
|
As a plugin:
|
|
@@ -472,7 +719,12 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
472
719
|
|
|
473
720
|
- **session-start**: reports the facts briefly, proposes three lots from the
|
|
474
721
|
plan, then waits for your priority — nothing starts before your answer.
|
|
475
|
-
- **session-close**: plan hygiene, clean repository,
|
|
722
|
+
- **session-close**: plan hygiene, clean repository, a cleanup proposal for stale
|
|
723
|
+
working files (`session.clean` patterns, older than `session.cleanDays`; never
|
|
724
|
+
anything git tracks, a git repository or a folder holding one, anything under
|
|
725
|
+
`.git`, a bare repository or separate git directory or anything inside one,
|
|
726
|
+
anything behind a symbolic link a `*` matched (only the link itself), a hidden `.xxx` name a `*` would not match, nor anything it could not
|
|
727
|
+
read entirely — it asks before deleting), memory limited to what the
|
|
476
728
|
repository does not say, new skills or agents proposed but never created, three
|
|
477
729
|
lines for next time.
|
|
478
730
|
- A project with its own tooling keeps it: its plan is read where it is (`plan:`),
|
|
@@ -483,9 +735,10 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
483
735
|
blind retry; after a green delivery that changes what a page shows or what it
|
|
484
736
|
is served, the `qa-reviewer` agent walks the delivered app.
|
|
485
737
|
- **lead**: from a folder holding several projects, one subagent per project
|
|
486
|
-
gathers the facts, you choose the priorities,
|
|
487
|
-
|
|
488
|
-
reviewed by the `code-reviewer` agent,
|
|
738
|
+
gathers the facts, you choose the priorities, the lots are delegated with
|
|
739
|
+
`cadence orchestrate` (fresh short sessions with a standard brief — test first,
|
|
740
|
+
commits citing the lot, no push — reviewed by the `code-reviewer` agent, see
|
|
741
|
+
[orchestrate](#orchestrate)), re-verified by the lead, then delivered
|
|
489
742
|
one project at a time; a delivery that changes what a page shows or what it is
|
|
490
743
|
served is then checked in the running app by the `qa-reviewer` agent, whose
|
|
491
744
|
blocking findings come back to you. Two
|
|
@@ -512,7 +765,7 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
512
765
|
broken, or a universal check failing with a visible effect, with or without an
|
|
513
766
|
expectations file), suspects (it looks like missing or wrong data and no
|
|
514
767
|
expectation settles it) or noise (a console error or a failed request with no
|
|
515
|
-
visible effect, ranked minor), ranked, each with
|
|
768
|
+
visible effect, ranked minor) or already planned (an open lot covers it: returned in one line, the lot id and its title), ranked, each with
|
|
516
769
|
the route, what was expected, what was measured and the evidence; pages checked
|
|
517
770
|
N/N, follow-ups as `raf add` lines, what it could not verify, a one-line
|
|
518
771
|
verdict. Read-only: GET only, no login, nothing submitted; it stops at a PIN.
|
|
@@ -523,14 +776,18 @@ A version exists in three places and is published in two; a release does all of
|
|
|
523
776
|
|
|
524
777
|
1. Bump `version` in `package.json` (then `npm install` to refresh `package-lock.json`),
|
|
525
778
|
`.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`, in the commit that closes the lot.
|
|
526
|
-
2. `
|
|
527
|
-
|
|
528
|
-
|
|
779
|
+
2. `git tag v<version> && git push origin main v<version>` — the tag starts `.github/workflows/publish.yml`,
|
|
780
|
+
which publishes to npm through Trusted Publishing (OIDC, no token stored anywhere): it checks the tag
|
|
781
|
+
matches `package.json`, `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`, then `npm publish --provenance`, where `prepublishOnly` runs the type-check and the
|
|
782
|
+
tests and `prepare` builds `dist/`; a red suite stops the publication. The trusted publisher is declared
|
|
783
|
+
once on npmjs.com (package settings → Trusted Publisher → GitHub Actions, `Sylad/cadence`, `publish.yml`).
|
|
784
|
+
3. Watch the run: `gh run watch` (or `gh run list --workflow publish.yml`).
|
|
529
785
|
4. Check the effect: `npm view @sylad/cadence version` answers the new version.
|
|
530
786
|
|
|
531
|
-
The Claude Code plugin is read from the repository, so
|
|
532
|
-
`npx @sylad/cadence` and a global install read
|
|
533
|
-
0.3.0 and 0.4.0 were never published
|
|
787
|
+
The Claude Code plugin is read from the repository, so pushing `main` is what updates it; npm is what
|
|
788
|
+
`npx @sylad/cadence` and a global install read, and only the tag publishes there. A missing tag, or a red
|
|
789
|
+
publish run, leaves npm behind without any other error — 0.3.0 and 0.4.0 were never published — hence
|
|
790
|
+
step 4.
|
|
534
791
|
|
|
535
792
|
## License
|
|
536
793
|
|
package/agents/qa-reviewer.md
CHANGED
|
@@ -23,6 +23,9 @@ missing, or the URL does not answer, say so and stop.
|
|
|
23
23
|
|
|
24
24
|
1. **Read how to reach the app**: the project's CLAUDE.md, then its README — the routes, the demo
|
|
25
25
|
data, what sits behind a PIN or a login.
|
|
26
|
+
Read the open lots of the plan (status `todo` or `doing`) with `raf list --status todo` and
|
|
27
|
+
`raf list --status doing`, or the project's own tool when the plan is read-only, to know what is
|
|
28
|
+
already planned.
|
|
26
29
|
2. **Read the expectations**: `docs/qa/expectations.md`, or the file named by `qa.expectations` in
|
|
27
30
|
`cadence.yaml`. One `## <route>` section per page: what the page `shows:` (the content that
|
|
28
31
|
must be present and non-empty, with a count where one exists), what must `never:` appear (error
|
|
@@ -42,6 +45,7 @@ missing, or the URL does not answer, say so and stop.
|
|
|
42
45
|
and **390 px** wide. Let it settle: after `load`, wait a fixed few seconds, scroll through the
|
|
43
46
|
page (lazy images), wait again — never for network idle, which streams and polling never reach.
|
|
44
47
|
Then measure:
|
|
48
|
+
- browser state, once before the first page, in a profile already used (a persistent context, not a fresh one — a `userDataDir` reserved for QA and kept between passes, never the user's own browser profile): compare the bundle the page loaded (its script URL) with the one `index.html` references, re-read without cache — a returning visitor still holds the old one, so a difference is stated in the report — then clear the cache and measure;
|
|
45
49
|
- the expected content is present and non-empty — name the selector or the text found and its
|
|
46
50
|
count (`.player-card` ×14), not "the list looks fine";
|
|
47
51
|
- no `never:` text on screen, and no other error or missing-data message;
|
|
@@ -56,19 +60,22 @@ missing, or the URL does not answer, say so and stop.
|
|
|
56
60
|
items that should carry an image and have no loaded `<img>` — a fallback badge replacing a
|
|
57
61
|
failed image has no `<img>` at all;
|
|
58
62
|
- at 390 px, content hidden on the phone by design is not a defect unless a `shows:` line
|
|
59
|
-
requires it at 390; content pushed outside the visible area (it needs a sideways scroll) is
|
|
60
|
-
|
|
63
|
+
requires it at 390; content pushed outside the visible area (it needs a sideways scroll) is reported as suspect and handed to `ux-reviewer` in one line;
|
|
64
|
+
- the layout width is measured, not judged by eye: `document.documentElement.scrollWidth` against `clientWidth` at each width;
|
|
61
65
|
- states behind controls: tabs, filters and other controls that only change the view may be used
|
|
62
66
|
and are part of the page (a tab that triggers its own API call is checked like a page); a
|
|
63
67
|
control that writes is never used;
|
|
68
|
+
- texts are compared without case (`never:` texts, labels, statuses): "Eliminated" and "ELIMINATED" are the same text;
|
|
69
|
+
- outbound links (another origin): each has a real `href` (not empty, not `#`) — `rel=noopener` is not required, `target=_blank` implies it since Chrome 88, Firefox 79 and Safari 12.1 — read from the attributes, never requested;
|
|
70
|
+
- simulated states: a route you intercept to fail or answer empty may be used to see how the page copes; what it shows is never a finding by itself — only what the real app serves is;
|
|
64
71
|
- pacing: pause between pages; when a 429 (or any rate-limit answer) appears, re-run that page
|
|
65
72
|
ALONE after a quiet minute before concluding — if it reproduces, an ordinary visitor gets it;
|
|
66
|
-
if not, it was your own pace and it is not a finding;
|
|
73
|
+
if not, it was your own pace and it is not a finding; the quiet minute is spent on `about:blank`, never on the app, whose polling would keep calling its backend;
|
|
67
74
|
- the frontend source may be read to LOCATE a cause after a measurement, never as evidence.
|
|
68
75
|
5. **GET only, and nothing that writes**: never log in, never submit a form that writes, never
|
|
69
76
|
click a control that changes data, never send a POST, PUT, PATCH or DELETE yourself. If a PIN
|
|
70
77
|
or a login wall is met, say so and stop there for those pages: they go under "not verified",
|
|
71
|
-
they are neither a finding nor a page checked.
|
|
78
|
+
they are neither a finding nor a page checked. GET only is not "without effect": a probe on an asset name that does not exist was cached for 4 hours by the CDN and then served to real visitors. Request only URLs the app itself uses, or add a cache-busting query parameter.
|
|
72
79
|
6. **Classify** what you see:
|
|
73
80
|
- *defect* — a line of the expectations is broken, or a universal check fails with a visible
|
|
74
81
|
effect on the page: an error message shown, a failed API call whose content is missing on
|
|
@@ -81,11 +88,12 @@ missing, or the URL does not answer, say so and stop.
|
|
|
81
88
|
contradicted by the page's own data ("eliminated" beside a won match), a stale season
|
|
82
89
|
label. Say why, and propose the line of expectations that would settle it;
|
|
83
90
|
- *noise* — a console error or a failed request with no visible effect: reported, ranked minor;
|
|
91
|
+
- *already planned* — a finding already planned by an open lot is returned in one line — the lot id and its title — not as a new finding and not as a follow-up;
|
|
84
92
|
- *out of scope* — usability and accessibility belong to `ux-reviewer`, code quality to
|
|
85
93
|
`code-reviewer`: one line at most, never a finding.
|
|
86
94
|
7. **Rank** each finding: *blocking* (a page's main content is missing, its main information is
|
|
87
95
|
false, or an error is shown to the user), *major* (secondary content missing or wrong, a section
|
|
88
|
-
silently dropped after a failed or empty API call, a broken content image), *minor* (noise).
|
|
96
|
+
silently dropped after a failed or empty API call, a broken content image), *minor* (noise). A broken line of the expectations with no visible loss on the page (the content is on screen by another path) is *minor* too.
|
|
89
97
|
|
|
90
98
|
## Output
|
|
91
99
|
|
|
@@ -99,6 +107,7 @@ A short report:
|
|
|
99
107
|
quote the line of the expectations, or name the universal check, or, for a suspect, give the
|
|
100
108
|
expectation line you propose —, what was measured, and the evidence — status code, response
|
|
101
109
|
size, the text on screen, the capture. No finding without a measurement.
|
|
110
|
+
- **Already planned**: one line per finding already planned by an open lot — the lot id and its title —, kept out of Findings and of Proposed follow-ups.
|
|
102
111
|
- **Not verified**: pages behind a PIN or a login, states that need data you could not get, a
|
|
103
112
|
browser tool that was missing or could not give status and size — stated plainly.
|
|
104
113
|
- **Proposed follow-ups**: one `raf add "…"` line per finding worth doing; on a read-only plan
|
|
@@ -109,7 +118,7 @@ A short report:
|
|
|
109
118
|
returned". It is the last line of the report.
|
|
110
119
|
|
|
111
120
|
Captures and temporary files go in a temporary directory outside the repository, or in the one
|
|
112
|
-
the caller names; remove them, or list their paths in the report. The working tree is left as you
|
|
121
|
+
the caller names; remove them, or list their paths in the report — except the QA browser profile, which is kept for the next pass. The working tree is left as you
|
|
113
122
|
found it.
|
|
114
123
|
|
|
115
124
|
## Do not
|
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', 'orchestrate'].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'));
|
|
@@ -31,6 +31,11 @@ if (tool === 'raf') {
|
|
|
31
31
|
cadence deliver [--dry-run] [--sha rév] [--config cadence.yaml] [-- arguments du script du projet]
|
|
32
32
|
CI du sha poussé → déploiement → vérifications de l'effet ;
|
|
33
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
|
|
37
|
+
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run]
|
|
38
|
+
une session claude neuve par étape (implémentation, revues, corrections) ; --status, --resume
|
|
34
39
|
cadence skills install [--dir .claude] [--force]
|
|
35
40
|
installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
|
|
36
41
|
process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
|
package/dist/audit.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
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';
|
|
7
|
+
import { isRecurring } from './recurring.js';
|
|
6
8
|
import { isOpen } from './plan.js';
|
|
7
|
-
/**
|
|
9
|
+
/** Chemin de la configuration lue, relatif à la racine : celui de --config, sinon cadence.yaml. */
|
|
10
|
+
function configRel(plan, root) {
|
|
11
|
+
return plan.configFile ? relative(root, plan.configFile) : 'cadence.yaml';
|
|
12
|
+
}
|
|
13
|
+
/** Le plan, sa page Gantt, la configuration lue (cadence.yaml) et les fichiers tenus avec lui (plan.files). */
|
|
8
14
|
function ownFiles(plan, root) {
|
|
9
|
-
return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), ...plan.files]);
|
|
15
|
+
return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), configRel(plan, root), ...plan.files]);
|
|
16
|
+
}
|
|
17
|
+
function withoutPlanKey(text) {
|
|
18
|
+
if (text === null)
|
|
19
|
+
return '{}';
|
|
20
|
+
try {
|
|
21
|
+
const { plan: _plan, ...rest } = (parse(text) ?? {});
|
|
22
|
+
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));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Le commit ne change de la configuration que la clé `plan:` ? Un fichier illisible avant ou après compte comme du travail. */
|
|
29
|
+
function onlyPlanKeyChanged(root, sha, file) {
|
|
30
|
+
const before = withoutPlanKey(fileAt(root, `${sha}^`, file));
|
|
31
|
+
const after = withoutPlanKey(fileAt(root, sha, file));
|
|
32
|
+
return before !== null && before === after;
|
|
10
33
|
}
|
|
11
34
|
/** Commits du dépôt sans les commits automatiques (motifs `ignore`) : ni audités ni comptés pour un lot. */
|
|
12
35
|
export function planCommits(plan, root, opts = {}) {
|
|
@@ -14,28 +37,25 @@ export function planCommits(plan, root, opts = {}) {
|
|
|
14
37
|
const commits = readCommits(root, opts);
|
|
15
38
|
return patterns.length ? commits.filter((c) => !patterns.some((re) => re.test(c.subject))) : commits;
|
|
16
39
|
}
|
|
17
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Commit d'entretien du plan : TOUS ses fichiers sont des fichiers du plan (le plan, sa page Gantt,
|
|
42
|
+
* plan.files — son plan publié par exemple — et la configuration lue, mais celle-ci seulement quand la
|
|
43
|
+
* clé `plan:` est la seule à changer : deliver/session/… sont du travail). Les fichiers décident,
|
|
44
|
+
* jamais le sujet : « chore(plan): … » qui touche un fichier source est un commit comme un autre.
|
|
45
|
+
*/
|
|
18
46
|
export function isPlanOnly(sha, plan, root) {
|
|
19
47
|
const own = ownFiles(plan, root);
|
|
48
|
+
const config = configRel(plan, root);
|
|
20
49
|
const files = changedFiles(root, sha);
|
|
21
|
-
return files.length > 0 && files.every((f) => own.has(f));
|
|
50
|
+
return files.length > 0 && files.every((f) => own.has(f) && (f !== config || onlyPlanKeyChanged(root, sha, f)));
|
|
22
51
|
}
|
|
23
52
|
/**
|
|
24
|
-
* N'ont pas besoin de citer un lot : un commit d'entretien du plan
|
|
25
|
-
*
|
|
26
|
-
* exemple) — et un commit automatique dont le sujet correspond à un motif `ignore:` du plan.
|
|
27
|
-
* Les fichiers décident, jamais le sujet : « chore(plan): … » qui touche un fichier source est un
|
|
28
|
-
* commit comme un autre.
|
|
53
|
+
* N'ont pas besoin de citer un lot : un commit d'entretien du plan (cf. isPlanOnly) et un commit
|
|
54
|
+
* automatique dont le sujet correspond à un motif `ignore:` du plan.
|
|
29
55
|
*/
|
|
30
56
|
export function exemptPlanOnly(linked, plan, root) {
|
|
31
|
-
const own = ownFiles(plan, root);
|
|
32
57
|
const { patterns } = plan.ignore;
|
|
33
|
-
const orphans = linked.orphans.filter((c) =>
|
|
34
|
-
if (patterns.some((re) => re.test(c.subject)))
|
|
35
|
-
return false;
|
|
36
|
-
const files = changedFiles(root, c.sha);
|
|
37
|
-
return files.length === 0 || !files.every((f) => own.has(f));
|
|
38
|
-
});
|
|
58
|
+
const orphans = linked.orphans.filter((c) => !patterns.some((re) => re.test(c.subject)) && !isPlanOnly(c.sha, plan, root));
|
|
39
59
|
return { ...linked, orphans };
|
|
40
60
|
}
|
|
41
61
|
/** Fenêtre de l'audit : --since, sinon la date d'adoption du plan, sinon 30 jours. */
|
|
@@ -73,7 +93,7 @@ function workCommits(plan, root, commits) {
|
|
|
73
93
|
return commits.filter((c) => (!adopted || c.day >= adopted) && !isPlanOnly(c.sha, plan, root));
|
|
74
94
|
}
|
|
75
95
|
/** Commits liés à un lot, du plus récent au plus ancien, avant tout tri : commits de plan et d'avant l'adoption compris. */
|
|
76
|
-
function lotCommits(plan, root, lotId) {
|
|
96
|
+
export function lotCommits(plan, root, lotId) {
|
|
77
97
|
return linkCommits(plan.lots(), planCommits(plan, root), plan.refs).byLot.get(lotId) ?? [];
|
|
78
98
|
}
|
|
79
99
|
/** Commits à relire d'un lot, du plus récent au plus ancien — ce que compte la porte de revue de code et que liste `raf commits`. */
|
|
@@ -136,7 +156,9 @@ export function reviewIssues(plan, root, byLot) {
|
|
|
136
156
|
}));
|
|
137
157
|
}
|
|
138
158
|
/** Ce qui vient ensuite : lots en cours, puis lots prêts (dépendances closes), gains rapides d'abord. */
|
|
139
|
-
export function nextUp(
|
|
159
|
+
export function nextUp(all) {
|
|
160
|
+
// Les lots récurrents ont leur propre section (raf now) : ils ne sont ni « en cours » ni « à suivre ».
|
|
161
|
+
const lots = all.filter((l) => !isRecurring(l));
|
|
140
162
|
const byId = new Map(lots.map((l) => [l.id, l]));
|
|
141
163
|
const doing = lots.filter((l) => l.status === 'doing');
|
|
142
164
|
const ready = lots
|
package/dist/check.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { diffDays } from './dates.js';
|
|
2
2
|
import { isOpen } from './plan.js';
|
|
3
|
+
import { isRecurring } from './recurring.js';
|
|
3
4
|
/** Un commit sur une ligne : sha abrégé et sujet. */
|
|
4
5
|
export const short = (c) => `${c.sha.slice(0, 7)} ${c.subject}`;
|
|
5
6
|
export function check(lots, linked, today, idleDays = 7) {
|
|
@@ -12,10 +13,12 @@ export function check(lots, linked, today, idleDays = 7) {
|
|
|
12
13
|
}
|
|
13
14
|
for (const lot of lots) {
|
|
14
15
|
const commits = linked.byLot.get(lot.id) ?? [];
|
|
15
|
-
|
|
16
|
+
// Un lot récurrent reste ouvert par construction : ni todo-avec-commits ni idle.
|
|
17
|
+
const recurring = isRecurring(lot);
|
|
18
|
+
if (lot.status === 'todo' && !recurring && commits.length > 0) {
|
|
16
19
|
issues.push({ kind: 'todo-with-commits', message: `${lot.id} a ${commits.length} commit(s) mais est encore todo — raf start ${lot.id}` });
|
|
17
20
|
}
|
|
18
|
-
if (lot.status === 'doing') {
|
|
21
|
+
if (lot.status === 'doing' && !recurring) {
|
|
19
22
|
const last = commits[0]?.day ?? lot.started;
|
|
20
23
|
if (last && diffDays(last, today) > idleDays) {
|
|
21
24
|
issues.push({ kind: 'idle', message: `${lot.id} en cours sans commit depuis ${diffDays(last, today)} j (${lot.title})` });
|