@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.
- package/LICENSE +71 -0
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +149 -11527
- package/dist/hooks/post-edit-context.js +127 -11493
- package/dist/hooks/post-tool-use.js +169 -11550
- package/dist/hooks/pre-compact.js +149 -11530
- package/dist/hooks/pre-delete-check.js +144 -11523
- package/dist/hooks/quality-event.js +149 -11527
- package/dist/hooks/session-end.js +188 -11570
- package/dist/hooks/session-start.js +159 -11534
- package/dist/hooks/user-prompt.js +149 -11530
- package/package.json +14 -19
- package/src/adr-generator.ts +292 -0
- package/src/analytics.ts +373 -0
- package/src/audit-trail.ts +450 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +190 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +395 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/cost-tracker.ts +355 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +337 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +517 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +1376 -0
- package/src/memory-tools.ts +391 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +343 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +332 -0
- package/src/regression-detector.ts +319 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +405 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +578 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +512 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +189 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +407 -0
- package/src/tools.ts +847 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +106 -0
- package/src/validation-engine.ts +358 -0
- package/dist/cli.js +0 -7890
- 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();
|