claude-prism 0.3.2 → 0.4.1
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 +44 -19
- package/README.md +43 -17
- package/bin/cli.mjs +29 -0
- package/hooks/alignment.mjs +94 -0
- package/hooks/commit-guard.mjs +5 -3
- package/hooks/debug-loop.mjs +20 -5
- package/hooks/scope-guard.mjs +32 -8
- package/hooks/test-tracker.mjs +65 -10
- package/hooks/turn-reporter.mjs +70 -0
- package/lib/adapter.mjs +1 -0
- package/lib/config.mjs +21 -2
- package/lib/installer.mjs +119 -9
- package/lib/messages.mjs +60 -0
- package/lib/pipeline.mjs +188 -0
- package/lib/session.mjs +108 -0
- package/package.json +9 -1
- package/templates/commands/claude-prism/checkpoint.md +17 -1
- package/templates/commands/claude-prism/prism.md +19 -6
- package/templates/rules.en.md +33 -3
- package/templates/rules.ja.md +33 -3
- package/templates/rules.ko.md +33 -3
- package/templates/rules.zh.md +33 -3
- package/templates/runners/post-tool.mjs +13 -0
- package/templates/runners/pre-tool.mjs +9 -0
- package/templates/runners/user-prompt.mjs +7 -0
- package/templates/settings.json +10 -19
- package/templates/skills/prism/SKILL.md +19 -6
package/README.ko.md
CHANGED
|
@@ -32,6 +32,22 @@ AI 코딩 문제 분해 도구 — Understand, Decompose, Execute, Checkpoint (U
|
|
|
32
32
|
|
|
33
33
|
AI 코딩의 가장 비싼 실패는 나쁜 코드가 아니라 — **틀린 것을 만드는 것**이다. AI 에이전트는 이해를 건너뛰고, 분해를 건너뛰고, 30분간 자율 실행한 뒤 사용자가 원하지 않은 것을 만들어냄. Prism은 CLAUDE.md에 방법론 규칙을 주입하여 Claude의 사고방식을 바꾼다.
|
|
34
34
|
|
|
35
|
+
### v0.4.0 주요 변경
|
|
36
|
+
|
|
37
|
+
- **태스크 크기 태그** — 모든 태스크에 `[S]`, `[M]`, `[L]` 부여, 적응형 배치 구성 (S+S+M = 1배치, L = 단독)
|
|
38
|
+
- **태스크별 검증 전략** — 플랜 템플릿에 `| 검증: TDD`, `| 검증: Build`, `| 검증: Visual` 명시
|
|
39
|
+
- **사전 분해 체크리스트** — 플랜 작성 전 타입/스키마/의존성 확인 필수화
|
|
40
|
+
- **진행률 대시보드** — 각 체크포인트에 Phase/Batch/Task 퍼센트와 시각적 프로그레스 바
|
|
41
|
+
- **적응형 체크포인트** — 3회 연속 승인 시 남은 Phase 동안 배치 크기 5-8로 확대
|
|
42
|
+
- **Scope Guard 디스크 폴백** — 세션 간 `docs/plans/*.md` 존재를 디스크에서 직접 확인 (거짓 "without a plan" 경고 해결)
|
|
43
|
+
- **20개 테스트 러너 패턴** — bun, pnpm, yarn, deno, rspec, dotnet, mvn, gradle 감지 추가
|
|
44
|
+
- **9개 프레임워크별 결과 감지** — node, jest, vitest, pytest, go, cargo, mocha, rspec, dotnet 정확한 성공/실패 판별
|
|
45
|
+
- **통합 hook 파이프라인** — hook 이벤트당 단일 프로세스 (I/O 감소)
|
|
46
|
+
- **i18n 메시지 시스템** — 모든 hook 메시지 en/ko/ja/zh 지역화
|
|
47
|
+
- **세션 이벤트 로깅** — JSONL 기반 세션별 이벤트 기록
|
|
48
|
+
- **정렬 감지** — 범위 이탈 추적 및 주요 결정 플래깅
|
|
49
|
+
- **커스텀 규칙** — 설정을 통한 사용자 정의 hook 규칙
|
|
50
|
+
|
|
35
51
|
## 설치
|
|
36
52
|
|
|
37
53
|
프로젝트 루트에서:
|
|
@@ -65,19 +81,23 @@ npx claude-prism update --global # 글로벌 스킬도 업데이트
|
|
|
65
81
|
│ │ ├── help.md # /claude-prism:help
|
|
66
82
|
│ │ └── update.md # /claude-prism:update
|
|
67
83
|
│ ├── hooks/ # (선택, --no-hooks 시 생략)
|
|
68
|
-
│ │ ├──
|
|
69
|
-
│ │ ├──
|
|
70
|
-
│ │
|
|
71
|
-
│ │ └── scope-guard.mjs
|
|
84
|
+
│ │ ├── pre-tool.mjs # 통합 PreToolUse 러너
|
|
85
|
+
│ │ ├── post-tool.mjs # 통합 PostToolUse 러너
|
|
86
|
+
│ │ └── user-prompt.mjs # UserPromptSubmit 러너
|
|
72
87
|
│ ├── rules/ # hook 규칙 로직
|
|
73
88
|
│ │ ├── commit-guard.mjs
|
|
74
89
|
│ │ ├── debug-loop.mjs
|
|
75
90
|
│ │ ├── test-tracker.mjs
|
|
76
|
-
│ │
|
|
91
|
+
│ │ ├── scope-guard.mjs
|
|
92
|
+
│ │ ├── alignment.mjs
|
|
93
|
+
│ │ └── turn-reporter.mjs
|
|
77
94
|
│ ├── lib/ # hook 의존 모듈
|
|
78
95
|
│ │ ├── adapter.mjs
|
|
96
|
+
│ │ ├── pipeline.mjs
|
|
79
97
|
│ │ ├── state.mjs
|
|
98
|
+
│ │ ├── session.mjs
|
|
80
99
|
│ │ ├── config.mjs
|
|
100
|
+
│ │ ├── messages.mjs
|
|
81
101
|
│ │ └── utils.mjs
|
|
82
102
|
│ └── settings.json # Claude Code hook 등록
|
|
83
103
|
└── docs/plans/ # /claude-prism:prism 실행 시 계획 파일 생성
|
|
@@ -235,7 +255,7 @@ prism stats
|
|
|
235
255
|
|
|
236
256
|
출력:
|
|
237
257
|
```
|
|
238
|
-
Version: v0.
|
|
258
|
+
Version: v0.4.0
|
|
239
259
|
Language: ko
|
|
240
260
|
Plans: 2 file(s)
|
|
241
261
|
OMC: ✅ v4.1.1
|
|
@@ -311,16 +331,19 @@ Hook은 선택 사항인 CLI 가드로, 개발 중 규율을 강제합니다. `p
|
|
|
311
331
|
|
|
312
332
|
테스트 커맨드 실행을 감지하고 타임스탐프와 성공/실패 상태를 기록합니다. `commit-guard`가 최근 테스트 실행을 확인하는 데 사용합니다.
|
|
313
333
|
|
|
314
|
-
**감지하는
|
|
315
|
-
- `npm test`, `
|
|
316
|
-
- `jest`, `vitest`
|
|
317
|
-
- `node --test`
|
|
318
|
-
- `npx
|
|
319
|
-
- `pytest`
|
|
320
|
-
- `
|
|
321
|
-
- `go test`
|
|
334
|
+
**감지하는 테스트 (20개 패턴):**
|
|
335
|
+
- `npm test`, `pnpm test`, `yarn test`, `bun test`
|
|
336
|
+
- `jest`, `vitest`, `mocha`, `rspec`
|
|
337
|
+
- `node --test`, `deno test`
|
|
338
|
+
- `npx jest`, `npx vitest`, `npx mocha`, `bunx vitest`
|
|
339
|
+
- `pytest`, `cargo test`, `go test`
|
|
340
|
+
- `dotnet test`, `mvn test`, `gradle test`
|
|
322
341
|
- `make test`
|
|
323
342
|
|
|
343
|
+
**프레임워크별 결과 감지 (9개):**
|
|
344
|
+
- Node test runner, Jest, Vitest, Pytest, Go, Cargo, Mocha, RSpec, dotnet
|
|
345
|
+
- stdout와 stderr를 모두 분석하여 정확한 성공/실패 판별
|
|
346
|
+
|
|
324
347
|
**설정:**
|
|
325
348
|
```json
|
|
326
349
|
{
|
|
@@ -366,6 +389,7 @@ Hook은 선택 사항인 CLI 가드로, 개발 중 규율을 강제합니다. `p
|
|
|
366
389
|
- **플랜 인식**: 플랜 파일 생성 시 (`docs/plans/*.md`) 임계값이 자동으로 2배
|
|
367
390
|
- 표준 + 플랜: 8개에서 경고, 14개에서 차단
|
|
368
391
|
- Agent + 플랜: 16개에서 경고, 24개에서 차단
|
|
392
|
+
- **세션 간 지속성**: 현재 세션에서 생성된 플랜뿐만 아니라 디스크에 존재하는 기존 플랜 파일(`docs/plans/*.md`)도 감지. 새 세션에서 작업 재개 시 거짓 "without a plan" 경고 해결.
|
|
369
393
|
|
|
370
394
|
## 설정
|
|
371
395
|
|
|
@@ -411,7 +435,7 @@ prism doctor # 진단 정보에 OMC 감지 표시
|
|
|
411
435
|
## 기술 사양
|
|
412
436
|
|
|
413
437
|
- **패키지명**: `claude-prism`
|
|
414
|
-
- **버전**: 0.
|
|
438
|
+
- **버전**: 0.4.0
|
|
415
439
|
- **CLI 커맨드**: `prism`
|
|
416
440
|
- **Node 버전**: >= 18
|
|
417
441
|
- **의존성**: 0 (순수 ESM 모듈)
|
|
@@ -430,10 +454,11 @@ prism doctor # 진단 정보에 OMC 감지 표시
|
|
|
430
454
|
|
|
431
455
|
1. **정보 충분성 판별** — 요청이 명확한가? 질문이 필요한가?
|
|
432
456
|
2. **최대 3라운드 질문** — 한 번에 하나씩, 객관식 우선
|
|
433
|
-
3. **분해 5
|
|
434
|
-
4.
|
|
435
|
-
5.
|
|
436
|
-
6.
|
|
457
|
+
3. **분해 5원칙 + 사전 체크** — 단위 크기, 테스트 선행, 독립 검증, 파일 명시, 의존성 명시 + 타입/스키마/교차패키지 확인
|
|
458
|
+
4. **크기 태그 + 적응형 배치** — [S/M/L] 태그 기반 배치 구성, 3회 연속 승인 시 배치 확대
|
|
459
|
+
5. **태스크별 검증 전략** — TDD / Build / Visual 명시, 파일 경로별 자동 선택
|
|
460
|
+
6. **진행률 대시보드** — Phase/Batch/Task % 시각화, 체크포인트마다 보고
|
|
461
|
+
7. **자기 교정** — 같은 파일 3회 편집 시 멈추고 재검토
|
|
437
462
|
|
|
438
463
|
## 라이선스
|
|
439
464
|
|
package/README.md
CHANGED
|
@@ -21,6 +21,22 @@ An AI coding problem decomposition tool for Claude Code. Installs the **UDEC** m
|
|
|
21
21
|
|
|
22
22
|
**Core philosophy:** Never implement what you haven't understood. Never execute what you haven't decomposed.
|
|
23
23
|
|
|
24
|
+
### What's New in v0.4.0
|
|
25
|
+
|
|
26
|
+
- **Task size tags** — Every task gets `[S]`, `[M]`, or `[L]` with adaptive batch composition (S+S+M = 1 batch, L = solo)
|
|
27
|
+
- **Verification strategy per task** — `| Verify: TDD`, `| Verify: Build`, or `| Verify: Visual` in plan templates
|
|
28
|
+
- **Pre-decomposition checklist** — Mandatory type/schema/dependency check before creating plans
|
|
29
|
+
- **Progress dashboard** — Visual progress bar with phase/batch/task percentages at each checkpoint
|
|
30
|
+
- **Adaptive checkpoints** — After 3 consecutive approvals, batch size expands to 5-8 for the rest of the phase
|
|
31
|
+
- **Scope guard disk fallback** — Detects existing `docs/plans/*.md` on disk, not just in-session writes (fixes false "without a plan" warnings across sessions)
|
|
32
|
+
- **20 test runner patterns** — Added bun, pnpm, yarn, deno, rspec, dotnet, mvn, gradle detection
|
|
33
|
+
- **9 framework-specific result detectors** — Accurate pass/fail for node, jest, vitest, pytest, go, cargo, mocha, rspec, dotnet
|
|
34
|
+
- **Unified hook pipeline** — Single process per hook event instead of separate processes per rule (reduced I/O)
|
|
35
|
+
- **i18n message system** — All hook messages localized in en/ko/ja/zh
|
|
36
|
+
- **Session event logging** — JSONL-based per-session event recording
|
|
37
|
+
- **Alignment detection** — Scope drift tracking and major decision flagging
|
|
38
|
+
- **Custom rules** — User-defined hook rules via config
|
|
39
|
+
|
|
24
40
|
## The Problem
|
|
25
41
|
|
|
26
42
|
Without structure, Claude does this:
|
|
@@ -66,11 +82,13 @@ After running `prism init`, your project gains:
|
|
|
66
82
|
- `/claude-prism:update` — Update rules and commands to latest version
|
|
67
83
|
- `/claude-prism:help` — Command reference
|
|
68
84
|
|
|
69
|
-
**Hooks** (optional, unless `--no-hooks` is set) —
|
|
85
|
+
**Hooks** (optional, unless `--no-hooks` is set) — Six CLI guards that enforce discipline:
|
|
70
86
|
- `commit-guard` — Prevents commits when tests haven't run recently
|
|
71
87
|
- `debug-loop` — Detects divergent editing patterns on the same file (catches infinite debugging loops)
|
|
72
|
-
- `test-tracker` — Detects test
|
|
73
|
-
- `scope-guard` — Warns at 4 unique files modified, blocks at 7 (agent-aware
|
|
88
|
+
- `test-tracker` — Detects test execution (20 patterns, 9 framework-specific result detectors) and records pass/fail
|
|
89
|
+
- `scope-guard` — Warns at 4 unique files modified, blocks at 7 (agent-aware, plan-aware with disk fallback)
|
|
90
|
+
- `alignment` — Detects scope drift (new directories outside base scope) and major decisions (package installs, db migrations, destructive deletes)
|
|
91
|
+
- `turn-reporter` — Tracks turn count, warns at 5 autonomous turns without user input, provides previous turn summary
|
|
74
92
|
|
|
75
93
|
**Configuration** — `.prism.json` stores language preference and hook settings. Includes OMC (oh-my-claudecode) detection.
|
|
76
94
|
|
|
@@ -91,19 +109,23 @@ your-project/
|
|
|
91
109
|
│ │ ├── update.md # /claude-prism:update
|
|
92
110
|
│ │ └── help.md # /claude-prism:help
|
|
93
111
|
│ ├── hooks/ # (optional, if --no-hooks not set)
|
|
94
|
-
│ │ ├──
|
|
95
|
-
│ │ ├──
|
|
96
|
-
│ │
|
|
97
|
-
│ │ └── scope-guard.mjs
|
|
112
|
+
│ │ ├── pre-tool.mjs # Unified PreToolUse runner
|
|
113
|
+
│ │ ├── post-tool.mjs # Unified PostToolUse runner
|
|
114
|
+
│ │ └── user-prompt.mjs # UserPromptSubmit runner
|
|
98
115
|
│ ├── rules/ # Hook logic modules
|
|
99
116
|
│ │ ├── commit-guard.mjs
|
|
100
117
|
│ │ ├── debug-loop.mjs
|
|
101
118
|
│ │ ├── test-tracker.mjs
|
|
102
|
-
│ │
|
|
119
|
+
│ │ ├── scope-guard.mjs
|
|
120
|
+
│ │ ├── alignment.mjs
|
|
121
|
+
│ │ └── turn-reporter.mjs
|
|
103
122
|
│ ├── lib/ # Hook dependencies
|
|
104
123
|
│ │ ├── adapter.mjs
|
|
124
|
+
│ │ ├── pipeline.mjs
|
|
105
125
|
│ │ ├── state.mjs
|
|
126
|
+
│ │ ├── session.mjs
|
|
106
127
|
│ │ ├── config.mjs
|
|
128
|
+
│ │ ├── messages.mjs
|
|
107
129
|
│ │ └── utils.mjs
|
|
108
130
|
│ └── settings.json # Claude Code hook registration
|
|
109
131
|
└── docs/plans/
|
|
@@ -268,16 +290,19 @@ Detects editing patterns on the same file. Distinguishes between **divergent** e
|
|
|
268
290
|
|
|
269
291
|
Detects test command execution and records the timestamp and pass/fail state. Used by `commit-guard` to verify tests ran recently.
|
|
270
292
|
|
|
271
|
-
**Detects:**
|
|
272
|
-
- `npm test`, `
|
|
273
|
-
- `jest`, `vitest`
|
|
274
|
-
- `node --test`
|
|
275
|
-
- `npx
|
|
276
|
-
- `pytest`
|
|
277
|
-
- `
|
|
278
|
-
- `go test`
|
|
293
|
+
**Detects (20 patterns):**
|
|
294
|
+
- `npm test`, `pnpm test`, `yarn test`, `bun test`
|
|
295
|
+
- `jest`, `vitest`, `mocha`, `rspec`
|
|
296
|
+
- `node --test`, `deno test`
|
|
297
|
+
- `npx jest`, `npx vitest`, `npx mocha`, `bunx vitest`
|
|
298
|
+
- `pytest`, `cargo test`, `go test`
|
|
299
|
+
- `dotnet test`, `mvn test`, `gradle test`
|
|
279
300
|
- `make test`
|
|
280
301
|
|
|
302
|
+
**Framework-specific result detection (9 detectors):**
|
|
303
|
+
- Node test runner, Jest, Vitest, Pytest, Go, Cargo, Mocha, RSpec, dotnet
|
|
304
|
+
- Analyzes both stdout and stderr for accurate pass/fail determination
|
|
305
|
+
|
|
281
306
|
**Configuration:**
|
|
282
307
|
```json
|
|
283
308
|
{
|
|
@@ -323,6 +348,7 @@ Tracks unique source files modified per session. Warns when scope grows without
|
|
|
323
348
|
- **Plan-aware**: When a plan file is created (`docs/plans/*.md`), thresholds are automatically doubled
|
|
324
349
|
- Standard with plan: warns at 8, blocks at 14
|
|
325
350
|
- Agent with plan: warns at 16, blocks at 24
|
|
351
|
+
- **Cross-session persistence**: Detects existing plan files on disk (`docs/plans/*.md`), not just plans created in the current session. Fixes false "without a plan" warnings when resuming work in a new session.
|
|
326
352
|
|
|
327
353
|
## Configuration
|
|
328
354
|
|
|
@@ -412,7 +438,7 @@ prism stats
|
|
|
412
438
|
|
|
413
439
|
Output:
|
|
414
440
|
```
|
|
415
|
-
Version: v0.
|
|
441
|
+
Version: v0.4.0
|
|
416
442
|
Language: en
|
|
417
443
|
Plans: 2 file(s)
|
|
418
444
|
OMC: ✅ v4.1.1
|
package/bin/cli.mjs
CHANGED
|
@@ -35,6 +35,7 @@ if (hasFlag('version') || hasFlag('v')) {
|
|
|
35
35
|
|
|
36
36
|
const cwd = process.cwd();
|
|
37
37
|
|
|
38
|
+
try {
|
|
38
39
|
switch (command) {
|
|
39
40
|
case 'init': {
|
|
40
41
|
if (hasFlag('global')) {
|
|
@@ -49,6 +50,19 @@ switch (command) {
|
|
|
49
50
|
const language = getFlag('lang') || 'en';
|
|
50
51
|
const hooks = !hasFlag('no-hooks');
|
|
51
52
|
|
|
53
|
+
if (hasFlag('dry-run')) {
|
|
54
|
+
const { dryRun } = await import('../lib/installer.mjs');
|
|
55
|
+
const result = dryRun(cwd, { language, hooks });
|
|
56
|
+
console.log('🌈 claude-prism init --dry-run\n');
|
|
57
|
+
console.log(' Files that would be created/updated:\n');
|
|
58
|
+
for (const action of result.actions) {
|
|
59
|
+
const icon = action.status === 'create' ? '🆕' : '🔄';
|
|
60
|
+
console.log(` ${icon} [${action.status}] ${action.path}`);
|
|
61
|
+
}
|
|
62
|
+
console.log(`\n Total: ${result.actions.length} files`);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
console.log('🌈 claude-prism init\n');
|
|
53
67
|
await init(cwd, { language, hooks });
|
|
54
68
|
|
|
@@ -182,8 +196,23 @@ Usage:
|
|
|
182
196
|
Options:
|
|
183
197
|
--lang=XX Language: en (default), ko, ja, zh
|
|
184
198
|
--no-hooks Skip enforcement hooks
|
|
199
|
+
--dry-run Show what init would do without making changes
|
|
185
200
|
--global Install/uninstall globally (all projects)
|
|
186
201
|
--ci Output JSON for CI integration
|
|
187
202
|
--version Show version`);
|
|
188
203
|
}
|
|
189
204
|
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const msg = err.message || String(err);
|
|
207
|
+
process.stderr.write(`🌈 Prism Error: ${msg}\n`);
|
|
208
|
+
|
|
209
|
+
if (/EACCES|permission/i.test(msg)) {
|
|
210
|
+
process.stderr.write('💡 Check directory permissions\n');
|
|
211
|
+
} else if (/JSON|parse/i.test(msg)) {
|
|
212
|
+
process.stderr.write('💡 Config file may be corrupted. Try `prism reset` or delete .claude-prism.json\n');
|
|
213
|
+
} else if (/ENOENT.*package\.json/i.test(msg)) {
|
|
214
|
+
process.stderr.write('💡 Not in a project directory?\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-prism — Alignment Detection
|
|
3
|
+
* Detects scope drift and major unconfirmed decisions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readJsonState, writeJsonState } from '../lib/state.mjs';
|
|
7
|
+
import { getMessage } from '../lib/messages.mjs';
|
|
8
|
+
|
|
9
|
+
// Major decision patterns — commands that represent significant choices
|
|
10
|
+
const MAJOR_DECISION_PATTERNS = [
|
|
11
|
+
{ pattern: /\bnpm\s+install\b|\bpnpm\s+add\b|\byarn\s+add\b|\bbun\s+add\b/, label: 'package-install' },
|
|
12
|
+
{ pattern: /\bprisma\s+migrate\b|\bsequelize\b|\bknex\s+migrate\b/, label: 'db-migration' },
|
|
13
|
+
{ pattern: /\brm\s+-rf?\b|\brmdir\b/, label: 'destructive-delete' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Config files that represent major changes when modified
|
|
17
|
+
const MAJOR_CONFIG_FILES = [
|
|
18
|
+
'tsconfig.json', 'package.json', '.env', 'docker-compose.yml',
|
|
19
|
+
'Dockerfile', '.github/workflows', 'webpack.config', 'vite.config',
|
|
20
|
+
'next.config', 'tailwind.config',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const alignment = {
|
|
24
|
+
name: 'alignment',
|
|
25
|
+
|
|
26
|
+
evaluate(ctx, config, stateDir) {
|
|
27
|
+
if (!config.enabled) return { type: 'pass' };
|
|
28
|
+
|
|
29
|
+
const messages = [];
|
|
30
|
+
|
|
31
|
+
// 1. Directory scope tracking
|
|
32
|
+
if (ctx.filePath) {
|
|
33
|
+
const dir = ctx.filePath.split('/').slice(0, -1).join('/') || '.';
|
|
34
|
+
let scopeDirs = readJsonState(stateDir, 'scope-directories') || [];
|
|
35
|
+
|
|
36
|
+
if (!scopeDirs.includes(dir)) {
|
|
37
|
+
// First 3 unique directories establish the "base scope"
|
|
38
|
+
if (scopeDirs.length < 3) {
|
|
39
|
+
scopeDirs.push(dir);
|
|
40
|
+
writeJsonState(stateDir, 'scope-directories', scopeDirs);
|
|
41
|
+
} else {
|
|
42
|
+
// New directory outside base scope — potential drift
|
|
43
|
+
const driftCount = parseInt(
|
|
44
|
+
(readJsonState(stateDir, 'drift-count') || 0).toString(), 10
|
|
45
|
+
) || 0;
|
|
46
|
+
const newDriftCount = driftCount + 1;
|
|
47
|
+
writeJsonState(stateDir, 'drift-count', newDriftCount);
|
|
48
|
+
|
|
49
|
+
if (newDriftCount >= (config.driftThreshold || 2)) {
|
|
50
|
+
return {
|
|
51
|
+
type: 'warn',
|
|
52
|
+
message: `🌈 Prism 🧭 Scope drift: editing ${dir} (outside base scope: ${scopeDirs.slice(0, 3).join(', ')}). Verify this is intended.`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Major config file detection
|
|
59
|
+
const fileName = ctx.filePath.split('/').pop();
|
|
60
|
+
const isMajorConfig = MAJOR_CONFIG_FILES.some(f =>
|
|
61
|
+
ctx.filePath.includes(f) || fileName === f
|
|
62
|
+
);
|
|
63
|
+
if (isMajorConfig) {
|
|
64
|
+
messages.push(`🌈 Prism 🔧 Config change: ${fileName}. Ensure this was discussed with user.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. Major command detection
|
|
69
|
+
if (ctx.command) {
|
|
70
|
+
for (const { pattern, label } of MAJOR_DECISION_PATTERNS) {
|
|
71
|
+
if (pattern.test(ctx.command)) {
|
|
72
|
+
if (label === 'destructive-delete') {
|
|
73
|
+
return {
|
|
74
|
+
type: 'warn',
|
|
75
|
+
message: `🌈 Prism ⚠️ Destructive command detected: ${ctx.command.slice(0, 60)}. Confirm with user before proceeding.`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (label === 'package-install') {
|
|
79
|
+
messages.push(`🌈 Prism 📦 New dependency being installed. Verify this was agreed upon.`);
|
|
80
|
+
}
|
|
81
|
+
if (label === 'db-migration') {
|
|
82
|
+
messages.push(`🌈 Prism 🗄️ Database migration detected. This is a major decision — confirm with user.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (messages.length > 0) {
|
|
89
|
+
return { type: 'pass', message: messages.join('\n') };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { type: 'pass' };
|
|
93
|
+
}
|
|
94
|
+
};
|
package/hooks/commit-guard.mjs
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readState } from '../lib/state.mjs';
|
|
7
|
+
import { getMessage } from '../lib/messages.mjs';
|
|
7
8
|
|
|
8
9
|
export const commitGuard = {
|
|
9
10
|
name: 'commit-guard',
|
|
10
11
|
|
|
11
12
|
evaluate(ctx, config, stateDir) {
|
|
12
13
|
const command = ctx.command || '';
|
|
14
|
+
const lang = config.language || 'en';
|
|
13
15
|
|
|
14
16
|
if (!command.includes('git commit')) return { type: 'pass' };
|
|
15
17
|
if (command.includes('--allow-empty')) return { type: 'pass' };
|
|
@@ -19,7 +21,7 @@ export const commitGuard = {
|
|
|
19
21
|
if (testResult !== null && testResult.trim() === 'fail') {
|
|
20
22
|
return {
|
|
21
23
|
type: 'block',
|
|
22
|
-
message: '
|
|
24
|
+
message: getMessage(lang, 'commit-guard.block.failed')
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -28,7 +30,7 @@ export const commitGuard = {
|
|
|
28
30
|
if (lastTestRaw === null) {
|
|
29
31
|
return {
|
|
30
32
|
type: 'warn',
|
|
31
|
-
message: '
|
|
33
|
+
message: getMessage(lang, 'commit-guard.warn.no-test')
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -39,7 +41,7 @@ export const commitGuard = {
|
|
|
39
41
|
if (diff > (config.maxTestAge || 300)) {
|
|
40
42
|
return {
|
|
41
43
|
type: 'warn',
|
|
42
|
-
message:
|
|
44
|
+
message: getMessage(lang, 'commit-guard.warn.stale', { minutes: Math.floor(diff / 60) })
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
|
package/hooks/debug-loop.mjs
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { readState, writeState, readJsonState, writeJsonState } from '../lib/state.mjs';
|
|
8
|
+
import { DEFAULTS, buildSourcePattern } from '../lib/config.mjs';
|
|
9
|
+
import { getMessage } from '../lib/messages.mjs';
|
|
8
10
|
|
|
9
11
|
export const debugLoop = {
|
|
10
12
|
name: 'debug-loop',
|
|
@@ -14,7 +16,8 @@ export const debugLoop = {
|
|
|
14
16
|
if (!filePath) return { type: 'pass' };
|
|
15
17
|
|
|
16
18
|
// Skip non-source files
|
|
17
|
-
|
|
19
|
+
const sourcePattern = buildSourcePattern(config.sourceExtensions || DEFAULTS.sourceExtensions);
|
|
20
|
+
if (!sourcePattern.test(filePath)) return { type: 'pass' };
|
|
18
21
|
|
|
19
22
|
const hash = createHash('md5').update(filePath).digest('hex').slice(0, 8);
|
|
20
23
|
const countKey = `edit-count-${hash}`;
|
|
@@ -34,6 +37,7 @@ export const debugLoop = {
|
|
|
34
37
|
writeJsonState(stateDir, logKey, editLog);
|
|
35
38
|
|
|
36
39
|
const name = filePath.split('/').pop();
|
|
40
|
+
const lang = config.language || 'en';
|
|
37
41
|
const pattern = count >= config.warnAt ? analyzePattern(editLog) : null;
|
|
38
42
|
|
|
39
43
|
if (count >= config.blockAt) {
|
|
@@ -41,12 +45,12 @@ export const debugLoop = {
|
|
|
41
45
|
if (pattern === 'divergent') {
|
|
42
46
|
return {
|
|
43
47
|
type: 'block',
|
|
44
|
-
message:
|
|
48
|
+
message: getMessage(lang, 'debug-loop.block.divergent', { name, count })
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
return {
|
|
48
52
|
type: 'warn',
|
|
49
|
-
message:
|
|
53
|
+
message: getMessage(lang, 'debug-loop.warn.convergent', { name, count })
|
|
50
54
|
};
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -55,7 +59,7 @@ export const debugLoop = {
|
|
|
55
59
|
if (pattern === 'divergent') {
|
|
56
60
|
return {
|
|
57
61
|
type: 'warn',
|
|
58
|
-
message:
|
|
62
|
+
message: getMessage(lang, 'debug-loop.warn.divergent', { name, count })
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
// Convergent edits (different areas) = normal progressive work → pass
|
|
@@ -68,8 +72,19 @@ export const debugLoop = {
|
|
|
68
72
|
function analyzePattern(log) {
|
|
69
73
|
if (log.length < 3) return null;
|
|
70
74
|
const recent = log.slice(-3).map(e => e.snippet);
|
|
75
|
+
|
|
76
|
+
// Filter out empty/whitespace snippets — can't determine pattern
|
|
77
|
+
if (recent.some(s => !s || !s.trim())) return null;
|
|
78
|
+
|
|
79
|
+
// Exact duplicates = clearly divergent (same code edited repeatedly)
|
|
71
80
|
const uniqueSnippets = new Set(recent).size;
|
|
72
81
|
if (uniqueSnippets === 1) return 'divergent';
|
|
73
|
-
|
|
82
|
+
|
|
83
|
+
// Check for meaningful overlap using longer window
|
|
84
|
+
const baseSnippet = recent[0].slice(0, 40);
|
|
85
|
+
// Skip overlap check if base is too short for meaningful comparison
|
|
86
|
+
if (baseSnippet.length < 10) return uniqueSnippets <= 2 ? 'divergent' : 'convergent';
|
|
87
|
+
|
|
88
|
+
const overlap = recent.filter(s => s.includes(baseSnippet)).length;
|
|
74
89
|
return overlap >= 2 ? 'divergent' : 'convergent';
|
|
75
90
|
}
|
package/hooks/scope-guard.mjs
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readJsonState, writeJsonState } from '../lib/state.mjs';
|
|
7
|
+
import { DEFAULTS, buildSourcePattern, buildTestPattern } from '../lib/config.mjs';
|
|
8
|
+
import { getMessage } from '../lib/messages.mjs';
|
|
9
|
+
import { existsSync, readdirSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
7
11
|
|
|
8
|
-
const SOURCE_PATTERN = /\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|svelte|vue)$/;
|
|
9
|
-
const TEST_PATTERN = /\.(test|spec|_test)\./;
|
|
10
12
|
const PLAN_PATTERN = /(?:^|\/)docs\/plans\/.*\.md$|(?:^|\/).*plan.*\.md$/i;
|
|
11
13
|
|
|
12
14
|
export const scopeGuard = {
|
|
@@ -17,16 +19,20 @@ export const scopeGuard = {
|
|
|
17
19
|
if (!filePath) return { type: 'pass' };
|
|
18
20
|
|
|
19
21
|
// Plan file created → mark plan as active (thresholds will be doubled)
|
|
22
|
+
const lang = config.language || 'en';
|
|
20
23
|
if (PLAN_PATTERN.test(filePath)) {
|
|
21
24
|
writeJsonState(stateDir, 'scope-has-plan', true);
|
|
22
|
-
return { type: 'pass', message: '
|
|
25
|
+
return { type: 'pass', message: getMessage(lang, 'scope-guard.plan-detected') };
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
// Only track source files
|
|
26
|
-
|
|
29
|
+
const sourcePattern = buildSourcePattern(config.sourceExtensions || DEFAULTS.sourceExtensions);
|
|
30
|
+
const testPattern = buildTestPattern(config.testPatterns || DEFAULTS.testPatterns);
|
|
31
|
+
|
|
32
|
+
if (!sourcePattern.test(filePath)) return { type: 'pass' };
|
|
27
33
|
|
|
28
34
|
// Don't count test files toward scope
|
|
29
|
-
if (
|
|
35
|
+
if (testPattern.test(filePath)) return { type: 'pass' };
|
|
30
36
|
|
|
31
37
|
// Track unique files
|
|
32
38
|
let files = readJsonState(stateDir, 'scope-files') || [];
|
|
@@ -43,7 +49,25 @@ export const scopeGuard = {
|
|
|
43
49
|
let blockAt = isAgent ? (config.agentBlockAt || 12) : (config.blockAt || 7);
|
|
44
50
|
|
|
45
51
|
// Active plan → double thresholds (planned work is expected to touch many files)
|
|
46
|
-
|
|
52
|
+
let hasPlan = readJsonState(stateDir, 'scope-has-plan');
|
|
53
|
+
|
|
54
|
+
// Fallback: check disk for existing plan files (survives session restart)
|
|
55
|
+
if (!hasPlan) {
|
|
56
|
+
try {
|
|
57
|
+
const root = config.projectRoot || process.cwd();
|
|
58
|
+
const plansDir = join(root, 'docs', 'plans');
|
|
59
|
+
if (existsSync(plansDir)) {
|
|
60
|
+
const planFiles = readdirSync(plansDir).filter(f => f.endsWith('.md'));
|
|
61
|
+
if (planFiles.length > 0) {
|
|
62
|
+
hasPlan = true;
|
|
63
|
+
writeJsonState(stateDir, 'scope-has-plan', true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Ignore filesystem errors — fail open
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
if (hasPlan) {
|
|
48
72
|
warnAt *= 2;
|
|
49
73
|
blockAt *= 2;
|
|
@@ -52,14 +76,14 @@ export const scopeGuard = {
|
|
|
52
76
|
if (count >= blockAt) {
|
|
53
77
|
return {
|
|
54
78
|
type: 'block',
|
|
55
|
-
message:
|
|
79
|
+
message: getMessage(lang, 'scope-guard.block', { count })
|
|
56
80
|
};
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
if (count >= warnAt) {
|
|
60
84
|
return {
|
|
61
85
|
type: 'warn',
|
|
62
|
-
message:
|
|
86
|
+
message: getMessage(lang, 'scope-guard.warn', { count })
|
|
63
87
|
};
|
|
64
88
|
}
|
|
65
89
|
|