@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.
@@ -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.13.1",
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.13.1",
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, one subagent per project
941
- gathers the facts, you choose the priorities, the lots are delegated with
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.13.1",
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",
@@ -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 — fan out one subagent per project to gather the facts, 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.
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 read and changed by subagents, each with its own context. The projects
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
- For each project, a subagent (read-only) runs `cd <project> && cadence session start` and returns at
34
- most five lines: lots in progress (silent ones flagged), drift, notes from the last close, the next
35
- ready lot. Two at a time, in the background.
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