@sylad/cadence 0.13.1 → 0.14.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 +29 -3
- package/bin/cadence.js +3 -1
- package/dist/cli.js +23 -0
- package/dist/lead.js +91 -0
- package/dist/session.js +2 -2
- package/package.json +1 -1
- package/skills/lead/SKILL.md +8 -5
|
@@ -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.14.0",
|
|
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.14.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
|
@@ -24,7 +24,7 @@ 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.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
|
|
27
|
+
**0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **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
28
|
stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
|
|
29
29
|
(sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
|
|
30
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),
|
|
@@ -455,6 +455,32 @@ The commands get `CADENCE_SINCE` (the `--since` in effect) and `CADENCE_TODAY`.
|
|
|
455
455
|
add facts and decide nothing: a failing command is reported and changes neither
|
|
456
456
|
the exit code nor the verdict.
|
|
457
457
|
|
|
458
|
+
## lead tour
|
|
459
|
+
|
|
460
|
+
`cadence lead tour [folder] [--idle 3] [--json]` is the lead's morning table, with no model in
|
|
461
|
+
the loop. From a parent folder (default: the current one) it takes every direct sub-folder that
|
|
462
|
+
holds `docs/plan/raf.yaml`, or a `cadence.yaml` with a `plan:` key (a plan kept by another tool
|
|
463
|
+
included), and for each one reads the same facts as `cadence session start` — in-process, no
|
|
464
|
+
sub-process, no call to `claude` — then prints **one line per project**:
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
alpha · en cours L1 (silencieux 4j) · dérive 1 (✗ L2 a 1 commit(s) mais est encore todo) · notes : finir L1 puis livrer · prochain L3 Typo · dépôt non commité, non poussé
|
|
468
|
+
beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second · dépôt propre
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
| Column | Content |
|
|
472
|
+
|---|---|
|
|
473
|
+
| `en cours` | ids of the lots in progress; `(aucune activité)` when it has none at all, `(silencieux Nj)` when the lot's last activity (commit, note or start) is more than `--idle` days old (default 3) |
|
|
474
|
+
| `dérive` | the gaps `raf check` reports, counted and the first two shown |
|
|
475
|
+
| `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters |
|
|
476
|
+
| `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters |
|
|
477
|
+
| `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
|
|
478
|
+
|
|
479
|
+
`--json` prints the same content as an array of objects (`project`, `doing`, `drift`, `notes`,
|
|
480
|
+
`next`, `repo`, and `error` when the project could not be read). The tour is read-only: it changes
|
|
481
|
+
no plan. It exits 0 even when a project is in error — that project's line reads
|
|
482
|
+
`beta · ✗ erreur : <cause>`. The `lead` skill runs it instead of one subagent per project.
|
|
483
|
+
|
|
458
484
|
## deliver
|
|
459
485
|
|
|
460
486
|
A delivery is done when its checks pass, not when a tool says "success".
|
|
@@ -937,8 +963,8 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
937
963
|
- **deliver**: dry run, delivery, and on failure the cause fixed rather than a
|
|
938
964
|
blind retry; after a green delivery that changes what a page shows or what it
|
|
939
965
|
is served, the `qa-reviewer` agent walks the delivered app.
|
|
940
|
-
- **lead**: from a folder holding several projects,
|
|
941
|
-
|
|
966
|
+
- **lead**: from a folder holding several projects, `cadence lead tour` (a program,
|
|
967
|
+
no model: see [lead tour](#lead-tour)) prints one line of facts per project, you choose the priorities, the lots are delegated with
|
|
942
968
|
`cadence orchestrate` (fresh short sessions with a standard brief — test first,
|
|
943
969
|
README and usage documentation updated with the change, commits citing the lot, no push — reviewed by the `code-reviewer` agent, see
|
|
944
970
|
[orchestrate](#orchestrate)), re-verified by the lead, then delivered
|
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', 'orchestrate'].includes(tool)) {
|
|
16
|
+
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead'].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'));
|
|
@@ -37,6 +37,8 @@ if (tool === 'raf') {
|
|
|
37
37
|
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--max-sessions 2] [--wave id] [--dry-run]
|
|
38
38
|
une session claude neuve par étape (implémentation, revues, corrections) ; --status, --resume ;
|
|
39
39
|
--max-sessions : sessions simultanées, toutes vagues confondues (CADENCE_MAX_SESSIONS)
|
|
40
|
+
cadence lead tour [dossier] [--idle 3] [--json]
|
|
41
|
+
le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan
|
|
40
42
|
cadence skills install [--dir .claude] [--force]
|
|
41
43
|
installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
|
|
42
44
|
process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { Plan, RafError, STATUSES } from './plan.js';
|
|
|
16
16
|
import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
17
17
|
import { schedule } from './schedule.js';
|
|
18
18
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
19
|
+
import { leadTour, tourLine } from './lead.js';
|
|
19
20
|
import { orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
20
21
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
21
22
|
import { sessionClose, sessionStart } from './session.js';
|
|
@@ -41,6 +42,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
41
42
|
raf gantt [-o docs/plan/gantt.html]
|
|
42
43
|
raf hook install
|
|
43
44
|
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"]
|
|
45
|
+
cadence lead tour [dossier] [--idle 3] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan (en cours, dérive, notes de clôture, prochain lot prêt, dépôt)
|
|
44
46
|
cadence verify [--retry s] [--sha rév] rejoue deliver.verify hors livraison : 0 vert, 1 effet rouge, 2 rien à vérifier
|
|
45
47
|
raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
|
|
46
48
|
|
|
@@ -120,6 +122,7 @@ function dispatch(argv, io) {
|
|
|
120
122
|
clear: { type: 'boolean' },
|
|
121
123
|
ignored: { type: 'boolean' },
|
|
122
124
|
notes: { type: 'boolean' },
|
|
125
|
+
json: { type: 'boolean' },
|
|
123
126
|
help: { type: 'boolean', short: 'h' },
|
|
124
127
|
},
|
|
125
128
|
});
|
|
@@ -132,6 +135,26 @@ function dispatch(argv, io) {
|
|
|
132
135
|
if (!isDay(today))
|
|
133
136
|
throw new RafError(`RAF_TODAY invalide : ${today} (attendu AAAA-MM-JJ)`);
|
|
134
137
|
const root = gitRoot(io.cwd) ?? io.cwd;
|
|
138
|
+
// Le tour parcourt les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant.
|
|
139
|
+
if (command === 'lead') {
|
|
140
|
+
if (rest[0] !== 'tour' || rest.length > 2)
|
|
141
|
+
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--json]');
|
|
142
|
+
const idle = values.idle === undefined ? undefined : Number(values.idle);
|
|
143
|
+
if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
|
|
144
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
145
|
+
const parent = resolve(io.cwd, rest[1] ?? '.');
|
|
146
|
+
if (!existsSync(parent))
|
|
147
|
+
throw new RafError(`lead tour : dossier introuvable : ${parent}`);
|
|
148
|
+
const rows = leadTour(parent, today, idle);
|
|
149
|
+
if (values.json)
|
|
150
|
+
io.out(JSON.stringify(rows, null, 2));
|
|
151
|
+
else if (rows.length === 0)
|
|
152
|
+
io.out(`aucun projet sous ${parent} (docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
153
|
+
else
|
|
154
|
+
for (const r of rows)
|
|
155
|
+
io.out(tourLine(r));
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
135
158
|
// cadence.yaml peut dire où est le plan et, s'il est tenu par un autre outil, comment le lire.
|
|
136
159
|
const configPath = resolve(io.cwd, values.config ?? join(root, 'cadence.yaml'));
|
|
137
160
|
// Installer les skills ou le hook ne lit pas le plan : un cadence.yaml fautif ne doit pas l'empêcher.
|
package/dist/lead.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { audit, nextUp, planCommits } from './audit.js';
|
|
4
|
+
import { readPlanConfig } from './config.js';
|
|
5
|
+
import { diffDays } from './dates.js';
|
|
6
|
+
import { gitRoot, repoStatus } from './git.js';
|
|
7
|
+
import { linkCommits } from './link.js';
|
|
8
|
+
import { Plan } from './plan.js';
|
|
9
|
+
import { lastActivity, lockStatus } from './session.js';
|
|
10
|
+
import { readNext, sharedStateDir, stateDir } from './state.js';
|
|
11
|
+
export const NOTES_MAX = 120;
|
|
12
|
+
export const TITLE_MAX = 60;
|
|
13
|
+
/** Au-delà de ce nombre de jours sans activité, un lot en cours est dit silencieux. */
|
|
14
|
+
export const TOUR_IDLE = 3;
|
|
15
|
+
const empty = (project) => ({ project, doing: [], drift: [], notes: [], next: null, repo: [] });
|
|
16
|
+
export function truncate(text, max) {
|
|
17
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
18
|
+
}
|
|
19
|
+
/** Sous-dossiers directs qui portent un plan : docs/plan/raf.yaml, ou un cadence.yaml avec `plan:`. */
|
|
20
|
+
export function findProjects(parent) {
|
|
21
|
+
return readdirSync(parent, { withFileTypes: true })
|
|
22
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
23
|
+
.map((e) => join(parent, e.name))
|
|
24
|
+
.filter((dir) => {
|
|
25
|
+
if (existsSync(join(dir, 'docs/plan/raf.yaml')))
|
|
26
|
+
return true;
|
|
27
|
+
const conf = join(dir, 'cadence.yaml');
|
|
28
|
+
if (!existsSync(conf) || !statSync(conf).isFile())
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
const c = readPlanConfig(conf);
|
|
32
|
+
// Une clé qa: seule ne désigne aucun plan.
|
|
33
|
+
return !!c && (!!c.path || Object.keys(c.settings).some((k) => k !== 'qaExpectations'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return true; // un cadence.yaml illisible reste un projet : sa ligne dit l'erreur
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.sort();
|
|
40
|
+
}
|
|
41
|
+
/** Les faits de `cadence session start` pour un projet, sous forme de données ; lecture seule. */
|
|
42
|
+
export function tourRow(dir, today, idle = TOUR_IDLE) {
|
|
43
|
+
const row = empty(basename(dir));
|
|
44
|
+
try {
|
|
45
|
+
const root = gitRoot(dir) ?? dir;
|
|
46
|
+
const configPath = join(root, 'cadence.yaml');
|
|
47
|
+
const planConfig = readPlanConfig(configPath, root);
|
|
48
|
+
const planPath = resolve(root, planConfig?.path ?? 'docs/plan/raf.yaml');
|
|
49
|
+
const plan = Plan.load(planPath, { ...planConfig?.settings, config: configPath });
|
|
50
|
+
const lots = plan.lots();
|
|
51
|
+
const all = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
52
|
+
const { doing, ready } = nextUp(lots);
|
|
53
|
+
row.doing = doing.map((l) => {
|
|
54
|
+
const last = lastActivity(l, all.byLot.get(l.id) ?? []);
|
|
55
|
+
if (!last)
|
|
56
|
+
return { id: l.id, noActivity: true };
|
|
57
|
+
const days = diffDays(last, today);
|
|
58
|
+
return days > idle ? { id: l.id, silentDays: days } : { id: l.id };
|
|
59
|
+
});
|
|
60
|
+
row.drift = audit(plan, root, join(root, 'docs/nouveautes'), today).map((i) => `${i.warning ? '⚠' : '✗'} ${i.message}`);
|
|
61
|
+
row.notes = readNext(stateDir(root))?.lines ?? [];
|
|
62
|
+
row.next = ready[0] ? { id: ready[0].id, title: ready[0].title } : null;
|
|
63
|
+
const s = repoStatus(root);
|
|
64
|
+
if (s.dirty || s.untracked)
|
|
65
|
+
row.repo.push('non commité');
|
|
66
|
+
if (s.ahead)
|
|
67
|
+
row.repo.push('non poussé');
|
|
68
|
+
if (lockStatus(sharedStateDir(root))?.live)
|
|
69
|
+
row.repo.push('livraison en cours');
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return { ...empty(row.project), error: (e instanceof Error ? e.message : String(e)).split('\n')[0] };
|
|
73
|
+
}
|
|
74
|
+
return row;
|
|
75
|
+
}
|
|
76
|
+
export function leadTour(parent, today, idle = TOUR_IDLE) {
|
|
77
|
+
return findProjects(parent).map((dir) => tourRow(dir, today, idle));
|
|
78
|
+
}
|
|
79
|
+
const DRIFT_SHOWN = 2;
|
|
80
|
+
/** Une ligne par projet : projet · en cours · dérive · notes · prochain lot prêt · dépôt. */
|
|
81
|
+
export function tourLine(r) {
|
|
82
|
+
if (r.error)
|
|
83
|
+
return `${r.project} · ✗ erreur : ${r.error}`;
|
|
84
|
+
const doing = r.doing.length ? r.doing.map((d) => (d.noActivity ? `${d.id} (aucune activité)` : d.silentDays === undefined ? d.id : `${d.id} (silencieux ${d.silentDays}j)`)).join(', ') : 'rien';
|
|
85
|
+
const drift = r.drift.length
|
|
86
|
+
? `${r.drift.length} (${r.drift.slice(0, DRIFT_SHOWN).join(' ; ')}${r.drift.length > DRIFT_SHOWN ? ' ; …' : ''})`
|
|
87
|
+
: 'aucune';
|
|
88
|
+
const notes = r.notes.length ? truncate(r.notes.join(' / '), NOTES_MAX) : 'aucune';
|
|
89
|
+
const next = r.next ? `${r.next.id} ${truncate(r.next.title, TITLE_MAX)}` : 'aucun';
|
|
90
|
+
return [r.project, `en cours ${doing}`, `dérive ${drift}`, `notes : ${notes}`, `prochain ${next}`, `dépôt ${r.repo.length ? r.repo.join(', ') : 'propre'}`].join(' · ');
|
|
91
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -46,7 +46,7 @@ function byLotLines(linked) {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
/** Ligne de livraison en cours ; `live` faux quand le verrou appartient à un processus mort. */
|
|
49
|
-
function lockStatus(state) {
|
|
49
|
+
export function lockStatus(state) {
|
|
50
50
|
const lock = readLock(state);
|
|
51
51
|
if (!lock)
|
|
52
52
|
return null;
|
|
@@ -77,7 +77,7 @@ function section(out, title, lines) {
|
|
|
77
77
|
out(` ${l}`);
|
|
78
78
|
}
|
|
79
79
|
/** Dernière activité d'un lot : dernier commit, dernière note ou démarrage. */
|
|
80
|
-
function lastActivity(lot, commits) {
|
|
80
|
+
export function lastActivity(lot, commits) {
|
|
81
81
|
const days = [commits[0]?.day, lot.started, ...lot.notes.map((n) => n.date)].filter((d) => !!d);
|
|
82
82
|
return days.length ? days.reduce(maxDay) : undefined;
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lead
|
|
3
|
-
description: Lead several cadence projects from a parent folder without loading them into the main context —
|
|
3
|
+
description: Lead several cadence projects from a parent folder without loading them into the main context — take the facts of every project with `cadence lead tour`, agree the priorities with the human, delegate each chosen lot to a subagent with a standard brief, have the work reviewed, re-verify it yourself, then deliver one project at a time. Triggers — "/lead", "pilot all projects", "on pilote tout", "délègue aux sous-agents", start of a multi-project day.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Lead — decide at the top, work in subagents
|
|
7
7
|
|
|
8
8
|
The lead session stays small: it reads summaries, decides with the human, delegates, checks and
|
|
9
|
-
delivers. Project files are
|
|
9
|
+
delivers. Project files are changed by subagents, each with its own context; the facts of the tour come from `cadence lead tour`, a program. The projects
|
|
10
10
|
are the sub-folders of the current directory that contain `docs/plan/raf.yaml`, or a `cadence.yaml`
|
|
11
11
|
with a `plan:` key. A project whose `cadence.yaml` maps the fields of a plan kept by its own tool is
|
|
12
12
|
**read-only** for `raf`: it takes part in the tour, and its plan is changed with the project's own
|
|
@@ -30,9 +30,12 @@ commands (its CLAUDE.md names them), never with `raf start|done|note|ux|review`.
|
|
|
30
30
|
|
|
31
31
|
## 1. Tour of the projects
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
Run `cadence lead tour` from the parent folder (one command, no subagent, no model: it reads the same
|
|
34
|
+
facts as `cadence session start` for every project). It prints one line per project — lots in progress
|
|
35
|
+
(`silencieux Nj` when idle for more than three days), drift, notes from the last close, the next ready lot,
|
|
36
|
+
repository state — and exits 0 even when a project is in error (its line says so). `--json` gives the same
|
|
37
|
+
content. Read the lines yourself; open a project's own `cadence session start` only for the one you need to
|
|
38
|
+
look at closer.
|
|
36
39
|
|
|
37
40
|
Then report to the human in a compact table — one row per project — and propose **three** items
|
|
38
41
|
across projects, each with project, lot id and one sentence of justification: finish what is in
|