@cloverleaf/reference-impl 0.7.4 → 0.7.5
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 +1 -1
- package/README.md +32 -0
- package/VERSION +1 -1
- package/dist/cli.mjs +22 -0
- package/dist/rfc-tasks.mjs +83 -0
- package/lib/cli.ts +21 -0
- package/lib/rfc-tasks.ts +112 -0
- package/package.json +1 -1
- package/skills/cloverleaf-new-task/SKILL.md +1 -1
- package/skills/cloverleaf-run-plan/SKILL.md +14 -19
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloverleaf",
|
|
3
3
|
"description": "Cloverleaf reference implementation — Claude Code skills for task scaffolding and the Delivery pipeline (implementer, documenter, reviewer, UI reviewer with multi-viewport visual diff, QA, merge, release).",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Renato D'Arrigo",
|
|
7
7
|
"email": "renato.darrigo@gmail.com"
|
package/README.md
CHANGED
|
@@ -166,6 +166,38 @@ npm test # run the Vitest suite
|
|
|
166
166
|
npm run test:watch
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
## Plans vs RFC-direct tasks
|
|
170
|
+
|
|
171
|
+
Cloverleaf has two ways to get from an approved RFC to executed Tasks. Both are first-class; the right one depends on the size and shape of the work.
|
|
172
|
+
|
|
173
|
+
**Plan-task (Discovery flow):** Operator invokes `/cloverleaf-discover`. The Plan agent decomposes the RFC into a Plan with a `task_dag`; the operator approves the decomposition at `task_batch_gate`; tasks are materialised under the Plan; the walker drives them through Delivery. Use this when the work spans ≥3 related tasks AND there's value in reviewing the decomposition before any task materializes (the gate is a checkpoint, not ceremony).
|
|
174
|
+
|
|
175
|
+
**RFC-direct task (skip the Plan layer):** Operator invokes `/cloverleaf-new-task --rfc=<RFC-ID> "<brief>"`. A single task is created with `context.rfc` set and `parent` absent — no Plan, no `task_batch_gate`. Use this for:
|
|
176
|
+
|
|
177
|
+
- **Hotfixes after a Plan has delivered** — a small bug or polish item surfaces after the Plan's tasks are all merged. Creating a new Plan for one task is pure overhead; an RFC-direct task is faster and equally trackable.
|
|
178
|
+
- **Incremental RFC progress without batch decomposition** — operator hasn't yet decided how to decompose the next chunk of work, but a single concrete task is clear. Create it now; defer the Plan formation until later (or skip Plans entirely if the work continues to arrive one task at a time).
|
|
179
|
+
|
|
180
|
+
### Auto-advance: how the walker treats them
|
|
181
|
+
|
|
182
|
+
When the walker (`/cloverleaf-run-plan`) finishes a Plan's final task, it asks `cloverleaf-cli rfc-tasks <repo_root> <RFC-ID>` whether the parent RFC can also advance from `approved` to `completed`. The check considers BOTH sibling Plans AND RFC-direct tasks under the same RFC:
|
|
183
|
+
|
|
184
|
+
- An **in-flight** Plan (`drafting`/`gate-pending`/`approved`) OR standalone task (any non-terminal state) blocks the RFC advance — there's still work pending under this RFC.
|
|
185
|
+
- A **delivered** Plan (`completed`) OR standalone task (`merged`) counts toward the at-least-one-delivered requirement — the RFC must have produced at least one successful piece of work to advance.
|
|
186
|
+
- If all delivered work was rejected/escalated, the operator decides: abandon the RFC, re-decompose, or accept the RFC as not-shippable. The walker won't auto-advance.
|
|
187
|
+
|
|
188
|
+
### Operator visibility
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
cloverleaf-cli rfc-tasks <repo_root> <RFC-ID> # compact JSON
|
|
192
|
+
cloverleaf-cli rfc-tasks <repo_root> <RFC-ID> --pretty # indented for humans
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Returns the RFC's status, all sibling Plans (with their child tasks), all standalone tasks, and a summary block with in-flight/delivered counts plus `can_auto_advance_rfc`. Pure read; no side effects.
|
|
196
|
+
|
|
197
|
+
### The tradeoff to name
|
|
198
|
+
|
|
199
|
+
Skipping the Plan = skipping `task_batch_gate`. That's the right tradeoff for hotfixes (one task; no decomposition to review) and for one-task-at-a-time incremental work. It's the wrong tradeoff for a large multi-task scope where the human's review of the decomposition is the load-bearing checkpoint. Plans are a checkpoint, not ceremony.
|
|
200
|
+
|
|
169
201
|
## License
|
|
170
202
|
|
|
171
203
|
MIT — see [../LICENSE](../LICENSE).
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.7.
|
|
1
|
+
0.7.5
|
package/dist/cli.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* load-rfc <repoRoot> <id>
|
|
21
21
|
* save-rfc <repoRoot> <filePath>
|
|
22
22
|
* advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]
|
|
23
|
+
* rfc-tasks <repoRoot> <rfcId> [--pretty]
|
|
23
24
|
* load-spike <repoRoot> <id>
|
|
24
25
|
* save-spike <repoRoot> <filePath>
|
|
25
26
|
* advance-spike <repoRoot> <id> <toStatus> <agent|human>
|
|
@@ -63,6 +64,7 @@ import { computeReadyTasks, detectCycle } from './dag-walker.mjs';
|
|
|
63
64
|
import { readWalkState, writeWalkState, walkStatePath } from './walk-state.mjs';
|
|
64
65
|
import { loadWalkerConfig } from './walker-config.mjs';
|
|
65
66
|
import { classifyFiles } from './scope-check.mjs';
|
|
67
|
+
import { computeRfcTasksView } from './rfc-tasks.mjs';
|
|
66
68
|
function die(msg, code = 1) {
|
|
67
69
|
process.stderr.write(msg + '\n');
|
|
68
70
|
process.exit(code);
|
|
@@ -87,6 +89,7 @@ function usage(msg) {
|
|
|
87
89
|
' load-rfc <repoRoot> <id>\n' +
|
|
88
90
|
' save-rfc <repoRoot> <filePath>\n' +
|
|
89
91
|
' advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
|
|
92
|
+
' rfc-tasks <repoRoot> <rfcId> [--pretty]\n' +
|
|
90
93
|
' load-spike <repoRoot> <id>\n' +
|
|
91
94
|
' save-spike <repoRoot> <filePath>\n' +
|
|
92
95
|
' advance-spike <repoRoot> <id> <toStatus> <agent|human>\n' +
|
|
@@ -367,6 +370,25 @@ try {
|
|
|
367
370
|
advanceRfcStatus(repoRoot, id, toStatus, actor, opts);
|
|
368
371
|
break;
|
|
369
372
|
}
|
|
373
|
+
case 'rfc-tasks': {
|
|
374
|
+
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
375
|
+
const flags = rest.filter((a) => a.startsWith('--'));
|
|
376
|
+
const [repoRoot, rfcId] = positional;
|
|
377
|
+
if (!repoRoot || !rfcId)
|
|
378
|
+
usage('rfc-tasks <repoRoot> <rfcId> [--pretty]');
|
|
379
|
+
const pretty = flags.includes('--pretty');
|
|
380
|
+
let view;
|
|
381
|
+
try {
|
|
382
|
+
view = computeRfcTasksView(repoRoot, rfcId);
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
process.stderr.write(`cloverleaf-cli rfc-tasks: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
386
|
+
process.exit(2);
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write(pretty ? JSON.stringify(view, null, 2) : JSON.stringify(view));
|
|
389
|
+
process.stdout.write('\n');
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
370
392
|
case 'load-spike': {
|
|
371
393
|
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
372
394
|
const flags = rest.filter((a) => a.startsWith('--'));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { plansDir, tasksDir, rfcsDir } from './paths.mjs';
|
|
4
|
+
/**
|
|
5
|
+
* A task is "standalone" (RFC-direct) iff it has no parent (absent or null)
|
|
6
|
+
* AND it has a non-empty context.rfc.id. See
|
|
7
|
+
* docs/superpowers/specs/2026-05-12-rfc-direct-tasks-design.md §"Discriminator".
|
|
8
|
+
*/
|
|
9
|
+
export function isStandaloneTask(task) {
|
|
10
|
+
const parent = task.parent;
|
|
11
|
+
if (parent != null)
|
|
12
|
+
return false;
|
|
13
|
+
const ctx = task.context;
|
|
14
|
+
const rfc = ctx?.rfc;
|
|
15
|
+
return !!(rfc && typeof rfc.id === 'string' && rfc.id.length > 0);
|
|
16
|
+
}
|
|
17
|
+
const PLAN_INFLIGHT = new Set(['drafting', 'gate-pending', 'approved']);
|
|
18
|
+
const TASK_TERMINAL = new Set(['merged', 'rejected', 'escalated']);
|
|
19
|
+
export function computeRfcTasksView(repoRoot, rfcId) {
|
|
20
|
+
const rfcPath = join(rfcsDir(repoRoot), `${rfcId}.json`);
|
|
21
|
+
if (!existsSync(rfcPath)) {
|
|
22
|
+
throw new Error(`rfc ${rfcId} not found at ${rfcPath}`);
|
|
23
|
+
}
|
|
24
|
+
const rfc = JSON.parse(readFileSync(rfcPath, 'utf-8'));
|
|
25
|
+
// Load all plans of this RFC + their child task statuses
|
|
26
|
+
const plans = [];
|
|
27
|
+
const plansDirPath = plansDir(repoRoot);
|
|
28
|
+
if (existsSync(plansDirPath)) {
|
|
29
|
+
for (const f of readdirSync(plansDirPath)) {
|
|
30
|
+
if (!f.endsWith('.json'))
|
|
31
|
+
continue;
|
|
32
|
+
const plan = JSON.parse(readFileSync(join(plansDirPath, f), 'utf-8'));
|
|
33
|
+
if (plan.parent_rfc?.project !== rfc.project || plan.parent_rfc.id !== rfc.id)
|
|
34
|
+
continue;
|
|
35
|
+
const tasks = [];
|
|
36
|
+
for (const node of plan.task_dag?.nodes ?? []) {
|
|
37
|
+
const taskPath = join(tasksDir(repoRoot), `${node.id}.json`);
|
|
38
|
+
if (!existsSync(taskPath))
|
|
39
|
+
continue;
|
|
40
|
+
const t = JSON.parse(readFileSync(taskPath, 'utf-8'));
|
|
41
|
+
tasks.push({ id: t.id, status: t.status });
|
|
42
|
+
}
|
|
43
|
+
plans.push({ project: plan.project, id: plan.id, status: plan.status, tasks });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
plans.sort((a, b) => a.id.localeCompare(b.id));
|
|
47
|
+
// Load standalone tasks: parent absent/null AND context.rfc matches
|
|
48
|
+
const standalone = [];
|
|
49
|
+
const tasksDirPath = tasksDir(repoRoot);
|
|
50
|
+
if (existsSync(tasksDirPath)) {
|
|
51
|
+
for (const f of readdirSync(tasksDirPath)) {
|
|
52
|
+
if (!f.endsWith('.json'))
|
|
53
|
+
continue;
|
|
54
|
+
const t = JSON.parse(readFileSync(join(tasksDirPath, f), 'utf-8'));
|
|
55
|
+
if (!isStandaloneTask(t))
|
|
56
|
+
continue;
|
|
57
|
+
const ctxRfc = t.context.rfc;
|
|
58
|
+
if (ctxRfc.project !== rfc.project || ctxRfc.id !== rfc.id)
|
|
59
|
+
continue;
|
|
60
|
+
standalone.push({ id: t.id, status: t.status });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
standalone.sort((a, b) => a.id.localeCompare(b.id));
|
|
64
|
+
const inflight_plans = plans.filter(p => PLAN_INFLIGHT.has(p.status)).length;
|
|
65
|
+
const inflight_standalone = standalone.filter(t => !TASK_TERMINAL.has(t.status)).length;
|
|
66
|
+
const delivered_plans = plans.filter(p => p.status === 'completed').length;
|
|
67
|
+
const delivered_standalone = standalone.filter(t => t.status === 'merged').length;
|
|
68
|
+
const can_auto_advance_rfc = rfc.status === 'approved' &&
|
|
69
|
+
inflight_plans + inflight_standalone === 0 &&
|
|
70
|
+
delivered_plans + delivered_standalone > 0;
|
|
71
|
+
return {
|
|
72
|
+
rfc: { project: rfc.project, id: rfc.id, status: rfc.status },
|
|
73
|
+
plans,
|
|
74
|
+
standalone_tasks: standalone,
|
|
75
|
+
summary: {
|
|
76
|
+
inflight_plans,
|
|
77
|
+
inflight_standalone,
|
|
78
|
+
delivered_plans,
|
|
79
|
+
delivered_standalone,
|
|
80
|
+
can_auto_advance_rfc,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
package/lib/cli.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* load-rfc <repoRoot> <id>
|
|
21
21
|
* save-rfc <repoRoot> <filePath>
|
|
22
22
|
* advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]
|
|
23
|
+
* rfc-tasks <repoRoot> <rfcId> [--pretty]
|
|
23
24
|
* load-spike <repoRoot> <id>
|
|
24
25
|
* save-spike <repoRoot> <filePath>
|
|
25
26
|
* advance-spike <repoRoot> <id> <toStatus> <agent|human>
|
|
@@ -66,6 +67,7 @@ import { readWalkState, writeWalkState, walkStatePath } from './walk-state.js';
|
|
|
66
67
|
import { loadWalkerConfig } from './walker-config.js';
|
|
67
68
|
import { classifyFiles } from './scope-check.js';
|
|
68
69
|
import type { SiblingScope } from './scope-check.js';
|
|
70
|
+
import { computeRfcTasksView, type RfcTasksView } from './rfc-tasks.js';
|
|
69
71
|
|
|
70
72
|
function die(msg: string, code = 1): never {
|
|
71
73
|
process.stderr.write(msg + '\n');
|
|
@@ -92,6 +94,7 @@ function usage(msg?: string): never {
|
|
|
92
94
|
' load-rfc <repoRoot> <id>\n' +
|
|
93
95
|
' save-rfc <repoRoot> <filePath>\n' +
|
|
94
96
|
' advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
|
|
97
|
+
' rfc-tasks <repoRoot> <rfcId> [--pretty]\n' +
|
|
95
98
|
' load-spike <repoRoot> <id>\n' +
|
|
96
99
|
' save-spike <repoRoot> <filePath>\n' +
|
|
97
100
|
' advance-spike <repoRoot> <id> <toStatus> <agent|human>\n' +
|
|
@@ -383,6 +386,24 @@ try {
|
|
|
383
386
|
break;
|
|
384
387
|
}
|
|
385
388
|
|
|
389
|
+
case 'rfc-tasks': {
|
|
390
|
+
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
391
|
+
const flags = rest.filter((a) => a.startsWith('--'));
|
|
392
|
+
const [repoRoot, rfcId] = positional;
|
|
393
|
+
if (!repoRoot || !rfcId) usage('rfc-tasks <repoRoot> <rfcId> [--pretty]');
|
|
394
|
+
const pretty = flags.includes('--pretty');
|
|
395
|
+
let view: RfcTasksView;
|
|
396
|
+
try {
|
|
397
|
+
view = computeRfcTasksView(repoRoot, rfcId);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
process.stderr.write(`cloverleaf-cli rfc-tasks: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
400
|
+
process.exit(2);
|
|
401
|
+
}
|
|
402
|
+
process.stdout.write(pretty ? JSON.stringify(view, null, 2) : JSON.stringify(view));
|
|
403
|
+
process.stdout.write('\n');
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
|
|
386
407
|
case 'load-spike': {
|
|
387
408
|
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
388
409
|
const flags = rest.filter((a) => a.startsWith('--'));
|
package/lib/rfc-tasks.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { plansDir, tasksDir, rfcsDir } from './paths.js';
|
|
4
|
+
import type { TaskDoc } from './task.js';
|
|
5
|
+
import type { PlanDoc } from './plan.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A task is "standalone" (RFC-direct) iff it has no parent (absent or null)
|
|
9
|
+
* AND it has a non-empty context.rfc.id. See
|
|
10
|
+
* docs/superpowers/specs/2026-05-12-rfc-direct-tasks-design.md §"Discriminator".
|
|
11
|
+
*/
|
|
12
|
+
export function isStandaloneTask(task: TaskDoc): boolean {
|
|
13
|
+
const parent = (task as Record<string, unknown>).parent;
|
|
14
|
+
if (parent != null) return false;
|
|
15
|
+
const ctx = task.context as Record<string, unknown> | undefined;
|
|
16
|
+
const rfc = ctx?.rfc as { project?: string; id?: string } | undefined;
|
|
17
|
+
return !!(rfc && typeof rfc.id === 'string' && rfc.id.length > 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RfcSummary {
|
|
21
|
+
inflight_plans: number;
|
|
22
|
+
inflight_standalone: number;
|
|
23
|
+
delivered_plans: number;
|
|
24
|
+
delivered_standalone: number;
|
|
25
|
+
can_auto_advance_rfc: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RfcTasksView {
|
|
29
|
+
rfc: { project: string; id: string; status: string };
|
|
30
|
+
plans: Array<{
|
|
31
|
+
project: string;
|
|
32
|
+
id: string;
|
|
33
|
+
status: string;
|
|
34
|
+
tasks: Array<{ id: string; status: string }>;
|
|
35
|
+
}>;
|
|
36
|
+
standalone_tasks: Array<{ id: string; status: string }>;
|
|
37
|
+
summary: RfcSummary;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PLAN_INFLIGHT = new Set(['drafting', 'gate-pending', 'approved']);
|
|
41
|
+
const TASK_TERMINAL = new Set(['merged', 'rejected', 'escalated']);
|
|
42
|
+
|
|
43
|
+
export function computeRfcTasksView(repoRoot: string, rfcId: string): RfcTasksView {
|
|
44
|
+
const rfcPath = join(rfcsDir(repoRoot), `${rfcId}.json`);
|
|
45
|
+
if (!existsSync(rfcPath)) {
|
|
46
|
+
throw new Error(`rfc ${rfcId} not found at ${rfcPath}`);
|
|
47
|
+
}
|
|
48
|
+
const rfc = JSON.parse(readFileSync(rfcPath, 'utf-8')) as {
|
|
49
|
+
project: string; id: string; status: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Load all plans of this RFC + their child task statuses
|
|
53
|
+
const plans: RfcTasksView['plans'] = [];
|
|
54
|
+
const plansDirPath = plansDir(repoRoot);
|
|
55
|
+
if (existsSync(plansDirPath)) {
|
|
56
|
+
for (const f of readdirSync(plansDirPath)) {
|
|
57
|
+
if (!f.endsWith('.json')) continue;
|
|
58
|
+
const plan = JSON.parse(readFileSync(join(plansDirPath, f), 'utf-8')) as PlanDoc;
|
|
59
|
+
if (plan.parent_rfc?.project !== rfc.project || plan.parent_rfc.id !== rfc.id) continue;
|
|
60
|
+
|
|
61
|
+
const tasks: Array<{ id: string; status: string }> = [];
|
|
62
|
+
for (const node of plan.task_dag?.nodes ?? []) {
|
|
63
|
+
const taskPath = join(tasksDir(repoRoot), `${node.id}.json`);
|
|
64
|
+
if (!existsSync(taskPath)) continue;
|
|
65
|
+
const t = JSON.parse(readFileSync(taskPath, 'utf-8')) as { id: string; status: string };
|
|
66
|
+
tasks.push({ id: t.id, status: t.status });
|
|
67
|
+
}
|
|
68
|
+
plans.push({ project: plan.project, id: plan.id, status: plan.status, tasks });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
plans.sort((a, b) => a.id.localeCompare(b.id));
|
|
72
|
+
|
|
73
|
+
// Load standalone tasks: parent absent/null AND context.rfc matches
|
|
74
|
+
const standalone: Array<{ id: string; status: string }> = [];
|
|
75
|
+
const tasksDirPath = tasksDir(repoRoot);
|
|
76
|
+
if (existsSync(tasksDirPath)) {
|
|
77
|
+
for (const f of readdirSync(tasksDirPath)) {
|
|
78
|
+
if (!f.endsWith('.json')) continue;
|
|
79
|
+
const t = JSON.parse(readFileSync(join(tasksDirPath, f), 'utf-8')) as TaskDoc;
|
|
80
|
+
if (!isStandaloneTask(t)) continue;
|
|
81
|
+
const ctxRfc = (t.context as Record<string, unknown>).rfc as {
|
|
82
|
+
project?: string; id?: string;
|
|
83
|
+
};
|
|
84
|
+
if (ctxRfc.project !== rfc.project || ctxRfc.id !== rfc.id) continue;
|
|
85
|
+
standalone.push({ id: t.id, status: t.status });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
standalone.sort((a, b) => a.id.localeCompare(b.id));
|
|
89
|
+
|
|
90
|
+
const inflight_plans = plans.filter(p => PLAN_INFLIGHT.has(p.status)).length;
|
|
91
|
+
const inflight_standalone = standalone.filter(t => !TASK_TERMINAL.has(t.status)).length;
|
|
92
|
+
const delivered_plans = plans.filter(p => p.status === 'completed').length;
|
|
93
|
+
const delivered_standalone = standalone.filter(t => t.status === 'merged').length;
|
|
94
|
+
|
|
95
|
+
const can_auto_advance_rfc =
|
|
96
|
+
rfc.status === 'approved' &&
|
|
97
|
+
inflight_plans + inflight_standalone === 0 &&
|
|
98
|
+
delivered_plans + delivered_standalone > 0;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
rfc: { project: rfc.project, id: rfc.id, status: rfc.status },
|
|
102
|
+
plans,
|
|
103
|
+
standalone_tasks: standalone,
|
|
104
|
+
summary: {
|
|
105
|
+
inflight_plans,
|
|
106
|
+
inflight_standalone,
|
|
107
|
+
delivered_plans,
|
|
108
|
+
delivered_standalone,
|
|
109
|
+
can_auto_advance_rfc,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloverleaf/reference-impl",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
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",
|
|
@@ -75,7 +75,7 @@ The user has invoked this skill with a brief. Your job: turn the brief into a st
|
|
|
75
75
|
## Rules
|
|
76
76
|
|
|
77
77
|
- Do not guess at acceptance criteria. If the brief is too vague (e.g., "make it faster" with no target), ask the user a clarifying question before writing the file.
|
|
78
|
-
- **`--rfc=<ID>` flag:** When the brief includes `--rfc=<RFC-ID>`, the task's `context.rfc` is populated from the on-disk RFC document (see step 4).
|
|
78
|
+
- **`--rfc=<ID>` flag:** When the brief includes `--rfc=<RFC-ID>`, the task's `context.rfc` is populated from the on-disk RFC document (see step 4). This is the canonical way to scaffold an **RFC-direct task** (no Plan parent, no `task_batch_gate`) — used for hotfixes after a Plan has delivered, or for incremental RFC progress without forming a Plan. The walker's RFC auto-advance treats RFC-direct tasks as first-class: an in-flight one blocks RFC completion; a merged one counts toward delivery. See `reference-impl/README.md` § "Plans vs RFC-direct tasks" for the full pattern docs and when to pick this over `/cloverleaf-discover`. If `--rfc` is omitted, `context` is left empty.
|
|
79
79
|
- **risk_class inference:** `risk_class` determines the Delivery pipeline (`"low"` → fast lane; `"high"` → full pipeline). Rules:
|
|
80
80
|
1. If the user passed `--risk=high` or `--risk=low` as a flag on the skill invocation, honor it.
|
|
81
81
|
2. Otherwise, set `risk_class: "high"` when the brief OR any acceptance criterion matches (case-insensitive) any of these keywords:
|
|
@@ -292,36 +292,29 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
|
|
|
292
292
|
|
|
293
293
|
If not every task is merged (some escalated or awaiting), do NOT advance the Plan. Print the partial-completion report from the bullets above.
|
|
294
294
|
|
|
295
|
-
**RFC auto-advance (Standard 0.6.0+).** Immediately after the Plan-advance commit lands,
|
|
295
|
+
**RFC auto-advance (Standard 0.6.0+ + rfc-tasks 0.7.5+).** Immediately after the Plan-advance commit lands, ask `cloverleaf-cli rfc-tasks` whether the parent RFC can also advance to `completed`. The CLI considers BOTH sibling Plans AND RFC-direct (standalone) tasks under the same `parent_rfc` — an in-flight standalone task blocks the advance, a merged standalone task counts toward the at-least-one-delivered requirement.
|
|
296
296
|
|
|
297
297
|
```bash
|
|
298
|
-
PARENT_RFC_PROJECT=$(jq -r '.parent_rfc.project' <repo_root>/.cloverleaf/plans/<PLAN-ID>.json)
|
|
299
298
|
PARENT_RFC_ID=$(jq -r '.parent_rfc.id' <repo_root>/.cloverleaf/plans/<PLAN-ID>.json)
|
|
300
|
-
RFC_STATUS=$(jq -r '.status' <repo_root>/.cloverleaf/rfcs/"$PARENT_RFC_ID".json)
|
|
301
299
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
'[.[] | select(.parent_rfc.project == $proj and .parent_rfc.id == $id and (.status == "drafting" or .status == "gate-pending" or .status == "approved"))] | length' \
|
|
305
|
-
<repo_root>/.cloverleaf/plans/*.json)
|
|
300
|
+
RFC_VIEW=$(cloverleaf-cli rfc-tasks <repo_root> "$PARENT_RFC_ID")
|
|
301
|
+
CAN_ADVANCE=$(echo "$RFC_VIEW" | jq -r '.summary.can_auto_advance_rfc')
|
|
306
302
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
'[.[] | select(.parent_rfc.project == $proj and .parent_rfc.id == $id and .status == "completed")] | length' \
|
|
310
|
-
<repo_root>/.cloverleaf/plans/*.json)
|
|
311
|
-
|
|
312
|
-
if [ "$RFC_STATUS" = "approved" ] && [ "$INFLIGHT" = "0" ] && [ "$COMPLETED" != "0" ]; then
|
|
303
|
+
if [ "$CAN_ADVANCE" = "true" ]; then
|
|
304
|
+
DELIVERED=$(echo "$RFC_VIEW" | jq -r '"\(.summary.delivered_plans) plans, \(.summary.delivered_standalone) standalone tasks"')
|
|
313
305
|
cloverleaf-cli advance-rfc <repo_root> "$PARENT_RFC_ID" completed agent
|
|
314
306
|
git -C <repo_root> add .cloverleaf/rfcs/"$PARENT_RFC_ID".json .cloverleaf/events/
|
|
315
|
-
git -C <repo_root> commit -m "cloverleaf: rfc $PARENT_RFC_ID completed ($
|
|
307
|
+
git -C <repo_root> commit -m "cloverleaf: rfc $PARENT_RFC_ID completed ($DELIVERED delivered, 0 in-flight)"
|
|
316
308
|
echo "✓ RFC $PARENT_RFC_ID advanced approved → completed."
|
|
317
309
|
fi
|
|
318
310
|
```
|
|
319
311
|
|
|
320
|
-
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
-
|
|
324
|
-
|
|
312
|
+
Skip conditions encoded in `can_auto_advance_rfc`:
|
|
313
|
+
- RFC is not at `approved` (already terminal, abandoned, or somehow still pre-approval).
|
|
314
|
+
- Any sibling Plan is in-flight (`drafting`, `gate-pending`, or `approved`) OR any standalone task is in a non-terminal state.
|
|
315
|
+
- No Plan reached `completed` AND no standalone task reached `merged` — the RFC has nothing delivered (e.g. all child Plans were rejected); operator must decide whether to abandon or re-decompose.
|
|
316
|
+
|
|
317
|
+
Idempotent: the `rfc.status === "approved"` guard inside `can_auto_advance_rfc` makes re-runs of `/cloverleaf-run-plan` against a fully-merged plan safe.
|
|
325
318
|
|
|
326
319
|
## Next steps (release publishing)
|
|
327
320
|
|
|
@@ -399,3 +392,5 @@ The concrete policy JSON is the same one used during the CLV-16..CLV-20 dogfood
|
|
|
399
392
|
## Notes
|
|
400
393
|
|
|
401
394
|
**Vocab dependency.** The walker reads `notification_contract.vocabulary` from the `start_session` response advisorily — if the expected tokens (`DONE`, `NEEDS-INPUT`) are absent it warns to stderr but continues. The authoritative source of truth for which tokens Session B will emit is the SDK's `--driven-tokens` flag passed when claw-drive spawns the session. A mismatch between the contract and the actual flag signals a version skew between the claw-drive server and the reference-impl skill; upgrade both components together to keep them in sync.
|
|
395
|
+
|
|
396
|
+
**RFC-direct task participation in RFC auto-advance.** Tasks with no `parent` field but with `context.rfc` set (created via `/cloverleaf-new-task --rfc=<RFC-ID>`) are *first-class* participants in the `can_auto_advance_rfc` check. They block the advance when in-flight; they count toward delivery when merged. See `cloverleaf-cli rfc-tasks <repo_root> <RFC-ID>` for the categorized view this dispatch reads from, or `reference-impl/README.md` § "Plans vs RFC-direct tasks" for the user-facing pattern docs.
|