claude-prism 0.3.1 โ†’ 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }