@sylad/cadence 0.2.0 → 0.6.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 +2 -2
- package/README.md +273 -12
- package/agents/code-reviewer.md +89 -0
- package/agents/qa-reviewer.md +122 -0
- package/bin/cadence.js +3 -2
- package/dist/audit.js +95 -5
- package/dist/check.js +2 -1
- package/dist/cli.js +100 -46
- package/dist/config.js +106 -0
- package/dist/dates.js +8 -0
- package/dist/deliver.js +87 -29
- package/dist/git.js +34 -1
- package/dist/link.js +3 -3
- package/dist/news.js +82 -9
- package/dist/plan.js +203 -19
- package/dist/session.js +36 -6
- package/package.json +3 -2
- package/skills/deliver/SKILL.md +32 -1
- package/skills/lead/SKILL.md +94 -0
- package/skills/session-close/SKILL.md +17 -0
- package/skills/session-start/SKILL.md +25 -1
package/dist/plan.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isDay, toDay } from './dates.js';
|
|
|
5
5
|
export const STATUSES = ['todo', 'doing', 'done', 'dropped'];
|
|
6
6
|
export class RafError extends Error {
|
|
7
7
|
}
|
|
8
|
+
export const FIELDS = ['id', 'title', 'status', 'estimate', 'quickwin', 'visible', 'after', 'created', 'started', 'finished', 'notes', 'parent'];
|
|
8
9
|
export function isOpen(status) {
|
|
9
10
|
return status === 'todo' || status === 'doing';
|
|
10
11
|
}
|
|
@@ -33,12 +34,24 @@ export class Plan {
|
|
|
33
34
|
path;
|
|
34
35
|
doc;
|
|
35
36
|
indentSeq;
|
|
37
|
+
settings;
|
|
36
38
|
constructor(path, doc,
|
|
37
39
|
/** false quand le fichier écrit à la main met les « - » en colonne de la clé parente. */
|
|
38
|
-
indentSeq) {
|
|
40
|
+
indentSeq, settings = {}) {
|
|
39
41
|
this.path = path;
|
|
40
42
|
this.doc = doc;
|
|
41
43
|
this.indentSeq = indentSeq;
|
|
44
|
+
this.settings = settings;
|
|
45
|
+
}
|
|
46
|
+
/** Un plan lu dans un autre format ne change pas : sa lecture et le motif de ses références sont gardés. */
|
|
47
|
+
foreign;
|
|
48
|
+
get readonly() {
|
|
49
|
+
return !!this.settings.format;
|
|
50
|
+
}
|
|
51
|
+
writable() {
|
|
52
|
+
if (this.readonly) {
|
|
53
|
+
throw new RafError(`plan en lecture seule : ${this.path} est tenu par un autre outil (cadence.yaml : plan) — le modifier avec l'outil du projet`);
|
|
54
|
+
}
|
|
42
55
|
}
|
|
43
56
|
static create(path, project, prefix = 'L', since) {
|
|
44
57
|
if (existsSync(path))
|
|
@@ -49,7 +62,7 @@ export class Plan {
|
|
|
49
62
|
writeFileSync(path, initialContent(project, prefix, since ?? toDay(new Date())));
|
|
50
63
|
return Plan.load(path);
|
|
51
64
|
}
|
|
52
|
-
static load(path) {
|
|
65
|
+
static load(path, settings = {}) {
|
|
53
66
|
if (!existsSync(path))
|
|
54
67
|
throw new RafError(`pas de plan à ${path} — lancer « raf init »`);
|
|
55
68
|
const text = readFileSync(path, 'utf8');
|
|
@@ -58,27 +71,90 @@ export class Plan {
|
|
|
58
71
|
throw new RafError(`${path} : ${doc.errors[0].message}`);
|
|
59
72
|
if (!isMap(doc.contents))
|
|
60
73
|
throw new RafError(`${path} : la racine doit être un objet`);
|
|
74
|
+
if (settings.format) {
|
|
75
|
+
const key = settings.format.lots;
|
|
76
|
+
if (!isSeq(doc.get(key)))
|
|
77
|
+
throw new RafError(`${path} : la clé « ${key} » doit porter la liste des lots (cadence.yaml : plan.lots)`);
|
|
78
|
+
return new Plan(path, doc, true, settings);
|
|
79
|
+
}
|
|
61
80
|
const lots = doc.get('lots');
|
|
62
81
|
if (!isSeq(lots))
|
|
63
82
|
doc.set('lots', doc.createNode([]));
|
|
64
83
|
else
|
|
65
84
|
lots.flow = false; // « lots: [] » écrit par init : passer en style bloc
|
|
66
|
-
return new Plan(path, doc, !/^lots:[^\n]*\n(?:#[^\n]*\n)*- /m.test(text));
|
|
85
|
+
return new Plan(path, doc, !/^lots:[^\n]*\n(?:#[^\n]*\n)*- /m.test(text), settings);
|
|
67
86
|
}
|
|
68
87
|
save() {
|
|
88
|
+
this.writable();
|
|
69
89
|
writeFileSync(this.path, this.doc.toString({ lineWidth: 0, indentSeq: this.indentSeq }));
|
|
70
90
|
}
|
|
71
91
|
get project() {
|
|
72
|
-
return String(this.doc.get('project') ?? '');
|
|
92
|
+
return String(this.doc.get('project') ?? this.settings.project ?? '');
|
|
73
93
|
}
|
|
74
94
|
get prefix() {
|
|
75
95
|
return String(this.doc.get('prefix') ?? 'L');
|
|
76
96
|
}
|
|
77
97
|
/** Date d'adoption de raf : les commits plus anciens ne sont pas audités. */
|
|
78
98
|
get since() {
|
|
79
|
-
const v = this.doc.get('since');
|
|
99
|
+
const v = this.doc.get('since') ?? this.settings.since;
|
|
80
100
|
return v == null ? undefined : String(v);
|
|
81
101
|
}
|
|
102
|
+
/** Motifs (expressions régulières sur le sujet) des commits automatiques à ne pas auditer. */
|
|
103
|
+
get ignore() {
|
|
104
|
+
const raw = this.doc.get('ignore');
|
|
105
|
+
const own = isSeq(raw) ? raw.toJSON().map(String) : raw == null ? [] : [String(raw)];
|
|
106
|
+
const list = [...own, ...(this.settings.ignore ?? [])];
|
|
107
|
+
const patterns = [];
|
|
108
|
+
const invalid = [];
|
|
109
|
+
for (const src of list) {
|
|
110
|
+
try {
|
|
111
|
+
patterns.push(new RegExp(src));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
invalid.push(src);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { patterns, invalid };
|
|
118
|
+
}
|
|
119
|
+
/** Fichiers tenus avec le plan, relatifs à la racine du dépôt. */
|
|
120
|
+
get files() {
|
|
121
|
+
return this.settings.files ?? [];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Références citées par un texte. Format de raf : préfixe et numéro (`L3`, `L3/t1`). Autre format :
|
|
125
|
+
* les identifiants du plan eux-mêmes, quelle que soit leur forme (`E-A2`, `NC2.4`, `B33/t1-fusion`).
|
|
126
|
+
*/
|
|
127
|
+
refs = (text) => {
|
|
128
|
+
if (!this.settings.format)
|
|
129
|
+
return extractRefs(text, this.prefix);
|
|
130
|
+
const { refs, tasks, ids } = this.read();
|
|
131
|
+
if (!refs)
|
|
132
|
+
return [];
|
|
133
|
+
return [...text.matchAll(refs)].flatMap((m) => {
|
|
134
|
+
if (!m[2])
|
|
135
|
+
return [{ lot: m[1] }];
|
|
136
|
+
if (tasks.has(`${m[1]}/${m[2]}`))
|
|
137
|
+
return [{ lot: m[1], task: m[2] }];
|
|
138
|
+
// « B33/E-A2 » : deux lots séparés par une barre, pas une sous-tâche.
|
|
139
|
+
return ids.has(m[2]) ? [{ lot: m[1] }, { lot: m[2] }] : [{ lot: m[1] }];
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
read() {
|
|
143
|
+
if (!this.foreign) {
|
|
144
|
+
const raw = this.doc.get(this.settings.format.lots).toJSON();
|
|
145
|
+
const lots = foreignLots(raw, this.settings.format);
|
|
146
|
+
const ids = lots.map((l) => l.id).sort((a, b) => b.length - a.length);
|
|
147
|
+
this.foreign = {
|
|
148
|
+
lots,
|
|
149
|
+
ids: new Set(ids),
|
|
150
|
+
tasks: new Set(lots.flatMap((l) => l.tasks.map((t) => `${l.id}/${t.id}`))),
|
|
151
|
+
refs: ids.length
|
|
152
|
+
? new RegExp(`(?<![\\w/.-])(${ids.map(escapeRe).join('|')})(?:/([\\w-]+(?:\\.[\\w-]+)*))?(?![\\w-]|\\.\\w)`, 'g')
|
|
153
|
+
: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return this.foreign;
|
|
157
|
+
}
|
|
82
158
|
/** Date d'activation de la revue UX obligatoire des lots visibles ; absente = règle inactive. */
|
|
83
159
|
get uxSince() {
|
|
84
160
|
const v = this.doc.get('uxSince');
|
|
@@ -86,27 +162,53 @@ export class Plan {
|
|
|
86
162
|
}
|
|
87
163
|
/** Active la revue UX ; false si elle l'était déjà. */
|
|
88
164
|
enableUx(today) {
|
|
89
|
-
|
|
165
|
+
return this.enableGate('uxSince', today);
|
|
166
|
+
}
|
|
167
|
+
recordUx(lotId, verdict, today) {
|
|
168
|
+
this.recordVerdict('ux', 'la revue UX', lotId, { date: today, verdict });
|
|
169
|
+
}
|
|
170
|
+
/** Date d'activation de la revue de code obligatoire des lots à commits ; absente = règle inactive. */
|
|
171
|
+
get reviewSince() {
|
|
172
|
+
const v = this.doc.get('reviewSince');
|
|
173
|
+
return v == null ? undefined : String(v);
|
|
174
|
+
}
|
|
175
|
+
/** Active la revue de code ; false si elle l'était déjà. */
|
|
176
|
+
enableReview(today) {
|
|
177
|
+
return this.enableGate('reviewSince', today);
|
|
178
|
+
}
|
|
179
|
+
/** `commit` : sha du dernier commit compté du lot (le plan ne lit pas git), null s'il n'en a aucun. */
|
|
180
|
+
recordReview(lotId, verdict, today, commit) {
|
|
181
|
+
this.recordVerdict('review', 'la revue de code', lotId, { date: today, verdict, commit });
|
|
182
|
+
}
|
|
183
|
+
enableGate(key, today) {
|
|
184
|
+
this.writable();
|
|
185
|
+
if (this[key])
|
|
90
186
|
return false;
|
|
91
|
-
this.doc.set(
|
|
187
|
+
this.doc.set(key, today);
|
|
92
188
|
// Placer la clé avant « lots » pour garder les réglages groupés en tête.
|
|
93
189
|
const map = this.doc.contents;
|
|
94
|
-
const idx = map.items.findIndex((p) => String(p.key?.value ?? p.key) ===
|
|
190
|
+
const idx = map.items.findIndex((p) => String(p.key?.value ?? p.key) === key);
|
|
95
191
|
const lotsIdx = map.items.findIndex((p) => String(p.key?.value ?? p.key) === 'lots');
|
|
96
192
|
if (idx > lotsIdx && lotsIdx >= 0)
|
|
97
193
|
map.items.splice(lotsIdx, 0, ...map.items.splice(idx, 1));
|
|
98
194
|
return true;
|
|
99
195
|
}
|
|
100
|
-
|
|
196
|
+
recordVerdict(key, label, lotId, entry) {
|
|
197
|
+
this.writable();
|
|
101
198
|
if (lotId.includes('/'))
|
|
102
|
-
throw new RafError(
|
|
103
|
-
|
|
199
|
+
throw new RafError(`${label} se note sur un lot, pas une sous-tâche`);
|
|
200
|
+
// Un verdict vide ouvrirait la porte sans rien dire de la revue.
|
|
201
|
+
if (entry.verdict.trim() === '')
|
|
202
|
+
throw new RafError(`verdict vide : ${label} attend son verdict — raf ${key} ${lotId} "verdict"`);
|
|
203
|
+
const node = this.doc.createNode(entry);
|
|
104
204
|
node.flow = true;
|
|
105
|
-
this.lotNode(lotId).set(
|
|
205
|
+
this.lotNode(lotId).set(key, node);
|
|
106
206
|
}
|
|
107
207
|
lots() {
|
|
208
|
+
if (this.settings.format)
|
|
209
|
+
return this.read().lots;
|
|
108
210
|
const raw = this.doc.get('lots').toJSON();
|
|
109
|
-
return raw.map(normalizeLot);
|
|
211
|
+
return raw.map((r) => normalizeLot(r));
|
|
110
212
|
}
|
|
111
213
|
lot(id) {
|
|
112
214
|
const found = this.lots().find((l) => l.id === id);
|
|
@@ -129,6 +231,7 @@ export class Plan {
|
|
|
129
231
|
return node;
|
|
130
232
|
}
|
|
131
233
|
add(title, today, opts = {}) {
|
|
234
|
+
this.writable();
|
|
132
235
|
const known = new Set(this.lots().map((l) => l.id));
|
|
133
236
|
for (const dep of opts.after ?? [])
|
|
134
237
|
if (!known.has(dep))
|
|
@@ -152,6 +255,7 @@ export class Plan {
|
|
|
152
255
|
return id;
|
|
153
256
|
}
|
|
154
257
|
addTask(lotId, title) {
|
|
258
|
+
this.writable();
|
|
155
259
|
const lot = this.lotNode(lotId);
|
|
156
260
|
let tasks = lot.get('tasks');
|
|
157
261
|
if (!isSeq(tasks)) {
|
|
@@ -166,8 +270,13 @@ export class Plan {
|
|
|
166
270
|
seq.add(node);
|
|
167
271
|
return `${lotId}/${id}`;
|
|
168
272
|
}
|
|
169
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* `ref` is `L3` or `L3/t1`. `unreviewed` : nombre de commits du lot que sa revue de code ne couvre
|
|
275
|
+
* pas (le plan ne lit pas git) — tous sans verdict, ceux postérieurs au commit relu sinon ; sans lui,
|
|
276
|
+
* la revue de code n'est pas exigée.
|
|
277
|
+
*/
|
|
170
278
|
setStatus(ref, status, today, opts = {}) {
|
|
279
|
+
this.writable();
|
|
171
280
|
const [lotId, taskId] = ref.split('/');
|
|
172
281
|
if (taskId) {
|
|
173
282
|
const task = this.taskNode(lotId, taskId);
|
|
@@ -191,6 +300,11 @@ export class Plan {
|
|
|
191
300
|
if (this.uxSince && lot.visible && !lot.ux && !opts.force) {
|
|
192
301
|
throw new RafError(`${lotId} est visible : revue UX attendue avant done — raf ux ${lotId} "verdict" (--force pour passer outre)`);
|
|
193
302
|
}
|
|
303
|
+
if (this.reviewSince && opts.unreviewed && !opts.force) {
|
|
304
|
+
throw new RafError(lot.review
|
|
305
|
+
? `${lotId} : la revue de code précède ${opts.unreviewed} commit(s) du lot, elle est à refaire — raf review ${lotId} "verdict" (--force pour passer outre)`
|
|
306
|
+
: `${lotId} a ${opts.unreviewed} commit(s) : revue de code attendue avant done — raf review ${lotId} "verdict" (--force pour passer outre)`);
|
|
307
|
+
}
|
|
194
308
|
}
|
|
195
309
|
node.set('status', status);
|
|
196
310
|
if (!lot.started && status !== 'dropped')
|
|
@@ -199,6 +313,7 @@ export class Plan {
|
|
|
199
313
|
node.set('finished', today);
|
|
200
314
|
}
|
|
201
315
|
note(ref, text, today) {
|
|
316
|
+
this.writable();
|
|
202
317
|
const [lotId, taskId] = ref.split('/');
|
|
203
318
|
const lot = this.lotNode(lotId);
|
|
204
319
|
if (taskId)
|
|
@@ -213,9 +328,69 @@ export class Plan {
|
|
|
213
328
|
notes.add(entry);
|
|
214
329
|
}
|
|
215
330
|
}
|
|
216
|
-
|
|
331
|
+
/** Lit une liste écrite dans un autre format : champs traduits, entrées à parent repliées en sous-tâches. */
|
|
332
|
+
function foreignLots(raw, format) {
|
|
333
|
+
const entries = raw.map((r, i) => {
|
|
334
|
+
if (!r || typeof r !== 'object' || Array.isArray(r))
|
|
335
|
+
throw new RafError(`entrée n° ${i + 1} du plan : un objet est attendu`);
|
|
336
|
+
const pick = (field) => {
|
|
337
|
+
for (const key of format.fields[field] ?? [field])
|
|
338
|
+
if (r[key] != null)
|
|
339
|
+
return r[key];
|
|
340
|
+
return undefined;
|
|
341
|
+
};
|
|
342
|
+
// Un horodatage vaut pour son jour.
|
|
343
|
+
const day = (field) => {
|
|
344
|
+
const v = pick(field);
|
|
345
|
+
return typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(v) ? v.slice(0, 10) : v;
|
|
346
|
+
};
|
|
347
|
+
const id = pick('id');
|
|
348
|
+
if ((typeof id !== 'string' && typeof id !== 'number') || String(id).trim() === '') {
|
|
349
|
+
throw new RafError(`entrée n° ${i + 1} du plan : identifiant absent`);
|
|
350
|
+
}
|
|
351
|
+
const problems = [];
|
|
352
|
+
const theirs = pick('status');
|
|
353
|
+
const mapped = Object.keys(format.statuses).length > 0;
|
|
354
|
+
if (mapped && theirs != null && !Object.hasOwn(format.statuses, String(theirs))) {
|
|
355
|
+
problems.push(`état « ${String(theirs)} » sans correspondance (cadence.yaml : plan.statuses)`);
|
|
356
|
+
}
|
|
357
|
+
const effort = pick('estimate');
|
|
358
|
+
if (typeof effort === 'string' && !(effort in format.estimates)) {
|
|
359
|
+
problems.push(`effort « ${effort} » sans correspondance (cadence.yaml : plan.estimates)`);
|
|
360
|
+
}
|
|
361
|
+
const notes = pick('notes');
|
|
362
|
+
const lot = normalizeLot({
|
|
363
|
+
id,
|
|
364
|
+
title: pick('title'),
|
|
365
|
+
status: mapped ? (Object.hasOwn(format.statuses, String(theirs)) ? format.statuses[String(theirs)] : undefined) : theirs,
|
|
366
|
+
estimate: typeof effort === 'string' ? format.estimates[effort] : effort,
|
|
367
|
+
quickwin: pick('quickwin'),
|
|
368
|
+
visible: pick('visible'),
|
|
369
|
+
after: pick('after'),
|
|
370
|
+
created: day('created'),
|
|
371
|
+
started: day('started'),
|
|
372
|
+
finished: day('finished'),
|
|
373
|
+
notes: typeof notes === 'string' ? [{ text: notes }] : notes,
|
|
374
|
+
}, problems);
|
|
375
|
+
const parent = pick('parent');
|
|
376
|
+
return { lot, parent: parent == null ? undefined : String(parent) };
|
|
377
|
+
});
|
|
378
|
+
const top = new Map(entries.filter((e) => !e.parent).map((e) => [e.lot.id, e.lot]));
|
|
379
|
+
const lots = [];
|
|
380
|
+
for (const { lot, parent } of entries) {
|
|
381
|
+
const into = parent ? top.get(parent) : undefined;
|
|
382
|
+
if (into && lot.id.startsWith(`${into.id}/`)) {
|
|
383
|
+
into.tasks.push({ id: lot.id.slice(into.id.length + 1), title: lot.title, status: lot.status });
|
|
384
|
+
into.problems.push(...lot.problems.map((p) => `${lot.id} : ${p}`));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
lots.push(lot);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return lots;
|
|
391
|
+
}
|
|
392
|
+
function normalizeLot(raw, problems = []) {
|
|
217
393
|
const status = STATUSES.includes(raw.status) ? raw.status : 'todo';
|
|
218
|
-
const problems = [];
|
|
219
394
|
const asDay = (field) => {
|
|
220
395
|
const v = raw[field];
|
|
221
396
|
if (v == null)
|
|
@@ -247,9 +422,18 @@ function normalizeLot(raw) {
|
|
|
247
422
|
status: STATUSES.includes(t.status) ? t.status : 'todo',
|
|
248
423
|
}))
|
|
249
424
|
: [],
|
|
250
|
-
...(raw.ux
|
|
251
|
-
|
|
252
|
-
: {}),
|
|
425
|
+
...(asVerdict(raw.ux) ? { ux: asVerdict(raw.ux) } : {}),
|
|
426
|
+
...(asVerdict(raw.review) ? { review: asVerdict(raw.review) } : {}),
|
|
253
427
|
problems,
|
|
254
428
|
};
|
|
255
429
|
}
|
|
430
|
+
function asVerdict(raw) {
|
|
431
|
+
if (!raw || typeof raw !== 'object')
|
|
432
|
+
return undefined;
|
|
433
|
+
const v = raw;
|
|
434
|
+
const verdict = { date: String(v.date ?? ''), verdict: String(v.verdict ?? '') };
|
|
435
|
+
// Champ présent mais vide : relu « jusqu'à rien », comme un verdict noté sans commit.
|
|
436
|
+
if ('commit' in v)
|
|
437
|
+
verdict.commit = v.commit == null || String(v.commit).trim() === '' ? null : String(v.commit).trim();
|
|
438
|
+
return verdict;
|
|
439
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { audit, exemptPlanOnly, nextUp, planCommits } from './audit.js';
|
|
2
3
|
import { diffDays, maxDay } from './dates.js';
|
|
3
|
-
import {
|
|
4
|
+
import { repoStatus } from './git.js';
|
|
4
5
|
import { linkCommits } from './link.js';
|
|
5
6
|
import { lockAlive, readLock, readNext } from './state.js';
|
|
7
|
+
const FACTS_TIMEOUT = 120_000;
|
|
8
|
+
/**
|
|
9
|
+
* Sortie de la commande du projet, ligne à ligne. Un échec se dit et ne bloque rien : ce sont des
|
|
10
|
+
* faits en plus, pas une condition de la session.
|
|
11
|
+
*/
|
|
12
|
+
function projectFacts(ctx, since) {
|
|
13
|
+
if (!ctx.facts)
|
|
14
|
+
return [];
|
|
15
|
+
const r = spawnSync('sh', ['-c', ctx.facts], {
|
|
16
|
+
cwd: ctx.root,
|
|
17
|
+
env: { ...process.env, CADENCE_SINCE: since, CADENCE_TODAY: ctx.today },
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
20
|
+
timeout: FACTS_TIMEOUT,
|
|
21
|
+
killSignal: 'SIGKILL',
|
|
22
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
23
|
+
});
|
|
24
|
+
const lines = `${r.stdout ?? ''}${r.stderr ?? ''}`.replace(/\s+$/, '').split('\n').filter((l, i, all) => l !== '' || all.length > 1);
|
|
25
|
+
if (r.error || r.signal)
|
|
26
|
+
lines.push(`✗ commande interrompue (${r.signal ? `délai de ${FACTS_TIMEOUT / 1000} s dépassé` : r.error.message}) : ${ctx.facts}`);
|
|
27
|
+
else if (r.status !== 0)
|
|
28
|
+
lines.push(`✗ commande en échec (code ${r.status})`);
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
6
31
|
const short = (c) => `${c.sha.slice(0, 7)} ${c.subject}`;
|
|
7
32
|
/** Commits de la fenêtre reliés aux lots, sans compter comme « sans lot » ceux qui ne touchent que le plan. */
|
|
8
33
|
function period(ctx, since) {
|
|
9
|
-
const commits =
|
|
10
|
-
|
|
34
|
+
const commits = planCommits(ctx.plan, ctx.root, { since });
|
|
35
|
+
const linked = exemptPlanOnly(linkCommits(ctx.plan.lots(), commits, ctx.plan.refs), ctx.plan, ctx.root);
|
|
36
|
+
// Comme raf check : un commit antérieur à l'adoption du plan n'avait pas à citer de lot.
|
|
37
|
+
const adopted = ctx.plan.since;
|
|
38
|
+
return adopted ? { ...linked, orphans: linked.orphans.filter((c) => c.day >= adopted) } : linked;
|
|
11
39
|
}
|
|
12
40
|
function byLotLines(linked) {
|
|
13
41
|
return [...linked.byLot].map(([id, cs]) => {
|
|
@@ -54,7 +82,7 @@ function lastActivity(lot, commits) {
|
|
|
54
82
|
export function sessionStart(ctx, opts) {
|
|
55
83
|
const { plan, out, today } = ctx;
|
|
56
84
|
const lots = plan.lots();
|
|
57
|
-
const all = linkCommits(lots,
|
|
85
|
+
const all = linkCommits(lots, planCommits(plan, ctx.root), plan.refs);
|
|
58
86
|
out(`${plan.project} — reprise du ${today}`);
|
|
59
87
|
const next = readNext(ctx.state);
|
|
60
88
|
if (next)
|
|
@@ -79,6 +107,7 @@ export function sessionStart(ctx, opts) {
|
|
|
79
107
|
section(out, `Fait depuis ${opts.since}`, done);
|
|
80
108
|
section(out, 'Écarts (raf check)', audit(plan, ctx.root, ctx.newsDir, today).map((i) => `✗ ${i.message}`));
|
|
81
109
|
section(out, 'Dépôt', [repoLine(ctx.root).line]);
|
|
110
|
+
section(out, 'Faits propres au projet', projectFacts(ctx, opts.since));
|
|
82
111
|
const proposals = [
|
|
83
112
|
...doing.map((l) => ({ l, why: 'en cours' })),
|
|
84
113
|
...ready.map((l) => ({ l, why: l.quickwin ? 'gain rapide prêt' : 'prêt' })),
|
|
@@ -96,12 +125,13 @@ export function sessionClose(ctx, opts) {
|
|
|
96
125
|
}
|
|
97
126
|
section(out, 'Commits de la période', commits.length ? commits : ['(aucun)']);
|
|
98
127
|
const quiet = plan.lots().filter((l) => l.status === 'doing' && !recent.byLot.has(l.id));
|
|
99
|
-
section(out, 'Lots en cours', quiet.map((l) => `${l.id} ${l.title} — aucun commit sur la période : raf done ou raf note`));
|
|
128
|
+
section(out, 'Lots en cours', quiet.map((l) => `${l.id} ${l.title} — aucun commit sur la période : ${plan.readonly ? "le fermer ou l'annoter avec l'outil du projet" : 'raf done ou raf note'}`));
|
|
100
129
|
const issues = audit(plan, ctx.root, ctx.newsDir, today);
|
|
101
130
|
section(out, 'Écarts (raf check)', issues.map((i) => `✗ ${i.message}`));
|
|
102
131
|
const repo = repoLine(ctx.root);
|
|
103
132
|
const lock = lockStatus(ctx.shared);
|
|
104
133
|
section(out, 'Dépôt', [repo.line, ...(lock ? [lock.line] : [])]);
|
|
134
|
+
section(out, 'Faits propres au projet', projectFacts(ctx, opts.since));
|
|
105
135
|
const open = issues.length + repo.open + (lock?.live ? 1 : 0);
|
|
106
136
|
out(open === 0 ? '\n✓ prêt à fermer' : `\n✗ pas fermé : ${open} point(s)`);
|
|
107
137
|
return open === 0 ? 0 : 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json",
|
|
16
16
|
"test": "vitest run",
|
|
17
17
|
"typecheck": "tsc --noEmit",
|
|
18
|
-
"prepare": "npm run build"
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"yaml": "^2.5.0"
|
package/skills/deliver/SKILL.md
CHANGED
|
@@ -18,6 +18,28 @@ deliver:
|
|
|
18
18
|
contains: "${SHORT}" # the new version is the one being served
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
A project that already has its own delivery script declares it instead — cadence then keeps the
|
|
22
|
+
preconditions, the lock, the log and the delivered lots, and the script keeps the CI wait, the deploy
|
|
23
|
+
and its business checks:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
deliver:
|
|
27
|
+
script: ./scripts/ship.sh "$CADENCE_SHORT"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Its arguments go after `--`: `cadence deliver -- api frontend` (`--sha <rev>` to deliver a pushed
|
|
31
|
+
commit other than HEAD). Ask the human (or the project's own
|
|
32
|
+
delivery skill) which arguments this delivery needs; never guess them.
|
|
33
|
+
|
|
34
|
+
## Where to run
|
|
35
|
+
|
|
36
|
+
Every `cadence` / `raf` command works on the git repository of the current directory. When the
|
|
37
|
+
session runs from a parent folder that holds several projects (not itself a repository), run each
|
|
38
|
+
command inside the project concerned: `cd <project> && cadence …`. The projects are the sub-folders
|
|
39
|
+
that contain `docs/plan/raf.yaml`, or a `cadence.yaml` with a `plan:` key.
|
|
40
|
+
|
|
41
|
+
Deliver one project at a time: the one the human names, or ask.
|
|
42
|
+
|
|
21
43
|
## Steps
|
|
22
44
|
|
|
23
45
|
1. Before anything: tests and build green locally, everything committed **and pushed** — the CI can
|
|
@@ -28,8 +50,17 @@ deliver:
|
|
|
28
50
|
3. `cadence deliver`. Exit codes: 0 delivered and verified, 1 a step failed, 2 refused before acting.
|
|
29
51
|
4. On failure: read which step failed and why. Fix the cause, commit, push, deliver again. Never rerun
|
|
30
52
|
blindly, never skip a check to make it pass.
|
|
31
|
-
5. On success: `raf done <id>` for the lots it lists **whose effect you have seen**; if one of them is
|
|
53
|
+
5. On success: `raf done <id>` (or the project's own tool when its plan is read-only) for the lots it lists **whose effect you have seen**; if one of them is
|
|
32
54
|
`visible`, `cadence news build` and deliver the news too.
|
|
55
|
+
6. After a green delivery that changes what a page shows or what it is served (screen, API, data
|
|
56
|
+
source, configuration of either) — in practice every delivery except docs-, plan- or tests-only
|
|
57
|
+
ones — have the `qa-reviewer` agent walk the delivered app in a real browser, whether the lot
|
|
58
|
+
is `visible` or not: give it the repository path, the base URL and the lot id. When the lot
|
|
59
|
+
touched only the backend, the agent starts with the pages that call the changed endpoints. It
|
|
60
|
+
checks each page against `docs/qa/expectations.md` — what the user must find there — and
|
|
61
|
+
reports a page left empty, an error shown, an API call that failed or came back empty: what
|
|
62
|
+
the checks of `cadence.yaml` do not see. Bring its blocking findings to the human. It is not a
|
|
63
|
+
gate: the delivery stays done, a finding becomes a new lot.
|
|
33
64
|
|
|
34
65
|
## Rules
|
|
35
66
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lead — decide at the top, work in subagents
|
|
7
|
+
|
|
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
|
|
10
|
+
are the sub-folders of the current directory that contain `docs/plan/raf.yaml`, or a `cadence.yaml`
|
|
11
|
+
with a `plan:` key. A project whose `cadence.yaml` maps the fields of a plan kept by its own tool is
|
|
12
|
+
**read-only** for `raf`: it takes part in the tour, and its plan is changed with the project's own
|
|
13
|
+
commands (its CLAUDE.md names them), never with `raf start|done|note|ux|review`.
|
|
14
|
+
|
|
15
|
+
## Limits that always apply
|
|
16
|
+
|
|
17
|
+
- **At most two subagents running at once.** Queue the rest.
|
|
18
|
+
- **Never two subagents in the same repository at the same time**, and the lead does not commit in a
|
|
19
|
+
repository where a subagent is working.
|
|
20
|
+
- **Deliveries and `raf ux` / `raf review` verdicts are done by the lead, one project at a time** —
|
|
21
|
+
never by a subagent, never two deliveries in parallel.
|
|
22
|
+
- A subagent cannot ask the human anything. When it hits an ambiguity it stops and reports; the lead
|
|
23
|
+
brings the question to the human.
|
|
24
|
+
|
|
25
|
+
## 1. Tour of the projects
|
|
26
|
+
|
|
27
|
+
For each project, a subagent (read-only) runs `cd <project> && cadence session start` and returns at
|
|
28
|
+
most five lines: lots in progress (silent ones flagged), drift, notes from the last close, the next
|
|
29
|
+
ready lot. Two at a time, in the background.
|
|
30
|
+
|
|
31
|
+
Then report to the human in a compact table — one row per project — and propose **three** items
|
|
32
|
+
across projects, each with project, lot id and one sentence of justification: finish what is in
|
|
33
|
+
progress, then drift that blocks a delivery, then ready quick wins. **Stop and wait for the priority.**
|
|
34
|
+
|
|
35
|
+
## 2. Delegation
|
|
36
|
+
|
|
37
|
+
For each chosen lot, the lead runs `cd <project> && raf start <lot>`, then gives a subagent this brief
|
|
38
|
+
(fill in the brackets, keep the rest verbatim):
|
|
39
|
+
|
|
40
|
+
> Work in `<absolute path of the project>` on lot `<id>` — "<title>" — of its plan
|
|
41
|
+
> (`docs/plan/raf.yaml`, or the file named by `plan:` in `cadence.yaml`; read the lot, its notes and sub-tasks, and the project's CLAUDE.md first).
|
|
42
|
+
> Goal: <what done looks like, from the human's words>.
|
|
43
|
+
> Rules: test first; commit each sub-part as soon as its tests pass, with explicit paths (never
|
|
44
|
+
> `git add -A` or `commit -a`), and a message that cites the lot (`feat(<id>): …`); run the project's
|
|
45
|
+
> full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
|
|
46
|
+
> `raf review`.
|
|
47
|
+
> If something is ambiguous or needs a decision, stop and report the question instead of guessing.
|
|
48
|
+
> Report: commits (sha + subject), tests and build results with their numbers, what you could not
|
|
49
|
+
> verify, open questions.
|
|
50
|
+
|
|
51
|
+
A lot that adds or changes a screen is `visible`: after the implementation, have the
|
|
52
|
+
`ux-reviewer` agent review it (give it the URL or the way to run the app) and bring its verdict and
|
|
53
|
+
proposed sub-tasks back to the human.
|
|
54
|
+
|
|
55
|
+
## 3. Check
|
|
56
|
+
|
|
57
|
+
1. The `code-reviewer` agent, as a fresh subagent (most capable model), reviews the lot. Give it
|
|
58
|
+
the absolute path of the project and the lot id, **not** the author's report: it reads the diff
|
|
59
|
+
itself from the commits that cite the lot, runs the checks, and returns real defects only,
|
|
60
|
+
ranked, with what it could not verify and a one-line verdict.
|
|
61
|
+
2. **The lead re-verifies itself**: `git log` shows the commits, the test suite and build pass when
|
|
62
|
+
run by the lead, `raf check` is clean. A subagent is green on what it *could* test; say plainly
|
|
63
|
+
what nobody could verify.
|
|
64
|
+
3. Fix or re-delegate what the review found; then record the verdict, `raf review <lot> "…"` with
|
|
65
|
+
the code reviewer's last line brought up to date (and `raf ux <lot> "…"` for a visible lot, with
|
|
66
|
+
the UX reviewer's), then `raf done <lot>`. Where the gate is on (`raf review enable`, the
|
|
67
|
+
human's decision for each repository), `raf done` refuses a lot that has commits and no
|
|
68
|
+
verdict, or a verdict older than its latest commit: a fix made after the review means a new
|
|
69
|
+
review of the lot, not an edited verdict. A read-only plan has no gate in `raf` (a `uxSince` or `reviewSince` written in it is
|
|
70
|
+
ignored): the verdict goes into the project's own tool.
|
|
71
|
+
|
|
72
|
+
## 4. Delivery
|
|
73
|
+
|
|
74
|
+
One project at a time, by the lead: push, then the `deliver` skill (`cadence deliver --dry-run`, then
|
|
75
|
+
`cadence deliver`). Follow the human's standing instructions about confirmation before production.
|
|
76
|
+
|
|
77
|
+
After a green delivery that changes what a page shows or what it is served (screen, API, data
|
|
78
|
+
source, configuration of either) — in practice every delivery except docs-, plan- or tests-only ones
|
|
79
|
+
— have the `qa-reviewer` agent check the delivered app, as a fresh subagent: give it the absolute
|
|
80
|
+
path of the project, the base URL of the delivered app and the lot id. The lot need not be
|
|
81
|
+
`visible`: a backend-only lot can empty a page without changing a screen. When the lot touched only
|
|
82
|
+
the backend, the agent starts with the pages that call the changed endpoints. It walks the pages in
|
|
83
|
+
a real browser against the project's expectations (`docs/qa/expectations.md`: per page, what the
|
|
84
|
+
user must find there) and returns measured findings; it reads only, and never logs in. Bring its
|
|
85
|
+
blocking findings back to the human — a page whose main content is missing, or that shows an error,
|
|
86
|
+
is a defect even when the delivery checks are green — with its proposed follow-up lines. A project
|
|
87
|
+
without an expectations file gets a draft back: show it to the human, who corrects it and decides
|
|
88
|
+
whether it is committed. It is not a gate: `raf done` does not wait for it, and a finding becomes a
|
|
89
|
+
new lot, not a reopened one.
|
|
90
|
+
|
|
91
|
+
## 5. Close
|
|
92
|
+
|
|
93
|
+
At the end, the `session-close` routine in each project touched, and `cadence session next` lines in
|
|
94
|
+
each. Give the human one line per project: delivered, in progress, blocked (and why).
|
|
@@ -5,6 +5,16 @@ description: Close a work session on a repository that uses cadence — plan hyg
|
|
|
5
5
|
|
|
6
6
|
# Session close — close without memorising everything
|
|
7
7
|
|
|
8
|
+
## Where to run
|
|
9
|
+
|
|
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 each
|
|
12
|
+
command inside the project concerned: `cd <project> && cadence …`. The projects are the sub-folders
|
|
13
|
+
that contain `docs/plan/raf.yaml`, or a `cadence.yaml` with a `plan:` key.
|
|
14
|
+
|
|
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.
|
|
17
|
+
|
|
8
18
|
## Steps
|
|
9
19
|
|
|
10
20
|
1. Run `cadence session close` (over several days: `--since "2 days ago"`). Exit code 1 means
|
|
@@ -16,6 +26,10 @@ description: Close a work session on a repository that uses cadence — plan hyg
|
|
|
16
26
|
is genuinely outside the plan (docs, chores);
|
|
17
27
|
- a finished lot marked `visible` without a news entry → `cadence news new <id>`, written for the
|
|
18
28
|
user, with a screenshot;
|
|
29
|
+
- a lot that `raf done` refuses, or that the check reports as finished, for lack of a review —
|
|
30
|
+
code review of a lot with commits, or one older than the lot's latest commit
|
|
31
|
+
(`raf review enable`), UX review of a visible lot (`raf ux enable`) → have the `code-reviewer` / `ux-reviewer` agent review it, then record its
|
|
32
|
+
verdict with `raf review <id> "…"` / `raf ux <id> "…"`; never write a verdict nobody gave;
|
|
19
33
|
- rerun until the check part is clean.
|
|
20
34
|
3. **Memory, filtered** (only if you keep a persistent memory): write down what the repository does
|
|
21
35
|
NOT already say — a trap and its cause, a decision or correction from the human, a collaboration
|
|
@@ -23,6 +37,9 @@ description: Close a work session on a repository that uses cadence — plan hyg
|
|
|
23
37
|
4. **Skills and agents, on threshold, PROPOSED**: a skill when the same chain of commands was done by
|
|
24
38
|
hand at least twice today; an agent update when an agent got something wrong or its domain moved.
|
|
25
39
|
List them with the benefit; the human decides. Never create them here.
|
|
40
|
+
When the report has a "Faits propres au projet" section (`session.close` in `cadence.yaml`), treat
|
|
41
|
+
what it flags as part of this hygiene; with a read-only plan, use the project's own tool wherever
|
|
42
|
+
these steps say `raf`.
|
|
26
43
|
5. **Clean state**: everything committed and pushed, no delivery running. If the command still exits 1,
|
|
27
44
|
say what remains and do NOT say the session is closed.
|
|
28
45
|
6. **Three lines for next time**: `cadence session next "…" "…" "…"` — the next `session-start` shows them.
|