pan-wizard 3.4.1 → 3.5.1

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.
@@ -0,0 +1,653 @@
1
+ 'use strict';
2
+ // optimize.cjs — Circular optimization loop: trace → learn → apply → repeat.
3
+ //
4
+ // Every agent spawn is logged to a trace session. After a build, /pan:learn
5
+ // invokes pan-optimizer to analyze the trace. /pan:optimize apply writes memory
6
+ // entries, config notes, and prompt suggestions back into the project, making
7
+ // the next run smarter.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output } = require('./core.cjs');
12
+ const { PLANNING_DIR } = require('./constants.cjs');
13
+
14
+ // ─── Storage layout ──────────────────────────────────────────────────────────
15
+
16
+ const OPTIMIZE_DIR = 'optimization';
17
+ const TRACES_DIR = 'traces';
18
+ const OPT_REPORTS_DIR = 'reports';
19
+ const CURRENT_SESSION_FILE = 'current-session';
20
+ const TRACE_EVENT_FILE = 'trace.jsonl';
21
+ const OPT_SESSION_FILE = 'session.json';
22
+ const APPLIED_LOG = 'applied.jsonl';
23
+
24
+ // Event types the trace system recognizes
25
+ const EVENT_TYPES = ['decision', 'error', 'gap', 'correction', 'redundancy', 'memory_hit', 'memory_miss', 'surprise'];
26
+ // Impact levels
27
+ const IMPACT_LEVELS = ['critical', 'major', 'minor', 'trivial'];
28
+
29
+ // ─── Path helpers ─────────────────────────────────────────────────────────────
30
+
31
+ function getOptimizeDir(cwd) {
32
+ return path.join(cwd, PLANNING_DIR, OPTIMIZE_DIR);
33
+ }
34
+
35
+ function getTracesDir(cwd) {
36
+ return path.join(getOptimizeDir(cwd), TRACES_DIR);
37
+ }
38
+
39
+ function getReportsDir(cwd) {
40
+ return path.join(getOptimizeDir(cwd), OPT_REPORTS_DIR);
41
+ }
42
+
43
+ // ─── Session management ───────────────────────────────────────────────────────
44
+
45
+ function generateSessionId() {
46
+ const now = new Date();
47
+ // sess_20260421T180000
48
+ return 'sess_' + now.toISOString().replace(/[-:.Z]/g, '').slice(0, 15);
49
+ }
50
+
51
+ function initTraceSession(cwd, opts = {}) {
52
+ const sessionId = opts.sessionId || generateSessionId();
53
+ const sessionDir = path.join(getTracesDir(cwd), sessionId);
54
+
55
+ try {
56
+ fs.mkdirSync(sessionDir, { recursive: true });
57
+
58
+ const meta = {
59
+ session_id: sessionId,
60
+ started_at: new Date().toISOString(),
61
+ description: opts.description || null,
62
+ command: opts.command || null,
63
+ phase: opts.phase || null,
64
+ agent_count: 0,
65
+ event_count: 0,
66
+ ended_at: null,
67
+ };
68
+
69
+ fs.writeFileSync(path.join(sessionDir, OPT_SESSION_FILE), JSON.stringify(meta, null, 2) + '\n');
70
+
71
+ // Record as active session
72
+ const optimizeDir = getOptimizeDir(cwd);
73
+ fs.mkdirSync(optimizeDir, { recursive: true });
74
+ fs.writeFileSync(path.join(optimizeDir, CURRENT_SESSION_FILE), sessionId + '\n');
75
+
76
+ return { session_id: sessionId, started_at: meta.started_at, directory: sessionDir };
77
+ } catch (e) {
78
+ return { error: e.message };
79
+ }
80
+ }
81
+
82
+ function getCurrentSessionId(cwd) {
83
+ try {
84
+ const content = fs.readFileSync(path.join(getOptimizeDir(cwd), CURRENT_SESSION_FILE), 'utf-8');
85
+ return content.trim() || null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function logTraceEvent(cwd, event, sessionId) {
92
+ const sid = sessionId || getCurrentSessionId(cwd);
93
+ if (!sid) return false;
94
+
95
+ try {
96
+ const sessionDir = path.join(getTracesDir(cwd), sid);
97
+
98
+ // W3 fix: inherit session phase so per-phase filtering doesn't require session-join
99
+ let resolvedPhase = event.phase || null;
100
+ if (!resolvedPhase) {
101
+ try {
102
+ const meta = JSON.parse(fs.readFileSync(path.join(sessionDir, OPT_SESSION_FILE), 'utf-8'));
103
+ resolvedPhase = meta.phase || null;
104
+ } catch {}
105
+ }
106
+
107
+ const record = {
108
+ ts: new Date().toISOString(),
109
+ session: sid,
110
+ agent: event.agent || null,
111
+ phase: resolvedPhase,
112
+ type: EVENT_TYPES.includes(event.type) ? event.type : 'unknown',
113
+ category: event.category || null,
114
+ description: String(event.description || ''),
115
+ context: event.context || null,
116
+ impact: IMPACT_LEVELS.includes(event.impact) ? event.impact : 'minor',
117
+ correction: event.correction || null,
118
+ tokens_wasted: typeof event.tokens_wasted === 'number' ? event.tokens_wasted : null,
119
+ };
120
+
121
+ fs.mkdirSync(sessionDir, { recursive: true });
122
+ fs.appendFileSync(path.join(sessionDir, TRACE_EVENT_FILE), JSON.stringify(record) + '\n');
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ function endTraceSession(cwd, sessionId) {
130
+ const sid = sessionId || getCurrentSessionId(cwd);
131
+ if (!sid) return { error: 'No active session' };
132
+
133
+ try {
134
+ const sessionDir = path.join(getTracesDir(cwd), sid);
135
+ const metaPath = path.join(sessionDir, OPT_SESSION_FILE);
136
+
137
+ let meta = {};
138
+ try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch {}
139
+
140
+ let eventCount = 0;
141
+ const agentNames = new Set();
142
+ const typeCounts = {};
143
+
144
+ try {
145
+ const raw = fs.readFileSync(path.join(sessionDir, TRACE_EVENT_FILE), 'utf-8');
146
+ raw.trim().split('\n').filter(Boolean).forEach(line => {
147
+ try {
148
+ const e = JSON.parse(line);
149
+ eventCount++;
150
+ if (e.agent) agentNames.add(e.agent);
151
+ typeCounts[e.type] = (typeCounts[e.type] || 0) + 1;
152
+ } catch {}
153
+ });
154
+ } catch {}
155
+
156
+ meta.ended_at = new Date().toISOString();
157
+ meta.event_count = eventCount;
158
+ meta.agent_count = agentNames.size;
159
+ meta.agents = Array.from(agentNames);
160
+ meta.type_counts = typeCounts;
161
+
162
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
163
+
164
+ return {
165
+ session_id: sid,
166
+ event_count: eventCount,
167
+ agent_count: agentNames.size,
168
+ type_counts: typeCounts,
169
+ ended_at: meta.ended_at,
170
+ };
171
+ } catch (e) {
172
+ return { error: e.message };
173
+ }
174
+ }
175
+
176
+ function readTraceSession(cwd, sessionId) {
177
+ try {
178
+ const sessionDir = path.join(getTracesDir(cwd), sessionId);
179
+
180
+ let metadata = {};
181
+ try {
182
+ metadata = JSON.parse(fs.readFileSync(path.join(sessionDir, OPT_SESSION_FILE), 'utf-8'));
183
+ } catch {}
184
+
185
+ const events = [];
186
+ try {
187
+ const raw = fs.readFileSync(path.join(sessionDir, TRACE_EVENT_FILE), 'utf-8');
188
+ raw.trim().split('\n').filter(Boolean).forEach(line => {
189
+ try { events.push(JSON.parse(line)); } catch {}
190
+ });
191
+ } catch {}
192
+
193
+ return { session_id: sessionId, metadata, events, event_count: events.length };
194
+ } catch (e) {
195
+ return { error: e.message };
196
+ }
197
+ }
198
+
199
+ function listTraceSessions(cwd) {
200
+ try {
201
+ const tracesDir = getTracesDir(cwd);
202
+ try { fs.accessSync(tracesDir); } catch { return { sessions: [], count: 0 }; }
203
+
204
+ const dirs = fs.readdirSync(tracesDir, { withFileTypes: true })
205
+ .filter(e => e.isDirectory() && e.name.startsWith('sess_'));
206
+
207
+ const sessions = dirs.map(e => {
208
+ const sessionDir = path.join(tracesDir, e.name);
209
+ let meta = { session_id: e.name };
210
+ try {
211
+ meta = JSON.parse(fs.readFileSync(path.join(sessionDir, OPT_SESSION_FILE), 'utf-8'));
212
+ } catch {}
213
+ return meta;
214
+ }).sort((a, b) => (b.started_at || '').localeCompare(a.started_at || ''));
215
+
216
+ return { sessions, count: sessions.length };
217
+ } catch (e) {
218
+ return { error: e.message };
219
+ }
220
+ }
221
+
222
+ // ─── Local analysis (no agent) ────────────────────────────────────────────────
223
+
224
+ function analyzeEvents(events, sessionMeta) {
225
+ const errors = events.filter(e => e.type === 'error');
226
+ const gaps = events.filter(e => e.type === 'gap');
227
+ const redundancies = events.filter(e => e.type === 'redundancy');
228
+ const decisions = events.filter(e => e.type === 'decision');
229
+ const corrections = events.filter(e => e.type === 'correction');
230
+ const memoryMisses = events.filter(e => e.type === 'memory_miss');
231
+ const reviewerCorrections = events.filter(e => e.type === 'error' && e.category === 'reviewer_correction');
232
+ const memoryPrimed = events.filter(e => e.type === 'decision' && e.category === 'memory_primed');
233
+
234
+ function frequencyMap(arr) {
235
+ const map = {};
236
+ arr.forEach(e => {
237
+ const key = e.category || e.description.slice(0, 60);
238
+ map[key] = (map[key] || 0) + 1;
239
+ });
240
+ return Object.entries(map)
241
+ .map(([key, count]) => ({ pattern: key, count }))
242
+ .sort((a, b) => b.count - a.count);
243
+ }
244
+
245
+ const agentStats = {};
246
+ events.forEach(e => {
247
+ if (!e.agent) return;
248
+ if (!agentStats[e.agent]) agentStats[e.agent] = { total: 0, errors: 0, gaps: 0, corrections: 0 };
249
+ agentStats[e.agent].total++;
250
+ if (e.type === 'error') agentStats[e.agent].errors++;
251
+ if (e.type === 'gap') agentStats[e.agent].gaps++;
252
+ if (e.type === 'correction') agentStats[e.agent].corrections++;
253
+ });
254
+
255
+ Object.keys(agentStats).forEach(a => {
256
+ const s = agentStats[a];
257
+ s.error_rate = s.total > 0 ? Math.round((s.errors / s.total) * 100) / 100 : 0;
258
+ });
259
+
260
+ const wastedTokens = redundancies.reduce((sum, e) => sum + (e.tokens_wasted || 0), 0);
261
+
262
+ // ── Timing analysis from wall-clock timestamps ────────────────────────────
263
+ // Token data is unavailable (Claude Code SubagentStop doesn't populate usage).
264
+ // Use event timestamps + session start/end for meaningful timing analysis.
265
+ const timing = {};
266
+
267
+ // Session total duration
268
+ if (sessionMeta && sessionMeta.started_at && sessionMeta.ended_at) {
269
+ timing.session_duration_ms = new Date(sessionMeta.ended_at) - new Date(sessionMeta.started_at);
270
+ timing.session_duration_human = _msToHuman(timing.session_duration_ms);
271
+ }
272
+
273
+ // Per-agent intervals: time between consecutive events of the same agent
274
+ const agentIntervals = {};
275
+ const sorted = [...events].filter(e => e.ts).sort((a, b) => a.ts.localeCompare(b.ts));
276
+ for (let i = 1; i < sorted.length; i++) {
277
+ const gap = new Date(sorted[i].ts) - new Date(sorted[i - 1].ts);
278
+ const agent = sorted[i].agent || 'unknown';
279
+ if (!agentIntervals[agent]) agentIntervals[agent] = [];
280
+ agentIntervals[agent].push(gap);
281
+ }
282
+
283
+ // Slow-agent detection: flag agents whose avg interval exceeds 3 minutes
284
+ const slowAgents = [];
285
+ Object.entries(agentIntervals).forEach(([agent, intervals]) => {
286
+ const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
287
+ if (avg > 180000) slowAgents.push({ agent, avg_interval_ms: Math.round(avg), avg_human: _msToHuman(avg) });
288
+ });
289
+ if (slowAgents.length) timing.slow_agents = slowAgents;
290
+
291
+ // Wave timing from wave_complete events (have context.duration_ms when wired)
292
+ const waveEvents = events.filter(e => e.category === 'wave_complete');
293
+ if (waveEvents.length) {
294
+ timing.waves = waveEvents.map(e => ({
295
+ description: e.description,
296
+ duration_ms: e.context && e.context.duration_ms ? e.context.duration_ms : null,
297
+ }));
298
+ }
299
+
300
+ timing.token_data_available = events.some(e => e.context && (e.context.input_tokens || 0) > 0);
301
+
302
+ return {
303
+ summary: {
304
+ total_events: events.length,
305
+ errors: errors.length,
306
+ gaps: gaps.length,
307
+ redundancies: redundancies.length,
308
+ decisions: decisions.length,
309
+ corrections: corrections.length,
310
+ memory_misses: memoryMisses.length,
311
+ wasted_tokens: wastedTokens,
312
+ reviewer_corrections: reviewerCorrections.length,
313
+ memory_primed_count: memoryPrimed.length,
314
+ },
315
+ timing,
316
+ error_patterns: frequencyMap(errors),
317
+ gap_patterns: frequencyMap(gaps),
318
+ memory_miss_patterns: frequencyMap(memoryMisses),
319
+ agent_stats: agentStats,
320
+ critical_events: events.filter(e => e.impact === 'critical'),
321
+ major_events: events.filter(e => e.impact === 'major'),
322
+ };
323
+ }
324
+
325
+ function _msToHuman(ms) {
326
+ if (ms < 1000) return `${ms}ms`;
327
+ if (ms < 60000) return `${Math.round(ms / 1000)}s`;
328
+ const m = Math.floor(ms / 60000);
329
+ const s = Math.round((ms % 60000) / 1000);
330
+ return s > 0 ? `${m}m${s}s` : `${m}m`;
331
+ }
332
+
333
+ function generateLocalReport(cwd, sessionId) {
334
+ const session = readTraceSession(cwd, sessionId);
335
+ if (session.error) return session;
336
+
337
+ return {
338
+ session_id: sessionId,
339
+ generated_at: new Date().toISOString(),
340
+ metadata: session.metadata,
341
+ ...analyzeEvents(session.events, session.metadata),
342
+ raw_events: session.events,
343
+ };
344
+ }
345
+
346
+ // ─── Report storage + parsing ─────────────────────────────────────────────────
347
+
348
+ function listOptimizationReports(cwd) {
349
+ try {
350
+ const reportsDir = getReportsDir(cwd);
351
+ try { fs.accessSync(reportsDir); } catch { return { reports: [], count: 0 }; }
352
+
353
+ const reports = fs.readdirSync(reportsDir)
354
+ .filter(f => f.endsWith('.md') || f.endsWith('.json'))
355
+ .map(f => {
356
+ const p = path.join(reportsDir, f);
357
+ let stat;
358
+ try { stat = fs.statSync(p); } catch { return null; }
359
+ return { filename: f, path: p, size: stat.size, created_at: stat.birthtime.toISOString() };
360
+ })
361
+ .filter(Boolean)
362
+ .sort((a, b) => b.created_at.localeCompare(a.created_at));
363
+
364
+ return { reports, count: reports.length };
365
+ } catch (e) {
366
+ return { error: e.message };
367
+ }
368
+ }
369
+
370
+ // Extracts the JSON block from the "## Auto-Apply Actions" section of an
371
+ // optimizer markdown report.
372
+ function parseAutoApplyBlock(reportContent) {
373
+ const match = reportContent.match(/##\s+Auto-Apply Actions[\s\S]*?```json\n([\s\S]*?)```/);
374
+ if (!match) return null;
375
+ try { return JSON.parse(match[1]); } catch { return null; }
376
+ }
377
+
378
+ // ─── Apply recommendations ────────────────────────────────────────────────────
379
+
380
+ function applyReportRecommendations(cwd, reportPath) {
381
+ let reportContent;
382
+ try {
383
+ reportContent = fs.readFileSync(reportPath, 'utf-8');
384
+ } catch (e) {
385
+ return { error: `Cannot read report: ${e.message}` };
386
+ }
387
+
388
+ // Support both markdown reports (with auto-apply block) and JSON analysis files
389
+ let actions = parseAutoApplyBlock(reportContent);
390
+ if (!actions) {
391
+ // Try parsing as raw JSON analysis — generate basic memory entries from memory_miss patterns
392
+ try {
393
+ const analysis = JSON.parse(reportContent);
394
+ actions = deriveActionsFromAnalysis(analysis);
395
+ } catch {
396
+ return { applied: [], skipped: [], note: 'No auto-apply actions found in report' };
397
+ }
398
+ }
399
+
400
+ if (!Array.isArray(actions)) {
401
+ return { applied: [], skipped: [], note: 'Auto-apply block is not a valid array' };
402
+ }
403
+
404
+ const applied = [];
405
+ const skipped = [];
406
+
407
+ for (const action of actions) {
408
+ try {
409
+ if (action.type === 'memory') {
410
+ // Write new memory entry (skip if file exists to avoid overwriting manual edits)
411
+ const memPath = path.join(cwd, action.path);
412
+ try {
413
+ fs.accessSync(memPath);
414
+ skipped.push({ action, reason: 'File already exists — skipped to preserve manual edits' });
415
+ } catch {
416
+ fs.mkdirSync(path.dirname(memPath), { recursive: true });
417
+ fs.writeFileSync(memPath, action.content, 'utf-8');
418
+ applied.push({ action, result: `Written to ${action.path}` });
419
+ }
420
+ } else if (action.type === 'memory_append') {
421
+ // Append to existing memory file
422
+ const memPath = path.join(cwd, action.path);
423
+ fs.mkdirSync(path.dirname(memPath), { recursive: true });
424
+ fs.appendFileSync(memPath, '\n' + action.content, 'utf-8');
425
+ applied.push({ action, result: `Appended to ${action.path}` });
426
+ } else if (action.type === 'note') {
427
+ // Write a human-readable suggestion note
428
+ const notePath = path.join(getOptimizeDir(cwd), 'suggestions.md');
429
+ const entry = `\n## ${new Date().toISOString()}: ${action.description || 'Suggestion'}\n\n${action.content || action.suggestion || ''}\n\n**Target:** ${action.target || 'unspecified'}\n`;
430
+ fs.appendFileSync(notePath, entry);
431
+ applied.push({ action, result: 'Suggestion written to optimization/suggestions.md' });
432
+ } else if (action.type === 'planning_note') {
433
+ // Write optimization note into .planning/optimization/config-suggestions.md
434
+ const notePath = path.join(getOptimizeDir(cwd), 'config-suggestions.md');
435
+ const entry = `\n## ${new Date().toISOString()}\n${action.content}\n`;
436
+ fs.appendFileSync(notePath, entry);
437
+ applied.push({ action, result: 'Config suggestion recorded' });
438
+ } else {
439
+ skipped.push({ action, reason: `Unknown action type: ${action.type}` });
440
+ }
441
+ } catch (e) {
442
+ skipped.push({ action, reason: e.message });
443
+ }
444
+ }
445
+
446
+ // Log what was applied for cumulative stats
447
+ try {
448
+ const logEntry = {
449
+ ts: new Date().toISOString(),
450
+ report: path.basename(reportPath),
451
+ applied_count: applied.length,
452
+ skipped_count: skipped.length,
453
+ applied_types: applied.map(a => a.action.type),
454
+ };
455
+ fs.appendFileSync(path.join(getOptimizeDir(cwd), APPLIED_LOG), JSON.stringify(logEntry) + '\n');
456
+ } catch {}
457
+
458
+ return { applied, skipped };
459
+ }
460
+
461
+ // Derive basic memory actions from a raw JSON analysis when no optimizer agent
462
+ // report is available (fallback for /pan:optimize apply on a JSON file).
463
+ function deriveActionsFromAnalysis(analysis) {
464
+ const actions = [];
465
+ const { memory_miss_patterns, gap_patterns, summary } = analysis;
466
+
467
+ if (summary && summary.memory_misses > 0 && memory_miss_patterns) {
468
+ memory_miss_patterns.slice(0, 3).forEach(p => {
469
+ actions.push({
470
+ type: 'note',
471
+ description: `Memory miss: ${p.pattern}`,
472
+ content: `This topic was missing from memory ${p.count} time(s) during the traced session. Consider adding a memory entry for it.`,
473
+ target: '.planning/memory/',
474
+ });
475
+ });
476
+ }
477
+
478
+ if (gap_patterns && gap_patterns.length > 0) {
479
+ gap_patterns.slice(0, 3).forEach(p => {
480
+ actions.push({
481
+ type: 'note',
482
+ description: `Knowledge gap: ${p.pattern}`,
483
+ content: `The agent had to infer this ${p.count} time(s). Research and cache the answer.`,
484
+ target: '.planning/memory/',
485
+ });
486
+ });
487
+ }
488
+
489
+ return actions;
490
+ }
491
+
492
+ // ─── Cumulative stats ─────────────────────────────────────────────────────────
493
+
494
+ function getOptimizeStats(cwd) {
495
+ try {
496
+ const sessions = listTraceSessions(cwd);
497
+ const reports = listOptimizationReports(cwd);
498
+
499
+ let totalEvents = 0;
500
+ let totalErrors = 0;
501
+ sessions.sessions && sessions.sessions.forEach(s => {
502
+ totalEvents += s.event_count || 0;
503
+ if (s.type_counts) totalErrors += s.type_counts.error || 0;
504
+ });
505
+
506
+ let totalApplied = 0;
507
+ let totalSkipped = 0;
508
+ let applyRuns = 0;
509
+ try {
510
+ const raw = fs.readFileSync(path.join(getOptimizeDir(cwd), APPLIED_LOG), 'utf-8');
511
+ raw.trim().split('\n').filter(Boolean).forEach(line => {
512
+ try {
513
+ const e = JSON.parse(line);
514
+ totalApplied += e.applied_count || 0;
515
+ totalSkipped += e.skipped_count || 0;
516
+ applyRuns++;
517
+ } catch {}
518
+ });
519
+ } catch {}
520
+
521
+ return {
522
+ trace_sessions: sessions.count || 0,
523
+ optimization_reports: reports.count || 0,
524
+ total_events_traced: totalEvents,
525
+ total_errors_traced: totalErrors,
526
+ total_optimizations_applied: totalApplied,
527
+ total_skipped: totalSkipped,
528
+ apply_runs: applyRuns,
529
+ current_session: getCurrentSessionId(cwd),
530
+ };
531
+ } catch (e) {
532
+ return { error: e.message };
533
+ }
534
+ }
535
+
536
+ // ─── CLI commands ─────────────────────────────────────────────────────────────
537
+
538
+ function cmdOptimizeTrace(cwd, sub, opts, raw) {
539
+ if (sub === 'init') {
540
+ output(initTraceSession(cwd, opts), raw);
541
+ } else if (sub === 'log') {
542
+ const logged = logTraceEvent(cwd, opts);
543
+ output({ logged, session: getCurrentSessionId(cwd) }, raw);
544
+ } else if (sub === 'end') {
545
+ output(endTraceSession(cwd, opts.sessionId), raw);
546
+ } else if (sub === 'current') {
547
+ const sessionId = getCurrentSessionId(cwd);
548
+ output({ session_id: sessionId, active: !!sessionId }, raw);
549
+ } else if (sub === 'list') {
550
+ output(listTraceSessions(cwd), raw);
551
+ } else if (sub === 'show') {
552
+ if (!opts.sessionId) { output({ error: 'Session ID required (--session <id>)' }, raw); return; }
553
+ output(readTraceSession(cwd, opts.sessionId), raw);
554
+ } else {
555
+ output({ error: 'Unknown trace subcommand. Available: init, log, end, current, list, show' }, raw);
556
+ }
557
+ }
558
+
559
+ function cmdOptimizeLearn(cwd, opts, raw) {
560
+ const sessionId = opts.sessionId || getCurrentSessionId(cwd);
561
+ if (!sessionId) {
562
+ output({ error: 'No trace session active. Start one with: pan-tools optimize trace init' }, raw);
563
+ return;
564
+ }
565
+
566
+ const report = generateLocalReport(cwd, sessionId);
567
+ if (report.error) { output(report, raw); return; }
568
+
569
+ // Persist as JSON analysis for the optimizer agent to read
570
+ const reportsDir = getReportsDir(cwd);
571
+ try { fs.mkdirSync(reportsDir, { recursive: true }); } catch {}
572
+ const reportName = `${sessionId}-analysis.json`;
573
+ const reportPath = path.join(reportsDir, reportName);
574
+ try {
575
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
576
+ } catch (e) {
577
+ output({ error: `Failed to write analysis: ${e.message}` }, raw);
578
+ return;
579
+ }
580
+
581
+ output({
582
+ session_id: sessionId,
583
+ analysis_path: path.join(PLANNING_DIR, OPTIMIZE_DIR, OPT_REPORTS_DIR, reportName).replace(/\\/g, '/'),
584
+ summary: report.summary,
585
+ top_error_patterns: report.error_patterns.slice(0, 5),
586
+ top_gap_patterns: report.gap_patterns.slice(0, 5),
587
+ top_memory_misses: report.memory_miss_patterns.slice(0, 5),
588
+ agent_stats: report.agent_stats,
589
+ next_step: 'Invoke pan-optimizer agent to generate optimization report from this analysis',
590
+ }, raw);
591
+ }
592
+
593
+ function cmdOptimizeApply(cwd, opts, raw) {
594
+ const reports = listOptimizationReports(cwd);
595
+ if (reports.error || reports.count === 0) {
596
+ output({ error: 'No optimization reports found. Run /pan:learn first.' }, raw);
597
+ return;
598
+ }
599
+
600
+ const reportPath = opts.reportPath || reports.reports[0].path;
601
+ const result = applyReportRecommendations(cwd, reportPath);
602
+ output({ report: path.basename(reportPath), ...result }, raw);
603
+ }
604
+
605
+ function cmdOptimizeStats(cwd, raw) {
606
+ output(getOptimizeStats(cwd), raw);
607
+ }
608
+
609
+ function cmdOptimizeList(cwd, raw) {
610
+ output(listOptimizationReports(cwd), raw);
611
+ }
612
+
613
+ // ─── Exports ─────────────────────────────────────────────────────────────────
614
+
615
+ module.exports = {
616
+ // Session management
617
+ initTraceSession,
618
+ getCurrentSessionId,
619
+ logTraceEvent,
620
+ endTraceSession,
621
+ readTraceSession,
622
+ listTraceSessions,
623
+ // Analysis
624
+ analyzeEvents,
625
+ generateLocalReport,
626
+ // Reports
627
+ listOptimizationReports,
628
+ parseAutoApplyBlock,
629
+ applyReportRecommendations,
630
+ deriveActionsFromAnalysis,
631
+ // Stats
632
+ getOptimizeStats,
633
+ // Commands
634
+ cmdOptimizeTrace,
635
+ cmdOptimizeLearn,
636
+ cmdOptimizeApply,
637
+ cmdOptimizeStats,
638
+ cmdOptimizeList,
639
+ // Path helpers (used by hook)
640
+ getOptimizeDir,
641
+ getTracesDir,
642
+ getReportsDir,
643
+ // Constants (exported for hook + tests)
644
+ OPTIMIZE_DIR,
645
+ TRACES_DIR,
646
+ OPT_REPORTS_DIR,
647
+ APPLIED_LOG,
648
+ TRACE_EVENT_FILE,
649
+ OPT_SESSION_FILE,
650
+ CURRENT_SESSION_FILE,
651
+ EVENT_TYPES,
652
+ IMPACT_LEVELS,
653
+ };