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 CHANGED
@@ -142,6 +142,7 @@ Claude Code 세션
142
142
  | `/smith-report` | 상세 규정 준수 리포트 |
143
143
  | `/smith-check` | 설치 검증 |
144
144
  | `/smith-plan` | 분해 계획 템플릿 생성 |
145
+ | `/smith-decompose` | 복잡한 작업을 위한 5단계 가이드 분해 |
145
146
  | `/smith-update` | 버전 체크 + 업그레이드 |
146
147
 
147
148
  ## 명령어
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: 4 - missingCommands.length, total: 4, missing: missingCommands };
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: 4/4 installed`
413
- : `❌ Commands: ${4 - missingCommands.length}/4 (missing: ${missingCommands.join(', ')})`);
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
- const output = JSON.stringify({
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
  }
@@ -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
  }
@@ -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
  }
@@ -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
- const output = JSON.stringify({
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
- const output = JSON.stringify({
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);
@@ -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
- const output = JSON.stringify({
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
  }
@@ -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
- const output = JSON.stringify({
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
  }
@@ -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
- const output = JSON.stringify({
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
  }
@@ -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
- const output = JSON.stringify({
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
  }
@@ -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
- const output = JSON.stringify({
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
- const output = JSON.stringify({
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'));
@@ -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
- const output = JSON.stringify({
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
  }
@@ -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
- const output = JSON.stringify({
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
@@ -9,7 +9,7 @@ import { join } from 'path';
9
9
  const DEFAULTS = {
10
10
  language: 'en',
11
11
  omcCompat: false,
12
- notify: false,
12
+ notify: true,
13
13
  hooks: {
14
14
  'tdd-guard': { enabled: true },
15
15
  'commit-guard': { enabled: true, maxTestAge: 300 },
@@ -48,15 +48,17 @@ export function parseHookInput() {
48
48
  }
49
49
 
50
50
  /**
51
- * Send a brief notification to stderr for user visibility
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
- process.stderr.write(`🕵️ Smith — ${icon} ${hookName}: ${message}\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smith",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Claude Code workflow enforcement CLI - forging coding discipline into every session",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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