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.
- package/README.md +40 -31
- package/package.json +1 -1
- package/src/cli.js +6 -3
- package/src/commands/init.js +160 -5
- package/src/commands/remove.js +53 -2
- package/src/commands/upgrade.js +84 -4
- package/src/lib/installer.js +182 -8
- package/src/lib/manifest.js +1 -1
- package/templates/.claude/CLAUDE.md +2 -2
- package/templates/.claude/hooks/comment-guard.js +1 -1
- package/templates/.claude/hooks/glob-guard.js +8 -0
- package/templates/.claude/hooks/path-guard.sh +32 -26
- package/templates/.claude/hooks/self-review.sh +1 -0
- package/templates/.claude/hooks/sensitive-guard.sh +9 -9
- package/templates/.claude/{commands/mf-test.md → skills/mf-build/SKILL.md} +23 -3
- package/templates/.claude/{commands/mf-challenge.md → skills/mf-challenge/SKILL.md} +42 -27
- package/templates/.claude/{commands/mf-commit.md → skills/mf-commit/SKILL.md} +39 -8
- package/templates/.claude/{commands/mf-fix.md → skills/mf-fix/SKILL.md} +22 -1
- package/templates/.claude/{commands/mf-plan.md → skills/mf-plan/SKILL.md} +59 -48
- package/templates/.claude/{commands/mf-review.md → skills/mf-review/SKILL.md} +4 -0
- package/templates/docs/WORKFLOW.md +5 -5
package/src/lib/installer.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
'.claude/
|
|
24
|
-
'.claude/
|
|
25
|
-
'.claude/
|
|
26
|
-
'.claude/
|
|
27
|
-
'.claude/
|
|
28
|
-
'.claude/
|
|
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', '
|
|
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
|
+
}
|
package/src/lib/manifest.js
CHANGED
|
@@ -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', '
|
|
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-
|
|
17
|
-
| Update feature | `/mf-plan <spec-path> "changes"` → code → `/mf-
|
|
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
|
|
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
|
|
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
|
-
|
|
51
|
+
SEP="[/\\\\]"
|
|
52
|
+
BLOCKED="(^|[ /\\\\])node_modules(${SEP}|$| )"
|
|
47
53
|
BLOCKED+="|(__pycache__)"
|
|
48
|
-
BLOCKED+="|\.git
|
|
49
|
-
BLOCKED+="|(^|[
|
|
50
|
-
BLOCKED+="|(^|[
|
|
51
|
-
BLOCKED+="|\.next
|
|
52
|
-
BLOCKED+="|(^|[
|
|
53
|
-
BLOCKED+="|(^|[
|
|
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+="|(^|[
|
|
62
|
+
BLOCKED+="|\.gradle${SEP}"
|
|
63
|
+
BLOCKED+="|(^|[ /\\\\])target${SEP}"
|
|
58
64
|
BLOCKED+="|\.nuget"
|
|
59
|
-
BLOCKED+="|\.cache(
|
|
65
|
+
BLOCKED+="|\.cache(${SEP}|$| )"
|
|
60
66
|
# Python
|
|
61
|
-
BLOCKED+="|(^|[
|
|
62
|
-
BLOCKED+="|(^|[
|
|
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+="|(^|[
|
|
69
|
-
BLOCKED+="|(^|[
|
|
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+="|(^|[
|
|
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
|
-
|
|
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
|
-
|
|
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 →
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 each — walk 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-
|
|
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.
|
|
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,
|
|
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 "
|
|
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.
|
|
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
|
|