claude-prism 0.5.2 → 0.6.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.
@@ -33,7 +33,16 @@ export const alignment = {
33
33
  const dir = ctx.filePath.split('/').slice(0, -1).join('/') || '.';
34
34
  let scopeDirs = readJsonState(stateDir, 'scope-directories') || [];
35
35
 
36
- if (!scopeDirs.includes(dir)) {
36
+ // Check if dir is within any existing base scope (subdirectory match)
37
+ const isWithinScope = scopeDirs.some(base =>
38
+ dir === base || dir.startsWith(base + '/')
39
+ );
40
+ // Also check if dir is a parent of an existing scope dir
41
+ const isParentOfScope = scopeDirs.some(base =>
42
+ base.startsWith(dir + '/')
43
+ );
44
+
45
+ if (!isWithinScope && !isParentOfScope && !scopeDirs.includes(dir)) {
37
46
  // First 3 unique directories establish the "base scope"
38
47
  if (scopeDirs.length < 3) {
39
48
  scopeDirs.push(dir);
@@ -69,6 +69,13 @@ export const debugLoop = {
69
69
  }
70
70
  };
71
71
 
72
+ // Common statement prefixes that don't indicate "same area"
73
+ const BOILERPLATE_RE = /^(?:import\s.*?from\s|import\s*\{|export\s+(?:default\s+)?(?:const|let|var|function|class|interface|type)\s|const\s|let\s|var\s|function\s|return\s)/;
74
+
75
+ function stripBoilerplate(snippet) {
76
+ return snippet.replace(BOILERPLATE_RE, '').trim();
77
+ }
78
+
72
79
  function analyzePattern(log) {
73
80
  if (log.length < 3) return null;
74
81
  const recent = log.slice(-3).map(e => e.snippet);
@@ -80,11 +87,18 @@ function analyzePattern(log) {
80
87
  const uniqueSnippets = new Set(recent).size;
81
88
  if (uniqueSnippets === 1) return 'divergent';
82
89
 
83
- // Check for meaningful overlap using longer window
84
- const baseSnippet = recent[0].slice(0, 40);
85
- // Skip overlap check if base is too short for meaningful comparison
86
- if (baseSnippet.length < 10) return uniqueSnippets <= 2 ? 'divergent' : 'convergent';
90
+ // Strip boilerplate prefixes before comparing (import X from, const, etc.)
91
+ const stripped = recent.map(stripBoilerplate);
92
+ const uniqueStripped = new Set(stripped).size;
93
+
94
+ // After stripping, if all unique → editing different areas
95
+ if (uniqueStripped === recent.length) return 'convergent';
96
+
97
+ // Check for meaningful overlap using stripped content
98
+ const baseSnippet = stripped[0].slice(0, 30);
99
+ if (baseSnippet.length < 10) return uniqueStripped <= 1 ? 'divergent' : 'convergent';
87
100
 
88
- const overlap = recent.filter(s => s.includes(baseSnippet)).length;
89
- return overlap >= 2 ? 'divergent' : 'convergent';
101
+ // Require all 3 to overlap (stricter than previous >= 2)
102
+ const overlap = stripped.filter(s => s.includes(baseSnippet)).length;
103
+ return overlap >= 3 ? 'divergent' : 'convergent';
90
104
  }
@@ -69,11 +69,19 @@ export const scopeGuard = {
69
69
  }
70
70
 
71
71
  if (hasPlan) {
72
- warnAt *= 2;
73
- blockAt *= 2;
72
+ const multiplier = config.planMultiplier || 3;
73
+ warnAt *= multiplier;
74
+ blockAt *= multiplier;
74
75
  }
75
76
 
76
77
  if (count >= blockAt) {
78
+ // With a plan: downgrade block → warn (planned large tasks are expected)
79
+ if (hasPlan) {
80
+ return {
81
+ type: 'warn',
82
+ message: getMessage(lang, 'scope-guard.block-with-plan', { count, blockAt })
83
+ };
84
+ }
77
85
  return {
78
86
  type: 'block',
79
87
  message: getMessage(lang, 'scope-guard.block', { count })
@@ -81,9 +89,10 @@ export const scopeGuard = {
81
89
  }
82
90
 
83
91
  if (count >= warnAt) {
92
+ const msgKey = hasPlan ? 'scope-guard.warn-with-plan' : 'scope-guard.warn';
84
93
  return {
85
94
  type: 'warn',
86
- message: getMessage(lang, 'scope-guard.warn', { count })
95
+ message: getMessage(lang, msgKey, { count, blockAt })
87
96
  };
88
97
  }
89
98
 
package/lib/config.mjs CHANGED
@@ -14,7 +14,7 @@ const DEFAULTS = {
14
14
  'commit-guard': { enabled: true, maxTestAge: 300 },
15
15
  'debug-loop': { enabled: true, warnAt: 3, blockAt: 5 },
16
16
  'test-tracker': { enabled: true },
17
- 'scope-guard': { enabled: true, warnAt: 4, blockAt: 7, agentWarnAt: 8, agentBlockAt: 12 },
17
+ 'scope-guard': { enabled: true, warnAt: 4, blockAt: 7, agentWarnAt: 8, agentBlockAt: 12, planMultiplier: 3 },
18
18
  'alignment': { enabled: true, driftThreshold: 2 }
19
19
  }
20
20
  };
package/lib/installer.mjs CHANGED
@@ -232,6 +232,22 @@ export async function update(projectDir) {
232
232
  if (existsSync(p)) rmSync(p);
233
233
  }
234
234
 
235
+ // Migration: remove legacy individual hook entries from settings.json
236
+ const settingsPath = join(claudeDir, 'settings.json');
237
+ if (existsSync(settingsPath)) {
238
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
239
+ if (settings.hooks) {
240
+ const legacyCommands = ['commit-guard', 'debug-loop', 'test-tracker', 'scope-guard'];
241
+ for (const [event, hookList] of Object.entries(settings.hooks)) {
242
+ settings.hooks[event] = hookList.filter(
243
+ h => !h.hooks?.some(hh => legacyCommands.some(lc => hh.command?.includes(lc)))
244
+ );
245
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
246
+ }
247
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
248
+ }
249
+ }
250
+
235
251
  // Remove old config so init creates a fresh one
236
252
  if (existsSync(configPath)) rmSync(configPath);
237
253
 
package/lib/messages.mjs CHANGED
@@ -10,7 +10,9 @@ const MESSAGES = {
10
10
  'debug-loop.warn.divergent': '🌈 Prism > Debug Loop: {name} edited {count} times on same area. Stop and investigate root cause.',
11
11
  'debug-loop.warn.convergent': '🌈 Prism > Debug Loop: {name} edited {count} times (different areas). Consider if this is expected.',
12
12
  'scope-guard.block': '🌈 Prism ✋ Scope Guard: {count} unique files modified without a plan. Run /prism to decompose before continuing.',
13
+ 'scope-guard.block-with-plan': '🌈 Prism > Scope Guard: {count} files modified (plan threshold: {blockAt}). Verify changes stay within plan scope.',
13
14
  'scope-guard.warn': '🌈 Prism > Scope Guard: {count} unique files modified. Consider running /prism to decompose the task.',
15
+ 'scope-guard.warn-with-plan': '🌈 Prism > Scope Guard: {count} files modified (plan threshold: {blockAt}). On track if within plan.',
14
16
  'scope-guard.plan-detected': '🌈 Prism 📋 Plan file detected. Scope thresholds raised.',
15
17
  'test-tracker.warn.failed': '🌈 Prism 📊 Tests FAILED. Fix before committing.',
16
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-prism",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "AI coding problem decomposition tool — Understand, Decompose, Execute, Checkpoint.",
5
5
  "type": "module",
6
6
  "bin": {