@sylad/cadence 0.13.1 → 0.15.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 +64 -4
- package/bin/cadence.js +4 -1
- package/dist/audit.js +1 -1
- package/dist/cli.js +142 -11
- package/dist/config.js +27 -0
- package/dist/git.js +38 -0
- package/dist/hook.js +99 -4
- package/dist/lead.js +106 -0
- package/dist/session.js +2 -2
- package/package.json +1 -1
- package/skills/lead/SKILL.md +8 -5
- package/skills/session-close/SKILL.md +6 -5
- package/skills/session-start/SKILL.md +5 -4
|
@@ -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.15.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.15.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.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **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),
|
|
@@ -99,7 +99,30 @@ raf gantt # docs/plan/gantt.html
|
|
|
99
99
|
| `raf check --ignored` | list the acknowledged commits with their date and reason |
|
|
100
100
|
| `raf check [--since date] [--idle 7]` | since the plan's adoption date by default (a visible lot without a public title is only a `⚠` warning: it never changes the exit code): commits without a lot (commits touching only plan files are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
|
|
101
101
|
| `raf gantt [-o file]` | standalone Gantt page |
|
|
102
|
-
| `raf hook install` | add the
|
|
102
|
+
| `raf hook install` | add the post-commit hook (read-only, never blocks) and the pre-commit hook (does nothing unless `hook.autostart` says so, see below) |
|
|
103
|
+
|
|
104
|
+
#### A commit on a lot that is still `todo`
|
|
105
|
+
|
|
106
|
+
By default the post-commit hook only warns (`L1 est encore todo — raf start L1`). The `hook.autostart`
|
|
107
|
+
key of `cadence.yaml` changes what the **pre-commit** hook does when the message cites a `todo` lot
|
|
108
|
+
(a recurring lot, a lot already `doing`, a commit without a lot and a plan-upkeep commit are never concerned):
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
hook:
|
|
112
|
+
autostart: refuse # warn (default) | refuse | start
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `warn`: today's behaviour, the commit goes through, the post-commit hook warns.
|
|
116
|
+
- `refuse`: the commit fails, loudly, with `raf start <id>` in the message (`--no-verify` still bypasses it).
|
|
117
|
+
- `start`: the hook runs `raf start <id>` and stages the plan, so the plan change is **in the same commit** —
|
|
118
|
+
no amend afterwards, no dirty plan left behind. Only when `docs/plan/raf.yaml` is identical to `HEAD` (no
|
|
119
|
+
uncommitted change to the plan, staged or not): otherwise nothing is started, the hook says why
|
|
120
|
+
(`plan modifié non commité : démarrage automatique sauté, raf start <id> à la main`) and behaves like `warn`.
|
|
121
|
+
|
|
122
|
+
The pre-commit hook reads the message from the command line of the `git commit` that runs it (`-m`, `-am`,
|
|
123
|
+
`--message`, `-F`; Linux, through `/proc`) or from `CADENCE_COMMIT_MESSAGE`. When the message is not known yet
|
|
124
|
+
(editor, `--amend`) it does nothing and the post-commit warning stays. Run `raf hook install` again to add the
|
|
125
|
+
pre-commit block to an existing repository. A read-only plan is never started by the hook. Only a refusal (exit code 3 of `raf hook pre-commit`) stops a commit: an older or broken `raf` lets it through.
|
|
103
126
|
|
|
104
127
|
Global options: `--file path` or `RAF_FILE`; `RAF_TODAY=YYYY-MM-DD` to freeze
|
|
105
128
|
the date. `cadence raf …` is the same as `raf …`.
|
|
@@ -387,10 +410,21 @@ cadence session start --since "3 days ago" --idle 2
|
|
|
387
410
|
cadence session close # today's commits by lot, commits without a lot, lots in progress
|
|
388
411
|
# with no commit today, drift, uncommitted / unpushed work
|
|
389
412
|
# exit 1 while something is still open
|
|
413
|
+
cadence session start --all # from a folder that is not a repository: one "## project" section per
|
|
414
|
+
cadence session close --all --depth 2 # sub-folder holding a plan (--depth n: n levels down, 1 by default);
|
|
415
|
+
# exit = the worst code of the projects (2 error, 1 open, 0 closed)
|
|
390
416
|
cadence session next "finish L3" "review L4" # shown by the next session start; replaces the previous notes
|
|
391
417
|
cadence session next --clear # erase those notes, on purpose
|
|
392
418
|
```
|
|
393
419
|
|
|
420
|
+
`--all` runs the same command inside each project found under the current folder —
|
|
421
|
+
`docs/plan/raf.yaml`, or a `cadence.yaml` with `plan:` — and never enters a project
|
|
422
|
+
to look for another. `--since` and `--idle` go to every project; an error in one
|
|
423
|
+
project is printed in its section and the others still run. It replaces the shell
|
|
424
|
+
loop over the sub-folders. A project that is not the root of its own git repository gets a `✗` line
|
|
425
|
+
(and exit 2) instead of the parent repository's plan. `--all` and `--depth` are refused outside `session`,
|
|
426
|
+
and `--all` refuses `--file`, `--config` and `RAF_FILE`: each project reads its own plan. For a one-line-per-project overview, `cadence lead tour`.
|
|
427
|
+
|
|
394
428
|
`cadence session next` without a line refuses (exit 2) and leaves the notes of the
|
|
395
429
|
last close as they are — it used to erase them silently; erasing is `--clear`.
|
|
396
430
|
|
|
@@ -455,6 +489,32 @@ The commands get `CADENCE_SINCE` (the `--since` in effect) and `CADENCE_TODAY`.
|
|
|
455
489
|
add facts and decide nothing: a failing command is reported and changes neither
|
|
456
490
|
the exit code nor the verdict.
|
|
457
491
|
|
|
492
|
+
## lead tour
|
|
493
|
+
|
|
494
|
+
`cadence lead tour [folder] [--idle 3] [--json]` is the lead's morning table, with no model in
|
|
495
|
+
the loop. From a parent folder (default: the current one) it takes every direct sub-folder that
|
|
496
|
+
holds `docs/plan/raf.yaml`, or a `cadence.yaml` with a `plan:` key (a plan kept by another tool
|
|
497
|
+
included), and for each one reads the same facts as `cadence session start` — in-process, no
|
|
498
|
+
sub-process, no call to `claude` — then prints **one line per project**:
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
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é
|
|
502
|
+
beta · en cours rien · dérive aucune · notes : aucune · prochain T2 Second · dépôt propre
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
| Column | Content |
|
|
506
|
+
|---|---|
|
|
507
|
+
| `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) |
|
|
508
|
+
| `dérive` | the gaps `raf check` reports, counted and the first two shown |
|
|
509
|
+
| `notes` | the notes left by the last `session close`, joined with ` / ` and cut at 120 characters |
|
|
510
|
+
| `prochain` | the first ready lot (quick wins first) — id and title cut at 60 characters |
|
|
511
|
+
| `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
|
|
512
|
+
|
|
513
|
+
`--json` prints the same content as an array of objects (`project`, `doing`, `drift`, `notes`,
|
|
514
|
+
`next`, `repo`, and `error` when the project could not be read). The tour is read-only: it changes
|
|
515
|
+
no plan. It exits 0 even when a project is in error — that project's line reads
|
|
516
|
+
`beta · ✗ erreur : <cause>`. The `lead` skill runs it instead of one subagent per project.
|
|
517
|
+
|
|
458
518
|
## deliver
|
|
459
519
|
|
|
460
520
|
A delivery is done when its checks pass, not when a tool says "success".
|
|
@@ -937,8 +997,8 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
|
|
|
937
997
|
- **deliver**: dry run, delivery, and on failure the cause fixed rather than a
|
|
938
998
|
blind retry; after a green delivery that changes what a page shows or what it
|
|
939
999
|
is served, the `qa-reviewer` agent walks the delivered app.
|
|
940
|
-
- **lead**: from a folder holding several projects,
|
|
941
|
-
|
|
1000
|
+
- **lead**: from a folder holding several projects, `cadence lead tour` (a program,
|
|
1001
|
+
no model: see [lead tour](#lead-tour)) prints one line of facts per project, you choose the priorities, the lots are delegated with
|
|
942
1002
|
`cadence orchestrate` (fresh short sessions with a standard brief — test first,
|
|
943
1003
|
README and usage documentation updated with the change, commits citing the lot, no push — reviewed by the `code-reviewer` agent, see
|
|
944
1004
|
[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'));
|
|
@@ -26,6 +26,7 @@ if (tool === 'raf') {
|
|
|
26
26
|
cadence session start [--since "24 hours ago"] [--idle 2]
|
|
27
27
|
faits de reprise : notes de la veille, en cours, fait depuis, écarts, propositions
|
|
28
28
|
cadence session close [--since …] faits de clôture ; code 1 tant que ce n'est pas fermé
|
|
29
|
+
cadence session start|close --all [--depth 1] depuis un dossier non dépôt : une section par projet des sous-dossiers
|
|
29
30
|
cadence session next "ligne" … notes pour la prochaine session (remplacent les précédentes)
|
|
30
31
|
cadence session next --clear efface ces notes ; sans ligne ni --clear, la commande refuse
|
|
31
32
|
cadence deliver [--dry-run] [--sha rév] [--config cadence.yaml] [-- arguments du script du projet]
|
|
@@ -37,6 +38,8 @@ if (tool === 'raf') {
|
|
|
37
38
|
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--max-sessions 2] [--wave id] [--dry-run]
|
|
38
39
|
une session claude neuve par étape (implémentation, revues, corrections) ; --status, --resume ;
|
|
39
40
|
--max-sessions : sessions simultanées, toutes vagues confondues (CADENCE_MAX_SESSIONS)
|
|
41
|
+
cadence lead tour [dossier] [--idle 3] [--json]
|
|
42
|
+
le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan
|
|
40
43
|
cadence skills install [--dir .claude] [--force]
|
|
41
44
|
installe les skills Claude Code session-start, session-close, deliver et l'agent ux-reviewer`);
|
|
42
45
|
process.exitCode = !tool || ['help', '--help', '-h'].includes(tool) ? 0 : 2;
|
package/dist/audit.js
CHANGED
|
@@ -13,7 +13,7 @@ function configRel(plan, root) {
|
|
|
13
13
|
return plan.configFile ? relative(root, plan.configFile) : 'cadence.yaml';
|
|
14
14
|
}
|
|
15
15
|
/** Le plan, sa page Gantt, la configuration lue (cadence.yaml) et les fichiers tenus avec lui (plan.files). */
|
|
16
|
-
function ownFiles(plan, root) {
|
|
16
|
+
export function ownFiles(plan, root) {
|
|
17
17
|
return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), configRel(plan, root), ...plan.maintained]);
|
|
18
18
|
}
|
|
19
19
|
function withoutPlanKey(text) {
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { readNewsConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
|
-
import { audit, exemptPlanOnly, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
4
|
+
import { readHookConfig, readNewsConfig, readPlanConfig, readSessionConfig } from './config.js';
|
|
5
|
+
import { audit, exemptPlanOnly, ownFiles, isPlanOnly, lotWork, nextUp, planCommits, unreviewedWork } from './audit.js';
|
|
6
6
|
import { short } from './check.js';
|
|
7
7
|
import { isDay, toDay } from './dates.js';
|
|
8
8
|
import { deliver, parseDeliverConfig, realDeps } from './deliver.js';
|
|
9
9
|
import { defaultTarget, effectLines, realCheckDeps, verifyCommand } from './verify.js';
|
|
10
10
|
import { ganttData, renderGantt } from './gantt.js';
|
|
11
|
-
import { gitRoot, readCommits, resolveCommit } from './git.js';
|
|
12
|
-
import { installHook } from './hook.js';
|
|
11
|
+
import { gitRoot, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
|
|
12
|
+
import { installHook, pendingCommitMessage, REFUSED } from './hook.js';
|
|
13
13
|
import { citedRefs, linkCommits } from './link.js';
|
|
14
14
|
import { buildNews, loadEntries, newEntry, newsData, newsIssues, publicTitleTooLong, stampEntries } from './news.js';
|
|
15
15
|
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 { findProjects, 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';
|
|
@@ -39,8 +40,9 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
39
40
|
raf check [--since date] [--idle 7] (défaut : date « since » du plan) code 1 s'il y a des écarts
|
|
40
41
|
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
42
|
raf gantt [-o docs/plan/gantt.html]
|
|
42
|
-
raf hook install
|
|
43
|
+
raf hook install (pre-commit et post-commit ; cadence.yaml hook.autostart: warn|refuse|start)
|
|
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
|
|
|
@@ -112,6 +114,8 @@ function dispatch(argv, io) {
|
|
|
112
114
|
status: { type: 'string' },
|
|
113
115
|
since: { type: 'string' },
|
|
114
116
|
idle: { type: 'string' },
|
|
117
|
+
all: { type: 'boolean' },
|
|
118
|
+
depth: { type: 'string' },
|
|
115
119
|
output: { type: 'string', short: 'o' },
|
|
116
120
|
config: { type: 'string' },
|
|
117
121
|
'dry-run': { type: 'boolean' },
|
|
@@ -120,6 +124,7 @@ function dispatch(argv, io) {
|
|
|
120
124
|
clear: { type: 'boolean' },
|
|
121
125
|
ignored: { type: 'boolean' },
|
|
122
126
|
notes: { type: 'boolean' },
|
|
127
|
+
json: { type: 'boolean' },
|
|
123
128
|
help: { type: 'boolean', short: 'h' },
|
|
124
129
|
},
|
|
125
130
|
});
|
|
@@ -128,10 +133,35 @@ function dispatch(argv, io) {
|
|
|
128
133
|
io.out(HELP);
|
|
129
134
|
return 0;
|
|
130
135
|
}
|
|
136
|
+
if (command !== 'session' && (values.all || values.depth !== undefined))
|
|
137
|
+
throw new RafError(`${values.all ? '--all' : '--depth'} ne va qu'avec session start ou session close`);
|
|
131
138
|
const today = io.env.RAF_TODAY ?? toDay(io.now());
|
|
132
139
|
if (!isDay(today))
|
|
133
140
|
throw new RafError(`RAF_TODAY invalide : ${today} (attendu AAAA-MM-JJ)`);
|
|
134
141
|
const root = gitRoot(io.cwd) ?? io.cwd;
|
|
142
|
+
// Le tour parcourt les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant.
|
|
143
|
+
if (command === 'lead') {
|
|
144
|
+
if (rest[0] !== 'tour' || rest.length > 2)
|
|
145
|
+
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--json]');
|
|
146
|
+
const idle = values.idle === undefined ? undefined : Number(values.idle);
|
|
147
|
+
if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
|
|
148
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
149
|
+
const parent = resolve(io.cwd, rest[1] ?? '.');
|
|
150
|
+
if (!existsSync(parent))
|
|
151
|
+
throw new RafError(`lead tour : dossier introuvable : ${parent}`);
|
|
152
|
+
const rows = leadTour(parent, today, idle);
|
|
153
|
+
if (values.json)
|
|
154
|
+
io.out(JSON.stringify(rows, null, 2));
|
|
155
|
+
else if (rows.length === 0)
|
|
156
|
+
io.out(`aucun projet sous ${parent} (docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
157
|
+
else
|
|
158
|
+
for (const r of rows)
|
|
159
|
+
io.out(tourLine(r));
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
// --all balaie les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant.
|
|
163
|
+
if (command === 'session' && (values.all || values.depth !== undefined))
|
|
164
|
+
return sessionAll(rest, values, io);
|
|
135
165
|
// cadence.yaml peut dire où est le plan et, s'il est tenu par un autre outil, comment le lire.
|
|
136
166
|
const configPath = resolve(io.cwd, values.config ?? join(root, 'cadence.yaml'));
|
|
137
167
|
// Installer les skills ou le hook ne lit pas le plan : un cadence.yaml fautif ne doit pas l'empêcher.
|
|
@@ -360,8 +390,10 @@ function dispatch(argv, io) {
|
|
|
360
390
|
return 0;
|
|
361
391
|
}
|
|
362
392
|
if (rest[0] === 'post-commit')
|
|
363
|
-
return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io);
|
|
364
|
-
|
|
393
|
+
return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart);
|
|
394
|
+
if (rest[0] === 'pre-commit')
|
|
395
|
+
return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
|
|
396
|
+
throw new RafError('usage : raf hook install|pre-commit|post-commit');
|
|
365
397
|
}
|
|
366
398
|
case 'session': {
|
|
367
399
|
if (!gitRoot(io.cwd))
|
|
@@ -512,11 +544,66 @@ function now(plan, root, newsDir, today, io) {
|
|
|
512
544
|
io.out(`\n${issues.length} écart(s) entre le plan et l'historique — raf check`);
|
|
513
545
|
return 0;
|
|
514
546
|
}
|
|
515
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Pre-commit : un commit qui cite un lot encore todo. `warn` (défaut) : rien ici, le post-commit avertit ; `refuse` :
|
|
549
|
+
* échoue (REFUSED) avec « raf start <id> » ; `start` : démarre le lot et met le plan dans l'index, donc dans CE commit,
|
|
550
|
+
* mais seulement si le plan est identique à HEAD : sinon rien n'est démarré (message, puis avertissement du post-commit).
|
|
551
|
+
* Ne rend REFUSED que pour un refus : toute autre panne laisse le commit passer. Sans message lisible (éditeur), rien.
|
|
552
|
+
*/
|
|
553
|
+
function preCommit(load, mode, root, today, io) {
|
|
554
|
+
if (!load || !mode || mode === 'warn')
|
|
555
|
+
return 0;
|
|
556
|
+
try {
|
|
557
|
+
const message = pendingCommitMessage(io.env);
|
|
558
|
+
if (message === null)
|
|
559
|
+
return 0;
|
|
560
|
+
const plan = load();
|
|
561
|
+
const [subject = '', ...body] = message.split('\n');
|
|
562
|
+
const refs = citedRefs({ sha: '', day: today, subject, body: body.join('\n').trim() }, plan.refs);
|
|
563
|
+
const staged = stagedFiles(root);
|
|
564
|
+
const own = ownFiles(plan, root);
|
|
565
|
+
// Entretien du plan : tous les fichiers du commit sont ceux du plan (la configuration seule ne compte pas).
|
|
566
|
+
if (staged.length > 0 && staged.every((f) => own.has(f) && f !== relative(root, plan.configFile ?? join(root, 'cadence.yaml'))))
|
|
567
|
+
return 0;
|
|
568
|
+
const todo = [...new Set(refs.map((r) => r.lot))].filter((id) => {
|
|
569
|
+
const lot = plan.lots().find((l) => l.id === id);
|
|
570
|
+
return lot?.status === 'todo' && !isRecurring(lot);
|
|
571
|
+
});
|
|
572
|
+
if (todo.length === 0)
|
|
573
|
+
return 0;
|
|
574
|
+
if (mode === 'refuse') {
|
|
575
|
+
for (const id of todo)
|
|
576
|
+
io.err(`raf: ${id} est encore todo — commit refusé (hook.autostart: refuse)${plan.readonly ? '' : ` : raf start ${id}`} (--no-verify pour passer outre)`);
|
|
577
|
+
return REFUSED;
|
|
578
|
+
}
|
|
579
|
+
if (plan.readonly)
|
|
580
|
+
return 0;
|
|
581
|
+
const file = relative(root, plan.path);
|
|
582
|
+
// Un plan déjà modifié (stagé ou non) : le restager emporterait ces modifications dans ce commit, ou, sur un
|
|
583
|
+
// commit partiel, les perdrait. On ne démarre rien ; le post-commit avertit comme en mode warn.
|
|
584
|
+
if (!matchesHead(root, file)) {
|
|
585
|
+
io.err(`raf: plan modifié non commité : démarrage automatique sauté (hook.autostart: start), raf start ${todo.join(' ')} à la main`);
|
|
586
|
+
return 0;
|
|
587
|
+
}
|
|
588
|
+
for (const id of todo) {
|
|
589
|
+
plan.setStatus(id, 'doing', today, {});
|
|
590
|
+
io.err(`raf: ${id} était todo — raf start ${id} fait par le hook, plan inclus dans ce commit`);
|
|
591
|
+
}
|
|
592
|
+
plan.save();
|
|
593
|
+
stage(root, file);
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Un hook ne doit jamais gêner un commit, hors refus.
|
|
597
|
+
}
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
function postCommit(load, newsDir, root, today, io, mode) {
|
|
516
601
|
if (!load)
|
|
517
602
|
return 0;
|
|
518
603
|
try {
|
|
519
604
|
const plan = load();
|
|
605
|
+
if (mode === 'start' && !plan.readonly)
|
|
606
|
+
syncIndexWithHead(root, relative(root, plan.path));
|
|
520
607
|
const head = readCommits(root, { range: '-1' })[0];
|
|
521
608
|
if (!head)
|
|
522
609
|
return 0;
|
|
@@ -623,6 +710,50 @@ function morningEffects(configPath, root) {
|
|
|
623
710
|
return [`✗ ${e.message}`];
|
|
624
711
|
}
|
|
625
712
|
}
|
|
713
|
+
/** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
|
|
714
|
+
async function sessionAll(rest, values, io) {
|
|
715
|
+
const sub = rest[0];
|
|
716
|
+
if (!values.all)
|
|
717
|
+
throw new RafError('--depth demande --all');
|
|
718
|
+
// Chaque projet lit son propre plan : un plan ou une config imposés se retrouveraient dans toutes les sections.
|
|
719
|
+
const imposed = values.file !== undefined ? '--file' : values.config !== undefined ? '--config' : io.env.RAF_FILE !== undefined ? 'RAF_FILE' : null;
|
|
720
|
+
if (imposed)
|
|
721
|
+
throw new RafError(`${imposed} ne va pas avec --all : chaque projet lit son propre plan (lancer la commande dans le projet visé)`);
|
|
722
|
+
if (sub !== 'start' && sub !== 'close')
|
|
723
|
+
throw new RafError('--all ne va qu\'avec session start ou session close');
|
|
724
|
+
const depth = values.depth === undefined ? 1 : Number(values.depth);
|
|
725
|
+
if (!Number.isInteger(depth) || depth < 1)
|
|
726
|
+
throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
|
|
727
|
+
if (values.idle !== undefined && !(Number.isInteger(Number(values.idle)) && Number(values.idle) >= 0))
|
|
728
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
729
|
+
const projects = findProjects(io.cwd, depth);
|
|
730
|
+
if (projects.length === 0) {
|
|
731
|
+
io.out(`aucun projet sous ${io.cwd} (docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
732
|
+
return 0;
|
|
733
|
+
}
|
|
734
|
+
const argv = ['session', sub, ...(values.since === undefined ? [] : [`--since=${values.since}`]), ...(values.idle === undefined ? [] : ['--idle', values.idle])];
|
|
735
|
+
let worst = 0;
|
|
736
|
+
for (const dir of projects) {
|
|
737
|
+
io.out(`${dir === projects[0] ? '' : '\n'}## ${relative(io.cwd, dir)}`);
|
|
738
|
+
let code;
|
|
739
|
+
const top = gitRoot(dir);
|
|
740
|
+
if (top && realpathSync(top) !== realpathSync(dir)) {
|
|
741
|
+
// run() lirait le plan du dépôt parent : mieux vaut le dire que montrer le plan d'un autre projet.
|
|
742
|
+
io.out(`✗ ${relative(io.cwd, dir)} n'a pas son propre dépôt git (dépôt parent : ${top}) — lancer cadence dans ce dossier`);
|
|
743
|
+
worst = 2;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
code = await run(argv, { ...io, cwd: dir, err: (l) => io.out(`✗ ${l}`) });
|
|
748
|
+
}
|
|
749
|
+
catch (e) {
|
|
750
|
+
io.out(`✗ ${e instanceof Error ? e.message : String(e)}`);
|
|
751
|
+
code = 2;
|
|
752
|
+
}
|
|
753
|
+
worst = Math.max(worst, code);
|
|
754
|
+
}
|
|
755
|
+
return worst;
|
|
756
|
+
}
|
|
626
757
|
function session([sub, ...args], ctx, values) {
|
|
627
758
|
switch (sub) {
|
|
628
759
|
case 'start': {
|
|
@@ -653,6 +784,6 @@ function session([sub, ...args], ctx, values) {
|
|
|
653
784
|
return 0;
|
|
654
785
|
}
|
|
655
786
|
default:
|
|
656
|
-
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" … | next --clear');
|
|
787
|
+
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | start|close --all [--depth n] | next "ligne" … | next --clear');
|
|
657
788
|
}
|
|
658
789
|
}
|
package/dist/config.js
CHANGED
|
@@ -141,6 +141,33 @@ export function readNewsConfig(file) {
|
|
|
141
141
|
throw new RafError(`${file} : news.publicTitleMax : nombre entier de caractères (≥ 1) attendu`);
|
|
142
142
|
return { publicTitleMax: max };
|
|
143
143
|
}
|
|
144
|
+
const AUTOSTART = ['warn', 'refuse', 'start'];
|
|
145
|
+
/** Clé `hook:` de cadence.yaml. */
|
|
146
|
+
export function readHookConfig(file) {
|
|
147
|
+
if (!existsSync(file))
|
|
148
|
+
return {};
|
|
149
|
+
let raw;
|
|
150
|
+
try {
|
|
151
|
+
raw = parse(readFileSync(file, 'utf8'));
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
155
|
+
}
|
|
156
|
+
const h = raw?.hook;
|
|
157
|
+
if (h == null)
|
|
158
|
+
return {};
|
|
159
|
+
if (!isObject(h))
|
|
160
|
+
throw new RafError(`${file} : hook doit être un objet { autostart }`);
|
|
161
|
+
for (const k of Object.keys(h))
|
|
162
|
+
if (k !== 'autostart')
|
|
163
|
+
throw new RafError(`${file} : hook.${k} inconnu (attendu : autostart)`);
|
|
164
|
+
const a = h.autostart;
|
|
165
|
+
if (a == null)
|
|
166
|
+
return {};
|
|
167
|
+
if (typeof a !== 'string' || !AUTOSTART.includes(a))
|
|
168
|
+
throw new RafError(`${file} : hook.autostart : ${AUTOSTART.join(', ')} attendu`);
|
|
169
|
+
return { autostart: a };
|
|
170
|
+
}
|
|
144
171
|
/**
|
|
145
172
|
* Clé `session:` de cadence.yaml : les faits propres au projet (son script de reprise, de clôture),
|
|
146
173
|
* joués par cadence au lieu d'être remplacés.
|
package/dist/git.js
CHANGED
|
@@ -45,6 +45,44 @@ export function hooksDir(cwd) {
|
|
|
45
45
|
const dir = resolve(cwd, git(cwd, ['rev-parse', '--git-path', 'hooks']).trim());
|
|
46
46
|
return basename(dir) === '_' && basename(dirname(dir)) === '.husky' ? dirname(dir) : dir;
|
|
47
47
|
}
|
|
48
|
+
/** Fichiers de l'index (ce que le commit en préparation contient), relatifs à la racine du dépôt. */
|
|
49
|
+
export function stagedFiles(cwd) {
|
|
50
|
+
return git(cwd, ['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only', '-z']).split('\0').filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
/** Ajoute un fichier à l'index. */
|
|
53
|
+
export function stage(cwd, file) {
|
|
54
|
+
git(cwd, ['add', '--', file]);
|
|
55
|
+
}
|
|
56
|
+
/** Le fichier de travail est-il identique à sa version de HEAD (ni modification stagée, ni non stagée) ? Faux s'il n'est pas dans HEAD. */
|
|
57
|
+
export function matchesHead(cwd, file) {
|
|
58
|
+
try {
|
|
59
|
+
git(cwd, ['cat-file', '-e', `HEAD:${file}`]);
|
|
60
|
+
git(cwd, ['diff', '--quiet', 'HEAD', '--', file]);
|
|
61
|
+
git(cwd, ['diff', '--quiet', '--cached', 'HEAD', '--', file]);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Aligne l'index sur HEAD pour `file` quand le fichier de travail est déjà celui de HEAD : un commit partiel
|
|
70
|
+
* (`git commit <chemins>`) ne recopie dans l'index réel que ces chemins, et y laisse l'ancienne version du fichier
|
|
71
|
+
* stagé par un pre-commit. Sans effet si le fichier a d'autres modifications.
|
|
72
|
+
*/
|
|
73
|
+
export function syncIndexWithHead(cwd, file) {
|
|
74
|
+
const same = (args) => {
|
|
75
|
+
try {
|
|
76
|
+
git(cwd, ['diff', '--quiet', ...args, '--', file]);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
if (same(['HEAD']) && !same(['--cached']))
|
|
84
|
+
stage(cwd, file);
|
|
85
|
+
}
|
|
48
86
|
/** Fichiers modifiés par un commit, relatifs à la racine du dépôt (noms accentués ou à espaces compris). */
|
|
49
87
|
export function changedFiles(cwd, sha) {
|
|
50
88
|
// -z : noms séparés par NUL, jamais échappés ; quotepath=off en plus pour les sorties qui citeraient quand même.
|
package/dist/hook.js
CHANGED
|
@@ -4,17 +4,29 @@ import { hooksDir } from './git.js';
|
|
|
4
4
|
import { RafError } from './plan.js';
|
|
5
5
|
const BEGIN = '# >>> raf (cadence)';
|
|
6
6
|
const END = '# <<< raf (cadence)';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** Code de sortie de `raf hook pre-commit` qui refuse le commit. */
|
|
8
|
+
export const REFUSED = 3;
|
|
9
|
+
const BLOCKS = {
|
|
10
|
+
'post-commit': `${BEGIN}\ncommand -v raf >/dev/null 2>&1 && raf hook post-commit || true\n${END}\n`,
|
|
11
|
+
// Seul un refus (hook.autostart: refuse, code 3) arrête le commit : un raf plus ancien ou en panne le laisse passer.
|
|
12
|
+
'pre-commit': `${BEGIN}\nif command -v raf >/dev/null 2>&1; then raf hook pre-commit; [ $? -ne ${REFUSED} ] || exit 1; fi\n${END}\n`,
|
|
13
|
+
};
|
|
14
|
+
/** Adds the raf blocks to post-commit and pre-commit without touching anything else already there. */
|
|
9
15
|
export function installHook(cwd) {
|
|
16
|
+
const post = installBlock(cwd, 'post-commit');
|
|
17
|
+
const pre = installBlock(cwd, 'pre-commit');
|
|
18
|
+
return { path: post.path, changed: post.changed || pre.changed };
|
|
19
|
+
}
|
|
20
|
+
function installBlock(cwd, name) {
|
|
21
|
+
const BLOCK = BLOCKS[name];
|
|
10
22
|
const dir = hooksDir(cwd);
|
|
11
23
|
mkdirSync(dir, { recursive: true });
|
|
12
|
-
const path = join(dir,
|
|
24
|
+
const path = join(dir, name);
|
|
13
25
|
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
14
26
|
if (current.includes(BEGIN))
|
|
15
27
|
return { path, changed: false };
|
|
16
28
|
if (current !== '' && !/^#!.*\b(sh|bash|zsh|dash)\b/.test(current)) {
|
|
17
|
-
throw new RafError(`${path} n'est pas un script shell : y ajouter à la main « raf hook
|
|
29
|
+
throw new RafError(`${path} n'est pas un script shell : y ajouter à la main « raf hook ${name} »`);
|
|
18
30
|
}
|
|
19
31
|
let next;
|
|
20
32
|
if (current === '') {
|
|
@@ -34,3 +46,86 @@ export function installHook(cwd) {
|
|
|
34
46
|
chmodSync(path, 0o755);
|
|
35
47
|
return { path, changed: true };
|
|
36
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Message d'un `git commit` d'après ses arguments (-m, -am, --message, plusieurs -m, -F) ; null sans message
|
|
51
|
+
* en ligne de commande (éditeur, --amend, -C…) ou si ce n'est pas un commit.
|
|
52
|
+
*/
|
|
53
|
+
export function messageFromArgv(argv) {
|
|
54
|
+
const at = argv.indexOf('commit');
|
|
55
|
+
if (at < 0)
|
|
56
|
+
return null;
|
|
57
|
+
const parts = [];
|
|
58
|
+
let file = null;
|
|
59
|
+
for (let i = at + 1; i < argv.length; i++) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
if (a === '--')
|
|
62
|
+
break;
|
|
63
|
+
if (a.startsWith('--message='))
|
|
64
|
+
parts.push(a.slice('--message='.length));
|
|
65
|
+
else if (a === '--message')
|
|
66
|
+
parts.push(argv[++i] ?? '');
|
|
67
|
+
else if (a.startsWith('--file='))
|
|
68
|
+
file = a.slice('--file='.length);
|
|
69
|
+
else if (a === '--file')
|
|
70
|
+
file = argv[++i] ?? null;
|
|
71
|
+
else if (/^-[A-Za-z]+$/.test(a) && /[mF]$/.test(a)) {
|
|
72
|
+
// « -am », « -qm » : la dernière lettre prend l'argument suivant.
|
|
73
|
+
const value = argv[++i] ?? '';
|
|
74
|
+
if (a.endsWith('m'))
|
|
75
|
+
parts.push(value);
|
|
76
|
+
else
|
|
77
|
+
file = value;
|
|
78
|
+
}
|
|
79
|
+
else if (/^-[A-Za-z]*m./.test(a) && !a.startsWith('--'))
|
|
80
|
+
parts.push(a.slice(a.indexOf('m') + 1));
|
|
81
|
+
else if (/^-F./.test(a))
|
|
82
|
+
file = a.slice(2);
|
|
83
|
+
}
|
|
84
|
+
if (parts.length > 0)
|
|
85
|
+
return parts.join('\n\n');
|
|
86
|
+
if (file !== null) {
|
|
87
|
+
try {
|
|
88
|
+
return readFileSync(file === '-' ? 0 : file, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/** Arguments du processus `pid` (Linux : /proc) ; null si illisibles. */
|
|
97
|
+
function argvOf(pid) {
|
|
98
|
+
try {
|
|
99
|
+
return readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').slice(0, -1);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function parentOf(pid) {
|
|
106
|
+
try {
|
|
107
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
108
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1]);
|
|
109
|
+
return Number.isInteger(ppid) && ppid > 1 ? ppid : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Message du commit en cours depuis un pre-commit, quand git ne le donne pas encore : celui de la ligne de
|
|
117
|
+
* commande du `git commit` qui nous a lancés (ancêtres du hook), ou CADENCE_COMMIT_MESSAGE. Null sinon.
|
|
118
|
+
*/
|
|
119
|
+
export function pendingCommitMessage(env, pid = process.ppid) {
|
|
120
|
+
if (env.CADENCE_COMMIT_MESSAGE)
|
|
121
|
+
return env.CADENCE_COMMIT_MESSAGE;
|
|
122
|
+
let cur = pid;
|
|
123
|
+
for (let depth = 0; cur !== null && depth < 6; depth++) {
|
|
124
|
+
const argv = argvOf(cur);
|
|
125
|
+
const msg = argv && /(^|\/)git$/.test(argv[0] ?? '') ? messageFromArgv(argv) : null;
|
|
126
|
+
if (msg !== null)
|
|
127
|
+
return msg;
|
|
128
|
+
cur = parentOf(cur);
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
package/dist/lead.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
/** Un dossier est un projet s'il porte docs/plan/raf.yaml, ou un cadence.yaml avec `plan:`. */
|
|
20
|
+
function isProject(dir) {
|
|
21
|
+
if (existsSync(join(dir, 'docs/plan/raf.yaml')))
|
|
22
|
+
return true;
|
|
23
|
+
const conf = join(dir, 'cadence.yaml');
|
|
24
|
+
if (!existsSync(conf) || !statSync(conf).isFile())
|
|
25
|
+
return false;
|
|
26
|
+
try {
|
|
27
|
+
const c = readPlanConfig(conf);
|
|
28
|
+
// Une clé qa: seule ne désigne aucun plan.
|
|
29
|
+
return !!c && (!!c.path || Object.keys(c.settings).some((k) => k !== 'qaExpectations'));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return true; // un cadence.yaml illisible reste un projet : sa ligne dit l'erreur
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Les projets sous `parent`, jusqu'à `depth` niveaux (1 : sous-dossiers directs) ; on ne descend pas dans un projet. */
|
|
36
|
+
export function findProjects(parent, depth = 1) {
|
|
37
|
+
const found = [];
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(parent, { withFileTypes: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return found; // dossier illisible : on le saute, les autres projets tournent
|
|
44
|
+
}
|
|
45
|
+
for (const e of entries) {
|
|
46
|
+
if (!e.isDirectory() || e.name.startsWith('.') || e.name === 'node_modules')
|
|
47
|
+
continue;
|
|
48
|
+
const dir = join(parent, e.name);
|
|
49
|
+
if (isProject(dir))
|
|
50
|
+
found.push(dir);
|
|
51
|
+
else if (depth > 1)
|
|
52
|
+
found.push(...findProjects(dir, depth - 1));
|
|
53
|
+
}
|
|
54
|
+
return found.sort();
|
|
55
|
+
}
|
|
56
|
+
/** Les faits de `cadence session start` pour un projet, sous forme de données ; lecture seule. */
|
|
57
|
+
export function tourRow(dir, today, idle = TOUR_IDLE) {
|
|
58
|
+
const row = empty(basename(dir));
|
|
59
|
+
try {
|
|
60
|
+
const root = gitRoot(dir) ?? dir;
|
|
61
|
+
const configPath = join(root, 'cadence.yaml');
|
|
62
|
+
const planConfig = readPlanConfig(configPath, root);
|
|
63
|
+
const planPath = resolve(root, planConfig?.path ?? 'docs/plan/raf.yaml');
|
|
64
|
+
const plan = Plan.load(planPath, { ...planConfig?.settings, config: configPath });
|
|
65
|
+
const lots = plan.lots();
|
|
66
|
+
const all = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
67
|
+
const { doing, ready } = nextUp(lots);
|
|
68
|
+
row.doing = doing.map((l) => {
|
|
69
|
+
const last = lastActivity(l, all.byLot.get(l.id) ?? []);
|
|
70
|
+
if (!last)
|
|
71
|
+
return { id: l.id, noActivity: true };
|
|
72
|
+
const days = diffDays(last, today);
|
|
73
|
+
return days > idle ? { id: l.id, silentDays: days } : { id: l.id };
|
|
74
|
+
});
|
|
75
|
+
row.drift = audit(plan, root, join(root, 'docs/nouveautes'), today).map((i) => `${i.warning ? '⚠' : '✗'} ${i.message}`);
|
|
76
|
+
row.notes = readNext(stateDir(root))?.lines ?? [];
|
|
77
|
+
row.next = ready[0] ? { id: ready[0].id, title: ready[0].title } : null;
|
|
78
|
+
const s = repoStatus(root);
|
|
79
|
+
if (s.dirty || s.untracked)
|
|
80
|
+
row.repo.push('non commité');
|
|
81
|
+
if (s.ahead)
|
|
82
|
+
row.repo.push('non poussé');
|
|
83
|
+
if (lockStatus(sharedStateDir(root))?.live)
|
|
84
|
+
row.repo.push('livraison en cours');
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
return { ...empty(row.project), error: (e instanceof Error ? e.message : String(e)).split('\n')[0] };
|
|
88
|
+
}
|
|
89
|
+
return row;
|
|
90
|
+
}
|
|
91
|
+
export function leadTour(parent, today, idle = TOUR_IDLE) {
|
|
92
|
+
return findProjects(parent).map((dir) => tourRow(dir, today, idle));
|
|
93
|
+
}
|
|
94
|
+
const DRIFT_SHOWN = 2;
|
|
95
|
+
/** Une ligne par projet : projet · en cours · dérive · notes · prochain lot prêt · dépôt. */
|
|
96
|
+
export function tourLine(r) {
|
|
97
|
+
if (r.error)
|
|
98
|
+
return `${r.project} · ✗ erreur : ${r.error}`;
|
|
99
|
+
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';
|
|
100
|
+
const drift = r.drift.length
|
|
101
|
+
? `${r.drift.length} (${r.drift.slice(0, DRIFT_SHOWN).join(' ; ')}${r.drift.length > DRIFT_SHOWN ? ' ; …' : ''})`
|
|
102
|
+
: 'aucune';
|
|
103
|
+
const notes = r.notes.length ? truncate(r.notes.join(' / '), NOTES_MAX) : 'aucune';
|
|
104
|
+
const next = r.next ? `${r.next.id} ${truncate(r.next.title, TITLE_MAX)}` : 'aucun';
|
|
105
|
+
return [r.project, `en cours ${doing}`, `dérive ${drift}`, `notes : ${notes}`, `prochain ${next}`, `dépôt ${r.repo.length ? r.repo.join(', ') : 'propre'}`].join(' · ');
|
|
106
|
+
}
|
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.15.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
|
|
@@ -8,12 +8,13 @@ description: Close a work session on a repository that uses cadence — plan hyg
|
|
|
8
8
|
## Where to run
|
|
9
9
|
|
|
10
10
|
Every `cadence` / `raf` command works on the git repository of the current directory. When the
|
|
11
|
-
session runs from a parent folder that holds several projects (not itself a repository), run
|
|
12
|
-
|
|
13
|
-
that
|
|
11
|
+
session runs from a parent folder that holds several projects (not itself a repository), run
|
|
12
|
+
`cadence session close --all` there (`--depth 2` for projects one folder deeper): one "## project"
|
|
13
|
+
section per sub-folder that contains `docs/plan/raf.yaml`, or a `cadence.yaml` with a `plan:` key.
|
|
14
|
+
Other commands (`raf …`, `cadence deliver`) still run inside the project concerned: `cd <project> && cadence …`.
|
|
14
15
|
|
|
15
|
-
With no project named, run `cadence session close` in each project touched during the session
|
|
16
|
-
(`git log --since` or `git status` tell which) and close each one.
|
|
16
|
+
With no project named, run `cadence session close --all` from the parent folder, or `cadence session close` in each project touched during the session
|
|
17
|
+
(`git log --since` or `git status` tell which), and close each one.
|
|
17
18
|
|
|
18
19
|
## Steps
|
|
19
20
|
|
|
@@ -12,11 +12,12 @@ proposes, and stops.
|
|
|
12
12
|
## Where to run
|
|
13
13
|
|
|
14
14
|
Every `cadence` / `raf` command works on the git repository of the current directory. When the
|
|
15
|
-
session runs from a parent folder that holds several projects (not itself a repository), run
|
|
16
|
-
|
|
17
|
-
that
|
|
15
|
+
session runs from a parent folder that holds several projects (not itself a repository), run
|
|
16
|
+
`cadence session start --all` there (`--depth 2` for projects one folder deeper): one "## project"
|
|
17
|
+
section per sub-folder that contains `docs/plan/raf.yaml`, or a `cadence.yaml` with a `plan:` key.
|
|
18
|
+
Other commands (`raf …`, `cadence deliver`) still run inside the project concerned: `cd <project> && cadence …`.
|
|
18
19
|
|
|
19
|
-
With no project named, run `cadence session start` in **each** project that has a plan and report
|
|
20
|
+
With no project named, run `cadence session start --all` from the parent folder (or `cadence session start` in **each** project that has a plan) and report
|
|
20
21
|
one or two lines per project (in progress, drift, notes left at the last close), then propose the
|
|
21
22
|
three most useful items across projects and wait. With a project named, work in that one only.
|
|
22
23
|
|