@cloverleaf/reference-impl 0.6.6 → 0.6.7

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/dist/cli.mjs CHANGED
@@ -35,12 +35,10 @@
35
35
  * walk-state-read <repoRoot> <planId>
36
36
  * walk-state-write <repoRoot> <walkStateJsonPath>
37
37
  * walker-default-concurrency [--explain]
38
- * release-preflight <repoRoot> [--json]
39
38
  */
40
39
  import { readFileSync, mkdirSync, copyFileSync } from 'node:fs';
41
40
  import { dirname } from 'node:path';
42
41
  import { execSync } from 'node:child_process';
43
- import { runPreflightChecks } from './release-preflight.mjs';
44
42
  import { loadTask } from './task.mjs';
45
43
  import { advanceStatus } from './task.mjs';
46
44
  import { emitGateDecision } from './events.mjs';
@@ -110,11 +108,14 @@ if (!command) {
110
108
  try {
111
109
  switch (command) {
112
110
  case 'load-task': {
113
- const [repoRoot, taskId] = rest;
111
+ const positional = rest.filter((a) => !a.startsWith('--'));
112
+ const flags = rest.filter((a) => a.startsWith('--'));
113
+ const [repoRoot, taskId] = positional;
114
114
  if (!repoRoot || !taskId)
115
115
  usage('load-task requires <repoRoot> <taskId>');
116
+ const pretty = flags.includes('--pretty');
116
117
  const task = loadTask(repoRoot, taskId);
117
- process.stdout.write(JSON.stringify(task, null, 2) + '\n');
118
+ process.stdout.write((pretty ? JSON.stringify(task, null, 2) : JSON.stringify(task)) + '\n');
118
119
  break;
119
120
  }
120
121
  case 'infer-project': {
@@ -333,10 +334,14 @@ try {
333
334
  process.exit(0);
334
335
  }
335
336
  case 'load-rfc': {
336
- const [repoRoot, id] = rest;
337
+ const positional = rest.filter((a) => !a.startsWith('--'));
338
+ const flags = rest.filter((a) => a.startsWith('--'));
339
+ const [repoRoot, id] = positional;
337
340
  if (!repoRoot || !id)
338
341
  usage('load-rfc <repoRoot> <id>');
339
- process.stdout.write(JSON.stringify(loadRfc(repoRoot, id), null, 2));
342
+ const pretty = flags.includes('--pretty');
343
+ const doc = loadRfc(repoRoot, id);
344
+ process.stdout.write((pretty ? JSON.stringify(doc, null, 2) : JSON.stringify(doc)) + '\n');
340
345
  break;
341
346
  }
342
347
  case 'save-rfc': {
@@ -358,10 +363,14 @@ try {
358
363
  break;
359
364
  }
360
365
  case 'load-spike': {
361
- const [repoRoot, id] = rest;
366
+ const positional = rest.filter((a) => !a.startsWith('--'));
367
+ const flags = rest.filter((a) => a.startsWith('--'));
368
+ const [repoRoot, id] = positional;
362
369
  if (!repoRoot || !id)
363
370
  usage('load-spike <repoRoot> <id>');
364
- process.stdout.write(JSON.stringify(loadSpike(repoRoot, id), null, 2));
371
+ const pretty = flags.includes('--pretty');
372
+ const doc = loadSpike(repoRoot, id);
373
+ process.stdout.write((pretty ? JSON.stringify(doc, null, 2) : JSON.stringify(doc)) + '\n');
365
374
  break;
366
375
  }
367
376
  case 'save-spike': {
@@ -382,10 +391,14 @@ try {
382
391
  break;
383
392
  }
384
393
  case 'load-plan': {
385
- const [repoRoot, id] = rest;
394
+ const positional = rest.filter((a) => !a.startsWith('--'));
395
+ const flags = rest.filter((a) => a.startsWith('--'));
396
+ const [repoRoot, id] = positional;
386
397
  if (!repoRoot || !id)
387
398
  usage('load-plan <repoRoot> <id>');
388
- process.stdout.write(JSON.stringify(loadPlan(repoRoot, id), null, 2));
399
+ const pretty = flags.includes('--pretty');
400
+ const doc = loadPlan(repoRoot, id);
401
+ process.stdout.write((pretty ? JSON.stringify(doc, null, 2) : JSON.stringify(doc)) + '\n');
389
402
  break;
390
403
  }
391
404
  case 'save-plan': {
@@ -510,37 +523,6 @@ try {
510
523
  }
511
524
  process.exit(0);
512
525
  }
513
- case 'release-preflight': {
514
- const positional = rest.filter((a) => !a.startsWith('--'));
515
- const flags = rest.filter((a) => a.startsWith('--'));
516
- const [repoRoot] = positional;
517
- if (!repoRoot)
518
- usage('release-preflight requires <repoRoot>');
519
- const jsonMode = flags.includes('--json');
520
- const result = runPreflightChecks(repoRoot);
521
- if (jsonMode) {
522
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
523
- process.exit(0);
524
- }
525
- else {
526
- // Plain human-readable mode
527
- for (const check of result.checks) {
528
- let prefix;
529
- if (check.status === 'pass') {
530
- prefix = '✓';
531
- }
532
- else if (check.level === 'warning') {
533
- prefix = '⚠';
534
- }
535
- else {
536
- prefix = '✗';
537
- }
538
- process.stdout.write(`${prefix} ${check.id}: ${check.message}\n`);
539
- }
540
- const blockingFailed = result.checks.some((c) => c.level === 'blocking' && c.status === 'fail');
541
- process.exit(blockingFailed ? 1 : 0);
542
- }
543
- }
544
526
  default:
545
527
  usage(`Unknown command: ${command}`);
546
528
  }
package/lib/cli.ts CHANGED
@@ -35,13 +35,11 @@
35
35
  * walk-state-read <repoRoot> <planId>
36
36
  * walk-state-write <repoRoot> <walkStateJsonPath>
37
37
  * walker-default-concurrency [--explain]
38
- * release-preflight <repoRoot> [--json]
39
38
  */
40
39
 
41
40
  import { readFileSync, mkdirSync, copyFileSync } from 'node:fs';
42
41
  import { dirname } from 'node:path';
43
42
  import { execSync } from 'node:child_process';
44
- import { runPreflightChecks } from './release-preflight.js';
45
43
  import { loadTask } from './task.js';
46
44
  import { advanceStatus } from './task.js';
47
45
  import { emitGateDecision } from './events.js';
@@ -118,10 +116,13 @@ if (!command) {
118
116
  try {
119
117
  switch (command) {
120
118
  case 'load-task': {
121
- const [repoRoot, taskId] = rest;
119
+ const positional = rest.filter((a) => !a.startsWith('--'));
120
+ const flags = rest.filter((a) => a.startsWith('--'));
121
+ const [repoRoot, taskId] = positional;
122
122
  if (!repoRoot || !taskId) usage('load-task requires <repoRoot> <taskId>');
123
+ const pretty = flags.includes('--pretty');
123
124
  const task = loadTask(repoRoot, taskId);
124
- process.stdout.write(JSON.stringify(task, null, 2) + '\n');
125
+ process.stdout.write((pretty ? JSON.stringify(task, null, 2) : JSON.stringify(task)) + '\n');
125
126
  break;
126
127
  }
127
128
 
@@ -349,9 +350,13 @@ try {
349
350
  }
350
351
 
351
352
  case 'load-rfc': {
352
- const [repoRoot, id] = rest;
353
+ const positional = rest.filter((a) => !a.startsWith('--'));
354
+ const flags = rest.filter((a) => a.startsWith('--'));
355
+ const [repoRoot, id] = positional;
353
356
  if (!repoRoot || !id) usage('load-rfc <repoRoot> <id>');
354
- process.stdout.write(JSON.stringify(loadRfc(repoRoot, id), null, 2));
357
+ const pretty = flags.includes('--pretty');
358
+ const doc = loadRfc(repoRoot, id);
359
+ process.stdout.write((pretty ? JSON.stringify(doc, null, 2) : JSON.stringify(doc)) + '\n');
355
360
  break;
356
361
  }
357
362
 
@@ -373,9 +378,13 @@ try {
373
378
  }
374
379
 
375
380
  case 'load-spike': {
376
- const [repoRoot, id] = rest;
381
+ const positional = rest.filter((a) => !a.startsWith('--'));
382
+ const flags = rest.filter((a) => a.startsWith('--'));
383
+ const [repoRoot, id] = positional;
377
384
  if (!repoRoot || !id) usage('load-spike <repoRoot> <id>');
378
- process.stdout.write(JSON.stringify(loadSpike(repoRoot, id), null, 2));
385
+ const pretty = flags.includes('--pretty');
386
+ const doc = loadSpike(repoRoot, id);
387
+ process.stdout.write((pretty ? JSON.stringify(doc, null, 2) : JSON.stringify(doc)) + '\n');
379
388
  break;
380
389
  }
381
390
 
@@ -396,9 +405,13 @@ try {
396
405
  }
397
406
 
398
407
  case 'load-plan': {
399
- const [repoRoot, id] = rest;
408
+ const positional = rest.filter((a) => !a.startsWith('--'));
409
+ const flags = rest.filter((a) => a.startsWith('--'));
410
+ const [repoRoot, id] = positional;
400
411
  if (!repoRoot || !id) usage('load-plan <repoRoot> <id>');
401
- process.stdout.write(JSON.stringify(loadPlan(repoRoot, id), null, 2));
412
+ const pretty = flags.includes('--pretty');
413
+ const doc = loadPlan(repoRoot, id);
414
+ process.stdout.write((pretty ? JSON.stringify(doc, null, 2) : JSON.stringify(doc)) + '\n');
402
415
  break;
403
416
  }
404
417
 
@@ -524,36 +537,6 @@ try {
524
537
  process.exit(0);
525
538
  }
526
539
 
527
- case 'release-preflight': {
528
- const positional = rest.filter((a) => !a.startsWith('--'));
529
- const flags = rest.filter((a) => a.startsWith('--'));
530
- const [repoRoot] = positional;
531
- if (!repoRoot) usage('release-preflight requires <repoRoot>');
532
- const jsonMode = flags.includes('--json');
533
- const result = runPreflightChecks(repoRoot);
534
- if (jsonMode) {
535
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
536
- process.exit(0);
537
- } else {
538
- // Plain human-readable mode
539
- for (const check of result.checks) {
540
- let prefix: string;
541
- if (check.status === 'pass') {
542
- prefix = '✓';
543
- } else if (check.level === 'warning') {
544
- prefix = '⚠';
545
- } else {
546
- prefix = '✗';
547
- }
548
- process.stdout.write(`${prefix} ${check.id}: ${check.message}\n`);
549
- }
550
- const blockingFailed = result.checks.some(
551
- (c) => c.level === 'blocking' && c.status === 'fail',
552
- );
553
- process.exit(blockingFailed ? 1 : 0);
554
- }
555
- }
556
-
557
540
  default:
558
541
  usage(`Unknown command: ${command}`);
559
542
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloverleaf/reference-impl",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Reference implementation of the Cloverleaf methodology as Claude Code skills. Implements the Tight Loop (Implementer + Reviewer).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -111,23 +111,61 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
111
111
 
112
112
  Record the returned `session_id`, `worktree_path`, and `branch_name` in walk-state with `state: "running"`, `started_at: <now>`, `last_seq: 0`. Persist via `walk-state-write`.
113
113
 
114
- c. **Monitor live sessions.** Start `claw-drive watch <session_id> --since <last_seq> --idle-after 600` for each running session. Merge the streams into a single notification feed (e.g., the Monitor tool, or `claw-drive watch` run per-session in the background with a filter). The `--idle-after 600` flag instructs claw-drive to emit a synthetic `idle` event if a session produces no output for 600 seconds (10 minutes), enabling the walker to detect stalled sessions without polling.
114
+ Immediately after `mcp__claw-drive__start_session` returns, attach a Monitor tool stream for the new session:
115
115
 
116
- d. **Handle events.**
116
+ ```
117
+ Monitor(
118
+ watch_command: "claw-drive watch <session_id> --since 0 --idle-after 600",
119
+ persistent: true,
120
+ timeout_ms: 3600000
121
+ )
122
+ ```
123
+
124
+ This ensures the walker receives all child events without requiring Session A nudges. The `persistent: true` flag keeps the stream open across turns; `timeout_ms: 3600000` caps the watch at one hour.
125
+
126
+ c. **Monitor live sessions.** Each child session is already watched via the persistent Monitor stream attached in step 5b. When resuming after a walker restart (step 4), re-attach the Monitor with `--since <last_seq>` for each session still in `state: "running"`:
127
+
128
+ ```
129
+ Monitor(
130
+ watch_command: "claw-drive watch <session_id> --since <last_seq> --idle-after 600",
131
+ persistent: true,
132
+ timeout_ms: 3600000
133
+ )
134
+ ```
135
+
136
+ The `--idle-after 600` flag instructs claw-drive to emit a synthetic `idle` event if a session produces no output for 600 seconds (10 minutes), enabling the walker to detect stalled sessions without polling.
137
+
138
+ d. **Handle events.** Dispatch each incoming Monitor event by type:
139
+
140
+ - **`idle`** (`silent_for_ms >= 600000`) — the session has been silent for 10 minutes. For each child session emitting this event, check terminal state:
141
+ ```bash
142
+ claw-drive status <child_session_id>
143
+ ```
144
+ Read `last_token` from the status response, then branch:
145
+ - `last_token` is `[DONE]` → treat as terminal; proceed with drain (same as `session_stopped` → stopped cleanly).
146
+ - `last_token` is `[NEEDS-INPUT]` → the session is waiting for user input; surface to the user for a decision and send a reply via `mcp__claw-drive__send_turn`.
147
+ - Status output matches the transient-5xx pattern (`5\d\d\b`, `API Error: 5\d\d`, or `temporarily unavailable`) → invoke `mcp__claw-drive__send_turn` with message `'API recovered. Retry the last operation.'` to trigger self-healing.
148
+ - None of the above → the session is still working; continue waiting; do NOT auto-kill.
149
+ - **Per-session idle > 30 min** (no `idle` event received, wall-clock elapsed) → surface to user for inspection; do NOT auto-kill.
150
+
151
+ - **`tool_decision_required`** → let the walker policy decide (auto-approve per rules, defer to user for anything not covered).
152
+
153
+ - **`turn_completed [DONE]`** → the session has finished its current turn with a `[DONE]` terminal token. If the on-disk task status is `final-gate` or `automated-gates`, push onto the final-gate queue. Otherwise continue monitoring.
154
+
155
+ - **`turn_completed [NEEDS-INPUT]`** → the session is paused waiting for a user reply. Surface the assistant's last message to the driver and send the user's response via `mcp__claw-drive__send_turn`.
156
+
157
+ - **`session_stopped`** → reconcile as in step 4.
117
158
 
118
- - **tool_decision_required** → let the walker policy decide (auto-approve per rules, defer to user for anything not covered).
119
- - **turn_completed with final-gate prompt text** → push onto the final-gate queue.
120
159
  - **Escalation detected** (assistant text contains `escalated` / Reviewer/QA/UI-Reviewer bounce cap / git merge abort) → **surface to user immediately** with:
121
160
  > ⚠️ `<TASK-ID>` escalated at `<agent>` (reason: `<detail>`). Session `<session_id>`. Descendants in this Plan are now blocked until you unstick it.
122
161
  > To unstick: read feedback at `.cloverleaf/feedback/<TASK-ID>-*.json`, fix the issue, and run `/cloverleaf-run <TASK-ID>` manually. The walker will re-check on its next tick — when the task reaches `merged`, it'll pick up descendants automatically.
123
162
  Mark the task `state: "escalated"` in walk-state; do NOT queue it behind final-gate approvals; continue other branches.
124
- - **session_stopped** → reconcile as in step 4.
125
- - **idle** (`silent_for_ms >= 600000`) the session has been silent for 10 minutes. For each child session emitting this event, check whether it has reached a terminal state:
126
- ```bash
127
- claw-drive status <child_session_id>
128
- ```
129
- Read `last_token` from the status response. If `last_token` is `[DONE]` **or** the on-disk task status (`.cloverleaf/tasks/<TASK-ID>.json`) is `final-gate` or `automated-gates`, treat the session as terminal and proceed with drain (same as `session_stopped` stopped cleanly). If neither condition is met, surface to the user for inspection; do NOT auto-kill.
130
- - **Per-session idle > 30 min** (no `idle` event received, wall-clock elapsed) → surface to user for inspection; do NOT auto-kill.
163
+
164
+ **Transient-5xx self-healing.** Whenever any event's content (assistant text, error field, or status output) matches the pattern `5\d\d\b|API Error: 5\d\d|temporarily unavailable`, invoke:
165
+ ```
166
+ mcp__claw-drive__send_turn(session_id: <session_id>, text: "API recovered. Retry the last operation.")
167
+ ```
168
+ This covers transient API errors (e.g. HTTP 503, `API Error: 503`) and the `temporarily unavailable` service message without requiring a Session A nudge from the human.
131
169
 
132
170
  e. **Drain the final-gate queue serially and merge on main.** Session B does NOT invoke `/cloverleaf-merge` — it stops at automated-gates (fast lane) or final-gate (full pipeline) and reports. The walker performs the merge on main in the primary repo. For each queued task:
133
171
 
@@ -1,172 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { readFileSync, existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
-
5
- export type CheckLevel = 'blocking' | 'warning';
6
- export type CheckStatus = 'pass' | 'fail';
7
-
8
- export interface PreflightCheck {
9
- id: string;
10
- level: CheckLevel;
11
- status: CheckStatus;
12
- message: string;
13
- }
14
-
15
- export interface PreflightResult {
16
- checks: PreflightCheck[];
17
- version: string;
18
- tag: string;
19
- notes: string;
20
- }
21
-
22
- function shell(cmd: string, cwd: string): { out: string; ok: boolean } {
23
- try {
24
- const out = execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
25
- return { out: out.trim(), ok: true };
26
- } catch (err: unknown) {
27
- const e = err as { stdout?: string; stderr?: string; message?: string };
28
- const msg = (e.stderr ?? e.stdout ?? e.message ?? String(err)).trim();
29
- return { out: msg, ok: false };
30
- }
31
- }
32
-
33
- /**
34
- * Run all pre-flight checks for a release from the given repo root.
35
- * Never throws — all errors are captured into per-check `message` fields.
36
- */
37
- export function runPreflightChecks(repoRoot: string): PreflightResult {
38
- const checks: PreflightCheck[] = [];
39
-
40
- // Helper to add a check result
41
- function addCheck(
42
- id: string,
43
- level: CheckLevel,
44
- pass: boolean,
45
- failMsg: string,
46
- passMsg = 'ok',
47
- ): void {
48
- checks.push({ id, level, status: pass ? 'pass' : 'fail', message: pass ? passMsg : failMsg });
49
- }
50
-
51
- // ── Blocking checks ────────────────────────────────────────────────────────
52
-
53
- // 1. on-main: current branch must be main
54
- const branch = shell('git rev-parse --abbrev-ref HEAD', repoRoot);
55
- const isOnMain = branch.ok && branch.out === 'main';
56
- addCheck('on-main', 'blocking', isOnMain, `not on main (current: ${branch.out})`);
57
-
58
- // 2. clean-tree: no uncommitted changes
59
- const status = shell('git status --porcelain', repoRoot);
60
- const isClean = status.ok && status.out === '';
61
- addCheck('clean-tree', 'blocking', isClean, `working tree is dirty: ${status.out || status.out}`);
62
-
63
- // 3. in-sync-with-origin: no commits behind origin/main
64
- // Fetch quietly first so we can compare
65
- shell('git fetch origin main --quiet', repoRoot);
66
- const behind = shell('git rev-list --count HEAD..origin/main', repoRoot);
67
- const isSynced = behind.ok && behind.out === '0';
68
- addCheck(
69
- 'in-sync-with-origin',
70
- 'blocking',
71
- isSynced,
72
- behind.ok ? `${behind.out} commit(s) behind origin/main` : `could not check sync: ${behind.out}`,
73
- );
74
-
75
- // 4. valid-version: reference-impl/package.json has a valid semver
76
- let version = '';
77
- try {
78
- const pkgPath = join(repoRoot, 'reference-impl', 'package.json');
79
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };
80
- version = pkg.version ?? '';
81
- } catch (e: unknown) {
82
- const msg = e instanceof Error ? e.message : String(e);
83
- addCheck('valid-version', 'blocking', false, `could not read package.json: ${msg}`);
84
- version = '';
85
- }
86
- if (version !== '') {
87
- const semverRe = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
88
- const isValidSemver = semverRe.test(version);
89
- addCheck('valid-version', 'blocking', isValidSemver, `invalid semver: '${version}'`, `${version}`);
90
- }
91
-
92
- // 5. changelog-section: CHANGELOG.md has a section for this version
93
- const tag = version ? `reference-impl-v${version}` : 'reference-impl-v<unknown>';
94
- let changelogPass = false;
95
- let changelogMsg = 'no version resolved';
96
- if (version) {
97
- try {
98
- const changelogPath = join(repoRoot, 'reference-impl', 'CHANGELOG.md');
99
- const changelog = readFileSync(changelogPath, 'utf-8');
100
- // Look for a heading like "## 0.6.5" or "## [0.6.5]"
101
- const pattern = new RegExp(`^##\\s+\\[?${version.replace(/\./g, '\\.')}`, 'm');
102
- changelogPass = pattern.test(changelog);
103
- changelogMsg = changelogPass
104
- ? `section for ${version} found`
105
- : `no ## ${version} section found in CHANGELOG.md`;
106
- } catch (e: unknown) {
107
- const msg = e instanceof Error ? e.message : String(e);
108
- changelogMsg = `could not read CHANGELOG.md: ${msg}`;
109
- }
110
- }
111
- addCheck('changelog-section', 'blocking', changelogPass, changelogMsg, changelogMsg);
112
-
113
- // 6. tag-absent: the release tag must not already exist
114
- let tagAbsent = false;
115
- let tagMsg = 'no version resolved';
116
- if (version) {
117
- const localTag = shell(`git tag -l "${tag}"`, repoRoot);
118
- const remoteTag = shell(`git ls-remote --tags origin "${tag}"`, repoRoot);
119
- const existsLocally = localTag.ok && localTag.out !== '';
120
- const existsRemotely = remoteTag.ok && remoteTag.out !== '';
121
- tagAbsent = !existsLocally && !existsRemotely;
122
- if (existsLocally && existsRemotely) {
123
- tagMsg = `tag ${tag} already exists locally and on origin`;
124
- } else if (existsLocally) {
125
- tagMsg = `tag ${tag} already exists locally`;
126
- } else if (existsRemotely) {
127
- tagMsg = `tag ${tag} already exists on origin`;
128
- } else {
129
- tagMsg = `tag ${tag} is absent (ok)`;
130
- }
131
- }
132
- addCheck('tag-absent', 'blocking', tagAbsent, tagMsg, tagMsg);
133
-
134
- // ── Warning checks ─────────────────────────────────────────────────────────
135
-
136
- // 7. npm-authenticated: `npm whoami` must exit 0
137
- const npmAuth = shell('npm whoami', repoRoot);
138
- addCheck(
139
- 'npm-authenticated',
140
- 'warning',
141
- npmAuth.ok,
142
- `npm not authenticated: ${npmAuth.out}`,
143
- `logged in as ${npmAuth.out}`,
144
- );
145
-
146
- // 8. gh-authenticated: `gh auth status` must exit 0
147
- const ghAuth = shell('gh auth status', repoRoot);
148
- addCheck(
149
- 'gh-authenticated',
150
- 'warning',
151
- ghAuth.ok,
152
- `gh not authenticated: ${ghAuth.out}`,
153
- 'gh CLI authenticated',
154
- );
155
-
156
- // Derive release notes from CHANGELOG section
157
- let notes = '';
158
- if (version) {
159
- try {
160
- const changelogPath = join(repoRoot, 'reference-impl', 'CHANGELOG.md');
161
- const changelog = readFileSync(changelogPath, 'utf-8');
162
- const versionRegex = new RegExp(`^##\\s+\\[?${version.replace(/\./g, '\\.')}`, 'm');
163
- const sections = changelog.split(/\n(?=## )/);
164
- const match = sections.find((s) => versionRegex.test(s));
165
- notes = match ? match.replace(/^[^\n]*\n/, '').trim() : '';
166
- } catch {
167
- notes = '';
168
- }
169
- }
170
-
171
- return { checks, version, tag, notes };
172
- }
@@ -1,109 +0,0 @@
1
- ---
2
- name: cloverleaf-release
3
- description: Publish a new @cloverleaf/reference-impl release. Runs pre-flight checks, displays the 5-command release plan, and executes git tag -a / git push origin main / git push origin <tag> / npm publish / gh release create. Accepts [--dry-run] [--yes] flags.
4
- ---
5
-
6
- # Cloverleaf — release
7
-
8
- The user has invoked this skill, optionally with `--dry-run` and/or `--yes`.
9
-
10
- ## Args
11
-
12
- - `--dry-run` — print the pre-flight check list and the 5-command release plan, then exit 0 without executing any release command.
13
- - `--yes` — skip the interactive `y/N` prompt and execute the 5 commands unattended.
14
-
15
- ## Steps
16
-
17
- 1. **Parse flags.** Extract `--dry-run` and `--yes` from the invocation arguments if present.
18
-
19
- 2. **Capture the repo root.**
20
-
21
- ```bash
22
- REPO_ROOT=$(git rev-parse --show-toplevel)
23
- ```
24
-
25
- 3. **Run pre-flight checks.**
26
-
27
- ```bash
28
- PREFLIGHT=$(cloverleaf-cli release-preflight "$REPO_ROOT" --json)
29
- ```
30
-
31
- Parse the JSON. Extract `version`, `tag`, and `notes`.
32
-
33
- 4. **Display pre-flight check list.** For each check in `checks[]`:
34
-
35
- - Status `pass` → prefix `✓`
36
- - Status `fail` and level `warning` → prefix `⚠`
37
- - Status `fail` and level `blocking` → prefix `✗`
38
-
39
- Print one line per check: `<prefix> <id>: <message>`
40
-
41
- 5. **Bail on any blocking failure.**
42
-
43
- If any check has `level === "blocking"` and `status === "fail"`, print:
44
-
45
- ```
46
- ✗ Pre-flight failed — fix the issues above before releasing.
47
- ```
48
-
49
- And exit 1.
50
-
51
- 6. **Display release plan.**
52
-
53
- Print:
54
-
55
- ```
56
- Release plan for <tag>:
57
- 1. git tag -a <tag> -m "Release <tag>"
58
- 2. git push origin main
59
- 3. git push origin <tag>
60
- 4. cd reference-impl && npm publish --access public
61
- 5. gh release create <tag> --notes-file /tmp/release-notes-$VERSION.md
62
-
63
- Version: <version>
64
- Notes preview:
65
- <notes (first 10 lines or "(no notes)" if empty)>
66
- ```
67
-
68
- 7. **If `--dry-run`:** Print `Dry run complete — no release commands executed.` and exit 0.
69
-
70
- 8. **If not `--yes`:** Prompt:
71
-
72
- ```
73
- Proceed with release of <tag>? (y/N)
74
- ```
75
-
76
- Read a single line from the user. If the response is not `y` or `Y`, print `Aborted.` and exit 0.
77
-
78
- 9. **Write the release notes file.**
79
-
80
- ```bash
81
- VERSION=<version>
82
- printf '%s' "$NOTES" > /tmp/release-notes-$VERSION.md
83
- ```
84
-
85
- 10. **Execute the 5 release commands sequentially, bail-fast on first non-zero exit.**
86
-
87
- ```bash
88
- git tag -a "reference-impl-v$VERSION" -m "Release reference-impl-v$VERSION"
89
- git push origin main
90
- git push origin "reference-impl-v$VERSION"
91
- cd reference-impl && npm publish --access public
92
- gh release create "reference-impl-v$VERSION" --notes-file "/tmp/release-notes-$VERSION.md"
93
- ```
94
-
95
- If any command fails, print `✗ Release failed at step N: <command>` and exit 1.
96
-
97
- 11. **Report success.**
98
-
99
- ```
100
- ✓ Released reference-impl-v<version>
101
- ```
102
-
103
- ## Rules
104
-
105
- - Never skip pre-flight checks, even with `--yes`.
106
- - Warning-level check failures (`⚠`) do not block execution — they are informational only.
107
- - Do NOT modify `.cloverleaf/` — this skill only releases, it does not change task state.
108
- - The skill's working directory is the consumer's repo root.
109
- - Do not use hardcoded plugin paths — use `cloverleaf-cli` for all CLI invocations.