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.
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { writeState } from '../lib/state.mjs';
7
+ import { getMessage } from '../lib/messages.mjs';
7
8
 
8
9
  const TEST_PATTERNS = [
9
10
  /\bnpm\s+test\b/,
@@ -16,6 +17,64 @@ const TEST_PATTERNS = [
16
17
  /\bgo\s+test\b/,
17
18
  /\bmake\s+test\b/,
18
19
  /\bnpx\s+(jest|vitest|mocha)\b/,
20
+ /\bbun\s+test\b/,
21
+ /\bpnpm\s+test\b/,
22
+ /\byarn\s+test\b/,
23
+ /\bdeno\s+test\b/,
24
+ /\bpnpm\s+exec\s+jest\b/,
25
+ /\bbunx\s+vitest\b/,
26
+ /\brspec\b/,
27
+ /\bdotnet\s+test\b/,
28
+ /\bmvn\s+test\b/,
29
+ /\bgradle\s+test\b/,
30
+ ];
31
+
32
+ const RESULT_DETECTORS = [
33
+ {
34
+ name: 'node-test-runner',
35
+ detect: (output) => /# (?:pass|fail) \d+/.test(output),
36
+ isFail: (output) => /# fail [1-9]/.test(output),
37
+ },
38
+ {
39
+ name: 'jest',
40
+ detect: (output) => /Tests:.*\d+/.test(output),
41
+ isFail: (output) => /\d+ failed/.test(output),
42
+ },
43
+ {
44
+ name: 'vitest',
45
+ detect: (output) => /Tests\s+\d+ (?:passed|failed)/.test(output),
46
+ isFail: (output) => /Tests\s+\d+ failed/.test(output),
47
+ },
48
+ {
49
+ name: 'pytest',
50
+ detect: (output) => /={3,}.*={3,}/.test(output) || /\d+ passed/.test(output),
51
+ isFail: (output) => /\d+ failed/.test(output) || /\d+ error/.test(output),
52
+ },
53
+ {
54
+ name: 'go',
55
+ detect: (output) => /^(?:PASS|FAIL)$|--- (?:PASS|FAIL)/m.test(output),
56
+ isFail: (output) => /^FAIL$/m.test(output) || /--- FAIL:/m.test(output),
57
+ },
58
+ {
59
+ name: 'cargo',
60
+ detect: (output) => /test result:/.test(output),
61
+ isFail: (output) => /test result: FAILED/.test(output),
62
+ },
63
+ {
64
+ name: 'mocha',
65
+ detect: (output) => /\d+ passing/.test(output),
66
+ isFail: (output) => /\d+ failing/.test(output),
67
+ },
68
+ {
69
+ name: 'rspec',
70
+ detect: (output) => /\d+ examples?/.test(output),
71
+ isFail: (output) => /[1-9] failures?/.test(output),
72
+ },
73
+ {
74
+ name: 'dotnet',
75
+ detect: (output) => /Test Run (?:Successful|Failed)/.test(output),
76
+ isFail: (output) => /Test Run Failed/.test(output),
77
+ },
19
78
  ];
20
79
 
21
80
  export const testTracker = {
@@ -27,32 +86,28 @@ export const testTracker = {
27
86
  const isTestCommand = TEST_PATTERNS.some(p => p.test(command));
28
87
  if (!isTestCommand) return { type: 'pass' };
29
88
 
30
- // Record timestamp
31
89
  const now = Math.floor(Date.now() / 1000);
32
90
  writeState(stateDir, 'last-test-run', String(now));
33
91
 
34
- // Record result — Claude Code does not provide exitCode,
35
- // so we infer pass/fail from stdout and interrupted flag
36
92
  let passed;
37
93
  if (ctx.interrupted) {
38
94
  passed = false;
39
95
  } else {
40
- const stdout = ctx.stdout || '';
41
- const failMatch = stdout.match(/# fail (\d+)/);
42
- if (failMatch) {
43
- passed = parseInt(failMatch[1], 10) === 0;
44
- } else if (/(?:FAIL|FAILED|ERROR)\b/i.test(stdout) && !/# pass \d+/.test(stdout)) {
45
- passed = false;
96
+ const output = (ctx.stdout || '') + '\n' + (ctx.stderr || '');
97
+ const matched = RESULT_DETECTORS.find(d => d.detect(output));
98
+ if (matched) {
99
+ passed = !matched.isFail(output);
46
100
  } else {
47
101
  passed = true;
48
102
  }
49
103
  }
104
+
50
105
  writeState(stateDir, 'last-test-result', passed ? 'pass' : 'fail');
51
106
 
52
107
  if (!passed) {
53
108
  return {
54
109
  type: 'warn',
55
- message: '🌈 Prism 📊 Tests FAILED. Fix before committing.'
110
+ message: getMessage(config.language || 'en', 'test-tracker.warn.failed')
56
111
  };
57
112
  }
58
113
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * claude-prism — Turn Reporter
3
+ * UserPromptSubmit hook: tracks turns, injects previous turn summary
4
+ */
5
+
6
+ import { readState, writeState, readJsonState, writeJsonState } from '../lib/state.mjs';
7
+ import { getMessage } from '../lib/messages.mjs';
8
+
9
+ export const turnReporter = {
10
+ name: 'turn-reporter',
11
+
12
+ evaluate(ctx, config, stateDir) {
13
+ // Increment turn counter
14
+ const prevTurn = parseInt(readState(stateDir, 'turn-count') || '0', 10) || 0;
15
+ const turnNumber = prevTurn + 1;
16
+ writeState(stateDir, 'turn-count', String(turnNumber));
17
+
18
+ // Read previous turn actions (recorded by post-tool pipeline)
19
+ const prevActions = readJsonState(stateDir, 'turn-actions') || [];
20
+ // Reset actions for new turn
21
+ writeJsonState(stateDir, 'turn-actions', []);
22
+
23
+ // Check autonomous run length
24
+ const autoTurns = parseInt(readState(stateDir, 'auto-turns') || '0', 10) || 0;
25
+ if (ctx.userPrompt) {
26
+ // User input detected — reset auto counter
27
+ writeState(stateDir, 'auto-turns', '0');
28
+ } else {
29
+ // Autonomous turn
30
+ const newAutoTurns = autoTurns + 1;
31
+ writeState(stateDir, 'auto-turns', String(newAutoTurns));
32
+
33
+ if (newAutoTurns >= (config.silentTurnsWarning || 5)) {
34
+ const scopeFiles = readJsonState(stateDir, 'scope-files') || [];
35
+ return {
36
+ type: 'warn',
37
+ message: `🌈 Prism ⏰ ${newAutoTurns} turns without user input. Files changed: ${scopeFiles.length}. Report progress before continuing.`
38
+ };
39
+ }
40
+ }
41
+
42
+ // Build previous turn summary if there were actions
43
+ if (prevActions.length === 0) return { type: 'pass' };
44
+
45
+ const fileActions = prevActions.filter(a => a.type === 'file-edit' || a.type === 'file-create');
46
+ const testActions = prevActions.filter(a => a.type === 'test-run');
47
+ const blockActions = prevActions.filter(a => a.type === 'block');
48
+
49
+ const parts = [`🌈 Prism Turn #${turnNumber - 1}:`];
50
+ if (fileActions.length > 0) {
51
+ const names = [...new Set(fileActions.map(a => a.file))].slice(0, 5);
52
+ parts.push(`Files: ${names.join(', ')}${fileActions.length > 5 ? ` +${fileActions.length - 5} more` : ''}`);
53
+ }
54
+ if (testActions.length > 0) {
55
+ const passed = testActions.filter(a => a.passed).length;
56
+ const failed = testActions.length - passed;
57
+ parts.push(`Tests: ${passed} passed${failed > 0 ? `, ${failed} failed` : ''}`);
58
+ }
59
+ if (blockActions.length > 0) {
60
+ parts.push(`Blocks: ${blockActions.length}`);
61
+ }
62
+
63
+ if (parts.length <= 1) return { type: 'pass' };
64
+
65
+ return {
66
+ type: 'pass',
67
+ message: parts.join(' | ')
68
+ };
69
+ }
70
+ };
package/lib/adapter.mjs CHANGED
@@ -19,6 +19,7 @@ const TOOL_ACTION_MAP = {
19
19
  const EVENT_PHASE_MAP = {
20
20
  'PreToolUse': 'pre',
21
21
  'PostToolUse': 'post',
22
+ 'UserPromptSubmit': 'prompt',
22
23
  };
23
24
 
24
25
  export function parseInput() {
package/lib/config.mjs CHANGED
@@ -8,11 +8,15 @@ import { join } from 'path';
8
8
 
9
9
  const DEFAULTS = {
10
10
  language: 'en',
11
+ sourceExtensions: ['ts', 'tsx', 'js', 'jsx', 'py', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'svelte', 'vue', 'rb', 'kt', 'swift', 'php', 'cs', 'scala', 'ex', 'clj', 'zig', 'lua', 'dart'],
12
+ testPatterns: ['test', 'spec', '_test'],
13
+ customRules: [],
11
14
  hooks: {
12
15
  'commit-guard': { enabled: true, maxTestAge: 300 },
13
16
  'debug-loop': { enabled: true, warnAt: 3, blockAt: 5 },
14
17
  'test-tracker': { enabled: true },
15
- 'scope-guard': { enabled: true, warnAt: 4, blockAt: 7, agentWarnAt: 8, agentBlockAt: 12 }
18
+ 'scope-guard': { enabled: true, warnAt: 4, blockAt: 7, agentWarnAt: 8, agentBlockAt: 12 },
19
+ 'alignment': { enabled: true, driftThreshold: 2 }
16
20
  }
17
21
  };
18
22
 
@@ -33,7 +37,12 @@ export function loadConfig(projectRoot) {
33
37
 
34
38
  export function getHookConfig(hookName, projectRoot) {
35
39
  const config = loadConfig(projectRoot);
36
- return config.hooks?.[hookName] || DEFAULTS.hooks[hookName] || { enabled: true };
40
+ const hookConfig = config.hooks?.[hookName] || DEFAULTS.hooks[hookName] || { enabled: true };
41
+ // Include top-level fields needed by hooks
42
+ hookConfig.language = config.language || DEFAULTS.language;
43
+ hookConfig.sourceExtensions = config.sourceExtensions || DEFAULTS.sourceExtensions;
44
+ hookConfig.testPatterns = config.testPatterns || DEFAULTS.testPatterns;
45
+ return hookConfig;
37
46
  }
38
47
 
39
48
  const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
@@ -51,4 +60,14 @@ function deepMerge(target, source) {
51
60
  return result;
52
61
  }
53
62
 
63
+ export function buildSourcePattern(extensions) {
64
+ const escaped = extensions.map(e => e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
65
+ return new RegExp(`\\.(${escaped.join('|')})$`);
66
+ }
67
+
68
+ export function buildTestPattern(patterns) {
69
+ const escaped = patterns.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
70
+ return new RegExp(`\\.(${escaped.join('|')})\\.`);
71
+ }
72
+
54
73
  export { DEFAULTS };
package/lib/installer.mjs CHANGED
@@ -49,6 +49,11 @@ export async function init(projectDir, options = {}) {
49
49
  copyFileSync(join(runnersDir, 'test-tracker.mjs'), join(hooksDir, 'test-tracker.mjs'));
50
50
  copyFileSync(join(runnersDir, 'scope-guard.mjs'), join(hooksDir, 'scope-guard.mjs'));
51
51
 
52
+ // Copy unified pipeline runners
53
+ copyFileSync(join(runnersDir, 'post-tool.mjs'), join(hooksDir, 'post-tool.mjs'));
54
+ copyFileSync(join(runnersDir, 'pre-tool.mjs'), join(hooksDir, 'pre-tool.mjs'));
55
+ copyFileSync(join(runnersDir, 'user-prompt.mjs'), join(hooksDir, 'user-prompt.mjs'));
56
+
52
57
  // Copy rule logic files
53
58
  const rulesDestDir = join(claudeDir, 'rules');
54
59
  mkdirSync(rulesDestDir, { recursive: true });
@@ -57,12 +62,14 @@ export async function init(projectDir, options = {}) {
57
62
  copyFileSync(join(hooksSourceDir, 'debug-loop.mjs'), join(rulesDestDir, 'debug-loop.mjs'));
58
63
  copyFileSync(join(hooksSourceDir, 'test-tracker.mjs'), join(rulesDestDir, 'test-tracker.mjs'));
59
64
  copyFileSync(join(hooksSourceDir, 'scope-guard.mjs'), join(rulesDestDir, 'scope-guard.mjs'));
65
+ copyFileSync(join(hooksSourceDir, 'turn-reporter.mjs'), join(rulesDestDir, 'turn-reporter.mjs'));
66
+ copyFileSync(join(hooksSourceDir, 'alignment.mjs'), join(rulesDestDir, 'alignment.mjs'));
60
67
 
61
68
  // Copy lib dependencies (adapter + state + config + utils)
62
69
  const libDestDir = join(claudeDir, 'lib');
63
70
  mkdirSync(libDestDir, { recursive: true });
64
71
  const libSourceDir = join(__dirname);
65
- for (const file of ['adapter.mjs', 'state.mjs', 'config.mjs', 'utils.mjs']) {
72
+ for (const file of ['adapter.mjs', 'state.mjs', 'config.mjs', 'utils.mjs', 'pipeline.mjs']) {
66
73
  copyFileSync(join(libSourceDir, file), join(libDestDir, file));
67
74
  }
68
75
 
@@ -82,10 +89,16 @@ export async function init(projectDir, options = {}) {
82
89
  'commit-guard': { enabled: true, maxTestAge: 300 },
83
90
  'debug-loop': { enabled: true, warnAt: 3, blockAt: 5 },
84
91
  'test-tracker': { enabled: true },
85
- 'scope-guard': { enabled: true, warnAt: 4, blockAt: 7, agentWarnAt: 8, agentBlockAt: 12 }
92
+ 'scope-guard': { enabled: true, warnAt: 4, blockAt: 7, agentWarnAt: 8, agentBlockAt: 12 },
93
+ 'alignment': { enabled: true, driftThreshold: 2 }
86
94
  }
87
95
  }, null, 2) + '\n');
88
96
  }
97
+
98
+ // Write version file for doctor to detect mismatches
99
+ const pkgPath = join(__dirname, '..', 'package.json');
100
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
101
+ writeFileSync(join(claudeDir, '.prism-version'), pkg.version);
89
102
  }
90
103
 
91
104
  /**
@@ -153,7 +166,7 @@ export function uninstall(projectDir) {
153
166
  }
154
167
 
155
168
  // 3. Remove prism hooks
156
- for (const hook of ['commit-guard.mjs', 'debug-loop.mjs', 'test-tracker.mjs', 'scope-guard.mjs']) {
169
+ for (const hook of ['commit-guard.mjs', 'debug-loop.mjs', 'test-tracker.mjs', 'scope-guard.mjs', 'user-prompt.mjs']) {
157
170
  const p = join(claudeDir, 'hooks', hook);
158
171
  if (existsSync(p)) rmSync(p);
159
172
  }
@@ -171,7 +184,7 @@ export function uninstall(projectDir) {
171
184
  if (settings.hooks) {
172
185
  for (const [event, hookList] of Object.entries(settings.hooks)) {
173
186
  settings.hooks[event] = hookList.filter(
174
- h => !h.hooks?.some(hh => hh.command?.includes('commit-guard') || hh.command?.includes('debug-loop') || hh.command?.includes('test-tracker') || hh.command?.includes('scope-guard'))
187
+ h => !h.hooks?.some(hh => hh.command?.includes('commit-guard') || hh.command?.includes('debug-loop') || hh.command?.includes('test-tracker') || hh.command?.includes('scope-guard') || hh.command?.includes('user-prompt'))
175
188
  );
176
189
  if (settings.hooks[event].length === 0) delete settings.hooks[event];
177
190
  }
@@ -184,6 +197,10 @@ export function uninstall(projectDir) {
184
197
  if (existsSync(configPath)) rmSync(configPath);
185
198
  const legacyConfigPath = join(projectDir, '.prism.json');
186
199
  if (existsSync(legacyConfigPath)) rmSync(legacyConfigPath);
200
+
201
+ // Remove version file
202
+ const versionFile = join(claudeDir, '.prism-version');
203
+ if (existsSync(versionFile)) rmSync(versionFile);
187
204
  }
188
205
 
189
206
  /**
@@ -265,6 +282,14 @@ export function doctor(projectDir, options = {}) {
265
282
  }
266
283
  }
267
284
 
285
+ // Check optional hooks (warn only, don't affect ok)
286
+ for (const hook of ['user-prompt.mjs']) {
287
+ if (!existsSync(join(claudeDir, 'hooks', hook))) {
288
+ issues.push(`Missing optional hook: ${hook} (turn reporter)`);
289
+ fixes.push('Run `prism update` to restore missing files');
290
+ }
291
+ }
292
+
268
293
  // Check CLAUDE.md
269
294
  const claudeMdPath = join(projectDir, 'CLAUDE.md');
270
295
  if (!existsSync(claudeMdPath)) {
@@ -300,6 +325,18 @@ export function doctor(projectDir, options = {}) {
300
325
  fixes.push('Run `prism update` to restore');
301
326
  }
302
327
 
328
+ // Check version mismatch
329
+ const versionFile = join(claudeDir, '.prism-version');
330
+ if (existsSync(versionFile)) {
331
+ const installedVersion = readFileSync(versionFile, 'utf8').trim();
332
+ const pkgPath = join(__dirname, '..', 'package.json');
333
+ const currentVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
334
+ if (installedVersion !== currentVersion) {
335
+ issues.push(`Version mismatch: installed v${installedVersion}, CLI v${currentVersion}`);
336
+ fixes.push('Run `prism update` to sync installed files with current CLI version');
337
+ }
338
+ }
339
+
303
340
  // Deduplicate fixes
304
341
  const uniqueFixes = [...new Set(fixes)];
305
342
 
@@ -419,20 +456,93 @@ export function uninstallGlobal(options = {}) {
419
456
  if (existsSync(skillDir)) rmSync(skillDir, { recursive: true });
420
457
  }
421
458
 
459
+ /**
460
+ * Dry-run: show what init would do without making changes
461
+ * @param {string} projectDir
462
+ * @param {Object} options
463
+ * @returns {{ actions: Array<{type: string, path: string, status: string}> }}
464
+ */
465
+ export function dryRun(projectDir, options = {}) {
466
+ const { language = 'en', hooks = true } = options;
467
+ const claudeDir = join(projectDir, '.claude');
468
+ const actions = [];
469
+
470
+ // Commands
471
+ const nsCommandsDir = join(claudeDir, 'commands', 'claude-prism');
472
+ const commandFiles = ['prism.md', 'checkpoint.md', 'plan.md', 'doctor.md', 'stats.md', 'help.md', 'update.md'];
473
+ for (const cmd of commandFiles) {
474
+ const target = join(nsCommandsDir, cmd);
475
+ actions.push({
476
+ type: 'command',
477
+ path: `.claude/commands/claude-prism/${cmd}`,
478
+ status: existsSync(target) ? 'update' : 'create'
479
+ });
480
+ }
481
+
482
+ // Hooks
483
+ if (hooks) {
484
+ const hookFiles = ['commit-guard.mjs', 'debug-loop.mjs', 'test-tracker.mjs', 'scope-guard.mjs', 'pre-tool.mjs', 'post-tool.mjs', 'user-prompt.mjs'];
485
+ for (const hook of hookFiles) {
486
+ const target = join(claudeDir, 'hooks', hook);
487
+ actions.push({
488
+ type: 'hook',
489
+ path: `.claude/hooks/${hook}`,
490
+ status: existsSync(target) ? 'update' : 'create'
491
+ });
492
+ }
493
+
494
+ const ruleFiles = ['commit-guard.mjs', 'debug-loop.mjs', 'test-tracker.mjs', 'scope-guard.mjs', 'turn-reporter.mjs'];
495
+ for (const rule of ruleFiles) {
496
+ const target = join(claudeDir, 'rules', rule);
497
+ actions.push({
498
+ type: 'rule',
499
+ path: `.claude/rules/${rule}`,
500
+ status: existsSync(target) ? 'update' : 'create'
501
+ });
502
+ }
503
+
504
+ const libFiles = ['adapter.mjs', 'state.mjs', 'config.mjs', 'utils.mjs', 'messages.mjs', 'pipeline.mjs'];
505
+ for (const lib of libFiles) {
506
+ const target = join(claudeDir, 'lib', lib);
507
+ actions.push({
508
+ type: 'lib',
509
+ path: `.claude/lib/${lib}`,
510
+ status: existsSync(target) ? 'update' : 'create'
511
+ });
512
+ }
513
+ }
514
+
515
+ // CLAUDE.md
516
+ const claudeMdPath = join(projectDir, 'CLAUDE.md');
517
+ actions.push({
518
+ type: 'rules',
519
+ path: 'CLAUDE.md',
520
+ status: existsSync(claudeMdPath) ? 'update' : 'create'
521
+ });
522
+
523
+ // Config
524
+ const configPath = join(projectDir, '.claude-prism.json');
525
+ if (!existsSync(configPath)) {
526
+ actions.push({ type: 'config', path: '.claude-prism.json', status: 'create' });
527
+ }
528
+
529
+ return { actions };
530
+ }
531
+
422
532
  // ─── internal helpers ───
423
533
 
424
534
  function injectRules(projectDir, language) {
425
535
  const claudeMdPath = join(projectDir, 'CLAUDE.md');
426
536
  const rulesFile = `rules.${language}.md`;
427
- const rulesPath = join(TEMPLATES_DIR, rulesFile);
537
+ let rulesPath = join(TEMPLATES_DIR, rulesFile);
428
538
 
539
+ // Fallback to English if requested language not available
429
540
  if (!existsSync(rulesPath)) {
430
- // Fallback to English
431
- const fallback = join(TEMPLATES_DIR, 'rules.en.md');
432
- if (!existsSync(fallback)) return;
541
+ rulesPath = join(TEMPLATES_DIR, 'rules.en.md');
542
+ if (!existsSync(rulesPath)) return;
433
543
  }
434
544
 
435
- const rules = readFileSync(existsSync(rulesPath) ? rulesPath : join(TEMPLATES_DIR, 'rules.en.md'), 'utf8');
545
+ const rules = readFileSync(rulesPath, 'utf8');
436
546
 
437
547
  let existing = '';
438
548
  if (existsSync(claudeMdPath)) {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * claude-prism — Hook Message Templates
3
+ * Provides localized messages for hook output
4
+ */
5
+
6
+ const MESSAGES = {
7
+ en: {
8
+ 'commit-guard.block.failed': '🌈 Prism ✋ Commit blocked: last test run FAILED. Fix tests before committing.',
9
+ 'commit-guard.warn.no-test': '🌈 Prism > No test run detected this session. Run tests before committing.',
10
+ 'commit-guard.warn.stale': '🌈 Prism > Last test run was {minutes}min ago. Run tests before committing.',
11
+ 'debug-loop.block.divergent': '🌈 Prism ✋ Debug Loop blocked: {name} edited {count} times on same area. Discuss approach with user before continuing.',
12
+ 'debug-loop.warn.divergent': '🌈 Prism > Debug Loop: {name} edited {count} times on same area. Stop and investigate root cause.',
13
+ 'debug-loop.warn.convergent': '🌈 Prism > Debug Loop: {name} edited {count} times (different areas). Consider if this is expected.',
14
+ 'scope-guard.block': '🌈 Prism ✋ Scope Guard: {count} unique files modified without a plan. Run /prism to decompose before continuing.',
15
+ 'scope-guard.warn': '🌈 Prism > Scope Guard: {count} unique files modified. Consider running /prism to decompose the task.',
16
+ 'scope-guard.plan-detected': '🌈 Prism 📋 Plan file detected. Scope thresholds raised.',
17
+ 'test-tracker.warn.failed': '🌈 Prism 📊 Tests FAILED. Fix before committing.',
18
+ },
19
+ ko: {
20
+ 'commit-guard.block.failed': '🌈 Prism ✋ 커밋 차단: 마지막 테스트 실패. 테스트를 수정한 후 커밋하세요.',
21
+ 'commit-guard.warn.no-test': '🌈 Prism > 이 세션에서 테스트 실행 이력이 없습니다. 커밋 전에 테스트를 실행하세요.',
22
+ 'commit-guard.warn.stale': '🌈 Prism > 마지막 테스트가 {minutes}분 전입니다. 커밋 전에 테스트를 실행하세요.',
23
+ 'debug-loop.block.divergent': '🌈 Prism ✋ 디버그 루프 차단: {name}이 같은 영역에서 {count}회 수정됨. 사용자와 접근 방식을 논의하세요.',
24
+ 'debug-loop.warn.divergent': '🌈 Prism > 디버그 루프: {name}이 같은 영역에서 {count}회 수정됨. 멈추고 근본 원인을 조사하세요.',
25
+ 'debug-loop.warn.convergent': '🌈 Prism > 디버그 루프: {name}이 {count}회 수정됨 (다른 영역). 예상된 작업인지 확인하세요.',
26
+ 'scope-guard.block': '🌈 Prism ✋ 스코프 가드: 계획 없이 {count}개 고유 파일 수정됨. /prism으로 분해 후 계속하세요.',
27
+ 'scope-guard.warn': '🌈 Prism > 스코프 가드: {count}개 고유 파일 수정됨. /prism으로 작업을 분해하는 것을 고려하세요.',
28
+ 'scope-guard.plan-detected': '🌈 Prism 📋 계획 파일 감지됨. 스코프 임계값이 상향 조정되었습니다.',
29
+ 'test-tracker.warn.failed': '🌈 Prism 📊 테스트 실패. 커밋 전에 수정하세요.',
30
+ },
31
+ ja: {
32
+ 'commit-guard.block.failed': '🌈 Prism ✋ コミットブロック: 最後のテストが失敗しました。テストを修正してからコミットしてください。',
33
+ 'commit-guard.warn.no-test': '🌈 Prism > このセッションでテスト実行が検出されません。コミット前にテストを実行してください。',
34
+ 'commit-guard.warn.stale': '🌈 Prism > 最後のテスト実行は{minutes}分前です。コミット前にテストを実行してください。',
35
+ 'debug-loop.block.divergent': '🌈 Prism ✋ デバッグループブロック: {name}が同じ領域で{count}回編集されました。ユーザーとアプローチを議論してください。',
36
+ 'debug-loop.warn.divergent': '🌈 Prism > デバッグループ: {name}が同じ領域で{count}回編集されました。停止して根本原因を調査してください。',
37
+ 'debug-loop.warn.convergent': '🌈 Prism > デバッグループ: {name}が{count}回編集されました(異なる領域)。想定通りか確認してください。',
38
+ 'scope-guard.block': '🌈 Prism ✋ スコープガード: 計画なしに{count}個のファイルが変更されました。/prismで分解してから続行してください。',
39
+ 'scope-guard.warn': '🌈 Prism > スコープガード: {count}個のファイルが変更されました。/prismでタスクの分解を検討してください。',
40
+ 'scope-guard.plan-detected': '🌈 Prism 📋 計画ファイルを検出。スコープ閾値を引き上げました。',
41
+ 'test-tracker.warn.failed': '🌈 Prism 📊 テスト失敗。コミット前に修正してください。',
42
+ },
43
+ zh: {
44
+ 'commit-guard.block.failed': '🌈 Prism ✋ 提交被阻止:上次测试失败。请修复测试后再提交。',
45
+ 'commit-guard.warn.no-test': '🌈 Prism > 本次会话未检测到测试运行。请在提交前运行测试。',
46
+ 'commit-guard.warn.stale': '🌈 Prism > 上次测试运行在{minutes}分钟前。请在提交前运行测试。',
47
+ 'debug-loop.block.divergent': '🌈 Prism ✋ 调试循环阻止:{name}在同一区域被编辑了{count}次。请与用户讨论方法。',
48
+ 'debug-loop.warn.divergent': '🌈 Prism > 调试循环:{name}在同一区域被编辑了{count}次。停止并调查根本原因。',
49
+ 'debug-loop.warn.convergent': '🌈 Prism > 调试循环:{name}被编辑了{count}次(不同区域)。请确认这是否是预期行为。',
50
+ 'scope-guard.block': '🌈 Prism ✋ 范围守卫:未制定计划就修改了{count}个文件。请运行/prism分解后再继续。',
51
+ 'scope-guard.warn': '🌈 Prism > 范围守卫:已修改{count}个文件。请考虑运行/prism来分解任务。',
52
+ 'scope-guard.plan-detected': '🌈 Prism 📋 检测到计划文件。范围阈值已提高。',
53
+ 'test-tracker.warn.failed': '🌈 Prism 📊 测试失败。请在提交前修复。',
54
+ },
55
+ };
56
+
57
+ export function getMessage(lang, key, params = {}) {
58
+ const template = MESSAGES[lang]?.[key] || MESSAGES.en[key] || key;
59
+ return template.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`);
60
+ }