@sylad/cadence 0.6.0 → 0.8.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 +143 -11
- package/bin/cadence.js +6 -2
- package/dist/audit.js +36 -14
- package/dist/cli.js +76 -9
- package/dist/deliver.js +90 -32
- package/dist/git.js +12 -2
- package/dist/link.js +15 -1
- package/dist/news.js +58 -13
- package/dist/plan.js +17 -3
- package/dist/proc.js +161 -0
- package/dist/session.js +1 -0
- package/dist/state.js +10 -7
- package/dist/verify.js +114 -0
- package/package.json +13 -3
- package/skills/deliver/SKILL.md +3 -1
- package/skills/session-close/SKILL.md +5 -0
- package/skills/session-start/SKILL.md +3 -0
package/dist/verify.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:os';
|
|
3
|
+
import { onTermination } from './proc.js';
|
|
4
|
+
import { describeCheck, realDeps, retryCheck, TIMED_OUT } from './deliver.js';
|
|
5
|
+
function killGroup(pgid, sig) {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(-pgid, sig);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// groupe déjà vide
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Une commande de vérification, en groupe de processus détaché (nouvelle session, sans tty) : au délai,
|
|
15
|
+
* TOUT le groupe est tué (aucun enfant orphelin) ; un signal reçu par cadence lui est relayé. Asynchrone :
|
|
16
|
+
* les vérifications tournent en parallèle. Réservé aux vérifications (verify, session start) — jamais
|
|
17
|
+
* aux commandes de deliver, qui gardent le groupe de premier plan et le tty.
|
|
18
|
+
*/
|
|
19
|
+
function execGroup(root, cmd, env, timeoutMs, quiet) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const child = spawn('sh', ['-c', cmd], {
|
|
22
|
+
cwd: root,
|
|
23
|
+
env: { ...process.env, ...env },
|
|
24
|
+
stdio: quiet ? 'ignore' : ['ignore', 'inherit', 'inherit'],
|
|
25
|
+
detached: true,
|
|
26
|
+
});
|
|
27
|
+
const pgid = child.pid;
|
|
28
|
+
if (pgid === undefined) {
|
|
29
|
+
child.once('error', () => resolve(127));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Ctrl-C, SIGTERM ou raccrochage reçu par cadence : relayé au groupe avant de sortir.
|
|
33
|
+
const forget = onTermination((sig) => killGroup(pgid, sig));
|
|
34
|
+
let timedOut = false;
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
timedOut = true;
|
|
37
|
+
killGroup(pgid, 'SIGKILL');
|
|
38
|
+
}, timeoutMs);
|
|
39
|
+
child.once('exit', (code, signal) => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
forget();
|
|
42
|
+
if (timedOut)
|
|
43
|
+
killGroup(pgid, 'SIGKILL'); // sh mort avant ses enfants : le groupe est vidé quand même
|
|
44
|
+
resolve(timedOut ? TIMED_OUT : signal ? 128 + (constants.signals[signal] ?? 0) : (code ?? 1));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Dépendances réelles des vérifications. `quiet` : sortie des commandes non relayée (rapport de session start). */
|
|
49
|
+
export function realCheckDeps(root, opts = {}) {
|
|
50
|
+
const { fetch, sleep, now } = realDeps(root);
|
|
51
|
+
return { fetch, sleep, now, exec: (cmd, env, timeoutMs) => execGroup(root, cmd, env, timeoutMs, !!opts.quiet) };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Rejoue des vérifications d'effet, TOUTES EN PARALLÈLE : chaque essai reçoit le budget entier
|
|
55
|
+
* (`attemptMs`, tuée au-delà : « délai dépassé »), une vérification lente ne prend rien aux autres.
|
|
56
|
+
* UNE passe par défaut ; avec `retryMs`, chacune est réessayée de son côté (toutes les POLL_VERIFY)
|
|
57
|
+
* jusqu'à ce délai. Durée totale bornée par retryMs + attemptMs. Résultats dans l'ordre des vérifications.
|
|
58
|
+
* Le code d'une vérification est celui de deliver (tryCheck/retryCheck) : aucune règle n'est dupliquée.
|
|
59
|
+
*/
|
|
60
|
+
export async function replayChecks(checks, deps, sha, env, opts) {
|
|
61
|
+
const until = deps.now() + opts.retryMs;
|
|
62
|
+
return Promise.all(checks.map(async (c) => ({ label: describeCheck(c, sha), reason: await retryCheck(c, deps, sha, env, until, () => opts.attemptMs) })));
|
|
63
|
+
}
|
|
64
|
+
export const resultLine = (r) => (r.reason === null ? `✓ ${r.label}` : `✗ ${r.label} — ${r.reason}`);
|
|
65
|
+
export function summaryLine(results) {
|
|
66
|
+
const red = results.filter((r) => r.reason !== null).length;
|
|
67
|
+
const n = results.length;
|
|
68
|
+
return red === 0
|
|
69
|
+
? `verify : ${n}/${n} vérifications vertes`
|
|
70
|
+
: `verify : ${red} effet${red > 1 ? 's' : ''} rouge${red > 1 ? 's' : ''} sur ${n} vérification${n > 1 ? 's' : ''}`;
|
|
71
|
+
}
|
|
72
|
+
/** Variables passées aux commandes de vérification, comme à celles de deliver. */
|
|
73
|
+
export function verifyEnv(sha) {
|
|
74
|
+
return { CADENCE_SHA: sha, CADENCE_SHORT: sha.slice(0, 7), CADENCE_BRANCH: '' };
|
|
75
|
+
}
|
|
76
|
+
/** Délai de chaque essai d'une vérification de `cadence verify`. */
|
|
77
|
+
export const VERIFY_ATTEMPT_MS = 120_000;
|
|
78
|
+
/** `cadence verify` : 0 tout vert, 1 un effet rouge, 2 rien à vérifier. */
|
|
79
|
+
export async function verifyCommand(ctx, deps) {
|
|
80
|
+
const { config, out } = ctx;
|
|
81
|
+
if (config.verify.length === 0) {
|
|
82
|
+
out(config.script !== undefined
|
|
83
|
+
? 'verify : aucune vérification déclarée — ce projet livre par son script (deliver.script) et ses contrôles sont les siens ; ' +
|
|
84
|
+
'déclarer deliver.verify dans cadence.yaml pour les rejouer hors livraison'
|
|
85
|
+
: 'verify : aucune vérification déclarée (deliver.verify)');
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
const results = await replayChecks(config.verify, deps, ctx.sha, verifyEnv(ctx.sha), { retryMs: ctx.retry * 1000, attemptMs: VERIFY_ATTEMPT_MS });
|
|
89
|
+
for (const r of results)
|
|
90
|
+
out(resultLine(r));
|
|
91
|
+
out(summaryLine(results));
|
|
92
|
+
return results.some((r) => r.reason !== null) ? 1 : 0;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Délai de chaque vérification de « session start », lancées en parallèle : c'est aussi, à la mise à mort
|
|
96
|
+
* près, la durée totale — la reprise ne doit pas attendre un réseau absent.
|
|
97
|
+
*/
|
|
98
|
+
export const MORNING_BUDGET_MS = 10_000;
|
|
99
|
+
/**
|
|
100
|
+
* Lignes « Effets en production » du rapport de reprise : un seul essai par vérification, borné. Rien à dire
|
|
101
|
+
* (liste vide) pour un projet sans verify ; ne lève jamais — c'est un fait de plus, pas une condition.
|
|
102
|
+
*/
|
|
103
|
+
export async function effectLines(config, sha, deps) {
|
|
104
|
+
if (config.verify.length === 0)
|
|
105
|
+
return [];
|
|
106
|
+
try {
|
|
107
|
+
const results = await replayChecks(config.verify, deps, sha, verifyEnv(sha), { retryMs: 0, attemptMs: MORNING_BUDGET_MS });
|
|
108
|
+
const shown = results.filter((r) => r.reason !== null);
|
|
109
|
+
return shown.length === 0 ? [`✓ ${summaryLine(results)}`] : [...shown.map(resultLine), summaryLine(results)];
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return [`✗ verify : ${e.message}`];
|
|
113
|
+
}
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -9,8 +9,18 @@
|
|
|
9
9
|
"raf": "bin/raf.js",
|
|
10
10
|
"cadence": "bin/cadence.js"
|
|
11
11
|
},
|
|
12
|
-
"files": [
|
|
13
|
-
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"dist",
|
|
15
|
+
"skills",
|
|
16
|
+
"agents",
|
|
17
|
+
".claude-plugin",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
14
24
|
"scripts": {
|
|
15
25
|
"build": "tsc -p tsconfig.build.json",
|
|
16
26
|
"test": "vitest run",
|
package/skills/deliver/SKILL.md
CHANGED
|
@@ -51,7 +51,9 @@ Deliver one project at a time: the one the human names, or ask.
|
|
|
51
51
|
4. On failure: read which step failed and why. Fix the cause, commit, push, deliver again. Never rerun
|
|
52
52
|
blindly, never skip a check to make it pass.
|
|
53
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
|
|
54
|
-
`visible`, `cadence news build` and deliver the news too.
|
|
54
|
+
`visible`, `cadence news build` and deliver the news too. With a read-only plan the list holds only
|
|
55
|
+
the lots that were in progress when the delivery started; the project's own tool has the last word
|
|
56
|
+
on what it marked delivered.
|
|
55
57
|
6. After a green delivery that changes what a page shows or what it is served (screen, API, data
|
|
56
58
|
source, configuration of either) — in practice every delivery except docs-, plan- or tests-only
|
|
57
59
|
ones — have the `qa-reviewer` agent walk the delivered app in a real browser, whether the lot
|
|
@@ -24,6 +24,9 @@ With no project named, run `cadence session close` in each project touched durin
|
|
|
24
24
|
otherwise `raf note <id> "where it stands, what blocks"`;
|
|
25
25
|
- a commit without a lot that belongs to one → `raf note <id> "commits: <sha> …"`; nothing if it
|
|
26
26
|
is genuinely outside the plan (docs, chores);
|
|
27
|
+
- a plan commit reported because it also touches a file generated from the plan (a published
|
|
28
|
+
plan) → propose to declare that file under `plan.files` in `cadence.yaml`; the files of a commit
|
|
29
|
+
decide whether it is plan upkeep, never its subject;
|
|
27
30
|
- a finished lot marked `visible` without a news entry → `cadence news new <id>`, written for the
|
|
28
31
|
user, with a screenshot;
|
|
29
32
|
- a lot that `raf done` refuses, or that the check reports as finished, for lack of a review —
|
|
@@ -43,6 +46,8 @@ With no project named, run `cadence session close` in each project touched durin
|
|
|
43
46
|
5. **Clean state**: everything committed and pushed, no delivery running. If the command still exits 1,
|
|
44
47
|
say what remains and do NOT say the session is closed.
|
|
45
48
|
6. **Three lines for next time**: `cadence session next "…" "…" "…"` — the next `session-start` shows them.
|
|
49
|
+
The lines replace the previous notes. Without a line the command refuses and keeps them; erase
|
|
50
|
+
them on purpose with `cadence session next --clear`, only when nothing is left to say.
|
|
46
51
|
|
|
47
52
|
## Do not
|
|
48
53
|
|
|
@@ -30,6 +30,9 @@ three most useful items across projects and wait. With a project named, work in
|
|
|
30
30
|
- **Done since**: 3 to 6 lines, grouped by lot, not by commit. Mention commits without a lot.
|
|
31
31
|
- **Drift**: each `✗` line from the check, with the one command that fixes it.
|
|
32
32
|
- **Delivery in progress** or a stale lock: no new delivery until it is resolved.
|
|
33
|
+
- **Effects in production** ("Effets en production"): a red `✗` line is the first thing to say —
|
|
34
|
+
a delivered feature that no longer works outranks any new lot; re-run `cadence verify` to confirm
|
|
35
|
+
(a network blip also shows as red).
|
|
33
36
|
- **Notes from the last close**, if any.
|
|
34
37
|
- **Project facts** ("Faits propres au projet"), when the project plugs its own morning script in
|
|
35
38
|
(`session.start` in `cadence.yaml`): summarise what bears on today's choice — deadlines, the
|