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/hooks/test-tracker.mjs
CHANGED
|
@@ -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
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
passed =
|
|
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:
|
|
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
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
|
-
|
|
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
|
-
|
|
537
|
+
let rulesPath = join(TEMPLATES_DIR, rulesFile);
|
|
428
538
|
|
|
539
|
+
// Fallback to English if requested language not available
|
|
429
540
|
if (!existsSync(rulesPath)) {
|
|
430
|
-
|
|
431
|
-
|
|
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(
|
|
545
|
+
const rules = readFileSync(rulesPath, 'utf8');
|
|
436
546
|
|
|
437
547
|
let existing = '';
|
|
438
548
|
if (existsSync(claudeMdPath)) {
|
package/lib/messages.mjs
ADDED
|
@@ -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
|
+
}
|