@cloverleaf/reference-impl 0.6.5 → 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/.claude-plugin/plugin.json +0 -6
- package/dist/cli.mjs +23 -41
- package/dist/release-preflight.mjs +4 -3
- package/lib/cli.ts +23 -40
- package/package.json +1 -1
- package/skills/cloverleaf-run-plan/SKILL.md +58 -21
- package/lib/release-preflight.ts +0 -171
- package/skills/cloverleaf-release/SKILL.md +0 -109
|
@@ -14,11 +14,5 @@
|
|
|
14
14
|
"methodology",
|
|
15
15
|
"ai-first",
|
|
16
16
|
"reference-implementation"
|
|
17
|
-
],
|
|
18
|
-
"skills": [
|
|
19
|
-
{
|
|
20
|
-
"name": "cloverleaf-release",
|
|
21
|
-
"description": "Publish a new @cloverleaf/reference-impl release. Runs pre-flight checks then executes git tag, git push, npm publish, gh release create. Accepts [--dry-run] [--yes] flags."
|
|
22
|
-
}
|
|
23
17
|
]
|
|
24
18
|
}
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -111,9 +111,10 @@ export function runPreflightChecks(repoRoot) {
|
|
|
111
111
|
try {
|
|
112
112
|
const changelogPath = join(repoRoot, 'reference-impl', 'CHANGELOG.md');
|
|
113
113
|
const changelog = readFileSync(changelogPath, 'utf-8');
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
114
|
+
const versionRegex = new RegExp(`^##\\s+\\[?${version.replace(/\./g, '\\.')}`, 'm');
|
|
115
|
+
const sections = changelog.split(/\n(?=## )/);
|
|
116
|
+
const match = sections.find((s) => versionRegex.test(s));
|
|
117
|
+
notes = match ? match.replace(/^[^\n]*\n/, '').trim() : '';
|
|
117
118
|
}
|
|
118
119
|
catch {
|
|
119
120
|
notes = '';
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -12,7 +12,7 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
|
|
|
12
12
|
```bash
|
|
13
13
|
cd <repo_root>
|
|
14
14
|
current=$(git rev-parse --abbrev-ref HEAD)
|
|
15
|
-
if [ "$current" != "main" ]; then git checkout main; fi
|
|
15
|
+
if [ "$current" != "main" ]; then git -C <repo_root> checkout main; fi
|
|
16
16
|
git status --short
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -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
|
-
|
|
114
|
+
Immediately after `mcp__claw-drive__start_session` returns, attach a Monitor tool stream for the new session:
|
|
115
115
|
|
|
116
|
-
|
|
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
|
-
|
|
125
|
-
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
|
@@ -146,15 +184,14 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
|
|
|
146
184
|
|
|
147
185
|
First, **guard against conflict markers** — scan every file changed on the task branch for unresolved conflict markers before attempting the merge:
|
|
148
186
|
```bash
|
|
149
|
-
|
|
150
|
-
git
|
|
151
|
-
CHANGED_FILES=$(git diff --name-only main..cloverleaf/<TASK-ID>)
|
|
187
|
+
git -C <repo_root> checkout main
|
|
188
|
+
CHANGED_FILES=$(git -C <repo_root> diff --name-only main..cloverleaf/<TASK-ID>)
|
|
152
189
|
if [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | xargs grep -l -E '^(<{7}|={7}|>{7})' 2>/dev/null | grep -q .; then
|
|
153
190
|
echo "ERROR: conflict markers found in changed files — aborting merge for <TASK-ID>"
|
|
154
191
|
echo "$CHANGED_FILES" | xargs grep -l -E '^(<{7}|={7}|>{7})' 2>/dev/null
|
|
155
192
|
# Do NOT proceed; mark task escalated and surface to user
|
|
156
193
|
else
|
|
157
|
-
git merge --no-ff cloverleaf/<TASK-ID> -m "cloverleaf: <TASK-ID> merged (<fast_lane | full_pipeline>)"
|
|
194
|
+
git -C <repo_root> merge --no-ff cloverleaf/<TASK-ID> -m "cloverleaf: <TASK-ID> merged (<fast_lane | full_pipeline>)"
|
|
158
195
|
fi
|
|
159
196
|
```
|
|
160
197
|
|
|
@@ -170,11 +207,11 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
|
|
|
170
207
|
cloverleaf-cli advance-status <repo_root> <TASK-ID> merged human final_approval_gate full_pipeline
|
|
171
208
|
```
|
|
172
209
|
```bash
|
|
173
|
-
git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> merged"
|
|
210
|
+
git -C <repo_root> add .cloverleaf/ && git -C <repo_root> commit -m "cloverleaf: <TASK-ID> merged"
|
|
174
211
|
```
|
|
175
212
|
Capture the merge commit SHA:
|
|
176
213
|
```bash
|
|
177
|
-
MERGE_COMMIT=$(git rev-parse HEAD)
|
|
214
|
+
MERGE_COMMIT=$(git -C <repo_root> rev-parse HEAD)
|
|
178
215
|
```
|
|
179
216
|
Immediately update walk-state to record the successful merge (bug #7 fix — walk-state must reflect `merged` state):
|
|
180
217
|
```bash
|
|
@@ -207,9 +244,9 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
|
|
|
207
244
|
Once all tasks are merged, run the following commands in order to tag and publish the release:
|
|
208
245
|
|
|
209
246
|
```bash
|
|
210
|
-
git tag -a reference-impl-v<VERSION> -m "reference-impl v<VERSION>"
|
|
211
|
-
git push origin main
|
|
212
|
-
git push origin reference-impl-v<VERSION>
|
|
247
|
+
git -C <repo_root> tag -a reference-impl-v<VERSION> -m "reference-impl v<VERSION>"
|
|
248
|
+
git -C <repo_root> push origin main
|
|
249
|
+
git -C <repo_root> push origin reference-impl-v<VERSION>
|
|
213
250
|
(cd reference-impl && npm publish --access public)
|
|
214
251
|
gh release create reference-impl-v<VERSION> --title "reference-impl v<VERSION>" --notes-from-tag
|
|
215
252
|
```
|
package/lib/release-preflight.ts
DELETED
|
@@ -1,171 +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 pattern = new RegExp(`^##\\s+\\[?${version.replace(/\./g, '\\.')}[^\\n]*\\n([\\s\\S]*?)(?=^##\\s|$)`, 'm');
|
|
163
|
-
const m = changelog.match(pattern);
|
|
164
|
-
notes = m ? m[1].trim() : '';
|
|
165
|
-
} catch {
|
|
166
|
-
notes = '';
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return { checks, version, tag, notes };
|
|
171
|
-
}
|
|
@@ -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.
|