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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/hooks/plan-progress-tracker.mjs +121 -0
- package/lib/installer.mjs +3 -1
- package/lib/plan-lifecycle.mjs +43 -0
- package/package.json +1 -1
- package/templates/rules.md +6 -0
- package/templates/runners/post-tool.mjs +2 -0
- package/templates/settings.json +10 -0
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);
|
package/lib/plan-lifecycle.mjs
CHANGED
|
@@ -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
package/templates/rules.md
CHANGED
|
@@ -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');
|
package/templates/settings.json
CHANGED