@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.
Files changed (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +274 -17
  4. package/agents/qa-reviewer.md +15 -6
  5. package/bin/cadence.js +6 -1
  6. package/dist/audit.js +41 -19
  7. package/dist/check.js +5 -2
  8. package/dist/clean.js +225 -0
  9. package/dist/cli.js +113 -12
  10. package/dist/config.js +82 -6
  11. package/dist/deliver.js +83 -32
  12. package/dist/git.js +12 -2
  13. package/dist/link.js +15 -1
  14. package/dist/orchestrate/briefs.js +52 -0
  15. package/dist/orchestrate/command.js +498 -0
  16. package/dist/orchestrate/cycle.js +673 -0
  17. package/dist/orchestrate/guard.js +131 -0
  18. package/dist/orchestrate/launch.js +221 -0
  19. package/dist/orchestrate/lock.js +85 -0
  20. package/dist/orchestrate/pool.js +51 -0
  21. package/dist/orchestrate/result.js +94 -0
  22. package/dist/orchestrate/schemas.js +48 -0
  23. package/dist/orchestrate/state.js +93 -0
  24. package/dist/orchestrate/table.js +73 -0
  25. package/dist/plan.js +61 -2
  26. package/dist/proc.js +260 -0
  27. package/dist/recurring.js +19 -0
  28. package/dist/schedule.js +4 -1
  29. package/dist/session.js +18 -1
  30. package/dist/verify.js +114 -0
  31. package/package.json +18 -3
  32. package/skills/lead/SKILL.md +33 -13
  33. package/skills/session-close/SKILL.md +25 -4
  34. package/skills/session-start/SKILL.md +3 -0
  35. package/templates/orchestrate/fix-minors.md +18 -0
  36. package/templates/orchestrate/fix.md +16 -0
  37. package/templates/orchestrate/implement.md +20 -0
  38. package/templates/orchestrate/review-recheck.md +11 -0
  39. package/templates/orchestrate/review-small.md +11 -0
  40. package/templates/orchestrate/review.md +8 -0
  41. 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.7.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.7.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, or a file the
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 and does not make a code review stale. The files
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, memory limited to what the
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, each lot is delegated to a
487
- subagent with a standard brief (test first, commits citing the lot, no push),
488
- reviewed by the `code-reviewer` agent, re-verified by the lead, then delivered
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. `npm publish --access public` — `prepublishOnly` runs the type-check and the tests first, `prepare`
527
- builds `dist/`; a red suite stops the publication.
528
- 3. `git tag v<version> && git push origin main v<version>`.
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 step 3 is what updates it; npm is what
532
- `npx @sylad/cadence` and a global install read. Skipping step 2 leaves npm behind without any error —
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
 
@@ -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
- reported as suspect and handed to `ux-reviewer` in one line;
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 { changedFiles, readCommits } from './git.js';
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
- /** Le plan, sa page Gantt et les fichiers tenus avec lui (cadence.yaml : plan.files). */
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
- /** Commit d'entretien du plan : ne touche-t-il que des fichiers du plan (plan, page Gantt, plan.files) ? */
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 — TOUS ses fichiers sont des fichiers
25
- * du plan (le plan, sa page Gantt, ceux que le projet déclare sous plan.files, son plan publié par
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(lots) {
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
- if (lot.status === 'todo' && commits.length > 0) {
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})` });