@kodrunhq/opencode-autopilot 1.15.2 → 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/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/health/checks.ts +29 -4
- package/src/index.ts +103 -11
- 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 +26 -2
- 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/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/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
|
/**
|
|
@@ -249,19 +253,40 @@ export async function skillHealthCheck(
|
|
|
249
253
|
*/
|
|
250
254
|
export async function memoryHealthCheck(baseDir?: string): Promise<HealthResult> {
|
|
251
255
|
const resolvedBase = baseDir ?? getGlobalConfigDir();
|
|
252
|
-
const dbPath =
|
|
256
|
+
const dbPath = getAutopilotDbPath(resolvedBase);
|
|
257
|
+
const legacyDbPath = getLegacyMemoryDbPath(resolvedBase);
|
|
253
258
|
|
|
254
259
|
try {
|
|
255
260
|
await access(dbPath);
|
|
256
261
|
} catch (error: unknown) {
|
|
257
262
|
const code = (error as NodeJS.ErrnoException).code;
|
|
258
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
|
+
|
|
259
283
|
return Object.freeze({
|
|
260
284
|
name: "memory-db",
|
|
261
285
|
status: "pass" as const,
|
|
262
|
-
message: `
|
|
286
|
+
message: `Legacy memory DB found -- unified DB will be created on next write`,
|
|
263
287
|
});
|
|
264
288
|
}
|
|
289
|
+
|
|
265
290
|
const msg = error instanceof Error ? error.message : String(error);
|
|
266
291
|
return Object.freeze({
|
|
267
292
|
name: "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 {
|
|
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,
|
|
@@ -38,6 +43,7 @@ import { ocDoctor, setOpenCodeConfig as setDoctorOpenCodeConfig } from "./tools/
|
|
|
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:
|
|
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)
|
|
@@ -269,6 +357,10 @@ const plugin: Plugin = async (input) => {
|
|
|
269
357
|
parts: unknown[];
|
|
270
358
|
},
|
|
271
359
|
) => {
|
|
360
|
+
if (memoryChatMessageHandler) {
|
|
361
|
+
await memoryChatMessageHandler(hookInput, output);
|
|
362
|
+
}
|
|
363
|
+
|
|
272
364
|
if (fallbackConfig.enabled) {
|
|
273
365
|
await chatMessageHandler(hookInput, output);
|
|
274
366
|
}
|