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.
- 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 +8 -20
- 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
|
@@ -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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
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
|
}
|