@kodrunhq/opencode-autopilot 1.15.1 → 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 (64) hide show
  1. package/README.md +14 -0
  2. package/bin/cli.ts +5 -0
  3. package/bin/inspect.ts +337 -0
  4. package/package.json +1 -1
  5. package/src/agents/autopilot.ts +7 -15
  6. package/src/agents/index.ts +54 -21
  7. package/src/health/checks.ts +108 -4
  8. package/src/health/runner.ts +3 -0
  9. package/src/index.ts +105 -12
  10. package/src/inspect/formatters.ts +225 -0
  11. package/src/inspect/repository.ts +882 -0
  12. package/src/kernel/database.ts +45 -0
  13. package/src/kernel/migrations.ts +62 -0
  14. package/src/kernel/repository.ts +571 -0
  15. package/src/kernel/schema.ts +122 -0
  16. package/src/kernel/types.ts +66 -0
  17. package/src/memory/capture.ts +221 -25
  18. package/src/memory/database.ts +74 -12
  19. package/src/memory/index.ts +17 -1
  20. package/src/memory/project-key.ts +6 -0
  21. package/src/memory/repository.ts +833 -42
  22. package/src/memory/retrieval.ts +83 -169
  23. package/src/memory/schemas.ts +39 -7
  24. package/src/memory/types.ts +4 -0
  25. package/src/observability/event-handlers.ts +28 -17
  26. package/src/observability/event-store.ts +29 -1
  27. package/src/observability/forensic-log.ts +159 -0
  28. package/src/observability/forensic-schemas.ts +69 -0
  29. package/src/observability/forensic-types.ts +10 -0
  30. package/src/observability/index.ts +21 -27
  31. package/src/observability/log-reader.ts +142 -111
  32. package/src/observability/log-writer.ts +41 -83
  33. package/src/observability/retention.ts +2 -2
  34. package/src/observability/session-logger.ts +36 -57
  35. package/src/observability/summary-generator.ts +31 -19
  36. package/src/observability/types.ts +12 -24
  37. package/src/orchestrator/contracts/invariants.ts +14 -0
  38. package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
  39. package/src/orchestrator/fallback/event-handler.ts +47 -3
  40. package/src/orchestrator/handlers/architect.ts +2 -1
  41. package/src/orchestrator/handlers/build.ts +55 -97
  42. package/src/orchestrator/handlers/retrospective.ts +2 -1
  43. package/src/orchestrator/handlers/types.ts +0 -1
  44. package/src/orchestrator/lesson-memory.ts +29 -9
  45. package/src/orchestrator/orchestration-logger.ts +37 -23
  46. package/src/orchestrator/phase.ts +8 -4
  47. package/src/orchestrator/state.ts +79 -17
  48. package/src/projects/database.ts +47 -0
  49. package/src/projects/repository.ts +264 -0
  50. package/src/projects/resolve.ts +301 -0
  51. package/src/projects/schemas.ts +30 -0
  52. package/src/projects/types.ts +12 -0
  53. package/src/review/memory.ts +29 -9
  54. package/src/tools/doctor.ts +40 -5
  55. package/src/tools/forensics.ts +7 -12
  56. package/src/tools/logs.ts +6 -5
  57. package/src/tools/memory-preferences.ts +157 -0
  58. package/src/tools/memory-status.ts +17 -96
  59. package/src/tools/orchestrate.ts +97 -81
  60. package/src/tools/pipeline-report.ts +3 -2
  61. package/src/tools/quick.ts +2 -2
  62. package/src/tools/review.ts +39 -6
  63. package/src/tools/session-stats.ts +3 -2
  64. package/src/utils/paths.ts +20 -1
@@ -5,6 +5,7 @@ import {
5
5
  commandHealthCheck,
6
6
  configHealthCheck,
7
7
  memoryHealthCheck,
8
+ nativeAgentSuppressionHealthCheck,
8
9
  skillHealthCheck,
9
10
  } from "./checks";
10
11
  import type { HealthReport, HealthResult } from "./types";
@@ -45,6 +46,7 @@ export async function runHealthChecks(options?: {
45
46
  const settled = await Promise.allSettled([
46
47
  configHealthCheck(options?.configPath),
47
48
  agentHealthCheck(options?.openCodeConfig ?? null),
49
+ nativeAgentSuppressionHealthCheck(options?.openCodeConfig ?? null),
48
50
  assetHealthCheck(options?.assetsDir, options?.targetDir),
49
51
  skillHealthCheck(options?.projectRoot ?? process.cwd()),
50
52
  memoryHealthCheck(options?.targetDir),
@@ -54,6 +56,7 @@ export async function runHealthChecks(options?: {
54
56
  const fallbackNames = [
55
57
  "config-validity",
56
58
  "agent-injection",
59
+ "native-agent-suppression",
57
60
  "asset-directories",
58
61
  "skill-loading",
59
62
  "memory-db",
package/src/index.ts CHANGED
@@ -4,7 +4,12 @@ import { isFirstLoad, loadConfig } from "./config";
4
4
  import { runHealthChecks } from "./health/runner";
5
5
  import { createAntiSlopHandler } from "./hooks/anti-slop";
6
6
  import { installAssets } from "./installer";
7
- import { createMemoryCaptureHandler, createMemoryInjector, getMemoryDb } from "./memory";
7
+ import {
8
+ createMemoryCaptureHandler,
9
+ createMemoryChatMessageHandler,
10
+ createMemoryInjector,
11
+ getMemoryDb,
12
+ } from "./memory";
8
13
  import { ContextMonitor } from "./observability/context-monitor";
9
14
  import {
10
15
  createObservabilityEventHandler,
@@ -12,9 +17,9 @@ import {
12
17
  createToolExecuteBeforeHandler,
13
18
  } from "./observability/event-handlers";
14
19
  import { SessionEventStore } from "./observability/event-store";
20
+ import { createForensicEvent } from "./observability/forensic-log";
15
21
  import { writeSessionLog } from "./observability/log-writer";
16
22
  import { pruneOldLogs } from "./observability/retention";
17
- import type { SessionEvent } from "./observability/types";
18
23
  import type { SdkOperations } from "./orchestrator/fallback";
19
24
  import {
20
25
  createChatMessageHandler,
@@ -34,10 +39,11 @@ import {
34
39
  import { ocCreateAgent } from "./tools/create-agent";
35
40
  import { ocCreateCommand } from "./tools/create-command";
36
41
  import { ocCreateSkill } from "./tools/create-skill";
37
- import { ocDoctor } from "./tools/doctor";
42
+ import { ocDoctor, setOpenCodeConfig as setDoctorOpenCodeConfig } from "./tools/doctor";
38
43
  import { ocForensics } from "./tools/forensics";
39
44
  import { ocHashlineEdit } from "./tools/hashline-edit";
40
45
  import { ocLogs } from "./tools/logs";
46
+ import { ocMemoryPreferences } from "./tools/memory-preferences";
41
47
  import { ocMemoryStatus } from "./tools/memory-status";
42
48
  import { ocMockFallback } from "./tools/mock-fallback";
43
49
  import { ocOrchestrate } from "./tools/orchestrate";
@@ -148,6 +154,29 @@ const plugin: Plugin = async (input) => {
148
154
  manager,
149
155
  sdk: sdkOps,
150
156
  config: fallbackConfig,
157
+ onFallbackEvent: (event) => {
158
+ if (event.type === "fallback") {
159
+ eventStore.appendEvent(event.sessionId, {
160
+ type: "fallback",
161
+ timestamp: new Date().toISOString(),
162
+ sessionId: event.sessionId,
163
+ failedModel: event.failedModel ?? "unknown",
164
+ nextModel: event.nextModel ?? "unknown",
165
+ reason: event.reason ?? "fallback",
166
+ success: event.success === true,
167
+ });
168
+ return;
169
+ }
170
+
171
+ eventStore.appendEvent(event.sessionId, {
172
+ type: "model_switch",
173
+ timestamp: new Date().toISOString(),
174
+ sessionId: event.sessionId,
175
+ fromModel: event.fromModel ?? "unknown",
176
+ toModel: event.toModel ?? "unknown",
177
+ trigger: event.trigger ?? "fallback",
178
+ });
179
+ },
151
180
  });
152
181
  const chatMessageHandler = createChatMessageHandler(manager);
153
182
  const toolExecuteAfterHandler = createToolExecuteAfterHandler(manager);
@@ -165,6 +194,9 @@ const plugin: Plugin = async (input) => {
165
194
  const memoryCaptureHandler = memoryConfig.enabled
166
195
  ? createMemoryCaptureHandler({ getDb: () => getMemoryDb(), projectRoot: process.cwd() })
167
196
  : null;
197
+ const memoryChatMessageHandler = memoryConfig.enabled
198
+ ? createMemoryChatMessageHandler({ getDb: () => getMemoryDb(), projectRoot: process.cwd() })
199
+ : null;
168
200
 
169
201
  const memoryInjector = memoryConfig.enabled
170
202
  ? createMemoryInjector({
@@ -183,18 +215,73 @@ const plugin: Plugin = async (input) => {
183
215
  showToast: sdkOps.showToast,
184
216
  writeSessionLog: async (sessionData) => {
185
217
  if (!sessionData) return;
186
- // Filter to schema-valid event types that match SessionEvent discriminated union
187
- const schemaEvents: SessionEvent[] = sessionData.events.filter(
188
- (e): e is SessionEvent =>
189
- e.type === "fallback" ||
190
- e.type === "error" ||
191
- e.type === "decision" ||
192
- e.type === "model_switch",
193
- );
194
218
  await writeSessionLog({
219
+ projectRoot: process.cwd(),
195
220
  sessionId: sessionData.sessionId,
196
221
  startedAt: sessionData.startedAt,
197
- events: schemaEvents,
222
+ events: sessionData.events.map((event) =>
223
+ createForensicEvent({
224
+ projectRoot: process.cwd(),
225
+ domain: "session",
226
+ timestamp: event.timestamp,
227
+ sessionId: event.sessionId,
228
+ type: event.type,
229
+ message: event.type === "error" ? event.message : null,
230
+ code:
231
+ event.type === "error"
232
+ ? event.errorType
233
+ : event.type === "fallback"
234
+ ? "FALLBACK"
235
+ : null,
236
+ payload:
237
+ event.type === "error"
238
+ ? {
239
+ model: event.model,
240
+ errorType: event.errorType,
241
+ ...(event.statusCode !== undefined ? { statusCode: event.statusCode } : {}),
242
+ }
243
+ : event.type === "fallback"
244
+ ? {
245
+ failedModel: event.failedModel,
246
+ nextModel: event.nextModel,
247
+ reason: event.reason,
248
+ success: event.success,
249
+ }
250
+ : event.type === "decision"
251
+ ? {
252
+ decision: event.decision,
253
+ rationale: event.rationale,
254
+ }
255
+ : event.type === "model_switch"
256
+ ? {
257
+ fromModel: event.fromModel,
258
+ toModel: event.toModel,
259
+ trigger: event.trigger,
260
+ }
261
+ : event.type === "context_warning"
262
+ ? {
263
+ utilization: event.utilization,
264
+ contextLimit: event.contextLimit,
265
+ inputTokens: event.inputTokens,
266
+ }
267
+ : event.type === "tool_complete"
268
+ ? {
269
+ tool: event.tool,
270
+ durationMs: event.durationMs,
271
+ success: event.success,
272
+ }
273
+ : event.type === "phase_transition"
274
+ ? {
275
+ fromPhase: event.fromPhase,
276
+ toPhase: event.toPhase,
277
+ }
278
+ : event.type === "compacted"
279
+ ? {
280
+ trigger: event.trigger,
281
+ }
282
+ : {},
283
+ }),
284
+ ),
198
285
  });
199
286
  },
200
287
  });
@@ -224,6 +311,7 @@ const plugin: Plugin = async (input) => {
224
311
  oc_stocktake: ocStocktake,
225
312
  oc_update_docs: ocUpdateDocs,
226
313
  oc_memory_status: ocMemoryStatus,
314
+ oc_memory_preferences: ocMemoryPreferences,
227
315
  },
228
316
  event: async ({ event }) => {
229
317
  // 1. Observability: collect (pure observer, no side effects on session)
@@ -255,6 +343,7 @@ const plugin: Plugin = async (input) => {
255
343
  config: async (cfg: Config) => {
256
344
  openCodeConfig = cfg;
257
345
  setOpenCodeConfig(cfg);
346
+ setDoctorOpenCodeConfig(cfg);
258
347
  await configHook(cfg);
259
348
  },
260
349
  "chat.message": async (
@@ -268,6 +357,10 @@ const plugin: Plugin = async (input) => {
268
357
  parts: unknown[];
269
358
  },
270
359
  ) => {
360
+ if (memoryChatMessageHandler) {
361
+ await memoryChatMessageHandler(hookInput, output);
362
+ }
363
+
271
364
  if (fallbackConfig.enabled) {
272
365
  await chatMessageHandler(hookInput, output);
273
366
  }
@@ -0,0 +1,225 @@
1
+ import type {
2
+ InspectEventSummary,
3
+ InspectLessonSummary,
4
+ InspectMemoryOverview,
5
+ InspectPreferenceSummary,
6
+ InspectProjectDetails,
7
+ InspectProjectSummary,
8
+ InspectRunSummary,
9
+ } from "./repository";
10
+
11
+ function sanitizeCell(value: string | number | boolean | null): string {
12
+ return String(value ?? "")
13
+ .replace(/\|/g, "\\|")
14
+ .replace(/\n/g, " ");
15
+ }
16
+
17
+ function formatTimestamp(value: string | null): string {
18
+ return value ?? "-";
19
+ }
20
+
21
+ export function formatProjects(projects: readonly InspectProjectSummary[]): string {
22
+ if (projects.length === 0) {
23
+ return "No projects found.";
24
+ }
25
+
26
+ const lines = [
27
+ "Projects",
28
+ "",
29
+ "| Project | Current Path | Updated | Runs | Events | Lessons |",
30
+ "|---------|--------------|---------|------|--------|---------|",
31
+ ];
32
+
33
+ for (const project of projects) {
34
+ lines.push(
35
+ `| ${sanitizeCell(project.name)} | ${sanitizeCell(project.path)} | ${sanitizeCell(project.lastUpdated)} | ${project.runCount} | ${project.eventCount} | ${project.lessonCount} |`,
36
+ );
37
+ }
38
+
39
+ return lines.join("\n");
40
+ }
41
+
42
+ export function formatProjectDetails(details: InspectProjectDetails): string {
43
+ const { project, paths, gitFingerprints } = details;
44
+ const lines = [
45
+ `Project: ${project.name}`,
46
+ "",
47
+ `ID: ${project.id}`,
48
+ `Current Path: ${project.path}`,
49
+ `First Seen: ${project.firstSeenAt}`,
50
+ `Last Updated: ${project.lastUpdated}`,
51
+ `Runs: ${project.runCount}`,
52
+ `Events: ${project.eventCount}`,
53
+ `Lessons: ${project.lessonCount}`,
54
+ `Observations: ${project.observationCount}`,
55
+ "",
56
+ "Paths:",
57
+ ];
58
+
59
+ if (paths.length === 0) {
60
+ lines.push("- none");
61
+ } else {
62
+ for (const path of paths) {
63
+ lines.push(`- ${path.path}${path.isCurrent ? " [current]" : ""}`);
64
+ }
65
+ }
66
+
67
+ lines.push("", "Git Fingerprints:");
68
+ if (gitFingerprints.length === 0) {
69
+ lines.push("- none");
70
+ } else {
71
+ for (const fingerprint of gitFingerprints) {
72
+ lines.push(
73
+ `- ${fingerprint.normalizedRemoteUrl}${fingerprint.defaultBranch ? ` (${fingerprint.defaultBranch})` : ""}`,
74
+ );
75
+ }
76
+ }
77
+
78
+ return lines.join("\n");
79
+ }
80
+
81
+ export function formatRuns(runs: readonly InspectRunSummary[]): string {
82
+ if (runs.length === 0) {
83
+ return "No runs found.";
84
+ }
85
+
86
+ const lines = [
87
+ "Runs",
88
+ "",
89
+ "| Project | Run ID | Status | Phase | Revision | Updated |",
90
+ "|---------|--------|--------|-------|----------|---------|",
91
+ ];
92
+
93
+ for (const run of runs) {
94
+ lines.push(
95
+ `| ${sanitizeCell(run.projectName)} | ${sanitizeCell(run.runId)} | ${sanitizeCell(run.status)} | ${sanitizeCell(run.currentPhase ?? "-")} | ${run.stateRevision} | ${sanitizeCell(run.lastUpdatedAt)} |`,
96
+ );
97
+ }
98
+
99
+ return lines.join("\n");
100
+ }
101
+
102
+ export function formatEvents(events: readonly InspectEventSummary[]): string {
103
+ if (events.length === 0) {
104
+ return "No events found.";
105
+ }
106
+
107
+ const lines = [
108
+ "Events",
109
+ "",
110
+ "| Timestamp | Project | Domain | Type | Phase | Agent | Code | Message |",
111
+ "|-----------|---------|--------|------|-------|-------|------|---------|",
112
+ ];
113
+
114
+ for (const event of events) {
115
+ lines.push(
116
+ `| ${sanitizeCell(event.timestamp)} | ${sanitizeCell(event.projectName)} | ${sanitizeCell(event.domain)} | ${sanitizeCell(event.type)} | ${sanitizeCell(event.phase ?? "-")} | ${sanitizeCell(event.agent ?? "-")} | ${sanitizeCell(event.code ?? "-")} | ${sanitizeCell(event.message ?? "")} |`,
117
+ );
118
+ }
119
+
120
+ return lines.join("\n");
121
+ }
122
+
123
+ export function formatLessons(lessons: readonly InspectLessonSummary[]): string {
124
+ if (lessons.length === 0) {
125
+ return "No lessons found.";
126
+ }
127
+
128
+ const lines = [
129
+ "Lessons",
130
+ "",
131
+ "| Extracted | Project | Domain | Source Phase | Content |",
132
+ "|-----------|---------|--------|--------------|---------|",
133
+ ];
134
+
135
+ for (const lesson of lessons) {
136
+ lines.push(
137
+ `| ${sanitizeCell(lesson.extractedAt)} | ${sanitizeCell(lesson.projectName)} | ${sanitizeCell(lesson.domain)} | ${sanitizeCell(lesson.sourcePhase)} | ${sanitizeCell(lesson.content)} |`,
138
+ );
139
+ }
140
+
141
+ return lines.join("\n");
142
+ }
143
+
144
+ export function formatPreferences(preferences: readonly InspectPreferenceSummary[]): string {
145
+ if (preferences.length === 0) {
146
+ return "No preferences found.";
147
+ }
148
+
149
+ const lines = [
150
+ "Preferences",
151
+ "",
152
+ "| Key | Scope | Value | Confidence | Evidence | Updated |",
153
+ "|-----|-------|-------|------------|----------|---------|",
154
+ ];
155
+
156
+ for (const preference of preferences) {
157
+ lines.push(
158
+ `| ${sanitizeCell(preference.key)} | ${sanitizeCell(preference.scope)}${preference.projectId ? `:${sanitizeCell(preference.projectId)}` : ""} | ${sanitizeCell(preference.value)} | ${sanitizeCell(preference.confidence)} | ${sanitizeCell(preference.evidenceCount)} | ${sanitizeCell(preference.lastUpdated)} |`,
159
+ );
160
+ }
161
+
162
+ return lines.join("\n");
163
+ }
164
+
165
+ export function formatMemoryOverview(overview: InspectMemoryOverview): string {
166
+ const lines = [
167
+ "Memory Overview",
168
+ "",
169
+ `Total observations: ${overview.stats.totalObservations}`,
170
+ `Total projects: ${overview.stats.totalProjects}`,
171
+ `Total preferences: ${overview.stats.totalPreferences}`,
172
+ `Storage size: ${overview.stats.storageSizeKb} KB`,
173
+ "",
174
+ "Observations by type:",
175
+ ];
176
+
177
+ for (const [type, count] of Object.entries(overview.stats.observationsByType)) {
178
+ lines.push(`- ${type}: ${count}`);
179
+ }
180
+
181
+ lines.push("", "Recent observations:");
182
+ if (overview.recentObservations.length === 0) {
183
+ lines.push("- none");
184
+ } else {
185
+ for (const observation of overview.recentObservations) {
186
+ lines.push(
187
+ `- [${observation.type}] ${observation.summary} (${formatTimestamp(observation.createdAt)})`,
188
+ );
189
+ }
190
+ }
191
+
192
+ lines.push("", "Preferences:");
193
+ if (overview.preferences.length === 0) {
194
+ lines.push("- none");
195
+ } else {
196
+ for (const preference of overview.preferences) {
197
+ lines.push(
198
+ `- ${preference.key}: ${preference.value} (${preference.scope}, confidence ${preference.confidence}, evidence ${preference.evidenceCount})`,
199
+ );
200
+ }
201
+ }
202
+
203
+ return lines.join("\n");
204
+ }
205
+
206
+ export function formatPaths(details: InspectProjectDetails): string {
207
+ if (details.paths.length === 0) {
208
+ return `No paths found for ${details.project.name}.`;
209
+ }
210
+
211
+ const lines = [
212
+ `Paths for ${details.project.name}`,
213
+ "",
214
+ "| Path | Current | First Seen | Last Updated |",
215
+ "|------|---------|------------|--------------|",
216
+ ];
217
+
218
+ for (const path of details.paths) {
219
+ lines.push(
220
+ `| ${sanitizeCell(path.path)} | ${path.isCurrent ? "yes" : "no"} | ${sanitizeCell(path.firstSeenAt)} | ${sanitizeCell(path.lastUpdated)} |`,
221
+ );
222
+ }
223
+
224
+ return lines.join("\n");
225
+ }