claude-smith 3.1.0 → 3.2.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 +45 -3
- package/README.md +45 -3
- package/bin/cli.mjs +14 -13
- package/hooks/batch-checkpoint.mjs +7 -22
- package/hooks/build-guard.mjs +5 -12
- package/hooks/build-tracker.mjs +5 -12
- package/hooks/commit-guard.mjs +16 -17
- package/hooks/commit-message.mjs +15 -17
- package/hooks/debug-loop.mjs +8 -22
- package/hooks/file-size-warn.mjs +5 -12
- package/hooks/plan-guard.mjs +10 -24
- package/hooks/scope-guard.mjs +7 -23
- package/hooks/subagent-inject.mjs +4 -12
- package/hooks/tdd-guard.mjs +6 -13
- package/hooks/test-tracker.mjs +5 -12
- package/lib/config.mjs +4 -1
- package/lib/event-log.mjs +25 -3
- package/lib/hook-utils.mjs +69 -0
- package/lib/stats.mjs +32 -5
- package/package.json +1 -1
- package/templates/commands/smith-update.md +1 -1
- package/templates/rules.en.md +3 -3
- package/templates/rules.ja.md +3 -3
- package/templates/rules.ko.md +3 -3
- package/templates/rules.md +3 -3
- package/templates/rules.zh.md +3 -3
package/README.ko.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[English](README.md) | **한국어**
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# 🕵️ claude-smith
|
|
4
4
|
|
|
5
|
-
>
|
|
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 — 진행 자체를 막음)
|
|
@@ -148,6 +166,7 @@ claude-smith uninstall # 모든 구성 요소 제거
|
|
|
148
166
|
```json
|
|
149
167
|
{
|
|
150
168
|
"language": "ko",
|
|
169
|
+
"notify": false,
|
|
151
170
|
"hooks": {
|
|
152
171
|
"commit-guard": { "maxTestAge": 300 },
|
|
153
172
|
"debug-loop": { "warnAt": 3, "blockAt": 5 },
|
|
@@ -187,13 +206,36 @@ claude-smith uninstall # 모든 구성 요소 제거
|
|
|
187
206
|
- **안전한 권한**: 디렉터리 `0o700`, 파일 `0o600`
|
|
188
207
|
- **경쟁 조건 방지**: Hook별 원자적 카운터 (공유 JSON 미사용)
|
|
189
208
|
- **제로 의존성**: Node.js 기본 모듈만 사용
|
|
190
|
-
-
|
|
209
|
+
- **포괄적 테스트 스위트**: 보안 엣지 케이스, 에러 처리, 크로스 플랫폼 시나리오
|
|
191
210
|
|
|
192
211
|
## 요구 사항
|
|
193
212
|
|
|
194
213
|
- Node.js >= 18
|
|
195
214
|
- Hook을 지원하는 Claude Code (`.claude/settings.json`)
|
|
196
215
|
|
|
216
|
+
## 문제 해결
|
|
217
|
+
|
|
218
|
+
### Hook이 작동하지 않음
|
|
219
|
+
1. 설치 검증: `claude-smith check`
|
|
220
|
+
2. `.claude/settings.json`에 Hook 정의가 있는지 확인
|
|
221
|
+
3. Node.js >= 18 확인: `node --version`
|
|
222
|
+
|
|
223
|
+
### 병렬 에이전트에서 오탐 발생
|
|
224
|
+
`.claude-smith.json`에서 OMC 호환 모드 활성화:
|
|
225
|
+
```json
|
|
226
|
+
{ "omcCompat": true }
|
|
227
|
+
```
|
|
228
|
+
debug-loop, batch-checkpoint, scope-guard, plan-guard 임계값이 상향됩니다.
|
|
229
|
+
|
|
230
|
+
### 테스트가 "실행 안 됨"으로 표시됨
|
|
231
|
+
test-tracker Hook은 Bash 도구 호출의 테스트 명령만 감지합니다. 인라인이 아닌 터미널을 통해 테스트를 실행하세요.
|
|
232
|
+
|
|
233
|
+
### Hook 에러
|
|
234
|
+
개별 Hook을 수동으로 실행하여 디버깅:
|
|
235
|
+
```bash
|
|
236
|
+
echo '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"},"session_id":"debug"}' | node .claude/hooks/tdd-guard.mjs
|
|
237
|
+
```
|
|
238
|
+
|
|
197
239
|
## 라이선스
|
|
198
240
|
|
|
199
241
|
MIT
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
**English** | [한국어](README.ko.md)
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# 🕵️ claude-smith
|
|
4
4
|
|
|
5
|
-
> **
|
|
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)
|
|
@@ -148,6 +166,7 @@ Project-level settings in `.claude-smith.json` (all optional):
|
|
|
148
166
|
```json
|
|
149
167
|
{
|
|
150
168
|
"language": "en",
|
|
169
|
+
"notify": false,
|
|
151
170
|
"hooks": {
|
|
152
171
|
"commit-guard": { "maxTestAge": 300 },
|
|
153
172
|
"debug-loop": { "warnAt": 3, "blockAt": 5 },
|
|
@@ -187,13 +206,36 @@ User settings always take priority over `omcCompat` defaults.
|
|
|
187
206
|
- **Secure permissions**: Directories `0o700`, files `0o600`
|
|
188
207
|
- **Race-condition safe**: Per-hook atomic counters (no shared JSON)
|
|
189
208
|
- **Zero dependencies**: Node.js built-in modules only
|
|
190
|
-
- **
|
|
209
|
+
- **Comprehensive test suite**: Security edge cases, error handling, cross-platform scenarios
|
|
191
210
|
|
|
192
211
|
## Requirements
|
|
193
212
|
|
|
194
213
|
- Node.js >= 18
|
|
195
214
|
- Claude Code with hook support (`.claude/settings.json`)
|
|
196
215
|
|
|
216
|
+
## Troubleshooting
|
|
217
|
+
|
|
218
|
+
### Hooks not firing
|
|
219
|
+
1. Verify installation: `claude-smith check`
|
|
220
|
+
2. Check `.claude/settings.json` contains hook definitions
|
|
221
|
+
3. Ensure Node.js >= 18: `node --version`
|
|
222
|
+
|
|
223
|
+
### False positives in parallel agents
|
|
224
|
+
Enable OMC compatibility mode in `.claude-smith.json`:
|
|
225
|
+
```json
|
|
226
|
+
{ "omcCompat": true }
|
|
227
|
+
```
|
|
228
|
+
This raises thresholds for debug-loop, batch-checkpoint, scope-guard, and plan-guard.
|
|
229
|
+
|
|
230
|
+
### Tests show as "not run"
|
|
231
|
+
The test-tracker hook only detects test commands in Bash tool calls. Ensure you run tests via the terminal, not inline.
|
|
232
|
+
|
|
233
|
+
### Hook errors
|
|
234
|
+
Run individual hooks manually to debug:
|
|
235
|
+
```bash
|
|
236
|
+
echo '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"},"session_id":"debug"}' | node .claude/hooks/tdd-guard.mjs
|
|
237
|
+
```
|
|
238
|
+
|
|
197
239
|
## License
|
|
198
240
|
|
|
199
241
|
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
|
|
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
|
|
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
|
|
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');
|
|
@@ -448,7 +449,7 @@ function check() {
|
|
|
448
449
|
}
|
|
449
450
|
|
|
450
451
|
function stats() {
|
|
451
|
-
console.log(`\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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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:
|
|
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);
|
package/hooks/build-guard.mjs
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
}
|
package/hooks/build-tracker.mjs
CHANGED
|
@@ -5,22 +5,19 @@
|
|
|
5
5
|
* Records build pass/fail result for build-guard reference
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
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
|
-
|
|
19
|
-
|
|
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
|
}
|
package/hooks/commit-guard.mjs
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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:
|
|
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: "
|
|
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', '커밋 허용');
|
package/hooks/commit-message.mjs
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|| command.match(/-m\s+"
|
|
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] ||
|
|
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:
|
|
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
|
}
|
package/hooks/debug-loop.mjs
CHANGED
|
@@ -12,16 +12,13 @@ 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
16
|
|
|
16
17
|
const config = getHookConfig('debug-loop', process.cwd());
|
|
17
18
|
if (!config.enabled) process.exit(0);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
22
|
-
} catch {
|
|
23
|
-
process.exit(0);
|
|
24
|
-
}
|
|
20
|
+
const input = parseHookInput();
|
|
21
|
+
if (!input) process.exit(0);
|
|
25
22
|
|
|
26
23
|
const toolName = input.tool_name;
|
|
27
24
|
const filePath = input.tool_input?.file_path || '';
|
|
@@ -32,11 +29,6 @@ if (!filePath) process.exit(0);
|
|
|
32
29
|
// Skip non-source files
|
|
33
30
|
if (!/\.(ts|tsx|js|jsx|py|go|rs)$/.test(filePath)) process.exit(0);
|
|
34
31
|
|
|
35
|
-
function sanitizeId(id) {
|
|
36
|
-
if (!id || typeof id !== 'string') return 'default';
|
|
37
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
32
|
// Isolate state per agent when sub-agents provide their own ID
|
|
41
33
|
const sessionId = sanitizeId(input.session_id);
|
|
42
34
|
const agentId = sanitizeId(input.agent_id || '');
|
|
@@ -45,15 +37,7 @@ const stateDir = join(tmpdir(), '.claude-smith', stateKey);
|
|
|
45
37
|
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
46
38
|
|
|
47
39
|
// Raise thresholds during OMC autonomous modes to avoid false positives in parallel execution
|
|
48
|
-
|
|
49
|
-
const omcAutoModes = ['autopilot-state.json', 'ultrawork-state.json', 'ralph-state.json'];
|
|
50
|
-
const isOmcAutoMode = omcAutoModes.some(f => {
|
|
51
|
-
try {
|
|
52
|
-
const state = JSON.parse(readFileSync(join(omcStateDir, f), 'utf8'));
|
|
53
|
-
return state.active === true || state.status === 'running';
|
|
54
|
-
} catch { return false; }
|
|
55
|
-
});
|
|
56
|
-
if (isOmcAutoMode) {
|
|
40
|
+
if (isOmcAutoMode(process.cwd())) {
|
|
57
41
|
config.warnAt = Math.max(config.warnAt, 6);
|
|
58
42
|
config.blockAt = Math.max(config.blockAt, 10);
|
|
59
43
|
}
|
|
@@ -102,16 +86,18 @@ const patternHint = pattern === 'divergent'
|
|
|
102
86
|
if (count === config.warnAt) {
|
|
103
87
|
recordEvent(sessionId, 'debug-loop', 'warn');
|
|
104
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' ? ' (반복 패턴)' : ''}`);
|
|
105
90
|
const output = JSON.stringify({
|
|
106
91
|
hookSpecificOutput: {
|
|
107
92
|
hookEventName: "PostToolUse",
|
|
108
|
-
additionalContext:
|
|
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.`
|
|
109
94
|
}
|
|
110
95
|
});
|
|
111
96
|
process.stdout.write(output);
|
|
112
97
|
} else if (count >= config.blockAt) {
|
|
113
98
|
recordEvent(sessionId, 'debug-loop', 'block');
|
|
114
99
|
appendEvent(process.cwd(), { hook: 'debug-loop', event: 'block', file: filePath, message: `Edit #${count} - blocked` });
|
|
115
|
-
|
|
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.`);
|
|
116
102
|
process.exit(2);
|
|
117
103
|
}
|
package/hooks/file-size-warn.mjs
CHANGED
|
@@ -10,16 +10,13 @@ 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
14
|
|
|
14
15
|
const config = getHookConfig('file-size-warn', process.cwd());
|
|
15
16
|
if (!config.enabled) process.exit(0);
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
20
|
-
} catch {
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
18
|
+
const input = parseHookInput();
|
|
19
|
+
if (!input) process.exit(0);
|
|
23
20
|
|
|
24
21
|
const filePath = input.tool_input?.file_path || '';
|
|
25
22
|
|
|
@@ -31,11 +28,6 @@ if (!/\.(ts|tsx|js|jsx|py|go|rs)$/.test(filePath)) process.exit(0);
|
|
|
31
28
|
|
|
32
29
|
if (!existsSync(filePath)) process.exit(0);
|
|
33
30
|
|
|
34
|
-
function sanitizeId(id) {
|
|
35
|
-
if (!id || typeof id !== 'string') return 'default';
|
|
36
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
31
|
const sessionId = sanitizeId(input.session_id);
|
|
40
32
|
|
|
41
33
|
const lines = readFileSync(filePath, 'utf8').split('\n').length;
|
|
@@ -44,10 +36,11 @@ const threshold = config.maxLines || 500;
|
|
|
44
36
|
if (lines > threshold) {
|
|
45
37
|
recordEvent(sessionId, 'file-size-warn', 'warn');
|
|
46
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})`);
|
|
47
40
|
const output = JSON.stringify({
|
|
48
41
|
hookSpecificOutput: {
|
|
49
42
|
hookEventName: "PostToolUse",
|
|
50
|
-
additionalContext:
|
|
43
|
+
additionalContext: `🕵️ Smith > File Size: ${basename(filePath)} has ${lines} lines (>${threshold}). Consider splitting into smaller modules.`
|
|
51
44
|
}
|
|
52
45
|
});
|
|
53
46
|
process.stdout.write(output);
|
package/hooks/plan-guard.mjs
CHANGED
|
@@ -13,25 +13,17 @@ 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
17
|
|
|
17
18
|
const config = getHookConfig('plan-guard', process.cwd());
|
|
18
19
|
if (!config.enabled) process.exit(0);
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
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 filePath = input.tool_input?.file_path || '';
|
|
28
25
|
if (!filePath) process.exit(0);
|
|
29
26
|
|
|
30
|
-
function sanitizeId(id) {
|
|
31
|
-
if (!id || typeof id !== 'string') return 'default';
|
|
32
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
27
|
const sessionId = sanitizeId(input.session_id);
|
|
36
28
|
const agentId = sanitizeId(input.agent_id || '');
|
|
37
29
|
const stateKey = agentId && agentId !== 'default' ? `${sessionId}-${agentId}` : sessionId;
|
|
@@ -92,23 +84,16 @@ count++;
|
|
|
92
84
|
writeFileSync(counterFile, String(count), { mode: 0o600 });
|
|
93
85
|
|
|
94
86
|
// Raise threshold during OMC autonomous modes
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
try {
|
|
99
|
-
const state = JSON.parse(readFileSync(join(omcStateDir, f), 'utf8'));
|
|
100
|
-
return state.active === true || state.status === 'running';
|
|
101
|
-
} catch { return false; }
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const warnAt = isOmcAutoMode ? (config.warnAt || 5) * 2 : (config.warnAt || 5);
|
|
105
|
-
const blockAt = isOmcAutoMode ? (config.blockAt || 10) * 2 : (config.blockAt || 10);
|
|
87
|
+
const isOmc = isOmcAutoMode(process.cwd());
|
|
88
|
+
const warnAt = isOmc ? (config.warnAt || 5) * 2 : (config.warnAt || 5);
|
|
89
|
+
const blockAt = isOmc ? (config.blockAt || 10) * 2 : (config.blockAt || 10);
|
|
106
90
|
|
|
107
91
|
// Block if threshold reached
|
|
108
92
|
if (count >= blockAt) {
|
|
109
93
|
recordEvent(sessionId, 'plan-guard', 'block');
|
|
110
94
|
appendEvent(process.cwd(), { hook: 'plan-guard', event: 'block', message: `${count} files without plan` });
|
|
111
|
-
|
|
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.`);
|
|
112
97
|
process.exit(2);
|
|
113
98
|
}
|
|
114
99
|
|
|
@@ -116,10 +101,11 @@ if (count >= blockAt) {
|
|
|
116
101
|
if (count >= warnAt) {
|
|
117
102
|
recordEvent(sessionId, 'plan-guard', 'warn');
|
|
118
103
|
appendEvent(process.cwd(), { hook: 'plan-guard', event: 'warn', message: `${count} files without plan` });
|
|
104
|
+
notifyUser(config.notify, 'Plan Guard', 'warn', `${count}개 파일 계획 없이 편집`);
|
|
119
105
|
const output = JSON.stringify({
|
|
120
106
|
hookSpecificOutput: {
|
|
121
107
|
hookEventName: "PreToolUse",
|
|
122
|
-
additionalContext:
|
|
108
|
+
additionalContext: `🕵️ Smith > Plan Guard: ${count} code files edited without a plan. Complex work needs decomposition first.
|
|
123
109
|
💡 Create a plan file (docs/plans/*.md or plan.md) that includes:
|
|
124
110
|
1. Each unit's responsibility (one sentence)
|
|
125
111
|
2. Dependency direction between units
|
package/hooks/scope-guard.mjs
CHANGED
|
@@ -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, isOmcAutoMode, notifyUser } from '../lib/hook-utils.mjs';
|
|
14
15
|
|
|
15
16
|
const config = getHookConfig('scope-guard', process.cwd());
|
|
16
17
|
if (!config.enabled) process.exit(0);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
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 filePath = input.tool_input?.file_path || '';
|
|
26
23
|
|
|
@@ -28,11 +25,6 @@ if (!['Edit', 'Write'].includes(input.tool_name)) process.exit(0);
|
|
|
28
25
|
if (!filePath) process.exit(0);
|
|
29
26
|
if (!/\.(ts|tsx|js|jsx|py|go|rs|css|json)$/.test(filePath)) 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
|
// Isolate state per agent when sub-agents provide their own ID
|
|
37
29
|
const sessionId = sanitizeId(input.session_id);
|
|
38
30
|
const agentId = sanitizeId(input.agent_id || '');
|
|
@@ -40,16 +32,6 @@ const stateKey = agentId && agentId !== 'default' ? `${sessionId}-${agentId}` :
|
|
|
40
32
|
const stateDir = join(tmpdir(), '.claude-smith', stateKey);
|
|
41
33
|
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
42
34
|
|
|
43
|
-
// Raise threshold during OMC autonomous modes to avoid false positives in parallel execution
|
|
44
|
-
const omcStateDir = join(process.cwd(), '.omc', 'state');
|
|
45
|
-
const omcAutoModes = ['autopilot-state.json', 'ultrawork-state.json', 'ralph-state.json'];
|
|
46
|
-
const isOmcAutoMode = omcAutoModes.some(f => {
|
|
47
|
-
try {
|
|
48
|
-
const state = JSON.parse(readFileSync(join(omcStateDir, f), 'utf8'));
|
|
49
|
-
return state.active === true || state.status === 'running';
|
|
50
|
-
} catch { return false; }
|
|
51
|
-
});
|
|
52
|
-
|
|
53
35
|
const stateFile = join(stateDir, 'scope-directories');
|
|
54
36
|
const dir = dirname(filePath);
|
|
55
37
|
|
|
@@ -67,15 +49,17 @@ if (!dirs.includes(dir)) {
|
|
|
67
49
|
writeFileSync(stateFile, JSON.stringify(dirs), { mode: 0o600 });
|
|
68
50
|
}
|
|
69
51
|
|
|
70
|
-
|
|
52
|
+
// Raise threshold during OMC autonomous modes to avoid false positives in parallel execution
|
|
53
|
+
const threshold = isOmcAutoMode(process.cwd()) ? Math.max((config.maxDirectories || 5) * 2, 10) : (config.maxDirectories || 5);
|
|
71
54
|
|
|
72
55
|
if (dirs.length >= threshold && (dirs.length === threshold || dirs.length % threshold === 0)) {
|
|
73
56
|
recordEvent(sessionId, 'scope-guard', 'warn');
|
|
74
57
|
appendEvent(process.cwd(), { hook: 'scope-guard', event: 'warn', message: `${dirs.length} directories` });
|
|
58
|
+
notifyUser(config.notify, 'Scope Guard', 'warn', `${dirs.length}개 디렉토리 변경`);
|
|
75
59
|
const output = JSON.stringify({
|
|
76
60
|
hookSpecificOutput: {
|
|
77
61
|
hookEventName: "PostToolUse",
|
|
78
|
-
additionalContext:
|
|
62
|
+
additionalContext: `🕵️ Smith > Scope Guard: Changes span ${dirs.length} directories (>=${threshold}). Is the scope too broad? Consider breaking into smaller tasks.`
|
|
79
63
|
}
|
|
80
64
|
});
|
|
81
65
|
process.stdout.write(output);
|
|
@@ -5,31 +5,23 @@
|
|
|
5
5
|
* Injects core TDD/verification rules into subagent context
|
|
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('subagent-inject', process.cwd());
|
|
14
14
|
if (!config.enabled) process.exit(0);
|
|
15
15
|
|
|
16
16
|
// Consume stdin (required by hook protocol)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
20
|
-
} catch {
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function sanitizeId(id) {
|
|
25
|
-
if (!id || typeof id !== 'string') return 'default';
|
|
26
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
27
|
-
}
|
|
17
|
+
const input = parseHookInput();
|
|
18
|
+
if (!input) process.exit(0);
|
|
28
19
|
|
|
29
20
|
const sessionId = sanitizeId(input.session_id);
|
|
30
21
|
|
|
31
22
|
recordEvent(sessionId, 'subagent-inject', 'fire');
|
|
32
23
|
appendEvent(process.cwd(), { hook: 'subagent-inject', event: 'fire', message: 'Rules injected into subagent' });
|
|
24
|
+
notifyUser(config.notify, 'Subagent Inject', 'inject', '규칙 주입됨');
|
|
33
25
|
const output = JSON.stringify({
|
|
34
26
|
hookSpecificOutput: {
|
|
35
27
|
hookEventName: "SubagentStart",
|
package/hooks/tdd-guard.mjs
CHANGED
|
@@ -5,21 +5,18 @@
|
|
|
5
5
|
* Warns when editing a source file that has no corresponding test file
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
9
|
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
14
|
|
|
14
15
|
const config = getHookConfig('tdd-guard', process.cwd());
|
|
15
16
|
if (!config.enabled) process.exit(0);
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
20
|
-
} catch {
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
18
|
+
const input = parseHookInput();
|
|
19
|
+
if (!input) process.exit(0);
|
|
23
20
|
|
|
24
21
|
const filePath = input.tool_input?.file_path || '';
|
|
25
22
|
|
|
@@ -42,11 +39,6 @@ if (/node_modules|\.next|dist|build|\.storybook/.test(filePath)) process.exit(0)
|
|
|
42
39
|
// Skip layout, page files (Next.js App Router)
|
|
43
40
|
if (/\/(layout|page|loading|error|not-found)\.(ts|tsx)$/.test(filePath)) process.exit(0);
|
|
44
41
|
|
|
45
|
-
function sanitizeId(id) {
|
|
46
|
-
if (!id || typeof id !== 'string') return 'default';
|
|
47
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
42
|
const sessionId = sanitizeId(input.session_id);
|
|
51
43
|
|
|
52
44
|
const dir = dirname(filePath);
|
|
@@ -61,10 +53,11 @@ const testFileTs = join(dir, `${name}.test.ts`);
|
|
|
61
53
|
if (!existsSync(testFile) && !existsSync(specFile) && !existsSync(testFileTsx) && !existsSync(testFileTs)) {
|
|
62
54
|
recordEvent(sessionId, 'tdd-guard', 'warn');
|
|
63
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)} 테스트 파일 없음`);
|
|
64
57
|
const output = JSON.stringify({
|
|
65
58
|
hookSpecificOutput: {
|
|
66
59
|
hookEventName: "PreToolUse",
|
|
67
|
-
additionalContext:
|
|
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.`
|
|
68
61
|
}
|
|
69
62
|
});
|
|
70
63
|
process.stdout.write(output);
|
package/hooks/test-tracker.mjs
CHANGED
|
@@ -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('test-tracker', process.cwd());
|
|
16
17
|
if (!config.enabled) process.exit(0);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
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 toolName = input.tool_name;
|
|
26
23
|
const command = input.tool_input?.command || '';
|
|
@@ -29,11 +26,6 @@ if (toolName !== 'Bash') process.exit(0);
|
|
|
29
26
|
|
|
30
27
|
// Detect test commands
|
|
31
28
|
if (/pnpm test|npm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test|make test|mocha/.test(command)) {
|
|
32
|
-
function sanitizeId(id) {
|
|
33
|
-
if (!id || typeof id !== 'string') return 'default';
|
|
34
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
29
|
const sessionId = sanitizeId(input.session_id);
|
|
38
30
|
const stateDir = join(tmpdir(), '.claude-smith', sessionId);
|
|
39
31
|
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
@@ -50,6 +42,7 @@ if (/pnpm test|npm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test
|
|
|
50
42
|
writeFileSync(join(stateDir, 'last-test-result'), failed ? 'fail' : 'pass', { mode: 0o600 });
|
|
51
43
|
recordEvent(sessionId, 'test-tracker', 'fire');
|
|
52
44
|
appendEvent(process.cwd(), { hook: 'test-tracker', event: 'fire', message: `${failed ? 'fail' : 'pass'}` });
|
|
45
|
+
notifyUser(config.notify, 'Test Tracker', 'track', failed ? '❌ fail' : '✅ pass');
|
|
53
46
|
|
|
54
47
|
// Extract coverage percentage from test output
|
|
55
48
|
function parseCoverage(text) {
|
|
@@ -82,7 +75,7 @@ if (/pnpm test|npm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test
|
|
|
82
75
|
const output = JSON.stringify({
|
|
83
76
|
hookSpecificOutput: {
|
|
84
77
|
hookEventName: "PostToolUse",
|
|
85
|
-
additionalContext:
|
|
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`
|
|
86
79
|
}
|
|
87
80
|
});
|
|
88
81
|
process.stdout.write(output);
|
package/lib/config.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from 'path';
|
|
|
9
9
|
const DEFAULTS = {
|
|
10
10
|
language: 'en',
|
|
11
11
|
omcCompat: false,
|
|
12
|
+
notify: false,
|
|
12
13
|
hooks: {
|
|
13
14
|
'tdd-guard': { enabled: true },
|
|
14
15
|
'commit-guard': { enabled: true, maxTestAge: 300 },
|
|
@@ -68,7 +69,9 @@ export function loadConfig(projectRoot) {
|
|
|
68
69
|
|
|
69
70
|
export function getHookConfig(hookName, projectRoot) {
|
|
70
71
|
const config = loadConfig(projectRoot);
|
|
71
|
-
|
|
72
|
+
const hookConfig = config.hooks?.[hookName] || DEFAULTS.hooks[hookName] || { enabled: true };
|
|
73
|
+
hookConfig.notify = config.notify || false;
|
|
74
|
+
return hookConfig;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
package/lib/event-log.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Provides project-level audit trail of hook activity
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { appendFileSync, readFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
|
|
9
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync, readdirSync, lstatSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -21,9 +21,19 @@ export function appendEvent(projectDir, { hook, event, file = '', message = '' }
|
|
|
21
21
|
const eventsDir = join(projectDir, '.smith', 'events');
|
|
22
22
|
mkdirSync(eventsDir, { recursive: true, mode: 0o700 });
|
|
23
23
|
|
|
24
|
+
// Symlink check: ensure events dir is a real directory, not a symlink
|
|
25
|
+
const stat = lstatSync(eventsDir);
|
|
26
|
+
if (!stat.isDirectory()) return; // Don't write to symlinks
|
|
27
|
+
|
|
24
28
|
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
25
29
|
const logFile = join(eventsDir, `${today}.jsonl`);
|
|
26
30
|
|
|
31
|
+
// Also check the log file isn't a symlink
|
|
32
|
+
if (existsSync(logFile)) {
|
|
33
|
+
const fileStat = lstatSync(logFile);
|
|
34
|
+
if (!fileStat.isFile()) return;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
const entry = JSON.stringify({
|
|
28
38
|
t: new Date().toISOString(),
|
|
29
39
|
h: hook,
|
|
@@ -50,10 +60,22 @@ export function readEvents(projectDir, { days = 7 } = {}) {
|
|
|
50
60
|
const eventsDir = join(projectDir, '.smith', 'events');
|
|
51
61
|
if (!existsSync(eventsDir)) return [];
|
|
52
62
|
|
|
63
|
+
// Symlink check: ensure events dir is a real directory
|
|
64
|
+
const stat = lstatSync(eventsDir);
|
|
65
|
+
if (!stat.isDirectory()) return [];
|
|
66
|
+
|
|
67
|
+
// Calculate cutoff date for filtering
|
|
68
|
+
const cutoff = new Date();
|
|
69
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
70
|
+
const cutoffStr = cutoff.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
71
|
+
|
|
53
72
|
const files = readdirSync(eventsDir)
|
|
54
73
|
.filter(f => f.endsWith('.jsonl'))
|
|
55
|
-
.
|
|
56
|
-
|
|
74
|
+
.filter(f => {
|
|
75
|
+
const dateStr = f.replace('.jsonl', '');
|
|
76
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(dateStr) && dateStr >= cutoffStr;
|
|
77
|
+
})
|
|
78
|
+
.sort();
|
|
57
79
|
|
|
58
80
|
const events = [];
|
|
59
81
|
for (const file of files) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Utilities
|
|
3
|
+
* Common functions used across hook scripts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize session/agent IDs to prevent path traversal
|
|
11
|
+
* Currently duplicated 11 times across hooks
|
|
12
|
+
* @param {string} id - The ID to sanitize
|
|
13
|
+
* @returns {string} Sanitized ID safe for filesystem use
|
|
14
|
+
*/
|
|
15
|
+
export function sanitizeId(id) {
|
|
16
|
+
if (!id || typeof id !== 'string') return 'default';
|
|
17
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if OMC autonomous mode is active
|
|
22
|
+
* Currently duplicated 4 times (debug-loop, scope-guard, batch-checkpoint, plan-guard)
|
|
23
|
+
* @param {string} cwd - Current working directory
|
|
24
|
+
* @returns {boolean} True if any OMC autonomous mode is active
|
|
25
|
+
*/
|
|
26
|
+
export function isOmcAutoMode(cwd) {
|
|
27
|
+
const omcStateDir = join(cwd, '.omc', 'state');
|
|
28
|
+
const omcAutoModes = ['autopilot-state.json', 'ultrawork-state.json', 'ralph-state.json'];
|
|
29
|
+
return omcAutoModes.some(f => {
|
|
30
|
+
try {
|
|
31
|
+
const state = JSON.parse(readFileSync(join(omcStateDir, f), 'utf8'));
|
|
32
|
+
return state.active === true || state.status === 'running';
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse hook input from stdin (JSON)
|
|
39
|
+
* Common pattern across all hooks
|
|
40
|
+
* @returns {object|null} Parsed input or null if parsing fails
|
|
41
|
+
*/
|
|
42
|
+
export function parseHookInput() {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(0, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send a brief notification to stderr for user visibility
|
|
52
|
+
* Only active when notify: true in .claude-smith.json
|
|
53
|
+
* @param {boolean} notify - Whether notifications are enabled
|
|
54
|
+
* @param {string} hookName - Display name of the hook
|
|
55
|
+
* @param {string} eventType - fire|warn|block|inject|track
|
|
56
|
+
* @param {string} message - Brief description
|
|
57
|
+
*/
|
|
58
|
+
export function notifyUser(notify, hookName, eventType, message) {
|
|
59
|
+
if (!notify) return;
|
|
60
|
+
const icons = {
|
|
61
|
+
fire: '✅',
|
|
62
|
+
warn: '⚠️',
|
|
63
|
+
block: '🚫',
|
|
64
|
+
inject: '🤖',
|
|
65
|
+
track: '📊'
|
|
66
|
+
};
|
|
67
|
+
const icon = icons[eventType] || '📝';
|
|
68
|
+
process.stderr.write(`🕵️ Smith — ${icon} ${hookName}: ${message}\n`);
|
|
69
|
+
}
|
package/lib/stats.mjs
CHANGED
|
@@ -7,8 +7,20 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
9
9
|
|
|
10
|
+
const KNOWN_EVENTS = ['fire', 'warn', 'block'];
|
|
11
|
+
|
|
10
12
|
export function recordEvent(sessionId, hookName, eventType) {
|
|
11
|
-
|
|
13
|
+
// Guard: only record known event types
|
|
14
|
+
if (!KNOWN_EVENTS.includes(eventType)) return;
|
|
15
|
+
|
|
16
|
+
// Sanitize sessionId and validate path
|
|
17
|
+
const safeId = (sessionId || 'default').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
18
|
+
const stateDir = join(tmpdir(), '.claude-smith', safeId);
|
|
19
|
+
|
|
20
|
+
// Verify resolved path is under expected parent
|
|
21
|
+
const expectedParent = join(tmpdir(), '.claude-smith');
|
|
22
|
+
if (!stateDir.startsWith(expectedParent)) return;
|
|
23
|
+
|
|
12
24
|
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
13
25
|
|
|
14
26
|
const counterFile = join(stateDir, `stat-${hookName}-${eventType}`);
|
|
@@ -18,16 +30,31 @@ export function recordEvent(sessionId, hookName, eventType) {
|
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
export function readStats(sessionId) {
|
|
21
|
-
|
|
33
|
+
// Sanitize sessionId and validate path
|
|
34
|
+
const safeId = (sessionId || 'default').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'default';
|
|
35
|
+
const stateDir = join(tmpdir(), '.claude-smith', safeId);
|
|
36
|
+
|
|
37
|
+
// Verify resolved path is under expected parent
|
|
38
|
+
const expectedParent = join(tmpdir(), '.claude-smith');
|
|
39
|
+
if (!stateDir.startsWith(expectedParent)) return {};
|
|
40
|
+
|
|
22
41
|
const stats = {};
|
|
23
42
|
|
|
24
43
|
try {
|
|
25
44
|
const files = readdirSync(stateDir).filter(f => f.startsWith('stat-'));
|
|
26
45
|
for (const f of files) {
|
|
27
46
|
// Format: stat-{hookName}-{eventType}
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
47
|
+
// Use safer parsing with explicit known event types
|
|
48
|
+
const raw = f.replace('stat-', ''); // e.g., "batch-checkpoint-warn"
|
|
49
|
+
const lastDash = raw.lastIndexOf('-');
|
|
50
|
+
if (lastDash === -1) continue;
|
|
51
|
+
|
|
52
|
+
const eventType = raw.slice(lastDash + 1);
|
|
53
|
+
const hookName = raw.slice(0, lastDash);
|
|
54
|
+
|
|
55
|
+
// Skip unknown event types
|
|
56
|
+
if (!KNOWN_EVENTS.includes(eventType)) continue;
|
|
57
|
+
|
|
31
58
|
if (!stats[hookName]) stats[hookName] = { fire: 0, warn: 0, block: 0 };
|
|
32
59
|
try {
|
|
33
60
|
stats[hookName][eventType] = parseInt(readFileSync(join(stateDir, f), 'utf8'), 10) || 0;
|
package/package.json
CHANGED
|
@@ -4,6 +4,6 @@ Steps:
|
|
|
4
4
|
1. Run: `npx claude-smith --version` to get the current installed version
|
|
5
5
|
2. Run: `npm view claude-smith version 2>/dev/null` to check the latest published version
|
|
6
6
|
3. Compare the two versions:
|
|
7
|
-
- If they match: Report "
|
|
7
|
+
- If they match: Report "🕵️ Smith is up to date (vX.X.X)"
|
|
8
8
|
- If latest is newer: Report the version difference and run `npx claude-smith update` to apply the update
|
|
9
9
|
- If npm check fails: Report that version check failed (possibly offline) and suggest running `npx claude-smith update` manually
|
package/templates/rules.en.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - Workflow Enforcement Rules
|
|
3
3
|
|
|
4
|
-
> Installed by
|
|
4
|
+
> Installed by 🕵️ Smith {{SMITH_VERSION}}. Do not edit manually.
|
|
5
5
|
> Update with: `claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. Workflow Protocol (auto-applied)
|
package/templates/rules.ja.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - ワークフロー実行ルール
|
|
3
3
|
|
|
4
|
-
>
|
|
4
|
+
> 🕵️ Smith {{SMITH_VERSION}} によりインストール。手動で編集しないでください。
|
|
5
5
|
> 更新コマンド:`claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. ワークフロープロトコル(自動適用)
|
package/templates/rules.ko.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - Workflow Enforcement Rules
|
|
3
3
|
|
|
4
|
-
> Installed by
|
|
4
|
+
> Installed by 🕵️ Smith {{SMITH_VERSION}}. Do not edit manually.
|
|
5
5
|
> Update with: `claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. Workflow Protocol (자동 적용)
|
package/templates/rules.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<!-- CLAUDE-SMITH:START
|
|
2
|
-
#
|
|
1
|
+
<!-- CLAUDE-SMITH:START {{SMITH_VERSION}} -->
|
|
2
|
+
# 🕵️ Smith - Workflow Enforcement Rules
|
|
3
3
|
|
|
4
|
-
> Installed by
|
|
4
|
+
> Installed by 🕵️ Smith {{SMITH_VERSION}}. Do not edit manually.
|
|
5
5
|
> Update with: `claude-smith update`
|
|
6
6
|
|
|
7
7
|
## 1. Workflow Protocol (auto-applied)
|
package/templates/rules.zh.md
CHANGED