claude-prism 1.2.7 → 1.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.
- package/.claude-plugin/plugin.json +10 -0
- package/CHANGELOG.md +57 -1
- package/README.md +79 -19
- package/bin/cli.mjs +1 -1
- package/hooks/plan-enforcement.mjs +7 -2
- package/hooks/precompact-handler.mjs +44 -0
- package/hooks/session-end-handler.mjs +57 -0
- package/hooks/subagent-scope-injector.mjs +53 -0
- package/hooks/task-plan-sync.mjs +143 -0
- package/lib/config.mjs +8 -3
- package/lib/handoff.mjs +204 -0
- package/lib/installer.mjs +137 -34
- package/lib/messages.mjs +5 -1
- package/lib/webhook.mjs +57 -0
- package/package.json +4 -1
- package/plugin-hooks.json +82 -0
- package/scripts/post-tool.mjs +7 -0
- package/scripts/pre-tool.mjs +9 -0
- package/scripts/precompact.mjs +20 -0
- package/scripts/session-end.mjs +19 -0
- package/scripts/subagent-start.mjs +19 -0
- package/scripts/task-completed.mjs +19 -0
- package/templates/commands/claude-prism/checkpoint.md +1 -1
- package/templates/commands/claude-prism/doctor.md +2 -2
- package/templates/commands/claude-prism/help.md +1 -1
- package/templates/commands/claude-prism/plan.md +3 -3
- package/templates/commands/claude-prism/prism.md +3 -3
- package/templates/commands/claude-prism/stats.md +2 -2
- package/templates/rules.md +10 -3
- package/templates/runners/precompact.mjs +19 -0
- package/templates/runners/session-end.mjs +19 -0
- package/templates/runners/subagent-start.mjs +19 -0
- package/templates/runners/task-completed.mjs +19 -0
- package/templates/settings.json +44 -0
- package/templates/skills/prism/SKILL.md +3 -3
package/lib/handoff.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-prism — HANDOFF.md Generation
|
|
3
|
+
* Shared logic for auto-generating session handoff documents
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate HANDOFF.md content from current project state
|
|
12
|
+
* @param {string} projectRoot - Project root directory
|
|
13
|
+
* @returns {string} Markdown content for HANDOFF.md
|
|
14
|
+
*/
|
|
15
|
+
export function generateHandoff(projectRoot) {
|
|
16
|
+
const sections = [];
|
|
17
|
+
sections.push('# HANDOFF — Session Transition Document\n');
|
|
18
|
+
sections.push(`> Auto-generated by claude-prism at ${new Date().toISOString()}\n`);
|
|
19
|
+
|
|
20
|
+
// 1. Plan progress
|
|
21
|
+
const planInfo = getActivePlanInfo(projectRoot);
|
|
22
|
+
sections.push('## Status\n');
|
|
23
|
+
if (planInfo) {
|
|
24
|
+
const { total, done, planName } = planInfo;
|
|
25
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
26
|
+
sections.push(`- Active plan: \`${planName}\``);
|
|
27
|
+
sections.push(`- Progress: ${done}/${total} tasks (${pct}%)`);
|
|
28
|
+
if (planInfo.nextBatch) {
|
|
29
|
+
sections.push(`- Next batch: ${planInfo.nextBatch}`);
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
sections.push('- No active plan file found');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Current git state
|
|
36
|
+
sections.push('\n## Current State\n');
|
|
37
|
+
const gitInfo = getGitInfo(projectRoot);
|
|
38
|
+
sections.push(`- Branch: \`${gitInfo.branch}\``);
|
|
39
|
+
if (gitInfo.uncommitted > 0) {
|
|
40
|
+
sections.push(`- Uncommitted changes: ${gitInfo.uncommitted} file(s)`);
|
|
41
|
+
} else {
|
|
42
|
+
sections.push('- Working tree clean');
|
|
43
|
+
}
|
|
44
|
+
if (gitInfo.recentCommits.length > 0) {
|
|
45
|
+
sections.push('\nRecent commits:');
|
|
46
|
+
for (const commit of gitInfo.recentCommits) {
|
|
47
|
+
sections.push(`- ${commit}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Next steps (derived from plan)
|
|
52
|
+
sections.push('\n## Next Steps\n');
|
|
53
|
+
if (planInfo && planInfo.nextTasks.length > 0) {
|
|
54
|
+
for (const task of planInfo.nextTasks) {
|
|
55
|
+
sections.push(`- [ ] ${task}`);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
sections.push('- Review current state and determine next action');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 4. Decisions placeholder
|
|
62
|
+
sections.push('\n## Decisions Made\n');
|
|
63
|
+
sections.push('- (Populated by session context — review plan file for architectural decisions)');
|
|
64
|
+
|
|
65
|
+
// 5. Known issues
|
|
66
|
+
sections.push('\n## Known Issues\n');
|
|
67
|
+
sections.push('- (None auto-detected — review test results and build output)');
|
|
68
|
+
|
|
69
|
+
return sections.join('\n') + '\n';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read active plan file and extract progress info
|
|
74
|
+
*/
|
|
75
|
+
export function getActivePlanInfo(projectRoot) {
|
|
76
|
+
const plansDir = join(projectRoot, '.prism', 'plans');
|
|
77
|
+
if (!existsSync(plansDir)) return null;
|
|
78
|
+
|
|
79
|
+
const planFiles = readdirSync(plansDir)
|
|
80
|
+
.filter(f => f.endsWith('.md'))
|
|
81
|
+
.sort()
|
|
82
|
+
.reverse();
|
|
83
|
+
|
|
84
|
+
if (planFiles.length === 0) return null;
|
|
85
|
+
|
|
86
|
+
const planName = planFiles[0];
|
|
87
|
+
const content = readFileSync(join(plansDir, planName), 'utf8');
|
|
88
|
+
|
|
89
|
+
return parsePlanContent(content, planName);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse plan markdown content for progress info
|
|
94
|
+
* @param {string} content - Plan file content
|
|
95
|
+
* @param {string} planName - Plan filename
|
|
96
|
+
* @returns {{ planName, total, done, nextBatch, nextTasks, currentBatchFiles }}
|
|
97
|
+
*/
|
|
98
|
+
export function parsePlanContent(content, planName) {
|
|
99
|
+
const lines = content.split('\n');
|
|
100
|
+
let total = 0;
|
|
101
|
+
let done = 0;
|
|
102
|
+
let nextBatch = null;
|
|
103
|
+
const nextTasks = [];
|
|
104
|
+
let currentBatchFiles = [];
|
|
105
|
+
let inUnfinishedBatch = false;
|
|
106
|
+
let currentBatchName = '';
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const checkboxMatch = line.match(/^[-*]\s+\[([ x])\]\s+(.+)/);
|
|
110
|
+
if (checkboxMatch) {
|
|
111
|
+
total++;
|
|
112
|
+
if (checkboxMatch[1] === 'x') {
|
|
113
|
+
done++;
|
|
114
|
+
} else {
|
|
115
|
+
if (!inUnfinishedBatch) {
|
|
116
|
+
// First unfinished task — we're in the current batch
|
|
117
|
+
inUnfinishedBatch = true;
|
|
118
|
+
}
|
|
119
|
+
if (nextTasks.length < 5) {
|
|
120
|
+
nextTasks.push(checkboxMatch[2]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Detect batch headers
|
|
126
|
+
const batchMatch = line.match(/^#{1,3}\s+Batch\s+\d+[:\s]*(.+)?/i);
|
|
127
|
+
if (batchMatch) {
|
|
128
|
+
if (!nextBatch && inUnfinishedBatch) {
|
|
129
|
+
// Already past it
|
|
130
|
+
} else if (!nextBatch) {
|
|
131
|
+
// Check if there are unchecked items below
|
|
132
|
+
}
|
|
133
|
+
currentBatchName = batchMatch[0];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Track file references in current batch
|
|
137
|
+
if (inUnfinishedBatch) {
|
|
138
|
+
const fileMatch = line.match(/`([^`]+\.\w+)`/);
|
|
139
|
+
if (fileMatch && fileMatch[1].includes('/')) {
|
|
140
|
+
currentBatchFiles.push(fileMatch[1]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Find the first batch with uncompleted items
|
|
146
|
+
let batchDone = 0;
|
|
147
|
+
let batchTotal = 0;
|
|
148
|
+
let foundFirstUncompleted = false;
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const batchMatch = line.match(/^#{1,3}\s+(Batch\s+\d+[:\s]*.+)/i);
|
|
151
|
+
if (batchMatch) {
|
|
152
|
+
if (batchTotal > 0 && batchDone < batchTotal && !foundFirstUncompleted) {
|
|
153
|
+
nextBatch = currentBatchName;
|
|
154
|
+
foundFirstUncompleted = true;
|
|
155
|
+
}
|
|
156
|
+
currentBatchName = batchMatch[1];
|
|
157
|
+
batchDone = 0;
|
|
158
|
+
batchTotal = 0;
|
|
159
|
+
}
|
|
160
|
+
const cb = line.match(/^[-*]\s+\[([ x])\]/);
|
|
161
|
+
if (cb) {
|
|
162
|
+
batchTotal++;
|
|
163
|
+
if (cb[1] === 'x') batchDone++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!foundFirstUncompleted && batchTotal > 0 && batchDone < batchTotal) {
|
|
167
|
+
nextBatch = currentBatchName;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { planName, total, done, nextBatch, nextTasks, currentBatchFiles };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get git status info
|
|
175
|
+
*/
|
|
176
|
+
function getGitInfo(projectRoot) {
|
|
177
|
+
const info = { branch: 'unknown', uncommitted: 0, recentCommits: [] };
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
info.branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
181
|
+
cwd: projectRoot, encoding: 'utf8', timeout: 5000
|
|
182
|
+
}).trim();
|
|
183
|
+
} catch { /* not a git repo */ }
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const status = execSync('git status --porcelain', {
|
|
187
|
+
cwd: projectRoot, encoding: 'utf8', timeout: 5000
|
|
188
|
+
}).trim();
|
|
189
|
+
if (status) {
|
|
190
|
+
info.uncommitted = status.split('\n').filter(Boolean).length;
|
|
191
|
+
}
|
|
192
|
+
} catch { /* ignore */ }
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const log = execSync('git log --oneline -3', {
|
|
196
|
+
cwd: projectRoot, encoding: 'utf8', timeout: 5000
|
|
197
|
+
}).trim();
|
|
198
|
+
if (log) {
|
|
199
|
+
info.recentCommits = log.split('\n');
|
|
200
|
+
}
|
|
201
|
+
} catch { /* ignore */ }
|
|
202
|
+
|
|
203
|
+
return info;
|
|
204
|
+
}
|
package/lib/installer.mjs
CHANGED
|
@@ -46,6 +46,12 @@ export async function init(projectDir, options = {}) {
|
|
|
46
46
|
copyFileSync(join(runnersDir, 'pre-tool.mjs'), join(hooksDir, 'pre-tool.mjs'));
|
|
47
47
|
copyFileSync(join(runnersDir, 'post-tool.mjs'), join(hooksDir, 'post-tool.mjs'));
|
|
48
48
|
|
|
49
|
+
// Copy new event runners
|
|
50
|
+
copyFileSync(join(runnersDir, 'precompact.mjs'), join(hooksDir, 'precompact.mjs'));
|
|
51
|
+
copyFileSync(join(runnersDir, 'session-end.mjs'), join(hooksDir, 'session-end.mjs'));
|
|
52
|
+
copyFileSync(join(runnersDir, 'subagent-start.mjs'), join(hooksDir, 'subagent-start.mjs'));
|
|
53
|
+
copyFileSync(join(runnersDir, 'task-completed.mjs'), join(hooksDir, 'task-completed.mjs'));
|
|
54
|
+
|
|
49
55
|
// Copy rule logic files
|
|
50
56
|
const rulesDestDir = join(claudeDir, 'rules');
|
|
51
57
|
mkdirSync(rulesDestDir, { recursive: true });
|
|
@@ -54,11 +60,17 @@ export async function init(projectDir, options = {}) {
|
|
|
54
60
|
copyFileSync(join(hooksSourceDir, 'test-tracker.mjs'), join(rulesDestDir, 'test-tracker.mjs'));
|
|
55
61
|
copyFileSync(join(hooksSourceDir, 'plan-enforcement.mjs'), join(rulesDestDir, 'plan-enforcement.mjs'));
|
|
56
62
|
|
|
63
|
+
// Copy new handler rule files
|
|
64
|
+
copyFileSync(join(hooksSourceDir, 'precompact-handler.mjs'), join(rulesDestDir, 'precompact-handler.mjs'));
|
|
65
|
+
copyFileSync(join(hooksSourceDir, 'session-end-handler.mjs'), join(rulesDestDir, 'session-end-handler.mjs'));
|
|
66
|
+
copyFileSync(join(hooksSourceDir, 'subagent-scope-injector.mjs'), join(rulesDestDir, 'subagent-scope-injector.mjs'));
|
|
67
|
+
copyFileSync(join(hooksSourceDir, 'task-plan-sync.mjs'), join(rulesDestDir, 'task-plan-sync.mjs'));
|
|
68
|
+
|
|
57
69
|
// Copy lib dependencies
|
|
58
70
|
const libDestDir = join(claudeDir, 'lib');
|
|
59
71
|
mkdirSync(libDestDir, { recursive: true });
|
|
60
72
|
const libSourceDir = join(__dirname);
|
|
61
|
-
for (const file of ['state.mjs', 'config.mjs', 'utils.mjs', 'messages.mjs', 'pipeline.mjs', 'session.mjs']) {
|
|
73
|
+
for (const file of ['state.mjs', 'config.mjs', 'utils.mjs', 'messages.mjs', 'pipeline.mjs', 'session.mjs', 'handoff.mjs', 'webhook.mjs']) {
|
|
62
74
|
copyFileSync(join(libSourceDir, file), join(libDestDir, file));
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -69,8 +81,11 @@ export async function init(projectDir, options = {}) {
|
|
|
69
81
|
// 4. Inject rules into CLAUDE.md
|
|
70
82
|
injectRules(projectDir);
|
|
71
83
|
|
|
72
|
-
// 5. Create .
|
|
73
|
-
const
|
|
84
|
+
// 5. Create .prism/ directory with config.json
|
|
85
|
+
const prismDir = join(projectDir, '.prism');
|
|
86
|
+
mkdirSync(prismDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
const configPath = join(prismDir, 'config.json');
|
|
74
89
|
if (!existsSync(configPath)) {
|
|
75
90
|
writeFileSync(configPath, JSON.stringify({
|
|
76
91
|
version: 1,
|
|
@@ -85,7 +100,13 @@ export async function init(projectDir, options = {}) {
|
|
|
85
100
|
// Write version file
|
|
86
101
|
const pkgPath = join(__dirname, '..', 'package.json');
|
|
87
102
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
88
|
-
writeFileSync(join(
|
|
103
|
+
writeFileSync(join(prismDir, '.version'), pkg.version);
|
|
104
|
+
|
|
105
|
+
// Create .prism/.gitignore (version file is local-only)
|
|
106
|
+
const prismGitignore = join(prismDir, '.gitignore');
|
|
107
|
+
if (!existsSync(prismGitignore)) {
|
|
108
|
+
writeFileSync(prismGitignore, '.version\n');
|
|
109
|
+
}
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
/**
|
|
@@ -107,9 +128,13 @@ export function check(projectDir) {
|
|
|
107
128
|
&& readFileSync(claudeMdPath, 'utf8').includes('<!-- PRISM:START -->');
|
|
108
129
|
|
|
109
130
|
const hooks = existsSync(join(claudeDir, 'hooks', 'pre-tool.mjs'))
|
|
110
|
-
&& existsSync(join(claudeDir, 'hooks', 'post-tool.mjs'))
|
|
131
|
+
&& existsSync(join(claudeDir, 'hooks', 'post-tool.mjs'))
|
|
132
|
+
&& existsSync(join(claudeDir, 'hooks', 'precompact.mjs'))
|
|
133
|
+
&& existsSync(join(claudeDir, 'hooks', 'session-end.mjs'))
|
|
134
|
+
&& existsSync(join(claudeDir, 'hooks', 'subagent-start.mjs'))
|
|
135
|
+
&& existsSync(join(claudeDir, 'hooks', 'task-completed.mjs'));
|
|
111
136
|
|
|
112
|
-
const config = existsSync(join(projectDir, '.
|
|
137
|
+
const config = existsSync(join(projectDir, '.prism', 'config.json'));
|
|
113
138
|
|
|
114
139
|
return {
|
|
115
140
|
commands,
|
|
@@ -151,7 +176,7 @@ export function uninstall(projectDir) {
|
|
|
151
176
|
}
|
|
152
177
|
|
|
153
178
|
// 3. Remove hooks
|
|
154
|
-
for (const hook of ['pre-tool.mjs', 'post-tool.mjs', 'user-prompt.mjs']) {
|
|
179
|
+
for (const hook of ['pre-tool.mjs', 'post-tool.mjs', 'user-prompt.mjs', 'precompact.mjs', 'session-end.mjs', 'subagent-start.mjs', 'task-completed.mjs']) {
|
|
155
180
|
const p = join(claudeDir, 'hooks', hook);
|
|
156
181
|
if (existsSync(p)) rmSync(p);
|
|
157
182
|
}
|
|
@@ -175,7 +200,7 @@ export function uninstall(projectDir) {
|
|
|
175
200
|
if (settings.hooks) {
|
|
176
201
|
for (const [event, hookList] of Object.entries(settings.hooks)) {
|
|
177
202
|
settings.hooks[event] = hookList.filter(
|
|
178
|
-
h => !h.hooks?.some(hh => hh.command?.includes('pre-tool') || hh.command?.includes('post-tool') || hh.command?.includes('user-prompt') || hh.command?.includes('commit-guard') || hh.command?.includes('debug-loop') || hh.command?.includes('test-tracker') || hh.command?.includes('scope-guard'))
|
|
203
|
+
h => !h.hooks?.some(hh => hh.command?.includes('pre-tool') || hh.command?.includes('post-tool') || hh.command?.includes('user-prompt') || hh.command?.includes('commit-guard') || hh.command?.includes('debug-loop') || hh.command?.includes('test-tracker') || hh.command?.includes('scope-guard') || hh.command?.includes('precompact') || hh.command?.includes('session-end') || hh.command?.includes('subagent-start') || hh.command?.includes('task-completed'))
|
|
179
204
|
);
|
|
180
205
|
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
181
206
|
}
|
|
@@ -183,15 +208,31 @@ export function uninstall(projectDir) {
|
|
|
183
208
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
184
209
|
}
|
|
185
210
|
|
|
186
|
-
// 6. Remove config files
|
|
187
|
-
const
|
|
211
|
+
// 6. Remove config files (new + legacy paths)
|
|
212
|
+
const prismDir = join(projectDir, '.prism');
|
|
213
|
+
const configPath = join(prismDir, 'config.json');
|
|
188
214
|
if (existsSync(configPath)) rmSync(configPath);
|
|
189
|
-
const
|
|
190
|
-
if (existsSync(legacyConfigPath)) rmSync(legacyConfigPath);
|
|
191
|
-
|
|
192
|
-
// Remove version file
|
|
193
|
-
const versionFile = join(claudeDir, '.prism-version');
|
|
215
|
+
const versionFile = join(prismDir, '.version');
|
|
194
216
|
if (existsSync(versionFile)) rmSync(versionFile);
|
|
217
|
+
const prismGitignore = join(prismDir, '.gitignore');
|
|
218
|
+
if (existsSync(prismGitignore)) rmSync(prismGitignore);
|
|
219
|
+
// Remove .prism/ directory if empty
|
|
220
|
+
if (existsSync(prismDir)) {
|
|
221
|
+
try {
|
|
222
|
+
const remaining = readdirSync(prismDir);
|
|
223
|
+
if (remaining.length === 0) rmSync(prismDir, { recursive: true });
|
|
224
|
+
} catch { /* ignore */ }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Legacy config paths
|
|
228
|
+
const legacyConfig = join(projectDir, '.claude-prism.json');
|
|
229
|
+
if (existsSync(legacyConfig)) rmSync(legacyConfig);
|
|
230
|
+
const legacyConfig2 = join(projectDir, '.prism.json');
|
|
231
|
+
if (existsSync(legacyConfig2)) rmSync(legacyConfig2);
|
|
232
|
+
|
|
233
|
+
// Legacy version file
|
|
234
|
+
const legacyVersion = join(claudeDir, '.prism-version');
|
|
235
|
+
if (existsSync(legacyVersion)) rmSync(legacyVersion);
|
|
195
236
|
}
|
|
196
237
|
|
|
197
238
|
/**
|
|
@@ -215,14 +256,72 @@ export async function update(projectDir) {
|
|
|
215
256
|
} catch { /* not our package, proceed normally */ }
|
|
216
257
|
}
|
|
217
258
|
|
|
218
|
-
|
|
259
|
+
const claudeDir = join(projectDir, '.claude');
|
|
260
|
+
|
|
261
|
+
// Migration chain: .prism.json → .claude-prism.json → .prism/config.json
|
|
262
|
+
const prismDir = join(projectDir, '.prism');
|
|
263
|
+
mkdirSync(prismDir, { recursive: true });
|
|
264
|
+
|
|
219
265
|
const oldConfigPath = join(projectDir, '.prism.json');
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
266
|
+
const midConfigPath = join(projectDir, '.claude-prism.json');
|
|
267
|
+
const configPath = join(prismDir, 'config.json');
|
|
268
|
+
|
|
269
|
+
// Step 1: .prism.json → .claude-prism.json (legacy v0.x)
|
|
270
|
+
if (existsSync(oldConfigPath) && !existsSync(midConfigPath) && !existsSync(configPath)) {
|
|
271
|
+
renameSync(oldConfigPath, configPath);
|
|
272
|
+
}
|
|
273
|
+
// Step 2: .claude-prism.json → .prism/config.json (legacy v1.x)
|
|
274
|
+
if (existsSync(midConfigPath) && !existsSync(configPath)) {
|
|
275
|
+
renameSync(midConfigPath, configPath);
|
|
276
|
+
}
|
|
277
|
+
// Clean up leftover legacy configs
|
|
278
|
+
if (existsSync(oldConfigPath)) rmSync(oldConfigPath);
|
|
279
|
+
if (existsSync(midConfigPath)) rmSync(midConfigPath);
|
|
280
|
+
|
|
281
|
+
// Migration: .claude/.prism-version → .prism/.version
|
|
282
|
+
const legacyVersionFile = join(claudeDir, '.prism-version');
|
|
283
|
+
const newVersionFile = join(prismDir, '.version');
|
|
284
|
+
if (existsSync(legacyVersionFile)) {
|
|
285
|
+
if (!existsSync(newVersionFile)) {
|
|
286
|
+
renameSync(legacyVersionFile, newVersionFile);
|
|
287
|
+
} else {
|
|
288
|
+
rmSync(legacyVersionFile);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Migration: docs/plans/ → .prism/plans/
|
|
293
|
+
const legacyPlansDir = join(projectDir, 'docs', 'plans');
|
|
294
|
+
const newPlansDir = join(prismDir, 'plans');
|
|
295
|
+
if (existsSync(legacyPlansDir)) {
|
|
296
|
+
mkdirSync(newPlansDir, { recursive: true });
|
|
297
|
+
const planFiles = readdirSync(legacyPlansDir).filter(f => f.endsWith('.md'));
|
|
298
|
+
for (const f of planFiles) {
|
|
299
|
+
const src = join(legacyPlansDir, f);
|
|
300
|
+
const dest = join(newPlansDir, f);
|
|
301
|
+
if (!existsSync(dest)) {
|
|
302
|
+
renameSync(src, dest);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Clean up empty legacy plans dir
|
|
306
|
+
try {
|
|
307
|
+
const remaining = readdirSync(legacyPlansDir);
|
|
308
|
+
if (remaining.length === 0) {
|
|
309
|
+
rmSync(legacyPlansDir, { recursive: true });
|
|
310
|
+
// Also remove docs/ if empty
|
|
311
|
+
const docsDir = join(projectDir, 'docs');
|
|
312
|
+
if (existsSync(docsDir) && readdirSync(docsDir).length === 0) {
|
|
313
|
+
rmSync(docsDir, { recursive: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch { /* ignore */ }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Create .prism/.gitignore (.version is local-only)
|
|
320
|
+
const prismGitignore = join(prismDir, '.gitignore');
|
|
321
|
+
if (!existsSync(prismGitignore)) {
|
|
322
|
+
writeFileSync(prismGitignore, '.version\n');
|
|
223
323
|
}
|
|
224
324
|
|
|
225
|
-
const configPath = newConfigPath;
|
|
226
325
|
let hooks = true;
|
|
227
326
|
|
|
228
327
|
if (existsSync(configPath)) {
|
|
@@ -230,8 +329,6 @@ export async function update(projectDir) {
|
|
|
230
329
|
hooks = config.hooks?.['commit-guard']?.enabled !== false;
|
|
231
330
|
}
|
|
232
331
|
|
|
233
|
-
const claudeDir = join(projectDir, '.claude');
|
|
234
|
-
|
|
235
332
|
// Migration: remove all legacy files from previous versions
|
|
236
333
|
const legacyFiles = [
|
|
237
334
|
// Legacy flat commands
|
|
@@ -251,6 +348,8 @@ export async function update(projectDir) {
|
|
|
251
348
|
join(claudeDir, 'rules', 'turn-reporter.mjs'),
|
|
252
349
|
// Removed lib files
|
|
253
350
|
join(claudeDir, 'lib', 'adapter.mjs'),
|
|
351
|
+
// Legacy version file (moved to .prism/.version)
|
|
352
|
+
join(claudeDir, '.prism-version'),
|
|
254
353
|
];
|
|
255
354
|
for (const p of legacyFiles) {
|
|
256
355
|
if (existsSync(p)) rmSync(p);
|
|
@@ -318,7 +417,7 @@ export function doctor(projectDir, options = {}) {
|
|
|
318
417
|
}
|
|
319
418
|
|
|
320
419
|
// Check hooks
|
|
321
|
-
for (const hook of ['pre-tool.mjs', 'post-tool.mjs']) {
|
|
420
|
+
for (const hook of ['pre-tool.mjs', 'post-tool.mjs', 'precompact.mjs', 'session-end.mjs', 'subagent-start.mjs', 'task-completed.mjs']) {
|
|
322
421
|
if (!existsSync(join(claudeDir, 'hooks', hook))) {
|
|
323
422
|
issues.push(`Missing hook: ${hook}`);
|
|
324
423
|
fixes.push('Run `prism update` to restore missing files');
|
|
@@ -338,9 +437,9 @@ export function doctor(projectDir, options = {}) {
|
|
|
338
437
|
}
|
|
339
438
|
}
|
|
340
439
|
|
|
341
|
-
// Check config
|
|
342
|
-
if (!existsSync(join(projectDir, '.
|
|
343
|
-
issues.push('Missing .
|
|
440
|
+
// Check config (.prism/config.json)
|
|
441
|
+
if (!existsSync(join(projectDir, '.prism', 'config.json'))) {
|
|
442
|
+
issues.push('Missing .prism/config.json');
|
|
344
443
|
fixes.push('Run `prism init` to create config');
|
|
345
444
|
}
|
|
346
445
|
|
|
@@ -359,6 +458,7 @@ export function doctor(projectDir, options = {}) {
|
|
|
359
458
|
{ path: join(claudeDir, 'rules', 'alignment.mjs'), label: 'Legacy alignment rule' },
|
|
360
459
|
{ path: join(claudeDir, 'hooks', 'user-prompt.mjs'), label: 'Legacy user-prompt hook' },
|
|
361
460
|
{ path: join(projectDir, '.prism.json'), label: 'Legacy .prism.json config' },
|
|
461
|
+
{ path: join(projectDir, '.claude-prism.json'), label: 'Legacy .claude-prism.json config' },
|
|
362
462
|
];
|
|
363
463
|
for (const { path, label } of legacyCheck) {
|
|
364
464
|
if (existsSync(path)) {
|
|
@@ -368,7 +468,7 @@ export function doctor(projectDir, options = {}) {
|
|
|
368
468
|
}
|
|
369
469
|
|
|
370
470
|
// Check version mismatch
|
|
371
|
-
const versionFile = join(
|
|
471
|
+
const versionFile = join(projectDir, '.prism', '.version');
|
|
372
472
|
if (existsSync(versionFile)) {
|
|
373
473
|
const installedVersion = readFileSync(versionFile, 'utf8').trim();
|
|
374
474
|
const pkgPath = join(__dirname, '..', 'package.json');
|
|
@@ -404,7 +504,7 @@ export function stats(projectDir, options = {}) {
|
|
|
404
504
|
const pkgPath = join(__dirname, '..', 'package.json');
|
|
405
505
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
406
506
|
|
|
407
|
-
const configPath = join(projectDir, '.
|
|
507
|
+
const configPath = join(projectDir, '.prism', 'config.json');
|
|
408
508
|
let hookConfig = {};
|
|
409
509
|
|
|
410
510
|
if (existsSync(configPath)) {
|
|
@@ -417,9 +517,12 @@ export function stats(projectDir, options = {}) {
|
|
|
417
517
|
}
|
|
418
518
|
|
|
419
519
|
let planFiles = 0;
|
|
420
|
-
const plansDir = join(projectDir, '
|
|
520
|
+
const plansDir = join(projectDir, '.prism', 'plans');
|
|
521
|
+
const legacyPlansDir = join(projectDir, 'docs', 'plans');
|
|
421
522
|
if (existsSync(plansDir)) {
|
|
422
523
|
planFiles = readdirSync(plansDir).filter(f => f.endsWith('.md')).length;
|
|
524
|
+
} else if (existsSync(legacyPlansDir)) {
|
|
525
|
+
planFiles = readdirSync(legacyPlansDir).filter(f => f.endsWith('.md')).length;
|
|
423
526
|
}
|
|
424
527
|
|
|
425
528
|
const omc = detectOmc(options.homeDir);
|
|
@@ -513,7 +616,7 @@ export function dryRun(projectDir, options = {}) {
|
|
|
513
616
|
|
|
514
617
|
// Hooks
|
|
515
618
|
if (hooks) {
|
|
516
|
-
for (const hook of ['pre-tool.mjs', 'post-tool.mjs']) {
|
|
619
|
+
for (const hook of ['pre-tool.mjs', 'post-tool.mjs', 'precompact.mjs', 'session-end.mjs', 'subagent-start.mjs', 'task-completed.mjs']) {
|
|
517
620
|
const target = join(claudeDir, 'hooks', hook);
|
|
518
621
|
actions.push({
|
|
519
622
|
type: 'hook',
|
|
@@ -522,7 +625,7 @@ export function dryRun(projectDir, options = {}) {
|
|
|
522
625
|
});
|
|
523
626
|
}
|
|
524
627
|
|
|
525
|
-
for (const rule of ['commit-guard.mjs', 'test-tracker.mjs', 'plan-enforcement.mjs']) {
|
|
628
|
+
for (const rule of ['commit-guard.mjs', 'test-tracker.mjs', 'plan-enforcement.mjs', 'precompact-handler.mjs', 'session-end-handler.mjs', 'subagent-scope-injector.mjs', 'task-plan-sync.mjs']) {
|
|
526
629
|
const target = join(claudeDir, 'rules', rule);
|
|
527
630
|
actions.push({
|
|
528
631
|
type: 'rule',
|
|
@@ -531,7 +634,7 @@ export function dryRun(projectDir, options = {}) {
|
|
|
531
634
|
});
|
|
532
635
|
}
|
|
533
636
|
|
|
534
|
-
for (const lib of ['state.mjs', 'config.mjs', 'utils.mjs', 'messages.mjs', 'pipeline.mjs', 'session.mjs']) {
|
|
637
|
+
for (const lib of ['state.mjs', 'config.mjs', 'utils.mjs', 'messages.mjs', 'pipeline.mjs', 'session.mjs', 'handoff.mjs', 'webhook.mjs']) {
|
|
535
638
|
const target = join(claudeDir, 'lib', lib);
|
|
536
639
|
actions.push({
|
|
537
640
|
type: 'lib',
|
|
@@ -550,9 +653,9 @@ export function dryRun(projectDir, options = {}) {
|
|
|
550
653
|
});
|
|
551
654
|
|
|
552
655
|
// Config
|
|
553
|
-
const configPath = join(projectDir, '.
|
|
656
|
+
const configPath = join(projectDir, '.prism', 'config.json');
|
|
554
657
|
if (!existsSync(configPath)) {
|
|
555
|
-
actions.push({ type: 'config', path: '.
|
|
658
|
+
actions.push({ type: 'config', path: '.prism/config.json', status: 'create' });
|
|
556
659
|
}
|
|
557
660
|
|
|
558
661
|
return { actions };
|
package/lib/messages.mjs
CHANGED
|
@@ -7,7 +7,11 @@ const MESSAGES = {
|
|
|
7
7
|
'commit-guard.warn.no-test': '🌈 Prism > No test run detected this session. Run tests before committing.',
|
|
8
8
|
'commit-guard.warn.stale': '🌈 Prism > Last test run was {minutes}min ago. Run tests before committing.',
|
|
9
9
|
'test-tracker.warn.failed': '🌈 Prism 📊 Tests FAILED. Fix before committing.',
|
|
10
|
-
'plan-enforcement.warn.no-plan': '🌈 Prism > Editing {count} unique source files without a plan. Create a plan at
|
|
10
|
+
'plan-enforcement.warn.no-plan': '🌈 Prism > Editing {count} unique source files without a plan. Create a plan at .prism/plans/ per EUDEC DECOMPOSE protocol.',
|
|
11
|
+
'precompact-handler.info.saved': '🌈 Prism > HANDOFF.md auto-saved before compaction.',
|
|
12
|
+
'session-end-handler.info.saved': '🌈 Prism > Session summary saved to PROJECT-MEMORY.md.',
|
|
13
|
+
'subagent-scope-injector.info.scope': '🌈 Prism Scope >',
|
|
14
|
+
'task-plan-sync.info.updated': '🌈 Prism > Plan updated: {task}. Progress: {done}/{total} ({pct}%)',
|
|
11
15
|
};
|
|
12
16
|
|
|
13
17
|
export function getMessage(_lang, key, params = {}) {
|
package/lib/webhook.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-prism — HTTP Webhook Dispatcher
|
|
3
|
+
* Non-blocking fire-and-forget webhook notifications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dispatch a webhook event to configured endpoints
|
|
8
|
+
* @param {Object} config - Prism config with webhooks array
|
|
9
|
+
* @param {string} event - Event name (e.g. 'compaction', 'session-end', 'batch-complete')
|
|
10
|
+
* @param {Object} payload - Event payload data
|
|
11
|
+
* @returns {Promise<void>}
|
|
12
|
+
*/
|
|
13
|
+
export async function dispatchWebhook(config, event, payload) {
|
|
14
|
+
const webhooks = config.webhooks || [];
|
|
15
|
+
if (webhooks.length === 0) return;
|
|
16
|
+
|
|
17
|
+
const body = JSON.stringify({
|
|
18
|
+
event,
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
source: 'claude-prism',
|
|
21
|
+
payload
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const promises = [];
|
|
25
|
+
|
|
26
|
+
for (const hook of webhooks) {
|
|
27
|
+
// Filter by subscribed events
|
|
28
|
+
if (hook.events && hook.events.length > 0 && !hook.events.includes(event)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!hook.url) continue;
|
|
33
|
+
|
|
34
|
+
const headers = {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'User-Agent': 'claude-prism-webhook/1.0',
|
|
37
|
+
...(hook.headers || {})
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Fire-and-forget with timeout
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
43
|
+
|
|
44
|
+
const p = fetch(hook.url, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers,
|
|
47
|
+
body,
|
|
48
|
+
signal: controller.signal
|
|
49
|
+
})
|
|
50
|
+
.catch(() => { /* silent fail — webhooks are best-effort */ })
|
|
51
|
+
.finally(() => clearTimeout(timeoutId));
|
|
52
|
+
|
|
53
|
+
promises.push(p);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await Promise.allSettled(promises);
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-prism",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "EUDEC methodology framework for AI coding agents — Essence, Understand, Decompose, Execute, Checkpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"bin/",
|
|
31
31
|
"lib/",
|
|
32
32
|
"hooks/",
|
|
33
|
+
"scripts/",
|
|
33
34
|
"templates/",
|
|
35
|
+
".claude-plugin/",
|
|
36
|
+
"plugin-hooks.json",
|
|
34
37
|
"README.md",
|
|
35
38
|
"CHANGELOG.md"
|
|
36
39
|
],
|