claude-smith 3.2.0 → 3.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.ko.md +1 -0
- package/README.md +1 -0
- package/bin/cli.mjs +4 -4
- package/hooks/batch-checkpoint.mjs +3 -9
- package/hooks/build-guard.mjs +2 -2
- package/hooks/build-tracker.mjs +3 -2
- package/hooks/commit-guard.mjs +9 -20
- package/hooks/commit-message.mjs +5 -10
- package/hooks/debug-loop.mjs +5 -11
- package/hooks/file-size-warn.mjs +3 -9
- package/hooks/plan-guard.mjs +12 -12
- package/hooks/scope-guard.mjs +3 -9
- package/hooks/subagent-inject.mjs +3 -9
- package/hooks/tdd-guard.mjs +3 -9
- package/hooks/test-tracker.mjs +8 -9
- package/lib/config.mjs +1 -1
- package/lib/hook-utils.mjs +16 -3
- package/package.json +1 -1
- package/templates/commands/smith-decompose.md +63 -0
package/README.ko.md
CHANGED
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ After installation, these commands are available in Claude Code:
|
|
|
142
142
|
| `/smith-report` | Detailed compliance report |
|
|
143
143
|
| `/smith-check` | Validate installation |
|
|
144
144
|
| `/smith-plan` | Generate decomposition plan template |
|
|
145
|
+
| `/smith-decompose` | Guided 5-step problem decomposition for complex tasks |
|
|
145
146
|
| `/smith-update` | Check for updates and upgrade |
|
|
146
147
|
|
|
147
148
|
## Commands
|
package/bin/cli.mjs
CHANGED
|
@@ -403,14 +403,14 @@ function check() {
|
|
|
403
403
|
|
|
404
404
|
// Check commands
|
|
405
405
|
const commandsDir = join(cwd, '.claude', 'commands');
|
|
406
|
-
const expectedCommands = ['smith.md', 'smith-report.md', 'smith-check.md', 'smith-plan.md'];
|
|
406
|
+
const expectedCommands = ['smith.md', 'smith-report.md', 'smith-check.md', 'smith-plan.md', 'smith-decompose.md'];
|
|
407
407
|
const missingCommands = expectedCommands.filter(c => !existsSync(join(commandsDir, c)));
|
|
408
|
-
results.commands = { installed:
|
|
408
|
+
results.commands = { installed: 5 - missingCommands.length, total: 5, missing: missingCommands };
|
|
409
409
|
|
|
410
410
|
if (!ciMode) {
|
|
411
411
|
console.log(missingCommands.length === 0
|
|
412
|
-
? `✅ Commands:
|
|
413
|
-
: `❌ Commands: ${
|
|
412
|
+
? `✅ Commands: 5/5 installed`
|
|
413
|
+
: `❌ Commands: ${5 - missingCommands.length}/5 (missing: ${missingCommands.join(', ')})`);
|
|
414
414
|
}
|
|
415
415
|
if (missingCommands.length > 0) results.ok = false;
|
|
416
416
|
|
|
@@ -11,7 +11,7 @@ import { tmpdir } from 'os';
|
|
|
11
11
|
import { getHookConfig } from '../lib/config.mjs';
|
|
12
12
|
import { recordEvent } from '../lib/stats.mjs';
|
|
13
13
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
14
|
-
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser } from '../lib/hook-utils.mjs';
|
|
14
|
+
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
15
15
|
|
|
16
16
|
const config = getHookConfig('batch-checkpoint', process.cwd());
|
|
17
17
|
if (!config.enabled) process.exit(0);
|
|
@@ -57,12 +57,6 @@ if (!editedFiles.includes(filePath)) {
|
|
|
57
57
|
if (editedFiles.length > 0 && editedFiles.length % config.fileThreshold === 0) {
|
|
58
58
|
recordEvent(sessionId, 'batch-checkpoint', 'warn');
|
|
59
59
|
appendEvent(process.cwd(), { hook: 'batch-checkpoint', event: 'warn', message: `${editedFiles.length} files edited` });
|
|
60
|
-
notifyUser(config.notify, 'Batch Checkpoint', 'warn', `${editedFiles.length}개 파일 편집`);
|
|
61
|
-
|
|
62
|
-
hookSpecificOutput: {
|
|
63
|
-
hookEventName: "PostToolUse",
|
|
64
|
-
additionalContext: `🕵️ Smith 📋 Batch Checkpoint (rule 2-9): ${editedFiles.length} files edited. Report progress to user before continuing.`
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
process.stdout.write(output);
|
|
60
|
+
const notify = notifyUser(config.notify, 'Batch Checkpoint', 'warn', `${editedFiles.length}개 파일 편집`);
|
|
61
|
+
writeHookOutput('PostToolUse', [notify, `🕵️ Smith 📋 Batch Checkpoint (rule 2-9): ${editedFiles.length} files edited. Report progress to user before continuing.`].filter(Boolean).join('\n'));
|
|
68
62
|
}
|
package/hooks/build-guard.mjs
CHANGED
|
@@ -36,8 +36,8 @@ if (existsSync(lastBuildResult)) {
|
|
|
36
36
|
if (result === 'fail') {
|
|
37
37
|
recordEvent(sessionId, 'build-guard', 'block');
|
|
38
38
|
appendEvent(process.cwd(), { hook: 'build-guard', event: 'block', message: 'Build failing' });
|
|
39
|
-
notifyUser(config.notify, 'Build Guard', 'block', '빌드 실패 — 커밋 차단');
|
|
40
|
-
process.stderr.write("🕵️ Smith ✋ Commit blocked: last build FAILED. Run build and fix errors before committing.");
|
|
39
|
+
const notify = notifyUser(config.notify, 'Build Guard', 'block', '빌드 실패 — 커밋 차단');
|
|
40
|
+
process.stderr.write([notify, "🕵️ Smith ✋ Commit blocked: last build FAILED. Run build and fix errors before committing."].filter(Boolean).join('\n'));
|
|
41
41
|
process.exit(2);
|
|
42
42
|
}
|
|
43
43
|
}
|
package/hooks/build-tracker.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { tmpdir } from 'os';
|
|
|
11
11
|
import { getHookConfig } from '../lib/config.mjs';
|
|
12
12
|
import { recordEvent } from '../lib/stats.mjs';
|
|
13
13
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
14
|
-
import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
|
|
14
|
+
import { sanitizeId, parseHookInput, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
15
15
|
|
|
16
16
|
const config = getHookConfig('build-tracker', process.cwd());
|
|
17
17
|
if (!config.enabled) process.exit(0);
|
|
@@ -36,5 +36,6 @@ if (/pnpm build|npm run build|yarn build|tsc|next build/.test(command)) {
|
|
|
36
36
|
writeFileSync(join(stateDir, 'last-build-result'), result, { mode: 0o600 });
|
|
37
37
|
recordEvent(sessionId, 'build-tracker', 'fire');
|
|
38
38
|
appendEvent(process.cwd(), { hook: 'build-tracker', event: 'fire', message: result });
|
|
39
|
-
notifyUser(config.notify, 'Build Tracker', 'track', failed ? '❌ fail' : '✅ pass');
|
|
39
|
+
const notify = notifyUser(config.notify, 'Build Tracker', 'track', failed ? '❌ fail' : '✅ pass');
|
|
40
|
+
if (notify) writeHookOutput('PostToolUse', notify);
|
|
40
41
|
}
|
package/hooks/commit-guard.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { tmpdir } from 'os';
|
|
|
13
13
|
import { getHookConfig } from '../lib/config.mjs';
|
|
14
14
|
import { recordEvent } from '../lib/stats.mjs';
|
|
15
15
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
16
|
-
import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
|
|
16
|
+
import { sanitizeId, parseHookInput, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
17
17
|
|
|
18
18
|
const config = getHookConfig('commit-guard', process.cwd());
|
|
19
19
|
if (!config.enabled) process.exit(0);
|
|
@@ -41,8 +41,8 @@ if (existsSync(lastResultFile)) {
|
|
|
41
41
|
if (result === 'fail') {
|
|
42
42
|
recordEvent(sessionId, 'commit-guard', 'block');
|
|
43
43
|
appendEvent(process.cwd(), { hook: 'commit-guard', event: 'block', message: 'Tests failing' });
|
|
44
|
-
notifyUser(config.notify, 'Commit Guard', 'block', '테스트 실패 — 커밋 차단');
|
|
45
|
-
process.stderr.write("🕵️ Smith ✋ Commit blocked: last test run FAILED. Run your test suite and fix failures before committing.\n💡 Coaching: 1) Run your test suite. 2) Fix failing tests one at a time. 3) Verify all pass. 4) Then commit.");
|
|
44
|
+
const notifyBlock = notifyUser(config.notify, 'Commit Guard', 'block', '테스트 실패 — 커밋 차단');
|
|
45
|
+
process.stderr.write([notifyBlock, "🕵️ Smith ✋ Commit blocked: last test run FAILED. Run your test suite and fix failures before committing.\n💡 Coaching: 1) Run your test suite. 2) Fix failing tests one at a time. 3) Verify all pass. 4) Then commit."].filter(Boolean).join('\n'));
|
|
46
46
|
process.exit(2);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -55,31 +55,20 @@ if (existsSync(lastTestFile)) {
|
|
|
55
55
|
if (diff > config.maxTestAge) {
|
|
56
56
|
recordEvent(sessionId, 'commit-guard', 'warn');
|
|
57
57
|
appendEvent(process.cwd(), { hook: 'commit-guard', event: 'warn', message: 'Tests stale' });
|
|
58
|
-
notifyUser(config.notify, 'Commit Guard', 'warn', '테스트 경과 시간 초과');
|
|
59
|
-
|
|
60
|
-
hookSpecificOutput: {
|
|
61
|
-
hookEventName: "PreToolUse",
|
|
62
|
-
additionalContext: `🕵️ Smith > Commit Guard: Last test run was ${Math.floor(diff / 60)}min ago (>5min). Run tests before committing.`
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
process.stdout.write(output);
|
|
58
|
+
const notifyStale = notifyUser(config.notify, 'Commit Guard', 'warn', '테스트 경과 시간 초과');
|
|
59
|
+
writeHookOutput('PreToolUse', [notifyStale, `🕵️ Smith > Commit Guard: Last test run was ${Math.floor(diff / 60)}min ago (>5min). Run tests before committing.`].filter(Boolean).join('\n'));
|
|
66
60
|
process.exit(0);
|
|
67
61
|
}
|
|
68
62
|
} else {
|
|
69
63
|
recordEvent(sessionId, 'commit-guard', 'warn');
|
|
70
64
|
appendEvent(process.cwd(), { hook: 'commit-guard', event: 'warn', message: 'No test run detected' });
|
|
71
|
-
notifyUser(config.notify, 'Commit Guard', 'warn', '테스트 실행 기록 없음');
|
|
72
|
-
|
|
73
|
-
hookSpecificOutput: {
|
|
74
|
-
hookEventName: "PreToolUse",
|
|
75
|
-
additionalContext: "🕵️ Smith > Commit Guard: No test run detected this session. Consider running tests before committing.\n💡 Coaching: Run your test suite to verify nothing is broken. Even a quick smoke test prevents broken commits."
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
process.stdout.write(output);
|
|
65
|
+
const notifyNoTest = notifyUser(config.notify, 'Commit Guard', 'warn', '테스트 실행 기록 없음');
|
|
66
|
+
writeHookOutput('PreToolUse', [notifyNoTest, "🕵️ Smith > Commit Guard: No test run detected this session. Consider running tests before committing.\n💡 Coaching: Run your test suite to verify nothing is broken. Even a quick smoke test prevents broken commits."].filter(Boolean).join('\n'));
|
|
79
67
|
process.exit(0);
|
|
80
68
|
}
|
|
81
69
|
|
|
82
70
|
// All checks passed - commit allowed
|
|
83
71
|
recordEvent(sessionId, 'commit-guard', 'fire');
|
|
84
72
|
appendEvent(process.cwd(), { hook: 'commit-guard', event: 'fire', message: 'Commit allowed' });
|
|
85
|
-
notifyUser(config.notify, 'Commit Guard', 'fire', '커밋 허용');
|
|
73
|
+
const notify = notifyUser(config.notify, 'Commit Guard', 'fire', '커밋 허용');
|
|
74
|
+
if (notify) writeHookOutput('PreToolUse', notify);
|
package/hooks/commit-message.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { getHookConfig } from '../lib/config.mjs';
|
|
9
9
|
import { recordEvent } from '../lib/stats.mjs';
|
|
10
10
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
11
|
-
import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
|
|
11
|
+
import { sanitizeId, parseHookInput, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
12
12
|
|
|
13
13
|
const config = getHookConfig('commit-message', process.cwd());
|
|
14
14
|
if (!config.enabled) process.exit(0);
|
|
@@ -39,17 +39,12 @@ const pattern = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)
|
|
|
39
39
|
if (!pattern.test(firstLine)) {
|
|
40
40
|
recordEvent(sessionId, 'commit-message', 'warn');
|
|
41
41
|
appendEvent(process.cwd(), { hook: 'commit-message', event: 'warn', message: `Non-conventional: ${firstLine.slice(0, 50)}` });
|
|
42
|
-
notifyUser(config.notify, 'Commit Message', 'warn', `비표준 형식: ${firstLine.slice(0, 40)}`);
|
|
43
|
-
|
|
44
|
-
hookSpecificOutput: {
|
|
45
|
-
hookEventName: "PreToolUse",
|
|
46
|
-
additionalContext: `🕵️ Smith > Commit Message: "${firstLine}" doesn't follow conventional commits. Expected: type(scope): description (e.g., feat(auth): add login, fix: resolve crash)`
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
process.stdout.write(output);
|
|
42
|
+
const notifyWarn = notifyUser(config.notify, 'Commit Message', 'warn', `비표준 형식: ${firstLine.slice(0, 40)}`);
|
|
43
|
+
writeHookOutput('PreToolUse', [notifyWarn, `🕵️ Smith > Commit Message: "${firstLine}" doesn't follow conventional commits. Expected: type(scope): description (e.g., feat(auth): add login, fix: resolve crash)`].filter(Boolean).join('\n'));
|
|
50
44
|
} else {
|
|
51
45
|
// Format is valid
|
|
52
46
|
recordEvent(sessionId, 'commit-message', 'fire');
|
|
53
47
|
appendEvent(process.cwd(), { hook: 'commit-message', event: 'fire', message: 'Format valid' });
|
|
54
|
-
notifyUser(config.notify, 'Commit Message', 'fire', '형식 유효');
|
|
48
|
+
const notify = notifyUser(config.notify, 'Commit Message', 'fire', '형식 유효');
|
|
49
|
+
if (notify) writeHookOutput('PreToolUse', notify);
|
|
55
50
|
}
|
package/hooks/debug-loop.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { createHash } from 'crypto';
|
|
|
12
12
|
import { getHookConfig } from '../lib/config.mjs';
|
|
13
13
|
import { recordEvent } from '../lib/stats.mjs';
|
|
14
14
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
15
|
-
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser } from '../lib/hook-utils.mjs';
|
|
15
|
+
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
16
16
|
|
|
17
17
|
const config = getHookConfig('debug-loop', process.cwd());
|
|
18
18
|
if (!config.enabled) process.exit(0);
|
|
@@ -86,18 +86,12 @@ const patternHint = pattern === 'divergent'
|
|
|
86
86
|
if (count === config.warnAt) {
|
|
87
87
|
recordEvent(sessionId, 'debug-loop', 'warn');
|
|
88
88
|
appendEvent(process.cwd(), { hook: 'debug-loop', event: 'warn', file: filePath, message: `Edit #${count}${patternHint ? ' ' + patternHint : ''}` });
|
|
89
|
-
notifyUser(config.notify, 'Debug Loop', 'warn', `${name} ${count}회 편집${pattern === 'divergent' ? ' (반복 패턴)' : ''}`);
|
|
90
|
-
|
|
91
|
-
hookSpecificOutput: {
|
|
92
|
-
hookEventName: "PostToolUse",
|
|
93
|
-
additionalContext: `🕵️ Smith > Debug Loop: ${name} edited ${config.warnAt} times.${patternHint}\n💡 Coaching: 1) STOP editing. Read the error message carefully. 2) Add logging to trace data flow. 3) Check recent git changes: git diff HEAD~3. 4) Find a working similar pattern and compare. 5) Form a hypothesis before making the next edit.`
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
process.stdout.write(output);
|
|
89
|
+
const notify = notifyUser(config.notify, 'Debug Loop', 'warn', `${name} ${count}회 편집${pattern === 'divergent' ? ' (반복 패턴)' : ''}`);
|
|
90
|
+
writeHookOutput('PostToolUse', [notify, `🕵️ Smith > Debug Loop: ${name} edited ${config.warnAt} times.${patternHint}\n💡 Coaching: 1) STOP editing. Read the error message carefully. 2) Add logging to trace data flow. 3) Check recent git changes: git diff HEAD~3. 4) Find a working similar pattern and compare. 5) Form a hypothesis before making the next edit.`].filter(Boolean).join('\n'));
|
|
97
91
|
} else if (count >= config.blockAt) {
|
|
98
92
|
recordEvent(sessionId, 'debug-loop', 'block');
|
|
99
93
|
appendEvent(process.cwd(), { hook: 'debug-loop', event: 'block', file: filePath, message: `Edit #${count} - blocked` });
|
|
100
|
-
notifyUser(config.notify, 'Debug Loop', 'block', `${name} ${count}회 편집 — 차단`);
|
|
101
|
-
process.stderr.write(`🕵️ Smith ✋ Debug Loop blocked: ${name} edited ${count} times.${patternHint}\nPer rule 2-6: 3+ failed fixes → question architecture. Discuss with user before continuing.`);
|
|
94
|
+
const notify = notifyUser(config.notify, 'Debug Loop', 'block', `${name} ${count}회 편집 — 차단`);
|
|
95
|
+
process.stderr.write([notify, `🕵️ Smith ✋ Debug Loop blocked: ${name} edited ${count} times.${patternHint}\nPer rule 2-6: 3+ failed fixes → question architecture. Discuss with user before continuing.`].filter(Boolean).join('\n'));
|
|
102
96
|
process.exit(2);
|
|
103
97
|
}
|
package/hooks/file-size-warn.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { basename } from 'path';
|
|
|
10
10
|
import { getHookConfig } from '../lib/config.mjs';
|
|
11
11
|
import { recordEvent } from '../lib/stats.mjs';
|
|
12
12
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
13
|
-
import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
|
|
13
|
+
import { sanitizeId, parseHookInput, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
14
14
|
|
|
15
15
|
const config = getHookConfig('file-size-warn', process.cwd());
|
|
16
16
|
if (!config.enabled) process.exit(0);
|
|
@@ -36,12 +36,6 @@ const threshold = config.maxLines || 500;
|
|
|
36
36
|
if (lines > threshold) {
|
|
37
37
|
recordEvent(sessionId, 'file-size-warn', 'warn');
|
|
38
38
|
appendEvent(process.cwd(), { hook: 'file-size-warn', event: 'warn', file: filePath, message: `${lines} lines` });
|
|
39
|
-
notifyUser(config.notify, 'File Size', 'warn', `${basename(filePath)} ${lines}줄 (>${threshold})`);
|
|
40
|
-
|
|
41
|
-
hookSpecificOutput: {
|
|
42
|
-
hookEventName: "PostToolUse",
|
|
43
|
-
additionalContext: `🕵️ Smith > File Size: ${basename(filePath)} has ${lines} lines (>${threshold}). Consider splitting into smaller modules.`
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
process.stdout.write(output);
|
|
39
|
+
const notify = notifyUser(config.notify, 'File Size', 'warn', `${basename(filePath)} ${lines}줄 (>${threshold})`);
|
|
40
|
+
writeHookOutput('PostToolUse', [notify, `🕵️ Smith > File Size: ${basename(filePath)} has ${lines} lines (>${threshold}). Consider splitting into smaller modules.`].filter(Boolean).join('\n'));
|
|
47
41
|
}
|
package/hooks/plan-guard.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { tmpdir } from 'os';
|
|
|
13
13
|
import { getHookConfig } from '../lib/config.mjs';
|
|
14
14
|
import { recordEvent } from '../lib/stats.mjs';
|
|
15
15
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
16
|
-
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser } from '../lib/hook-utils.mjs';
|
|
16
|
+
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
17
17
|
|
|
18
18
|
const config = getHookConfig('plan-guard', process.cwd());
|
|
19
19
|
if (!config.enabled) process.exit(0);
|
|
@@ -88,12 +88,18 @@ const isOmc = isOmcAutoMode(process.cwd());
|
|
|
88
88
|
const warnAt = isOmc ? (config.warnAt || 5) * 2 : (config.warnAt || 5);
|
|
89
89
|
const blockAt = isOmc ? (config.blockAt || 10) * 2 : (config.blockAt || 10);
|
|
90
90
|
|
|
91
|
+
// Early suggestion on first code edit (gentle, non-blocking)
|
|
92
|
+
if (count === 1) {
|
|
93
|
+
const notify = notifyUser(config.notify, 'Plan Guard', 'warn', '첫 코드 편집 — 복잡한 작업이면 /smith-decompose 먼저');
|
|
94
|
+
writeHookOutput('PreToolUse', [notify, `🕵️ Smith > Plan Guard: First code file edit detected without a plan. If this is a complex or abstract task, consider running /smith-decompose first to break it down into verifiable units.\n💡 Simple task? Ignore this and continue. Complex task? Decompose first, implement second.`].filter(Boolean).join('\n'));
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
// Block if threshold reached
|
|
92
98
|
if (count >= blockAt) {
|
|
93
99
|
recordEvent(sessionId, 'plan-guard', 'block');
|
|
94
100
|
appendEvent(process.cwd(), { hook: 'plan-guard', event: 'block', message: `${count} files without plan` });
|
|
95
|
-
notifyUser(config.notify, 'Plan Guard', 'block', `${count}개 파일 계획 없이 편집 — 차단`);
|
|
96
|
-
process.stderr.write(`🕵️ Smith ✋ Plan Guard: ${count} code files edited without a decomposition plan. Create a plan first.`);
|
|
101
|
+
const notify = notifyUser(config.notify, 'Plan Guard', 'block', `${count}개 파일 계획 없이 편집 — 차단`);
|
|
102
|
+
process.stderr.write([notify, `🕵️ Smith ✋ Plan Guard: ${count} code files edited without a decomposition plan. Create a plan first.`].filter(Boolean).join('\n'));
|
|
97
103
|
process.exit(2);
|
|
98
104
|
}
|
|
99
105
|
|
|
@@ -101,17 +107,11 @@ if (count >= blockAt) {
|
|
|
101
107
|
if (count >= warnAt) {
|
|
102
108
|
recordEvent(sessionId, 'plan-guard', 'warn');
|
|
103
109
|
appendEvent(process.cwd(), { hook: 'plan-guard', event: 'warn', message: `${count} files without plan` });
|
|
104
|
-
notifyUser(config.notify, 'Plan Guard', 'warn', `${count}개 파일 계획 없이 편집`);
|
|
105
|
-
|
|
106
|
-
hookSpecificOutput: {
|
|
107
|
-
hookEventName: "PreToolUse",
|
|
108
|
-
additionalContext: `🕵️ Smith > Plan Guard: ${count} code files edited without a plan. Complex work needs decomposition first.
|
|
110
|
+
const notify = notifyUser(config.notify, 'Plan Guard', 'warn', `${count}개 파일 계획 없이 편집`);
|
|
111
|
+
writeHookOutput('PreToolUse', [notify, `🕵️ Smith > Plan Guard: ${count} code files edited without a plan. Complex work needs decomposition first.
|
|
109
112
|
💡 Create a plan file (docs/plans/*.md or plan.md) that includes:
|
|
110
113
|
1. Each unit's responsibility (one sentence)
|
|
111
114
|
2. Dependency direction between units
|
|
112
115
|
3. Implementation order and rationale
|
|
113
|
-
4. Verification method for each unit`
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
process.stdout.write(output);
|
|
116
|
+
4. Verification method for each unit`].filter(Boolean).join('\n'));
|
|
117
117
|
}
|
package/hooks/scope-guard.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { tmpdir } from 'os';
|
|
|
11
11
|
import { getHookConfig } from '../lib/config.mjs';
|
|
12
12
|
import { recordEvent } from '../lib/stats.mjs';
|
|
13
13
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
14
|
-
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser } from '../lib/hook-utils.mjs';
|
|
14
|
+
import { sanitizeId, parseHookInput, isOmcAutoMode, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
15
15
|
|
|
16
16
|
const config = getHookConfig('scope-guard', process.cwd());
|
|
17
17
|
if (!config.enabled) process.exit(0);
|
|
@@ -55,12 +55,6 @@ const threshold = isOmcAutoMode(process.cwd()) ? Math.max((config.maxDirectories
|
|
|
55
55
|
if (dirs.length >= threshold && (dirs.length === threshold || dirs.length % threshold === 0)) {
|
|
56
56
|
recordEvent(sessionId, 'scope-guard', 'warn');
|
|
57
57
|
appendEvent(process.cwd(), { hook: 'scope-guard', event: 'warn', message: `${dirs.length} directories` });
|
|
58
|
-
notifyUser(config.notify, 'Scope Guard', 'warn', `${dirs.length}개 디렉토리 변경`);
|
|
59
|
-
|
|
60
|
-
hookSpecificOutput: {
|
|
61
|
-
hookEventName: "PostToolUse",
|
|
62
|
-
additionalContext: `🕵️ Smith > Scope Guard: Changes span ${dirs.length} directories (>=${threshold}). Is the scope too broad? Consider breaking into smaller tasks.`
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
process.stdout.write(output);
|
|
58
|
+
const notify = notifyUser(config.notify, 'Scope Guard', 'warn', `${dirs.length}개 디렉토리 변경`);
|
|
59
|
+
writeHookOutput('PostToolUse', [notify, `🕵️ Smith > Scope Guard: Changes span ${dirs.length} directories (>=${threshold}). Is the scope too broad? Consider breaking into smaller tasks.`].filter(Boolean).join('\n'));
|
|
66
60
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { getHookConfig } from '../lib/config.mjs';
|
|
9
9
|
import { recordEvent } from '../lib/stats.mjs';
|
|
10
10
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
11
|
-
import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
|
|
11
|
+
import { sanitizeId, parseHookInput, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
12
12
|
|
|
13
13
|
const config = getHookConfig('subagent-inject', process.cwd());
|
|
14
14
|
if (!config.enabled) process.exit(0);
|
|
@@ -21,11 +21,5 @@ const sessionId = sanitizeId(input.session_id);
|
|
|
21
21
|
|
|
22
22
|
recordEvent(sessionId, 'subagent-inject', 'fire');
|
|
23
23
|
appendEvent(process.cwd(), { hook: 'subagent-inject', event: 'fire', message: 'Rules injected into subagent' });
|
|
24
|
-
notifyUser(config.notify, 'Subagent Inject', 'inject', '규칙 주입됨');
|
|
25
|
-
|
|
26
|
-
hookSpecificOutput: {
|
|
27
|
-
hookEventName: "SubagentStart",
|
|
28
|
-
additionalContext: "🔨 SMITH ENFORCEMENT: TDD Iron Law - write failing test BEFORE code. Run tests BEFORE commit. Never claim completion without fresh test evidence. Systematic debugging: 3+ failed fixes on same file → stop and question architecture."
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
process.stdout.write(output);
|
|
24
|
+
const notify = notifyUser(config.notify, 'Subagent Inject', 'inject', '규칙 주입됨');
|
|
25
|
+
writeHookOutput('SubagentStart', [notify, '🔨 SMITH ENFORCEMENT: TDD Iron Law - write failing test BEFORE code. Run tests BEFORE commit. Never claim completion without fresh test evidence. Systematic debugging: 3+ failed fixes on same file → stop and question architecture.'].filter(Boolean).join('\n'));
|
package/hooks/tdd-guard.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { dirname, basename, join, extname } from 'path';
|
|
|
10
10
|
import { getHookConfig } from '../lib/config.mjs';
|
|
11
11
|
import { recordEvent } from '../lib/stats.mjs';
|
|
12
12
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
13
|
-
import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
|
|
13
|
+
import { sanitizeId, parseHookInput, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
14
14
|
|
|
15
15
|
const config = getHookConfig('tdd-guard', process.cwd());
|
|
16
16
|
if (!config.enabled) process.exit(0);
|
|
@@ -53,12 +53,6 @@ const testFileTs = join(dir, `${name}.test.ts`);
|
|
|
53
53
|
if (!existsSync(testFile) && !existsSync(specFile) && !existsSync(testFileTsx) && !existsSync(testFileTs)) {
|
|
54
54
|
recordEvent(sessionId, 'tdd-guard', 'warn');
|
|
55
55
|
appendEvent(process.cwd(), { hook: 'tdd-guard', event: 'warn', file: filePath, message: `No test file for ${basename(filePath)}` });
|
|
56
|
-
notifyUser(config.notify, 'TDD Guard', 'warn', `${basename(filePath)} 테스트 파일 없음`);
|
|
57
|
-
|
|
58
|
-
hookSpecificOutput: {
|
|
59
|
-
hookEventName: "PreToolUse",
|
|
60
|
-
additionalContext: `🕵️ Smith > TDD Guard: ${basename(filePath)} has no test file (${name}.test${ext}). TDD Iron Law: write failing test FIRST.\n💡 Coaching: 1) Create ${name}.test${ext} in the same directory. 2) Write a test that describes expected behavior. 3) Run the test to confirm it fails. 4) Then implement the code.`
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
process.stdout.write(output);
|
|
56
|
+
const notify = notifyUser(config.notify, 'TDD Guard', 'warn', `${basename(filePath)} 테스트 파일 없음`);
|
|
57
|
+
writeHookOutput('PreToolUse', [notify, `🕵️ Smith > TDD Guard: ${basename(filePath)} has no test file (${name}.test${ext}). TDD Iron Law: write failing test FIRST.\n💡 Coaching: 1) Create ${name}.test${ext} in the same directory. 2) Write a test that describes expected behavior. 3) Run the test to confirm it fails. 4) Then implement the code.`].filter(Boolean).join('\n'));
|
|
64
58
|
}
|
package/hooks/test-tracker.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { tmpdir } from 'os';
|
|
|
11
11
|
import { getHookConfig } from '../lib/config.mjs';
|
|
12
12
|
import { recordEvent } from '../lib/stats.mjs';
|
|
13
13
|
import { appendEvent } from '../lib/event-log.mjs';
|
|
14
|
-
import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
|
|
14
|
+
import { sanitizeId, parseHookInput, notifyUser, writeHookOutput } from '../lib/hook-utils.mjs';
|
|
15
15
|
|
|
16
16
|
const config = getHookConfig('test-tracker', process.cwd());
|
|
17
17
|
if (!config.enabled) process.exit(0);
|
|
@@ -42,7 +42,7 @@ if (/pnpm test|npm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test
|
|
|
42
42
|
writeFileSync(join(stateDir, 'last-test-result'), failed ? 'fail' : 'pass', { mode: 0o600 });
|
|
43
43
|
recordEvent(sessionId, 'test-tracker', 'fire');
|
|
44
44
|
appendEvent(process.cwd(), { hook: 'test-tracker', event: 'fire', message: `${failed ? 'fail' : 'pass'}` });
|
|
45
|
-
notifyUser(config.notify, 'Test Tracker', 'track', failed ? '❌ fail' : '✅ pass');
|
|
45
|
+
const notify = notifyUser(config.notify, 'Test Tracker', 'track', failed ? '❌ fail' : '✅ pass');
|
|
46
46
|
|
|
47
47
|
// Extract coverage percentage from test output
|
|
48
48
|
function parseCoverage(text) {
|
|
@@ -60,6 +60,7 @@ if (/pnpm test|npm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
let coverageMsg = '';
|
|
63
64
|
const coverage = parseCoverage(response);
|
|
64
65
|
if (coverage !== null) {
|
|
65
66
|
const covFile = join(stateDir, 'last-coverage');
|
|
@@ -72,13 +73,11 @@ if (/pnpm test|npm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test
|
|
|
72
73
|
if (prevCoverage !== null && coverage < prevCoverage) {
|
|
73
74
|
const delta = (coverage - prevCoverage).toFixed(1);
|
|
74
75
|
appendEvent(process.cwd(), { hook: 'test-tracker', event: 'warn', message: `Coverage: ${prevCoverage}% → ${coverage}%` });
|
|
75
|
-
|
|
76
|
-
hookSpecificOutput: {
|
|
77
|
-
hookEventName: "PostToolUse",
|
|
78
|
-
additionalContext: `🕵️ Smith 📊 Coverage Delta: ${prevCoverage}% → ${coverage}% (${delta}%). Test coverage decreased.\n💡 Coaching: 1) Check which lines lost coverage. 2) Add tests for uncovered paths. 3) Run coverage report: your-test-cmd --coverage`
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
process.stdout.write(output);
|
|
76
|
+
coverageMsg = `🕵️ Smith 📊 Coverage Delta: ${prevCoverage}% → ${coverage}% (${delta}%). Test coverage decreased.\n💡 Coaching: 1) Check which lines lost coverage. 2) Add tests for uncovered paths. 3) Run coverage report: your-test-cmd --coverage`;
|
|
82
77
|
}
|
|
83
78
|
}
|
|
79
|
+
|
|
80
|
+
// Single stdout write combining notify + coverage (if any)
|
|
81
|
+
const combined = [notify, coverageMsg].filter(Boolean).join('\n');
|
|
82
|
+
if (combined) writeHookOutput('PostToolUse', combined);
|
|
84
83
|
}
|
package/lib/config.mjs
CHANGED
package/lib/hook-utils.mjs
CHANGED
|
@@ -48,15 +48,17 @@ export function parseHookInput() {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* Build a brief notification string for user visibility
|
|
52
52
|
* Only active when notify: true in .claude-smith.json
|
|
53
|
+
* Returns formatted string (empty string if notify is disabled)
|
|
53
54
|
* @param {boolean} notify - Whether notifications are enabled
|
|
54
55
|
* @param {string} hookName - Display name of the hook
|
|
55
56
|
* @param {string} eventType - fire|warn|block|inject|track
|
|
56
57
|
* @param {string} message - Brief description
|
|
58
|
+
* @returns {string} Formatted notification or empty string
|
|
57
59
|
*/
|
|
58
60
|
export function notifyUser(notify, hookName, eventType, message) {
|
|
59
|
-
if (!notify) return;
|
|
61
|
+
if (!notify) return '';
|
|
60
62
|
const icons = {
|
|
61
63
|
fire: '✅',
|
|
62
64
|
warn: '⚠️',
|
|
@@ -65,5 +67,16 @@ export function notifyUser(notify, hookName, eventType, message) {
|
|
|
65
67
|
track: '📊'
|
|
66
68
|
};
|
|
67
69
|
const icon = icons[eventType] || '📝';
|
|
68
|
-
|
|
70
|
+
return `🕵️ Smith — ${icon} ${hookName}: ${message}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Write hook output to stdout as JSON (additionalContext)
|
|
75
|
+
* @param {string} hookEventName - e.g. "PreToolUse", "PostToolUse", "SubagentStart"
|
|
76
|
+
* @param {string} additionalContext - Message to inject into conversation
|
|
77
|
+
*/
|
|
78
|
+
export function writeHookOutput(hookEventName, additionalContext) {
|
|
79
|
+
process.stdout.write(JSON.stringify({
|
|
80
|
+
hookSpecificOutput: { hookEventName, additionalContext }
|
|
81
|
+
}));
|
|
69
82
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Decompose a complex or abstract request into verifiable work units.
|
|
2
|
+
|
|
3
|
+
Follow this 5-step framework strictly. Do NOT skip steps or jump to implementation.
|
|
4
|
+
|
|
5
|
+
## Step 1: Essence Extraction
|
|
6
|
+
|
|
7
|
+
Ask yourself: **"What is the user actually trying to achieve?"**
|
|
8
|
+
|
|
9
|
+
- Strip away implementation details and restate the core problem in one sentence
|
|
10
|
+
- Identify the success criteria: "How will we know this is done?"
|
|
11
|
+
- Output: `Core Problem: ...` and `Success Criteria: ...`
|
|
12
|
+
|
|
13
|
+
## Step 2: Uncertainty Map
|
|
14
|
+
|
|
15
|
+
List what you DON'T know. Be honest about assumptions.
|
|
16
|
+
|
|
17
|
+
| What I Know | What I Assume | What I Don't Know |
|
|
18
|
+
|-------------|---------------|-------------------|
|
|
19
|
+
| (facts) | (guesses) | (unknowns) |
|
|
20
|
+
|
|
21
|
+
Every item in "Assume" and "Don't Know" is a candidate for a user question.
|
|
22
|
+
|
|
23
|
+
## Step 3: Clarifying Questions
|
|
24
|
+
|
|
25
|
+
Use the AskUserQuestion tool to ask 1-2 targeted questions based on Step 2.
|
|
26
|
+
|
|
27
|
+
Rules:
|
|
28
|
+
- Ask about the MOST impactful unknowns only (not everything)
|
|
29
|
+
- Provide 2-3 concrete options per question with trade-offs
|
|
30
|
+
- Include your recommendation
|
|
31
|
+
- WAIT for user answers before proceeding to Step 4
|
|
32
|
+
|
|
33
|
+
## Step 4: Unit Decomposition
|
|
34
|
+
|
|
35
|
+
Break the work into independently verifiable units:
|
|
36
|
+
|
|
37
|
+
For each unit:
|
|
38
|
+
- **Name**: Short descriptive name
|
|
39
|
+
- **Responsibility**: One sentence (if it needs two, split the unit)
|
|
40
|
+
- **Inputs/Outputs**: What it receives, what it produces
|
|
41
|
+
- **Verification**: Specific test or check that proves it works
|
|
42
|
+
- **Files**: Create/modify/test file paths
|
|
43
|
+
|
|
44
|
+
Quality checks:
|
|
45
|
+
- Each unit has exactly ONE responsibility
|
|
46
|
+
- Dependencies are unidirectional (no cycles)
|
|
47
|
+
- Removing any unit doesn't break the others
|
|
48
|
+
- Each unit can be verified in isolation
|
|
49
|
+
|
|
50
|
+
## Step 5: Plan File Generation
|
|
51
|
+
|
|
52
|
+
Generate `docs/plans/YYYY-MM-DD-<topic>.md` (or `plan.md`) with:
|
|
53
|
+
|
|
54
|
+
1. Goal (from Step 1)
|
|
55
|
+
2. Decisions (from Step 3 answers)
|
|
56
|
+
3. Unit list with dependency graph
|
|
57
|
+
4. Implementation order (leaf-first: fewest dependencies first)
|
|
58
|
+
5. Per-unit tasks at 2-5 minute granularity
|
|
59
|
+
|
|
60
|
+
After generating the plan, report to the user:
|
|
61
|
+
- How many units and tasks were created
|
|
62
|
+
- Estimated implementation order
|
|
63
|
+
- Which unit to start with and why
|