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 +46 -3
- package/README.md +46 -3
- package/bin/cli.mjs +18 -17
- 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 +21 -23
- 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-decompose.md +63 -0
- 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 — 진행 자체를 막음)
|
|
@@ -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
|
-
-
|
|
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
|
-
#
|
|
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)
|
|
@@ -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
|
-
- **
|
|
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
|
|
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');
|
|
@@ -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:
|
|
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:
|
|
412
|
-
: `❌ Commands: ${
|
|
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
|
|
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
|
}
|