@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.5
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/bin/copilot-session-viewer +2 -2
- package/dist/server.min.js +99 -0
- package/package.json +5 -17
- package/public/vendor/marked.umd.min.js +8 -0
- package/public/vendor/purify.min.js +3 -0
- package/public/vendor/vue-virtual-scroller.css +1 -0
- package/public/vendor/vue-virtual-scroller.min.js +2 -0
- package/public/vendor/vue.global.prod.min.js +19 -0
- package/views/session-vue.ejs +5 -5
- package/views/time-analyze.ejs +2 -2
- package/lib/parsers/README.md +0 -239
- package/lib/parsers/base-parser.js +0 -53
- package/lib/parsers/claude-parser.js +0 -181
- package/lib/parsers/copilot-parser.js +0 -143
- package/lib/parsers/index.js +0 -15
- package/lib/parsers/parser-factory.js +0 -77
- package/lib/parsers/pi-mono-parser.js +0 -119
- package/lib/parsers/vscode-parser.js +0 -591
- package/server.js +0 -29
- package/src/app.js +0 -129
- package/src/config/index.js +0 -27
- package/src/controllers/insightController.js +0 -136
- package/src/controllers/sessionController.js +0 -449
- package/src/controllers/tagController.js +0 -113
- package/src/controllers/uploadController.js +0 -648
- package/src/middleware/common.js +0 -67
- package/src/middleware/rateLimiting.js +0 -62
- package/src/models/Session.js +0 -146
- package/src/routes/api.js +0 -11
- package/src/routes/insights.js +0 -12
- package/src/routes/pages.js +0 -12
- package/src/routes/uploads.js +0 -14
- package/src/schemas/event.schema.js +0 -73
- package/src/services/eventNormalizer.js +0 -291
- package/src/services/insightService.js +0 -535
- package/src/services/sessionRepository.js +0 -1092
- package/src/services/sessionService.js +0 -1919
- package/src/services/tagService.js +0 -205
- package/src/telemetry.js +0 -152
- package/src/utils/fileUtils.js +0 -305
- package/src/utils/helpers.js +0 -45
- package/src/utils/processManager.js +0 -85
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Insight Generation Service
|
|
3
|
-
* Handles session insight report generation with atomic operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs').promises;
|
|
7
|
-
const fsSync = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const { spawn } = require('child_process');
|
|
11
|
-
const config = require('../config');
|
|
12
|
-
const processManager = require('../utils/processManager');
|
|
13
|
-
|
|
14
|
-
class InsightService {
|
|
15
|
-
constructor() {
|
|
16
|
-
// No longer needs session directories - paths are passed directly
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Get CLI tool configuration based on session source
|
|
21
|
-
* @private
|
|
22
|
-
*/
|
|
23
|
-
_getToolConfig(source, sessionPath) {
|
|
24
|
-
const configs = {
|
|
25
|
-
copilot: {
|
|
26
|
-
name: 'Copilot',
|
|
27
|
-
cli: 'copilot',
|
|
28
|
-
args: (tmpDir, prompt) => ['--config-dir', tmpDir, '--yolo', '-p', prompt],
|
|
29
|
-
cwd: sessionPath
|
|
30
|
-
},
|
|
31
|
-
vscode: {
|
|
32
|
-
name: 'Copilot',
|
|
33
|
-
cli: 'copilot',
|
|
34
|
-
args: (tmpDir, prompt) => ['--config-dir', tmpDir, '--yolo', '-p', prompt],
|
|
35
|
-
cwd: sessionPath
|
|
36
|
-
},
|
|
37
|
-
claude: {
|
|
38
|
-
name: 'Claude Code',
|
|
39
|
-
cli: 'claude',
|
|
40
|
-
args: (_tmpDir, prompt) => ['-p', prompt, '--dangerously-skip-permissions'],
|
|
41
|
-
cwd: sessionPath
|
|
42
|
-
},
|
|
43
|
-
'pi-mono': {
|
|
44
|
-
name: 'Pi',
|
|
45
|
-
cli: 'pi',
|
|
46
|
-
args: (_tmpDir, prompt) => ['-p', prompt],
|
|
47
|
-
cwd: sessionPath
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return configs[source] || configs.copilot; // fallback to copilot
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Generate or retrieve insight report
|
|
56
|
-
* @param {string} sessionId - Session ID
|
|
57
|
-
* @param {string} sessionPath - Full path to session directory
|
|
58
|
-
* @param {string} source - Session source: 'copilot', 'claude', or 'pi-mono'
|
|
59
|
-
* @param {boolean} forceRegenerate - Force new generation
|
|
60
|
-
* @returns {Promise<Object>} Insight status and report
|
|
61
|
-
*/
|
|
62
|
-
async generateInsight(sessionId, sessionPath, source = 'copilot', forceRegenerate = false) {
|
|
63
|
-
// Use per-session insight file to avoid collisions in shared directories
|
|
64
|
-
const insightFile = path.join(sessionPath, `${sessionId}.agent-review.md`);
|
|
65
|
-
const lockFile = path.join(sessionPath, `${sessionId}.agent-review.md.lock`);
|
|
66
|
-
|
|
67
|
-
// Determine events file location based on directory structure
|
|
68
|
-
// Try standard events.jsonl first, then <sessionId>.jsonl (for file-type sessions),
|
|
69
|
-
// finally *_<sessionId>.jsonl (for Pi-Mono timestamped sessions)
|
|
70
|
-
let eventsFile = path.join(sessionPath, 'events.jsonl');
|
|
71
|
-
try {
|
|
72
|
-
await fs.access(eventsFile);
|
|
73
|
-
} catch {
|
|
74
|
-
// Try <sessionId>.jsonl (common for Claude file-type sessions)
|
|
75
|
-
try {
|
|
76
|
-
eventsFile = path.join(sessionPath, `${sessionId}.jsonl`);
|
|
77
|
-
await fs.access(eventsFile);
|
|
78
|
-
} catch {
|
|
79
|
-
// Try *_<sessionId>.jsonl (Pi-Mono format: YYYY-MM-DDTHH-mm-ss-SSSZ_<uuid>.jsonl)
|
|
80
|
-
const entries = await fs.readdir(sessionPath);
|
|
81
|
-
const piFile = entries.find(f => f.endsWith(`_${sessionId}.jsonl`));
|
|
82
|
-
if (piFile) {
|
|
83
|
-
eventsFile = path.join(sessionPath, piFile);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const toolConfig = this._getToolConfig(source, sessionPath);
|
|
89
|
-
const toolName = toolConfig.name;
|
|
90
|
-
|
|
91
|
-
// Check if complete insight exists
|
|
92
|
-
if (!forceRegenerate) {
|
|
93
|
-
try {
|
|
94
|
-
const report = await fs.readFile(insightFile, 'utf-8');
|
|
95
|
-
const stats = await fs.stat(insightFile);
|
|
96
|
-
return {
|
|
97
|
-
status: 'completed',
|
|
98
|
-
report,
|
|
99
|
-
generatedAt: stats.mtime
|
|
100
|
-
};
|
|
101
|
-
} catch (_err) {
|
|
102
|
-
// File doesn't exist, continue to generation
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Check if generation is already in progress (atomic check)
|
|
107
|
-
try {
|
|
108
|
-
// Try to create lock file exclusively (fails if exists)
|
|
109
|
-
await fs.writeFile(lockFile, JSON.stringify({
|
|
110
|
-
sessionId,
|
|
111
|
-
startTime: new Date().toISOString(),
|
|
112
|
-
pid: process.pid
|
|
113
|
-
}), { flag: 'wx' });
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (err.code === 'EEXIST') {
|
|
116
|
-
// Another process is generating, check if it's stale
|
|
117
|
-
try {
|
|
118
|
-
const _lockData = JSON.parse(await fs.readFile(lockFile, 'utf-8'));
|
|
119
|
-
const lockStats = await fs.stat(lockFile);
|
|
120
|
-
const ageMs = Date.now() - lockStats.mtime.getTime();
|
|
121
|
-
|
|
122
|
-
if (ageMs < config.INSIGHT_TIMEOUT_MS) {
|
|
123
|
-
// Still valid, return generating status
|
|
124
|
-
return {
|
|
125
|
-
status: 'generating',
|
|
126
|
-
report: `# Generating ${toolName} Insight...\n\nAnother request is currently generating this insight. Please wait.`,
|
|
127
|
-
startedAt: lockStats.birthtime,
|
|
128
|
-
lastUpdate: lockStats.mtime,
|
|
129
|
-
ageMs: Date.now() - lockStats.birthtime.getTime()
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Stale lock, remove it
|
|
134
|
-
console.log(`⚠️ Removing stale lock file (${Math.floor(ageMs/1000)}s old)`);
|
|
135
|
-
await fs.unlink(lockFile);
|
|
136
|
-
|
|
137
|
-
// Retry lock creation
|
|
138
|
-
await fs.writeFile(lockFile, JSON.stringify({
|
|
139
|
-
sessionId,
|
|
140
|
-
startTime: new Date().toISOString(),
|
|
141
|
-
pid: process.pid
|
|
142
|
-
}), { flag: 'wx' });
|
|
143
|
-
} catch (_retryErr) {
|
|
144
|
-
throw new Error('Failed to acquire lock for insight generation', { cause: _retryErr });
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
throw err;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Check if events file exists
|
|
152
|
-
try {
|
|
153
|
-
await fs.access(eventsFile);
|
|
154
|
-
} catch (_err) {
|
|
155
|
-
await fs.unlink(lockFile);
|
|
156
|
-
throw new Error('Events file not found', { cause: _err });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Clean up old files if force regenerate
|
|
160
|
-
if (forceRegenerate) {
|
|
161
|
-
try {
|
|
162
|
-
await fs.unlink(insightFile);
|
|
163
|
-
} catch (_err) {
|
|
164
|
-
// File might not exist
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Start generation
|
|
169
|
-
await this._spawnAnalysisProcess(sessionPath, eventsFile, insightFile, lockFile, toolConfig);
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
status: 'generating',
|
|
173
|
-
report: `# Generating ${toolName} Insight...\n\nAnalysis in progress. Please wait.`,
|
|
174
|
-
startedAt: new Date()
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Spawn analysis process safely (no shell)
|
|
180
|
-
* @private
|
|
181
|
-
*/
|
|
182
|
-
async _spawnAnalysisProcess(sessionPath, eventsFile, insightFile, lockFile, toolConfig) {
|
|
183
|
-
const sessionId = path.basename(sessionPath); // Extract session ID from path
|
|
184
|
-
const tmpDir = path.join(os.tmpdir(), `agent-review-${sessionId}-${Date.now()}`);
|
|
185
|
-
await fs.mkdir(tmpDir, { recursive: true});
|
|
186
|
-
|
|
187
|
-
const prompt = this._buildPrompt(insightFile, eventsFile);
|
|
188
|
-
const outputFile = path.join(sessionPath, `${sessionId}.agent-review.md.tmp`);
|
|
189
|
-
|
|
190
|
-
// Spawn analysis tool directly (no shell)
|
|
191
|
-
const cliPath = toolConfig.cli;
|
|
192
|
-
const args = toolConfig.args(tmpDir, prompt);
|
|
193
|
-
|
|
194
|
-
console.log(`🤖 Starting ${toolConfig.name} analysis: ${cliPath} ${args.slice(0, 2).join(' ')}...`);
|
|
195
|
-
console.log(`📋 Args count: ${args.length}, prompt length: ${prompt.length} chars`);
|
|
196
|
-
|
|
197
|
-
// Use system PATH - CLI should be in the user's PATH
|
|
198
|
-
const analysisProcess = spawn(cliPath, args, {
|
|
199
|
-
env: { ...process.env },
|
|
200
|
-
cwd: sessionPath,
|
|
201
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Register for cleanup
|
|
205
|
-
processManager.register(analysisProcess, { name: `insight-${sessionId}` });
|
|
206
|
-
|
|
207
|
-
// Pipe events file to stdin (for tools that read from stdin like copilot)
|
|
208
|
-
// Claude Code and Pi read files directly, so they don't need stdin
|
|
209
|
-
if (toolConfig.cli === 'copilot') {
|
|
210
|
-
const eventsStream = fsSync.createReadStream(eventsFile);
|
|
211
|
-
// Handle EPIPE: if process exits before stdin is fully written, suppress the error
|
|
212
|
-
analysisProcess.stdin.on('error', (err) => {
|
|
213
|
-
if (err.code === 'EPIPE') {
|
|
214
|
-
eventsStream.destroy();
|
|
215
|
-
} else {
|
|
216
|
-
console.error('❌ stdin error:', err);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
eventsStream.pipe(analysisProcess.stdin);
|
|
220
|
-
} else {
|
|
221
|
-
// Close stdin for tools that don't need it
|
|
222
|
-
analysisProcess.stdin.end();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Capture output
|
|
226
|
-
const outputStream = fsSync.createWriteStream(outputFile);
|
|
227
|
-
analysisProcess.stdout.pipe(outputStream);
|
|
228
|
-
|
|
229
|
-
// Capture stderr with size limit
|
|
230
|
-
const stderrChunks = [];
|
|
231
|
-
let stderrSize = 0;
|
|
232
|
-
const MAX_STDERR = 64 * 1024; // 64KB cap
|
|
233
|
-
|
|
234
|
-
analysisProcess.stderr.on('data', (data) => {
|
|
235
|
-
if (stderrSize < MAX_STDERR) {
|
|
236
|
-
stderrChunks.push(data);
|
|
237
|
-
stderrSize += data.length;
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
analysisProcess.on('close', async (code) => {
|
|
242
|
-
try {
|
|
243
|
-
outputStream.end();
|
|
244
|
-
|
|
245
|
-
const stderr = Buffer.concat(stderrChunks).toString('utf-8').slice(0, MAX_STDERR);
|
|
246
|
-
console.log(`📋 ${toolConfig.name} process exited with code ${code}`);
|
|
247
|
-
if (stderr) {
|
|
248
|
-
console.log(`📋 ${toolConfig.name} stderr:`, stderr.substring(0, 500));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (code !== 0) {
|
|
252
|
-
console.error(`❌ ${toolConfig.name} CLI failed (code ${code}):`, stderr);
|
|
253
|
-
await fs.writeFile(insightFile,
|
|
254
|
-
`# ❌ Generation Failed\n\nExit code: ${code}\n\n\`\`\`\n${stderr || '(no error output)'}\n\`\`\`\n`,
|
|
255
|
-
'utf-8'
|
|
256
|
-
);
|
|
257
|
-
} else {
|
|
258
|
-
// The agent was told to write directly to insightFile.
|
|
259
|
-
// Check if it did; if not, fall back to cleaning stdout from .tmp.
|
|
260
|
-
let hasDirectOutput = false;
|
|
261
|
-
try {
|
|
262
|
-
const direct = await fs.readFile(insightFile, 'utf-8');
|
|
263
|
-
if (direct && direct.trim().length > 50) {
|
|
264
|
-
hasDirectOutput = true;
|
|
265
|
-
console.log(`✅ Insight generated for session ${sessionId} (agent wrote directly)`);
|
|
266
|
-
}
|
|
267
|
-
} catch (_e) { /* file doesn't exist */ }
|
|
268
|
-
|
|
269
|
-
if (!hasDirectOutput) {
|
|
270
|
-
let report = await fs.readFile(outputFile, 'utf-8');
|
|
271
|
-
report = this._cleanReport(report);
|
|
272
|
-
await fs.writeFile(insightFile, report, 'utf-8');
|
|
273
|
-
console.log(`✅ Insight generated for session ${sessionId} (cleaned from stdout)`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Cleanup
|
|
278
|
-
await fs.unlink(outputFile).catch(() => {});
|
|
279
|
-
await fs.unlink(lockFile).catch(() => {});
|
|
280
|
-
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
281
|
-
// Clean up sub-agent working directory (safety net)
|
|
282
|
-
await fs.rm(path.join(sessionPath, '.output'), { recursive: true, force: true }).catch(() => {});
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.error('❌ Error finalizing insight:', err);
|
|
285
|
-
await fs.unlink(lockFile).catch(() => {});
|
|
286
|
-
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
analysisProcess.on('error', async (err) => {
|
|
291
|
-
console.error(`❌ Failed to spawn ${toolConfig.name}:`, err);
|
|
292
|
-
await fs.unlink(lockFile).catch(() => {});
|
|
293
|
-
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Build insight generation prompt
|
|
299
|
-
* @private
|
|
300
|
-
*/
|
|
301
|
-
_buildPrompt(outputPath, eventsFile) {
|
|
302
|
-
const sessionDir = path.dirname(outputPath);
|
|
303
|
-
const eventsFilename = path.basename(eventsFile);
|
|
304
|
-
const workDir = `${sessionDir}/.output`;
|
|
305
|
-
|
|
306
|
-
// For Pi-Mono: emphasize analyzing ONLY the specified file
|
|
307
|
-
const fileInstruction = eventsFilename.includes('_')
|
|
308
|
-
? `\n**IMPORTANT**: This directory may contain multiple .jsonl files. You MUST analyze ONLY the file named \`${eventsFilename}\`. Do NOT read or analyze any other .jsonl files in this directory.\n`
|
|
309
|
-
: '';
|
|
310
|
-
|
|
311
|
-
return `You are an expert AI agent evaluator. The current working directory is an AI coding agent session folder. It contains the raw session data from an agent run.
|
|
312
|
-
${fileInstruction}
|
|
313
|
-
**Step 1 — Discover session files.** Run \`ls -la\` to see what's available, then note which files exist:
|
|
314
|
-
- \`${eventsFilename}\` — the main session event log (JSONL, one JSON event per line). Primary data source. May be large. **This is the ONLY events file you should analyze.**
|
|
315
|
-
- \`plan.md\` — the agent's plan (if it exists).
|
|
316
|
-
- \`workspace.yaml\` — workspace configuration (if it exists).
|
|
317
|
-
- Any other relevant files.
|
|
318
|
-
|
|
319
|
-
**Step 2 — Spawn 3 sub-agents for parallel analysis.** First create the working directory: \`mkdir -p ${workDir}\`. Then use the Task tool to launch ALL of the following sub-agents simultaneously (in a single message with multiple Task tool calls). Each sub-agent should:
|
|
320
|
-
- Read \`${eventsFilename}\` from \`${sessionDir}\` (use Bash: \`cat\`, \`jq\`, or \`python3\` to parse) **— ONLY this file, ignore others**
|
|
321
|
-
- Read other session files as needed
|
|
322
|
-
- Write its findings to an intermediate file in \`${workDir}/\`
|
|
323
|
-
- Return a summary of its findings
|
|
324
|
-
|
|
325
|
-
Sub-agents to spawn:
|
|
326
|
-
|
|
327
|
-
1. **Tool Usage Analyst** — Analyze tool selection quality, redundant/wasted calls, error handling patterns, tool call counts and durations. Write findings to \`${workDir}/tools.md\`.
|
|
328
|
-
|
|
329
|
-
2. **Workflow Strategist** — Evaluate planning quality, sequencing logic, sub-agent decomposition, backtracking/wandering patterns. Write findings to \`${workDir}/workflow.md\`.
|
|
330
|
-
|
|
331
|
-
3. **Performance Profiler** — Calculate time distribution (LLM thinking vs tool execution vs idle gaps), identify bottlenecks, assess concurrency usage. Write findings to \`${workDir}/performance.md\`.
|
|
332
|
-
|
|
333
|
-
**CRITICAL: You MUST wait for ALL 3 sub-agents to complete before proceeding to Step 3.** Do NOT move on until every sub-agent has returned its results. After launching them, poll or wait for their completion.
|
|
334
|
-
|
|
335
|
-
**Step 3 — Synthesize the final report.** Once all sub-agents are done:
|
|
336
|
-
1. Read the intermediate files from \`${workDir}/\`
|
|
337
|
-
2. Synthesize a unified report
|
|
338
|
-
3. Write the final report to \`${outputPath}\`
|
|
339
|
-
4. Clean up by removing the entire working directory: \`rm -rf ${workDir}\`
|
|
340
|
-
|
|
341
|
-
The final report must be a markdown file with these sections:
|
|
342
|
-
|
|
343
|
-
## 🎯 Effectiveness Score: X/100
|
|
344
|
-
One-line verdict on how well the agent fulfilled the user's intent.
|
|
345
|
-
|
|
346
|
-
## 🔧 Tool Usage Analysis
|
|
347
|
-
- **Tool selection quality**: Did the agent pick the right tools? Any unnecessary or redundant tool calls? (e.g. repeated Read calls on the same file, Grep when Glob would suffice, excessive Bash calls)
|
|
348
|
-
- **Error handling**: How did the agent recover from tool errors? Did it retry blindly or adapt?
|
|
349
|
-
- **Efficiency**: Tool call count vs. actual value delivered. Identify wasted calls.
|
|
350
|
-
|
|
351
|
-
## 🔄 Workflow & Strategy
|
|
352
|
-
- **Planning quality**: Did the agent have a coherent strategy, or did it wander? Look for signs of backtracking, repeated attempts, or lack of direction.
|
|
353
|
-
- **Sub-agent usage** (if any): Were sub-agents spawned effectively? Was the decomposition logical? Any sub-agents that were unnecessary or too narrow/broad?
|
|
354
|
-
- **Sequencing**: Were operations done in a logical order, or was there unnecessary back-and-forth?
|
|
355
|
-
|
|
356
|
-
## ⚡ Performance
|
|
357
|
-
- **Time distribution**: Where did the wall-clock time actually go? (LLM thinking vs. tool execution vs. idle gaps)
|
|
358
|
-
- **Bottlenecks**: Identify the biggest time sinks and whether they were avoidable.
|
|
359
|
-
- **Concurrency**: Did the agent parallelize where it could? Missed opportunities?
|
|
360
|
-
|
|
361
|
-
## 💡 Top 3 Improvements
|
|
362
|
-
Specific, actionable recommendations to make this agent workflow better. Examples:
|
|
363
|
-
- "Batch the 12 sequential Read calls into a single Glob + targeted Reads"
|
|
364
|
-
- "The agent re-read file X 4 times — cache the content across turns"
|
|
365
|
-
- "Sub-agent 'code-explorer' ran for 45s but its output was barely used — consider inlining"
|
|
366
|
-
|
|
367
|
-
Be brutally honest. Generic advice like "add error handling" is useless — always tie recommendations to specific evidence from the session data.
|
|
368
|
-
|
|
369
|
-
IMPORTANT CONSTRAINTS:
|
|
370
|
-
- Be precise and concise. Every sentence must carry data or actionable insight — no filler, no fluff.
|
|
371
|
-
- The entire report MUST be under 3000 characters (including markdown formatting). Cut ruthlessly if needed.`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Clean report output — strip copilot CLI working logs, thinking blocks, meta-commentary.
|
|
376
|
-
* The copilot CLI dumps tool call logs (● Tool name, $ command, └ output) to stdout
|
|
377
|
-
* alongside the actual report. We need to extract just the final markdown.
|
|
378
|
-
* @private
|
|
379
|
-
*/
|
|
380
|
-
_cleanReport(report) {
|
|
381
|
-
// Remove <thinking>...</thinking> blocks
|
|
382
|
-
report = report.replace(/<thinking>[\s\S]*?<\/thinking>/g, '');
|
|
383
|
-
|
|
384
|
-
// Remove meta-commentary lines
|
|
385
|
-
report = report.replace(/^(Let me analyze|I'll analyze|Analyzing|Here's my analysis of|I need the session data).*$/gm, '');
|
|
386
|
-
|
|
387
|
-
// Strategy: try to find the final markdown report section.
|
|
388
|
-
// The copilot CLI log has patterns like:
|
|
389
|
-
// ● Tool name (tool call header)
|
|
390
|
-
// $ command (bash command)
|
|
391
|
-
// └ N lines... (output summary)
|
|
392
|
-
// (+N lines) (file write indicator)
|
|
393
|
-
// The actual report starts with a markdown heading like "## 🎯"
|
|
394
|
-
|
|
395
|
-
// Look for the last occurrence of the report structure (## 🎯 Effectiveness Score)
|
|
396
|
-
// which indicates the final output vs. intermediate attempts
|
|
397
|
-
const reportStartPattern = /^## 🎯\s*Effectiveness Score/m;
|
|
398
|
-
const matches = [...report.matchAll(new RegExp(reportStartPattern.source, 'gm'))];
|
|
399
|
-
|
|
400
|
-
if (matches.length > 0) {
|
|
401
|
-
// Take from the last match onward (in case the agent generated it multiple times)
|
|
402
|
-
const lastMatch = matches[matches.length - 1];
|
|
403
|
-
report = report.slice(lastMatch.index);
|
|
404
|
-
} else {
|
|
405
|
-
// Try broader: find any markdown heading (## with emoji or #)
|
|
406
|
-
const anyHeadingMatch = report.match(/^(## [🎯🔧🔄⚡💡#])/mu);
|
|
407
|
-
if (anyHeadingMatch) {
|
|
408
|
-
report = report.slice(anyHeadingMatch.index);
|
|
409
|
-
} else {
|
|
410
|
-
// Last resort: strip known copilot CLI log patterns line by line
|
|
411
|
-
const lines = report.split('\n');
|
|
412
|
-
const cleanedLines = [];
|
|
413
|
-
let skipBlock = false;
|
|
414
|
-
|
|
415
|
-
for (const line of lines) {
|
|
416
|
-
// Skip copilot CLI working log patterns
|
|
417
|
-
if (/^● /.test(line)) { skipBlock = true; continue; }
|
|
418
|
-
if (/^ {2}\$ /.test(line)) { skipBlock = true; continue; }
|
|
419
|
-
if (/^ {2}└ /.test(line)) { skipBlock = false; continue; }
|
|
420
|
-
if (/^\(\+\d+ lines?\)/.test(line)) { continue; }
|
|
421
|
-
if (/^ {2}└ \d+ lines/.test(line)) { continue; }
|
|
422
|
-
// Skip "Asked user" and "User responded" log lines
|
|
423
|
-
if (/^● Asked user:/.test(line)) { skipBlock = true; continue; }
|
|
424
|
-
if (/^ {2}└ User responded:/.test(line)) { skipBlock = false; continue; }
|
|
425
|
-
|
|
426
|
-
// If we hit a non-log line, stop skipping
|
|
427
|
-
if (skipBlock && /\S/.test(line) && !/^ {2}/.test(line)) {
|
|
428
|
-
skipBlock = false;
|
|
429
|
-
}
|
|
430
|
-
if (skipBlock) continue;
|
|
431
|
-
|
|
432
|
-
cleanedLines.push(line);
|
|
433
|
-
}
|
|
434
|
-
report = cleanedLines.join('\n');
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Trim excessive whitespace
|
|
439
|
-
report = report.replace(/\n{3,}/g, '\n\n').trim();
|
|
440
|
-
|
|
441
|
-
return report;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Get insight status
|
|
446
|
-
*/
|
|
447
|
-
/**
|
|
448
|
-
* Get insight status
|
|
449
|
-
* @param {string} sessionId - Session ID
|
|
450
|
-
* @param {string} sessionPath - Full path to session directory
|
|
451
|
-
* @param {string} source - Session source
|
|
452
|
-
* @returns {Promise<Object>} Status object
|
|
453
|
-
*/
|
|
454
|
-
async getInsightStatus(sessionId, sessionPath, _source = 'copilot') {
|
|
455
|
-
return await this._getStatusForSource(sessionId, sessionPath);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Get status for a specific session directory
|
|
460
|
-
* @private
|
|
461
|
-
*/
|
|
462
|
-
async _getStatusForSource(sessionId, sessionPath) {
|
|
463
|
-
const insightFile = path.join(sessionPath, `${sessionId}.agent-review.md`);
|
|
464
|
-
const lockFile = path.join(sessionPath, `${sessionId}.agent-review.md.lock`);
|
|
465
|
-
const tmpFile = path.join(sessionPath, `${sessionId}.agent-review.md.tmp`);
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
const report = await fs.readFile(insightFile, 'utf-8');
|
|
469
|
-
const stats = await fs.stat(insightFile);
|
|
470
|
-
return {
|
|
471
|
-
status: 'completed',
|
|
472
|
-
report,
|
|
473
|
-
generatedAt: stats.mtime
|
|
474
|
-
};
|
|
475
|
-
} catch (_err) {
|
|
476
|
-
// Check if generation is in progress
|
|
477
|
-
try {
|
|
478
|
-
await fs.access(lockFile);
|
|
479
|
-
const stats = await fs.stat(lockFile);
|
|
480
|
-
const ageMs = Date.now() - stats.birthtime.getTime();
|
|
481
|
-
|
|
482
|
-
// Read live working log from tmp file
|
|
483
|
-
let log = null;
|
|
484
|
-
try {
|
|
485
|
-
log = await fs.readFile(tmpFile, 'utf-8');
|
|
486
|
-
} catch (_tmpErr) {
|
|
487
|
-
// tmp file may not exist yet
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (ageMs >= config.INSIGHT_TIMEOUT_MS) {
|
|
491
|
-
return {
|
|
492
|
-
status: 'timeout',
|
|
493
|
-
log,
|
|
494
|
-
startedAt: stats.birthtime,
|
|
495
|
-
lastUpdate: stats.mtime,
|
|
496
|
-
ageMs
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
status: 'generating',
|
|
502
|
-
log,
|
|
503
|
-
startedAt: stats.birthtime,
|
|
504
|
-
lastUpdate: stats.mtime,
|
|
505
|
-
ageMs
|
|
506
|
-
};
|
|
507
|
-
} catch (_lockErr) {
|
|
508
|
-
return { status: 'not_started' };
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Delete insight report
|
|
515
|
-
* @param {string} sessionId - Session ID
|
|
516
|
-
* @param {string} sessionPath - Full path to session directory
|
|
517
|
-
* @param {string} source - Session source
|
|
518
|
-
* @returns {Promise<Object>} Result object
|
|
519
|
-
*/
|
|
520
|
-
async deleteInsight(sessionId, sessionPath, _source = 'copilot') {
|
|
521
|
-
const insightFile = path.join(sessionPath, `${sessionId}.agent-review.md`);
|
|
522
|
-
|
|
523
|
-
try {
|
|
524
|
-
await fs.unlink(insightFile);
|
|
525
|
-
return { success: true };
|
|
526
|
-
} catch (err) {
|
|
527
|
-
if (err.code === 'ENOENT') {
|
|
528
|
-
return { success: true, message: 'Insight file not found' };
|
|
529
|
-
}
|
|
530
|
-
throw err;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
module.exports = InsightService;
|