claude-devkit-cli 1.3.3 → 1.4.0

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.
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { join, dirname, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { chmod } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
6
7
  import { log } from './logger.js';
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -19,13 +20,13 @@ export const COMPONENTS = {
19
20
  '.claude/hooks/self-review.sh',
20
21
  '.claude/hooks/sensitive-guard.sh',
21
22
  ],
22
- commands: [
23
- '.claude/commands/mf-plan.md',
24
- '.claude/commands/mf-challenge.md',
25
- '.claude/commands/mf-test.md',
26
- '.claude/commands/mf-fix.md',
27
- '.claude/commands/mf-review.md',
28
- '.claude/commands/mf-commit.md',
23
+ skills: [
24
+ '.claude/skills/mf-plan/SKILL.md',
25
+ '.claude/skills/mf-build/SKILL.md',
26
+ '.claude/skills/mf-challenge/SKILL.md',
27
+ '.claude/skills/mf-fix/SKILL.md',
28
+ '.claude/skills/mf-review/SKILL.md',
29
+ '.claude/skills/mf-commit/SKILL.md',
29
30
  ],
30
31
  config: [
31
32
  '.claude/settings.json',
@@ -69,7 +70,7 @@ export function getTemplateDir() {
69
70
 
70
71
  /**
71
72
  * Get all files for the given component list.
72
- * @param {string[]} components - e.g. ['hooks', 'commands']
73
+ * @param {string[]} components - e.g. ['hooks', 'skills']
73
74
  * @returns {string[]} relative file paths
74
75
  */
75
76
  export function getFilesForComponents(components) {
@@ -183,3 +184,176 @@ export async function verifySettingsJson(targetDir) {
183
184
  return false;
184
185
  }
185
186
  }
187
+
188
+ /**
189
+ * Global skills directory: ~/.claude/skills/
190
+ */
191
+ export function getGlobalSkillsDir() {
192
+ return join(homedir(), '.claude', 'skills');
193
+ }
194
+
195
+ /**
196
+ * Global hooks directory: ~/.claude/hooks/
197
+ */
198
+ export function getGlobalHooksDir() {
199
+ return join(homedir(), '.claude', 'hooks');
200
+ }
201
+
202
+ /**
203
+ * Copy a hook to the global ~/.claude/hooks/ directory.
204
+ * Strips the '.claude/hooks/' prefix so path-guard.sh lands at
205
+ * ~/.claude/hooks/path-guard.sh.
206
+ * @returns {string} 'copied' | 'skipped' | 'identical'
207
+ */
208
+ export async function installHookGlobal(hookRelPath, globalHooksDir, { force = false } = {}) {
209
+ const stripped = hookRelPath.replace(/^\.claude\/hooks\//, '');
210
+ const src = join(getTemplateDir(), hookRelPath);
211
+ const dst = join(globalHooksDir, stripped);
212
+
213
+ if (existsSync(dst) && !force) {
214
+ try {
215
+ const { hashFile } = await import('./hasher.js');
216
+ const srcHash = await hashFile(src);
217
+ const dstHash = await hashFile(dst);
218
+ if (srcHash === dstHash) {
219
+ log.same(`~/.claude/hooks/${stripped} (identical)`);
220
+ return 'identical';
221
+ }
222
+ log.skip(`~/.claude/hooks/${stripped} (customized — use --force to overwrite)`);
223
+ return 'skipped';
224
+ } catch { /* hash failed */ }
225
+ }
226
+
227
+ await mkdir(dirname(dst), { recursive: true });
228
+ await fsCopyFile(src, dst);
229
+ await chmod(dst, 0o755);
230
+ log.copy(`~/.claude/hooks/${stripped}`);
231
+ return 'copied';
232
+ }
233
+
234
+ /**
235
+ * Build hook entries for ~/.claude/settings.json pointing to globalHooksDir.
236
+ */
237
+ function buildGlobalHookEntries(globalHooksDir) {
238
+ // Normalize to forward slashes — bash on all platforms (WSL, Git Bash, macOS, Linux)
239
+ // requires forward slashes even when the host OS is Windows.
240
+ const dir = globalHooksDir.replace(/\\/g, '/');
241
+ const h = (file) => `"${dir}/${file}"`;
242
+ return {
243
+ PreToolUse: [
244
+ { matcher: 'Bash', hooks: [
245
+ { type: 'command', command: `bash ${h('path-guard.sh')}` },
246
+ { type: 'command', command: `bash ${h('sensitive-guard.sh')}` },
247
+ ]},
248
+ { matcher: 'Read|Write|Edit|MultiEdit|Grep', hooks: [
249
+ { type: 'command', command: `bash ${h('sensitive-guard.sh')}` },
250
+ ]},
251
+ { matcher: 'Edit|MultiEdit', hooks: [
252
+ { type: 'command', command: `node ${h('comment-guard.js')}` },
253
+ ]},
254
+ { matcher: 'Glob', hooks: [
255
+ { type: 'command', command: `node ${h('glob-guard.js')}` },
256
+ ]},
257
+ ],
258
+ PostToolUse: [
259
+ { matcher: 'Write|Edit|MultiEdit', hooks: [
260
+ { type: 'command', command: `node ${h('file-guard.js')}` },
261
+ ]},
262
+ ],
263
+ Stop: [
264
+ { matcher: '', hooks: [
265
+ { type: 'command', command: `bash ${h('self-review.sh')}` },
266
+ ]},
267
+ ],
268
+ };
269
+ }
270
+
271
+ function isDevkitHookCommand(command) {
272
+ return command.includes('/.claude/hooks/');
273
+ }
274
+
275
+ function stripDevkitHooks(existingHooks) {
276
+ if (!existingHooks || typeof existingHooks !== 'object') return {};
277
+ const result = {};
278
+ for (const [event, matchers] of Object.entries(existingHooks)) {
279
+ if (!Array.isArray(matchers)) continue;
280
+ const kept = [];
281
+ for (const group of matchers) {
282
+ const keptHooks = (group.hooks || []).filter((h) => !isDevkitHookCommand(h.command || ''));
283
+ if (keptHooks.length > 0) kept.push({ ...group, hooks: keptHooks });
284
+ }
285
+ if (kept.length > 0) result[event] = kept;
286
+ }
287
+ return result;
288
+ }
289
+
290
+ /**
291
+ * Merge devkit hook registrations into ~/.claude/settings.json.
292
+ * Preserves any existing non-devkit hooks the user may have.
293
+ */
294
+ export async function mergeGlobalSettings(globalHooksDir) {
295
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
296
+ let existing = {};
297
+ try {
298
+ existing = JSON.parse(await readFile(settingsPath, 'utf-8'));
299
+ } catch { /* file doesn't exist yet — start fresh */ }
300
+
301
+ // Remove old devkit entries (identified by /.claude/hooks/ in command path)
302
+ const cleanedHooks = stripDevkitHooks(existing.hooks);
303
+
304
+ // Append new devkit entries
305
+ const newEntries = buildGlobalHookEntries(globalHooksDir);
306
+ const mergedHooks = { ...cleanedHooks };
307
+ for (const [event, entries] of Object.entries(newEntries)) {
308
+ mergedHooks[event] = [...(mergedHooks[event] || []), ...entries];
309
+ }
310
+
311
+ await mkdir(dirname(settingsPath), { recursive: true });
312
+ await writeFile(settingsPath, JSON.stringify({ ...existing, hooks: mergedHooks }, null, 2) + '\n');
313
+ }
314
+
315
+ /**
316
+ * Remove devkit hook registrations from ~/.claude/settings.json.
317
+ * Leaves any non-devkit hooks untouched.
318
+ */
319
+ export async function removeGlobalHooksFromSettings() {
320
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
321
+ let existing = {};
322
+ try {
323
+ existing = JSON.parse(await readFile(settingsPath, 'utf-8'));
324
+ } catch { return; }
325
+
326
+ const cleanedHooks = stripDevkitHooks(existing.hooks || {});
327
+ await writeFile(settingsPath, JSON.stringify({ ...existing, hooks: cleanedHooks }, null, 2) + '\n');
328
+ }
329
+
330
+ /**
331
+ * Copy a skill to the global ~/.claude/skills/ directory.
332
+ * Strips the '.claude/skills/' prefix so mf-plan/SKILL.md lands at
333
+ * ~/.claude/skills/mf-plan/SKILL.md.
334
+ * @returns {string} 'copied' | 'skipped' | 'identical'
335
+ */
336
+ export async function installSkillGlobal(skillRelPath, globalSkillsDir, { force = false } = {}) {
337
+ const stripped = skillRelPath.replace(/^\.claude\/skills\//, '');
338
+ const src = join(getTemplateDir(), skillRelPath);
339
+ const dst = join(globalSkillsDir, stripped);
340
+
341
+ if (existsSync(dst) && !force) {
342
+ try {
343
+ const { hashFile } = await import('./hasher.js');
344
+ const srcHash = await hashFile(src);
345
+ const dstHash = await hashFile(dst);
346
+ if (srcHash === dstHash) {
347
+ log.same(`~/.claude/skills/${stripped} (identical)`);
348
+ return 'identical';
349
+ }
350
+ log.skip(`~/.claude/skills/${stripped} (customized — use --force to overwrite)`);
351
+ return 'skipped';
352
+ } catch { /* hash failed, treat as conflict */ }
353
+ }
354
+
355
+ await mkdir(dirname(dst), { recursive: true });
356
+ await fsCopyFile(src, dst);
357
+ log.copy(`~/.claude/skills/${stripped}`);
358
+ return 'copied';
359
+ }
@@ -36,7 +36,7 @@ export function createManifest(version, projectType, components) {
36
36
  installedAt: now,
37
37
  updatedAt: now,
38
38
  projectType: projectType || null,
39
- components: components || ['hooks', 'commands', 'scripts', 'docs'],
39
+ components: components || ['hooks', 'skills', 'scripts', 'docs'],
40
40
  files: {},
41
41
  };
42
42
  }
@@ -13,8 +13,8 @@ Every change follows this cycle: **SPEC (with acceptance scenarios) → CODE + T
13
13
 
14
14
  | Trigger | Commands | Details |
15
15
  |---------|----------|---------|
16
- | New feature | `/mf-plan` → `/mf-challenge` (optional) → code in chunks → `/mf-test` each chunk | Start with spec or description |
17
- | Update feature | `/mf-plan <spec-path> "changes"` → code → `/mf-test` | Do NOT manually edit spec before /mf-plan |
16
+ | New feature | `/mf-plan` → `/mf-challenge` (optional) → code in chunks → `/mf-build` each chunk | Start with spec or description |
17
+ | Update feature | `/mf-plan <spec-path> "changes"` → code → `/mf-build` | Do NOT manually edit spec before /mf-plan |
18
18
  | Bug fix | `/mf-fix "description"` | Test-first: write failing test → fix → green |
19
19
  | Remove feature | `/mf-plan <spec-path> "remove stories"` → delete code + tests → build pass | /mf-plan handles snapshot before removal |
20
20
  | Pre-merge check | `/mf-review` | Diff-based quality gate |
@@ -34,7 +34,7 @@ function isCommentLine(line) {
34
34
  if (trimmed.startsWith("#") && !trimmed.startsWith("#!")) return true;
35
35
  if (trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.endsWith("*/")) return true;
36
36
  if (trimmed.startsWith("<!--")) return true;
37
- if (trimmed === "pass") return true; // Python pass statement as placeholder
37
+ if (trimmed === "pass" || /^pass\s*#/.test(trimmed)) return true; // Python pass / pass # comment
38
38
  return false;
39
39
  }
40
40
 
@@ -33,6 +33,14 @@ const SCOPED_DIRS = [
33
33
  "Sources", "Tests", "cmd", "pkg", "internal",
34
34
  ];
35
35
 
36
+ // Allow project-specific scoped dirs via env var
37
+ // e.g. GLOB_GUARD_SCOPED_DIRS=Feature,Domain,Presentation
38
+ const extraDirs = (process.env.GLOB_GUARD_SCOPED_DIRS || "")
39
+ .split(",")
40
+ .map((d) => d.trim())
41
+ .filter(Boolean);
42
+ if (extraDirs.length > 0) SCOPED_DIRS.push(...extraDirs);
43
+
36
44
  function isBroadPattern(pattern) {
37
45
  if (!pattern) return false;
38
46
  return BROAD_PATTERNS.some((re) => re.test(pattern.trim()));
@@ -14,6 +14,10 @@
14
14
 
15
15
  set -euo pipefail
16
16
 
17
+ # Windows note: this hook requires bash (WSL or Git Bash).
18
+ # On Windows without bash, Claude Code will fail to run this hook and skip it silently.
19
+ # Install WSL or Git Bash and ensure `bash` is in PATH to activate protection.
20
+
17
21
  # ─── Read hook payload from stdin ───────────────────────────────────
18
22
 
19
23
  INPUT=$(cat)
@@ -41,40 +45,42 @@ COMMAND=$(extract_command "$INPUT") || exit 0
41
45
 
42
46
  # ─── Blocked directory patterns ─────────────────────────────────────
43
47
 
44
- # Use word boundaries (\b) and explicit path separators to avoid substring false positives
48
+ # Use explicit path separators to avoid substring false positives.
49
+ # [/\\] matches both forward slash (Unix/macOS) and backslash (Windows Git Bash).
45
50
  # e.g. "build/" should not match "rebuild/src" or "my-build-tool"
46
- BLOCKED="(^|[ /])node_modules(/|$| )"
51
+ SEP="[/\\\\]"
52
+ BLOCKED="(^|[ /\\\\])node_modules(${SEP}|$| )"
47
53
  BLOCKED+="|(__pycache__)"
48
- BLOCKED+="|\.git/(objects|refs)"
49
- BLOCKED+="|(^|[ /])dist/"
50
- BLOCKED+="|(^|[ /])build/"
51
- BLOCKED+="|\.next/"
52
- BLOCKED+="|(^|[ /])vendor(/|$| )"
53
- BLOCKED+="|(^|[ /])Pods(/|$| )"
54
- BLOCKED+="|\.build/"
54
+ BLOCKED+="|\.git${SEP}(objects|refs)"
55
+ BLOCKED+="|(^|[ /\\\\])dist${SEP}"
56
+ BLOCKED+="|(^|[ /\\\\])build${SEP}"
57
+ BLOCKED+="|\.next${SEP}"
58
+ BLOCKED+="|(^|[ /\\\\])vendor(${SEP}|$| )"
59
+ BLOCKED+="|(^|[ /\\\\])Pods(${SEP}|$| )"
60
+ BLOCKED+="|\.build${SEP}"
55
61
  BLOCKED+="|DerivedData"
56
- BLOCKED+="|\.gradle/"
57
- BLOCKED+="|(^|[ /])target/"
62
+ BLOCKED+="|\.gradle${SEP}"
63
+ BLOCKED+="|(^|[ /\\\\])target${SEP}"
58
64
  BLOCKED+="|\.nuget"
59
- BLOCKED+="|\.cache(/|$| )"
65
+ BLOCKED+="|\.cache(${SEP}|$| )"
60
66
  # Python
61
- BLOCKED+="|(^|[ /])\.venv/"
62
- BLOCKED+="|(^|[ /])venv/"
63
- BLOCKED+="|\.mypy_cache/"
64
- BLOCKED+="|\.pytest_cache/"
65
- BLOCKED+="|\.ruff_cache/"
66
- BLOCKED+="|\.egg-info(/|$| )"
67
+ BLOCKED+="|(^|[ /\\\\])\.venv${SEP}"
68
+ BLOCKED+="|(^|[ /\\\\])venv${SEP}"
69
+ BLOCKED+="|\.mypy_cache${SEP}"
70
+ BLOCKED+="|\.pytest_cache${SEP}"
71
+ BLOCKED+="|\.ruff_cache${SEP}"
72
+ BLOCKED+="|\.egg-info(${SEP}|$| )"
67
73
  # C# .NET (match .NET-specific subdirs to avoid false positives on generic bin/)
68
- BLOCKED+="|(^|[ /])bin/(Debug|Release|net|x64|x86)"
69
- BLOCKED+="|(^|[ /])obj/(Debug|Release|net)"
74
+ BLOCKED+="|(^|[ /\\\\])bin${SEP}(Debug|Release|net|x64|x86)"
75
+ BLOCKED+="|(^|[ /\\\\])obj${SEP}(Debug|Release|net)"
70
76
  # Node.js frameworks
71
- BLOCKED+="|\.nuxt/"
72
- BLOCKED+="|\.svelte-kit/"
73
- BLOCKED+="|\.parcel-cache/"
74
- BLOCKED+="|\.turbo/"
75
- BLOCKED+="|(^|[ /])out/(server|static|_next)"
77
+ BLOCKED+="|\.nuxt${SEP}"
78
+ BLOCKED+="|\.svelte-kit${SEP}"
79
+ BLOCKED+="|\.parcel-cache${SEP}"
80
+ BLOCKED+="|\.turbo${SEP}"
81
+ BLOCKED+="|(^|[ /\\\\])out${SEP}(server|static|_next)"
76
82
  # Ruby
77
- BLOCKED+="|\.bundle/"
83
+ BLOCKED+="|\.bundle${SEP}"
78
84
 
79
85
  # Append project-specific patterns from env
80
86
  if [[ -n "${PATH_GUARD_EXTRA:-}" ]]; then
@@ -8,6 +8,7 @@
8
8
  # SELF_REVIEW_ENABLED — set to "false" to disable (default: true)
9
9
 
10
10
  # No set -euo pipefail — this hook must NEVER fail
11
+ # Windows note: requires bash (WSL or Git Bash). Silently skipped on Windows native.
11
12
 
12
13
  # Check if disabled
13
14
  if [[ "${SELF_REVIEW_ENABLED:-true}" == "false" ]]; then
@@ -13,6 +13,10 @@
13
13
 
14
14
  set -euo pipefail
15
15
 
16
+ # Windows note: this hook requires bash (WSL or Git Bash).
17
+ # On Windows without bash, Claude Code will fail to run this hook and skip it silently.
18
+ # Install WSL or Git Bash and ensure `bash` is in PATH to activate protection.
19
+
16
20
  # ─── Read hook payload from stdin ───────────────────────────────────
17
21
 
18
22
  INPUT=$(cat)
@@ -117,7 +121,11 @@ check_agentignore() {
117
121
 
118
122
  # Simple line-by-line match (not full gitignore glob, but covers common cases)
119
123
  local relpath
120
- relpath=$(echo "$filepath" | sed "s|^$(pwd)/||") 2>/dev/null || relpath="$filepath"
124
+ # Normalize separators to forward slash before stripping prefix (handles Git Bash on Windows)
125
+ local normalized_fp normalized_pwd
126
+ normalized_fp=$(printf '%s' "$filepath" | tr '\\' '/')
127
+ normalized_pwd=$(pwd | tr '\\' '/')
128
+ relpath=$(printf '%s' "$normalized_fp" | sed "s|^${normalized_pwd}/||") 2>/dev/null || relpath="$filepath"
121
129
 
122
130
  while IFS= read -r pattern || [[ -n "$pattern" ]]; do
123
131
  # Skip comments and empty lines
@@ -214,14 +222,6 @@ if [[ -n "$COMMAND" ]]; then
214
222
  fi
215
223
  fi
216
224
 
217
- # ─── Check Grep pattern for sensitive file paths ───────────────────
218
-
219
- if [[ -n "$PATTERN" ]]; then
220
- if is_sensitive "$PATTERN"; then
221
- block_with_message "$PATTERN"
222
- fi
223
- fi
224
-
225
225
  # ─── All checks passed ─────────────────────────────────────────────
226
226
 
227
227
  exit 0
@@ -1,4 +1,8 @@
1
- Write tests from spec acceptance scenarios, compile, run, fix until green.
1
+ ---
2
+ description: TDD delivery loop — write failing tests from spec, implement story by story, drive to GREEN
3
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
4
+ ---
5
+ TDD delivery loop — write failing tests from spec AS, implement story by story, drive to GREEN.
2
6
 
3
7
  ## Phase 0: Build Context
4
8
 
@@ -70,7 +74,23 @@ If `scripts/build-test.sh` doesn't exist, detect and run directly:
70
74
 
71
75
  If tests fail:
72
76
  1. Read error output. Is the test wrong or the production code wrong?
73
- 2. If production code seems wrong → **ASK the user:** "Test expects X but code does Y. Fix production code or adjust test?"
77
+ 2. If production code seems wrong → use `AskUserQuestion`:
78
+
79
+ ```json
80
+ {
81
+ "questions": [
82
+ {
83
+ "question": "Test expects <X> but code does <Y>. Which is correct?",
84
+ "header": "Test vs Code Mismatch",
85
+ "multiSelect": false,
86
+ "options": [
87
+ {"label": "Fix production code — the test is correct"},
88
+ {"label": "Adjust the test — the code behavior is intentional"}
89
+ ]
90
+ }
91
+ ]
92
+ }
93
+ ```
74
94
  3. Fix test code only. Re-run. Max 3 attempts, then stop and report.
75
95
 
76
96
  **NEVER:**
@@ -99,7 +119,7 @@ If a test fails due to an edge case, error path, or boundary condition that is N
99
119
 
100
120
  1. State explicitly: **"This failure suggests a missing acceptance scenario."**
101
121
  2. Describe the gap: what behavior was tested, which story it belongs to, why no AS covers it.
102
- 3. Prompt: **"Run `/mf-plan <spec-path> 'Add AS for <description>'` to add the missing scenario."**
122
+ 3. Prompt: **"Run `/mf-plan <spec-path> 'Add AS for <description>'` to add the missing scenario, then re-run `/mf-build`."**
103
123
 
104
124
  Do not silently fix the test and move on. A test that has no corresponding AS means the spec is incomplete — the spec must be updated first.
105
125
 
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Adversarial review — spawn hostile reviewers to break the plan before coding
3
+ allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, Agent
4
+ ---
1
5
  Adversarial review — spawn hostile reviewers to break the plan before coding.
2
6
 
3
7
  ## Input
@@ -173,32 +177,43 @@ Include 1-sentence rationale for each disposition. Be honest — don't reject va
173
177
 
174
178
  Show adjudicated findings using the reviewer output format plus Disposition and Rationale fields.
175
179
 
176
- Then present the decision:
177
-
178
- ```
179
- How to proceed with N accepted findings?
180
-
181
- A) Apply all accepted — bulk-apply all fixes at once
182
- Fit: N/10 | Trade-off: fast vs. no per-finding control
183
-
184
- B) Review each — walk through one by one, accept/reject/modify
185
- Fit: N/10 | Trade-off: precise control vs. slower
186
-
187
- RECOMMENDATION: [A or B]<reason based on finding count and severity>
188
- ```
189
-
190
- Score Fit based on context: if most findings are High/Critical, recommend B (review each). If mostly Medium with clear fixes, recommend A.
191
-
192
- If user picks B: for each finding, present:
193
-
194
- ```
195
- Finding [C-1]: <title>
196
-
197
- A) Accept — apply the suggested fix
198
- B) Modify — accept with changes (describe your modification)
199
- C) Reject — skip this finding
200
-
201
- RECOMMENDATION: [A/B/C] — <based on your adjudication>
180
+ Then present the decision using the `AskUserQuestion` tool:
181
+
182
+ ```json
183
+ {
184
+ "questions": [
185
+ {
186
+ "question": "How to proceed with N accepted findings? RECOMMENDATION: Choose A if mostly Medium fixes, B if any Critical/High findings.",
187
+ "header": "Apply Findings",
188
+ "multiSelect": false,
189
+ "options": [
190
+ {"label": "A) Apply all accepted — bulk-apply all fixes at once | Trade-off: fast vs. no per-finding control"},
191
+ {"label": "B) Review eachwalk through one by one, accept/reject/modify | Trade-off: precise control vs. slower"}
192
+ ]
193
+ }
194
+ ]
195
+ }
196
+ ```
197
+
198
+ Score: if most findings are High/Critical, recommend B. If mostly Medium with clear fixes, recommend A.
199
+
200
+ If user picks B: for each finding, use `AskUserQuestion`:
201
+
202
+ ```json
203
+ {
204
+ "questions": [
205
+ {
206
+ "question": "Finding [C-1]: <title>\n<flaw summary>\nRECOMMENDATION: Choose A — <adjudication rationale>.",
207
+ "header": "Finding C-1",
208
+ "multiSelect": false,
209
+ "options": [
210
+ {"label": "A) Accept — apply the suggested fix"},
211
+ {"label": "B) Modify — accept with changes (describe your modification)"},
212
+ {"label": "C) Reject — skip this finding"}
213
+ ]
214
+ }
215
+ ]
216
+ }
202
217
  ```
203
218
 
204
219
  ## Phase 7: Apply
@@ -215,7 +230,7 @@ Reviewers: N lenses
215
230
  Findings: X total → Y accepted, Z rejected
216
231
  Severity: N Critical, N High, N Medium
217
232
  Files modified: [list]
218
- Next: /mf-test to implement, or /mf-plan to regenerate if major changes.
233
+ Next: /mf-build to implement, or /mf-plan to regenerate if major changes.
219
234
  ```
220
235
 
221
236
  If a reviewer returns > 7 findings, take only top 7 by severity. If a reviewer fails, proceed with remaining reviewers.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Stage, scan secrets, generate conventional commit message
3
+ allowed-tools: Bash, AskUserQuestion
4
+ ---
1
5
  Stage, scan secrets, generate conventional commit message.
2
6
 
3
7
  ## Step 1 — Analyze (single compound command)
@@ -22,9 +26,41 @@ echo "=== DEBUG ===" && \
22
26
 
23
27
  **Secrets (hard block):** If count > 0, show matched lines and STOP. Do not commit.
24
28
 
25
- **Debug code (soft warn):** If count > 0, show matched lines. Proceed only after user confirms they're intentional.
29
+ **Debug code (soft warn):** If count > 0, show matched lines. Use `AskUserQuestion` to confirm:
30
+
31
+ ```json
32
+ {
33
+ "questions": [
34
+ {
35
+ "question": "Found <N> debug statements (console.log, debugger, etc.) in the diff. Are these intentional?",
36
+ "header": "Debug Code",
37
+ "multiSelect": false,
38
+ "options": [
39
+ {"label": "Yes, intentional — proceed with commit"},
40
+ {"label": "No, remove them first"}
41
+ ]
42
+ }
43
+ ]
44
+ }
45
+ ```
26
46
 
27
- **Large diff:** If > 10 files or > 300 lines, note: "Large commit — consider splitting for easier review." Continue unless user says to split.
47
+ **Large diff:** If > 10 files or > 300 lines, use `AskUserQuestion` to confirm:
48
+
49
+ ```json
50
+ {
51
+ "questions": [
52
+ {
53
+ "question": "Large commit detected (<N> files, <M> lines). Large commits are harder to review and revert.",
54
+ "header": "Large Commit",
55
+ "multiSelect": false,
56
+ "options": [
57
+ {"label": "Proceed — commit everything as one"},
58
+ {"label": "Split — I'll stage specific files myself"}
59
+ ]
60
+ }
61
+ ]
62
+ }
63
+ ```
28
64
 
29
65
  ---
30
66
 
@@ -67,12 +103,7 @@ Never stage: `.env`, credentials, build artifacts, generated files, binaries > 1
67
103
  ## Step 5 — Commit
68
104
 
69
105
  ```bash
70
- git commit -m "$(cat <<'EOF'
71
- type(scope): description
72
-
73
- Co-Authored-By: Claude <noreply@anthropic.com>
74
- EOF
75
- )"
106
+ git commit -m "type(scope): description"
76
107
  ```
77
108
 
78
109
  **Do NOT push** unless user explicitly asks.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Test-first bug fix — write failing test, fix code, verify green
3
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
4
+ ---
1
5
  Test-first bug fix — write failing test, fix code, verify green.
2
6
 
3
7
  Bug: $ARGUMENTS
@@ -29,7 +33,24 @@ bash scripts/build-test.sh --filter "<test name>"
29
33
  ```
30
34
 
31
35
  - **FAILS** → reproduced. Continue.
32
- - **PASSES** → hypothesis may be wrong. Ask: "Test passes — need different repro steps or environment details."
36
+ - **PASSES** → hypothesis may be wrong. Use `AskUserQuestion`:
37
+
38
+ ```json
39
+ {
40
+ "questions": [
41
+ {
42
+ "question": "The test passes with current code — the bug isn't reproduced yet. How to proceed?",
43
+ "header": "Test Passes Unexpectedly",
44
+ "multiSelect": false,
45
+ "options": [
46
+ {"label": "Provide different repro steps or environment details"},
47
+ {"label": "The bug may be environment-specific — describe the setup"},
48
+ {"label": "Skip test-first for this bug — fix directly"}
49
+ ]
50
+ }
51
+ ]
52
+ }
53
+ ```
33
54
 
34
55
  ---
35
56