@sylad/cadence 0.8.0 → 0.10.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 (39) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +207 -15
  4. package/agents/qa-reviewer.md +15 -6
  5. package/bin/cadence.js +3 -1
  6. package/dist/audit.js +5 -2
  7. package/dist/check.js +5 -2
  8. package/dist/clean.js +225 -0
  9. package/dist/cli.js +67 -14
  10. package/dist/config.js +82 -6
  11. package/dist/deliver.js +5 -4
  12. package/dist/git.js +29 -0
  13. package/dist/orchestrate/briefs.js +52 -0
  14. package/dist/orchestrate/command.js +498 -0
  15. package/dist/orchestrate/cycle.js +674 -0
  16. package/dist/orchestrate/guard.js +131 -0
  17. package/dist/orchestrate/launch.js +221 -0
  18. package/dist/orchestrate/lock.js +85 -0
  19. package/dist/orchestrate/pool.js +51 -0
  20. package/dist/orchestrate/result.js +94 -0
  21. package/dist/orchestrate/schemas.js +48 -0
  22. package/dist/orchestrate/state.js +93 -0
  23. package/dist/orchestrate/table.js +73 -0
  24. package/dist/plan.js +57 -2
  25. package/dist/proc.js +109 -10
  26. package/dist/recurring.js +19 -0
  27. package/dist/schedule.js +4 -1
  28. package/dist/session.js +17 -1
  29. package/dist/verify.js +30 -3
  30. package/package.json +6 -1
  31. package/skills/lead/SKILL.md +33 -13
  32. package/skills/session-close/SKILL.md +25 -4
  33. package/templates/orchestrate/fix-minors.md +22 -0
  34. package/templates/orchestrate/fix.md +21 -0
  35. package/templates/orchestrate/implement.md +25 -0
  36. package/templates/orchestrate/review-recheck.md +11 -0
  37. package/templates/orchestrate/review-small.md +11 -0
  38. package/templates/orchestrate/review.md +8 -0
  39. 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.8.0",
9
+ "version": "0.10.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.8.0",
4
+ "version": "0.10.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
@@ -65,7 +65,9 @@ raf gantt # docs/plan/gantt.html
65
65
  | Command | Effect |
66
66
  |---|---|
67
67
  | `raf init [--project name] [--prefix L] [--no-hook]` | create the plan and install the hook |
68
- | `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 |
69
71
  | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`) |
70
72
  | `raf note <id> "text"` | dated note — keep decisions next to the work |
71
73
  | `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first |
@@ -139,6 +141,7 @@ plan:
139
141
  lots: taches # root key holding the list (default: lots)
140
142
  fields: # raf field: key in the file (a list = first one present)
141
143
  title: titre
144
+ public: titre_public # the lot's title for the public; read when it is text, else ignored
142
145
  status: etat
143
146
  estimate: effort
144
147
  created: cree_le
@@ -154,7 +157,7 @@ plan:
154
157
  estimates: { S: 0.5, M: 1, L: 3 } # their effort labels, in working days
155
158
  ```
156
159
 
157
- - Fields: `id`, `title`, `status`, `estimate`, `quickwin`, `visible`, `after`, `created`, `started`,
160
+ - Fields: `id`, `title`, `status`, `estimate`, `quickwin`, `visible`, `public`, `every`, `last`, `after`, `created`, `started`,
158
161
  `finished`, `notes`, `parent`; one left out is read under its own name. A timestamp counts for
159
162
  its day; a note written as plain text is one note.
160
163
  - `parent`: an entry `B33/t1-fusion` whose parent is `B33` becomes the sub-task `t1-fusion` of `B33`.
@@ -382,6 +385,48 @@ session:
382
385
  close: ./scripts/evening.sh "$CADENCE_SINCE"
383
386
  ```
384
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
+
385
430
  The commands get `CADENCE_SINCE` (the `--since` in effect) and `CADENCE_TODAY`. They
386
431
  add facts and decide nothing: a failing command is reported and changes neither
387
432
  the exit code nor the verdict.
@@ -430,7 +475,14 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
430
475
  signal from the terminal: cadence gives the whole followed set, not just the
431
476
  command, up to 2 seconds to finish (a script's `trap`, git removing its
432
477
  `index.lock`), carries on the moment none of it is alive, and kills what is
433
- left after the 2 seconds, a background child that ignores Ctrl-C included.
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.
434
486
  A delay overrun and `SIGTERM` give no grace: a script's own `trap` does not
435
487
  get to finish there. The lock is held until the followed set is empty: when
436
488
  cadence dies of the signal it dies after the tree, and its stale lock, which
@@ -442,6 +494,12 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
442
494
  a background process still running when the command returns normally (it is
443
495
  neither waited for nor killed), and everything if cadence itself is killed
444
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.
445
503
  - Commands run in cadence's own process group and session, attached to the
446
504
  terminal: `ssh`, `sudo` or `pinentry` can prompt on `/dev/tty`, and Ctrl-C or
447
505
  closing the terminal stops the running command together with cadence (within
@@ -500,7 +558,7 @@ no journal entry, nothing written.
500
558
  ```sh
501
559
  cadence verify # one pass, one line per check, then a one-line summary
502
560
  cadence verify --retry 60 # retry each failing check for up to 60 s (deliver's 300 s is not applied)
503
- cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default: last delivery, else HEAD)
561
+ cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default: see below)
504
562
  ```
505
563
 
506
564
  ```
@@ -512,6 +570,22 @@ verify : 1 effet rouge sur 2 vérifications
512
570
  Exit code: **0** every check green · **1** at least one red effect · **2** nothing
513
571
  to verify or invalid configuration.
514
572
 
573
+ **Which sha is expected.** By default `${SHA}` / `${SHORT}` is the last recorded
574
+ delivery (else `HEAD`). One case differs: a project with `ci: none` (the default) and
575
+ **neither** deploy commands (`deliver.deploy`) nor a delivery script — a host that
576
+ builds every push, like Cloudflare Pages — delivers by pushing, and every push counts,
577
+ plan-maintenance commits included. The expected sha is then the head of the tracked
578
+ upstream branch (`origin/<branch>`, as last fetched — `verify` does no network git),
579
+ **provided that upstream is the remote's production branch**: read without network from
580
+ `refs/remotes/origin/HEAD` (set by `git clone` or `git remote set-head origin <branch>`);
581
+ when that reference does not exist locally (repository created with `git init` + push),
582
+ `main` then `master` are taken as the production branch. A work branch pushed with `-u`
583
+ is not published by such a host, so it keeps the default rule. A project with `ci: github`
584
+ or a `ci.command` and no deploy also keeps the last delivery. When local `HEAD` is ahead
585
+ of the upstream, the report says so first — `2 commit(s) non poussé(s) — l'effet vérifié
586
+ est celui de origin/main` — so a red there reads "waiting for a push", not "broken
587
+ effect". `session start` uses the same rule; `--sha` always wins.
588
+
515
589
  Time limits differ from deliver's. `verify` runs all the checks **in parallel**,
516
590
  each try with the whole 120 s limit (a `url` request gives up after 20 s; a slow
517
591
  check is killed at its limit, "délai dépassé", and takes nothing from the others); results are printed in the order
@@ -545,6 +619,114 @@ the others: it never changes the exit code, and an unreachable network shows up
545
619
  as red lines ("erreur réseau") without blocking the session. No section for a
546
620
  project without `deliver.verify`.
547
621
 
622
+ ## orchestrate
623
+
624
+ `cadence orchestrate` is a program above `/lead`, not a conversation: for each lot you choose, it runs
625
+ a **fresh** `claude -p` session per step with a short brief, reads the result, writes the state in
626
+ files and moves on. The lead session keeps only the decision (which lots), the final table and the
627
+ questions. Nothing is resumed: a correction is a new session, never the author's reopened. The one
628
+ exception is a **formatting retry** (below).
629
+
630
+ ```sh
631
+ cadence orchestrate finance-tracker:L41 ol-companion:L22 cadence:L18@haiku
632
+ cadence orchestrate L18 # from inside a project
633
+ cadence orchestrate … --budget 1.5M # 1500000, 1.5M, 800k; default 2M
634
+ cadence orchestrate … --dry-run # preconditions + the plan of the wave; nothing is started
635
+ cadence orchestrate --status [<wave>] # the table, read back from the state (default: the last wave)
636
+ cadence orchestrate --resume [<wave>] [--budget 1M] [--answer ol-companion:L22 "reply"]
637
+ ```
638
+
639
+ You choose the lots; the order is the order given (one queue per repository, two repositories at most
640
+ at the same time). `@haiku|@sonnet|@opus` sets the model of the implementation and corrections of that
641
+ lot (default Sonnet; reviews are always Opus; Haiku only when you write it, for a mechanical lot). Run it
642
+ in the background and read `--status`: it prints one line per transition and the final table.
643
+
644
+ **Cycle of a lot**: preconditions (clean tracked files, lot `todo` or `doing`, dependencies met) →
645
+ `raf start` (committed alone) → implementation → **UX review** if the lot is `visible` and the app is
646
+ declared → **code review**, which always comes last (a UX fix changes code) → compliant (no blocking, no
647
+ major finding) → `raf review` is recorded by the orchestrator with the sha the review read, then
648
+ "ready to deliver". Not compliant → a correction in a new session, then a new review (the UX review of a
649
+ visible lot is replayed too, the code having changed), **two passes at most**, then the lot goes back to
650
+ you with the findings. A small lot (`estimate` ≤ 0.5 or `quickwin`) gets one single Opus pass for code and
651
+ usability (code only when the lot is not `visible`). Failing tests (reported red, or red when
652
+ `orchestrate.test` is run) go straight to a correction.
653
+
654
+ **A lot that already has commits** (a spec commit, an interrupted wave, a lot committed by hand, a lot sent back after a
655
+ review) still starts with the implementation session: its brief tells it to read the lot, its notes and its open
656
+ sub-tasks, which carry the findings of any earlier review. If that session ends without a new commit on a lot that
657
+ already has commits, the wave goes on to the review (warning `implement sans nouveau commit`) instead of handing the
658
+ lot back; with no commit on the lot at all it is handed back as `implement sans commit`. The same goes for a
659
+ correction pass: a `fix` session that adds no commit to a lot that already has work commits goes on to a fresh review
660
+ (warning `fix sans nouveau commit : revue lancée sur les commits du lot`), the pass still counted — the cap of two
661
+ correction passes stays the guard against a loop; with no commit on the lot it is handed back as `fix sans commit`.
662
+
663
+ **Compliant with minor findings**: the minors are not left for a follow-up lot. One **minors pass** runs
664
+ before concluding: a new Sonnet session with its own brief (`fix-minors.md`) fixes the minors that are right
665
+ and lists in `choix`, with the reason, the ones it rejects (it never stops to ask); it does not count among the
666
+ two defect passes. A short code re-review follows (for a visible lot, after the UX review replayed), and
667
+ concludes even when it finds new minors, which are returned to you as proposals (no second minors pass).
668
+ When the pass makes no commit (every minor judged wrong) and HEAD has not moved since the compliant review,
669
+ the lot concludes on that original review (if HEAD moved, e.g. a resume after a cut-off session that had
670
+ committed, a short re-review of that commit runs instead): `ready`, verdict recorded with the sha that review read (`… + passe des mineurs sans commit`),
671
+ the untreated minors returned as proposals. Same when the budget is exhausted right after a compliant
672
+ review with minors: it concludes on that review instead of staying suspended.
673
+
674
+ **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: a decision that changes the scope or the architecture or is costly to undo, AND that the plan, its notes and CLAUDE.md do not settle; everything else is a choice.
675
+
676
+ **What stays with you**: choosing the lots, the questions raised (`--resume --answer`), re-verifying
677
+ after the wave (`git log`, tests, `raf check`), `raf done`, **`raf ux`** (the orchestrator reports the UX
678
+ verdict and screenshots, it does not record it), the push and the deliveries, one project at a time.
679
+
680
+ **Guards, imposed by the code**: at most two sessions, one per repository (a lock in the repository's
681
+ shared state, `orchestrate.lock`, which also makes `cadence deliver` refuse that repository) and one wave
682
+ per folder; `Agent`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions; a
683
+ temporary `pre-push` hook, installed for the duration of the wave and removed at its end, refuses any push
684
+ from a session (`CADENCE_ORCHESTRATED` is in their environment; a repository that already has another
685
+ `pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
686
+ upstream ref and `git ls-remote` are compared with the "before", and a review that changed `HEAD` or the
687
+ tree is an incident that stops the wave. `raf done|ux|review` and `cadence deliver` refuse when
688
+ `CADENCE_ORCHESTRATED` is set.
689
+
690
+ **Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
691
+ shown apart. When the budget (or the usage limit) is reached no new session starts, the running ones
692
+ finish, the wave is *suspended* (exit code 3) and `--resume --budget …` continues. A session that returns
693
+ nothing readable, times out (45 min for work, 25 for a review) or fails is not retried; the lot is handed
694
+ back with the cause. Exit codes: 0 every lot ready · 1 at least one lot handed back (question, failure,
695
+ review still not compliant after two passes) · 2 refused before acting · 3 wave suspended.
696
+
697
+ **Formatting retry** (the only `--resume` of a session): when a session ends successfully but in plain text,
698
+ without the `structured_output` the schema asks for (the verdict is there, not in the required shape), the
699
+ orchestrator resumes **that same session once** (`claude -p --resume <session-id> --json-schema <same schema>`,
700
+ same model, permission mode and denied tools) with a short prompt that only asks for the report in the required
701
+ format. It applies to every step that has a schema (implementation, correction, reviews). The tokens of the
702
+ retry count in the wave budget and in the step (`formatRetry: true`); if the output is still missing after it,
703
+ the step fails as usual — never a second retry.
704
+
705
+ State is in `.cadence/runs/<wave>/` of the folder where the command is run (added to `.git/info/exclude`
706
+ when that folder is in a repository): `wave.json`, one `<project>--<lot>.json` per lot (steps, tokens
707
+ kept apart, session ids, commits, verdicts), the JSON output of every session and a `journal.log`. After a
708
+ cut (Ctrl-C, WSL closed) `--resume` replays an interrupted step entirely in a new session whose brief
709
+ lists the commits already present; finished steps are never replayed.
710
+
711
+ Briefs are the templates of `templates/orchestrate/` (`implement.md` is the `lead` skill's standard
712
+ brief; `--dry-run` writes the rendered ones). A project can declare, in `cadence.yaml`:
713
+
714
+ ```yaml
715
+ orchestrate:
716
+ test: npm test # run by the orchestrator after a work step (optional)
717
+ ux: http://localhost:4200 # a URL, a launch command, or { url, command } — for the UX review
718
+ permissionMode: auto # default
719
+ addDirs: [/home/me/projects/tmp] # extra directories the sessions may use
720
+ timeouts: { implement: 45, review: 25 } # minutes
721
+ # a plan kept by the project's own tool is read-only for raf: the orchestrator calls these instead
722
+ start: python3 scripts/raf.py start {lot}
723
+ verdict: python3 scripts/raf.py note {lot} "revue de code : {verdict}"
724
+ ```
725
+
726
+ Without `start`, a read-only plan's `todo` lot is refused (start it with the project's tool); without
727
+ `verdict`, the review verdict stays in the wave's state and you report it. Only the plan's files
728
+ (`plan.path`, `plan.files`) are committed from those commands; anything else dirty stops the lot.
729
+
548
730
  ## Claude Code skills
549
731
 
550
732
  As a plugin:
@@ -562,7 +744,12 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
562
744
 
563
745
  - **session-start**: reports the facts briefly, proposes three lots from the
564
746
  plan, then waits for your priority — nothing starts before your answer.
565
- - **session-close**: plan hygiene, clean repository, memory limited to what the
747
+ - **session-close**: plan hygiene, clean repository, a cleanup proposal for stale
748
+ working files (`session.clean` patterns, older than `session.cleanDays`; never
749
+ anything git tracks, a git repository or a folder holding one, anything under
750
+ `.git`, a bare repository or separate git directory or anything inside one,
751
+ anything behind a symbolic link a `*` matched (only the link itself), a hidden `.xxx` name a `*` would not match, nor anything it could not
752
+ read entirely — it asks before deleting), memory limited to what the
566
753
  repository does not say, new skills or agents proposed but never created, three
567
754
  lines for next time.
568
755
  - A project with its own tooling keeps it: its plan is read where it is (`plan:`),
@@ -573,9 +760,10 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
573
760
  blind retry; after a green delivery that changes what a page shows or what it
574
761
  is served, the `qa-reviewer` agent walks the delivered app.
575
762
  - **lead**: from a folder holding several projects, one subagent per project
576
- gathers the facts, you choose the priorities, each lot is delegated to a
577
- subagent with a standard brief (test first, commits citing the lot, no push),
578
- reviewed by the `code-reviewer` agent, re-verified by the lead, then delivered
763
+ gathers the facts, you choose the priorities, the lots are delegated with
764
+ `cadence orchestrate` (fresh short sessions with a standard brief — test first,
765
+ commits citing the lot, no push — reviewed by the `code-reviewer` agent, see
766
+ [orchestrate](#orchestrate)), re-verified by the lead, then delivered
579
767
  one project at a time; a delivery that changes what a page shows or what it is
580
768
  served is then checked in the running app by the `qa-reviewer` agent, whose
581
769
  blocking findings come back to you. Two
@@ -602,7 +790,7 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
602
790
  broken, or a universal check failing with a visible effect, with or without an
603
791
  expectations file), suspects (it looks like missing or wrong data and no
604
792
  expectation settles it) or noise (a console error or a failed request with no
605
- visible effect, ranked minor), ranked, each with
793
+ 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
606
794
  the route, what was expected, what was measured and the evidence; pages checked
607
795
  N/N, follow-ups as `raf add` lines, what it could not verify, a one-line
608
796
  verdict. Read-only: GET only, no login, nothing submitted; it stops at a PIN.
@@ -613,14 +801,18 @@ A version exists in three places and is published in two; a release does all of
613
801
 
614
802
  1. Bump `version` in `package.json` (then `npm install` to refresh `package-lock.json`),
615
803
  `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`, in the commit that closes the lot.
616
- 2. `npm publish --access public` — `prepublishOnly` runs the type-check and the tests first, `prepare`
617
- builds `dist/`; a red suite stops the publication.
618
- 3. `git tag v<version> && git push origin main v<version>`.
804
+ 2. `git tag v<version> && git push origin main v<version>` — the tag starts `.github/workflows/publish.yml`,
805
+ which publishes to npm through Trusted Publishing (OIDC, no token stored anywhere): it checks the tag
806
+ matches `package.json`, `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`, then `npm publish --provenance`, where `prepublishOnly` runs the type-check and the
807
+ tests and `prepare` builds `dist/`; a red suite stops the publication. The trusted publisher is declared
808
+ once on npmjs.com (package settings → Trusted Publisher → GitHub Actions, `Sylad/cadence`, `publish.yml`).
809
+ 3. Watch the run: `gh run watch` (or `gh run list --workflow publish.yml`).
619
810
  4. Check the effect: `npm view @sylad/cadence version` answers the new version.
620
811
 
621
- The Claude Code plugin is read from the repository, so step 3 is what updates it; npm is what
622
- `npx @sylad/cadence` and a global install read. Skipping step 2 leaves npm behind without any error —
623
- 0.3.0 and 0.4.0 were never published.
812
+ The Claude Code plugin is read from the repository, so pushing `main` is what updates it; npm is what
813
+ `npx @sylad/cadence` and a global install read, and only the tag publishes there. A missing tag, or a red
814
+ publish run, leaves npm behind without any other error — 0.3.0 and 0.4.0 were never published — hence
815
+ step 4.
624
816
 
625
817
  ## License
626
818
 
@@ -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', 'verify', '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'));
@@ -34,6 +34,8 @@ if (tool === 'raf') {
34
34
  cadence verify [--retry secondes] [--sha rév]
35
35
  rejoue les vérifications d'effet (deliver.verify) hors livraison, une passe, en parallèle ;
36
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
37
39
  cadence skills install [--dir .claude] [--force]
38
40
  installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
39
41
  process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
package/dist/audit.js CHANGED
@@ -4,6 +4,7 @@ import { parse } from 'yaml';
4
4
  import { changedFiles, fileAt, readCommits } from './git.js';
5
5
  import { linkCommits } from './link.js';
6
6
  import { loadEntries, newsIssues } from './news.js';
7
+ import { isRecurring } from './recurring.js';
7
8
  import { isOpen } from './plan.js';
8
9
  /** Chemin de la configuration lue, relatif à la racine : celui de --config, sinon cadence.yaml. */
9
10
  function configRel(plan, root) {
@@ -92,7 +93,7 @@ function workCommits(plan, root, commits) {
92
93
  return commits.filter((c) => (!adopted || c.day >= adopted) && !isPlanOnly(c.sha, plan, root));
93
94
  }
94
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. */
95
- function lotCommits(plan, root, lotId) {
96
+ export function lotCommits(plan, root, lotId) {
96
97
  return linkCommits(plan.lots(), planCommits(plan, root), plan.refs).byLot.get(lotId) ?? [];
97
98
  }
98
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`. */
@@ -155,7 +156,9 @@ export function reviewIssues(plan, root, byLot) {
155
156
  }));
156
157
  }
157
158
  /** Ce qui vient ensuite : lots en cours, puis lots prêts (dépendances closes), gains rapides d'abord. */
158
- 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));
159
162
  const byId = new Map(lots.map((l) => [l.id, l]));
160
163
  const doing = lots.filter((l) => l.status === 'doing');
161
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})` });