@sylad/cadence 0.12.0 → 0.13.1
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 +22 -5
- package/dist/audit.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/orchestrate/command.js +63 -20
- package/dist/orchestrate/state.js +4 -2
- package/dist/plan.js +7 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +17 -2
- package/templates/orchestrate/fix-minors.md +1 -1
- package/templates/orchestrate/fix.md +1 -1
- package/templates/orchestrate/implement.md +1 -1
- package/templates/orchestrate/review-small.md +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "cadence",
|
|
10
10
|
"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).",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.13.1",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Sylvain Ladoire"
|
|
@@ -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.13.1",
|
|
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
|
@@ -24,7 +24,10 @@ Code prompt that shows the session's context, quota, cost and the orchestrate wa
|
|
|
24
24
|
|
|
25
25
|
## What's new
|
|
26
26
|
|
|
27
|
-
**0.
|
|
27
|
+
**0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
28
|
+
stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
|
|
29
|
+
(sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
|
|
30
|
+
terminal cells and kept to ASCII; `publish.yml` on the v5 actions. **0.12.0**: the `cadence-hud` plugin (a status band above the Claude Code prompt),
|
|
28
31
|
`raf ignore` to acknowledge a commit without a lot, waves that size the review to the lot,
|
|
29
32
|
hand back a lot whose deliverable is already there and start the app themselves for the UX
|
|
30
33
|
review (`orchestrate.ux`), and briefs that require the README to follow the change. **0.11.0**: waves on different repositories run side by side under a shared cap of
|
|
@@ -481,7 +484,7 @@ news:
|
|
|
481
484
|
publicTitleMax: 80 # characters; without the key, `raf check` warns at 80 in a project that has a news directory
|
|
482
485
|
```
|
|
483
486
|
|
|
484
|
-
`raf check` also warns (`⚠`, not an error: exit code 0 if nothing else is wrong) about every visible lot, dropped ones excepted, that has no public title: the site would show its technical title (a done lot cited by a News entry is spared: the site reuses that entry's title). Fix it with `raf public <id> "…"`.
|
|
487
|
+
`raf check` also warns (`⚠`, not an error: exit code 0 if nothing else is wrong) about every visible lot, dropped ones excepted, that has no public title: the site would show its technical title (a done lot cited by a News entry is spared: the site reuses that entry's title). Fix it with `raf public <id> "…"`. On a plan kept by another tool (read-only), the warning only applies if `plan.fields` maps a `public` field, or if some lot already carries a literal `public:` key (a field left out is read under its own name): without either there is no public title to set, so it would come back at every session.
|
|
485
488
|
|
|
486
489
|
With the key, `raf public`, `raf add --public`, `raf done` (on a lot whose public title is too long) and
|
|
487
490
|
`cadence news new` (its title) refuse above it; without it, only `raf check` warns.
|
|
@@ -664,18 +667,32 @@ exception is a **formatting retry** (below).
|
|
|
664
667
|
```sh
|
|
665
668
|
cadence orchestrate finance-tracker:L41 ol-companion:L22 cadence:L18@haiku
|
|
666
669
|
cadence orchestrate L18 # from inside a project
|
|
670
|
+
cadence orchestrate maritime-atlas:Q4/accueil-4-ux12@haiku # a lot id may contain "/" (sub-tasks of read-only plans)
|
|
667
671
|
cadence orchestrate … --budget 1.5M # 1500000, 1.5M, 800k; default 2M
|
|
668
672
|
cadence orchestrate … --max-sessions 3 # sessions running at the same moment, all waves together; default 2
|
|
669
673
|
cadence orchestrate … --dry-run # preconditions + the plan of the wave; nothing is started
|
|
670
674
|
cadence orchestrate --status [<wave>] # the live waves and the repositories they hold, then the table (default: the last wave of this folder)
|
|
675
|
+
cadence orchestrate --status [<wave>] --watch [--interval 10] # the same, redrawn every 10 s (--interval in seconds); stops by itself when no wave is running (with an explicit `<wave>`: when that wave is no longer running)
|
|
671
676
|
cadence orchestrate --resume [<wave>] [--budget 1M] [--answer ol-companion:L22 "reply"]
|
|
672
677
|
```
|
|
673
678
|
|
|
679
|
+
**Lot ids containing `/` (L120)**: the first `:` separates the project from the lot, a final `@` the model, and the lot
|
|
680
|
+
keeps its `/` (`maritime-atlas:Q4/accueil-4-ux12@haiku`); this holds for `--dry-run`, `--status`, `--resume` and
|
|
681
|
+
`--answer` too. In `.cadence/runs/<wave>/` the `/` of the lot becomes `__` (`maritime-atlas--Q4__accueil-4-ux12.json`,
|
|
682
|
+
same for the lot folder and the dry-run briefs); ids without `/` are unchanged.
|
|
683
|
+
|
|
674
684
|
You choose the lots; the order is the order given (one queue per repository, as many repositories at the
|
|
675
685
|
same time as the session cap allows — 2 by default). `@haiku|@sonnet|@opus` sets the model of the implementation and corrections of that
|
|
676
686
|
lot (default Sonnet; reviews are Opus, except the light review below; Haiku only when you write it, for a mechanical lot). Run it
|
|
677
687
|
in the background and read `--status`: it prints one line per transition and the final table.
|
|
678
688
|
|
|
689
|
+
**Following a wave (L49)**: `--status --watch` clears the screen and redraws the live waves and the table every
|
|
690
|
+
`--interval` seconds (default 10), then stops by itself, leaving the last render, as soon as no wave is running
|
|
691
|
+
(exit code 0, like `--status`; a wave already finished gives a single render). The `cadence-lead` skill names the background
|
|
692
|
+
task « vague `<id>` : `<project>:<lot>`, … », starts the wave with `--wave <id>` and arms a follow-up on
|
|
693
|
+
`.cadence/runs/<id>/journal.log` that pushes each transition (lot → state, question, ready, handed back) into the conversation.
|
|
694
|
+
The Claude Code status band is the `cadence-hud` plugin.
|
|
695
|
+
|
|
679
696
|
**Cycle of a lot**: preconditions (clean tracked files, lot `todo` or `doing`, dependencies met) →
|
|
680
697
|
`raf start` (committed alone) → implementation → **UX review** if the lot is `visible` and the app is
|
|
681
698
|
declared → **code review**, which always comes last (a UX fix changes code) → compliant (no blocking, no
|
|
@@ -797,7 +814,7 @@ lists the commits already present; finished steps are never replayed.
|
|
|
797
814
|
Briefs are the templates of `templates/orchestrate/` (`implement.md` is the `lead` skill's standard
|
|
798
815
|
brief; `--dry-run` writes the rendered ones). The `implement` brief of a `visible` lot also carries the
|
|
799
816
|
News instruction (`cadence news new <lot>`, factual user-side text, a screenshot in `docs/nouveautes/captures/` or
|
|
800
|
-
`nocapture:`, `cadence news check` green); the brief gives the absolute path of the wave's Playwright output directory and the `implement` session gets `--add-dir` on it, to copy the capture into the repository; a lot that is not `visible` gets nothing. A project can declare, in `cadence.yaml`:
|
|
817
|
+
`nocapture:`, `cadence news check` green); the brief gives the absolute path of the wave's Playwright output directory and the `implement` session gets `--add-dir` on it, to copy the capture into the repository; the `fix` and `fix-minors` briefs of a `visible` lot give the same folder (and their sessions the same `--add-dir`), to fetch a screenshot a finding asks for; a lot that is not `visible` gets nothing. A `--dry-run` brief of a `visible` lot has no wave folder (the wave does not exist yet), nor does a brief delegated by hand: the News instruction then says to save the screenshot under the OS temp directory, never inside the repository, then copy it into `docs/nouveautes/captures/`. A project can declare, in `cadence.yaml`:
|
|
801
818
|
|
|
802
819
|
```yaml
|
|
803
820
|
orchestrate:
|
|
@@ -962,9 +979,9 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
962
979
|
A band above the Claude Code prompt (terminal and desktop app), refreshed every 5 s:
|
|
963
980
|
|
|
964
981
|
```
|
|
965
|
-
ctx ▰▰▰▰▱▱▱▱▱▱ 42 % 84k/200k │ 5h 23 % ↻ 2 h 10 │ 7j 61 % ↻ 6 j 15 h │ $1.23 │ fable 410k
|
|
982
|
+
ctx ▰▰▰▰▱▱▱▱▱▱ 42 % 84k/200k │ 5h 23 % ↻ 2 h 10 │ 7j 61 % ↻ 6 j 15 h │ $1.23 │ fable 410k sonnet 85k │ ⚙ 2 agents
|
|
966
983
|
⟳ cadence · 2026-10-07-2131 en cours │ budget ▱▱▱▱▱▱▱▱▱▱ 1 % 14k/2M │ 1 session/2
|
|
967
|
-
L112 implémente implement@sonnet 34 s global-setup coupe le cache de compilation de Node
|
|
984
|
+
L112 implémente implement@sonnet 34 s global-setup coupe le cache de compilation de Node...
|
|
968
985
|
```
|
|
969
986
|
|
|
970
987
|
Context of the session (green / orange / red at 50 and 75 %), the 5-hour and 7-day quota
|
package/dist/audit.js
CHANGED
|
@@ -109,7 +109,7 @@ export function audit(plan, root, newsDir, today, opts = {}) {
|
|
|
109
109
|
const m = l.public ? publicTitleTooLong(l.public, max) : reused ? publicTitleTooLong(reused, max) : null;
|
|
110
110
|
return m ? [{ message: `${l.id} : ${reused && !l.public ? m.replace('titre public', 'titre public repris de la Nouveauté') : m}` }] : [];
|
|
111
111
|
});
|
|
112
|
-
const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, entries, newsDir), ...titles, ...gates, ...missingPublicTitles(lots, entries),
|
|
112
|
+
const issues = [...check(lots, { ...linked, byLot: all.byLot }, today, opts.idle ?? 7), ...newsIssues(lots, entries, newsDir), ...titles, ...gates, ...(plan.hasPublicField ? missingPublicTitles(lots, entries) : []),
|
|
113
113
|
...plan.ignore.invalid.map((src) => ({ message: `ignore : motif invalide « ${src} »` }))];
|
|
114
114
|
// Un plan en lecture seule se corrige avec l'outil du projet : ne pas conseiller une commande raf qui refuserait.
|
|
115
115
|
return plan.readonly ? issues.map((i) => ({ ...i, message: i.message.replace(/ — raf (start|public) .*$/, '') })) : issues;
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
40
40
|
raf ignore <sha> | "<sujet exact>" [--reason texte] acquitte un commit sans lot (ou citant un id inconnu) sans réécrire l'historique ; raf check --ignored les liste
|
|
41
41
|
raf gantt [-o docs/plan/gantt.html]
|
|
42
42
|
raf hook install
|
|
43
|
-
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run] [--wave nom] une session claude neuve par étape ; --status [vague] ; --resume [vague] [--budget 1M] [--answer projet:lot "réponse"]
|
|
43
|
+
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--dry-run] [--wave nom] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague] [--budget 1M] [--answer projet:lot "réponse"]
|
|
44
44
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
45
45
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
46
46
|
|
|
@@ -18,7 +18,7 @@ import { buildArgs, killSessions, mcpServersFor, readAgents, realClaude } from '
|
|
|
18
18
|
import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
19
19
|
import { runPool } from './pool.js';
|
|
20
20
|
import { schemaFor } from './schemas.js';
|
|
21
|
-
import { excludeState, lotKey, newLot, RunStore } from './state.js';
|
|
21
|
+
import { excludeState, lotKey, lotSlug, newLot, RunStore } from './state.js';
|
|
22
22
|
import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
23
23
|
import { quotaText, renderTable } from './table.js';
|
|
24
24
|
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, snapshotExists, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
@@ -45,10 +45,12 @@ export function parseMaxSessions(text, what = '--max-sessions') {
|
|
|
45
45
|
throw new RafError(`${what} invalide : ${text} (entier ≥ 1)`);
|
|
46
46
|
return Number(text);
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
// une vague (2026-10-04-1412) n'a ni « : » ni « / » : leur présence désigne un lot, quel que soit son motif (R-M7/ux-1)
|
|
49
|
+
const LOT_LIKE = /^(?:[\w.-]+:[\w.-][\w./-]*|[\w.-]+\/[\w./-]*|[A-Za-z]+\d+)(?:@\w+)?$/;
|
|
49
50
|
const MODELS = ['sonnet', 'opus', 'haiku'];
|
|
50
51
|
function lotArg(text) {
|
|
51
|
-
|
|
52
|
+
// le premier « : » sépare le projet du lot, un « @ » final le modèle ; le lot garde ses « / » (L120)
|
|
53
|
+
const m = /^(?:([\w.-]+):)?([\w.-][\w./-]*?)(?:@(\w+))?$/.exec(text);
|
|
52
54
|
if (!m)
|
|
53
55
|
throw new RafError(`lot invalide : ${text} (projet:lot, lot, projet:lot@haiku)`);
|
|
54
56
|
if (m[3] && !MODELS.includes(m[3]))
|
|
@@ -56,7 +58,7 @@ function lotArg(text) {
|
|
|
56
58
|
return { project: m[1], lot: m[2], model: m[3] };
|
|
57
59
|
}
|
|
58
60
|
export function parseOrchestrateArgs(argv) {
|
|
59
|
-
const a = { lots: [], dryRun: false, answers: [] };
|
|
61
|
+
const a = { lots: [], dryRun: false, watch: false, answers: [] };
|
|
60
62
|
const optional = (i) => (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-') && !LOT_LIKE.test(argv[i + 1]) ? argv[i + 1] : undefined);
|
|
61
63
|
for (let i = 0; i < argv.length; i++) {
|
|
62
64
|
const t = argv[i];
|
|
@@ -73,6 +75,10 @@ export function parseOrchestrateArgs(argv) {
|
|
|
73
75
|
i++;
|
|
74
76
|
a[t === '--status' ? 'status' : 'resume'] = v ?? true;
|
|
75
77
|
}
|
|
78
|
+
else if (t === '--watch')
|
|
79
|
+
a.watch = true;
|
|
80
|
+
else if (t === '--interval')
|
|
81
|
+
a.interval = parseMaxSessions(value(t), '--interval');
|
|
76
82
|
else if (t === '--budget')
|
|
77
83
|
a.budget = parseBudget(value(t));
|
|
78
84
|
else if (t === '--max-sessions')
|
|
@@ -256,11 +262,11 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
256
262
|
io.out(` étapes : ${steps.map((s) => `${s.kind} (${s.model})`).join(' → ')}${l.small ? ' — petit lot' : ''}${l.light ? ` — revue légère (${light}, pas de passe des mineurs ; un bloquant ou un majeur → correction puis revue ${full})` : ''}${l.visible && !env.config.ux ? ' — UX à faire par le lead (orchestrate.ux absent)' : ''}`);
|
|
257
263
|
io.out(` corrections : ${MAX_PASSES} passe(s) au plus, en session neuve`);
|
|
258
264
|
io.out(l.light ? ' revue conforme avec mineurs : pas de passe des mineurs, ils sont rendus en notes' : ' revue conforme avec mineurs : une passe de correction des mineurs (session neuve), puis une revue courte ; les mineurs refusés sont rendus en « choix »');
|
|
259
|
-
const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id,
|
|
265
|
+
const pwDir = join(resolve(RunStore.runsDir(io.cwd)), id, lotSlug(l.project, l.lot), 'playwright');
|
|
260
266
|
// brief écrit par --dry-run = base d'une délégation à la main : aucun dossier de vague (il n'existe pas)
|
|
261
267
|
const vars = { chemin: l.repo, lot: l.lot, titre: lot.title, objectif: objective(lot), commits: '', reponse: '', constats: '', ux: '', choix: '', checks: '', news: l.visible ? newsText(l.lot) : '', captures: '' };
|
|
262
268
|
for (const s of steps) {
|
|
263
|
-
const file = join(tmp, `${l.project
|
|
269
|
+
const file = join(tmp, `${lotSlug(l.project, l.lot)}--${s.kind}.md`);
|
|
264
270
|
const brief = renderBrief(s.kind, vars, deps.templatesDir);
|
|
265
271
|
writeFileSync(file, brief);
|
|
266
272
|
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small).playwright;
|
|
@@ -268,7 +274,7 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
268
274
|
io.out(` ${s.kind} : claude ${args.join(' ')}`);
|
|
269
275
|
io.out(` brief : ${file}`);
|
|
270
276
|
const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small));
|
|
271
|
-
io.out(` mcp : ${servers.length ? `${servers.join(', ')} (captures dans le dossier de la vague : <vague>/${l.project
|
|
277
|
+
io.out(` mcp : ${servers.length ? `${servers.join(', ')} (captures dans le dossier de la vague : <vague>/${lotSlug(l.project, l.lot)}/playwright)` : 'aucun'}`);
|
|
272
278
|
}
|
|
273
279
|
}
|
|
274
280
|
}
|
|
@@ -294,6 +300,21 @@ function liveLines() {
|
|
|
294
300
|
lines.push(` ${w.wave} (pid ${w.pid}, depuis ${w.started}) lancée depuis ${w.cwd} · plafond ${w.cap ?? '?'} · dépôts : ${w.repos.join(', ')}`);
|
|
295
301
|
return lines;
|
|
296
302
|
}
|
|
303
|
+
/** Efface l'écran et replace le curseur en haut : l'entrée d'un rendu de `--status --watch`. */
|
|
304
|
+
const CLEAR_SCREEN = '\x1b[H\x1b[2J';
|
|
305
|
+
const DEFAULT_WATCH_INTERVAL = 10;
|
|
306
|
+
/** Ce que `--status` montre à un instant : les vagues vivantes, puis le tableau (absent si seules les vagues vivantes comptent). */
|
|
307
|
+
function statusView(args, launch) {
|
|
308
|
+
const store = typeof args.status === 'string' ? RunStore.find(launch, args.status) : RunStore.last(launch);
|
|
309
|
+
const live = liveLines();
|
|
310
|
+
if (!store && live.length && args.status === true)
|
|
311
|
+
return { live, running: true };
|
|
312
|
+
if (!store)
|
|
313
|
+
throw new RafError(typeof args.status === 'string' ? `vague inconnue : ${args.status}` : 'aucune vague dans ce dossier');
|
|
314
|
+
// `running` : la vague suivie tourne — pour un <id> explicite, elle seule compte, pas les autres vagues de la machine
|
|
315
|
+
const running = typeof args.status === 'string' ? liveWaves(cadenceHome()).some((w) => w.wave === store.readWave().id) : live.length > 0;
|
|
316
|
+
return { live, table: renderTable(store.readWave(), store.lots()), running };
|
|
317
|
+
}
|
|
297
318
|
/** Refus d'une `--wave` déjà existante, identique en simulation et au vrai lancement. */
|
|
298
319
|
function waveExists(wave, io) {
|
|
299
320
|
io.err(`orchestrate : --wave ${wave} : cette vague existe déjà`);
|
|
@@ -304,26 +325,48 @@ export async function orchestrate(argv, io, deps) {
|
|
|
304
325
|
const args = parseOrchestrateArgs(argv);
|
|
305
326
|
const launch = io.cwd;
|
|
306
327
|
const today = io.env.RAF_TODAY && isDay(io.env.RAF_TODAY) ? io.env.RAF_TODAY : toDay(io.now());
|
|
328
|
+
if (args.watch && args.status === undefined)
|
|
329
|
+
throw new RafError('--watch s\'utilise avec --status');
|
|
330
|
+
if (args.interval !== undefined && !args.watch)
|
|
331
|
+
throw new RafError('--interval s\'utilise avec --status --watch');
|
|
307
332
|
if (args.status !== undefined) {
|
|
308
|
-
const store = typeof args.status === 'string' ? RunStore.find(launch, args.status) : RunStore.last(launch);
|
|
309
333
|
maxSessions(args, io); // une valeur invalide est refusée ici aussi
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
if (!args.watch) {
|
|
335
|
+
const { live, table } = statusView(args, launch);
|
|
336
|
+
for (const line of live)
|
|
337
|
+
io.out(line);
|
|
338
|
+
if (!table)
|
|
339
|
+
return 0; // sans identifiant : les vagues vivantes suffisent
|
|
340
|
+
if (live.length)
|
|
341
|
+
io.out('');
|
|
342
|
+
for (const line of table)
|
|
343
|
+
io.out(line);
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
// --watch : le même tableau, rafraîchi ; il s'arrête seul quand plus aucune vague ne tourne (le dernier rendu reste affiché)
|
|
347
|
+
const sleep = deps.watchSleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
|
|
348
|
+
for (;;) {
|
|
349
|
+
const { live, table, running } = statusView(args, launch);
|
|
350
|
+
io.out(CLEAR_SCREEN);
|
|
351
|
+
for (const line of live)
|
|
352
|
+
io.out(line);
|
|
353
|
+
if (table) {
|
|
354
|
+
if (live.length)
|
|
355
|
+
io.out('');
|
|
356
|
+
for (const line of table)
|
|
357
|
+
io.out(line);
|
|
358
|
+
}
|
|
359
|
+
if (!running)
|
|
360
|
+
return 0;
|
|
318
361
|
io.out('');
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
362
|
+
io.out(`rafraîchi toutes les ${args.interval ?? DEFAULT_WATCH_INTERVAL} s — Ctrl-C pour quitter`);
|
|
363
|
+
await sleep((args.interval ?? DEFAULT_WATCH_INTERVAL) * 1000);
|
|
364
|
+
}
|
|
322
365
|
}
|
|
323
366
|
if (args.resume !== undefined)
|
|
324
367
|
return resume(args, argv, io, deps, launch, today);
|
|
325
368
|
if (args.lots.length === 0)
|
|
326
|
-
throw new RafError('usage : cadence orchestrate <projet>:<lot>… [--budget 2M] [--max-sessions 2] [--dry-run] | --status [vague] | --resume [vague] [--answer projet:lot "réponse"]');
|
|
369
|
+
throw new RafError('usage : cadence orchestrate <projet>:<lot>… [--budget 2M] [--max-sessions 2] [--dry-run] | --status [vague] [--watch [--interval 10]] | --resume [vague] [--answer projet:lot "réponse"]');
|
|
327
370
|
const refusals = [];
|
|
328
371
|
const targets = resolveTargets(args, io, refusals);
|
|
329
372
|
const pre = await preflight(args, targets, io, deps, launch);
|
|
@@ -5,7 +5,9 @@ export function newLot(o) {
|
|
|
5
5
|
return { ...o, status: 'queued', pass: 0, next: null, steps: [], constats: [], verdict: null, uxVerdict: null, questions: [], choix: [], answers: [], proposals: [], warnings: [], outcome: null };
|
|
6
6
|
}
|
|
7
7
|
export const lotKey = (project, lot) => `${project}:${lot}`;
|
|
8
|
-
|
|
8
|
+
/** Nom de dossier d'un lot dans la vague : le « / » d'un id de sous-tâche (`Q4/accueil-4-ux12`) devient « __ » ; un id sans « / » est inchangé. */
|
|
9
|
+
export const lotSlug = (project, lot) => `${project}--${lot.split('/').join('__')}`;
|
|
10
|
+
const lotFile = (project, lot) => `${lotSlug(project, lot)}.json`;
|
|
9
11
|
function atomicWrite(file, data) {
|
|
10
12
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
11
13
|
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`);
|
|
@@ -64,7 +66,7 @@ export class RunStore {
|
|
|
64
66
|
return existsSync(join(RunStore.runsDir(launchDir), id, 'wave.json')) ? new RunStore(launchDir, id) : null;
|
|
65
67
|
}
|
|
66
68
|
lotDir(project, lot) {
|
|
67
|
-
const d = join(this.dir,
|
|
69
|
+
const d = join(this.dir, lotSlug(project, lot));
|
|
68
70
|
mkdirSync(d, { recursive: true });
|
|
69
71
|
return d;
|
|
70
72
|
}
|
package/dist/plan.js
CHANGED
|
@@ -51,6 +51,13 @@ export class Plan {
|
|
|
51
51
|
}
|
|
52
52
|
/** Un plan lu dans un autre format ne change pas : sa lecture et le motif de ses références sont gardés. */
|
|
53
53
|
foreign;
|
|
54
|
+
/**
|
|
55
|
+
* Un plan en lecture seule dont `fields` ne déclare pas de champ `public` n'a pas de titre public à poser ni à lire,
|
|
56
|
+
* sauf si l'outil écrit déjà la clé `public` sous son propre nom sur un lot.
|
|
57
|
+
*/
|
|
58
|
+
get hasPublicField() {
|
|
59
|
+
return !this.settings.format || !!this.settings.format.fields.public || this.read().lots.some((l) => l.public !== undefined);
|
|
60
|
+
}
|
|
54
61
|
get readonly() {
|
|
55
62
|
return !!this.settings.format;
|
|
56
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sylvain Ladoire",
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -55,12 +55,27 @@ proposed sub-tasks back to the human.
|
|
|
55
55
|
### 2b. A wave through `cadence orchestrate`
|
|
56
56
|
|
|
57
57
|
Once the human has chosen the lots, the lead runs, **in the background** (it is notified at the end; no
|
|
58
|
-
silent wait, no polling loop — `cadence orchestrate --status` shows where the wave is):
|
|
58
|
+
silent wait, no polling loop — `cadence orchestrate --status` shows where the wave is, and the follow-up below pushes each transition):
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
|
-
cadence orchestrate <project>:<lot> <project>:<lot>@haiku … [--budget 2M]
|
|
61
|
+
cadence orchestrate <project>:<lot> <project>:<lot>@haiku … --wave <id> [--budget 2M]
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
**Make the wave visible to the human.** Name the background task after what it runs — « vague `<id>` :
|
|
65
|
+
`<project>:<lot>`, … » (its `description`), so the task list says which projects and lots, not just « a task
|
|
66
|
+
is running » — and pass `--wave <id>` (e.g. `2026-10-08-1030`) so its state folder is known in advance. Right after
|
|
67
|
+
starting it, arm **one follow-up** (the Monitor tool) on the wave's journal, which holds one line per transition (lot → new
|
|
68
|
+
state, question, ready, handed back). The final table is not in the journal: it arrives with the background task's end notification, at which point stop the follow-up (it never ends by itself; a later `--resume` writes to the same journal):
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
tail -n +1 -F .cadence/runs/<id>/journal.log # from the folder the wave was launched in
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Each line it prints reaches this conversation as it happens: relay it to the human in one short sentence, and
|
|
75
|
+
bring a question back at once (`--resume --answer`). That follow-up replaces any waiting loop: still no polling,
|
|
76
|
+
no `sleep`. The human can also watch from a terminal with `cadence orchestrate --status --watch` (table refreshed
|
|
77
|
+
every 10 s, `--interval <s>` to change it; it stops by itself when no wave is running).
|
|
78
|
+
|
|
64
79
|
`cadence orchestrate --dry-run …` first when a precondition is in doubt. The program, not the lead, runs
|
|
65
80
|
for each lot a fresh short session per step — implementation (Sonnet), UX review if the lot is `visible`
|
|
66
81
|
and the project declares how to see its app (`orchestrate.ux` in `cadence.yaml`), code review last
|
|
@@ -22,4 +22,4 @@ You are one short session of an orchestrated wave: do not launch subagents (the
|
|
|
22
22
|
Give your final report as the structured output (commits, tests, build, what you could not verify, choices).
|
|
23
23
|
{{reponse}}
|
|
24
24
|
{{captures}}
|
|
25
|
-
Browser (Playwright): if a Playwright MCP server is available in this session
|
|
25
|
+
Browser (Playwright): if a Playwright MCP server is available in this session (only in an orchestrated wave: a brief delegated by hand has none), the orchestrator launched it with its own output directory, in the orchestrator's run directory outside the repository: give screenshots, snapshots and traces a relative file name only (e.g. `page-home.png`), they land there. Never an absolute path, never the `--output-dir` option (the server's launch argument, which a session cannot set). If no such server is available, do not install or start one; if you use the Playwright CLI, write its outputs under the OS temp directory (or the temporary folder the project's CLAUDE.md names), never inside the repository, which must stay clean.
|
|
@@ -22,4 +22,4 @@ You are one short session of an orchestrated wave: do not launch subagents (the
|
|
|
22
22
|
Give your final report as the structured output (commits, tests, build, what you could not verify, choices, questions).
|
|
23
23
|
{{reponse}}
|
|
24
24
|
{{captures}}
|
|
25
|
-
Browser (Playwright): if a Playwright MCP server is available in this session
|
|
25
|
+
Browser (Playwright): if a Playwright MCP server is available in this session (only in an orchestrated wave: a brief delegated by hand has none), the orchestrator launched it with its own output directory, in the orchestrator's run directory outside the repository: give screenshots, snapshots and traces a relative file name only (e.g. `page-home.png`), they land there. Never an absolute path, never the `--output-dir` option (the server's launch argument, which a session cannot set). If no such server is available, do not install or start one; if you use the Playwright CLI, write its outputs under the OS temp directory (or the temporary folder the project's CLAUDE.md names), never inside the repository, which must stay clean.
|
|
@@ -26,4 +26,4 @@ Give your final report as the structured output, not as free text. If the plan i
|
|
|
26
26
|
{{news}}
|
|
27
27
|
{{commits}}
|
|
28
28
|
{{reponse}}
|
|
29
|
-
Browser (Playwright): if a Playwright MCP server is available in this session
|
|
29
|
+
Browser (Playwright): if a Playwright MCP server is available in this session (only in an orchestrated wave: a brief delegated by hand has none), the orchestrator launched it with its own output directory, in the orchestrator's run directory outside the repository: give screenshots, snapshots and traces a relative file name only (e.g. `page-home.png`), they land there. Never an absolute path, never the `--output-dir` option (the server's launch argument, which a session cannot set). If no such server is available, do not install or start one; if you use the Playwright CLI, write its outputs under the OS temp directory (or the temporary folder the project's CLAUDE.md names), never inside the repository, which must stay clean.
|
|
@@ -11,4 +11,4 @@ Give your report as the structured output: counts of blocking / major / minor fi
|
|
|
11
11
|
findings included, each tied to a named rule — Nielsen heuristic or WCAG 2.2 AA criterion — or a
|
|
12
12
|
measurement), each finding (severity, file, line, text), proposed sub-tasks, what you could not
|
|
13
13
|
verify, and the one-line verdict for `raf review {{lot}}`.
|
|
14
|
-
Browser (Playwright): if a Playwright MCP server is available in this session
|
|
14
|
+
Browser (Playwright): if a Playwright MCP server is available in this session (only in an orchestrated wave: a brief delegated by hand has none), the orchestrator launched it with its own output directory, in the orchestrator's run directory outside the repository: give screenshots, snapshots and traces a relative file name only (e.g. `page-home.png`), they land there. Never an absolute path, never the `--output-dir` option (the server's launch argument, which a session cannot set). If no such server is available, do not install or start one; if you use the Playwright CLI, write its outputs under the OS temp directory (or the temporary folder the project's CLAUDE.md names), never inside the repository, which must stay clean.
|