@cloverleaf/reference-impl 0.7.3 → 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.
@@ -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.6.5",
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.6.0
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('--'));
@@ -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",
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",
@@ -40,6 +40,7 @@
40
40
  "cloverleaf-cli": "./dist/cli.mjs"
41
41
  },
42
42
  "scripts": {
43
+ "pretest": "node scripts/check-standard-prepped.mjs",
43
44
  "test": "tsc --noEmit && vitest run",
44
45
  "test:watch": "vitest",
45
46
  "typecheck": "tsc --noEmit",
@@ -48,7 +49,7 @@
48
49
  "prepublishOnly": "node scripts/check-standard-prepped.mjs && npm test && npm run build"
49
50
  },
50
51
  "dependencies": {
51
- "@cloverleaf/standard": "^0.5.0",
52
+ "@cloverleaf/standard": "^0.6.0",
52
53
  "ajv": "^8.17.1",
53
54
  "ajv-formats": "^3.0.1",
54
55
  "axe-core": "^4.10.0",
@@ -32,13 +32,23 @@ The user has invoked this skill with a brief. Your job: turn the brief into a st
32
32
  "owner": { "kind": "agent", "id": "implementer" },
33
33
  "project": "<project>",
34
34
  "title": "<concise title derived from brief>",
35
- "context": {},
35
+ "context": <see "context.rfc injection" below>,
36
36
  "acceptance_criteria": ["<criterion 1>", "<criterion 2>", "..."],
37
37
  "definition_of_done": ["<terminal statement of completion>"],
38
38
  "risk_class": "low"
39
39
  }
40
40
  ```
41
41
 
42
+ **`context.rfc` injection:**
43
+
44
+ - If the brief includes `--rfc=<RFC-ID>` (e.g. `/cloverleaf-new-task --rfc=CLV-9 "Brief text..."`), read `<repo_root>/.cloverleaf/rfcs/<RFC-ID>.json` and inject the workItemRef shape:
45
+ ```json
46
+ "context": { "rfc": { "project": "<rfc-project-field>", "id": "<RFC-ID>" } }
47
+ ```
48
+ The `project` field comes from the loaded RFC document, NOT the task's own project (a task in project FOO may legitimately reference an RFC in project BAR).
49
+ - If `<repo_root>/.cloverleaf/rfcs/<RFC-ID>.json` does not exist, abort and ask the user to verify the RFC ID. Do not write the task file.
50
+ - If `--rfc=<ID>` is not passed, leave `context` as `{}` — same as pre-v0.7.4 behavior.
51
+
42
52
  Derive 2-5 acceptance criteria from the brief. Each must be verifiable. Derive one or more Definition of Done strings as an array.
43
53
 
44
54
  5. Write the file to `<repo_root>/.cloverleaf/tasks/<allocated-id>.json`.
@@ -65,6 +75,7 @@ The user has invoked this skill with a brief. Your job: turn the brief into a st
65
75
  ## Rules
66
76
 
67
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). 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.
68
79
  - **risk_class inference:** `risk_class` determines the Delivery pipeline (`"low"` → fast lane; `"high"` → full pipeline). Rules:
69
80
  1. If the user passed `--risk=high` or `--risk=low` as a flag on the skill invocation, honor it.
70
81
  2. Otherwise, set `risk_class: "high"` when the brief OR any acceptance criterion matches (case-insensitive) any of these keywords:
@@ -280,7 +280,41 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
280
280
  - `awaiting_final_gate: [ ... ]` — user said `n`; re-invoke `/cloverleaf-merge <TASK-ID>` to retry.
281
281
  - `unreachable: [ ... ]` — descendants of escalated tasks.
282
282
 
283
- If every task in the plan's `task_dag.nodes` has `state: "merged"`, print: "✓ Plan `<PLAN-ID>` complete."
283
+ **Plan completion (Standard 0.6.0+).** If every task in the plan's `task_dag.nodes` has `state: "merged"` AND the Plan's on-disk `status` is `"approved"`, advance the Plan to `completed`:
284
+
285
+ ```bash
286
+ cloverleaf-cli advance-plan <repo_root> <PLAN-ID> completed agent
287
+ git -C <repo_root> add .cloverleaf/plans/<PLAN-ID>.json .cloverleaf/events/
288
+ git -C <repo_root> commit -m "cloverleaf: plan <PLAN-ID> completed (all tasks merged)"
289
+ ```
290
+
291
+ This closes the previous state-sync gap where Plans whose tasks were all merged stayed at `status: "approved"` indefinitely. Skip the advance if the Plan is already at `completed` (idempotent re-runs of `/cloverleaf-run-plan` on a fully-merged plan must not error). After the advance, print: "✓ Plan `<PLAN-ID>` completed (status advanced approved → completed)."
292
+
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
+
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
+
297
+ ```bash
298
+ PARENT_RFC_ID=$(jq -r '.parent_rfc.id' <repo_root>/.cloverleaf/plans/<PLAN-ID>.json)
299
+
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')
302
+
303
+ if [ "$CAN_ADVANCE" = "true" ]; then
304
+ DELIVERED=$(echo "$RFC_VIEW" | jq -r '"\(.summary.delivered_plans) plans, \(.summary.delivered_standalone) standalone tasks"')
305
+ cloverleaf-cli advance-rfc <repo_root> "$PARENT_RFC_ID" completed agent
306
+ git -C <repo_root> add .cloverleaf/rfcs/"$PARENT_RFC_ID".json .cloverleaf/events/
307
+ git -C <repo_root> commit -m "cloverleaf: rfc $PARENT_RFC_ID completed ($DELIVERED delivered, 0 in-flight)"
308
+ echo "✓ RFC $PARENT_RFC_ID advanced approved → completed."
309
+ fi
310
+ ```
311
+
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.
284
318
 
285
319
  ## Next steps (release publishing)
286
320
 
@@ -358,3 +392,5 @@ The concrete policy JSON is the same one used during the CLV-16..CLV-20 dogfood
358
392
  ## Notes
359
393
 
360
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.