@kodrunhq/opencode-autopilot 1.15.2 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/bin/cli.ts +5 -0
  2. package/bin/inspect.ts +337 -0
  3. package/package.json +1 -1
  4. package/src/agents/autopilot.ts +7 -15
  5. package/src/health/checks.ts +29 -4
  6. package/src/index.ts +103 -11
  7. package/src/inspect/formatters.ts +225 -0
  8. package/src/inspect/repository.ts +882 -0
  9. package/src/kernel/database.ts +45 -0
  10. package/src/kernel/migrations.ts +62 -0
  11. package/src/kernel/repository.ts +571 -0
  12. package/src/kernel/schema.ts +122 -0
  13. package/src/kernel/types.ts +66 -0
  14. package/src/memory/capture.ts +221 -25
  15. package/src/memory/database.ts +74 -12
  16. package/src/memory/index.ts +17 -1
  17. package/src/memory/project-key.ts +6 -0
  18. package/src/memory/repository.ts +833 -42
  19. package/src/memory/retrieval.ts +83 -169
  20. package/src/memory/schemas.ts +39 -7
  21. package/src/memory/types.ts +4 -0
  22. package/src/observability/event-handlers.ts +28 -17
  23. package/src/observability/event-store.ts +29 -1
  24. package/src/observability/forensic-log.ts +159 -0
  25. package/src/observability/forensic-schemas.ts +69 -0
  26. package/src/observability/forensic-types.ts +10 -0
  27. package/src/observability/index.ts +21 -27
  28. package/src/observability/log-reader.ts +142 -111
  29. package/src/observability/log-writer.ts +41 -83
  30. package/src/observability/retention.ts +2 -2
  31. package/src/observability/session-logger.ts +36 -57
  32. package/src/observability/summary-generator.ts +31 -19
  33. package/src/observability/types.ts +12 -24
  34. package/src/orchestrator/contracts/invariants.ts +14 -0
  35. package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
  36. package/src/orchestrator/fallback/event-handler.ts +47 -3
  37. package/src/orchestrator/handlers/architect.ts +2 -1
  38. package/src/orchestrator/handlers/build.ts +55 -97
  39. package/src/orchestrator/handlers/retrospective.ts +2 -1
  40. package/src/orchestrator/handlers/types.ts +0 -1
  41. package/src/orchestrator/lesson-memory.ts +29 -9
  42. package/src/orchestrator/orchestration-logger.ts +37 -23
  43. package/src/orchestrator/phase.ts +8 -4
  44. package/src/orchestrator/state.ts +79 -17
  45. package/src/projects/database.ts +47 -0
  46. package/src/projects/repository.ts +264 -0
  47. package/src/projects/resolve.ts +301 -0
  48. package/src/projects/schemas.ts +30 -0
  49. package/src/projects/types.ts +12 -0
  50. package/src/review/memory.ts +29 -9
  51. package/src/tools/doctor.ts +26 -2
  52. package/src/tools/forensics.ts +7 -12
  53. package/src/tools/logs.ts +6 -5
  54. package/src/tools/memory-preferences.ts +157 -0
  55. package/src/tools/memory-status.ts +17 -96
  56. package/src/tools/orchestrate.ts +97 -81
  57. package/src/tools/pipeline-report.ts +3 -2
  58. package/src/tools/quick.ts +2 -2
  59. package/src/tools/review.ts +39 -6
  60. package/src/tools/session-stats.ts +3 -2
  61. package/src/utils/paths.ts +20 -1
@@ -0,0 +1,882 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { Database as SqliteDatabase } from "bun:sqlite";
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { OBSERVATION_TYPES } from "../memory/constants";
5
+ import { preferenceEvidenceSchema, preferenceSchema } from "../memory/schemas";
6
+ import type { Preference, PreferenceEvidence } from "../memory/types";
7
+ import { lessonMemorySchema } from "../orchestrator/lesson-schemas";
8
+ import type { Lesson } from "../orchestrator/lesson-types";
9
+ import {
10
+ getProjectByAnyPath,
11
+ getProjectById,
12
+ listProjectGitFingerprints,
13
+ listProjectPaths,
14
+ } from "../projects/repository";
15
+ import { projectRecordSchema } from "../projects/schemas";
16
+ import type { ProjectGitFingerprint, ProjectPathRecord, ProjectRecord } from "../projects/types";
17
+ import { getAutopilotDbPath } from "../utils/paths";
18
+
19
+ type InspectDbInput = Database | string | undefined;
20
+
21
+ interface ProjectRow {
22
+ readonly id: string;
23
+ readonly path: string;
24
+ readonly name: string;
25
+ readonly first_seen_at: string | null;
26
+ readonly last_updated: string;
27
+ }
28
+
29
+ interface ProjectCountRow extends ProjectRow {
30
+ readonly path_count: number;
31
+ readonly fingerprint_count: number;
32
+ readonly run_count: number;
33
+ readonly event_count: number;
34
+ readonly observation_count: number;
35
+ readonly has_active_review_state: number;
36
+ readonly has_review_memory: number;
37
+ }
38
+
39
+ interface PipelineRunSummaryRow {
40
+ readonly project_id: string;
41
+ readonly project_name: string;
42
+ readonly project_path: string;
43
+ readonly run_id: string;
44
+ readonly status: string;
45
+ readonly current_phase: string | null;
46
+ readonly idea: string;
47
+ readonly state_revision: number;
48
+ readonly started_at: string;
49
+ readonly last_updated_at: string;
50
+ readonly failure_phase: string | null;
51
+ readonly failure_agent: string | null;
52
+ readonly failure_message: string | null;
53
+ readonly last_successful_phase: string | null;
54
+ }
55
+
56
+ interface ForensicEventSummaryRow {
57
+ readonly event_id: number;
58
+ readonly project_id: string;
59
+ readonly project_name: string;
60
+ readonly project_path: string;
61
+ readonly timestamp: string;
62
+ readonly domain: string;
63
+ readonly run_id: string | null;
64
+ readonly session_id: string | null;
65
+ readonly parent_session_id: string | null;
66
+ readonly phase: string | null;
67
+ readonly dispatch_id: string | null;
68
+ readonly task_id: number | null;
69
+ readonly agent: string | null;
70
+ readonly type: string;
71
+ readonly code: string | null;
72
+ readonly message: string | null;
73
+ readonly payload_json: string;
74
+ }
75
+
76
+ interface LessonMemoryRow {
77
+ readonly project_id: string;
78
+ readonly project_name: string;
79
+ readonly project_path: string;
80
+ readonly state_json: string;
81
+ readonly last_updated_at: string | null;
82
+ }
83
+
84
+ interface PreferenceRow {
85
+ readonly id: string;
86
+ readonly key: string;
87
+ readonly value: string;
88
+ readonly scope?: string;
89
+ readonly project_id?: string | null;
90
+ readonly status?: string;
91
+ readonly confidence: number;
92
+ readonly source_session: string | null;
93
+ readonly created_at: string;
94
+ readonly last_updated: string;
95
+ readonly evidence_count?: number;
96
+ }
97
+
98
+ interface PreferenceEvidenceRow {
99
+ readonly id: string;
100
+ readonly preference_id: string;
101
+ readonly session_id: string | null;
102
+ readonly run_id: string | null;
103
+ readonly statement: string;
104
+ readonly statement_hash: string;
105
+ readonly confidence: number;
106
+ readonly confirmed: number;
107
+ readonly created_at: string;
108
+ }
109
+
110
+ interface ObservationRow {
111
+ readonly id: number;
112
+ readonly project_id: string | null;
113
+ readonly project_name: string | null;
114
+ readonly session_id: string;
115
+ readonly type: string;
116
+ readonly summary: string;
117
+ readonly confidence: number;
118
+ readonly created_at: string;
119
+ }
120
+
121
+ interface CountRow {
122
+ readonly cnt: number;
123
+ }
124
+
125
+ interface TypeCountRow {
126
+ readonly type: string;
127
+ readonly cnt: number;
128
+ }
129
+
130
+ function tableExists(db: Database, tableName: string): boolean {
131
+ const row = db
132
+ .query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
133
+ .get(tableName) as { name?: string } | null;
134
+ return row?.name === tableName;
135
+ }
136
+
137
+ function countSubquery(tableName: string, alias: string): string {
138
+ return `SELECT project_id, COUNT(*) AS ${alias} FROM ${tableName} GROUP BY project_id`;
139
+ }
140
+
141
+ function emptyCountSubquery(alias: string): string {
142
+ return `SELECT NULL AS project_id, 0 AS ${alias} WHERE 0`;
143
+ }
144
+
145
+ export interface InspectProjectSummary {
146
+ readonly id: string;
147
+ readonly name: string;
148
+ readonly path: string;
149
+ readonly firstSeenAt: string;
150
+ readonly lastUpdated: string;
151
+ readonly pathCount: number;
152
+ readonly fingerprintCount: number;
153
+ readonly runCount: number;
154
+ readonly eventCount: number;
155
+ readonly observationCount: number;
156
+ readonly lessonCount: number;
157
+ readonly hasActiveReviewState: boolean;
158
+ readonly hasReviewMemory: boolean;
159
+ }
160
+
161
+ export interface InspectProjectDetails {
162
+ readonly project: InspectProjectSummary;
163
+ readonly paths: readonly ProjectPathRecord[];
164
+ readonly gitFingerprints: readonly ProjectGitFingerprint[];
165
+ }
166
+
167
+ export interface InspectRunSummary {
168
+ readonly projectId: string;
169
+ readonly projectName: string;
170
+ readonly projectPath: string;
171
+ readonly runId: string;
172
+ readonly status: string;
173
+ readonly currentPhase: string | null;
174
+ readonly idea: string;
175
+ readonly stateRevision: number;
176
+ readonly startedAt: string;
177
+ readonly lastUpdatedAt: string;
178
+ readonly failurePhase: string | null;
179
+ readonly failureAgent: string | null;
180
+ readonly failureMessage: string | null;
181
+ readonly lastSuccessfulPhase: string | null;
182
+ }
183
+
184
+ export interface InspectEventSummary {
185
+ readonly eventId: number;
186
+ readonly projectId: string;
187
+ readonly projectName: string;
188
+ readonly projectPath: string;
189
+ readonly timestamp: string;
190
+ readonly domain: string;
191
+ readonly runId: string | null;
192
+ readonly sessionId: string | null;
193
+ readonly parentSessionId: string | null;
194
+ readonly phase: string | null;
195
+ readonly dispatchId: string | null;
196
+ readonly taskId: number | null;
197
+ readonly agent: string | null;
198
+ readonly type: string;
199
+ readonly code: string | null;
200
+ readonly message: string | null;
201
+ readonly payload: Record<string, unknown>;
202
+ }
203
+
204
+ export interface InspectLessonSummary {
205
+ readonly projectId: string;
206
+ readonly projectName: string;
207
+ readonly projectPath: string;
208
+ readonly extractedAt: string;
209
+ readonly domain: Lesson["domain"];
210
+ readonly sourcePhase: Lesson["sourcePhase"];
211
+ readonly content: string;
212
+ readonly lastUpdatedAt: string | null;
213
+ }
214
+
215
+ export interface InspectPreferenceSummary extends Preference {
216
+ readonly evidence: readonly PreferenceEvidence[];
217
+ }
218
+
219
+ export interface InspectObservationSummary {
220
+ readonly id: number;
221
+ readonly projectId: string | null;
222
+ readonly projectName: string | null;
223
+ readonly sessionId: string;
224
+ readonly type: string;
225
+ readonly summary: string;
226
+ readonly confidence: number;
227
+ readonly createdAt: string;
228
+ }
229
+
230
+ export interface InspectMemoryOverview {
231
+ readonly stats: {
232
+ readonly totalObservations: number;
233
+ readonly totalProjects: number;
234
+ readonly totalPreferences: number;
235
+ readonly storageSizeKb: number;
236
+ readonly observationsByType: Readonly<Record<string, number>>;
237
+ };
238
+ readonly recentObservations: readonly InspectObservationSummary[];
239
+ readonly preferences: readonly InspectPreferenceSummary[];
240
+ }
241
+
242
+ export interface InspectRunQuery {
243
+ readonly projectRef?: string;
244
+ readonly limit?: number;
245
+ }
246
+
247
+ export interface InspectEventQuery {
248
+ readonly projectRef?: string;
249
+ readonly runId?: string;
250
+ readonly sessionId?: string;
251
+ readonly type?: string;
252
+ readonly limit?: number;
253
+ }
254
+
255
+ export interface InspectLessonQuery {
256
+ readonly projectRef?: string;
257
+ readonly limit?: number;
258
+ }
259
+
260
+ function rowToProject(row: ProjectRow): ProjectRecord {
261
+ return projectRecordSchema.parse({
262
+ id: row.id,
263
+ path: row.path,
264
+ name: row.name,
265
+ firstSeenAt: row.first_seen_at ?? row.last_updated,
266
+ lastUpdated: row.last_updated,
267
+ });
268
+ }
269
+
270
+ function safeParseJson(value: string): Record<string, unknown> {
271
+ try {
272
+ const parsed = JSON.parse(value) as unknown;
273
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
274
+ ? (parsed as Record<string, unknown>)
275
+ : {};
276
+ } catch {
277
+ return {};
278
+ }
279
+ }
280
+
281
+ function rowToPreference(row: PreferenceRow): InspectPreferenceSummary {
282
+ return preferenceSchema.parse({
283
+ id: row.id,
284
+ key: row.key,
285
+ value: row.value,
286
+ scope: row.scope ?? "global",
287
+ projectId: row.project_id ?? null,
288
+ status: row.status ?? "confirmed",
289
+ confidence: row.confidence,
290
+ evidenceCount: row.evidence_count ?? 0,
291
+ sourceSession: row.source_session,
292
+ createdAt: row.created_at,
293
+ lastUpdated: row.last_updated,
294
+ }) as InspectPreferenceSummary;
295
+ }
296
+
297
+ function rowToPreferenceEvidence(row: PreferenceEvidenceRow): PreferenceEvidence {
298
+ return preferenceEvidenceSchema.parse({
299
+ id: row.id,
300
+ preferenceId: row.preference_id,
301
+ sessionId: row.session_id,
302
+ runId: row.run_id,
303
+ statement: row.statement,
304
+ statementHash: row.statement_hash,
305
+ confidence: row.confidence,
306
+ confirmed: row.confirmed === 1,
307
+ createdAt: row.created_at,
308
+ });
309
+ }
310
+
311
+ function countLessonsByProject(rows: readonly LessonMemoryRow[]): ReadonlyMap<string, number> {
312
+ const counts = new Map<string, number>();
313
+
314
+ for (const row of rows) {
315
+ const parsed = lessonMemorySchema.safeParse(safeParseJson(row.state_json));
316
+ if (!parsed.success) {
317
+ counts.set(row.project_id, 0);
318
+ continue;
319
+ }
320
+ counts.set(row.project_id, parsed.data.lessons.length);
321
+ }
322
+
323
+ return counts;
324
+ }
325
+
326
+ function openInspectDb(input: InspectDbInput): {
327
+ readonly db: Database;
328
+ readonly dbPath: string | null;
329
+ close: () => void;
330
+ } | null {
331
+ if (input instanceof SqliteDatabase) {
332
+ return {
333
+ db: input,
334
+ dbPath: null,
335
+ close() {},
336
+ };
337
+ }
338
+
339
+ const dbPath = typeof input === "string" ? input : getAutopilotDbPath();
340
+ if (!existsSync(dbPath)) {
341
+ return null;
342
+ }
343
+
344
+ const db = new SqliteDatabase(dbPath, { readonly: true });
345
+ return {
346
+ db,
347
+ dbPath,
348
+ close() {
349
+ db.close();
350
+ },
351
+ };
352
+ }
353
+
354
+ function withInspectDb<T>(
355
+ input: InspectDbInput,
356
+ onMissing: T,
357
+ callback: (db: Database, dbPath: string | null) => T,
358
+ ): T {
359
+ const handle = openInspectDb(input);
360
+ if (handle === null) {
361
+ return onMissing;
362
+ }
363
+
364
+ try {
365
+ return callback(handle.db, handle.dbPath);
366
+ } finally {
367
+ handle.close();
368
+ }
369
+ }
370
+
371
+ function resolveProjectReferenceInDb(projectRef: string, db: Database): ProjectRecord | null {
372
+ const trimmed = projectRef.trim();
373
+ if (trimmed.length === 0) {
374
+ return null;
375
+ }
376
+
377
+ const byId = getProjectById(trimmed, db);
378
+ if (byId !== null) {
379
+ return byId;
380
+ }
381
+
382
+ const byPath = getProjectByAnyPath(trimmed, db);
383
+ if (byPath !== null) {
384
+ return byPath;
385
+ }
386
+
387
+ const nameMatches = db
388
+ .query("SELECT * FROM projects WHERE name = ? ORDER BY last_updated DESC, id DESC LIMIT 2")
389
+ .all(trimmed) as ProjectRow[];
390
+ if (nameMatches.length === 1) {
391
+ return rowToProject(nameMatches[0]);
392
+ }
393
+ if (nameMatches.length > 1) {
394
+ throw new Error(`Project reference '${trimmed}' is ambiguous; use project id or path.`);
395
+ }
396
+
397
+ return null;
398
+ }
399
+
400
+ function buildProjectSummary(
401
+ row: ProjectCountRow,
402
+ lessonCounts: ReadonlyMap<string, number>,
403
+ ): InspectProjectSummary {
404
+ return Object.freeze({
405
+ id: row.id,
406
+ name: row.name,
407
+ path: row.path,
408
+ firstSeenAt: row.first_seen_at ?? row.last_updated,
409
+ lastUpdated: row.last_updated,
410
+ pathCount: row.path_count,
411
+ fingerprintCount: row.fingerprint_count,
412
+ runCount: row.run_count,
413
+ eventCount: row.event_count,
414
+ observationCount: row.observation_count,
415
+ lessonCount: lessonCounts.get(row.id) ?? 0,
416
+ hasActiveReviewState: row.has_active_review_state === 1,
417
+ hasReviewMemory: row.has_review_memory === 1,
418
+ });
419
+ }
420
+
421
+ function readProjectRows(db: Database): readonly ProjectCountRow[] {
422
+ const runCounts = tableExists(db, "pipeline_runs")
423
+ ? countSubquery("pipeline_runs", "run_count")
424
+ : emptyCountSubquery("run_count");
425
+ const eventCounts = tableExists(db, "forensic_events")
426
+ ? countSubquery("forensic_events", "event_count")
427
+ : emptyCountSubquery("event_count");
428
+ const observationCounts = tableExists(db, "observations")
429
+ ? countSubquery("observations", "observation_count")
430
+ : emptyCountSubquery("observation_count");
431
+ const activeReviewJoin = tableExists(db, "active_review_state")
432
+ ? "LEFT JOIN active_review_state ars ON ars.project_id = p.id"
433
+ : "LEFT JOIN (SELECT NULL AS project_id WHERE 0) ars ON ars.project_id = p.id";
434
+ const reviewMemoryJoin = tableExists(db, "project_review_memory")
435
+ ? "LEFT JOIN project_review_memory prm ON prm.project_id = p.id"
436
+ : "LEFT JOIN (SELECT NULL AS project_id WHERE 0) prm ON prm.project_id = p.id";
437
+
438
+ return Object.freeze(
439
+ db
440
+ .query(
441
+ `SELECT
442
+ p.*,
443
+ COALESCE(path_counts.path_count, 0) AS path_count,
444
+ COALESCE(fingerprint_counts.fingerprint_count, 0) AS fingerprint_count,
445
+ COALESCE(run_counts.run_count, 0) AS run_count,
446
+ COALESCE(event_counts.event_count, 0) AS event_count,
447
+ COALESCE(observation_counts.observation_count, 0) AS observation_count,
448
+ CASE WHEN ars.project_id IS NULL THEN 0 ELSE 1 END AS has_active_review_state,
449
+ CASE WHEN prm.project_id IS NULL THEN 0 ELSE 1 END AS has_review_memory
450
+ FROM projects p
451
+ LEFT JOIN (
452
+ SELECT project_id, COUNT(*) AS path_count
453
+ FROM project_paths
454
+ GROUP BY project_id
455
+ ) AS path_counts ON path_counts.project_id = p.id
456
+ LEFT JOIN (
457
+ SELECT project_id, COUNT(*) AS fingerprint_count
458
+ FROM project_git_fingerprints
459
+ GROUP BY project_id
460
+ ) AS fingerprint_counts ON fingerprint_counts.project_id = p.id
461
+ LEFT JOIN (${runCounts}) AS run_counts ON run_counts.project_id = p.id
462
+ LEFT JOIN (${eventCounts}) AS event_counts ON event_counts.project_id = p.id
463
+ LEFT JOIN (${observationCounts}) AS observation_counts ON observation_counts.project_id = p.id
464
+ ${activeReviewJoin}
465
+ ${reviewMemoryJoin}
466
+ ORDER BY p.last_updated DESC, p.name ASC, p.id ASC`,
467
+ )
468
+ .all() as ProjectCountRow[],
469
+ );
470
+ }
471
+
472
+ export function listProjects(input?: InspectDbInput): readonly InspectProjectSummary[] {
473
+ return withInspectDb(input, Object.freeze([]), (db) => {
474
+ const projectRows = readProjectRows(db);
475
+ const lessonRows = tableExists(db, "project_lesson_memory")
476
+ ? (db
477
+ .query(
478
+ `SELECT plm.project_id, p.name AS project_name, p.path AS project_path, plm.state_json, plm.last_updated_at
479
+ FROM project_lesson_memory plm
480
+ JOIN projects p ON p.id = plm.project_id`,
481
+ )
482
+ .all() as LessonMemoryRow[])
483
+ : [];
484
+ const lessonCounts = countLessonsByProject(lessonRows);
485
+ return Object.freeze(projectRows.map((row) => buildProjectSummary(row, lessonCounts)));
486
+ });
487
+ }
488
+
489
+ export function getProjectDetails(
490
+ projectRef: string,
491
+ input?: InspectDbInput,
492
+ ): InspectProjectDetails | null {
493
+ return withInspectDb(input, null, (db) => {
494
+ const resolvedProject = resolveProjectReferenceInDb(projectRef, db);
495
+ if (resolvedProject === null) {
496
+ return null;
497
+ }
498
+
499
+ const projectRows = readProjectRows(db);
500
+ const projectRow = projectRows.find((row) => row.id === resolvedProject.id);
501
+ if (projectRow === undefined) {
502
+ return null;
503
+ }
504
+
505
+ const lessonRows = tableExists(db, "project_lesson_memory")
506
+ ? (db
507
+ .query(
508
+ `SELECT plm.project_id, p.name AS project_name, p.path AS project_path, plm.state_json, plm.last_updated_at
509
+ FROM project_lesson_memory plm
510
+ JOIN projects p ON p.id = plm.project_id
511
+ WHERE plm.project_id = ?`,
512
+ )
513
+ .all(resolvedProject.id) as LessonMemoryRow[])
514
+ : [];
515
+ const lessonCounts = countLessonsByProject(lessonRows);
516
+
517
+ return Object.freeze({
518
+ project: buildProjectSummary(projectRow, lessonCounts),
519
+ paths: listProjectPaths(resolvedProject.id, db),
520
+ gitFingerprints: listProjectGitFingerprints(resolvedProject.id, db),
521
+ });
522
+ });
523
+ }
524
+
525
+ export function listProjectPathsByReference(
526
+ projectRef: string,
527
+ input?: InspectDbInput,
528
+ ): readonly ProjectPathRecord[] {
529
+ return withInspectDb(input, Object.freeze([]), (db) => {
530
+ const resolvedProject = resolveProjectReferenceInDb(projectRef, db);
531
+ if (resolvedProject === null) {
532
+ return Object.freeze([]);
533
+ }
534
+ return listProjectPaths(resolvedProject.id, db);
535
+ });
536
+ }
537
+
538
+ export function listRuns(
539
+ query: InspectRunQuery = {},
540
+ input?: InspectDbInput,
541
+ ): readonly InspectRunSummary[] {
542
+ return withInspectDb(input, Object.freeze([]), (db) => {
543
+ if (!tableExists(db, "pipeline_runs")) {
544
+ return Object.freeze([]);
545
+ }
546
+
547
+ const limit = Math.max(1, query.limit ?? 20);
548
+ const resolvedProject =
549
+ typeof query.projectRef === "string"
550
+ ? resolveProjectReferenceInDb(query.projectRef, db)
551
+ : null;
552
+ if (query.projectRef && resolvedProject === null) {
553
+ return Object.freeze([]);
554
+ }
555
+
556
+ const rows = resolvedProject
557
+ ? (db
558
+ .query(
559
+ `SELECT pr.*, p.name AS project_name, p.path AS project_path
560
+ FROM pipeline_runs pr
561
+ JOIN projects p ON p.id = pr.project_id
562
+ WHERE pr.project_id = ?
563
+ ORDER BY pr.last_updated_at DESC, pr.run_id DESC
564
+ LIMIT ?`,
565
+ )
566
+ .all(resolvedProject.id, limit) as PipelineRunSummaryRow[])
567
+ : (db
568
+ .query(
569
+ `SELECT pr.*, p.name AS project_name, p.path AS project_path
570
+ FROM pipeline_runs pr
571
+ JOIN projects p ON p.id = pr.project_id
572
+ ORDER BY pr.last_updated_at DESC, pr.run_id DESC
573
+ LIMIT ?`,
574
+ )
575
+ .all(limit) as PipelineRunSummaryRow[]);
576
+
577
+ return Object.freeze(
578
+ rows.map((row) =>
579
+ Object.freeze({
580
+ projectId: row.project_id,
581
+ projectName: row.project_name,
582
+ projectPath: row.project_path,
583
+ runId: row.run_id,
584
+ status: row.status,
585
+ currentPhase: row.current_phase,
586
+ idea: row.idea,
587
+ stateRevision: row.state_revision,
588
+ startedAt: row.started_at,
589
+ lastUpdatedAt: row.last_updated_at,
590
+ failurePhase: row.failure_phase,
591
+ failureAgent: row.failure_agent,
592
+ failureMessage: row.failure_message,
593
+ lastSuccessfulPhase: row.last_successful_phase,
594
+ }),
595
+ ),
596
+ );
597
+ });
598
+ }
599
+
600
+ export function listEvents(
601
+ query: InspectEventQuery = {},
602
+ input?: InspectDbInput,
603
+ ): readonly InspectEventSummary[] {
604
+ return withInspectDb(input, Object.freeze([]), (db) => {
605
+ if (!tableExists(db, "forensic_events")) {
606
+ return Object.freeze([]);
607
+ }
608
+
609
+ const limit = Math.max(1, query.limit ?? 50);
610
+ const resolvedProject =
611
+ typeof query.projectRef === "string"
612
+ ? resolveProjectReferenceInDb(query.projectRef, db)
613
+ : null;
614
+ if (query.projectRef && resolvedProject === null) {
615
+ return Object.freeze([]);
616
+ }
617
+
618
+ const conditions: string[] = [];
619
+ const params: Array<string | number> = [];
620
+
621
+ if (resolvedProject !== null) {
622
+ conditions.push("fe.project_id = ?");
623
+ params.push(resolvedProject.id);
624
+ }
625
+ if (query.runId) {
626
+ conditions.push("fe.run_id = ?");
627
+ params.push(query.runId);
628
+ }
629
+ if (query.sessionId) {
630
+ conditions.push("fe.session_id = ?");
631
+ params.push(query.sessionId);
632
+ }
633
+ if (query.type) {
634
+ conditions.push("fe.type = ?");
635
+ params.push(query.type);
636
+ }
637
+
638
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
639
+ const rows = db
640
+ .query(
641
+ `SELECT
642
+ fe.*,
643
+ p.name AS project_name,
644
+ p.path AS project_path
645
+ FROM forensic_events fe
646
+ JOIN projects p ON p.id = fe.project_id
647
+ ${whereClause}
648
+ ORDER BY fe.timestamp DESC, fe.event_id DESC
649
+ LIMIT ?`,
650
+ )
651
+ .all(...params, limit) as ForensicEventSummaryRow[];
652
+
653
+ return Object.freeze(
654
+ rows.map((row) =>
655
+ Object.freeze({
656
+ eventId: row.event_id,
657
+ projectId: row.project_id,
658
+ projectName: row.project_name,
659
+ projectPath: row.project_path,
660
+ timestamp: row.timestamp,
661
+ domain: row.domain,
662
+ runId: row.run_id,
663
+ sessionId: row.session_id,
664
+ parentSessionId: row.parent_session_id,
665
+ phase: row.phase,
666
+ dispatchId: row.dispatch_id,
667
+ taskId: row.task_id,
668
+ agent: row.agent,
669
+ type: row.type,
670
+ code: row.code,
671
+ message: row.message,
672
+ payload: safeParseJson(row.payload_json),
673
+ }),
674
+ ),
675
+ );
676
+ });
677
+ }
678
+
679
+ export function listLessons(
680
+ query: InspectLessonQuery = {},
681
+ input?: InspectDbInput,
682
+ ): readonly InspectLessonSummary[] {
683
+ return withInspectDb(input, Object.freeze([]), (db) => {
684
+ if (!tableExists(db, "project_lesson_memory")) {
685
+ return Object.freeze([]);
686
+ }
687
+
688
+ const limit = Math.max(1, query.limit ?? 50);
689
+ const resolvedProject =
690
+ typeof query.projectRef === "string"
691
+ ? resolveProjectReferenceInDb(query.projectRef, db)
692
+ : null;
693
+ if (query.projectRef && resolvedProject === null) {
694
+ return Object.freeze([]);
695
+ }
696
+
697
+ const rows = resolvedProject
698
+ ? (db
699
+ .query(
700
+ `SELECT plm.project_id, p.name AS project_name, p.path AS project_path, plm.state_json, plm.last_updated_at
701
+ FROM project_lesson_memory plm
702
+ JOIN projects p ON p.id = plm.project_id
703
+ WHERE plm.project_id = ?`,
704
+ )
705
+ .all(resolvedProject.id) as LessonMemoryRow[])
706
+ : (db
707
+ .query(
708
+ `SELECT plm.project_id, p.name AS project_name, p.path AS project_path, plm.state_json, plm.last_updated_at
709
+ FROM project_lesson_memory plm
710
+ JOIN projects p ON p.id = plm.project_id`,
711
+ )
712
+ .all() as LessonMemoryRow[]);
713
+
714
+ const lessons: InspectLessonSummary[] = [];
715
+ for (const row of rows) {
716
+ const parsed = lessonMemorySchema.safeParse(safeParseJson(row.state_json));
717
+ if (!parsed.success) {
718
+ continue;
719
+ }
720
+
721
+ for (const lesson of parsed.data.lessons) {
722
+ lessons.push(
723
+ Object.freeze({
724
+ projectId: row.project_id,
725
+ projectName: row.project_name,
726
+ projectPath: row.project_path,
727
+ extractedAt: lesson.extractedAt,
728
+ domain: lesson.domain,
729
+ sourcePhase: lesson.sourcePhase,
730
+ content: lesson.content,
731
+ lastUpdatedAt: row.last_updated_at,
732
+ }),
733
+ );
734
+ }
735
+ }
736
+
737
+ lessons.sort((a, b) => b.extractedAt.localeCompare(a.extractedAt));
738
+ return Object.freeze(lessons.slice(0, limit));
739
+ });
740
+ }
741
+
742
+ export function listPreferences(input?: InspectDbInput): readonly InspectPreferenceSummary[] {
743
+ return withInspectDb(input, Object.freeze([]), (db) => {
744
+ if (tableExists(db, "preference_records")) {
745
+ const rows = db
746
+ .query(
747
+ `SELECT
748
+ pr.*,
749
+ COALESCE(evidence_counts.evidence_count, 0) AS evidence_count
750
+ FROM preference_records pr
751
+ LEFT JOIN (
752
+ SELECT preference_id, COUNT(*) AS evidence_count
753
+ FROM preference_evidence
754
+ GROUP BY preference_id
755
+ ) AS evidence_counts ON evidence_counts.preference_id = pr.id
756
+ ORDER BY pr.last_updated DESC, pr.key ASC, pr.id ASC`,
757
+ )
758
+ .all() as PreferenceRow[];
759
+
760
+ return Object.freeze(
761
+ rows.map((row) => {
762
+ const evidence = tableExists(db, "preference_evidence")
763
+ ? (db
764
+ .query(
765
+ `SELECT *
766
+ FROM preference_evidence
767
+ WHERE preference_id = ?
768
+ ORDER BY created_at DESC, id DESC`,
769
+ )
770
+ .all(row.id) as PreferenceEvidenceRow[])
771
+ : [];
772
+
773
+ return Object.freeze({
774
+ ...rowToPreference(row),
775
+ evidence: Object.freeze(evidence.map(rowToPreferenceEvidence)),
776
+ });
777
+ }),
778
+ );
779
+ }
780
+
781
+ const rows = db
782
+ .query("SELECT * FROM preferences ORDER BY last_updated DESC, key ASC")
783
+ .all() as PreferenceRow[];
784
+ return Object.freeze(
785
+ rows.map((row) =>
786
+ Object.freeze({
787
+ ...rowToPreference(row),
788
+ evidence: Object.freeze([]),
789
+ }),
790
+ ),
791
+ );
792
+ });
793
+ }
794
+
795
+ export function getMemoryOverview(input?: InspectDbInput): InspectMemoryOverview {
796
+ return withInspectDb(
797
+ input,
798
+ Object.freeze({
799
+ stats: Object.freeze({
800
+ totalObservations: 0,
801
+ totalProjects: 0,
802
+ totalPreferences: 0,
803
+ storageSizeKb: 0,
804
+ observationsByType: Object.freeze(
805
+ Object.fromEntries(OBSERVATION_TYPES.map((type) => [type, 0])),
806
+ ),
807
+ }),
808
+ recentObservations: Object.freeze([]),
809
+ preferences: Object.freeze([]),
810
+ }),
811
+ (db, dbPath) => {
812
+ const totalObservations = (
813
+ db.query("SELECT COUNT(*) as cnt FROM observations").get() as CountRow
814
+ ).cnt;
815
+ const totalProjects = (db.query("SELECT COUNT(*) as cnt FROM projects").get() as CountRow)
816
+ .cnt;
817
+ const totalPreferences = (
818
+ db
819
+ .query(
820
+ tableExists(db, "preference_records")
821
+ ? "SELECT COUNT(*) as cnt FROM preference_records"
822
+ : "SELECT COUNT(*) as cnt FROM preferences",
823
+ )
824
+ .get() as CountRow
825
+ ).cnt;
826
+
827
+ const typeCounts = Object.fromEntries(OBSERVATION_TYPES.map((type) => [type, 0]));
828
+ const typeRows = db
829
+ .query("SELECT type, COUNT(*) as cnt FROM observations GROUP BY type")
830
+ .all() as TypeCountRow[];
831
+ for (const row of typeRows) {
832
+ typeCounts[row.type] = row.cnt;
833
+ }
834
+
835
+ const recentRows = db
836
+ .query(
837
+ `SELECT o.id, o.project_id, p.name AS project_name, o.session_id, o.type, o.summary, o.confidence, o.created_at
838
+ FROM observations o
839
+ LEFT JOIN projects p ON p.id = o.project_id
840
+ ORDER BY o.created_at DESC, o.id DESC
841
+ LIMIT 10`,
842
+ )
843
+ .all() as ObservationRow[];
844
+
845
+ const storageSizeKb =
846
+ dbPath === null
847
+ ? 0
848
+ : (() => {
849
+ try {
850
+ return Math.round(statSync(dbPath).size / 1024);
851
+ } catch {
852
+ return 0;
853
+ }
854
+ })();
855
+
856
+ return Object.freeze({
857
+ stats: Object.freeze({
858
+ totalObservations,
859
+ totalProjects,
860
+ totalPreferences,
861
+ storageSizeKb,
862
+ observationsByType: Object.freeze(typeCounts),
863
+ }),
864
+ recentObservations: Object.freeze(
865
+ recentRows.map((row) =>
866
+ Object.freeze({
867
+ id: row.id,
868
+ projectId: row.project_id,
869
+ projectName: row.project_name,
870
+ sessionId: row.session_id,
871
+ type: row.type,
872
+ summary: row.summary,
873
+ confidence: row.confidence,
874
+ createdAt: row.created_at,
875
+ }),
876
+ ),
877
+ ),
878
+ preferences: listPreferences(db),
879
+ });
880
+ },
881
+ );
882
+ }