@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.
- package/bin/cli.ts +5 -0
- package/bin/inspect.ts +337 -0
- package/package.json +1 -1
- package/src/agents/autopilot.ts +7 -15
- package/src/health/checks.ts +29 -4
- package/src/index.ts +103 -11
- package/src/inspect/formatters.ts +225 -0
- package/src/inspect/repository.ts +882 -0
- package/src/kernel/database.ts +45 -0
- package/src/kernel/migrations.ts +62 -0
- package/src/kernel/repository.ts +571 -0
- package/src/kernel/schema.ts +122 -0
- package/src/kernel/types.ts +66 -0
- package/src/memory/capture.ts +221 -25
- package/src/memory/database.ts +74 -12
- package/src/memory/index.ts +17 -1
- package/src/memory/project-key.ts +6 -0
- package/src/memory/repository.ts +833 -42
- package/src/memory/retrieval.ts +83 -169
- package/src/memory/schemas.ts +39 -7
- package/src/memory/types.ts +4 -0
- package/src/observability/event-handlers.ts +28 -17
- package/src/observability/event-store.ts +29 -1
- package/src/observability/forensic-log.ts +159 -0
- package/src/observability/forensic-schemas.ts +69 -0
- package/src/observability/forensic-types.ts +10 -0
- package/src/observability/index.ts +21 -27
- package/src/observability/log-reader.ts +142 -111
- package/src/observability/log-writer.ts +41 -83
- package/src/observability/retention.ts +2 -2
- package/src/observability/session-logger.ts +36 -57
- package/src/observability/summary-generator.ts +31 -19
- package/src/observability/types.ts +12 -24
- package/src/orchestrator/contracts/invariants.ts +14 -0
- package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
- package/src/orchestrator/fallback/event-handler.ts +47 -3
- package/src/orchestrator/handlers/architect.ts +2 -1
- package/src/orchestrator/handlers/build.ts +55 -97
- package/src/orchestrator/handlers/retrospective.ts +2 -1
- package/src/orchestrator/handlers/types.ts +0 -1
- package/src/orchestrator/lesson-memory.ts +29 -9
- package/src/orchestrator/orchestration-logger.ts +37 -23
- package/src/orchestrator/phase.ts +8 -4
- package/src/orchestrator/state.ts +79 -17
- package/src/projects/database.ts +47 -0
- package/src/projects/repository.ts +264 -0
- package/src/projects/resolve.ts +301 -0
- package/src/projects/schemas.ts +30 -0
- package/src/projects/types.ts +12 -0
- package/src/review/memory.ts +29 -9
- package/src/tools/doctor.ts +26 -2
- package/src/tools/forensics.ts +7 -12
- package/src/tools/logs.ts +6 -5
- package/src/tools/memory-preferences.ts +157 -0
- package/src/tools/memory-status.ts +17 -96
- package/src/tools/orchestrate.ts +97 -81
- package/src/tools/pipeline-report.ts +3 -2
- package/src/tools/quick.ts +2 -2
- package/src/tools/review.ts +39 -6
- package/src/tools/session-stats.ts +3 -2
- 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
|
+
}
|