@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
package/src/server.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
* Massu MCP Server
|
|
7
|
+
*
|
|
8
|
+
* An MCP server that provides project-specific intelligence on top of
|
|
9
|
+
* vanilla CodeGraph. Communicates via JSON-RPC 2.0 over stdio.
|
|
10
|
+
*
|
|
11
|
+
* Tool names are configurable via massu.config.yaml toolPrefix.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getCodeGraphDb, getDataDb } from './db.ts';
|
|
15
|
+
import { getConfig } from './config.ts';
|
|
16
|
+
import { getToolDefinitions, handleToolCall } from './tools.ts';
|
|
17
|
+
import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
|
|
18
|
+
|
|
19
|
+
interface JsonRpcRequest {
|
|
20
|
+
jsonrpc: '2.0';
|
|
21
|
+
id?: number | string;
|
|
22
|
+
method: string;
|
|
23
|
+
params?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JsonRpcResponse {
|
|
27
|
+
jsonrpc: '2.0';
|
|
28
|
+
id: number | string | null;
|
|
29
|
+
result?: unknown;
|
|
30
|
+
error?: { code: number; message: string; data?: unknown };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Server state
|
|
34
|
+
let codegraphDb: ReturnType<typeof getCodeGraphDb> | null = null;
|
|
35
|
+
let dataDb: ReturnType<typeof getDataDb> | null = null;
|
|
36
|
+
|
|
37
|
+
function getDb() {
|
|
38
|
+
if (!codegraphDb) codegraphDb = getCodeGraphDb();
|
|
39
|
+
if (!dataDb) dataDb = getDataDb();
|
|
40
|
+
return { codegraphDb, dataDb: dataDb };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
|
|
44
|
+
const { method, params, id } = request;
|
|
45
|
+
|
|
46
|
+
switch (method) {
|
|
47
|
+
case 'initialize': {
|
|
48
|
+
return {
|
|
49
|
+
jsonrpc: '2.0',
|
|
50
|
+
id: id ?? null,
|
|
51
|
+
result: {
|
|
52
|
+
protocolVersion: '2024-11-05',
|
|
53
|
+
capabilities: {
|
|
54
|
+
tools: {},
|
|
55
|
+
},
|
|
56
|
+
serverInfo: {
|
|
57
|
+
name: 'massu',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case 'notifications/initialized': {
|
|
65
|
+
// Client acknowledges initialization - no response needed for notifications
|
|
66
|
+
return { jsonrpc: '2.0', id: id ?? null, result: {} };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'tools/list': {
|
|
70
|
+
const tools = getToolDefinitions();
|
|
71
|
+
return {
|
|
72
|
+
jsonrpc: '2.0',
|
|
73
|
+
id: id ?? null,
|
|
74
|
+
result: { tools },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'tools/call': {
|
|
79
|
+
const toolName = (params as { name: string })?.name;
|
|
80
|
+
const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
|
|
81
|
+
|
|
82
|
+
const { codegraphDb: cgDb, dataDb: lDb } = getDb();
|
|
83
|
+
const result = handleToolCall(toolName, toolArgs, lDb, cgDb);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
jsonrpc: '2.0',
|
|
87
|
+
id: id ?? null,
|
|
88
|
+
result,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'ping': {
|
|
93
|
+
return { jsonrpc: '2.0', id: id ?? null, result: {} };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
default: {
|
|
97
|
+
return {
|
|
98
|
+
jsonrpc: '2.0',
|
|
99
|
+
id: id ?? null,
|
|
100
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// === Startup: prune stale memory data (non-blocking) ===
|
|
107
|
+
|
|
108
|
+
function pruneMemoryOnStartup(): void {
|
|
109
|
+
try {
|
|
110
|
+
const memDb = getMemoryDb();
|
|
111
|
+
try {
|
|
112
|
+
const turns = pruneOldConversationTurns(memDb, 7);
|
|
113
|
+
const obsDeleted = pruneOldObservations(memDb, 90);
|
|
114
|
+
|
|
115
|
+
const totalPruned = turns.turnsDeleted + turns.detailsDeleted + obsDeleted;
|
|
116
|
+
if (totalPruned > 0) {
|
|
117
|
+
process.stderr.write(
|
|
118
|
+
`massu: Pruned memory DB on startup — ` +
|
|
119
|
+
`${turns.turnsDeleted} conversation turns, ` +
|
|
120
|
+
`${turns.detailsDeleted} tool call details (>7d), ` +
|
|
121
|
+
`${obsDeleted} observations (>90d)\n`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
memDb.close();
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`massu: Memory pruning failed (non-fatal): ${error instanceof Error ? error.message : String(error)}\n`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
pruneMemoryOnStartup();
|
|
135
|
+
|
|
136
|
+
// === stdio JSON-RPC transport ===
|
|
137
|
+
|
|
138
|
+
let buffer = '';
|
|
139
|
+
|
|
140
|
+
process.stdin.setEncoding('utf-8');
|
|
141
|
+
process.stdin.on('data', (chunk: string) => {
|
|
142
|
+
buffer += chunk;
|
|
143
|
+
|
|
144
|
+
// Process complete messages (newline-delimited JSON-RPC)
|
|
145
|
+
let newlineIndex: number;
|
|
146
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
147
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
148
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
149
|
+
|
|
150
|
+
if (!line) continue;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const request = JSON.parse(line) as JsonRpcRequest;
|
|
154
|
+
const response = handleRequest(request);
|
|
155
|
+
|
|
156
|
+
// Don't send responses for notifications (no id)
|
|
157
|
+
if (request.id !== undefined) {
|
|
158
|
+
const responseStr = JSON.stringify(response);
|
|
159
|
+
process.stdout.write(responseStr + '\n');
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const errorResponse: JsonRpcResponse = {
|
|
163
|
+
jsonrpc: '2.0',
|
|
164
|
+
id: null,
|
|
165
|
+
error: {
|
|
166
|
+
code: -32700,
|
|
167
|
+
message: `Parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
process.stdout.write(JSON.stringify(errorResponse) + '\n');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
process.stdin.on('end', () => {
|
|
176
|
+
// Clean up database connections
|
|
177
|
+
if (codegraphDb) codegraphDb.close();
|
|
178
|
+
if (dataDb) dataDb.close();
|
|
179
|
+
process.exit(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Handle errors gracefully
|
|
183
|
+
process.on('uncaughtException', (error) => {
|
|
184
|
+
process.stderr.write(`massu: Uncaught exception: ${error.message}\n`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
process.on('unhandledRejection', (reason) => {
|
|
188
|
+
process.stderr.write(`massu: Unhandled rejection: ${reason}\n`);
|
|
189
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import type Database from 'better-sqlite3';
|
|
7
|
+
import { generateCurrentMd } from './session-state-generator.ts';
|
|
8
|
+
import { getProjectRoot } from './config.ts';
|
|
9
|
+
|
|
10
|
+
// ============================================================
|
|
11
|
+
// P5-002: Session Archiver
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Archive the current CURRENT.md and generate a new one from memory DB.
|
|
18
|
+
*/
|
|
19
|
+
export function archiveAndRegenerate(db: Database.Database, sessionId: string): {
|
|
20
|
+
archived: boolean;
|
|
21
|
+
archivePath?: string;
|
|
22
|
+
newContent: string;
|
|
23
|
+
} {
|
|
24
|
+
const currentMdPath = resolve(PROJECT_ROOT, '.claude/session-state/CURRENT.md');
|
|
25
|
+
const archiveDir = resolve(PROJECT_ROOT, '.claude/session-state/archive');
|
|
26
|
+
let archived = false;
|
|
27
|
+
let archivePath: string | undefined;
|
|
28
|
+
|
|
29
|
+
// 1. Archive existing CURRENT.md if it exists and has content
|
|
30
|
+
if (existsSync(currentMdPath)) {
|
|
31
|
+
const existingContent = readFileSync(currentMdPath, 'utf-8');
|
|
32
|
+
if (existingContent.trim().length > 10) {
|
|
33
|
+
// Extract date and task description for filename
|
|
34
|
+
const { date, slug } = extractArchiveInfo(existingContent);
|
|
35
|
+
archivePath = resolve(archiveDir, `${date}-${slug}.md`);
|
|
36
|
+
|
|
37
|
+
// Ensure archive directory exists
|
|
38
|
+
if (!existsSync(archiveDir)) {
|
|
39
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Move to archive (rename is atomic on same filesystem)
|
|
43
|
+
try {
|
|
44
|
+
renameSync(currentMdPath, archivePath);
|
|
45
|
+
archived = true;
|
|
46
|
+
} catch (_e) {
|
|
47
|
+
// If rename fails (cross-device), copy+delete
|
|
48
|
+
writeFileSync(archivePath, existingContent);
|
|
49
|
+
archived = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Generate new CURRENT.md from memory DB
|
|
55
|
+
const newContent = generateCurrentMd(db, sessionId);
|
|
56
|
+
|
|
57
|
+
// 3. Write new CURRENT.md
|
|
58
|
+
const dir = dirname(currentMdPath);
|
|
59
|
+
if (!existsSync(dir)) {
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(currentMdPath, newContent, 'utf-8');
|
|
63
|
+
|
|
64
|
+
return { archived, archivePath, newContent };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract date and slug from existing CURRENT.md content for archive naming.
|
|
69
|
+
*/
|
|
70
|
+
function extractArchiveInfo(content: string): { date: string; slug: string } {
|
|
71
|
+
// Try to extract date from "# Session State - January 30, 2026"
|
|
72
|
+
const dateMatch = content.match(/# Session State - (\w+ \d+, \d+)/);
|
|
73
|
+
let date = new Date().toISOString().split('T')[0]; // fallback
|
|
74
|
+
|
|
75
|
+
if (dateMatch) {
|
|
76
|
+
const parsed = new Date(dateMatch[1]);
|
|
77
|
+
if (!isNaN(parsed.getTime())) {
|
|
78
|
+
date = parsed.toISOString().split('T')[0];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Also try ISO date format "**Last Updated**: 2026-01-30"
|
|
83
|
+
const isoMatch = content.match(/(\d{4}-\d{2}-\d{2})/);
|
|
84
|
+
if (isoMatch) {
|
|
85
|
+
date = isoMatch[1];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract task description for slug
|
|
89
|
+
let slug = 'session';
|
|
90
|
+
const taskMatch = content.match(/\*\*Task\*\*:\s*(.+)/);
|
|
91
|
+
if (taskMatch) {
|
|
92
|
+
slug = taskMatch[1]
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
95
|
+
.replace(/^-|-$/g, '')
|
|
96
|
+
.slice(0, 50);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Extract status for slug if no task
|
|
100
|
+
if (slug === 'session') {
|
|
101
|
+
const statusMatch = content.match(/\*\*Status\*\*:\s*\w+\s*-\s*(.+)/);
|
|
102
|
+
if (statusMatch) {
|
|
103
|
+
slug = statusMatch[1]
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
106
|
+
.replace(/^-|-$/g, '')
|
|
107
|
+
.slice(0, 50);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { date, slug };
|
|
112
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import type Database from 'better-sqlite3';
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// P5-001: CURRENT.md Generator
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate CURRENT.md content from memory database.
|
|
12
|
+
* Replaces manual session state maintenance.
|
|
13
|
+
*/
|
|
14
|
+
export function generateCurrentMd(db: Database.Database, sessionId: string): string {
|
|
15
|
+
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as Record<string, unknown> | undefined;
|
|
16
|
+
if (!session) return '# Session State\n\nNo active session found.\n';
|
|
17
|
+
|
|
18
|
+
const observations = db.prepare(
|
|
19
|
+
'SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC'
|
|
20
|
+
).all(sessionId) as Array<Record<string, unknown>>;
|
|
21
|
+
|
|
22
|
+
const summary = db.prepare(
|
|
23
|
+
'SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1'
|
|
24
|
+
).get(sessionId) as Record<string, unknown> | undefined;
|
|
25
|
+
|
|
26
|
+
const prompts = db.prepare(
|
|
27
|
+
'SELECT prompt_text FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC LIMIT 1'
|
|
28
|
+
).all(sessionId) as Array<{ prompt_text: string }>;
|
|
29
|
+
|
|
30
|
+
const date = new Date().toISOString().split('T')[0];
|
|
31
|
+
const firstPrompt = prompts[0]?.prompt_text ?? 'Unknown task';
|
|
32
|
+
const taskSummary = firstPrompt.slice(0, 100).replace(/\n/g, ' ');
|
|
33
|
+
|
|
34
|
+
const lines: string[] = [];
|
|
35
|
+
lines.push(`# Session State - ${formatDate(date)}`);
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push(`**Last Updated**: ${new Date().toISOString().replace('T', ' ').slice(0, 19)} (auto-generated from massu-memory)`);
|
|
38
|
+
lines.push(`**Status**: ${session.status === 'active' ? 'IN PROGRESS' : (session.status as string).toUpperCase()} - ${taskSummary}`);
|
|
39
|
+
lines.push(`**Task**: ${taskSummary}`);
|
|
40
|
+
lines.push(`**Session ID**: ${sessionId}`);
|
|
41
|
+
lines.push(`**Branch**: ${session.git_branch ?? 'unknown'}`);
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push('---');
|
|
44
|
+
lines.push('');
|
|
45
|
+
|
|
46
|
+
// Completed work
|
|
47
|
+
const completedObs = observations.filter(o =>
|
|
48
|
+
['feature', 'bugfix', 'refactor', 'file_change'].includes(o.type as string)
|
|
49
|
+
);
|
|
50
|
+
if (completedObs.length > 0 || summary) {
|
|
51
|
+
lines.push('## COMPLETED WORK');
|
|
52
|
+
lines.push('');
|
|
53
|
+
|
|
54
|
+
if (summary?.completed) {
|
|
55
|
+
lines.push(summary.completed as string);
|
|
56
|
+
lines.push('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Files created
|
|
60
|
+
const filesCreated = observations
|
|
61
|
+
.filter(o => o.type === 'file_change' && (o.title as string).startsWith('Created'))
|
|
62
|
+
.map(o => {
|
|
63
|
+
const files = safeParseJson(o.files_involved as string, []) as string[];
|
|
64
|
+
return files[0] ?? (o.title as string).replace('Created/wrote: ', '');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (filesCreated.length > 0) {
|
|
68
|
+
lines.push('### Files Created');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push('| File | Purpose |');
|
|
71
|
+
lines.push('|------|---------|');
|
|
72
|
+
for (const f of filesCreated) {
|
|
73
|
+
lines.push(`| \`${f}\` | |`);
|
|
74
|
+
}
|
|
75
|
+
lines.push('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Files modified
|
|
79
|
+
const filesModified = observations
|
|
80
|
+
.filter(o => o.type === 'file_change' && (o.title as string).startsWith('Edited'))
|
|
81
|
+
.map(o => {
|
|
82
|
+
const files = safeParseJson(o.files_involved as string, []) as string[];
|
|
83
|
+
return files[0] ?? (o.title as string).replace('Edited: ', '');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (filesModified.length > 0) {
|
|
87
|
+
lines.push('### Files Modified');
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push('| File | Change |');
|
|
90
|
+
lines.push('|------|--------|');
|
|
91
|
+
for (const f of [...new Set(filesModified)]) {
|
|
92
|
+
lines.push(`| \`${f}\` | |`);
|
|
93
|
+
}
|
|
94
|
+
lines.push('');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Key decisions
|
|
99
|
+
const decisions = observations.filter(o => o.type === 'decision');
|
|
100
|
+
if (decisions.length > 0) {
|
|
101
|
+
lines.push('### Key Decisions');
|
|
102
|
+
lines.push('');
|
|
103
|
+
for (const d of decisions) {
|
|
104
|
+
lines.push(`- ${d.title}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Failed attempts
|
|
110
|
+
const failures = observations.filter(o => o.type === 'failed_attempt');
|
|
111
|
+
if (failures.length > 0) {
|
|
112
|
+
lines.push('## FAILED ATTEMPTS (DO NOT RETRY)');
|
|
113
|
+
lines.push('');
|
|
114
|
+
for (const f of failures) {
|
|
115
|
+
lines.push(`- ${f.title}`);
|
|
116
|
+
if (f.detail) lines.push(` ${(f.detail as string).slice(0, 200)}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Verification evidence
|
|
122
|
+
const vrChecks = observations.filter(o => o.type === 'vr_check');
|
|
123
|
+
if (vrChecks.length > 0) {
|
|
124
|
+
lines.push('## VERIFICATION EVIDENCE');
|
|
125
|
+
lines.push('');
|
|
126
|
+
for (const v of vrChecks) {
|
|
127
|
+
lines.push(`- ${v.title}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Pending / next steps
|
|
133
|
+
if (summary?.next_steps) {
|
|
134
|
+
lines.push('## PENDING');
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push(summary.next_steps as string);
|
|
137
|
+
lines.push('');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Plan document
|
|
141
|
+
if (session.plan_file) {
|
|
142
|
+
lines.push('## PLAN DOCUMENT');
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push(`\`${session.plan_file}\``);
|
|
145
|
+
|
|
146
|
+
// Show plan progress if available
|
|
147
|
+
if (summary?.plan_progress) {
|
|
148
|
+
const progress = safeParseJson(summary.plan_progress as string, {}) as Record<string, string>;
|
|
149
|
+
const total = Object.keys(progress).length;
|
|
150
|
+
const complete = Object.values(progress).filter(v => v === 'complete').length;
|
|
151
|
+
if (total > 0) {
|
|
152
|
+
lines.push(`- Progress: ${complete}/${total} items complete`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
lines.push('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatDate(dateStr: string): string {
|
|
162
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
163
|
+
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
164
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
165
|
+
return `${months[month - 1]} ${day}, ${year}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function safeParseJson(json: string, fallback: unknown): unknown {
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(json);
|
|
171
|
+
} catch (_e) {
|
|
172
|
+
return fallback;
|
|
173
|
+
}
|
|
174
|
+
}
|