@sylad/cadence 1.40.0 → 1.42.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/agents/spec-writer.md +1 -0
- package/agents/ux-reviewer.md +2 -0
- package/dist/archive.js +103 -6
- package/dist/audit.js +2 -2
- package/dist/clean.js +0 -4
- package/dist/cli/context.js +129 -0
- package/dist/cli/deliver-commands.js +114 -0
- package/dist/cli/gates.js +234 -0
- package/dist/cli/lead-commands.js +161 -0
- package/dist/cli/lot-commands.js +257 -0
- package/dist/cli/read-commands.js +227 -0
- package/dist/cli/session-commands.js +152 -0
- package/dist/cli/setup-commands.js +72 -0
- package/dist/cli/spec-commands.js +298 -0
- package/dist/cli/stats-commands.js +138 -0
- package/dist/cli-add.js +37 -0
- package/dist/cli.js +85 -1669
- package/dist/config.js +21 -108
- package/dist/corrections.js +28 -42
- package/dist/deliver-lots.js +37 -0
- package/dist/deliver.js +15 -39
- package/dist/docs-ok-file.js +25 -0
- package/dist/docsync.js +15 -3
- package/dist/lead.js +6 -24
- package/dist/lot-lines.js +51 -0
- package/dist/orchestrate/attente.js +82 -0
- package/dist/orchestrate/command.js +26 -25
- package/dist/orchestrate/continue.js +6 -14
- package/dist/orchestrate/cycle.js +18 -27
- package/dist/orchestrate/integrate.js +7 -2
- package/dist/orchestrate/integration-vivante.js +138 -0
- package/dist/orchestrate/launch.js +6 -18
- package/dist/orchestrate/lock.js +3 -20
- package/dist/orchestrate/lot-budget.js +2 -2
- package/dist/orchestrate/maquette.js +3 -0
- package/dist/orchestrate/registry.js +8 -1
- package/dist/orchestrate/schemas.js +5 -1
- package/dist/orchestrate/snapshot.js +13 -0
- package/dist/orchestrate/spec.js +51 -6
- package/dist/orchestrate/state.js +17 -0
- package/dist/plan-extras.js +119 -0
- package/dist/plan-ids.js +95 -0
- package/dist/plan-read.js +146 -0
- package/dist/plan-states.js +119 -0
- package/dist/plan.js +119 -190
- package/dist/raf-close-checks.js +25 -0
- package/dist/raf-status.js +28 -0
- package/dist/ref/command.js +2 -3
- package/dist/resources.js +3 -9
- package/dist/state.js +31 -8
- package/dist/verify-gate.js +33 -2
- package/dist/verify.js +20 -3
- package/dist/yaml-keys.js +21 -0
- package/docs/cadence-hud.md +1 -1
- package/docs/deliver.md +25 -3
- package/docs/fonctionnement.md +9 -5
- package/docs/lead.md +9 -7
- package/docs/news.md +2 -0
- package/docs/orchestrate.md +24 -7
- package/docs/perf.md +1 -0
- package/docs/qualite.md +14 -4
- package/docs/raf.md +62 -7
- package/docs/spec.md +5 -2
- package/docs/whats-new.md +9 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
- package/templates/orchestrate/spec.md +2 -2
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Les commandes qui lisent le plan et l'historique : commits, show, list, now, check, gantt, news (L488, sorti de cli.ts).
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { readOrchestrateConfig } from '../config.js';
|
|
5
|
+
import { repoSections } from '../repos.js';
|
|
6
|
+
import { lotJson, lotLines } from '../lot-lines.js';
|
|
7
|
+
import { audit, gateWaiting, hypothesesOf, lotWork, nextUp, planCommits, workByLot } from '../audit.js';
|
|
8
|
+
import { short } from '../check.js';
|
|
9
|
+
import { toDay } from '../dates.js';
|
|
10
|
+
import { ganttData, renderGantt } from '../gantt.js';
|
|
11
|
+
import { linkCommits } from '../link.js';
|
|
12
|
+
import { buildNews, loadEntries, newEntry, newsData, newsIssues, stampEntries } from '../news.js';
|
|
13
|
+
import { RafError, STATUSES } from '../plan.js';
|
|
14
|
+
import { dueLine, recurringByDue } from '../recurring.js';
|
|
15
|
+
import { schedule } from '../schedule.js';
|
|
16
|
+
import { coverageCommand } from '../ref/coverage.js';
|
|
17
|
+
import { knownLot } from './context.js';
|
|
18
|
+
function describe(l, commits) {
|
|
19
|
+
const bits = [`${l.estimate} j`];
|
|
20
|
+
if (commits)
|
|
21
|
+
bits.push(`${commits} commit(s)`);
|
|
22
|
+
if (l.tasks.length)
|
|
23
|
+
bits.push(`sous-tâches ${l.tasks.filter((t) => t.status === 'done').length}/${l.tasks.length}`);
|
|
24
|
+
return `${l.quickwin ? '⚡ ' : ''}${l.id} ${l.title} (${bits.join(', ')})`;
|
|
25
|
+
}
|
|
26
|
+
function now(plan, root, newsDir, today, io) {
|
|
27
|
+
const lots = plan.lots();
|
|
28
|
+
const linked = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
29
|
+
const { doing, ready, blocked } = nextUp(lots);
|
|
30
|
+
const work = workByLot(plan, root, linked, [...doing, ...ready.slice(0, 5)].map((l) => l.id));
|
|
31
|
+
const count = (id) => work.get(id)?.length ?? 0;
|
|
32
|
+
io.out(`${plan.project} — ${today}`);
|
|
33
|
+
io.out('\nEn cours');
|
|
34
|
+
if (doing.length === 0)
|
|
35
|
+
io.out(' (rien)');
|
|
36
|
+
for (const l of doing)
|
|
37
|
+
io.out(` ${describe(l, count(l.id))}`);
|
|
38
|
+
io.out('\nÀ suivre');
|
|
39
|
+
if (ready.length === 0)
|
|
40
|
+
io.out(' (rien de prêt)');
|
|
41
|
+
for (const l of ready.slice(0, 5))
|
|
42
|
+
io.out(` ${describe(l, count(l.id))}`);
|
|
43
|
+
if (ready.length > 5)
|
|
44
|
+
io.out(` … et ${ready.length - 5} autre(s)`);
|
|
45
|
+
const gated = gateWaiting(lots, blocked);
|
|
46
|
+
const byDeps = blocked.filter((l) => !gated.some((g) => g.lot === l));
|
|
47
|
+
if (byDeps.length)
|
|
48
|
+
io.out(` en attente de dépendances : ${byDeps.map((l) => l.id).join(', ')}`);
|
|
49
|
+
if (gated.length)
|
|
50
|
+
io.out(` en attente de condition de tirage : ${gated.map((g) => `${g.lot.id} (${g.text})`).join(', ')}`);
|
|
51
|
+
const recurring = recurringByDue(lots, today);
|
|
52
|
+
if (recurring.length) {
|
|
53
|
+
io.out('\nRécurrent');
|
|
54
|
+
for (const l of recurring)
|
|
55
|
+
io.out(` ${l.id} ${l.title} (tous les ${l.every} j, ${dueLine(l, today)})`);
|
|
56
|
+
}
|
|
57
|
+
const done = lots
|
|
58
|
+
.filter((l) => l.status === 'done' && l.finished)
|
|
59
|
+
.sort((a, b) => (b.finished > a.finished ? 1 : -1))
|
|
60
|
+
.slice(0, 3);
|
|
61
|
+
if (done.length) {
|
|
62
|
+
io.out('\nTerminés récemment');
|
|
63
|
+
for (const l of done)
|
|
64
|
+
io.out(` ${l.id} ${l.title} (${l.finished})`);
|
|
65
|
+
}
|
|
66
|
+
const issues = audit(plan, root, newsDir, today).filter((i) => !i.warning);
|
|
67
|
+
if (issues.length)
|
|
68
|
+
io.out(`\n${issues.length} écart(s) entre le plan et l'historique — raf check`);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
function news([sub, ...args], plan, dir, today, values, io, refuseLongTitle, hypotheses) {
|
|
72
|
+
const lots = plan.lots();
|
|
73
|
+
switch (sub) {
|
|
74
|
+
case 'new': {
|
|
75
|
+
if (args.length === 0)
|
|
76
|
+
throw new RafError('usage : cadence news new <lot…> [--title t]');
|
|
77
|
+
for (const id of args)
|
|
78
|
+
plan.lot(id);
|
|
79
|
+
// Mesuré comme newEntry l'écrit : espaces fusionnés.
|
|
80
|
+
const title = (values.title ?? plan.lot(args[0]).public ?? plan.lot(args[0]).title).replace(/\s+/g, ' ').trim();
|
|
81
|
+
refuseLongTitle(title, values.title === undefined ? ' — donner un titre court : cadence news new <lot> --title "…"' : '');
|
|
82
|
+
io.out(newEntry(dir, args, title, today, io.now()));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
case 'stamp': {
|
|
86
|
+
const stamped = stampEntries(dir, io.now());
|
|
87
|
+
for (const s of stamped)
|
|
88
|
+
io.out(`${s.file} created: ${s.created}`);
|
|
89
|
+
if (stamped.length === 0)
|
|
90
|
+
io.out('✓ toutes les entrées ont une heure de création');
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
case 'list':
|
|
94
|
+
for (const e of loadEntries(dir))
|
|
95
|
+
io.out(`${e.date} ${e.title} (${e.lots.join(', ')})`);
|
|
96
|
+
return 0;
|
|
97
|
+
case 'check':
|
|
98
|
+
case 'build': {
|
|
99
|
+
const entries = loadEntries(dir);
|
|
100
|
+
const issues = newsIssues(lots, entries, dir, sub === 'check' ? hypotheses() : undefined);
|
|
101
|
+
for (const i of issues)
|
|
102
|
+
io.out(`✗ ${i.message}`);
|
|
103
|
+
// Le build refuse seulement ce qu'il ne saurait pas publier : entrée illisible, capture absente.
|
|
104
|
+
const blocking = sub === 'check' ? issues : issues.filter((i) => i.kind === 'bad-entry' || i.kind === 'missing-capture');
|
|
105
|
+
if (sub === 'check')
|
|
106
|
+
io.out(issues.length === 0 ? '✓ Nouveautés cohérentes avec le plan' : `${issues.length} écart(s)`);
|
|
107
|
+
if (blocking.length > 0)
|
|
108
|
+
return 1;
|
|
109
|
+
if (sub === 'build') {
|
|
110
|
+
const stamp = io.now();
|
|
111
|
+
const data = newsData(plan.project, entries, `${toDay(stamp)} ${stamp.toTimeString().slice(0, 5)}`);
|
|
112
|
+
for (const f of buildNews(data, dir, resolve(io.cwd, values.output ?? join(dir, 'site'))))
|
|
113
|
+
io.out(f);
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
throw new RafError('usage : cadence news new|list|check|stamp|build');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** raf commits <lot> : les commits que compte la porte de revue. */
|
|
122
|
+
export function cmdCommits(c) {
|
|
123
|
+
const { rest, io, root, loadPlan, need } = c;
|
|
124
|
+
need(1, 'commits <lot>');
|
|
125
|
+
const plan = loadPlan();
|
|
126
|
+
const id = rest[0];
|
|
127
|
+
knownLot(plan, id, 'les commits se listent par lot, pas par sous-tâche');
|
|
128
|
+
// Une lecture : le même ensemble que « raf done » et « raf check », plan en lecture seule compris.
|
|
129
|
+
for (const c of lotWork(plan, root, id).reverse())
|
|
130
|
+
io.out(short(c));
|
|
131
|
+
for (const line of repoSections(plan, root, plan.lot(id)))
|
|
132
|
+
io.out(line);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
/** raf show <lot> [--notes]. */
|
|
136
|
+
export function cmdShow(c) {
|
|
137
|
+
const { rest, values, io, root, loadPlan, need } = c;
|
|
138
|
+
need(1, 'show <lot> [--notes]');
|
|
139
|
+
const plan = loadPlan();
|
|
140
|
+
const lot = knownLot(plan, rest[0], 'show se lit par lot, pas par sous-tâche');
|
|
141
|
+
const notes = lot.notes.map((n) => `${n.date} ${n.text}`);
|
|
142
|
+
if (values.notes) {
|
|
143
|
+
for (const n of notes)
|
|
144
|
+
io.out(n);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
for (const line of lotLines(plan, lot))
|
|
148
|
+
io.out(line);
|
|
149
|
+
if (notes.length) {
|
|
150
|
+
io.out('notes :');
|
|
151
|
+
// Les lignes suivantes d'une note multiligne s'alignent sous son texte, pas en colonne 0.
|
|
152
|
+
for (const n of lot.notes)
|
|
153
|
+
io.out(` ${n.date} ${n.text.replace(/\n/g, `\n${' '.repeat(n.date.length + 4)}`)}`);
|
|
154
|
+
}
|
|
155
|
+
const commits = lotWork(plan, root, lot.id).reverse();
|
|
156
|
+
io.out(`commits (${commits.length}) :`);
|
|
157
|
+
for (const c of commits)
|
|
158
|
+
io.out(` ${short(c)}`);
|
|
159
|
+
for (const line of repoSections(plan, root, lot, ' '))
|
|
160
|
+
io.out(line);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
/** raf list [--status s] [--json]. */
|
|
164
|
+
export function cmdList(c) {
|
|
165
|
+
const { values, io, loadPlan } = c;
|
|
166
|
+
const plan = loadPlan();
|
|
167
|
+
if (values.status && !STATUSES.includes(values.status))
|
|
168
|
+
throw new RafError(`statut inconnu : ${values.status}`);
|
|
169
|
+
const shown = plan.lots().filter((l) => !values.status || l.status === values.status);
|
|
170
|
+
if (values.json)
|
|
171
|
+
io.out(JSON.stringify(shown.map(lotJson), null, 2));
|
|
172
|
+
else
|
|
173
|
+
for (const l of shown)
|
|
174
|
+
io.out(`${l.id.padEnd(6)} ${l.status.padEnd(8)} ${l.quickwin ? '⚡ ' : ''}${l.title}`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
/** raf check : les écarts entre le plan et l'historique, code 1 s'il y en a. */
|
|
178
|
+
export function cmdCheck(c) {
|
|
179
|
+
const { values, io, today, root, loadPlan, newsDir } = c;
|
|
180
|
+
if (values.ignored) {
|
|
181
|
+
const list = loadPlan().acknowledged;
|
|
182
|
+
if (list.length === 0)
|
|
183
|
+
io.out('aucun commit acquitté');
|
|
184
|
+
for (const a of list)
|
|
185
|
+
io.out(`${a.sha ? a.sha.slice(0, 7) : 'sujet '} ${a.subject ?? ''} (${a.date}${a.reason ? ` — ${a.reason}` : ''})`);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
const idle = values.idle === undefined ? 7 : Number(values.idle);
|
|
189
|
+
if (!Number.isInteger(idle) || idle < 0)
|
|
190
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
191
|
+
const issues = audit(loadPlan(), root, newsDir, today, { since: values.since, idle });
|
|
192
|
+
for (const i of issues)
|
|
193
|
+
io.out(`${i.warning ? '⚠' : '✗'} ${i.message}`);
|
|
194
|
+
// Un avertissement s'affiche mais ne fait pas échouer : seuls les écarts comptent pour le code de sortie.
|
|
195
|
+
const errors = issues.filter((i) => !i.warning).length;
|
|
196
|
+
const warnings = issues.length - errors;
|
|
197
|
+
io.out(errors === 0 ? `✓ plan et historique cohérents${warnings ? ` (${warnings} avertissement(s))` : ''}` : `${errors} écart(s)${warnings ? `, ${warnings} avertissement(s)` : ''}`);
|
|
198
|
+
return errors === 0 ? 0 : 1;
|
|
199
|
+
}
|
|
200
|
+
/** raf gantt [-o fichier] : la page Gantt du plan. */
|
|
201
|
+
export function cmdGantt(c) {
|
|
202
|
+
const { values, io, today, root, planPath, loadPlan } = c;
|
|
203
|
+
const plan = loadPlan();
|
|
204
|
+
const lots = plan.lots();
|
|
205
|
+
const linked = linkCommits(lots, planCommits(plan, root), plan.refs);
|
|
206
|
+
const stamp = io.now();
|
|
207
|
+
const html = renderGantt(ganttData(plan.project, schedule(lots, workByLot(plan, root, linked), today), today, `${toDay(stamp)} ${stamp.toTimeString().slice(0, 5)}`));
|
|
208
|
+
const out = resolve(io.cwd, values.output ?? join(dirname(planPath), 'gantt.html'));
|
|
209
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
210
|
+
writeFileSync(out, html);
|
|
211
|
+
io.out(out);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
/** raf now : ce qui est en cours, la suite, les derniers terminés. */
|
|
215
|
+
export function cmdNow(c) {
|
|
216
|
+
return now(c.loadPlan(), c.root, c.newsDir, c.today, c.io);
|
|
217
|
+
}
|
|
218
|
+
/** raf news … : les entrées Nouveautés. */
|
|
219
|
+
export function cmdNews(c) {
|
|
220
|
+
const { rest, values, io, today, root, configPath, loadPlan, newsDir, refuseLongTitle } = c;
|
|
221
|
+
return news(rest, loadPlan(), newsDir, today, values, io, refuseLongTitle, () => hypothesesOf(root, configPath));
|
|
222
|
+
}
|
|
223
|
+
/** cadence ref coverage lit le plan (les lots faits qui citent les éléments) : il passe après le choix du plan, les autres ref n'y touchent pas (L367). */
|
|
224
|
+
export function cmdRef(c) {
|
|
225
|
+
const { rest, io, root, configPath, loadPlan } = c;
|
|
226
|
+
return coverageCommand(rest.slice(1), loadPlan, io, { repo: root, specDir: readOrchestrateConfig(configPath).spec.dir });
|
|
227
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// session start|close|next|context, et leur variante --all (L488, sorti de cli.ts).
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { readSessionConfig } from '../config.js';
|
|
5
|
+
import { parseDeliverConfig } from '../deliver.js';
|
|
6
|
+
import { defaultTarget, effectLines, realCheckDeps } from '../verify.js';
|
|
7
|
+
import { gitRoot } from '../git.js';
|
|
8
|
+
import { RafError } from '../plan.js';
|
|
9
|
+
import { closeMemoryLines } from '../memory-index.js';
|
|
10
|
+
import { findLeadDir, findProjects, readLeadContextThreshold } from '../lead.js';
|
|
11
|
+
import { readHudContext } from '../hud-context.js';
|
|
12
|
+
import { sessionClose, sessionStart } from '../session.js';
|
|
13
|
+
import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from '../state.js';
|
|
14
|
+
/**
|
|
15
|
+
* Reprise : lignes d'effets à rejouer, ou null quand il n'y a rien à vérifier (sans cadence.yaml, sans deliver,
|
|
16
|
+
* script sans verify) — la reprise reste alors synchrone. Ne lève jamais.
|
|
17
|
+
*/
|
|
18
|
+
function morningEffects(configPath, root) {
|
|
19
|
+
if (!existsSync(configPath))
|
|
20
|
+
return null;
|
|
21
|
+
const text = readFileSync(configPath, 'utf8');
|
|
22
|
+
if (!/^deliver\s*:/m.test(text))
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const config = parseDeliverConfig(text, configPath);
|
|
26
|
+
if (config.verify.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const target = defaultTarget(root, config);
|
|
29
|
+
return effectLines(config, target.sha, realCheckDeps(root, { quiet: true }), target.note);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return [`✗ ${e.message}`];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
|
|
36
|
+
async function sessionAll(rest, values, io, run) {
|
|
37
|
+
const sub = rest[0];
|
|
38
|
+
if (!values.all)
|
|
39
|
+
throw new RafError('--depth demande --all');
|
|
40
|
+
// Chaque projet lit son propre plan : un plan ou une config imposés se retrouveraient dans toutes les sections.
|
|
41
|
+
const imposed = values.file !== undefined ? '--file' : values.config !== undefined ? '--config' : io.env.RAF_FILE !== undefined ? 'RAF_FILE' : null;
|
|
42
|
+
if (imposed)
|
|
43
|
+
throw new RafError(`${imposed} ne va pas avec --all : chaque projet lit son propre plan (lancer la commande dans le projet visé)`);
|
|
44
|
+
if (sub !== 'start' && sub !== 'close')
|
|
45
|
+
throw new RafError('--all ne va qu\'avec session start ou session close');
|
|
46
|
+
const depth = values.depth === undefined ? 1 : Number(values.depth);
|
|
47
|
+
if (!Number.isInteger(depth) || depth < 1)
|
|
48
|
+
throw new RafError(`--depth invalide : ${values.depth} (un entier ≥ 1)`);
|
|
49
|
+
if (values.idle !== undefined && !(Number.isInteger(Number(values.idle)) && Number(values.idle) >= 0))
|
|
50
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
51
|
+
const projects = findProjects(io.cwd, depth);
|
|
52
|
+
if (projects.length === 0) {
|
|
53
|
+
io.out(`aucun projet sous ${io.cwd} (.cadence/raf.yaml, docs/plan/raf.yaml ou cadence.yaml avec plan:)`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const argv = ['session', sub, ...(values.since === undefined ? [] : [`--since=${values.since}`]), ...(values.idle === undefined ? [] : ['--idle', values.idle])];
|
|
57
|
+
let worst = 0;
|
|
58
|
+
for (const dir of projects) {
|
|
59
|
+
io.out(`${dir === projects[0] ? '' : '\n'}## ${relative(io.cwd, dir)}`);
|
|
60
|
+
let code;
|
|
61
|
+
const top = gitRoot(dir);
|
|
62
|
+
if (top && realpathSync(top) !== realpathSync(dir)) {
|
|
63
|
+
// run() lirait le plan du dépôt parent : mieux vaut le dire que montrer le plan d'un autre projet.
|
|
64
|
+
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`);
|
|
65
|
+
worst = 2;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
code = await run(argv, { ...io, cwd: dir, err: (l) => io.out(`✗ ${l}`), notice: io.notice ?? io.err });
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
io.out(`✗ ${e instanceof Error ? e.message : String(e)}`);
|
|
73
|
+
code = 2;
|
|
74
|
+
}
|
|
75
|
+
worst = Math.max(worst, code);
|
|
76
|
+
}
|
|
77
|
+
return worst;
|
|
78
|
+
}
|
|
79
|
+
/** Le contexte (et sa position par rapport à `lead.contextThreshold`, en tokens) de la session de `--session <id>`, sinon de `CLAUDE_CODE_SESSION_ID`, sinon (variable absente) de la seule session du dossier courant, tel que la bande cadence-hud le publie ; illisible = code 2, à lire comme « au seuil ». */
|
|
80
|
+
function sessionContext(io, session) {
|
|
81
|
+
const r = readHudContext(io.env, io.now().getTime(), { cwd: io.cwd, session });
|
|
82
|
+
if (!r.ok)
|
|
83
|
+
throw new RafError(`session context : contexte illisible — ${r.reason} ; à traiter comme au seuil`);
|
|
84
|
+
io.out(`ctx ${Math.round(r.ctx.percent)} % (${r.ctx.tokens}/${r.ctx.window})`);
|
|
85
|
+
// Lancée d'un sous-projet, la commande lit le cadence.yaml du dossier du lead (L457) et nomme le dossier lu.
|
|
86
|
+
const leadDir = findLeadDir(io.cwd);
|
|
87
|
+
const threshold = readLeadContextThreshold(leadDir);
|
|
88
|
+
io.out(`seuil ${threshold} tokens : ${r.ctx.tokens >= threshold ? 'atteint' : 'sous le seuil'}${leadDir === io.cwd ? '' : ` (lu dans ${join(leadDir, 'cadence.yaml')})`}`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
function session([sub, ...args], ctx, values) {
|
|
92
|
+
switch (sub) {
|
|
93
|
+
case 'start': {
|
|
94
|
+
const idle = values.idle === undefined ? 2 : Number(values.idle);
|
|
95
|
+
if (!Number.isInteger(idle) || idle < 0)
|
|
96
|
+
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
97
|
+
return sessionStart(ctx, { since: values.since ?? '24 hours ago', idle });
|
|
98
|
+
}
|
|
99
|
+
case 'close':
|
|
100
|
+
return sessionClose(ctx, { since: values.since });
|
|
101
|
+
case 'next': {
|
|
102
|
+
const lines = args.filter((l) => l.trim() !== '');
|
|
103
|
+
if (values.clear) {
|
|
104
|
+
if (lines.length)
|
|
105
|
+
throw new RafError('session next --clear efface les notes : ne pas lui passer de ligne');
|
|
106
|
+
const gone = clearNext(ctx.state);
|
|
107
|
+
ctx.out(gone ? `notes effacées (${gone.lines.length} ligne(s) du ${gone.date})` : 'aucune note à effacer');
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
// Lancée sans ligne (variable vide dans un script, agent pressé), la commande effaçait en silence
|
|
111
|
+
// les notes de la dernière clôture : effacer se demande exprès.
|
|
112
|
+
if (lines.length === 0) {
|
|
113
|
+
const kept = readNext(ctx.state);
|
|
114
|
+
throw new RafError(`session next : aucune ligne — ${kept ? `${kept.lines.length} ligne(s) du ${kept.date} conservée(s)` : "rien n'est écrit"} ; ` +
|
|
115
|
+
'usage : cadence session next "ligne" … (pour effacer les notes exprès : cadence session next --clear)');
|
|
116
|
+
}
|
|
117
|
+
writeNext(ctx.state, ctx.today, lines);
|
|
118
|
+
// Rien sur stdout laissait croire à un échec : on redit les lignes telles qu'enregistrées, pour que
|
|
119
|
+
// session-close les rapporte sans relancer session start.
|
|
120
|
+
const n = lines.length;
|
|
121
|
+
ctx.out(`${n} ligne${n > 1 ? 's' : ''} enregistrée${n > 1 ? 's' : ''} pour la prochaine ouverture\n${lines.map((l) => ` - ${l}`).join('\n')}`);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
default:
|
|
125
|
+
throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | start|close --all [--depth n] | next "ligne" … | next --clear | context [--session id]');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** cadence session start|close|next|context. */
|
|
129
|
+
export function cmdSession(c) {
|
|
130
|
+
const { rest, values, io, today, root, configPath, loadPlan, newsDir } = c;
|
|
131
|
+
// Le contexte de la session ne dépend d'aucun dépôt : le lead le lit depuis le dossier parent.
|
|
132
|
+
if (rest[0] === 'context')
|
|
133
|
+
return sessionContext(io, values.session);
|
|
134
|
+
if (!gitRoot(io.cwd))
|
|
135
|
+
throw new RafError('session : à lancer dans un dépôt git');
|
|
136
|
+
// « next » n'écrit que les notes : la commande du projet ne se joue qu'à la reprise et à la clôture.
|
|
137
|
+
const sconf = rest[0] === 'start' || rest[0] === 'close' ? readSessionConfig(configPath) : {};
|
|
138
|
+
const facts = rest[0] === 'start' || rest[0] === 'close' ? sconf[rest[0]] : undefined;
|
|
139
|
+
const clean = rest[0] === 'close' && sconf.clean ? { patterns: sconf.clean, days: sconf.cleanDays } : undefined;
|
|
140
|
+
const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, now: io.now(), out: io.out, facts, clean, memory: closeMemoryLines(io.env, io.cwd, configPath) };
|
|
141
|
+
if (rest[0] !== 'start')
|
|
142
|
+
return session(rest, sctx, values);
|
|
143
|
+
// La reprise rejoue les vérifications d'effet (un essai, borné) : un effet rouge d'hier se lit avec les faits du matin.
|
|
144
|
+
const effects = morningEffects(configPath, root);
|
|
145
|
+
if (effects === null)
|
|
146
|
+
return session(rest, sctx, values);
|
|
147
|
+
return Promise.resolve(effects).then((lines) => session(rest, { ...sctx, effects: lines }, values));
|
|
148
|
+
}
|
|
149
|
+
/** `--all` balaie les sous-dossiers : il ne lit ni le plan ni le cadence.yaml du dossier courant ; null sans --all ni --depth. */
|
|
150
|
+
export function cmdSessionAll(c, run) {
|
|
151
|
+
return c.values.all || c.values.depth !== undefined ? sessionAll(c.rest, c.values, c.io, run) : null;
|
|
152
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Installer et déplacer : init, skills, migrate (L488, sorti de cli.ts).
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import { gitRoot } from '../git.js';
|
|
5
|
+
import { installHook } from '../hook.js';
|
|
6
|
+
import { Plan, RafError } from '../plan.js';
|
|
7
|
+
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from '../skills.js';
|
|
8
|
+
import { applyMigration, describeMigration, planMigration, unhandledError } from '../migrate.js';
|
|
9
|
+
/** Ajoute `line` au .gitignore de `root` s'il n'y est pas déjà (avec ou sans `/` initial ou final). Rend vrai si écrit. */
|
|
10
|
+
export function ensureGitignore(root, line) {
|
|
11
|
+
const file = join(root, '.gitignore');
|
|
12
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
13
|
+
const norm = (l) => l.trim().replace(/^\//, '').replace(/\/$/, '');
|
|
14
|
+
if (text.split('\n').some((l) => norm(l) === norm(line)))
|
|
15
|
+
return false;
|
|
16
|
+
writeFileSync(file, `${text}${text === '' || text.endsWith('\n') ? '' : '\n'}${line}\n`);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
/** raf init : crée le plan, le dossier des notes, le .gitignore et le hook. */
|
|
20
|
+
export function cmdInit(c) {
|
|
21
|
+
const { values, io, today, root, configPath, planConfig, planPath, chosen } = c;
|
|
22
|
+
if (planConfig?.settings.format)
|
|
23
|
+
throw new RafError(`plan en lecture seule : ${configPath} décrit un plan tenu par un autre outil`);
|
|
24
|
+
const plan = Plan.create(planPath, values.project ?? basename(root), values.prefix ?? 'L', today);
|
|
25
|
+
io.out(`plan créé : ${plan.path}`);
|
|
26
|
+
if (chosen === undefined) {
|
|
27
|
+
mkdirSync(join(root, '.cadence/notes'), { recursive: true });
|
|
28
|
+
writeFileSync(join(root, '.cadence/notes/.gitkeep'), '');
|
|
29
|
+
}
|
|
30
|
+
if (ensureGitignore(root, '.playwright-mcp/'))
|
|
31
|
+
io.out('.gitignore : .playwright-mcp/');
|
|
32
|
+
if (!values['no-hook'] && gitRoot(io.cwd))
|
|
33
|
+
io.out(`hook : ${installHook(io.cwd).path}`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
/** cadence skills install : skills et agents dans le dossier .claude. */
|
|
37
|
+
export function cmdSkills(c) {
|
|
38
|
+
const { rest, values, io, root } = c;
|
|
39
|
+
if (rest[0] !== 'install')
|
|
40
|
+
throw new RafError('usage : cadence skills install [--dir .claude] [--force]');
|
|
41
|
+
// --dir désigne le dossier .claude (défaut : celui du dépôt) : skills/ et agents/ dedans.
|
|
42
|
+
const base = resolve(io.cwd, values.dir ?? join(root, '.claude'));
|
|
43
|
+
const label = { installed: 'installé', updated: 'mis à jour', unchanged: 'inchangé' };
|
|
44
|
+
const force = !!values.force;
|
|
45
|
+
// Vérifier les deux avant d'écrire : un conflit d'agent ne doit pas laisser les skills à moitié installés.
|
|
46
|
+
if (!force) {
|
|
47
|
+
installSkills(SKILLS_DIR, join(base, 'skills'), false, true);
|
|
48
|
+
installAgents(AGENTS_DIR, join(base, 'agents'), false, true);
|
|
49
|
+
}
|
|
50
|
+
for (const r of installSkills(SKILLS_DIR, join(base, 'skills'), force))
|
|
51
|
+
io.out(`${r.name} : ${label[r.status]}`);
|
|
52
|
+
for (const r of installAgents(AGENTS_DIR, join(base, 'agents'), force))
|
|
53
|
+
io.out(`${r.name} (agent) : ${label[r.status]}`);
|
|
54
|
+
io.out(base);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
/** Déplacer le plan ne lit pas le plan : pas d'avertissement « ancien emplacement » (L284). */
|
|
58
|
+
export function cmdMigrate(c) {
|
|
59
|
+
const { rest, values, io, root } = c;
|
|
60
|
+
if (rest.length > 0)
|
|
61
|
+
throw new RafError('usage : raf migrate [--dry-run]');
|
|
62
|
+
const migration = planMigration(root);
|
|
63
|
+
// --dry-run décrit ce qui serait fait avant de refuser ; sans lui, un refus n'annonce rien de fait.
|
|
64
|
+
if (values['dry-run'] || migration.unhandled.length === 0)
|
|
65
|
+
for (const l of describeMigration(migration, !!values['dry-run']))
|
|
66
|
+
io.out(l);
|
|
67
|
+
if (migration.unhandled.length > 0)
|
|
68
|
+
throw unhandledError(migration);
|
|
69
|
+
if (!values['dry-run'])
|
|
70
|
+
applyMigration(root, migration);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|