@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.
- package/README.md +14 -0
- 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/agents/index.ts +54 -21
- package/src/health/checks.ts +108 -4
- package/src/health/runner.ts +3 -0
- package/src/index.ts +105 -12
- 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 +40 -5
- 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
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.
|
|
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": [
|
package/src/agents/autopilot.ts
CHANGED
|
@@ -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
|
|
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
|
|
15
|
-
4. If action is "dispatch_multi":
|
|
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
|
-
-
|
|
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,
|
|
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",
|
package/src/agents/index.ts
CHANGED
|
@@ -71,22 +71,60 @@ function registerAgents(
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
117
|
-
//
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
154
|
+
// Backward compatibility for legacy mode.plan/mode.build config shape.
|
|
155
|
+
suppressLegacyModePlanBuild(config);
|
|
123
156
|
}
|
|
124
157
|
|
|
125
158
|
export { autopilotAgent } from "./autopilot";
|
package/src/health/checks.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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: `
|
|
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",
|