@sylad/cadence 0.2.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 +14 -0
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/agents/ux-reviewer.md +49 -0
- package/bin/cadence.js +35 -0
- package/bin/raf.js +10 -0
- package/dist/audit.js +54 -0
- package/dist/check.js +60 -0
- package/dist/cli.js +384 -0
- package/dist/dates.js +63 -0
- package/dist/deliver.js +305 -0
- package/dist/gantt.js +176 -0
- package/dist/git.js +91 -0
- package/dist/hook.js +36 -0
- package/dist/link.js +32 -0
- package/dist/news.js +200 -0
- package/dist/plan.js +255 -0
- package/dist/schedule.js +55 -0
- package/dist/session.js +108 -0
- package/dist/skills.js +61 -0
- package/dist/state.js +126 -0
- package/package.json +28 -0
- package/skills/deliver/SKILL.md +41 -0
- package/skills/session-close/SKILL.md +33 -0
- package/skills/session-start/SKILL.md +38 -0
package/dist/state.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, linkSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { gitCommonDir, gitPath } from './git.js';
|
|
4
|
+
/** État local propre au worktree (notes de clôture). */
|
|
5
|
+
export function stateDir(cwd) {
|
|
6
|
+
const dir = gitPath(cwd, 'cadence');
|
|
7
|
+
mkdirSync(dir, { recursive: true });
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
/** État local commun à tous les worktrees (verrou et journal des livraisons) : une seule livraison par dépôt. */
|
|
11
|
+
export function sharedStateDir(cwd) {
|
|
12
|
+
const dir = join(gitCommonDir(cwd), 'cadence');
|
|
13
|
+
mkdirSync(dir, { recursive: true });
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
export function readNext(dir) {
|
|
17
|
+
const file = join(dir, 'next.md');
|
|
18
|
+
if (!existsSync(file))
|
|
19
|
+
return null;
|
|
20
|
+
const [head, ...rest] = readFileSync(file, 'utf8').split('\n');
|
|
21
|
+
const lines = rest.map((l) => l.replace(/^- /, '')).filter(Boolean);
|
|
22
|
+
return { date: head.replace(/^# /, '').trim(), lines };
|
|
23
|
+
}
|
|
24
|
+
/** Remplace les notes pour la prochaine session ; sans ligne, les efface. */
|
|
25
|
+
export function writeNext(dir, date, lines) {
|
|
26
|
+
const file = join(dir, 'next.md');
|
|
27
|
+
if (lines.length === 0) {
|
|
28
|
+
rmSync(file, { force: true });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(file, `# ${date}\n${lines.map((l) => `- ${l.replace(/\n/g, ' ')}`).join('\n')}\n`);
|
|
32
|
+
}
|
|
33
|
+
export function lockPath(dir) {
|
|
34
|
+
return join(dir, 'deliver.lock');
|
|
35
|
+
}
|
|
36
|
+
function readLockFile(file) {
|
|
37
|
+
let text;
|
|
38
|
+
let ageMs;
|
|
39
|
+
try {
|
|
40
|
+
ageMs = Date.now() - statSync(file).mtimeMs;
|
|
41
|
+
text = readFileSync(file, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const raw = JSON.parse(text);
|
|
48
|
+
return { pid: Number(raw.pid), sha: String(raw.sha), started: String(raw.started), ageMs };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { pid: 0, sha: '?', started: '?', unreadable: true, ageMs };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function readLock(dir) {
|
|
55
|
+
return readLockFile(lockPath(dir));
|
|
56
|
+
}
|
|
57
|
+
/** Verrou tenu : processus vivant, ou fichier illisible mais tout récent (écriture en cours ailleurs). */
|
|
58
|
+
export function lockAlive(lock) {
|
|
59
|
+
return lock.unreadable ? lock.ageMs < 5_000 : pidAlive(lock.pid);
|
|
60
|
+
}
|
|
61
|
+
/** Pose le verrou de façon atomique (lien vers un fichier complet) ; false s'il existe déjà. */
|
|
62
|
+
export function writeLock(dir, lock) {
|
|
63
|
+
const tmp = join(dir, `deliver.lock.${lock.pid}.${Date.now()}.tmp`);
|
|
64
|
+
writeFileSync(tmp, JSON.stringify(lock));
|
|
65
|
+
try {
|
|
66
|
+
linkSync(tmp, lockPath(dir));
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
rmSync(tmp, { force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Retire un verrou périmé seulement s'il est encore celui qu'on a lu ; sinon le laisse en place. */
|
|
77
|
+
export function removeStaleLock(dir, seen) {
|
|
78
|
+
const aside = join(dir, `deliver.lock.stale.${process.pid}`);
|
|
79
|
+
try {
|
|
80
|
+
renameSync(lockPath(dir), aside);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const now = readLockFile(aside);
|
|
86
|
+
if (now && now.pid === seen.pid && now.sha === seen.sha && now.started === seen.started) {
|
|
87
|
+
rmSync(aside, { force: true });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Un autre processus a pris le verrou entre-temps : le lui rendre.
|
|
91
|
+
try {
|
|
92
|
+
linkSync(aside, lockPath(dir));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Un troisième l'a déjà repris : le sien fait foi.
|
|
96
|
+
}
|
|
97
|
+
rmSync(aside, { force: true });
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/** Retire le verrou s'il appartient à ce processus. */
|
|
101
|
+
export function releaseLock(dir, pid) {
|
|
102
|
+
const lock = readLock(dir);
|
|
103
|
+
if (lock && lock.pid === pid)
|
|
104
|
+
rmSync(lockPath(dir), { force: true });
|
|
105
|
+
}
|
|
106
|
+
export function pidAlive(pid) {
|
|
107
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
108
|
+
return false;
|
|
109
|
+
try {
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return e.code === 'EPERM';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function appendDelivery(dir, day, sha) {
|
|
118
|
+
appendFileSync(join(dir, 'deliveries.log'), `${day} ${sha}\n`);
|
|
119
|
+
}
|
|
120
|
+
export function lastDelivery(dir) {
|
|
121
|
+
const file = join(dir, 'deliveries.log');
|
|
122
|
+
if (!existsSync(file))
|
|
123
|
+
return null;
|
|
124
|
+
const last = readFileSync(file, 'utf8').trim().split('\n').pop();
|
|
125
|
+
return last?.split(' ')[1] ?? null;
|
|
126
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sylad/cadence",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"author": "Sylvain Ladoire",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"raf": "bin/raf.js",
|
|
10
|
+
"cadence": "bin/cadence.js"
|
|
11
|
+
},
|
|
12
|
+
"files": ["bin", "dist", "skills", "agents", ".claude-plugin", "README.md", "LICENSE"],
|
|
13
|
+
"engines": { "node": ">=20" },
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.build.json",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"prepare": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"yaml": "^2.5.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.14.0",
|
|
25
|
+
"typescript": "^5.5.0",
|
|
26
|
+
"vitest": "^2.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deliver
|
|
3
|
+
description: Deliver the pushed HEAD of a repository that uses cadence — wait for its CI run, run the configured deploy commands, then VERIFY THE EFFECT with the configured checks. A delivery is only done when its checks pass. Triggers — "/deliver", "ship it", "deploy", "livre", "mets en prod".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deliver — a delivery is proven by its effect
|
|
7
|
+
|
|
8
|
+
`cadence deliver` reads `cadence.yaml` at the repository root:
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
deliver:
|
|
12
|
+
ci: github # github | none | { command: "…" }
|
|
13
|
+
deploy:
|
|
14
|
+
- ./scripts/deploy.sh "$CADENCE_SHORT"
|
|
15
|
+
verify:
|
|
16
|
+
- url: https://app.example.com/api/health
|
|
17
|
+
- url: https://app.example.com/
|
|
18
|
+
contains: "${SHORT}" # the new version is the one being served
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. Before anything: tests and build green locally, everything committed **and pushed** — the CI can
|
|
24
|
+
only build what it has received, and `cadence deliver` refuses a sha that is not on a remote branch.
|
|
25
|
+
2. `cadence deliver --dry-run`: read the resolved commands and checks. If there is no `cadence.yaml`,
|
|
26
|
+
help write one: ask the human for the deploy command, and propose at least one check that proves
|
|
27
|
+
the NEW version is live (a version string, a new endpoint), not merely that the site answers.
|
|
28
|
+
3. `cadence deliver`. Exit codes: 0 delivered and verified, 1 a step failed, 2 refused before acting.
|
|
29
|
+
4. On failure: read which step failed and why. Fix the cause, commit, push, deliver again. Never rerun
|
|
30
|
+
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
|
|
32
|
+
`visible`, `cadence news build` and deliver the news too.
|
|
33
|
+
|
|
34
|
+
## Rules
|
|
35
|
+
|
|
36
|
+
- Never two deliveries at once: a second one can silently overwrite the first one's deploy. The lock
|
|
37
|
+
enforces it; do not delete a live lock.
|
|
38
|
+
- A green tool status (CI "success", a deploy tool "synced", a job "completed") is not a proof. Only
|
|
39
|
+
the checks are.
|
|
40
|
+
- Delivering to production is an outward-facing action: follow the human's standing instructions about
|
|
41
|
+
confirmation before running step 3.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-close
|
|
3
|
+
description: Close a work session on a repository that uses cadence — plan hygiene (lots in progress, commits without a lot, news entries), repository state (uncommitted, unpushed, delivery running), filtered memory, proposals for new skills or agents without creating them, and three lines for next time. Triggers — "/session-close", "let's stop for today", "on ferme", end of the day.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session close — close without memorising everything
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
|
|
10
|
+
1. Run `cadence session close` (over several days: `--since "2 days ago"`). Exit code 1 means
|
|
11
|
+
*not closed*: something below is still open.
|
|
12
|
+
2. **Plan hygiene**, lot by lot:
|
|
13
|
+
- a `doing` lot with no commit in the period → `raf done <id>` if its tests are green,
|
|
14
|
+
otherwise `raf note <id> "where it stands, what blocks"`;
|
|
15
|
+
- a commit without a lot that belongs to one → `raf note <id> "commits: <sha> …"`; nothing if it
|
|
16
|
+
is genuinely outside the plan (docs, chores);
|
|
17
|
+
- a finished lot marked `visible` without a news entry → `cadence news new <id>`, written for the
|
|
18
|
+
user, with a screenshot;
|
|
19
|
+
- rerun until the check part is clean.
|
|
20
|
+
3. **Memory, filtered** (only if you keep a persistent memory): write down what the repository does
|
|
21
|
+
NOT already say — a trap and its cause, a decision or correction from the human, a collaboration
|
|
22
|
+
rule. Test: "do `git log` or the docs already say it?" → then no memory.
|
|
23
|
+
4. **Skills and agents, on threshold, PROPOSED**: a skill when the same chain of commands was done by
|
|
24
|
+
hand at least twice today; an agent update when an agent got something wrong or its domain moved.
|
|
25
|
+
List them with the benefit; the human decides. Never create them here.
|
|
26
|
+
5. **Clean state**: everything committed and pushed, no delivery running. If the command still exits 1,
|
|
27
|
+
say what remains and do NOT say the session is closed.
|
|
28
|
+
6. **Three lines for next time**: `cadence session next "…" "…" "…"` — the next `session-start` shows them.
|
|
29
|
+
|
|
30
|
+
## Do not
|
|
31
|
+
|
|
32
|
+
- Write a recap of the day into memory: the plan, `raf now` and `git log` are the record.
|
|
33
|
+
- Close, drop or re-scope a lot the human has not agreed to.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-start
|
|
3
|
+
description: Start a work session on a repository that uses cadence — gather the facts (open lots, work done since last time, drift between plan and history, delivery in progress, notes left at the last close), report them briefly, propose three lots from the plan, then WAIT for the human's priority. Triggers — "/session-start", "let's resume", "where are we?", "on reprend", start of the day.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session start — resume without reciting
|
|
7
|
+
|
|
8
|
+
The repository already carries the state: the plan (`docs/plan/raf.yaml`), the history, the notes left at
|
|
9
|
+
the last close. The only thing missing is the human's priority for today. This skill gathers the facts,
|
|
10
|
+
proposes, and stops.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. Run `cadence session start` (after a weekend: `--since "3 days ago"`). If `cadence` is not on the
|
|
15
|
+
PATH, use `npx -p @sylad/cadence cadence session start`.
|
|
16
|
+
2. Report **briefly**, in the human's language, in this order — never paste the raw output:
|
|
17
|
+
- **In progress**: each `doing` lot; for a *silent* one, say whether it looks like a forgotten
|
|
18
|
+
`raf done` / `raf note` (commits exist, work looks finished) or a real stop.
|
|
19
|
+
- **Done since**: 3 to 6 lines, grouped by lot, not by commit. Mention commits without a lot.
|
|
20
|
+
- **Drift**: each `✗` line from the check, with the one command that fixes it.
|
|
21
|
+
- **Delivery in progress** or a stale lock: no new delivery until it is resolved.
|
|
22
|
+
- **Notes from the last close**, if any.
|
|
23
|
+
3. **The plan drives the work.** Propose exactly three items, each with its id and one sentence of
|
|
24
|
+
justification, in the order the command gives them: finish what is in progress, then ready quick
|
|
25
|
+
wins, then the next ready lots. An idea that is not in the plan is only proposed together with the
|
|
26
|
+
`raf add "…"` that would put it there.
|
|
27
|
+
4. **Stop and wait** for the priority. Start nothing and commit nothing before the answer — the only
|
|
28
|
+
exception is a `raf note <id> "…"` on a silent lot whose cause is already known.
|
|
29
|
+
|
|
30
|
+
## After the answer
|
|
31
|
+
|
|
32
|
+
`raf start <id>` on the chosen lot, then the project's usual development workflow. Every commit cites
|
|
33
|
+
the lot id (`feat(L3): …`) so that the history links itself to the plan.
|
|
34
|
+
|
|
35
|
+
## Do not
|
|
36
|
+
|
|
37
|
+
- Decide for the human: close a lot, drop one, create an agent or a skill.
|
|
38
|
+
- Re-read the whole plan or recite project instructions: they are already loaded.
|