claude-smith 3.1.0 → 3.3.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
@@ -1,8 +1,8 @@
1
1
  [English](README.md) | **한국어**
2
2
 
3
- # 🔨 claude-smith
3
+ # 🕵️ claude-smith
4
4
 
5
- > **Claude Code의 ESLint** — AI 코딩 세션에 물리적 강제를 더합니다.
5
+ > **코드의 조용한 수호자** — AI 코딩 세션의 물리적 규율 강제
6
6
 
7
7
  ## 문제
8
8
 
@@ -39,6 +39,24 @@ npx claude-smith init
39
39
 
40
40
  언어를 선택하면(en/ko/zh/ja) 끝. 12개 Hook 설치, 규칙 주입, 강제 활성화.
41
41
 
42
+ ## 실시간 알림
43
+
44
+ Claude Code 세션 중 Hook 활동을 실시간으로 확인하세요:
45
+
46
+ ```json
47
+ { "notify": true }
48
+ ```
49
+
50
+ 활성화하면 터미널에서 간략한 상태 메시지를 볼 수 있습니다:
51
+ ```
52
+ 🕵️ Smith — ⚠️ TDD Guard: LoginForm.tsx 테스트 파일 없음
53
+ 🕵️ Smith — ✅ Commit Guard: 커밋 허용
54
+ 🕵️ Smith — 🚫 Debug Loop: app.tsx 5회 편집 차단
55
+ 🕵️ Smith — 🤖 Subagent Inject: 규칙 주입됨
56
+ ```
57
+
58
+ `"notify": false` (기본값)로 설정하면 비활성화됩니다.
59
+
42
60
  ## 12개 Hook
43
61
 
44
62
  ### 차단하는 Hook (exit code 2 — 진행 자체를 막음)
@@ -124,6 +142,7 @@ Claude Code 세션
124
142
  | `/smith-report` | 상세 규정 준수 리포트 |
125
143
  | `/smith-check` | 설치 검증 |
126
144
  | `/smith-plan` | 분해 계획 템플릿 생성 |
145
+ | `/smith-decompose` | 복잡한 작업을 위한 5단계 가이드 분해 |
127
146
  | `/smith-update` | 버전 체크 + 업그레이드 |
128
147
 
129
148
  ## 명령어
@@ -148,6 +167,7 @@ claude-smith uninstall # 모든 구성 요소 제거
148
167
  ```json
149
168
  {
150
169
  "language": "ko",
170
+ "notify": false,
151
171
  "hooks": {
152
172
  "commit-guard": { "maxTestAge": 300 },
153
173
  "debug-loop": { "warnAt": 3, "blockAt": 5 },
@@ -187,13 +207,36 @@ claude-smith uninstall # 모든 구성 요소 제거
187
207
  - **안전한 권한**: 디렉터리 `0o700`, 파일 `0o600`
188
208
  - **경쟁 조건 방지**: Hook별 원자적 카운터 (공유 JSON 미사용)
189
209
  - **제로 의존성**: Node.js 기본 모듈만 사용
190
- - **63개 테스트**: 보안 엣지 케이스, 에러 처리, 크로스 플랫폼 시나리오
210
+ - **포괄적 테스트 스위트**: 보안 엣지 케이스, 에러 처리, 크로스 플랫폼 시나리오
191
211
 
192
212
  ## 요구 사항
193
213
 
194
214
  - Node.js >= 18
195
215
  - Hook을 지원하는 Claude Code (`.claude/settings.json`)
196
216
 
217
+ ## 문제 해결
218
+
219
+ ### Hook이 작동하지 않음
220
+ 1. 설치 검증: `claude-smith check`
221
+ 2. `.claude/settings.json`에 Hook 정의가 있는지 확인
222
+ 3. Node.js >= 18 확인: `node --version`
223
+
224
+ ### 병렬 에이전트에서 오탐 발생
225
+ `.claude-smith.json`에서 OMC 호환 모드 활성화:
226
+ ```json
227
+ { "omcCompat": true }
228
+ ```
229
+ debug-loop, batch-checkpoint, scope-guard, plan-guard 임계값이 상향됩니다.
230
+
231
+ ### 테스트가 "실행 안 됨"으로 표시됨
232
+ test-tracker Hook은 Bash 도구 호출의 테스트 명령만 감지합니다. 인라인이 아닌 터미널을 통해 테스트를 실행하세요.
233
+
234
+ ### Hook 에러
235
+ 개별 Hook을 수동으로 실행하여 디버깅:
236
+ ```bash
237
+ echo '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"},"session_id":"debug"}' | node .claude/hooks/tdd-guard.mjs
238
+ ```
239
+
197
240
  ## 라이선스
198
241
 
199
242
  MIT
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  **English** | [한국어](README.ko.md)
2
2
 
3
- # 🔨 claude-smith
3
+ # 🕵️ claude-smith
4
4
 
5
- > **Claude Code's ESLint** — Physical enforcement for AI coding sessions.
5
+ > **Your code's silent guardian** — Physical enforcement for AI coding sessions.
6
6
 
7
7
  ## The Problem
8
8
 
@@ -39,6 +39,24 @@ npx claude-smith init
39
39
 
40
40
  Select your language (en/ko/zh/ja), and you're done. 12 hooks installed, rules injected, enforcement active.
41
41
 
42
+ ## Live Notifications
43
+
44
+ See hook activity in real-time during Claude Code sessions:
45
+
46
+ ```json
47
+ { "notify": true }
48
+ ```
49
+
50
+ When enabled, hooks write brief status messages visible in your terminal:
51
+ ```
52
+ 🕵️ Smith — ⚠️ TDD Guard: LoginForm.tsx test file missing
53
+ 🕵️ Smith — ✅ Commit Guard: Commit allowed
54
+ 🕵️ Smith — 🚫 Debug Loop: app.tsx 5 edits blocked
55
+ 🕵️ Smith — 🤖 Subagent Inject: Rules injected
56
+ ```
57
+
58
+ Set `"notify": false` (default) to disable.
59
+
42
60
  ## The 12 Hooks
43
61
 
44
62
  ### Hooks That Block (exit code 2 — action physically prevented)
@@ -124,6 +142,7 @@ After installation, these commands are available in Claude Code:
124
142
  | `/smith-report` | Detailed compliance report |
125
143
  | `/smith-check` | Validate installation |
126
144
  | `/smith-plan` | Generate decomposition plan template |
145
+ | `/smith-decompose` | Guided 5-step problem decomposition for complex tasks |
127
146
  | `/smith-update` | Check for updates and upgrade |
128
147
 
129
148
  ## Commands
@@ -148,6 +167,7 @@ Project-level settings in `.claude-smith.json` (all optional):
148
167
  ```json
149
168
  {
150
169
  "language": "en",
170
+ "notify": false,
151
171
  "hooks": {
152
172
  "commit-guard": { "maxTestAge": 300 },
153
173
  "debug-loop": { "warnAt": 3, "blockAt": 5 },
@@ -187,13 +207,36 @@ User settings always take priority over `omcCompat` defaults.
187
207
  - **Secure permissions**: Directories `0o700`, files `0o600`
188
208
  - **Race-condition safe**: Per-hook atomic counters (no shared JSON)
189
209
  - **Zero dependencies**: Node.js built-in modules only
190
- - **63 tests**: Security edge cases, error handling, cross-platform scenarios
210
+ - **Comprehensive test suite**: Security edge cases, error handling, cross-platform scenarios
191
211
 
192
212
  ## Requirements
193
213
 
194
214
  - Node.js >= 18
195
215
  - Claude Code with hook support (`.claude/settings.json`)
196
216
 
217
+ ## Troubleshooting
218
+
219
+ ### Hooks not firing
220
+ 1. Verify installation: `claude-smith check`
221
+ 2. Check `.claude/settings.json` contains hook definitions
222
+ 3. Ensure Node.js >= 18: `node --version`
223
+
224
+ ### False positives in parallel agents
225
+ Enable OMC compatibility mode in `.claude-smith.json`:
226
+ ```json
227
+ { "omcCompat": true }
228
+ ```
229
+ This raises thresholds for debug-loop, batch-checkpoint, scope-guard, and plan-guard.
230
+
231
+ ### Tests show as "not run"
232
+ The test-tracker hook only detects test commands in Bash tool calls. Ensure you run tests via the terminal, not inline.
233
+
234
+ ### Hook errors
235
+ Run individual hooks manually to debug:
236
+ ```bash
237
+ echo '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"},"session_id":"debug"}' | node .claude/hooks/tdd-guard.mjs
238
+ ```
239
+
197
240
  ## License
198
241
 
199
242
  MIT
package/bin/cli.mjs CHANGED
@@ -79,7 +79,7 @@ switch (command) {
79
79
 
80
80
  async function init() {
81
81
  const cwd = process.cwd();
82
- console.log(`\n🔨 Smith v${VERSION} - init\n`);
82
+ console.log(`\n🕵️ Smith v${VERSION} - init\n`);
83
83
 
84
84
  // 0. Determine language
85
85
  const lang = await resolveLanguage();
@@ -265,7 +265,7 @@ async function resolveLanguage() {
265
265
 
266
266
  function update() {
267
267
  const cwd = process.cwd();
268
- console.log(`\n🔨 Smith v${VERSION} - update\n`);
268
+ console.log(`\n🕵️ Smith v${VERSION} - update\n`);
269
269
 
270
270
  // 1. Overwrite hooks (always latest)
271
271
  const hooksDir = join(cwd, '.claude', 'hooks');
@@ -319,7 +319,8 @@ function injectRules(claudeMdPath, lang = 'en') {
319
319
  const fallback = join(TEMPLATES_DIR, 'rules.en.md');
320
320
  const rulesPath = existsSync(rulesFile) ? rulesFile : fallback;
321
321
  const rules = readFileSync(rulesPath, 'utf8')
322
- .replace(/claude-smith v\d+\.\d+\.\d+/g, `claude-smith v${VERSION}`);
322
+ .replace(/claude-smith v\d+\.\d+\.\d+/g, `claude-smith v${VERSION}`)
323
+ .replace(/\{\{SMITH_VERSION\}\}/g, `v${VERSION}`);
323
324
 
324
325
  if (existsSync(claudeMdPath)) {
325
326
  let content = readFileSync(claudeMdPath, 'utf8');
@@ -385,7 +386,7 @@ function check() {
385
386
  const ciMode = process.argv.includes('--ci');
386
387
  const results = { version: VERSION, ok: true, hooks: {}, settings: false, rules: false };
387
388
 
388
- if (!ciMode) console.log(`\n🔨 Smith v${VERSION} - check\n`);
389
+ if (!ciMode) console.log(`\n🕵️ Smith v${VERSION} - check\n`);
389
390
 
390
391
  // Check hooks
391
392
  const hooksDir = join(cwd, '.claude', 'hooks');
@@ -402,14 +403,14 @@ function check() {
402
403
 
403
404
  // Check commands
404
405
  const commandsDir = join(cwd, '.claude', 'commands');
405
- 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'];
406
407
  const missingCommands = expectedCommands.filter(c => !existsSync(join(commandsDir, c)));
407
- results.commands = { installed: 4 - missingCommands.length, total: 4, missing: missingCommands };
408
+ results.commands = { installed: 5 - missingCommands.length, total: 5, missing: missingCommands };
408
409
 
409
410
  if (!ciMode) {
410
411
  console.log(missingCommands.length === 0
411
- ? `✅ Commands: 4/4 installed`
412
- : `❌ Commands: ${4 - missingCommands.length}/4 (missing: ${missingCommands.join(', ')})`);
412
+ ? `✅ Commands: 5/5 installed`
413
+ : `❌ Commands: ${5 - missingCommands.length}/5 (missing: ${missingCommands.join(', ')})`);
413
414
  }
414
415
  if (missingCommands.length > 0) results.ok = false;
415
416
 
@@ -448,7 +449,7 @@ function check() {
448
449
  }
449
450
 
450
451
  function stats() {
451
- console.log(`\n🔨 Smith v${VERSION} - stats\n`);
452
+ console.log(`\n🕵️ Smith v${VERSION} - stats\n`);
452
453
 
453
454
  // Try to find stats from any active session
454
455
  const baseDir = join(tmpdir(), '.claude-smith');
@@ -500,7 +501,7 @@ function stats() {
500
501
 
501
502
  async function uninstall() {
502
503
  const cwd = process.cwd();
503
- console.log(`\n🔨 Smith v${VERSION} - uninstall\n`);
504
+ console.log(`\n🕵️ Smith v${VERSION} - uninstall\n`);
504
505
 
505
506
  const SMITH_HOOKS = ['tdd-guard.mjs', 'commit-guard.mjs', 'test-tracker.mjs', 'debug-loop.mjs',
506
507
  'batch-checkpoint.mjs', 'subagent-inject.mjs', 'commit-message.mjs', 'build-guard.mjs',
@@ -595,7 +596,7 @@ async function uninstall() {
595
596
  }
596
597
 
597
598
  function report() {
598
- console.log(`\n🔨 Smith v${VERSION} - session report\n`);
599
+ console.log(`\n🕵️ Smith v${VERSION} - session report\n`);
599
600
 
600
601
  const baseDir = join(tmpdir(), '.claude-smith');
601
602
  if (!existsSync(baseDir)) {
@@ -784,7 +785,7 @@ ${!isPreTool ? "const toolResponse = input.tool_response || {};\n" : ""}
784
785
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
785
786
  }
786
787
 
787
- console.log(`\n🔨 Smith v${VERSION} - create-hook\n`);
788
+ console.log(`\n🕵️ Smith v${VERSION} - create-hook\n`);
788
789
  console.log(`✅ Hook created → .claude/hooks/${fileName}`);
789
790
  console.log(`✅ Registered in .claude/settings.json (${event})`);
790
791
  console.log(`\nNext steps:`);
@@ -803,7 +804,7 @@ function escapeHtml(str) {
803
804
  }
804
805
 
805
806
  function dashboard() {
806
- console.log(`\n🔨 Smith v${VERSION} - dashboard\n`);
807
+ console.log(`\n🕵️ Smith v${VERSION} - dashboard\n`);
807
808
 
808
809
  const baseDir = join(tmpdir(), '.claude-smith');
809
810
  if (!existsSync(baseDir)) {
@@ -1056,7 +1057,7 @@ function dashboard() {
1056
1057
  <head>
1057
1058
  <meta charset="UTF-8">
1058
1059
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1059
- <title>🔨 Smith Dashboard</title>
1060
+ <title>🕵️ Smith Dashboard</title>
1060
1061
  <style>
1061
1062
  * { margin: 0; padding: 0; box-sizing: border-box; }
1062
1063
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; }
@@ -1113,7 +1114,7 @@ function dashboard() {
1113
1114
  </style>
1114
1115
  </head>
1115
1116
  <body>
1116
- <h1>🔨 Smith</h1>
1117
+ <h1>🕵️ Smith</h1>
1117
1118
  <p class="subtitle">Session Protection Report — ${new Date().toISOString().split('T')[0]}</p>
1118
1119
 
1119
1120
  <div class="hero-grid">
@@ -1188,7 +1189,7 @@ ${hookNames.map((name, i) => {
1188
1189
  ${insightsHtml}
1189
1190
  </div>
1190
1191
 
1191
- <footer>Generated by 🔨 Smith v${escapeHtml(VERSION)} | ${escapeHtml(new Date().toISOString())}</footer>
1192
+ <footer>Generated by 🕵️ Smith v${escapeHtml(VERSION)} | ${escapeHtml(new Date().toISOString())}</footer>
1192
1193
  </body>
1193
1194
  </html>`;
1194
1195
 
@@ -1200,7 +1201,7 @@ ${insightsHtml}
1200
1201
 
1201
1202
  function help() {
1202
1203
  console.log(`
1203
- 🔨 Smith v${VERSION}
1204
+ 🕵️ Smith v${VERSION}
1204
1205
  Claude Code workflow enforcement CLI - forging coding discipline into every session
1205
1206
 
1206
1207
  Usage:
@@ -5,34 +5,23 @@
5
5
  * Counts distinct files edited, reminds to report progress at threshold (default: 3)
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  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
15
 
15
16
  const config = getHookConfig('batch-checkpoint', process.cwd());
16
17
  if (!config.enabled) process.exit(0);
17
18
 
18
19
  // Skip in OMC autonomous execution modes (autopilot, ultrawork, ralph)
19
20
  // These modes manage their own progress reporting and checkpoint would interfere
20
- const omcStateDir = join(process.cwd(), '.omc', 'state');
21
- const omcAutoModes = ['autopilot-state.json', 'ultrawork-state.json', 'ralph-state.json'];
22
- const isOmcAutoMode = omcAutoModes.some(f => {
23
- try {
24
- const state = JSON.parse(readFileSync(join(omcStateDir, f), 'utf8'));
25
- return state.active === true || state.status === 'running';
26
- } catch { return false; }
27
- });
28
- if (isOmcAutoMode) process.exit(0);
21
+ if (isOmcAutoMode(process.cwd())) process.exit(0);
29
22
 
30
- let input;
31
- try {
32
- input = JSON.parse(readFileSync(0, 'utf8'));
33
- } catch {
34
- process.exit(0);
35
- }
23
+ const input = parseHookInput();
24
+ if (!input) process.exit(0);
36
25
 
37
26
  const toolName = input.tool_name;
38
27
  const filePath = input.tool_input?.file_path || '';
@@ -43,11 +32,6 @@ if (!filePath) process.exit(0);
43
32
  // Skip non-source files
44
33
  if (!/\.(ts|tsx|js|jsx|py|go|rs|css|json)$/.test(filePath)) process.exit(0);
45
34
 
46
- function sanitizeId(id) {
47
- if (!id || typeof id !== 'string') return 'default';
48
- return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
49
- }
50
-
51
35
  const sessionId = sanitizeId(input.session_id);
52
36
  const stateDir = join(tmpdir(), '.claude-smith', sessionId);
53
37
  mkdirSync(stateDir, { recursive: true, mode: 0o700 });
@@ -73,10 +57,11 @@ if (!editedFiles.includes(filePath)) {
73
57
  if (editedFiles.length > 0 && editedFiles.length % config.fileThreshold === 0) {
74
58
  recordEvent(sessionId, 'batch-checkpoint', 'warn');
75
59
  appendEvent(process.cwd(), { hook: 'batch-checkpoint', event: 'warn', message: `${editedFiles.length} files edited` });
60
+ notifyUser(config.notify, 'Batch Checkpoint', 'warn', `${editedFiles.length}개 파일 편집`);
76
61
  const output = JSON.stringify({
77
62
  hookSpecificOutput: {
78
63
  hookEventName: "PostToolUse",
79
- additionalContext: `🔨 Smith 📋 Batch Checkpoint (rule 2-9): ${editedFiles.length} files edited. Report progress to user before continuing.`
64
+ additionalContext: `🕵️ Smith 📋 Batch Checkpoint (rule 2-9): ${editedFiles.length} files edited. Report progress to user before continuing.`
80
65
  }
81
66
  });
82
67
  process.stdout.write(output);
@@ -11,16 +11,13 @@ 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
15
 
15
16
  const config = getHookConfig('build-guard', process.cwd());
16
17
  if (!config.enabled) process.exit(0);
17
18
 
18
- let input;
19
- try {
20
- input = JSON.parse(readFileSync(0, 'utf8'));
21
- } catch {
22
- process.exit(0);
23
- }
19
+ const input = parseHookInput();
20
+ if (!input) process.exit(0);
24
21
 
25
22
  const command = input.tool_input?.command || '';
26
23
 
@@ -28,11 +25,6 @@ if (input.tool_name !== 'Bash') process.exit(0);
28
25
  if (!command.includes('git commit')) process.exit(0);
29
26
  if (command.includes('--allow-empty')) process.exit(0);
30
27
 
31
- function sanitizeId(id) {
32
- if (!id || typeof id !== 'string') return 'default';
33
- return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
34
- }
35
-
36
28
  const sessionId = sanitizeId(input.session_id);
37
29
  const stateDir = join(tmpdir(), '.claude-smith', sessionId);
38
30
  const lastBuildResult = join(stateDir, 'last-build-result');
@@ -44,7 +36,8 @@ if (existsSync(lastBuildResult)) {
44
36
  if (result === 'fail') {
45
37
  recordEvent(sessionId, 'build-guard', 'block');
46
38
  appendEvent(process.cwd(), { hook: 'build-guard', event: 'block', message: 'Build failing' });
47
- process.stderr.write("🔨 Smith Commit blocked: last build FAILED. Run build and fix errors before committing.");
39
+ notifyUser(config.notify, 'Build Guard', 'block', '빌드 실패 커밋 차단');
40
+ process.stderr.write("🕵️ Smith ✋ Commit blocked: last build FAILED. Run build and fix errors before committing.");
48
41
  process.exit(2);
49
42
  }
50
43
  }
@@ -5,22 +5,19 @@
5
5
  * Records build pass/fail result for build-guard reference
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { writeFileSync, mkdirSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  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
15
 
15
16
  const config = getHookConfig('build-tracker', process.cwd());
16
17
  if (!config.enabled) process.exit(0);
17
18
 
18
- let input;
19
- try {
20
- input = JSON.parse(readFileSync(0, 'utf8'));
21
- } catch {
22
- process.exit(0);
23
- }
19
+ const input = parseHookInput();
20
+ if (!input) process.exit(0);
24
21
 
25
22
  const command = input.tool_input?.command || '';
26
23
 
@@ -28,11 +25,6 @@ if (input.tool_name !== 'Bash') process.exit(0);
28
25
 
29
26
  // Detect build commands
30
27
  if (/pnpm build|npm run build|yarn build|tsc|next build/.test(command)) {
31
- function sanitizeId(id) {
32
- if (!id || typeof id !== 'string') return 'default';
33
- return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
34
- }
35
-
36
28
  const sessionId = sanitizeId(input.session_id);
37
29
  const stateDir = join(tmpdir(), '.claude-smith', sessionId);
38
30
  mkdirSync(stateDir, { recursive: true, mode: 0o700 });
@@ -44,4 +36,5 @@ if (/pnpm build|npm run build|yarn build|tsc|next build/.test(command)) {
44
36
  writeFileSync(join(stateDir, 'last-build-result'), result, { mode: 0o600 });
45
37
  recordEvent(sessionId, 'build-tracker', 'fire');
46
38
  appendEvent(process.cwd(), { hook: 'build-tracker', event: 'fire', message: result });
39
+ notifyUser(config.notify, 'Build Tracker', 'track', failed ? '❌ fail' : '✅ pass');
47
40
  }
@@ -13,16 +13,13 @@ 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
17
 
17
18
  const config = getHookConfig('commit-guard', process.cwd());
18
19
  if (!config.enabled) process.exit(0);
19
20
 
20
- let input;
21
- try {
22
- input = JSON.parse(readFileSync(0, 'utf8'));
23
- } catch {
24
- process.exit(0);
25
- }
21
+ const input = parseHookInput();
22
+ if (!input) process.exit(0);
26
23
 
27
24
  const toolName = input.tool_name;
28
25
  const command = input.tool_input?.command || '';
@@ -33,26 +30,19 @@ if (!command.includes('git commit')) process.exit(0);
33
30
  // Skip amend, merge commits
34
31
  if (command.includes('--allow-empty')) process.exit(0);
35
32
 
36
- function sanitizeId(id) {
37
- if (!id || typeof id !== 'string') return 'default';
38
- return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
39
- }
40
-
41
33
  const sessionId = sanitizeId(input.session_id);
42
34
  const stateDir = join(tmpdir(), '.claude-smith', sessionId);
43
35
  const lastTestFile = join(stateDir, 'last-test-run');
44
36
  const lastResultFile = join(stateDir, 'last-test-result');
45
37
 
46
- recordEvent(sessionId, 'commit-guard', 'fire');
47
- appendEvent(process.cwd(), { hook: 'commit-guard', event: 'fire', message: 'Tests passed, commit allowed' });
48
-
49
38
  // Check test result first (hard block on failure)
50
39
  if (existsSync(lastResultFile)) {
51
40
  const result = readFileSync(lastResultFile, 'utf8').trim();
52
41
  if (result === 'fail') {
53
42
  recordEvent(sessionId, 'commit-guard', 'block');
54
43
  appendEvent(process.cwd(), { hook: 'commit-guard', event: 'block', message: 'Tests failing' });
55
- 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
+ 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.");
56
46
  process.exit(2);
57
47
  }
58
48
  }
@@ -65,22 +55,31 @@ if (existsSync(lastTestFile)) {
65
55
  if (diff > config.maxTestAge) {
66
56
  recordEvent(sessionId, 'commit-guard', 'warn');
67
57
  appendEvent(process.cwd(), { hook: 'commit-guard', event: 'warn', message: 'Tests stale' });
58
+ notifyUser(config.notify, 'Commit Guard', 'warn', '테스트 경과 시간 초과');
68
59
  const output = JSON.stringify({
69
60
  hookSpecificOutput: {
70
61
  hookEventName: "PreToolUse",
71
- additionalContext: `🔨 Smith > Commit Guard: Last test run was ${Math.floor(diff / 60)}min ago (>5min). Run tests before committing.`
62
+ additionalContext: `🕵️ Smith > Commit Guard: Last test run was ${Math.floor(diff / 60)}min ago (>5min). Run tests before committing.`
72
63
  }
73
64
  });
74
65
  process.stdout.write(output);
66
+ process.exit(0);
75
67
  }
76
68
  } else {
77
69
  recordEvent(sessionId, 'commit-guard', 'warn');
78
70
  appendEvent(process.cwd(), { hook: 'commit-guard', event: 'warn', message: 'No test run detected' });
71
+ notifyUser(config.notify, 'Commit Guard', 'warn', '테스트 실행 기록 없음');
79
72
  const output = JSON.stringify({
80
73
  hookSpecificOutput: {
81
74
  hookEventName: "PreToolUse",
82
- 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."
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."
83
76
  }
84
77
  });
85
78
  process.stdout.write(output);
79
+ process.exit(0);
86
80
  }
81
+
82
+ // All checks passed - commit allowed
83
+ recordEvent(sessionId, 'commit-guard', 'fire');
84
+ appendEvent(process.cwd(), { hook: 'commit-guard', event: 'fire', message: 'Commit allowed' });
85
+ notifyUser(config.notify, 'Commit Guard', 'fire', '커밋 허용');
@@ -5,40 +5,32 @@
5
5
  * Enforces conventional commits format: type(scope): description
6
6
  */
7
7
 
8
- import { readFileSync } from 'fs';
9
8
  import { getHookConfig } from '../lib/config.mjs';
10
9
  import { recordEvent } from '../lib/stats.mjs';
11
10
  import { appendEvent } from '../lib/event-log.mjs';
11
+ import { sanitizeId, parseHookInput, notifyUser } from '../lib/hook-utils.mjs';
12
12
 
13
13
  const config = getHookConfig('commit-message', process.cwd());
14
14
  if (!config.enabled) process.exit(0);
15
15
 
16
- let input;
17
- try {
18
- input = JSON.parse(readFileSync(0, 'utf8'));
19
- } catch {
20
- process.exit(0);
21
- }
16
+ const input = parseHookInput();
17
+ if (!input) process.exit(0);
22
18
 
23
19
  const command = input.tool_input?.command || '';
24
20
 
25
21
  if (input.tool_name !== 'Bash') process.exit(0);
26
22
  if (!command.includes('git commit')) process.exit(0);
27
23
 
28
- function sanitizeId(id) {
29
- if (!id || typeof id !== 'string') return 'default';
30
- return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
31
- }
32
-
33
24
  const sessionId = sanitizeId(input.session_id);
34
25
 
35
26
  // Extract commit message from -m flag
36
- const msgMatch = command.match(/-m\s+"([^"]+)"/s)
37
- || command.match(/-m\s+'([^']+)'/s)
38
- || command.match(/-m\s+"\$\(cat\s+<<'?EOF'?\s*\n([\s\S]*?)\n\s*EOF/);
27
+ // Check HEREDOC format first (used by Claude Code)
28
+ const msgMatch = command.match(/-m\s+"\$\(cat\s+<<'?EOF'?\s*\n([\s\S]*?)\n\s*EOF/)
29
+ || command.match(/-m\s+"([^"]+)"/s)
30
+ || command.match(/-m\s+'([^']+)'/s);
39
31
  if (!msgMatch) process.exit(0); // Can't parse message, let it through
40
32
 
41
- const msg = msgMatch[1] || msgMatch[2] || '';
33
+ const msg = msgMatch[1] || '';
42
34
  const firstLine = msg.split('\n')[0].trim();
43
35
 
44
36
  // Conventional commit pattern: type(scope): description OR type: description
@@ -47,11 +39,17 @@ const pattern = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)
47
39
  if (!pattern.test(firstLine)) {
48
40
  recordEvent(sessionId, 'commit-message', 'warn');
49
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)}`);
50
43
  const output = JSON.stringify({
51
44
  hookSpecificOutput: {
52
45
  hookEventName: "PreToolUse",
53
- additionalContext: `🔨 Smith > Commit Message: "${firstLine}" doesn't follow conventional commits. Expected: type(scope): description (e.g., feat(auth): add login, fix: resolve crash)`
46
+ additionalContext: `🕵️ Smith > Commit Message: "${firstLine}" doesn't follow conventional commits. Expected: type(scope): description (e.g., feat(auth): add login, fix: resolve crash)`
54
47
  }
55
48
  });
56
49
  process.stdout.write(output);
50
+ } else {
51
+ // Format is valid
52
+ recordEvent(sessionId, 'commit-message', 'fire');
53
+ appendEvent(process.cwd(), { hook: 'commit-message', event: 'fire', message: 'Format valid' });
54
+ notifyUser(config.notify, 'Commit Message', 'fire', '형식 유효');
57
55
  }