claude-prism 1.7.1 → 1.8.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-prism",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "AI agent harness implementing the EUDEC methodology",
5
5
  "author": { "name": "lazysaturday91" },
6
6
  "repository": "https://github.com/lazysaturday91/claude-prism",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.8.0] — 2026-03-06
9
+
10
+ ### Added
11
+ - **Plan progress auto-tracking** — `PostToolUse [Edit|Write]` hook tracks file-level progress against active plan's "Files in Scope"
12
+ - `plan-progress-tracker` rule: matches edited files to scoped files, records milestones (25/50/75%), auto-transitions `draft → active` on first edit
13
+ - `PostToolUse [Edit|Write]` event matcher added to `settings.json` — enables hooks on file edits, not just Bash commands
14
+ - `parseScopedFiles(content)` in `plan-lifecycle.mjs` — extracts file paths from "Files in Scope" section
15
+ - `ensureFrontmatter(planPath, content)` in `plan-lifecycle.mjs` — auto-backfills frontmatter for plans without status (derives from checkbox progress)
16
+ - Self-Correction Trigger: "Plan file checkboxes not updated after batch"
17
+ - 27 new tests covering parseScopedFiles, ensureFrontmatter, plan-progress-tracker, and regression guards
18
+
19
+ ### Fixed
20
+ - `mergeSettings()` now compares both command and matcher — prevents skipping new matchers for existing hook commands during `prism update`
21
+
22
+ ## [1.7.2] — 2026-03-06
23
+
24
+ ### Fixed
25
+ - **Plan template frontmatter** — EUDEC plan template now includes `status: draft` frontmatter, so plans created during DECOMPOSE phase participate in lifecycle management automatically
26
+
8
27
  ## [1.7.1] — 2026-03-06
9
28
 
10
29
  ### Fixed
@@ -0,0 +1,121 @@
1
+ /**
2
+ * claude-prism — Plan Progress Tracker
3
+ * PostToolUse rule: tracks file-level progress against active plan's "Files in Scope"
4
+ */
5
+
6
+ import { readFileSync, existsSync, readdirSync } from 'fs';
7
+ import { join, relative } from 'path';
8
+ import { readJsonState, writeJsonState } from '../lib/state.mjs';
9
+ import { parseScopedFiles, ensureFrontmatter, updatePlanStatus, appendHistory } from '../lib/plan-lifecycle.mjs';
10
+ import { parseFrontmatter } from '../lib/handoff.mjs';
11
+
12
+ export const planProgressTracker = {
13
+ name: 'plan-progress-tracker',
14
+
15
+ /**
16
+ * @param {Object} ctx - { action, filePath, ... }
17
+ * @param {Object} config - Prism config (projectRoot, hooks settings)
18
+ * @param {string} stateDir - Session state directory
19
+ * @returns {{ type: string, message?: string }}
20
+ */
21
+ evaluate(ctx, config, stateDir) {
22
+ // Only process Edit/Write actions
23
+ if (ctx.action !== 'edit' && ctx.action !== 'write') return { type: 'pass' };
24
+ if (!ctx.filePath) return { type: 'pass' };
25
+
26
+ const projectRoot = config.projectRoot || process.cwd();
27
+
28
+ // Find most recent active/draft plan
29
+ const plan = findActivePlan(projectRoot);
30
+ if (!plan) return { type: 'pass' };
31
+
32
+ // Parse "Files in Scope" → check if edited file is in scope
33
+ const scopedFiles = parseScopedFiles(plan.content);
34
+ if (scopedFiles.length === 0) return { type: 'pass' };
35
+
36
+ const relativePath = relative(projectRoot, ctx.filePath);
37
+ const inScope = scopedFiles.some(f =>
38
+ relativePath === f || relativePath.endsWith(f) || f.endsWith(relativePath)
39
+ );
40
+ if (!inScope) return { type: 'pass' };
41
+
42
+ // Accumulate touched files in session state
43
+ const state = readJsonState(stateDir, 'plan-progress') || { touched: [], recordedMilestones: [] };
44
+ if (!state.recordedMilestones) state.recordedMilestones = [];
45
+ if (!state.touched.includes(relativePath)) {
46
+ state.touched.push(relativePath);
47
+ }
48
+
49
+ // Ensure frontmatter exists (auto-backfill if missing)
50
+ try {
51
+ ensureFrontmatter(plan.path, plan.content);
52
+ } catch { /* should not break the hook */ }
53
+
54
+ // Calculate progress
55
+ const pct = Math.floor((state.touched.length / scopedFiles.length) * 100);
56
+ const fm = plan.frontmatter;
57
+ const planStatus = fm.status || 'active';
58
+
59
+ // draft → active (first scoped file edit)
60
+ if (planStatus === 'draft') {
61
+ try {
62
+ updatePlanStatus(plan.path, 'active');
63
+ appendHistory(projectRoot, {
64
+ plan: plan.name, event: 'status_change',
65
+ from: 'draft', to: 'active',
66
+ actor: 'hook:plan-progress-tracker',
67
+ detail: 'First scoped file edited'
68
+ });
69
+ } catch { /* lifecycle errors should not break the hook */ }
70
+ }
71
+
72
+ // Progress milestones (25%, 50%, 75%)
73
+ for (const m of [25, 50, 75]) {
74
+ if (pct >= m && !state.recordedMilestones.includes(m)) {
75
+ state.recordedMilestones.push(m);
76
+ try {
77
+ appendHistory(projectRoot, {
78
+ plan: plan.name, event: 'progress',
79
+ actor: 'hook:plan-progress-tracker',
80
+ detail: `Progress: ${state.touched.length}/${scopedFiles.length} (${pct}%)`
81
+ });
82
+ } catch { /* ignore */ }
83
+ }
84
+ }
85
+
86
+ // Save state
87
+ writeJsonState(stateDir, 'plan-progress', state);
88
+
89
+ return {
90
+ type: 'info',
91
+ message: `🌈 Plan progress: ${state.touched.length}/${scopedFiles.length} files (${pct}%) — ${plan.name}`
92
+ };
93
+ }
94
+ };
95
+
96
+ /**
97
+ * Find the most recent active or draft plan in .prism/plans/
98
+ * @param {string} projectRoot
99
+ * @returns {{ name: string, path: string, content: string, frontmatter: Object }|null}
100
+ */
101
+ function findActivePlan(projectRoot) {
102
+ const plansDir = join(projectRoot, '.prism', 'plans');
103
+ if (!existsSync(plansDir)) return null;
104
+
105
+ let files;
106
+ try {
107
+ files = readdirSync(plansDir).filter(f => f.endsWith('.md')).sort().reverse();
108
+ } catch { return null; }
109
+
110
+ for (const f of files) {
111
+ const path = join(plansDir, f);
112
+ let content;
113
+ try { content = readFileSync(path, 'utf8'); } catch { continue; }
114
+ const fm = parseFrontmatter(content);
115
+ const status = fm.status || 'active';
116
+ if (status === 'active' || status === 'draft') {
117
+ return { name: f, path, content, frontmatter: fm };
118
+ }
119
+ }
120
+ return null;
121
+ }
package/lib/installer.mjs CHANGED
@@ -66,6 +66,7 @@ export async function init(projectDir, options = {}) {
66
66
  copyFileSync(join(hooksSourceDir, 'session-end-handler.mjs'), join(rulesDestDir, 'session-end-handler.mjs'));
67
67
  copyFileSync(join(hooksSourceDir, 'subagent-scope-injector.mjs'), join(rulesDestDir, 'subagent-scope-injector.mjs'));
68
68
  copyFileSync(join(hooksSourceDir, 'task-plan-sync.mjs'), join(rulesDestDir, 'task-plan-sync.mjs'));
69
+ copyFileSync(join(hooksSourceDir, 'plan-progress-tracker.mjs'), join(rulesDestDir, 'plan-progress-tracker.mjs'));
69
70
 
70
71
  // Copy lib dependencies
71
72
  const libDestDir = join(claudeDir, 'lib');
@@ -661,7 +662,7 @@ export function dryRun(projectDir, options = {}) {
661
662
  });
662
663
  }
663
664
 
664
- 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']) {
665
+ 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', 'plan-progress-tracker.mjs']) {
665
666
  const target = join(claudeDir, 'rules', rule);
666
667
  actions.push({
667
668
  type: 'rule',
@@ -904,6 +905,7 @@ function mergeSettings(claudeDir) {
904
905
  for (const hook of hookList) {
905
906
  const alreadyExists = existing.hooks[event].some(
906
907
  h => h.hooks?.[0]?.command === hook.hooks?.[0]?.command
908
+ && (h.matcher || '') === (hook.matcher || '')
907
909
  );
908
910
  if (!alreadyExists) {
909
911
  existing.hooks[event].push(hook);
@@ -119,6 +119,49 @@ export function resolvePlan(projectRoot, planName) {
119
119
  return plans.find(p => (p.status || 'active') === 'active') || plans[0];
120
120
  }
121
121
 
122
+ // ─── Scoped Files & Frontmatter Helpers ───
123
+
124
+ /**
125
+ * Parse "Files in Scope" section from plan content
126
+ * Extracts backtick-wrapped file paths (e.g., `path/to/file.ts`)
127
+ * @param {string} content - Plan markdown content
128
+ * @returns {string[]} File paths extracted
129
+ */
130
+ export function parseScopedFiles(content) {
131
+ const section = content.match(/##\s+Files\s+in\s+Scope\s*\n([\s\S]*?)(?=\n##|\n---|\s*$)/i);
132
+ if (!section) return [];
133
+ return [...section[1].matchAll(/`([^`]+\.[a-z]+)`/g)].map(m => m[1]);
134
+ }
135
+
136
+ /**
137
+ * Ensure a plan file has frontmatter; add if missing
138
+ * Derives status from checkbox progress (0/N → draft, M/N → active, N/N → completed)
139
+ * @param {string} planPath - Absolute path to plan file
140
+ * @param {string} content - Plan file content
141
+ */
142
+ export function ensureFrontmatter(planPath, content) {
143
+ const fm = parseFrontmatter(content);
144
+ if (fm.status) return; // Already has frontmatter with status
145
+
146
+ // Derive status from checkbox state
147
+ let total = 0, done = 0;
148
+ for (const line of content.split('\n')) {
149
+ if (/^[-*]\s+\[x\]/i.test(line)) { total++; done++; }
150
+ else if (/^[-*]\s+\[ \]/.test(line)) { total++; }
151
+ }
152
+
153
+ let status = 'active';
154
+ if (total > 0 && done === 0) status = 'draft';
155
+ else if (total > 0 && done === total) status = 'completed';
156
+
157
+ // Extract date from filename (YYYY-MM-DD-topic.md)
158
+ const dateMatch = planPath.match(/(\d{4}-\d{2}-\d{2})/);
159
+ const created = dateMatch ? dateMatch[1] : new Date().toISOString().slice(0, 10);
160
+
161
+ const fmBlock = `---\nstatus: ${status}\ncreated: ${created}\n---\n\n`;
162
+ writeFileSync(planPath, fmBlock + content);
163
+ }
164
+
122
165
  // ─── Plan Discovery & Import ───
123
166
 
124
167
  const PLAN_PATTERN = /^\d{4}-\d{2}-\d{2}-.+\.md$/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-prism",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "AI agent harness implementing the EUDEC methodology — Essence, Understand, Decompose, Execute, Checkpoint.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -194,6 +194,11 @@ Save multi-step plans (6+ files) as markdown:
194
194
  - **Path**: `.prism/plans/YYYY-MM-DD-<topic>.md`
195
195
 
196
196
  ```markdown
197
+ ---
198
+ status: draft
199
+ created: YYYY-MM-DD
200
+ ---
201
+
197
202
  ## Goal
198
203
  One sentence: what we're building and why.
199
204
 
@@ -318,6 +323,7 @@ Choose verification proportional to the **risk of the change**, not the file pat
318
323
  - Adding workarounds to fix workarounds → "Design problem. Step back."
319
324
  - Copy-pasting similar code 3+ times → "Need abstraction? Ask user."
320
325
  - Dependency version mismatch detected → "Resolve before continuing."
326
+ - Plan file checkboxes not updated after batch → "Update plan checkboxes and frontmatter before continuing"
321
327
 
322
328
  **Goal Recitation** (prevents drift in long sessions):
323
329
  - At every batch boundary, re-read the plan file and confirm: "Current work aligns with: [original goal]"
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { runPipelineAsync } from '../lib/pipeline.mjs';
3
3
  import { testTracker } from '../rules/test-tracker.mjs';
4
+ import { planProgressTracker } from '../rules/plan-progress-tracker.mjs';
4
5
 
5
6
  await runPipelineAsync([
6
7
  { name: 'test-tracker', rule: testTracker },
8
+ { name: 'plan-progress-tracker', rule: planProgressTracker },
7
9
  ], 'PostToolUse');
@@ -32,6 +32,16 @@
32
32
  "timeout": 5000
33
33
  }
34
34
  ]
35
+ },
36
+ {
37
+ "matcher": "Edit|Write",
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "node .claude/hooks/post-tool.mjs",
42
+ "timeout": 5000
43
+ }
44
+ ]
35
45
  }
36
46
  ],
37
47
  "PreCompact": [