@kodrunhq/opencode-autopilot 1.15.2 → 1.17.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 (93) 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/config/index.ts +29 -0
  6. package/src/config/migrations.ts +196 -0
  7. package/src/config/v7.ts +45 -0
  8. package/src/config.ts +3 -3
  9. package/src/health/checks.ts +126 -4
  10. package/src/health/types.ts +1 -1
  11. package/src/index.ts +128 -13
  12. package/src/inspect/formatters.ts +225 -0
  13. package/src/inspect/repository.ts +882 -0
  14. package/src/kernel/database.ts +45 -0
  15. package/src/kernel/migrations.ts +62 -0
  16. package/src/kernel/repository.ts +571 -0
  17. package/src/kernel/schema.ts +122 -0
  18. package/src/kernel/transaction.ts +48 -0
  19. package/src/kernel/types.ts +65 -0
  20. package/src/logging/domains.ts +39 -0
  21. package/src/logging/forensic-writer.ts +177 -0
  22. package/src/logging/index.ts +4 -0
  23. package/src/logging/logger.ts +44 -0
  24. package/src/logging/performance.ts +59 -0
  25. package/src/logging/rotation.ts +261 -0
  26. package/src/logging/types.ts +33 -0
  27. package/src/memory/capture-utils.ts +149 -0
  28. package/src/memory/capture.ts +82 -67
  29. package/src/memory/database.ts +74 -12
  30. package/src/memory/decay.ts +11 -2
  31. package/src/memory/index.ts +17 -1
  32. package/src/memory/injector.ts +4 -1
  33. package/src/memory/lessons.ts +85 -0
  34. package/src/memory/observations.ts +177 -0
  35. package/src/memory/preferences.ts +718 -0
  36. package/src/memory/project-key.ts +6 -0
  37. package/src/memory/projects.ts +83 -0
  38. package/src/memory/repository.ts +52 -216
  39. package/src/memory/retrieval.ts +88 -170
  40. package/src/memory/schemas.ts +39 -7
  41. package/src/memory/types.ts +4 -0
  42. package/src/observability/context-display.ts +8 -0
  43. package/src/observability/event-handlers.ts +69 -20
  44. package/src/observability/event-store.ts +29 -1
  45. package/src/observability/forensic-log.ts +167 -0
  46. package/src/observability/forensic-schemas.ts +77 -0
  47. package/src/observability/forensic-types.ts +10 -0
  48. package/src/observability/index.ts +21 -27
  49. package/src/observability/log-reader.ts +161 -111
  50. package/src/observability/log-writer.ts +41 -83
  51. package/src/observability/retention.ts +2 -2
  52. package/src/observability/session-logger.ts +36 -57
  53. package/src/observability/summary-generator.ts +31 -19
  54. package/src/observability/types.ts +12 -24
  55. package/src/orchestrator/contracts/invariants.ts +14 -0
  56. package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
  57. package/src/orchestrator/error-context.ts +24 -0
  58. package/src/orchestrator/fallback/event-handler.ts +47 -3
  59. package/src/orchestrator/handlers/architect.ts +2 -1
  60. package/src/orchestrator/handlers/build-utils.ts +118 -0
  61. package/src/orchestrator/handlers/build.ts +42 -219
  62. package/src/orchestrator/handlers/retrospective.ts +2 -2
  63. package/src/orchestrator/handlers/types.ts +0 -1
  64. package/src/orchestrator/lesson-memory.ts +36 -11
  65. package/src/orchestrator/orchestration-logger.ts +53 -24
  66. package/src/orchestrator/phase.ts +8 -4
  67. package/src/orchestrator/progress.ts +63 -0
  68. package/src/orchestrator/state.ts +79 -17
  69. package/src/projects/database.ts +47 -0
  70. package/src/projects/repository.ts +264 -0
  71. package/src/projects/resolve.ts +301 -0
  72. package/src/projects/schemas.ts +30 -0
  73. package/src/projects/types.ts +12 -0
  74. package/src/review/memory.ts +39 -11
  75. package/src/review/parse-findings.ts +116 -0
  76. package/src/review/pipeline.ts +3 -107
  77. package/src/review/selection.ts +38 -4
  78. package/src/scoring/time-provider.ts +23 -0
  79. package/src/tools/doctor.ts +28 -4
  80. package/src/tools/forensics.ts +7 -12
  81. package/src/tools/logs.ts +38 -11
  82. package/src/tools/memory-preferences.ts +157 -0
  83. package/src/tools/memory-status.ts +17 -96
  84. package/src/tools/orchestrate.ts +108 -90
  85. package/src/tools/pipeline-report.ts +3 -2
  86. package/src/tools/quick.ts +2 -2
  87. package/src/tools/replay.ts +42 -0
  88. package/src/tools/review.ts +46 -7
  89. package/src/tools/session-stats.ts +3 -2
  90. package/src/tools/summary.ts +43 -0
  91. package/src/utils/paths.ts +20 -1
  92. package/src/utils/random.ts +33 -0
  93. package/src/ux/session-summary.ts +56 -0
@@ -1,8 +1,8 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { mkdirSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { getGlobalConfigDir } from "../utils/paths";
5
- import { DB_FILE, MEMORY_DIR } from "./constants";
3
+ import { dirname } from "node:path";
4
+ import { runProjectRegistryMigrations } from "../projects/database";
5
+ import { getAutopilotDbPath } from "../utils/paths";
6
6
 
7
7
  let db: Database | null = null;
8
8
 
@@ -11,12 +11,7 @@ let db: Database | null = null;
11
11
  * Idempotent via IF NOT EXISTS.
12
12
  */
13
13
  export function initMemoryDb(database: Database): void {
14
- database.run(`CREATE TABLE IF NOT EXISTS projects (
15
- id TEXT PRIMARY KEY,
16
- path TEXT NOT NULL UNIQUE,
17
- name TEXT NOT NULL,
18
- last_updated TEXT NOT NULL
19
- )`);
14
+ runProjectRegistryMigrations(database);
20
15
 
21
16
  database.run(`CREATE TABLE IF NOT EXISTS observations (
22
17
  id INTEGER PRIMARY KEY,
@@ -42,6 +37,73 @@ export function initMemoryDb(database: Database): void {
42
37
  last_updated TEXT NOT NULL
43
38
  )`);
44
39
 
40
+ database.run(`CREATE TABLE IF NOT EXISTS preference_records (
41
+ id TEXT PRIMARY KEY,
42
+ key TEXT NOT NULL,
43
+ value TEXT NOT NULL,
44
+ scope TEXT NOT NULL CHECK(scope IN ('global', 'project')),
45
+ project_id TEXT,
46
+ status TEXT NOT NULL CHECK(status IN ('candidate', 'confirmed', 'rejected')) DEFAULT 'confirmed',
47
+ confidence REAL NOT NULL DEFAULT 0.5,
48
+ source_session TEXT,
49
+ created_at TEXT NOT NULL,
50
+ last_updated TEXT NOT NULL,
51
+ FOREIGN KEY (project_id) REFERENCES projects(id),
52
+ UNIQUE(key, scope, project_id)
53
+ )`);
54
+
55
+ database.run(`CREATE INDEX IF NOT EXISTS idx_preference_records_scope_updated
56
+ ON preference_records(scope, last_updated DESC, key ASC)`);
57
+ database.run(`CREATE INDEX IF NOT EXISTS idx_preference_records_project_updated
58
+ ON preference_records(project_id, last_updated DESC, key ASC)`);
59
+
60
+ database.run(`CREATE TABLE IF NOT EXISTS preference_evidence (
61
+ id TEXT PRIMARY KEY,
62
+ preference_id TEXT NOT NULL,
63
+ session_id TEXT,
64
+ run_id TEXT,
65
+ statement TEXT NOT NULL,
66
+ statement_hash TEXT NOT NULL,
67
+ confidence REAL NOT NULL DEFAULT 0.5,
68
+ confirmed INTEGER NOT NULL DEFAULT 0,
69
+ created_at TEXT NOT NULL,
70
+ FOREIGN KEY (preference_id) REFERENCES preference_records(id) ON DELETE CASCADE,
71
+ UNIQUE(preference_id, statement_hash)
72
+ )`);
73
+
74
+ database.run(`CREATE INDEX IF NOT EXISTS idx_preference_evidence_preference_created
75
+ ON preference_evidence(preference_id, created_at DESC, id DESC)`);
76
+
77
+ database.run(`INSERT INTO preference_records (
78
+ id,
79
+ key,
80
+ value,
81
+ scope,
82
+ project_id,
83
+ status,
84
+ confidence,
85
+ source_session,
86
+ created_at,
87
+ last_updated
88
+ )
89
+ SELECT
90
+ p.id,
91
+ p.key,
92
+ p.value,
93
+ 'global',
94
+ NULL,
95
+ 'confirmed',
96
+ p.confidence,
97
+ p.source_session,
98
+ p.created_at,
99
+ p.last_updated
100
+ FROM preferences p
101
+ WHERE NOT EXISTS (
102
+ SELECT 1
103
+ FROM preference_records pr
104
+ WHERE pr.id = p.id
105
+ )`);
106
+
45
107
  database.run(`CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
46
108
  content, summary,
47
109
  content=observations,
@@ -79,9 +141,9 @@ export function getMemoryDb(dbPath?: string): Database {
79
141
  const resolvedPath =
80
142
  dbPath ??
81
143
  (() => {
82
- const memoryDir = join(getGlobalConfigDir(), MEMORY_DIR);
83
- mkdirSync(memoryDir, { recursive: true });
84
- return join(memoryDir, DB_FILE);
144
+ const runtimeDbPath = getAutopilotDbPath();
145
+ mkdirSync(dirname(runtimeDbPath), { recursive: true });
146
+ return runtimeDbPath;
85
147
  })();
86
148
 
87
149
  db = new Database(resolvedPath);
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { Database } from "bun:sqlite";
13
+ import { systemTimeProvider, type TimeProvider } from "../scoring/time-provider";
13
14
  import {
14
15
  DEFAULT_HALF_LIFE_DAYS,
15
16
  MAX_OBSERVATIONS_PER_PROJECT,
@@ -35,8 +36,9 @@ export function computeRelevanceScore(
35
36
  accessCount: number,
36
37
  type: ObservationType,
37
38
  halfLifeDays: number = DEFAULT_HALF_LIFE_DAYS,
39
+ timeProvider: TimeProvider = systemTimeProvider,
38
40
  ): number {
39
- const ageMs = Date.now() - new Date(lastAccessed).getTime();
41
+ const ageMs = timeProvider.now() - new Date(lastAccessed).getTime();
40
42
  const ageDays = ageMs / MS_PER_DAY;
41
43
  const timeDecay = Math.exp(-ageDays / halfLifeDays);
42
44
  const frequencyWeight = Math.max(Math.log2(accessCount + 1), 1);
@@ -55,6 +57,7 @@ export function computeRelevanceScore(
55
57
  export function pruneStaleObservations(
56
58
  projectId: string | null,
57
59
  db?: Database,
60
+ timeProvider: TimeProvider = systemTimeProvider,
58
61
  ): { readonly pruned: number } {
59
62
  const fetchLimit = MAX_OBSERVATIONS_PER_PROJECT + 1000;
60
63
  const observations = getObservationsByProject(projectId, fetchLimit, db);
@@ -64,7 +67,13 @@ export function pruneStaleObservations(
64
67
  .filter((obs): obs is typeof obs & { id: number } => obs.id !== undefined)
65
68
  .map((obs) => ({
66
69
  id: obs.id,
67
- score: computeRelevanceScore(obs.lastAccessed, obs.accessCount, obs.type),
70
+ score: computeRelevanceScore(
71
+ obs.lastAccessed,
72
+ obs.accessCount,
73
+ obs.type,
74
+ DEFAULT_HALF_LIFE_DAYS,
75
+ timeProvider,
76
+ ),
68
77
  }));
69
78
 
70
79
  let pruned = 0;
@@ -1,4 +1,9 @@
1
- export { createMemoryCaptureHandler, type MemoryCaptureDeps } from "./capture";
1
+ export {
2
+ createMemoryCaptureHandler,
3
+ createMemoryChatMessageHandler,
4
+ type MemoryCaptureDeps,
5
+ memoryCaptureInternals,
6
+ } from "./capture";
2
7
  export * from "./constants";
3
8
  export { closeMemoryDb, getMemoryDb, initMemoryDb } from "./database";
4
9
  export { computeRelevanceScore, pruneStaleObservations } from "./decay";
@@ -6,13 +11,24 @@ export { createMemoryInjector, type MemoryInjectorConfig } from "./injector";
6
11
  export { computeProjectKey } from "./project-key";
7
12
  export {
8
13
  deleteObservation,
14
+ deletePreferenceRecord,
15
+ deletePreferencesByKey,
9
16
  getAllPreferences,
17
+ getConfirmedPreferencesForProject,
10
18
  getObservationsByProject,
19
+ getPreferenceRecordById,
11
20
  getProjectByPath,
21
+ getRecentFailureObservations,
12
22
  insertObservation,
23
+ listPreferenceEvidence,
24
+ listPreferenceRecords,
25
+ listRelevantLessons,
26
+ prunePreferenceEvidence,
27
+ prunePreferences,
13
28
  searchObservations,
14
29
  updateAccessCount,
15
30
  upsertPreference,
31
+ upsertPreferenceRecord,
16
32
  upsertProject,
17
33
  } from "./repository";
18
34
  export {
@@ -11,8 +11,11 @@
11
11
  */
12
12
 
13
13
  import type { Database } from "bun:sqlite";
14
+ import { getLogger } from "../logging/domains";
14
15
  import { retrieveMemoryContext } from "./retrieval";
15
16
 
17
+ const logger = getLogger("memory", "injector");
18
+
16
19
  /**
17
20
  * Configuration for creating a memory injector.
18
21
  */
@@ -79,7 +82,7 @@ export function createMemoryInjector(config: MemoryInjectorConfig) {
79
82
  output.system.push(context);
80
83
  }
81
84
  } catch (err) {
82
- console.warn("[opencode-autopilot] memory injection failed:", err);
85
+ logger.warn("memory injection failed", { error: String(err) });
83
86
  }
84
87
  };
85
88
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lesson repository operations.
3
+ * Handles retrieval of extracted lessons from past runs.
4
+ */
5
+
6
+ import type { Database } from "bun:sqlite";
7
+ import { lessonMemorySchema } from "../orchestrator/lesson-schemas";
8
+ import type { Lesson } from "../orchestrator/lesson-types";
9
+ import { getMemoryDb } from "./database";
10
+
11
+ function resolveDb(db?: Database): Database {
12
+ return db ?? getMemoryDb();
13
+ }
14
+
15
+ function tableExists(db: Database, tableName: string): boolean {
16
+ const row = db
17
+ .query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
18
+ .get(tableName) as { name?: string } | null;
19
+ return row?.name === tableName;
20
+ }
21
+
22
+ interface ProjectLessonRow {
23
+ readonly content: string;
24
+ readonly domain: Lesson["domain"];
25
+ readonly extracted_at: string;
26
+ readonly source_phase: Lesson["sourcePhase"];
27
+ readonly last_updated_at: string | null;
28
+ }
29
+
30
+ function listLegacyLessons(projectId: string, db: Database): readonly Lesson[] {
31
+ if (!tableExists(db, "project_lesson_memory")) {
32
+ return Object.freeze([]);
33
+ }
34
+
35
+ const row = db
36
+ .query("SELECT state_json FROM project_lesson_memory WHERE project_id = ?")
37
+ .get(projectId) as { state_json?: string } | null;
38
+ if (row?.state_json === undefined) {
39
+ return Object.freeze([]);
40
+ }
41
+
42
+ try {
43
+ const parsed = lessonMemorySchema.parse(JSON.parse(row.state_json));
44
+ return Object.freeze(parsed.lessons);
45
+ } catch {
46
+ return Object.freeze([]);
47
+ }
48
+ }
49
+
50
+ function buildLessonsFromRows(rows: readonly ProjectLessonRow[]): readonly Lesson[] {
51
+ return Object.freeze(
52
+ rows.map((row) =>
53
+ Object.freeze({
54
+ content: row.content,
55
+ domain: row.domain,
56
+ extractedAt: row.extracted_at,
57
+ sourcePhase: row.source_phase,
58
+ }),
59
+ ),
60
+ );
61
+ }
62
+
63
+ export function listRelevantLessons(
64
+ projectId: string,
65
+ limit = 5,
66
+ db?: Database,
67
+ ): readonly Lesson[] {
68
+ const d = resolveDb(db);
69
+ if (tableExists(d, "project_lessons")) {
70
+ const rows = d
71
+ .query(
72
+ `SELECT content, domain, extracted_at, source_phase, last_updated_at
73
+ FROM project_lessons
74
+ WHERE project_id = ?
75
+ ORDER BY extracted_at DESC, lesson_id DESC
76
+ LIMIT ?`,
77
+ )
78
+ .all(projectId, limit) as ProjectLessonRow[];
79
+ if (rows.length > 0) {
80
+ return buildLessonsFromRows(rows);
81
+ }
82
+ }
83
+
84
+ return listLegacyLessons(projectId, d).slice(0, limit);
85
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Observation repository operations.
3
+ *
4
+ * Handles observation CRUD, search, and access tracking.
5
+ * Extracted from repository.ts for better module organization.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import type { Database } from "bun:sqlite";
11
+ import { withTransaction } from "../kernel/transaction";
12
+ import { OBSERVATION_TYPES } from "./constants";
13
+ import { getMemoryDb } from "./database";
14
+ import { observationSchema } from "./schemas";
15
+ import type { Observation, ObservationType } from "./types";
16
+
17
+ /** Resolve optional db parameter to singleton fallback. */
18
+ function resolveDb(db?: Database): Database {
19
+ return db ?? getMemoryDb();
20
+ }
21
+
22
+ /** Validate observation type at runtime. */
23
+ function parseObservationType(value: unknown): ObservationType {
24
+ if (typeof value === "string" && (OBSERVATION_TYPES as readonly string[]).includes(value)) {
25
+ return value as ObservationType;
26
+ }
27
+ return "context";
28
+ }
29
+
30
+ /** Map a snake_case DB row to camelCase Observation. */
31
+ function rowToObservation(row: Record<string, unknown>): Observation {
32
+ return {
33
+ id: row.id as number,
34
+ projectId: (row.project_id as string) ?? null,
35
+ sessionId: row.session_id as string,
36
+ type: parseObservationType(row.type),
37
+ content: row.content as string,
38
+ summary: row.summary as string,
39
+ confidence: row.confidence as number,
40
+ accessCount: row.access_count as number,
41
+ createdAt: row.created_at as string,
42
+ lastAccessed: row.last_accessed as string,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Insert an observation. Validates via Zod before writing.
48
+ * Returns the observation with the generated id.
49
+ */
50
+ export function insertObservation(obs: Omit<Observation, "id">, db?: Database): Observation {
51
+ const validated = observationSchema.omit({ id: true }).parse(obs);
52
+ const d = resolveDb(db);
53
+
54
+ return withTransaction(d, () => {
55
+ const row = d
56
+ .query<
57
+ { id: number },
58
+ [string | null, string, ObservationType, string, string, number, number, string, string]
59
+ >(
60
+ `INSERT INTO observations (project_id, session_id, type, content, summary, confidence, access_count, created_at, last_accessed)
61
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id`,
62
+ )
63
+ .get(
64
+ validated.projectId,
65
+ validated.sessionId,
66
+ validated.type,
67
+ validated.content,
68
+ validated.summary,
69
+ validated.confidence,
70
+ validated.accessCount,
71
+ validated.createdAt,
72
+ validated.lastAccessed,
73
+ );
74
+
75
+ if (!row) {
76
+ throw new Error("Failed to insert observation");
77
+ }
78
+
79
+ return { ...validated, id: row.id };
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Search observations using FTS5 MATCH with BM25 ranking.
85
+ * Filters by projectId (null for user-level observations).
86
+ */
87
+ export function searchObservations(
88
+ query: string,
89
+ projectId: string | null,
90
+ limit = 20,
91
+ db?: Database,
92
+ ): Array<Observation & { ftsRank: number }> {
93
+ const d = resolveDb(db);
94
+
95
+ const projectFilter = projectId === null ? "AND o.project_id IS NULL" : "AND o.project_id = ?";
96
+ const safeFtsQuery = `"${query.replace(/"/g, '""')}"`;
97
+ const params: Array<string | number> =
98
+ projectId === null ? [safeFtsQuery, limit] : [safeFtsQuery, projectId, limit];
99
+
100
+ const rows = d
101
+ .query(
102
+ `SELECT o.*, bm25(observations_fts) as fts_rank
103
+ FROM observations_fts f
104
+ JOIN observations o ON o.id = f.rowid
105
+ WHERE observations_fts MATCH ?
106
+ ${projectFilter}
107
+ ORDER BY fts_rank
108
+ LIMIT ?`,
109
+ )
110
+ .all(...params) as Array<Record<string, unknown>>;
111
+
112
+ return rows.map((row) => ({
113
+ ...rowToObservation(row),
114
+ ftsRank: row.fts_rank as number,
115
+ }));
116
+ }
117
+
118
+ /**
119
+ * Get observations filtered by project_id, ordered by created_at DESC.
120
+ */
121
+ export function getObservationsByProject(
122
+ projectId: string | null,
123
+ limit = 50,
124
+ db?: Database,
125
+ ): readonly Observation[] {
126
+ const d = resolveDb(db);
127
+
128
+ const whereClause = projectId === null ? "WHERE project_id IS NULL" : "WHERE project_id = ?";
129
+ const params: Array<string | number> = projectId === null ? [limit] : [projectId, limit];
130
+
131
+ const rows = d
132
+ .query(`SELECT * FROM observations ${whereClause} ORDER BY created_at DESC LIMIT ?`)
133
+ .all(...params) as Array<Record<string, unknown>>;
134
+
135
+ return rows.map(rowToObservation);
136
+ }
137
+
138
+ /**
139
+ * Get recent failure observations for a project.
140
+ */
141
+ export function getRecentFailureObservations(
142
+ projectId: string,
143
+ limit = 5,
144
+ db?: Database,
145
+ ): readonly Observation[] {
146
+ const d = resolveDb(db);
147
+ const rows = d
148
+ .query(
149
+ `SELECT *
150
+ FROM observations
151
+ WHERE project_id = ?
152
+ AND type = 'error'
153
+ ORDER BY created_at DESC, id DESC
154
+ LIMIT ?`,
155
+ )
156
+ .all(projectId, limit) as Array<Record<string, unknown>>;
157
+ return Object.freeze(rows.map(rowToObservation));
158
+ }
159
+
160
+ /**
161
+ * Delete an observation by id.
162
+ */
163
+ export function deleteObservation(id: number, db?: Database): void {
164
+ const d = resolveDb(db);
165
+ d.run("DELETE FROM observations WHERE id = ?", [id]);
166
+ }
167
+
168
+ /**
169
+ * Increment access_count and update last_accessed for an observation.
170
+ */
171
+ export function updateAccessCount(id: number, db?: Database): void {
172
+ const d = resolveDb(db);
173
+ d.run("UPDATE observations SET access_count = access_count + 1, last_accessed = ? WHERE id = ?", [
174
+ new Date().toISOString(),
175
+ id,
176
+ ]);
177
+ }