@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.
@@ -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",
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.4
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.4",
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). Use this when scaffolding a standalone task linked to an RFC without a Plan parent — e.g., a hotfix-task pattern off an open RFC, as practiced by claw-crypto's CC-43/44 and CC-045..052. If `--rfc` is omitted, `context` is left empty; the task can be attached to a Plan later via Plan formation, or `context.rfc` can be edited in by hand.
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, check whether the parent RFC can also advance to `completed`. Read the just-completed plan's `parent_rfc` (project + id), then scan all sibling plans of that RFC:
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
- # Count sibling Plans still in-flight (drafting, gate-pending, or approved)
303
- INFLIGHT=$(jq -s --arg proj "$PARENT_RFC_PROJECT" --arg id "$PARENT_RFC_ID" \
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
- # Count sibling Plans in completed (need at least one to advance)
308
- COMPLETED=$(jq -s --arg proj "$PARENT_RFC_PROJECT" --arg id "$PARENT_RFC_ID" \
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 ($COMPLETED sibling plans completed, 0 in-flight)"
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
- Auto-advance rules in plain language:
321
- - Skip entirely if the RFC's `status` is not `approved` (already terminal, already abandoned, or somehow still pre-approval — leave it).
322
- - Skip if any sibling Plan is still in-flight (`drafting`, `gate-pending`, or `approved`) more work is pending on this RFC.
323
- - Skip if no sibling Plan is `completed` (all `rejected`) — the RFC's decomposition was uniformly rejected; operator must decide whether to abandon or re-decompose.
324
- - Otherwise advance the RFC to `completed`. Idempotent: the status guard makes re-runs safe.
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.