@kodrunhq/opencode-autopilot 1.15.0 → 1.15.2

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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodrunhq/opencode-autopilot",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
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": [
@@ -71,6 +71,60 @@ function registerAgents(
71
71
  }
72
72
  }
73
73
 
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 {
98
+ if (!config.agent) return;
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]);
112
+ }
113
+ }
114
+ }
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
+
74
128
  export async function configHook(config: Config, configPath?: string): Promise<void> {
75
129
  if (!config.agent) {
76
130
  config.agent = {};
@@ -89,25 +143,16 @@ export async function configHook(config: Config, configPath?: string): Promise<v
89
143
  const groups: Readonly<Record<string, GroupModelAssignment>> = pluginConfig?.groups ?? {};
90
144
  const overrides: Readonly<Record<string, AgentOverride>> = pluginConfig?.overrides ?? {};
91
145
 
92
- // Snapshot built-in agent keys BEFORE we register ours — we only suppress
93
- // built-in Plan variants, not our own custom "planner" agent.
94
- const builtInKeys = new Set(Object.keys(config.agent));
95
-
96
146
  // Register standard agents and pipeline agents (v2 orchestrator subagents)
97
147
  registerAgents(agents, config, groups, overrides);
98
148
  registerAgents(pipelineAgents, config, groups, overrides);
99
149
 
100
- // Suppress built-in Plan agent our planner agent replaces it (D-17).
101
- // Only disable keys that existed before our registration (built-ins).
102
- const planVariants = ["Plan", "plan", "Planner", "planner"] as const;
103
- for (const variant of planVariants) {
104
- if (builtInKeys.has(variant) && config.agent[variant] !== undefined) {
105
- config.agent[variant] = {
106
- ...config.agent[variant],
107
- disable: true,
108
- };
109
- }
110
- }
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);
153
+
154
+ // Backward compatibility for legacy mode.plan/mode.build config shape.
155
+ suppressLegacyModePlanBuild(config);
111
156
  }
112
157
 
113
158
  export { autopilotAgent } from "./autopilot";
@@ -2,7 +2,7 @@ import type { AgentConfig } from "@opencode-ai/sdk";
2
2
 
3
3
  export const researcherAgent: Readonly<AgentConfig> = Object.freeze({
4
4
  description: "Searches the web about a topic and produces a comprehensive report with sources",
5
- mode: "subagent",
5
+ mode: "all",
6
6
  prompt: `You are a research specialist. Your job is to thoroughly investigate a given topic and produce a clear, well-organized report.
7
7
 
8
8
  ## Instructions
@@ -90,6 +90,85 @@ export async function agentHealthCheck(config: Config | null): Promise<HealthRes
90
90
  });
91
91
  }
92
92
 
93
+ /**
94
+ * Check that OpenCode native plan/build agents are suppressed by the plugin.
95
+ * Contract: both entries must have disable=true, mode=subagent, hidden=true.
96
+ */
97
+ export async function nativeAgentSuppressionHealthCheck(
98
+ config: Config | null,
99
+ ): Promise<HealthResult> {
100
+ if (!config?.agent) {
101
+ return Object.freeze({
102
+ name: "native-agent-suppression",
103
+ status: "fail" as const,
104
+ message: "No OpenCode config or agent map available",
105
+ });
106
+ }
107
+
108
+ const agentMap = config.agent as Record<string, unknown>;
109
+ const issues: string[] = [];
110
+ const requiredKeys = ["plan", "build"] as const;
111
+ const optionalKeys = ["Plan", "Build", "Planner", "Builder"] as const;
112
+
113
+ for (const key of requiredKeys) {
114
+ const raw = agentMap[key];
115
+ if (raw === undefined) {
116
+ issues.push(`${key}: missing config entry`);
117
+ continue;
118
+ }
119
+ if (typeof raw !== "object" || raw === null) {
120
+ issues.push(`${key}: invalid config entry type`);
121
+ continue;
122
+ }
123
+
124
+ const entry = raw as Record<string, unknown>;
125
+ if (entry.disable !== true) {
126
+ issues.push(`${key}: disable must be true`);
127
+ }
128
+ if (entry.mode !== "subagent") {
129
+ issues.push(`${key}: mode must be subagent`);
130
+ }
131
+ if (entry.hidden !== true) {
132
+ issues.push(`${key}: hidden must be true`);
133
+ }
134
+ }
135
+
136
+ for (const key of optionalKeys) {
137
+ const raw = agentMap[key];
138
+ if (raw === undefined) continue;
139
+ if (typeof raw !== "object" || raw === null) {
140
+ issues.push(`${key}: invalid config entry type`);
141
+ continue;
142
+ }
143
+
144
+ const entry = raw as Record<string, unknown>;
145
+ if (entry.disable !== true) {
146
+ issues.push(`${key}: disable must be true`);
147
+ }
148
+ if (entry.mode !== "subagent") {
149
+ issues.push(`${key}: mode must be subagent`);
150
+ }
151
+ if (entry.hidden !== true) {
152
+ issues.push(`${key}: hidden must be true`);
153
+ }
154
+ }
155
+
156
+ if (issues.length > 0) {
157
+ return Object.freeze({
158
+ name: "native-agent-suppression",
159
+ status: "fail" as const,
160
+ message: `${issues.length} native suppression issue(s) found`,
161
+ details: Object.freeze([...issues]),
162
+ });
163
+ }
164
+
165
+ return Object.freeze({
166
+ name: "native-agent-suppression",
167
+ status: "pass" as const,
168
+ message: "Native plan/build agents are suppressed",
169
+ });
170
+ }
171
+
93
172
  /**
94
173
  * Check that the source and target asset directories exist and are accessible.
95
174
  */
@@ -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
@@ -34,7 +34,7 @@ import {
34
34
  import { ocCreateAgent } from "./tools/create-agent";
35
35
  import { ocCreateCommand } from "./tools/create-command";
36
36
  import { ocCreateSkill } from "./tools/create-skill";
37
- import { ocDoctor } from "./tools/doctor";
37
+ import { ocDoctor, setOpenCodeConfig as setDoctorOpenCodeConfig } from "./tools/doctor";
38
38
  import { ocForensics } from "./tools/forensics";
39
39
  import { ocHashlineEdit } from "./tools/hashline-edit";
40
40
  import { ocLogs } from "./tools/logs";
@@ -255,6 +255,7 @@ const plugin: Plugin = async (input) => {
255
255
  config: async (cfg: Config) => {
256
256
  openCodeConfig = cfg;
257
257
  setOpenCodeConfig(cfg);
258
+ setDoctorOpenCodeConfig(cfg);
258
259
  await configHook(cfg);
259
260
  },
260
261
  "chat.message": async (
@@ -6,6 +6,12 @@ import { runHealthChecks } from "../health/runner";
6
6
  import type { HealthResult } from "../health/types";
7
7
  import { getProjectArtifactDir } from "../utils/paths";
8
8
 
9
+ let openCodeConfig: Config | null = null;
10
+
11
+ export function setOpenCodeConfig(config: Config | null): void {
12
+ openCodeConfig = config;
13
+ }
14
+
9
15
  /**
10
16
  * A single check in the doctor report, with an optional fix suggestion.
11
17
  */
@@ -63,6 +69,8 @@ const FIX_SUGGESTIONS: Readonly<Record<string, string>> = Object.freeze({
63
69
  "config-validity":
64
70
  "Run `bunx @kodrunhq/opencode-autopilot configure` to reconfigure, or delete ~/.config/opencode/opencode-autopilot.json to reset",
65
71
  "agent-injection": "Restart OpenCode to trigger agent re-injection via config hook",
72
+ "native-agent-suppression":
73
+ "Restart OpenCode and verify plugin config hook runs. If issue persists, check for conflicting OpenCode config or another plugin overriding agent entries",
66
74
  "asset-directories": "Restart OpenCode to trigger asset reinstallation",
67
75
  "skill-loading": "Ensure skills directory exists in ~/.config/opencode/skills/",
68
76
  "memory-db":
@@ -143,9 +151,12 @@ export async function doctorCore(options?: DoctorOptions): Promise<string> {
143
151
  export const ocDoctor = tool({
144
152
  description:
145
153
  "Run plugin health diagnostics. Reports pass/fail status for config, agents, " +
146
- "assets, and hooks. Like `brew doctor` for opencode-autopilot.",
154
+ "native suppression, assets, and hooks. Like `brew doctor` for opencode-autopilot.",
147
155
  args: {},
148
- async execute() {
149
- return doctorCore();
156
+ async execute(_args, context) {
157
+ return doctorCore({
158
+ openCodeConfig,
159
+ projectRoot: context.directory,
160
+ });
150
161
  },
151
162
  });