@massu/core 0.1.0 → 0.1.2

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.
Files changed (67) hide show
  1. package/LICENSE +71 -0
  2. package/README.md +2 -2
  3. package/dist/hooks/cost-tracker.js +149 -11527
  4. package/dist/hooks/post-edit-context.js +127 -11493
  5. package/dist/hooks/post-tool-use.js +169 -11550
  6. package/dist/hooks/pre-compact.js +149 -11530
  7. package/dist/hooks/pre-delete-check.js +144 -11523
  8. package/dist/hooks/quality-event.js +149 -11527
  9. package/dist/hooks/session-end.js +188 -11570
  10. package/dist/hooks/session-start.js +159 -11534
  11. package/dist/hooks/user-prompt.js +149 -11530
  12. package/package.json +14 -19
  13. package/src/adr-generator.ts +292 -0
  14. package/src/analytics.ts +373 -0
  15. package/src/audit-trail.ts +450 -0
  16. package/src/backfill-sessions.ts +180 -0
  17. package/src/cli.ts +105 -0
  18. package/src/cloud-sync.ts +190 -0
  19. package/src/commands/doctor.ts +300 -0
  20. package/src/commands/init.ts +395 -0
  21. package/src/commands/install-hooks.ts +26 -0
  22. package/src/config.ts +357 -0
  23. package/src/cost-tracker.ts +355 -0
  24. package/src/db.ts +233 -0
  25. package/src/dependency-scorer.ts +337 -0
  26. package/src/docs-map.json +100 -0
  27. package/src/docs-tools.ts +517 -0
  28. package/src/domains.ts +181 -0
  29. package/src/hooks/cost-tracker.ts +66 -0
  30. package/src/hooks/intent-suggester.ts +131 -0
  31. package/src/hooks/post-edit-context.ts +91 -0
  32. package/src/hooks/post-tool-use.ts +175 -0
  33. package/src/hooks/pre-compact.ts +146 -0
  34. package/src/hooks/pre-delete-check.ts +153 -0
  35. package/src/hooks/quality-event.ts +127 -0
  36. package/src/hooks/security-gate.ts +121 -0
  37. package/src/hooks/session-end.ts +467 -0
  38. package/src/hooks/session-start.ts +210 -0
  39. package/src/hooks/user-prompt.ts +91 -0
  40. package/src/import-resolver.ts +224 -0
  41. package/src/memory-db.ts +1376 -0
  42. package/src/memory-tools.ts +391 -0
  43. package/src/middleware-tree.ts +70 -0
  44. package/src/observability-tools.ts +343 -0
  45. package/src/observation-extractor.ts +411 -0
  46. package/src/page-deps.ts +283 -0
  47. package/src/prompt-analyzer.ts +332 -0
  48. package/src/regression-detector.ts +319 -0
  49. package/src/rules.ts +57 -0
  50. package/src/schema-mapper.ts +232 -0
  51. package/src/security-scorer.ts +405 -0
  52. package/src/security-utils.ts +133 -0
  53. package/src/sentinel-db.ts +578 -0
  54. package/src/sentinel-scanner.ts +405 -0
  55. package/src/sentinel-tools.ts +512 -0
  56. package/src/sentinel-types.ts +140 -0
  57. package/src/server.ts +189 -0
  58. package/src/session-archiver.ts +112 -0
  59. package/src/session-state-generator.ts +174 -0
  60. package/src/team-knowledge.ts +407 -0
  61. package/src/tools.ts +847 -0
  62. package/src/transcript-parser.ts +458 -0
  63. package/src/trpc-index.ts +214 -0
  64. package/src/validate-features-runner.ts +106 -0
  65. package/src/validation-engine.ts +358 -0
  66. package/dist/cli.js +0 -7890
  67. package/dist/server.js +0 -7008
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Massu. All rights reserved.
3
+ // Licensed under BSL 1.1 - see LICENSE file for details.
4
+
5
+ // ============================================================
6
+ // PostToolUse Hook: Cost Tracker
7
+ // Estimates token usage from tool input/output sizes and
8
+ // records cost events for per-session cost intelligence.
9
+ // Must complete in <500ms.
10
+ // ============================================================
11
+
12
+ import { getMemoryDb } from '../memory-db.ts';
13
+
14
+ interface HookInput {
15
+ session_id: string;
16
+ transcript_path: string;
17
+ cwd: string;
18
+ hook_event_name: string;
19
+ tool_name: string;
20
+ tool_input: Record<string, unknown>;
21
+ tool_response: string;
22
+ }
23
+
24
+ // Approximate: 4 characters per token (industry rule of thumb)
25
+ const CHARS_PER_TOKEN = 4;
26
+
27
+ function estimateTokens(text: string): number {
28
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
29
+ }
30
+
31
+ async function main(): Promise<void> {
32
+ try {
33
+ const input = await readStdin();
34
+ const hookInput = JSON.parse(input) as HookInput;
35
+ const { session_id, tool_name, tool_input, tool_response } = hookInput;
36
+
37
+ const inputStr = JSON.stringify(tool_input);
38
+ const estimatedInputTokens = estimateTokens(inputStr);
39
+ const estimatedOutputTokens = estimateTokens(tool_response ?? '');
40
+
41
+ const db = getMemoryDb();
42
+ try {
43
+ db.prepare(`
44
+ INSERT INTO tool_cost_events (session_id, tool_name, estimated_input_tokens, estimated_output_tokens, model)
45
+ VALUES (?, ?, ?, ?, ?)
46
+ `).run(session_id, tool_name, estimatedInputTokens, estimatedOutputTokens, '');
47
+ } finally {
48
+ db.close();
49
+ }
50
+ } catch (_e) {
51
+ // Best-effort: never block Claude Code
52
+ }
53
+ process.exit(0);
54
+ }
55
+
56
+ function readStdin(): Promise<string> {
57
+ return new Promise((resolve) => {
58
+ let data = '';
59
+ process.stdin.setEncoding('utf-8');
60
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
61
+ process.stdin.on('end', () => resolve(data));
62
+ setTimeout(() => resolve(data), 3000);
63
+ });
64
+ }
65
+
66
+ main();
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Massu. All rights reserved.
3
+ // Licensed under BSL 1.1 - see LICENSE file for details.
4
+
5
+ // ============================================================
6
+ // UserPromptSubmit Hook: Intent Suggester
7
+ // Matches prompt keywords to relevant slash commands and
8
+ // surfaces suggestions to the user as a non-blocking hint.
9
+ // ============================================================
10
+
11
+ // Force module mode for TypeScript (no external deps needed)
12
+ export {};
13
+
14
+ interface HookInput {
15
+ session_id: string;
16
+ transcript_path: string;
17
+ cwd: string;
18
+ hook_event_name: string;
19
+ prompt: string;
20
+ }
21
+
22
+ interface CommandMapping {
23
+ keywords: string[];
24
+ command: string;
25
+ description: string;
26
+ }
27
+
28
+ const COMMAND_MAPPINGS: CommandMapping[] = [
29
+ {
30
+ keywords: ['test', 'failing'],
31
+ command: '/massu-test',
32
+ description: 'Run and analyze tests',
33
+ },
34
+ {
35
+ keywords: ['debug', 'bug'],
36
+ command: '/massu-debug',
37
+ description: 'Debug an issue',
38
+ },
39
+ {
40
+ keywords: ['refactor'],
41
+ command: '/massu-refactor',
42
+ description: 'Guided refactoring workflow',
43
+ },
44
+ {
45
+ keywords: ['cleanup', 'dead code', 'unused'],
46
+ command: '/massu-cleanup',
47
+ description: 'Clean up dead code and unused exports',
48
+ },
49
+ {
50
+ keywords: ['document', 'jsdoc', 'readme'],
51
+ command: '/massu-doc-gen',
52
+ description: 'Generate documentation',
53
+ },
54
+ {
55
+ keywords: ['estimate', 'effort', 'how long'],
56
+ command: '/massu-estimate',
57
+ description: 'Estimate implementation effort',
58
+ },
59
+ {
60
+ keywords: ['release', 'deploy'],
61
+ command: '/massu-release',
62
+ description: 'Prepare a release',
63
+ },
64
+ {
65
+ keywords: ['commit'],
66
+ command: '/massu-commit',
67
+ description: 'Pre-commit verification gate',
68
+ },
69
+ {
70
+ keywords: ['push'],
71
+ command: '/massu-push',
72
+ description: 'Pre-push full verification gate',
73
+ },
74
+ {
75
+ keywords: ['plan'],
76
+ command: '/massu-create-plan',
77
+ description: 'Create an implementation plan',
78
+ },
79
+ ];
80
+
81
+ function findMatchingCommand(prompt: string): CommandMapping | null {
82
+ const lowerPrompt = prompt.toLowerCase();
83
+
84
+ for (const mapping of COMMAND_MAPPINGS) {
85
+ for (const keyword of mapping.keywords) {
86
+ if (lowerPrompt.includes(keyword.toLowerCase())) {
87
+ return mapping;
88
+ }
89
+ }
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ async function main(): Promise<void> {
96
+ try {
97
+ const input = await readStdin();
98
+ const hookInput = JSON.parse(input) as HookInput;
99
+ const { prompt } = hookInput;
100
+
101
+ if (!prompt || !prompt.trim()) {
102
+ process.exit(0);
103
+ return;
104
+ }
105
+
106
+ const match = findMatchingCommand(prompt);
107
+ if (!match) {
108
+ process.exit(0);
109
+ return;
110
+ }
111
+
112
+ process.stdout.write(
113
+ `Tip: Use ${match.command} to ${match.description}.`
114
+ );
115
+ } catch (_e) {
116
+ // Best-effort: never block Claude Code
117
+ }
118
+ process.exit(0);
119
+ }
120
+
121
+ function readStdin(): Promise<string> {
122
+ return new Promise((resolve) => {
123
+ let data = '';
124
+ process.stdin.setEncoding('utf-8');
125
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
126
+ process.stdin.on('end', () => resolve(data));
127
+ setTimeout(() => resolve(data), 3000);
128
+ });
129
+ }
130
+
131
+ main();
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Massu. All rights reserved.
3
+ // Licensed under BSL 1.1 - see LICENSE file for details.
4
+
5
+ // ============================================================
6
+ // PostToolUse Context Hook
7
+ // Surfaces applicable CLAUDE.md rules and warnings when editing
8
+ // src/ files. Uses matchRules() and isInMiddlewareTree() from
9
+ // the codegraph index - no MCP server HTTP call needed.
10
+ // Must complete in <500ms.
11
+ // ============================================================
12
+
13
+ import Database from 'better-sqlite3';
14
+ import { matchRules } from '../rules.ts';
15
+ import { isInMiddlewareTree } from '../middleware-tree.ts';
16
+ import { getResolvedPaths, getProjectRoot } from '../config.ts';
17
+
18
+ interface HookInput {
19
+ session_id: string;
20
+ tool_name: string;
21
+ tool_input: { file_path?: string };
22
+ }
23
+
24
+ async function main(): Promise<void> {
25
+ try {
26
+ const input = await readStdin();
27
+ const hookInput = JSON.parse(input) as HookInput;
28
+ const filePath = hookInput.tool_input?.file_path;
29
+
30
+ if (!filePath) {
31
+ process.exit(0);
32
+ return;
33
+ }
34
+
35
+ // Convert absolute path to relative
36
+ const root = getProjectRoot();
37
+ const rel = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
38
+
39
+ // Only process src/ files
40
+ if (!rel.startsWith('src/')) {
41
+ process.exit(0);
42
+ return;
43
+ }
44
+
45
+ const warnings: string[] = [];
46
+
47
+ // 1. Check applicable rules (uses rules.ts PATTERN_RULES)
48
+ const rules = matchRules(rel);
49
+ for (const rule of rules) {
50
+ if (rule.severity === 'CRITICAL' || rule.severity === 'HIGH') {
51
+ for (const r of rule.rules) {
52
+ warnings.push(`[${rule.severity}] ${r}`);
53
+ }
54
+ }
55
+ }
56
+
57
+ // 2. Check middleware tree membership
58
+ try {
59
+ const dataDb = new Database(getResolvedPaths().dataDbPath, { readonly: true });
60
+ try {
61
+ if (isInMiddlewareTree(dataDb, rel)) {
62
+ warnings.push('[CRITICAL] This file is in the middleware import tree. No Node.js deps allowed.');
63
+ }
64
+ } finally {
65
+ dataDb.close();
66
+ }
67
+ } catch (_e) {
68
+ // DB may not exist yet - skip middleware check
69
+ }
70
+
71
+ // 3. Output warnings if any
72
+ if (warnings.length > 0) {
73
+ console.log(`[Massu] ${warnings.join(' | ')}`);
74
+ }
75
+ } catch (_e) {
76
+ // Best-effort: never block Claude Code
77
+ }
78
+ process.exit(0);
79
+ }
80
+
81
+ function readStdin(): Promise<string> {
82
+ return new Promise((resolve) => {
83
+ let data = '';
84
+ process.stdin.setEncoding('utf-8');
85
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
86
+ process.stdin.on('end', () => resolve(data));
87
+ setTimeout(() => resolve(data), 3000);
88
+ });
89
+ }
90
+
91
+ main();
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Massu. All rights reserved.
3
+ // Licensed under BSL 1.1 - see LICENSE file for details.
4
+
5
+ // ============================================================
6
+ // P3-002: PostToolUse Observation Hook
7
+ // Captures tool usage as observations (lightweight, no AI needed).
8
+ // Must complete in <500ms.
9
+ // ============================================================
10
+
11
+ import { getMemoryDb, addObservation, createSession, deduplicateFailedAttempt, addSummary } from '../memory-db.ts';
12
+ import { classifyRealTimeToolCall, detectPlanProgress } from '../observation-extractor.ts';
13
+ import { logAuditEntry } from '../audit-trail.ts';
14
+ import { trackModification } from '../regression-detector.ts';
15
+ import { validateFile, storeValidationResult } from '../validation-engine.ts';
16
+ import { scoreFileSecurity, storeSecurityScore } from '../security-scorer.ts';
17
+
18
+ interface HookInput {
19
+ session_id: string;
20
+ transcript_path: string;
21
+ cwd: string;
22
+ hook_event_name: string;
23
+ tool_name: string;
24
+ tool_input: Record<string, unknown>;
25
+ tool_response: string;
26
+ }
27
+
28
+ // In-memory dedup for Read calls within this session
29
+ const seenReads = new Set<string>();
30
+ let currentSessionId: string | null = null;
31
+
32
+ async function main(): Promise<void> {
33
+ try {
34
+ const input = await readStdin();
35
+ const hookInput = JSON.parse(input) as HookInput;
36
+ const { session_id, tool_name, tool_input, tool_response } = hookInput;
37
+
38
+ // Reset seen reads if session changed
39
+ if (currentSessionId !== session_id) {
40
+ seenReads.clear();
41
+ currentSessionId = session_id;
42
+ }
43
+
44
+ const db = getMemoryDb();
45
+ try {
46
+ // Ensure session exists
47
+ createSession(db, session_id);
48
+
49
+ // Classify and filter
50
+ const observation = classifyRealTimeToolCall(tool_name, tool_input, tool_response, seenReads);
51
+ if (!observation) {
52
+ process.exit(0);
53
+ return;
54
+ }
55
+
56
+ // Deduplicate failed attempts
57
+ if (observation.type === 'failed_attempt') {
58
+ deduplicateFailedAttempt(db, session_id, observation.title, observation.detail, observation.opts);
59
+ } else {
60
+ addObservation(db, session_id, observation.type, observation.title, observation.detail, observation.opts);
61
+ }
62
+
63
+ // Auto-detect plan progress
64
+ if (tool_response) {
65
+ const progress = detectPlanProgress(tool_response);
66
+ if (progress.length > 0) {
67
+ // Update plan_progress in session summary
68
+ updatePlanProgress(db, session_id, progress);
69
+ }
70
+ }
71
+
72
+ // Audit trail logging for file changes
73
+ try {
74
+ if (tool_name === 'Edit' || tool_name === 'Write') {
75
+ const filePath = (tool_input.file_path as string) ?? '';
76
+ logAuditEntry(db, {
77
+ sessionId: session_id,
78
+ eventType: 'code_change',
79
+ actor: 'ai',
80
+ filePath,
81
+ changeType: tool_name === 'Write' ? 'create' : 'edit',
82
+ });
83
+
84
+ // Track modification for regression detection
85
+ if (filePath) {
86
+ const featureMatch = filePath.match(/(?:routers|components|app\/\(([^)]+)\))\/([^/.]+)/);
87
+ if (featureMatch) {
88
+ const featureKey = featureMatch[1] ?? featureMatch[2];
89
+ trackModification(db, featureKey);
90
+ }
91
+ }
92
+ }
93
+ } catch (_auditErr) {
94
+ // Best-effort: never block post-tool-use
95
+ }
96
+
97
+ // Real-time validation for Edit/Write
98
+ try {
99
+ if (tool_name === 'Edit' || tool_name === 'Write') {
100
+ const filePath = (tool_input.file_path as string) ?? '';
101
+ if (filePath && (filePath.endsWith('.ts') || filePath.endsWith('.tsx'))) {
102
+ const projectRoot = hookInput.cwd;
103
+ const checks = validateFile(filePath, projectRoot);
104
+ const violations = checks.filter(c => c.severity === 'error' || c.severity === 'critical');
105
+ if (violations.length > 0) {
106
+ storeValidationResult(db, filePath, checks, session_id);
107
+ }
108
+ }
109
+ }
110
+ } catch (_validationErr) {
111
+ // Best-effort: never block post-tool-use
112
+ }
113
+
114
+ // Auto-security scoring for router/API files
115
+ try {
116
+ if (tool_name === 'Edit' || tool_name === 'Write') {
117
+ const filePath = (tool_input.file_path as string) ?? '';
118
+ if (filePath && (filePath.includes('routers/') || filePath.includes('api/'))) {
119
+ const projectRoot = hookInput.cwd;
120
+ const { riskScore, findings } = scoreFileSecurity(filePath, projectRoot);
121
+ if (findings.length > 0) {
122
+ storeSecurityScore(db, session_id, filePath, riskScore, findings);
123
+ }
124
+ }
125
+ }
126
+ } catch (_securityErr) {
127
+ // Best-effort: never block post-tool-use
128
+ }
129
+ } finally {
130
+ db.close();
131
+ }
132
+ } catch (_e) {
133
+ // Best-effort: never block Claude Code
134
+ }
135
+ process.exit(0);
136
+ }
137
+
138
+ function updatePlanProgress(db: import('better-sqlite3').Database, sessionId: string, progress: Array<{ planItem: string; status: string }>): void {
139
+ // Get or create latest summary's plan_progress
140
+ const existing = db.prepare(
141
+ 'SELECT id, plan_progress FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1'
142
+ ).get(sessionId) as { id: number; plan_progress: string } | undefined;
143
+
144
+ if (existing) {
145
+ try {
146
+ const currentProgress = JSON.parse(existing.plan_progress) as Record<string, string>;
147
+ for (const p of progress) {
148
+ currentProgress[p.planItem] = p.status;
149
+ }
150
+ db.prepare('UPDATE session_summaries SET plan_progress = ? WHERE id = ?')
151
+ .run(JSON.stringify(currentProgress), existing.id);
152
+ } catch (_e) {
153
+ // Skip if JSON parse fails
154
+ }
155
+ } else {
156
+ // Create a minimal summary with plan progress
157
+ const progressMap: Record<string, string> = {};
158
+ for (const p of progress) {
159
+ progressMap[p.planItem] = p.status;
160
+ }
161
+ addSummary(db, sessionId, { planProgress: progressMap });
162
+ }
163
+ }
164
+
165
+ function readStdin(): Promise<string> {
166
+ return new Promise((resolve) => {
167
+ let data = '';
168
+ process.stdin.setEncoding('utf-8');
169
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
170
+ process.stdin.on('end', () => resolve(data));
171
+ setTimeout(() => resolve(data), 3000);
172
+ });
173
+ }
174
+
175
+ main();
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Massu. All rights reserved.
3
+ // Licensed under BSL 1.1 - see LICENSE file for details.
4
+
5
+ // ============================================================
6
+ // P3-006: PreCompact State Snapshot Hook
7
+ // Captures current session state into DB before compaction destroys context.
8
+ // ============================================================
9
+
10
+ import { getMemoryDb, addSummary, createSession } from '../memory-db.ts';
11
+ import { logAuditEntry } from '../audit-trail.ts';
12
+ import type { SessionSummary } from '../memory-db.ts';
13
+
14
+ interface HookInput {
15
+ session_id: string;
16
+ transcript_path: string;
17
+ cwd: string;
18
+ hook_event_name: string;
19
+ }
20
+
21
+ async function main(): Promise<void> {
22
+ try {
23
+ const input = await readStdin();
24
+ const hookInput = JSON.parse(input) as HookInput;
25
+ const { session_id } = hookInput;
26
+
27
+ const db = getMemoryDb();
28
+ try {
29
+ // Ensure session exists
30
+ createSession(db, session_id);
31
+
32
+ // 1. Get all observations for this session
33
+ const observations = db.prepare(
34
+ 'SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC'
35
+ ).all(session_id) as Array<Record<string, unknown>>;
36
+
37
+ // 2. Get user prompts
38
+ const prompts = db.prepare(
39
+ 'SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC'
40
+ ).all(session_id) as Array<{ prompt_text: string }>;
41
+
42
+ // 3. Generate mid-session summary snapshot
43
+ const summary = buildSnapshotSummary(observations, prompts);
44
+
45
+ // 4. Store with pre_compact marker in plan_progress
46
+ addSummary(db, session_id, summary);
47
+
48
+ // Log compaction event for audit trail continuity
49
+ try {
50
+ logAuditEntry(db, {
51
+ sessionId: session_id,
52
+ eventType: 'compaction',
53
+ actor: 'hook',
54
+ metadata: { observations_count: observations.length, prompts_count: prompts.length },
55
+ });
56
+ } catch (_auditErr) {
57
+ // Best-effort: never block compaction
58
+ }
59
+ } finally {
60
+ db.close();
61
+ }
62
+ } catch (_e) {
63
+ // Best-effort: never block Claude Code
64
+ }
65
+ process.exit(0);
66
+ }
67
+
68
+ function buildSnapshotSummary(
69
+ observations: Array<Record<string, unknown>>,
70
+ prompts: Array<{ prompt_text: string }>
71
+ ): SessionSummary {
72
+ const request = prompts[0]?.prompt_text?.slice(0, 500) ?? undefined;
73
+
74
+ const completed = observations
75
+ .filter(o => ['feature', 'bugfix', 'refactor'].includes(o.type as string))
76
+ .map(o => `- ${o.title}`)
77
+ .join('\n');
78
+
79
+ const failedAttempts = observations
80
+ .filter(o => o.type === 'failed_attempt')
81
+ .map(o => `- ${o.title}`)
82
+ .join('\n');
83
+
84
+ const decisions = observations
85
+ .filter(o => o.type === 'decision')
86
+ .map(o => `- ${o.title}`)
87
+ .join('\n');
88
+
89
+ // Collect file changes
90
+ const filesCreated: string[] = [];
91
+ const filesModified: string[] = [];
92
+ for (const o of observations) {
93
+ if (o.type !== 'file_change') continue;
94
+ const files = safeParseJson(o.files_involved as string, []) as string[];
95
+ const title = o.title as string;
96
+ if (title.startsWith('Created')) filesCreated.push(...files);
97
+ else if (title.startsWith('Edited')) filesModified.push(...files);
98
+ }
99
+
100
+ // Collect plan progress
101
+ const planProgress: Record<string, string> = { snapshot_type: 'pre_compact' };
102
+ for (const o of observations) {
103
+ if (!o.plan_item) continue;
104
+ planProgress[o.plan_item as string] = 'in_progress';
105
+ }
106
+
107
+ // Verification results
108
+ const verificationResults: Record<string, string> = {};
109
+ for (const o of observations) {
110
+ if (o.type !== 'vr_check') continue;
111
+ const vrType = o.vr_type as string;
112
+ const passed = (o.title as string).includes('PASS');
113
+ if (vrType) verificationResults[vrType] = passed ? 'PASS' : 'FAIL';
114
+ }
115
+
116
+ return {
117
+ request,
118
+ completed: completed || undefined,
119
+ failedAttempts: failedAttempts || undefined,
120
+ decisions: decisions || undefined,
121
+ filesCreated: [...new Set(filesCreated)],
122
+ filesModified: [...new Set(filesModified)],
123
+ verificationResults,
124
+ planProgress,
125
+ };
126
+ }
127
+
128
+ function safeParseJson(json: string, fallback: unknown): unknown {
129
+ try {
130
+ return JSON.parse(json);
131
+ } catch (_e) {
132
+ return fallback;
133
+ }
134
+ }
135
+
136
+ function readStdin(): Promise<string> {
137
+ return new Promise((resolve) => {
138
+ let data = '';
139
+ process.stdin.setEncoding('utf-8');
140
+ process.stdin.on('data', (chunk: string) => { data += chunk; });
141
+ process.stdin.on('end', () => resolve(data));
142
+ setTimeout(() => resolve(data), 3000);
143
+ });
144
+ }
145
+
146
+ main();