@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
@@ -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
  */
@@ -33,9 +39,33 @@ async function detectContractHealth(projectRoot?: string): Promise<ContractHealt
33
39
  const artifactDir = getProjectArtifactDir(projectRoot);
34
40
  const logPath = join(artifactDir, "orchestration.jsonl");
35
41
  const content = await readFile(logPath, "utf-8");
42
+ const entries = content
43
+ .split("\n")
44
+ .map((line) => line.trim())
45
+ .filter(Boolean)
46
+ .map((line) => {
47
+ try {
48
+ return JSON.parse(line) as Record<string, unknown>;
49
+ } catch {
50
+ return null;
51
+ }
52
+ })
53
+ .filter((entry): entry is Record<string, unknown> => entry !== null);
54
+ const legacyTasksFallbackSeen =
55
+ entries.some(
56
+ (entry) =>
57
+ typeof entry.message === "string" &&
58
+ entry.message.includes("PLAN fallback: parsed legacy tasks.md"),
59
+ ) || content.includes("PLAN fallback: parsed legacy tasks.md");
60
+ const legacyResultParserSeen =
61
+ entries.some(
62
+ (entry) =>
63
+ typeof entry.message === "string" &&
64
+ entry.message.includes("Legacy result parser path used"),
65
+ ) || content.includes("Legacy result parser path used");
36
66
  return {
37
- legacyTasksFallbackSeen: content.includes("PLAN fallback: parsed legacy tasks.md"),
38
- legacyResultParserSeen: content.includes("Legacy result parser path used"),
67
+ legacyTasksFallbackSeen,
68
+ legacyResultParserSeen,
39
69
  };
40
70
  } catch {
41
71
  return {
@@ -63,6 +93,8 @@ const FIX_SUGGESTIONS: Readonly<Record<string, string>> = Object.freeze({
63
93
  "config-validity":
64
94
  "Run `bunx @kodrunhq/opencode-autopilot configure` to reconfigure, or delete ~/.config/opencode/opencode-autopilot.json to reset",
65
95
  "agent-injection": "Restart OpenCode to trigger agent re-injection via config hook",
96
+ "native-agent-suppression":
97
+ "Restart OpenCode and verify plugin config hook runs. If issue persists, check for conflicting OpenCode config or another plugin overriding agent entries",
66
98
  "asset-directories": "Restart OpenCode to trigger asset reinstallation",
67
99
  "skill-loading": "Ensure skills directory exists in ~/.config/opencode/skills/",
68
100
  "memory-db":
@@ -143,9 +175,12 @@ export async function doctorCore(options?: DoctorOptions): Promise<string> {
143
175
  export const ocDoctor = tool({
144
176
  description:
145
177
  "Run plugin health diagnostics. Reports pass/fail status for config, agents, " +
146
- "assets, and hooks. Like `brew doctor` for opencode-autopilot.",
178
+ "native suppression, assets, and hooks. Like `brew doctor` for opencode-autopilot.",
147
179
  args: {},
148
- async execute() {
149
- return doctorCore();
180
+ async execute(_args, context) {
181
+ return doctorCore({
182
+ openCodeConfig,
183
+ projectRoot: context.directory,
184
+ });
150
185
  },
151
186
  });
@@ -1,6 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
1
  import { tool } from "@opencode-ai/plugin";
2
+ import { readForensicEvents } from "../observability/forensic-log";
4
3
  import { loadState } from "../orchestrator/state";
5
4
  import { getProjectArtifactDir } from "../utils/paths";
6
5
 
@@ -23,16 +22,12 @@ function getSuggestedAction(failedPhase: string, recoverable: boolean): "resume"
23
22
  return "resume";
24
23
  }
25
24
 
26
- async function readRecentContractEvents(artifactDir: string): Promise<readonly string[]> {
25
+ async function readRecentContractEvents(projectRoot: string): Promise<readonly string[]> {
27
26
  try {
28
- const raw = await readFile(join(artifactDir, "orchestration.jsonl"), "utf-8");
29
- const lines = raw
30
- .split("\n")
31
- .map((line) => line.trim())
32
- .filter(Boolean)
33
- .slice(-120);
27
+ const events = (await readForensicEvents(projectRoot)).slice(-120);
34
28
  const codes = new Set<string>();
35
- for (const line of lines) {
29
+ for (const event of events) {
30
+ const searchable = `${String(event.code ?? "")} ${String(event.message ?? "")} ${JSON.stringify(event.payload ?? {})}`;
36
31
  for (const code of [
37
32
  "E_INVALID_RESULT",
38
33
  "E_STALE_RESULT",
@@ -42,7 +37,7 @@ async function readRecentContractEvents(artifactDir: string): Promise<readonly s
42
37
  "E_BUILD_TASK_ID_REQUIRED",
43
38
  "E_BUILD_UNKNOWN_TASK",
44
39
  ]) {
45
- if (line.includes(code)) {
40
+ if (searchable.includes(code)) {
46
41
  codes.add(code);
47
42
  }
48
43
  }
@@ -98,7 +93,7 @@ export async function forensicsCore(
98
93
  const recoverable = isRecoverable(failureContext.failedPhase);
99
94
  const suggestedAction = getSuggestedAction(failureContext.failedPhase, recoverable);
100
95
  const phasesCompleted = state.phases.filter((p) => p.status === "DONE").map((p) => p.name);
101
- const deterministicErrorCodes = await readRecentContractEvents(artifactDir);
96
+ const deterministicErrorCodes = await readRecentContractEvents(projectRoot);
102
97
 
103
98
  return JSON.stringify({
104
99
  failedPhase: failureContext.failedPhase,
package/src/tools/logs.ts CHANGED
@@ -79,9 +79,10 @@ export async function logsCore(
79
79
  options?: LogsOptions,
80
80
  logsDir?: string,
81
81
  ): Promise<string> {
82
+ const logsRoot = logsDir ?? process.cwd();
82
83
  switch (mode) {
83
84
  case "list": {
84
- const sessions = await listSessionLogs(logsDir);
85
+ const sessions = await listSessionLogs(logsRoot);
85
86
 
86
87
  return JSON.stringify({
87
88
  action: "logs_list",
@@ -92,8 +93,8 @@ export async function logsCore(
92
93
 
93
94
  case "detail": {
94
95
  const log = options?.sessionID
95
- ? await readSessionLog(options.sessionID, logsDir)
96
- : await readLatestSessionLog(logsDir);
96
+ ? await readSessionLog(options.sessionID, logsRoot)
97
+ : await readLatestSessionLog(logsRoot);
97
98
 
98
99
  if (!log) {
99
100
  const target = options?.sessionID
@@ -117,8 +118,8 @@ export async function logsCore(
117
118
 
118
119
  case "search": {
119
120
  const log = options?.sessionID
120
- ? await readSessionLog(options.sessionID, logsDir)
121
- : await readLatestSessionLog(logsDir);
121
+ ? await readSessionLog(options.sessionID, logsRoot)
122
+ : await readLatestSessionLog(logsRoot);
122
123
 
123
124
  if (!log) {
124
125
  const target = options?.sessionID
@@ -0,0 +1,157 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import {
4
+ deletePreferenceRecord,
5
+ deletePreferencesByKey,
6
+ getMemoryDb,
7
+ prunePreferenceEvidence,
8
+ prunePreferences,
9
+ } from "../memory";
10
+ import { resolveProjectIdentitySync } from "../projects/resolve";
11
+
12
+ type PreferenceScopeArg = "global" | "project";
13
+ type PreferenceStatusArg = "candidate" | "confirmed" | "rejected" | "unconfirmed" | "any";
14
+
15
+ interface MemoryPreferencesArgs {
16
+ readonly subcommand: "delete" | "prune" | "prune-evidence";
17
+ readonly id?: string;
18
+ readonly key?: string;
19
+ readonly scope?: PreferenceScopeArg;
20
+ readonly olderThanDays?: number;
21
+ readonly status?: PreferenceStatusArg;
22
+ readonly keepLatestPerPreference?: number;
23
+ }
24
+
25
+ function resolveProjectId(projectRoot: string, db: Database): string | null {
26
+ const resolved = resolveProjectIdentitySync(projectRoot, {
27
+ db,
28
+ allowCreate: false,
29
+ });
30
+ return resolved.id.startsWith("project:") ? null : resolved.id;
31
+ }
32
+
33
+ export function memoryPreferencesCore(
34
+ args: MemoryPreferencesArgs,
35
+ projectRoot: string,
36
+ db?: Database,
37
+ ): string {
38
+ try {
39
+ const resolvedDb = db ?? getMemoryDb();
40
+ const scope = args.scope ?? "global";
41
+ const projectId = scope === "project" ? resolveProjectId(projectRoot, resolvedDb) : null;
42
+
43
+ if (scope === "project" && projectId === null) {
44
+ return JSON.stringify({
45
+ error: "no_project_preferences",
46
+ message: "No known project identity for current directory.",
47
+ });
48
+ }
49
+
50
+ switch (args.subcommand) {
51
+ case "delete": {
52
+ if (typeof args.id === "string" && args.id.trim().length > 0) {
53
+ return JSON.stringify({
54
+ ok: true,
55
+ subcommand: "delete",
56
+ result: deletePreferenceRecord(args.id, resolvedDb),
57
+ });
58
+ }
59
+ if (typeof args.key === "string" && args.key.trim().length > 0) {
60
+ return JSON.stringify({
61
+ ok: true,
62
+ subcommand: "delete",
63
+ result: deletePreferencesByKey(args.key, { scope, projectId }, resolvedDb),
64
+ });
65
+ }
66
+ return JSON.stringify({
67
+ error: "id_or_key_required",
68
+ message: "delete requires either id or key.",
69
+ });
70
+ }
71
+
72
+ case "prune": {
73
+ if (typeof args.olderThanDays !== "number" || args.olderThanDays <= 0) {
74
+ return JSON.stringify({
75
+ error: "older_than_days_required",
76
+ message: "prune requires olderThanDays > 0.",
77
+ });
78
+ }
79
+ return JSON.stringify({
80
+ ok: true,
81
+ subcommand: "prune",
82
+ result: prunePreferences(
83
+ {
84
+ olderThanDays: args.olderThanDays,
85
+ scope,
86
+ projectId,
87
+ status: args.status ?? "unconfirmed",
88
+ },
89
+ resolvedDb,
90
+ ),
91
+ });
92
+ }
93
+
94
+ case "prune-evidence": {
95
+ if (typeof args.olderThanDays !== "number" || args.olderThanDays <= 0) {
96
+ return JSON.stringify({
97
+ error: "older_than_days_required",
98
+ message: "prune-evidence requires olderThanDays > 0.",
99
+ });
100
+ }
101
+ return JSON.stringify({
102
+ ok: true,
103
+ subcommand: "prune-evidence",
104
+ result: prunePreferenceEvidence(
105
+ {
106
+ olderThanDays: args.olderThanDays,
107
+ keepLatestPerPreference: args.keepLatestPerPreference,
108
+ scope,
109
+ projectId,
110
+ status: args.status ?? "any",
111
+ },
112
+ resolvedDb,
113
+ ),
114
+ });
115
+ }
116
+ }
117
+ } catch (error: unknown) {
118
+ return JSON.stringify({
119
+ error: error instanceof Error ? error.message : String(error),
120
+ });
121
+ }
122
+ }
123
+
124
+ export const ocMemoryPreferences = tool({
125
+ description:
126
+ "Manage learned preferences. Supports delete by id/key and pruning stale preference records or evidence.",
127
+ args: {
128
+ subcommand: tool.schema
129
+ .enum(["delete", "prune", "prune-evidence"])
130
+ .describe("Preference maintenance operation"),
131
+ id: tool.schema.string().optional().describe("Preference record id for delete"),
132
+ key: tool.schema.string().optional().describe("Preference key for delete"),
133
+ scope: tool.schema
134
+ .enum(["global", "project"])
135
+ .default("global")
136
+ .describe("Preference scope filter"),
137
+ olderThanDays: tool.schema
138
+ .number()
139
+ .int()
140
+ .positive()
141
+ .optional()
142
+ .describe("Delete records/evidence older than this many days"),
143
+ status: tool.schema
144
+ .enum(["candidate", "confirmed", "rejected", "unconfirmed", "any"])
145
+ .optional()
146
+ .describe("Preference status filter for prune operations"),
147
+ keepLatestPerPreference: tool.schema
148
+ .number()
149
+ .int()
150
+ .min(0)
151
+ .optional()
152
+ .describe("For prune-evidence, keep this many newest evidence rows per preference"),
153
+ },
154
+ async execute(args) {
155
+ return memoryPreferencesCore(args, process.cwd());
156
+ },
157
+ });
@@ -2,20 +2,15 @@
2
2
  * oc_memory_status tool — inspect memory system state.
3
3
  *
4
4
  * Shows observation counts, storage size, recent observations,
5
- * preferences, and per-type breakdowns. Follows the *Core + tool()
6
- * pattern from create-agent.ts.
5
+ * preferences, and per-type breakdowns through the shared inspection
6
+ * query layer.
7
7
  *
8
8
  * @module
9
9
  */
10
10
 
11
- import { Database } from "bun:sqlite";
12
- import { statSync } from "node:fs";
13
- import { join } from "node:path";
11
+ import type { Database } from "bun:sqlite";
14
12
  import { tool } from "@opencode-ai/plugin";
15
- import { DB_FILE, MEMORY_DIR, OBSERVATION_TYPES } from "../memory/constants";
16
- import { getMemoryDb } from "../memory/database";
17
- import { getAllPreferences } from "../memory/repository";
18
- import { getGlobalConfigDir } from "../utils/paths";
13
+ import { getMemoryOverview } from "../inspect/repository";
19
14
 
20
15
  interface MemoryStatusResult {
21
16
  readonly stats: {
@@ -47,94 +42,22 @@ export function memoryStatusCore(
47
42
  _args: { readonly detail?: "summary" | "full" },
48
43
  dbOrPath?: Database | string,
49
44
  ): MemoryStatusResult {
50
- let ownedDb: Database | null = null;
51
45
  try {
52
- if (typeof dbOrPath === "string") {
53
- ownedDb = new Database(dbOrPath);
54
- }
55
- const db = dbOrPath instanceof Database ? dbOrPath : (ownedDb ?? getMemoryDb());
56
-
57
- // Count observations
58
- const obsCountRow = db.query("SELECT COUNT(*) as cnt FROM observations").get() as {
59
- cnt: number;
60
- };
61
- const totalObservations = obsCountRow.cnt;
62
-
63
- // Count by type
64
- const typeRows = db
65
- .query("SELECT type, COUNT(*) as cnt FROM observations GROUP BY type")
66
- .all() as Array<{ type: string; cnt: number }>;
67
-
68
- const observationsByType: Record<string, number> = {};
69
- for (const t of OBSERVATION_TYPES) {
70
- observationsByType[t] = 0;
71
- }
72
- for (const row of typeRows) {
73
- observationsByType[row.type] = row.cnt;
74
- }
75
-
76
- // Count projects
77
- const projCountRow = db.query("SELECT COUNT(*) as cnt FROM projects").get() as {
78
- cnt: number;
79
- };
80
- const totalProjects = projCountRow.cnt;
81
-
82
- // Count preferences
83
- const prefCountRow = db.query("SELECT COUNT(*) as cnt FROM preferences").get() as {
84
- cnt: number;
85
- };
86
- const totalPreferences = prefCountRow.cnt;
87
-
88
- // Storage size — derive from actual DB path, not always the global default
89
- let storageSizeKb = 0;
90
- try {
91
- const statPath =
92
- typeof dbOrPath === "string" && dbOrPath !== ":memory:"
93
- ? dbOrPath
94
- : join(getGlobalConfigDir(), MEMORY_DIR, DB_FILE);
95
- const stat = statSync(statPath);
96
- storageSizeKb = Math.round(stat.size / 1024);
97
- } catch {
98
- // DB might be in-memory or path doesn't exist
99
- }
100
-
101
- // Recent observations (last 10)
102
- const recentRows = db
103
- .query(
104
- "SELECT type, summary, created_at, confidence FROM observations ORDER BY created_at DESC LIMIT 10",
105
- )
106
- .all() as Array<{
107
- type: string;
108
- summary: string;
109
- created_at: string;
110
- confidence: number;
111
- }>;
112
-
113
- const recentObservations = recentRows.map((row) => ({
114
- type: row.type,
115
- summary: row.summary,
116
- createdAt: row.created_at,
117
- confidence: row.confidence,
118
- }));
119
-
120
- // All preferences
121
- const allPrefs = getAllPreferences(db);
122
- const preferences = allPrefs.map((p) => ({
123
- key: p.key,
124
- value: p.value,
125
- confidence: p.confidence,
126
- }));
46
+ const overview = getMemoryOverview(dbOrPath);
127
47
 
128
48
  return {
129
- stats: {
130
- totalObservations,
131
- totalProjects,
132
- totalPreferences,
133
- storageSizeKb,
134
- observationsByType,
135
- },
136
- recentObservations,
137
- preferences,
49
+ stats: overview.stats,
50
+ recentObservations: overview.recentObservations.map((row) => ({
51
+ type: row.type,
52
+ summary: row.summary,
53
+ createdAt: row.createdAt,
54
+ confidence: row.confidence,
55
+ })),
56
+ preferences: overview.preferences.map((row) => ({
57
+ key: row.key,
58
+ value: row.value,
59
+ confidence: row.confidence,
60
+ })),
138
61
  };
139
62
  } catch (err) {
140
63
  const detail = err instanceof Error ? err.message : String(err);
@@ -144,8 +67,6 @@ export function memoryStatusCore(
144
67
  preferences: [],
145
68
  error: `Memory system error: ${detail}`,
146
69
  };
147
- } finally {
148
- ownedDb?.close();
149
70
  }
150
71
  }
151
72