claude-smith 3.3.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.
@@ -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);
@@ -90,22 +90,16 @@ const blockAt = isOmc ? (config.blockAt || 10) * 2 : (config.blockAt || 10);
90
90
 
91
91
  // Early suggestion on first code edit (gentle, non-blocking)
92
92
  if (count === 1) {
93
- notifyUser(config.notify, 'Plan Guard', 'warn', '첫 코드 편집 — 복잡한 작업이면 /smith-decompose 먼저');
94
- const output = JSON.stringify({
95
- hookSpecificOutput: {
96
- hookEventName: "PreToolUse",
97
- additionalContext: `🕵️ 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.`
98
- }
99
- });
100
- process.stdout.write(output);
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'));
101
95
  }
102
96
 
103
97
  // Block if threshold reached
104
98
  if (count >= blockAt) {
105
99
  recordEvent(sessionId, 'plan-guard', 'block');
106
100
  appendEvent(process.cwd(), { hook: 'plan-guard', event: 'block', message: `${count} files without plan` });
107
- notifyUser(config.notify, 'Plan Guard', 'block', `${count}개 파일 계획 없이 편집 — 차단`);
108
- 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'));
109
103
  process.exit(2);
110
104
  }
111
105
 
@@ -113,17 +107,11 @@ if (count >= blockAt) {
113
107
  if (count >= warnAt) {
114
108
  recordEvent(sessionId, 'plan-guard', 'warn');
115
109
  appendEvent(process.cwd(), { hook: 'plan-guard', event: 'warn', message: `${count} files without plan` });
116
- notifyUser(config.notify, 'Plan Guard', 'warn', `${count}개 파일 계획 없이 편집`);
117
- const output = JSON.stringify({
118
- hookSpecificOutput: {
119
- hookEventName: "PreToolUse",
120
- 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.
121
112
  💡 Create a plan file (docs/plans/*.md or plan.md) that includes:
122
113
  1. Each unit's responsibility (one sentence)
123
114
  2. Dependency direction between units
124
115
  3. Implementation order and rationale
125
- 4. Verification method for each unit`
126
- }
127
- });
128
- process.stdout.write(output);
116
+ 4. Verification method for each unit`].filter(Boolean).join('\n'));
129
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.3.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": {