create-claude-cabinet 0.25.2 → 0.25.4

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/lib/cli.js CHANGED
@@ -875,6 +875,7 @@ async function run() {
875
875
  const alwaysCopyPhases = [
876
876
  'skills/onboard', 'skills/seed',
877
877
  'skills/cc-upgrade', 'skills/cc-extract',
878
+ 'skills/verify',
878
879
  ];
879
880
  const isSkill = tmpl.startsWith('skills/') && !alwaysCopyPhases.some(p => tmpl.startsWith(p));
880
881
  const results = await copyTemplates(srcPath, destPath, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -14,7 +14,6 @@ INPUT="$CLAUDE_TOOL_INPUT"
14
14
  FID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('fid',''))" 2>/dev/null)
15
15
 
16
16
  if [ -z "$FID" ]; then
17
- echo '{"decision":"allow"}'
18
17
  exit 0
19
18
  fi
20
19
 
@@ -43,4 +42,4 @@ if [ "$AC_VERIFIED" != "True" ]; then
43
42
  exit 0
44
43
  fi
45
44
 
46
- echo '{"decision":"allow"}'
45
+ exit 0
@@ -47,4 +47,4 @@ if ! echo "$NOTES" | grep -qiE '(## Surface|files:|dirs:)'; then
47
47
  exit 0
48
48
  fi
49
49
 
50
- echo '{"decision":"allow"}'
50
+ exit 0
@@ -14,13 +14,14 @@
14
14
  #
15
15
  # Hook contract:
16
16
  # Input: $CLAUDE_TOOL_INPUT has the tool use JSON with "file_path" field
17
- # Output: JSON on stdout with { "decision": "block"|"allow", "reason": "..." }
17
+ # Output: JSON on stdout with { "decision": "block", "reason": "..." }
18
+ # when blocking. Otherwise empty stdout + exit 0 (allow is the
19
+ # default; emitting "allow" violates the hook output schema).
18
20
 
19
21
  # Extract file_path from tool input
20
22
  FILE_PATH=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('file_path',''))" 2>/dev/null)
21
23
 
22
24
  if [ -z "$FILE_PATH" ]; then
23
- echo '{"decision":"allow"}'
24
25
  exit 0
25
26
  fi
26
27
 
@@ -42,7 +43,6 @@ PROJECT_ROOT=$(find_project_root)
42
43
 
43
44
  if [ -z "$PROJECT_ROOT" ]; then
44
45
  # No .ccrc.json found — not a CC project, allow everything
45
- echo '{"decision":"allow"}'
46
46
  exit 0
47
47
  fi
48
48
 
@@ -53,7 +53,6 @@ if [[ "$FILE_PATH" = /* ]]; then
53
53
  REL_PATH="${FILE_PATH#$PROJECT_ROOT/}"
54
54
  # If the path didn't change, the file is outside the project
55
55
  if [ "$REL_PATH" = "$FILE_PATH" ]; then
56
- echo '{"decision":"allow"}'
57
56
  exit 0
58
57
  fi
59
58
  else
@@ -74,6 +73,5 @@ except:
74
73
 
75
74
  if [ "$IN_MANIFEST" = "yes" ]; then
76
75
  echo "{\"decision\":\"block\",\"reason\":\"Blocked: $REL_PATH is managed by Claude Cabinet. CC-managed files are upstream-owned — edits come through /cc-upgrade, not direct modification. Put project-specific content in briefing files or phase files instead.\"}"
77
- else
78
- echo '{"decision":"allow"}'
79
76
  fi
77
+ exit 0
@@ -9,13 +9,15 @@
9
9
  #
10
10
  # Hook contract:
11
11
  # Input: $CLAUDE_TOOL_INPUT has the tool use JSON with "command" field
12
- # Output: JSON on stdout with { "decision": "block"|"allow", "reason": "..." }
12
+ # Output: JSON on stdout with { "decision": "block", "reason": "..." }
13
+ # when blocking. Otherwise empty stdout + exit 0 (Claude Code
14
+ # treats no-output as allow; emitting "allow" violates the
15
+ # hook output schema).
13
16
 
14
17
  # Read the command from the tool input
15
18
  COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('command',''))" 2>/dev/null)
16
19
 
17
20
  if [ -z "$COMMAND" ]; then
18
- echo '{"decision":"allow"}'
19
21
  exit 0
20
22
  fi
21
23
 
@@ -62,6 +64,5 @@ RESULT=$(check_command "$COMMAND")
62
64
 
63
65
  if [ -n "$RESULT" ]; then
64
66
  echo "$RESULT"
65
- else
66
- echo '{"decision":"allow"}'
67
67
  fi
68
+ exit 0
@@ -16,13 +16,14 @@
16
16
  #
17
17
  # Hook contract:
18
18
  # Input: $CLAUDE_TOOL_INPUT has the tool use JSON with "file_path" field
19
- # Output: JSON on stdout with { "decision": "block"|"allow", "reason": "..." }
19
+ # Output: JSON on stdout with { "decision": "block", "reason": "..." }
20
+ # when blocking. Otherwise empty stdout + exit 0 (allow is the
21
+ # default; emitting "allow" violates the hook output schema).
20
22
 
21
23
  # Extract file_path from tool input
22
24
  FILE_PATH=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('file_path',''))" 2>/dev/null)
23
25
 
24
26
  if [ -z "$FILE_PATH" ]; then
25
- echo '{"decision":"allow"}'
26
27
  exit 0
27
28
  fi
28
29
 
@@ -30,21 +31,18 @@ fi
30
31
  # Note: case patterns with * don't cross / boundaries in some shells,
31
32
  # so we use [[ ]] substring matching for absolute path compatibility.
32
33
  if [[ "$FILE_PATH" != *"/.claude/memory/"* ]] && [[ "$FILE_PATH" != *"/.claude/projects/"*"/memory/"* ]]; then
33
- echo '{"decision":"allow"}'
34
34
  exit 0
35
35
  fi
36
36
 
37
37
  # Allow MEMORY.md index files (structural, not memory content)
38
38
  BASENAME=$(basename "$FILE_PATH")
39
39
  if [ "$BASENAME" = "MEMORY.md" ]; then
40
- echo '{"decision":"allow"}'
41
40
  exit 0
42
41
  fi
43
42
 
44
43
  # Allow pattern files (enforcement pipeline artifacts, not semantic memories)
45
44
  case "$FILE_PATH" in
46
45
  */memory/patterns/*)
47
- echo '{"decision":"allow"}'
48
46
  exit 0
49
47
  ;;
50
48
  esac
@@ -53,7 +51,6 @@ esac
53
51
  OMEGA_PYTHON="$HOME/.claude-cabinet/omega-venv/bin/python3"
54
52
  if [ ! -x "$OMEGA_PYTHON" ]; then
55
53
  # Omega not available — flat markdown IS the correct fallback
56
- echo '{"decision":"allow"}'
57
54
  exit 0
58
55
  fi
59
56
 
@@ -73,7 +70,6 @@ find_adapter() {
73
70
  ADAPTER=$(find_adapter)
74
71
  if [ -z "$ADAPTER" ]; then
75
72
  # No adapter found — flat markdown fallback is correct
76
- echo '{"decision":"allow"}'
77
73
  exit 0
78
74
  fi
79
75
 
@@ -10,7 +10,6 @@ INPUT="$CLAUDE_TOOL_INPUT"
10
10
  COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('command',''))" 2>/dev/null)
11
11
 
12
12
  if [ -z "$COMMAND" ]; then
13
- echo '{"decision":"allow"}'
14
13
  exit 0
15
14
  fi
16
15
 
@@ -19,20 +18,18 @@ PHASE_FILE=".claude/skills/hooks/phases/work-tracker-guard.md"
19
18
  if [ -f "$PHASE_FILE" ]; then
20
19
  FIRST_LINE=$(head -1 "$PHASE_FILE")
21
20
  if [ "$FIRST_LINE" = "skip: true" ]; then
22
- echo '{"decision":"allow"}'
23
21
  exit 0
24
22
  fi
25
23
  fi
26
24
 
27
25
  # Check for SQL operations against actions table
28
26
  if echo "$COMMAND" | grep -qiE '(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+actions'; then
29
- # Override escape hatch
27
+ # Override escape hatch — allow is the default, just exit 0
30
28
  if echo "$COMMAND" | grep -q '\-\-force-raw-sql'; then
31
- echo '{"decision":"allow","reason":"Raw SQL override acknowledged. Quality gates bypassed."}'
32
29
  exit 0
33
30
  fi
34
31
  echo '{"decision":"block","reason":"Raw SQL against actions table detected. Use MCP tools instead: pib_create_action, pib_update_action, pib_complete_action, pib_get_action. These enforce quality gates. To override (almost certainly wrong): add --force-raw-sql to your command."}'
35
32
  exit 0
36
33
  fi
37
34
 
38
- echo '{"decision":"allow"}'
35
+ exit 0
@@ -1,21 +1,53 @@
1
1
  /**
2
- * Minimal triage server — serves the triage UI and holds findings/verdicts in memory.
2
+ * Minimal triage server — serves the triage UI and holds findings/verdicts.
3
3
  *
4
4
  * Claude POSTs findings, user triages in browser, Claude GETs verdicts.
5
+ * Each verdict is written through to disk as the user makes it, so page
6
+ * reloads or server restarts don't lose in-progress triage decisions.
5
7
  *
6
8
  * Usage: node triage-server.mjs [--port 3457]
7
9
  */
8
10
 
9
11
  import { createServer } from 'node:http';
10
- import { readFile } from 'node:fs/promises';
12
+ import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
11
13
  import { fileURLToPath } from 'node:url';
12
14
  import { dirname, join } from 'node:path';
13
15
 
14
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
17
  const PORT = parseInt(process.argv.find((_, i, a) => a[i - 1] === '--port') || '3457');
18
+ const DRAFT_PATH = join(process.cwd(), '.claude', 'triage-draft.json');
16
19
 
17
20
  let currentFindings = [];
18
21
  let currentVerdicts = null;
22
+ let drafts = {}; // { findingId: { verdict, feedback } }
23
+
24
+ async function loadDrafts() {
25
+ try {
26
+ drafts = JSON.parse(await readFile(DRAFT_PATH, 'utf-8'));
27
+ } catch {
28
+ drafts = {};
29
+ }
30
+ }
31
+
32
+ async function saveDrafts() {
33
+ await mkdir(dirname(DRAFT_PATH), { recursive: true });
34
+ await writeFile(DRAFT_PATH, JSON.stringify(drafts, null, 2));
35
+ }
36
+
37
+ function dedupById(items) {
38
+ const seen = new Set();
39
+ const out = [];
40
+ for (const f of items) {
41
+ if (!f || !f.id || seen.has(f.id)) continue;
42
+ seen.add(f.id);
43
+ out.push(f);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ function withDrafts(findings) {
49
+ return findings.map((f) => ({ ...f, draft: drafts[f.id] || null }));
50
+ }
19
51
 
20
52
  function json(res, data, status = 200) {
21
53
  res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
@@ -53,21 +85,47 @@ const server = createServer(async (req, res) => {
53
85
  for await (const chunk of req) body += chunk;
54
86
  try {
55
87
  const data = JSON.parse(body);
56
- currentFindings = data.findings || data;
57
- currentVerdicts = null; // Reset verdicts for new batch
58
- console.log(`Loaded ${currentFindings.length} findings`);
88
+ const raw = data.findings || data;
89
+ currentFindings = dedupById(Array.isArray(raw) ? raw : []);
90
+ currentVerdicts = null;
91
+ // Trim drafts to the new finding id set so a fresh batch doesn't
92
+ // surface stale entries, but a re-POST of the same batch preserves
93
+ // in-progress work.
94
+ const idSet = new Set(currentFindings.map((f) => f.id));
95
+ drafts = Object.fromEntries(Object.entries(drafts).filter(([id]) => idSet.has(id)));
96
+ await saveDrafts();
97
+ console.log(`Loaded ${currentFindings.length} findings (${Object.keys(drafts).length} drafts retained)`);
59
98
  return json(res, { ok: true, count: currentFindings.length });
60
99
  } catch (err) {
61
100
  return json(res, { error: 'Invalid JSON' }, 400);
62
101
  }
63
102
  }
64
103
 
65
- // GET /api/findings — UI fetches findings
104
+ // GET /api/findings — UI fetches findings (with any saved drafts attached)
66
105
  if (req.method === 'GET' && url.pathname === '/api/findings') {
67
- return json(res, { findings: currentFindings });
106
+ return json(res, { findings: withDrafts(currentFindings) });
107
+ }
108
+
109
+ // POST /api/verdict — UI writes a single verdict as the user makes it
110
+ if (req.method === 'POST' && url.pathname === '/api/verdict') {
111
+ let body = '';
112
+ for await (const chunk of req) body += chunk;
113
+ try {
114
+ const { id, verdict, feedback } = JSON.parse(body);
115
+ if (!id) return json(res, { error: 'Missing id' }, 400);
116
+ const existing = drafts[id] || { verdict: '', feedback: '' };
117
+ drafts[id] = {
118
+ verdict: verdict !== undefined ? verdict : existing.verdict,
119
+ feedback: feedback !== undefined ? feedback : existing.feedback,
120
+ };
121
+ await saveDrafts();
122
+ return json(res, { ok: true });
123
+ } catch (err) {
124
+ return json(res, { error: 'Invalid JSON' }, 400);
125
+ }
68
126
  }
69
127
 
70
- // POST /api/verdicts — UI submits verdicts
128
+ // POST /api/verdicts — UI submits final verdicts batch
71
129
  if (req.method === 'POST' && url.pathname === '/api/verdicts') {
72
130
  let body = '';
73
131
  for await (const chunk of req) body += chunk;
@@ -80,7 +138,7 @@ const server = createServer(async (req, res) => {
80
138
  }
81
139
  }
82
140
 
83
- // GET /api/verdicts — Claude reads verdicts
141
+ // GET /api/verdicts — Claude reads submitted verdicts
84
142
  if (req.method === 'GET' && url.pathname === '/api/verdicts') {
85
143
  if (!currentVerdicts) {
86
144
  return json(res, { submitted: false, message: 'No verdicts submitted yet' });
@@ -88,11 +146,23 @@ const server = createServer(async (req, res) => {
88
146
  return json(res, currentVerdicts);
89
147
  }
90
148
 
149
+ // DELETE /api/draft — clear in-progress draft state
150
+ if (req.method === 'DELETE' && url.pathname === '/api/draft') {
151
+ drafts = {};
152
+ try {
153
+ await unlink(DRAFT_PATH);
154
+ } catch {}
155
+ return json(res, { ok: true });
156
+ }
157
+
91
158
  // 404
92
159
  res.writeHead(404);
93
160
  res.end('Not found');
94
161
  });
95
162
 
163
+ await loadDrafts();
96
164
  server.listen(PORT, () => {
97
165
  console.log(`Triage server running at http://localhost:${PORT}`);
166
+ const draftCount = Object.keys(drafts).length;
167
+ if (draftCount) console.log(`Restored ${draftCount} in-progress draft verdict(s) from ${DRAFT_PATH}`);
98
168
  });
@@ -285,13 +285,42 @@
285
285
  let findings = [];
286
286
  const verdicts = {}; // { findingId: { verdict, feedback } }
287
287
  const memberNotes = {}; // { member: { comment, question } }
288
+ const feedbackDebounce = {}; // { findingId: timeoutId }
289
+
290
+ function dedupById(items) {
291
+ const seen = new Set();
292
+ const out = [];
293
+ for (const f of items) {
294
+ if (!f || !f.id || seen.has(f.id)) continue;
295
+ seen.add(f.id);
296
+ out.push(f);
297
+ }
298
+ return out;
299
+ }
300
+
301
+ // POST a verdict write-through to the server (silent on failure
302
+ // — write-through is best-effort; submit remains the source of truth)
303
+ function persistVerdict(id) {
304
+ const v = verdicts[id] || { verdict: '', feedback: '' };
305
+ fetch('/api/verdict', {
306
+ method: 'POST',
307
+ headers: { 'Content-Type': 'application/json' },
308
+ body: JSON.stringify({ id, verdict: v.verdict, feedback: v.feedback }),
309
+ }).catch(() => {});
310
+ }
288
311
 
289
312
  // ── Public API for Claude (javascript_tool) ──
290
313
  window.loadFindings = function(data) {
291
- findings = Array.isArray(data) ? data : [];
292
- // Pre-populate from recommendations
314
+ findings = dedupById(Array.isArray(data) ? data : []);
315
+ // Hydrate from server-persisted drafts first; recommendations only
316
+ // fill in when there's no prior draft.
293
317
  findings.forEach(f => {
294
- if (f.recommendation && !verdicts[f.id]) {
318
+ if (f.draft && f.draft.verdict !== undefined) {
319
+ verdicts[f.id] = {
320
+ verdict: f.draft.verdict || '',
321
+ feedback: f.draft.feedback || '',
322
+ };
323
+ } else if (f.recommendation && !verdicts[f.id]) {
295
324
  verdicts[f.id] = { verdict: f.recommendation, feedback: '' };
296
325
  }
297
326
  });
@@ -476,6 +505,7 @@
476
505
  const v = e.target.dataset.v;
477
506
  if (!verdicts[id]) verdicts[id] = { verdict: '', feedback: '' };
478
507
  verdicts[id].verdict = v;
508
+ persistVerdict(id);
479
509
  render();
480
510
  }
481
511
  // Bulk buttons
@@ -485,6 +515,7 @@
485
515
  findings.filter(f => f['cabinet-member'] === member).forEach(f => {
486
516
  if (!verdicts[f.id]) verdicts[f.id] = { verdict: '', feedback: '' };
487
517
  verdicts[f.id].verdict = v;
518
+ persistVerdict(f.id);
488
519
  });
489
520
  render();
490
521
  }
@@ -504,6 +535,9 @@
504
535
  const id = e.target.dataset.id;
505
536
  if (!verdicts[id]) verdicts[id] = { verdict: '', feedback: '' };
506
537
  verdicts[id].feedback = e.target.value;
538
+ // Debounce write-through so we don't POST on every keystroke
539
+ clearTimeout(feedbackDebounce[id]);
540
+ feedbackDebounce[id] = setTimeout(() => persistVerdict(id), 400);
507
541
  }
508
542
  if (e.target.classList.contains('member-input')) {
509
543
  const p = e.target.dataset.member;
@@ -74,16 +74,18 @@ For each remaining match, emit one Attention Items entry:
74
74
 
75
75
  > **`<fid>`** — `<action text>`
76
76
  > Pending plan touches UI but lacks a `## Verify Plan` section.
77
- > Suggest: `/plan <fid>` to backfill the section, or accept drift —
78
- > `/debrief` will flag this act on completion if it ships uncovered.
77
+ > Suggest: `/verify backfill <fid>` to draft the section interactively,
78
+ > or accept drift — `/debrief` will flag this act on completion if
79
+ > it ships uncovered.
79
80
 
80
81
  The entries go in the briefing's **Attention Items** section,
81
82
  alongside any items surfaced by deferred-check, health-checks, etc.
82
83
 
83
84
  ## What this phase does NOT do
84
85
 
85
- - It does not modify action notes. The operator runs `/plan <fid>`
86
- to backfill, or chooses to accept the drift.
86
+ - It does not modify action notes. The operator runs
87
+ `/verify backfill <fid>` to backfill, or chooses to accept the
88
+ drift.
87
89
  - It does not file new actions or projects.
88
90
  - It does not block orient. Even with 5 backfill candidates, orient
89
91
  completes; the Attention Items accumulate.
@@ -4,9 +4,10 @@ description: |
4
4
  Walkthrough verification harness. Cucumber + Playwright scenarios with
5
5
  human-in-the-loop verdict pauses (P/I/S/N) for subjective checks.
6
6
  Subcommands: bare (run the suite), 'learn' (bootstrap from cold start),
7
- 'update <change>' (sync feature files to a code change). Use when:
8
- "verify", "/verify", "/verify learn", "/verify update", "run walkthrough",
9
- "verify scenarios", after a multi-PR umbrella ships.
7
+ 'update <change>' (sync feature files to a code change), 'backfill <fid>'
8
+ (add a Verify Plan section to a pending action's notes). Use when:
9
+ "verify", "/verify", "/verify learn", "/verify update", "/verify backfill",
10
+ "run walkthrough", "verify scenarios", after a multi-PR umbrella ships.
10
11
  related:
11
12
  - type: file
12
13
  path: .claude/skills/verify/phases/discover.md
@@ -26,10 +27,13 @@ related:
26
27
  - type: file
27
28
  path: .claude/skills/verify/phases/scenario-template.md
28
29
  role: "Gherkin scenario template (cost+role tags, NN.NN checkIds)"
30
+ - type: file
31
+ path: .claude/skills/verify/phases/backfill.md
32
+ role: "How to draft a ## Verify Plan section for a pending action"
29
33
  - type: file
30
34
  path: cabinet/_briefing.md
31
35
  role: "Project identity and configuration"
32
- argument-hint: "subcommand — 'learn', 'update <change>', or empty to run"
36
+ argument-hint: "subcommand — 'learn', 'update <change>', 'backfill <fid>', or empty to run"
33
37
  user-invocable: true
34
38
  standing-mandate: []
35
39
  ---
@@ -47,6 +51,13 @@ If `$ARGUMENTS` is provided:
47
51
  extending an existing scenario set.
48
52
  - **'update <change-description>'**: Sync feature files to a code change.
49
53
  The change can be a pib-db action fid, a diff snippet, or free-text.
54
+ - **'backfill <fid>'**: Add a `## Verify Plan` section to a pending action's
55
+ notes. For actions that were planned before the verify module existed,
56
+ or planned without Verify Plan questions surfaced. Reads the existing
57
+ notes, interviews the user one question at a time about UI surface and
58
+ feature-file edits, drafts the section, and appends via
59
+ `pib_update_action`. Does NOT modify feature files — that's `/execute`'s
60
+ job at action ship time.
50
61
 
51
62
  ## Purpose
52
63
 
@@ -175,6 +186,30 @@ may want a new scenario, not an edit to an existing one).
175
186
  The proposed edits are presented inline. User approves, the skill
176
187
  writes them.
177
188
 
189
+ ### Mode D: `/verify backfill <fid>` — add Verify Plan to pending action
190
+
191
+ Read `phases/backfill.md` for the full flow. Brief overview:
192
+
193
+ 1. **Load the action.** Call `pib_get_action <fid>`. Read the
194
+ action's `text` and `notes` (especially the `## Surface Area`
195
+ section). Confirm the action is pending (`completed = 0`) — if
196
+ already shipped, redirect the user to `/verify update <fid>`.
197
+ 2. **Read existing feature files.** `ls e2e/features/*.feature` so
198
+ the interview can reference real scenarios and checkIds.
199
+ 3. **Interview, one question at a time.** Per CLAUDE.md global
200
+ convention. Walk the user through: which features need edits,
201
+ what verb (ADD/MODIFY/REMOVE/NEW), what anchor or scenario name.
202
+ 4. **Draft the section.** Format matches `verify-plan.md`'s output
203
+ spec (one `- features/<file>.feature:` line per entry).
204
+ 5. **Show the diff.** Display the action's current notes alongside
205
+ the proposed appended section.
206
+ 6. **Confirm + write.** On approval, call `pib_update_action` with
207
+ the augmented notes. Without approval, exit without changes.
208
+
209
+ This mode does NOT modify feature files. That's `/execute`'s job
210
+ once the action runs. Backfill only adds the planning artifact so
211
+ `/execute`'s `verify-emit` phase has something to read.
212
+
178
213
  ## Phase Summary
179
214
 
180
215
  | Phase | Absent = | What it customizes |
@@ -185,6 +220,7 @@ writes them.
185
220
  | `generate.md` | Default: write .feature + step stubs from calibrated draft | Generation rules (collision handling, naming) |
186
221
  | `update.md` | Default: action fid / diff / free-text dispatch | How change descriptions map to edits |
187
222
  | `scenario-template.md` | Default: Gherkin with cost+role tags, NN.NN checkIds | Project-specific scenario shape |
223
+ | `backfill.md` | Default: interview-driven Verify Plan section drafting | Project-specific backfill questions |
188
224
 
189
225
  ## Principles
190
226
 
@@ -0,0 +1,164 @@
1
+ # Backfill — add a Verify Plan section to a pending action
2
+
3
+ Default behavior for `/verify backfill <fid>`. The mode adds a
4
+ `## Verify Plan` section to a pending pib-db action's notes so
5
+ `/execute`'s `verify-emit` phase can read it later. Does NOT touch
6
+ feature files — that's `/execute`'s job at action ship time.
7
+
8
+ ## Inputs
9
+
10
+ - A pib-db action fid (`act:abc12345`). Passed as `$ARGUMENTS` to
11
+ `/verify backfill`. If missing or malformed, ask the user for
12
+ it; don't guess from context.
13
+
14
+ ## Preconditions
15
+
16
+ 1. The action exists. If `pib_get_action <fid>` returns nothing,
17
+ exit with "no action found for <fid>".
18
+ 2. The action is pending (`completed = 0`). If already shipped,
19
+ tell the user: "act:<fid> already shipped on <date>. To sync
20
+ feature files retroactively, use `/verify update <fid>` instead."
21
+ 3. The action's notes don't already have a `## Verify Plan` section.
22
+ If they do, ask: "act:<fid> already has a Verify Plan section.
23
+ Replace it or skip?"
24
+ 4. `e2e/features/` exists. If not, recommend `/verify learn` first.
25
+
26
+ ## Workflow
27
+
28
+ ### 1. Load and summarize
29
+
30
+ Print:
31
+
32
+ ```
33
+ Backfilling Verify Plan for act:abc12345 — <action text>
34
+
35
+ Current Surface Area (from notes):
36
+ - files: webapp/frontend/src/components/Foo.tsx
37
+ - files: webapp/frontend/src/hooks/useFoo.ts
38
+ ```
39
+
40
+ If no Surface Area section is found, say so and proceed to step 2 —
41
+ the user may want to add one inline.
42
+
43
+ ### 2. Show available feature files
44
+
45
+ ```
46
+ e2e/features/:
47
+ 01-desktop-rewrite.feature (@api-small, @as-user)
48
+ 02-browse-history.feature (@free, @as-user)
49
+ 04-admin-spot-check.feature (@free, @as-admin)
50
+ ...
51
+ ```
52
+
53
+ Read the first two lines of each `.feature` file to surface its
54
+ tags. Skip features tagged `@manual` from suggestions unless the
55
+ action explicitly mentions iOS/mobile.
56
+
57
+ ### 3. Interview, one question at a time
58
+
59
+ Per CLAUDE.md global convention — never batch. Each answer shapes
60
+ the next question. Typical sequence:
61
+
62
+ **Q1 — surface area mapping.**
63
+
64
+ > "act:abc12345 touches webapp/frontend/src/components/Foo.tsx.
65
+ > Which feature file(s) exercise that component or the route it
66
+ > renders on?"
67
+
68
+ Wait for answer. If unclear, propose candidates based on:
69
+ - The action's notes mentioning a route (`/admin`, `/history`)
70
+ - The scenario names in `e2e/features/`
71
+ - Cabinet-qa subagent if multiple plausible matches
72
+
73
+ **Q2 — edit verb.**
74
+
75
+ For each chosen feature file, ask:
76
+
77
+ > "For features/04-admin-spot-check.feature, what kind of edit?
78
+ > - ADD step after an existing anchor
79
+ > - MODIFY an existing step's assertion
80
+ > - NEW scenario in a new file
81
+ > - REMOVE a step (for deprecations)"
82
+
83
+ Wait for answer.
84
+
85
+ **Q3 — specifics.**
86
+
87
+ Depending on the verb, follow up:
88
+ - **ADD step after <anchor>**: "What's the anchor checkId, and what
89
+ should the new step assert?"
90
+ - **MODIFY step <checkId>**: "Which step? What should the new
91
+ assertion text say?"
92
+ - **NEW scenario**: "Scenario name? Tags (cost + role)? Brief
93
+ description of the journey?"
94
+ - **REMOVE step <checkId>**: "Which checkId? Confirm — removed
95
+ steps invalidate prior verdicts."
96
+
97
+ Wait for answer.
98
+
99
+ **Repeat Q1–Q3** for each additional feature file the action
100
+ touches. After the user signals "that's all," proceed.
101
+
102
+ ### 4. Draft the section
103
+
104
+ Format matches `verify-plan.md`'s output spec exactly so
105
+ `verify-emit` parses it identically to plan-time output:
106
+
107
+ ```markdown
108
+ ## Verify Plan
109
+
110
+ - features/04-admin-spot-check.feature: ADD step after the existing
111
+ "admin-history-list-loads" check — verify the new "balance owed"
112
+ column renders for users with outstanding refunds (4.12b
113
+ admin-balance-owed-column).
114
+ - features/14-downloaded-files.feature: (deferred) NEW scenario for
115
+ the bulk-download flow — requires Phase B framework migration to
116
+ ship first.
117
+ ```
118
+
119
+ Edit verbs: `ADD step after <anchor>`, `MODIFY step <checkId>`,
120
+ `NEW scenario`, `REMOVE step <checkId>`. Use `(deferred)` prefix
121
+ for entries that depend on other work.
122
+
123
+ ### 5. Show the diff
124
+
125
+ Display:
126
+
127
+ ```
128
+ Existing notes:
129
+ ─────────────
130
+ <truncated existing notes — last 20 lines>
131
+
132
+ Proposed appended section:
133
+ ──────────────────────────
134
+ ## Verify Plan
135
+
136
+ <the drafted section from step 4>
137
+ ```
138
+
139
+ ### 6. Confirm and write
140
+
141
+ > "Append this Verify Plan section to act:abc12345's notes? (y/n)"
142
+
143
+ If yes:
144
+ - Call `pib_update_action <fid> --notes "<existing_notes>\n\n<new_section>"`
145
+ - Confirm: "Updated act:abc12345 with Verify Plan (N entries)."
146
+
147
+ If no:
148
+ - Exit without changes.
149
+
150
+ ## Out of scope
151
+
152
+ - Modifying feature files. That's `/execute`'s `verify-emit` phase
153
+ when the action runs.
154
+ - Re-running the full /plan flow (acceptance criteria, surface area,
155
+ cabinet review). Backfill is narrow — Verify Plan section only.
156
+ - Creating new actions. The action already exists by definition.
157
+
158
+ ## When to escalate to a full /plan
159
+
160
+ If the interview reveals the action is mis-scoped — e.g., the user
161
+ realizes the surface area listed in the notes is wrong — recommend
162
+ re-planning the whole action via `/plan` instead. Backfill is only
163
+ useful when the action's scope and surface area are correct and
164
+ just the Verify Plan layer is missing.