@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
package/README.md CHANGED
@@ -62,6 +62,20 @@ npm install -g @kodrunhq/opencode-autopilot
62
62
 
63
63
  Launch OpenCode. The plugin auto-installs agents, skills, and commands on first load and shows a welcome toast.
64
64
 
65
+ ### Agent visibility defaults
66
+
67
+ Primary Tab-cycle agents provided by this plugin are:
68
+
69
+ - `autopilot`
70
+ - `coder`
71
+ - `debugger`
72
+ - `planner`
73
+ - `researcher`
74
+ - `reviewer`
75
+
76
+ OpenCode native `plan` and `build` are suppressed by the plugin config hook to avoid
77
+ duplicate planning/building entries in the primary Tab menu.
78
+
65
79
  ### Verify your setup
66
80
 
67
81
  ```bash
package/bin/cli.ts CHANGED
@@ -11,6 +11,7 @@ import { ALL_GROUP_IDS, DIVERSITY_RULES, GROUP_DEFINITIONS } from "../src/regist
11
11
  import type { GroupId } from "../src/registry/types";
12
12
  import { fileExists } from "../src/utils/fs-helpers";
13
13
  import { runConfigure } from "./configure-tui";
14
+ import { runInspect } from "./inspect";
14
15
 
15
16
  const execFile = promisify(execFileCb);
16
17
 
@@ -314,6 +315,7 @@ function printUsage(): void {
314
315
  console.log(" install Register the plugin and create starter config");
315
316
  console.log(" configure Interactive model assignment for each agent group");
316
317
  console.log(" doctor Check installation health and model assignments");
318
+ console.log(" inspect Read-only inspection of projects, runs, events, and memory");
317
319
  console.log("");
318
320
  console.log("Options:");
319
321
  console.log(" --help, -h Show this help message");
@@ -336,6 +338,9 @@ if (import.meta.main) {
336
338
  case "doctor":
337
339
  await runDoctor();
338
340
  break;
341
+ case "inspect":
342
+ await runInspect(args.slice(1));
343
+ break;
339
344
  case "--help":
340
345
  case "-h":
341
346
  case undefined:
package/bin/inspect.ts ADDED
@@ -0,0 +1,337 @@
1
+ import {
2
+ formatEvents,
3
+ formatLessons,
4
+ formatMemoryOverview,
5
+ formatPaths,
6
+ formatPreferences,
7
+ formatProjectDetails,
8
+ formatProjects,
9
+ formatRuns,
10
+ } from "../src/inspect/formatters";
11
+ import {
12
+ getMemoryOverview,
13
+ getProjectDetails,
14
+ listEvents,
15
+ listLessons,
16
+ listPreferences,
17
+ listProjects,
18
+ listRuns,
19
+ } from "../src/inspect/repository";
20
+
21
+ type InspectView =
22
+ | "projects"
23
+ | "project"
24
+ | "paths"
25
+ | "runs"
26
+ | "events"
27
+ | "lessons"
28
+ | "preferences"
29
+ | "memory";
30
+
31
+ export interface InspectCliOptions {
32
+ readonly dbPath?: string;
33
+ }
34
+
35
+ export interface InspectCliResult {
36
+ readonly isError: boolean;
37
+ readonly output: string;
38
+ readonly format: "text" | "json";
39
+ }
40
+
41
+ interface ParsedInspectArgs {
42
+ readonly view: InspectView | null;
43
+ readonly json: boolean;
44
+ readonly projectRef: string | null;
45
+ readonly limit: number;
46
+ readonly runId: string | null;
47
+ readonly sessionId: string | null;
48
+ readonly type: string | null;
49
+ readonly help: boolean;
50
+ readonly error: string | null;
51
+ }
52
+
53
+ const INSPECT_VIEWS: readonly InspectView[] = Object.freeze([
54
+ "projects",
55
+ "project",
56
+ "paths",
57
+ "runs",
58
+ "events",
59
+ "lessons",
60
+ "preferences",
61
+ "memory",
62
+ ]);
63
+
64
+ function inspectUsage(): string {
65
+ return [
66
+ "Usage: opencode-autopilot inspect <view> [options]",
67
+ "",
68
+ "Views:",
69
+ " projects List known projects",
70
+ " project --project <ref> Show one project's details",
71
+ " paths --project <ref> List one project's path history",
72
+ " runs [--project <ref>] List pipeline runs",
73
+ " events [--project <ref>] List forensic events",
74
+ " lessons [--project <ref>] List stored lessons",
75
+ " preferences List stored preferences",
76
+ " memory Show memory overview",
77
+ "",
78
+ "Options:",
79
+ " --project <ref> Project id, path, or unique name",
80
+ " --run-id <id> Filter events by run id",
81
+ " --session-id <id> Filter events by session id",
82
+ " --type <type> Filter events by type",
83
+ " --limit <n> Limit rows (default: 20 for runs, 50 elsewhere)",
84
+ " --json Emit JSON output",
85
+ " --help, -h Show inspect help",
86
+ ].join("\n");
87
+ }
88
+
89
+ function parsePositiveInt(raw: string): number | null {
90
+ const parsed = Number.parseInt(raw, 10);
91
+ if (!Number.isFinite(parsed) || parsed <= 0) {
92
+ return null;
93
+ }
94
+ return parsed;
95
+ }
96
+
97
+ function parseInspectArgs(args: readonly string[]): ParsedInspectArgs {
98
+ let view: InspectView | null = null;
99
+ let json = false;
100
+ let projectRef: string | null = null;
101
+ let limit = 50;
102
+ let runId: string | null = null;
103
+ let sessionId: string | null = null;
104
+ let type: string | null = null;
105
+ let help = false;
106
+ let error: string | null = null;
107
+
108
+ for (let index = 0; index < args.length; index += 1) {
109
+ const arg = args[index];
110
+ if (arg === "--help" || arg === "-h") {
111
+ help = true;
112
+ continue;
113
+ }
114
+ if (arg === "--json") {
115
+ json = true;
116
+ continue;
117
+ }
118
+ if (arg === "--project") {
119
+ projectRef = args[index + 1] ?? null;
120
+ if (projectRef === null) {
121
+ error = "Missing value for --project.";
122
+ break;
123
+ }
124
+ index += 1;
125
+ continue;
126
+ }
127
+ if (arg === "--limit") {
128
+ const parsed = parsePositiveInt(args[index + 1] ?? "");
129
+ if (parsed === null) {
130
+ error = "--limit must be a positive integer.";
131
+ break;
132
+ }
133
+ limit = parsed;
134
+ index += 1;
135
+ continue;
136
+ }
137
+ if (arg === "--run-id") {
138
+ runId = args[index + 1] ?? null;
139
+ if (runId === null) {
140
+ error = "Missing value for --run-id.";
141
+ break;
142
+ }
143
+ index += 1;
144
+ continue;
145
+ }
146
+ if (arg === "--session-id") {
147
+ sessionId = args[index + 1] ?? null;
148
+ if (sessionId === null) {
149
+ error = "Missing value for --session-id.";
150
+ break;
151
+ }
152
+ index += 1;
153
+ continue;
154
+ }
155
+ if (arg === "--type") {
156
+ type = args[index + 1] ?? null;
157
+ if (type === null) {
158
+ error = "Missing value for --type.";
159
+ break;
160
+ }
161
+ index += 1;
162
+ continue;
163
+ }
164
+
165
+ if (view === null) {
166
+ if ((INSPECT_VIEWS as readonly string[]).includes(arg)) {
167
+ view = arg as InspectView;
168
+ if (view === "runs") {
169
+ limit = 20;
170
+ }
171
+ continue;
172
+ }
173
+ error = `Unknown inspect view: ${arg}`;
174
+ break;
175
+ }
176
+
177
+ if (projectRef === null && (view === "project" || view === "paths")) {
178
+ projectRef = arg;
179
+ continue;
180
+ }
181
+
182
+ error = `Unexpected argument: ${arg}`;
183
+ break;
184
+ }
185
+
186
+ if (!help && error === null && view === null) {
187
+ help = true;
188
+ }
189
+
190
+ if (
191
+ error === null &&
192
+ (view === "project" || view === "paths") &&
193
+ (projectRef === null || projectRef.trim().length === 0)
194
+ ) {
195
+ error = `${view} view requires --project <ref> or a positional project reference.`;
196
+ }
197
+
198
+ return {
199
+ view,
200
+ json,
201
+ projectRef,
202
+ limit,
203
+ runId,
204
+ sessionId,
205
+ type,
206
+ help,
207
+ error,
208
+ };
209
+ }
210
+
211
+ function makeOutput(payload: unknown, json: boolean, text: string): InspectCliResult {
212
+ return Object.freeze({
213
+ isError: false,
214
+ format: json ? "json" : "text",
215
+ output: json ? JSON.stringify(payload, null, 2) : text,
216
+ });
217
+ }
218
+
219
+ function makeError(message: string, json: boolean): InspectCliResult {
220
+ return Object.freeze({
221
+ isError: true,
222
+ format: json ? "json" : "text",
223
+ output: json
224
+ ? JSON.stringify({ action: "error", message }, null, 2)
225
+ : `${message}\n\n${inspectUsage()}`,
226
+ });
227
+ }
228
+
229
+ export async function inspectCliCore(
230
+ args: readonly string[],
231
+ options: InspectCliOptions = {},
232
+ ): Promise<InspectCliResult> {
233
+ const parsed = parseInspectArgs(args);
234
+ if (parsed.help) {
235
+ return makeOutput({ action: "help", usage: inspectUsage() }, parsed.json, inspectUsage());
236
+ }
237
+ if (parsed.error !== null) {
238
+ return makeError(parsed.error, parsed.json);
239
+ }
240
+
241
+ const dbInput = options.dbPath;
242
+ switch (parsed.view) {
243
+ case "projects": {
244
+ const projects = listProjects(dbInput);
245
+ return makeOutput(
246
+ { action: "inspect_projects", projects },
247
+ parsed.json,
248
+ formatProjects(projects),
249
+ );
250
+ }
251
+ case "project": {
252
+ const details = getProjectDetails(parsed.projectRef!, dbInput);
253
+ if (details === null) {
254
+ return makeError(`Project not found: ${parsed.projectRef}`, parsed.json);
255
+ }
256
+ return makeOutput(
257
+ { action: "inspect_project", project: details },
258
+ parsed.json,
259
+ formatProjectDetails(details),
260
+ );
261
+ }
262
+ case "paths": {
263
+ const details = getProjectDetails(parsed.projectRef!, dbInput);
264
+ if (details === null) {
265
+ return makeError(`Project not found: ${parsed.projectRef}`, parsed.json);
266
+ }
267
+ return makeOutput(
268
+ { action: "inspect_paths", project: details.project, paths: details.paths },
269
+ parsed.json,
270
+ formatPaths(details),
271
+ );
272
+ }
273
+ case "runs": {
274
+ const runs = listRuns(
275
+ { projectRef: parsed.projectRef ?? undefined, limit: parsed.limit },
276
+ dbInput,
277
+ );
278
+ return makeOutput({ action: "inspect_runs", runs }, parsed.json, formatRuns(runs));
279
+ }
280
+ case "events": {
281
+ const events = listEvents(
282
+ {
283
+ projectRef: parsed.projectRef ?? undefined,
284
+ runId: parsed.runId ?? undefined,
285
+ sessionId: parsed.sessionId ?? undefined,
286
+ type: parsed.type ?? undefined,
287
+ limit: parsed.limit,
288
+ },
289
+ dbInput,
290
+ );
291
+ return makeOutput({ action: "inspect_events", events }, parsed.json, formatEvents(events));
292
+ }
293
+ case "lessons": {
294
+ const lessons = listLessons(
295
+ { projectRef: parsed.projectRef ?? undefined, limit: parsed.limit },
296
+ dbInput,
297
+ );
298
+ return makeOutput(
299
+ { action: "inspect_lessons", lessons },
300
+ parsed.json,
301
+ formatLessons(lessons),
302
+ );
303
+ }
304
+ case "preferences": {
305
+ const preferences = listPreferences(dbInput);
306
+ return makeOutput(
307
+ { action: "inspect_preferences", preferences },
308
+ parsed.json,
309
+ formatPreferences(preferences),
310
+ );
311
+ }
312
+ case "memory": {
313
+ const overview = getMemoryOverview(dbInput);
314
+ return makeOutput(
315
+ { action: "inspect_memory", overview },
316
+ parsed.json,
317
+ formatMemoryOverview(overview),
318
+ );
319
+ }
320
+ case null:
321
+ return makeOutput({ action: "help", usage: inspectUsage() }, parsed.json, inspectUsage());
322
+ }
323
+ }
324
+
325
+ export async function runInspect(
326
+ args: readonly string[],
327
+ options: InspectCliOptions = {},
328
+ ): Promise<void> {
329
+ const result = await inspectCliCore(args, options);
330
+ if (result.isError) {
331
+ console.error(result.output);
332
+ process.exitCode = 1;
333
+ return;
334
+ }
335
+
336
+ console.log(result.output);
337
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodrunhq/opencode-autopilot",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "description": "Curated agents, skills, and commands for the OpenCode AI coding CLI — autonomous orchestrator, multi-agent code review, model fallback, and in-session asset creation tools.",
5
5
  "main": "src/index.ts",
6
6
  "keywords": [
@@ -9,10 +9,10 @@ export const autopilotAgent: Readonly<AgentConfig> = Object.freeze({
9
9
 
10
10
  ## Loop
11
11
 
12
- 1. Call oc_orchestrate with your initial idea (first turn) or with the result from the previous agent.
12
+ 1. Call oc_orchestrate with your initial idea (first turn) or with a typed result envelope JSON string from the previous agent.
13
13
  2. Parse the JSON response.
14
- 3. If action is "dispatch": call the named agent with the provided prompt, then pass its output back to oc_orchestrate via the result parameter.
15
- 4. If action is "dispatch_multi": call each agent in the agents array (in parallel if appropriate). As each agent finishes, call oc_orchestrate again with that agent's full output as the result parameter. Do NOT combine multiple agents' outputs into a single result.
14
+ 3. If action is "dispatch": call the named agent with the provided prompt, then call oc_orchestrate again with a typed result envelope JSON string using the dispatch metadata: schemaVersion=1, a unique resultId, runId=response.runId, phase=response.phase, dispatchId=response.dispatchId, agent=response.agent, kind=response.expectedResultKind ?? response.resultKind, taskId=response.taskId ?? null, payload.text=<full agent output>.
15
+ 4. If action is "dispatch_multi": do the same for each agent entry. Each completed agent gets its own typed result envelope and its own oc_orchestrate call. Do NOT combine multiple agents' outputs into one result.
16
16
  5. If action is "complete": report the summary to the user. You are done.
17
17
  6. If action is "error": report the error to the user. Stop.
18
18
 
@@ -24,20 +24,12 @@ When editing files, prefer oc_hashline_edit over the built-in edit tool. Hash-an
24
24
 
25
25
  - NEVER skip calling oc_orchestrate. It is the single source of truth for pipeline state.
26
26
  - NEVER make pipeline decisions yourself. Always defer to oc_orchestrate.
27
- - ALWAYS pass the full agent output back as the result parameter.
27
+ - NEVER pass raw agent output as result. ALWAYS send a typed result envelope JSON string.
28
+ - ALWAYS preserve the full agent output in payload.text.
29
+ - ALWAYS use a unique resultId for every returned result.
28
30
  - Do not attempt to run phases out of order.
29
31
  - Do not retry a failed phase unless oc_orchestrate instructs you to.
30
- - If an agent dispatch fails, pass the error message back to oc_orchestrate as the result.
31
-
32
- ## Example Turn Sequence
33
-
34
- Turn 1: oc_orchestrate(idea="Build a CLI tool")
35
- -> {action:"dispatch", agent:"oc-researcher", prompt:"Research: Build a CLI tool", phase:"RECON"}
36
- Turn 2: @oc-researcher "Research: Build a CLI tool"
37
- -> "Research findings: ..."
38
- Turn 3: oc_orchestrate(result="Research findings: ...")
39
- -> {action:"dispatch", agent:"oc-challenger", prompt:"Challenge: ...", phase:"CHALLENGE"}
40
- ... continues until action is "complete"`,
32
+ - If an agent dispatch fails, wrap the error text in payload.text and still return a typed result envelope.`,
41
33
  permission: {
42
34
  edit: "allow",
43
35
  bash: "allow",
@@ -71,22 +71,60 @@ function registerAgents(
71
71
  }
72
72
  }
73
73
 
74
- function suppressBuiltInVariants(
75
- variants: readonly string[],
76
- builtInKeys: ReadonlySet<string>,
77
- config: Config,
78
- ): void {
74
+ const nativeSuppressionPatch = Object.freeze({
75
+ disable: true,
76
+ mode: "subagent" as const,
77
+ hidden: true,
78
+ });
79
+
80
+ const optionalNativePlanBuildKeys = Object.freeze(["Plan", "Build", "Planner", "Builder"] as const);
81
+
82
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
83
+ return typeof value === "object" && value !== null;
84
+ }
85
+
86
+ function mergeSuppressionPatch(entry: unknown): Record<string, unknown> {
87
+ if (isObjectRecord(entry)) {
88
+ return {
89
+ ...entry,
90
+ ...nativeSuppressionPatch,
91
+ };
92
+ }
93
+
94
+ return { ...nativeSuppressionPatch };
95
+ }
96
+
97
+ function suppressNativePlanBuildAgents(config: Config): void {
79
98
  if (!config.agent) return;
80
- for (const variant of variants) {
81
- if (builtInKeys.has(variant) && config.agent[variant] !== undefined) {
82
- config.agent[variant] = {
83
- ...config.agent[variant],
84
- disable: true,
85
- };
99
+
100
+ const agentRef = config.agent as Record<string, unknown>;
101
+
102
+ // Deterministically suppress native lowercase keys even if OpenCode did not
103
+ // pre-populate them before configHook execution.
104
+ for (const key of ["plan", "build"] as const) {
105
+ agentRef[key] = mergeSuppressionPatch(agentRef[key]);
106
+ }
107
+
108
+ // Also suppress optional native variants when present.
109
+ for (const key of optionalNativePlanBuildKeys) {
110
+ if (agentRef[key] !== undefined) {
111
+ agentRef[key] = mergeSuppressionPatch(agentRef[key]);
86
112
  }
87
113
  }
88
114
  }
89
115
 
116
+ function suppressLegacyModePlanBuild(config: Config): void {
117
+ if (!config.mode) return;
118
+
119
+ const modeRef = config.mode as Record<string, unknown>;
120
+ for (const key of ["plan", "build", ...optionalNativePlanBuildKeys] as const) {
121
+ const existing = modeRef[key];
122
+ if (existing === undefined) continue;
123
+ if (!isObjectRecord(existing)) continue;
124
+ modeRef[key] = mergeSuppressionPatch(existing);
125
+ }
126
+ }
127
+
90
128
  export async function configHook(config: Config, configPath?: string): Promise<void> {
91
129
  if (!config.agent) {
92
130
  config.agent = {};
@@ -105,21 +143,16 @@ export async function configHook(config: Config, configPath?: string): Promise<v
105
143
  const groups: Readonly<Record<string, GroupModelAssignment>> = pluginConfig?.groups ?? {};
106
144
  const overrides: Readonly<Record<string, AgentOverride>> = pluginConfig?.overrides ?? {};
107
145
 
108
- // Snapshot built-in agent keys BEFORE we register ours — we only suppress
109
- // built-in Plan/Build variants, never our custom planner/coder agents.
110
- const builtInKeys = new Set(Object.keys(config.agent));
111
-
112
146
  // Register standard agents and pipeline agents (v2 orchestrator subagents)
113
147
  registerAgents(agents, config, groups, overrides);
114
148
  registerAgents(pipelineAgents, config, groups, overrides);
115
149
 
116
- // Suppress built-in Plan/Build agents planner/coder replace them.
117
- // Only disable keys that existed before our registration (built-ins).
118
- const planVariants = ["Plan", "plan", "Planner", "planner"] as const;
119
- suppressBuiltInVariants(planVariants, builtInKeys, config);
150
+ // Suppress native built-in Plan/Build agents. This is deterministic and does
151
+ // not rely on whether OpenCode pre-populated keys before configHook runs.
152
+ suppressNativePlanBuildAgents(config);
120
153
 
121
- const buildVariants = ["Build", "build", "Builder", "builder"] as const;
122
- suppressBuiltInVariants(buildVariants, builtInKeys, config);
154
+ // Backward compatibility for legacy mode.plan/mode.build config shape.
155
+ suppressLegacyModePlanBuild(config);
123
156
  }
124
157
 
125
158
  export { autopilotAgent } from "./autopilot";
@@ -4,11 +4,15 @@ import { join } from "node:path";
4
4
  import type { Config } from "@opencode-ai/plugin";
5
5
  import { parse } from "yaml";
6
6
  import { loadConfig } from "../config";
7
- import { DB_FILE, MEMORY_DIR } from "../memory/constants";
8
7
  import { AGENT_NAMES } from "../orchestrator/handlers/types";
9
8
  import { detectProjectStackTags, filterSkillsByStack } from "../skills/adaptive-injector";
10
9
  import { loadAllSkills } from "../skills/loader";
11
- import { getAssetsDir, getGlobalConfigDir } from "../utils/paths";
10
+ import {
11
+ getAssetsDir,
12
+ getAutopilotDbPath,
13
+ getGlobalConfigDir,
14
+ getLegacyMemoryDbPath,
15
+ } from "../utils/paths";
12
16
  import type { HealthResult } from "./types";
13
17
 
14
18
  /**
@@ -90,6 +94,85 @@ export async function agentHealthCheck(config: Config | null): Promise<HealthRes
90
94
  });
91
95
  }
92
96
 
97
+ /**
98
+ * Check that OpenCode native plan/build agents are suppressed by the plugin.
99
+ * Contract: both entries must have disable=true, mode=subagent, hidden=true.
100
+ */
101
+ export async function nativeAgentSuppressionHealthCheck(
102
+ config: Config | null,
103
+ ): Promise<HealthResult> {
104
+ if (!config?.agent) {
105
+ return Object.freeze({
106
+ name: "native-agent-suppression",
107
+ status: "fail" as const,
108
+ message: "No OpenCode config or agent map available",
109
+ });
110
+ }
111
+
112
+ const agentMap = config.agent as Record<string, unknown>;
113
+ const issues: string[] = [];
114
+ const requiredKeys = ["plan", "build"] as const;
115
+ const optionalKeys = ["Plan", "Build", "Planner", "Builder"] as const;
116
+
117
+ for (const key of requiredKeys) {
118
+ const raw = agentMap[key];
119
+ if (raw === undefined) {
120
+ issues.push(`${key}: missing config entry`);
121
+ continue;
122
+ }
123
+ if (typeof raw !== "object" || raw === null) {
124
+ issues.push(`${key}: invalid config entry type`);
125
+ continue;
126
+ }
127
+
128
+ const entry = raw as Record<string, unknown>;
129
+ if (entry.disable !== true) {
130
+ issues.push(`${key}: disable must be true`);
131
+ }
132
+ if (entry.mode !== "subagent") {
133
+ issues.push(`${key}: mode must be subagent`);
134
+ }
135
+ if (entry.hidden !== true) {
136
+ issues.push(`${key}: hidden must be true`);
137
+ }
138
+ }
139
+
140
+ for (const key of optionalKeys) {
141
+ const raw = agentMap[key];
142
+ if (raw === undefined) continue;
143
+ if (typeof raw !== "object" || raw === null) {
144
+ issues.push(`${key}: invalid config entry type`);
145
+ continue;
146
+ }
147
+
148
+ const entry = raw as Record<string, unknown>;
149
+ if (entry.disable !== true) {
150
+ issues.push(`${key}: disable must be true`);
151
+ }
152
+ if (entry.mode !== "subagent") {
153
+ issues.push(`${key}: mode must be subagent`);
154
+ }
155
+ if (entry.hidden !== true) {
156
+ issues.push(`${key}: hidden must be true`);
157
+ }
158
+ }
159
+
160
+ if (issues.length > 0) {
161
+ return Object.freeze({
162
+ name: "native-agent-suppression",
163
+ status: "fail" as const,
164
+ message: `${issues.length} native suppression issue(s) found`,
165
+ details: Object.freeze([...issues]),
166
+ });
167
+ }
168
+
169
+ return Object.freeze({
170
+ name: "native-agent-suppression",
171
+ status: "pass" as const,
172
+ message: "Native plan/build agents are suppressed",
173
+ });
174
+ }
175
+
93
176
  /**
94
177
  * Check that the source and target asset directories exist and are accessible.
95
178
  */
@@ -170,19 +253,40 @@ export async function skillHealthCheck(
170
253
  */
171
254
  export async function memoryHealthCheck(baseDir?: string): Promise<HealthResult> {
172
255
  const resolvedBase = baseDir ?? getGlobalConfigDir();
173
- const dbPath = join(resolvedBase, MEMORY_DIR, DB_FILE);
256
+ const dbPath = getAutopilotDbPath(resolvedBase);
257
+ const legacyDbPath = getLegacyMemoryDbPath(resolvedBase);
174
258
 
175
259
  try {
176
260
  await access(dbPath);
177
261
  } catch (error: unknown) {
178
262
  const code = (error as NodeJS.ErrnoException).code;
179
263
  if (code === "ENOENT") {
264
+ try {
265
+ await access(legacyDbPath);
266
+ } catch (legacyError: unknown) {
267
+ const legacyCode = (legacyError as NodeJS.ErrnoException).code;
268
+ if (legacyCode === "ENOENT") {
269
+ return Object.freeze({
270
+ name: "memory-db",
271
+ status: "pass" as const,
272
+ message: `Memory DB not yet initialized -- will be created on first memory capture`,
273
+ });
274
+ }
275
+ const legacyMsg = legacyError instanceof Error ? legacyError.message : String(legacyError);
276
+ return Object.freeze({
277
+ name: "memory-db",
278
+ status: "fail" as const,
279
+ message: `Memory DB inaccessible: ${legacyMsg}`,
280
+ });
281
+ }
282
+
180
283
  return Object.freeze({
181
284
  name: "memory-db",
182
285
  status: "pass" as const,
183
- message: `Memory DB not yet initialized -- will be created on first memory capture`,
286
+ message: `Legacy memory DB found -- unified DB will be created on next write`,
184
287
  });
185
288
  }
289
+
186
290
  const msg = error instanceof Error ? error.message : String(error);
187
291
  return Object.freeze({
188
292
  name: "memory-db",