@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +207 -15
- package/agents/qa-reviewer.md +15 -6
- package/bin/cadence.js +3 -1
- package/dist/audit.js +5 -2
- package/dist/check.js +5 -2
- package/dist/clean.js +225 -0
- package/dist/cli.js +67 -14
- package/dist/config.js +82 -6
- package/dist/deliver.js +5 -4
- package/dist/git.js +29 -0
- package/dist/orchestrate/briefs.js +52 -0
- package/dist/orchestrate/command.js +498 -0
- package/dist/orchestrate/cycle.js +674 -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 +57 -2
- package/dist/proc.js +109 -10
- package/dist/recurring.js +19 -0
- package/dist/schedule.js +4 -1
- package/dist/session.js +17 -1
- package/dist/verify.js +30 -3
- package/package.json +6 -1
- package/skills/lead/SKILL.md +33 -13
- package/skills/session-close/SKILL.md +25 -4
- package/templates/orchestrate/fix-minors.md +22 -0
- package/templates/orchestrate/fix.md +21 -0
- package/templates/orchestrate/implement.md +25 -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.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.
|
|
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:
|
|
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,
|
|
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,
|
|
577
|
-
|
|
578
|
-
reviewed by the `code-reviewer` agent,
|
|
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. `
|
|
617
|
-
|
|
618
|
-
|
|
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
|
|
622
|
-
`npx @sylad/cadence` and a global install read
|
|
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
|
|
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', '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(
|
|
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
|
-
|
|
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})` });
|