@kodrunhq/opencode-autopilot 1.16.0 → 1.18.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 (61) hide show
  1. package/assets/commands/oc-doctor.md +17 -0
  2. package/bin/configure-tui.ts +1 -1
  3. package/bin/inspect.ts +2 -2
  4. package/package.json +1 -1
  5. package/src/config/index.ts +29 -0
  6. package/src/config/migrations.ts +196 -0
  7. package/src/config/v7.ts +45 -0
  8. package/src/config.ts +108 -24
  9. package/src/health/checks.ts +165 -0
  10. package/src/health/runner.ts +8 -2
  11. package/src/health/types.ts +1 -1
  12. package/src/index.ts +25 -2
  13. package/src/kernel/transaction.ts +48 -0
  14. package/src/kernel/types.ts +1 -2
  15. package/src/logging/domains.ts +39 -0
  16. package/src/logging/forensic-writer.ts +177 -0
  17. package/src/logging/index.ts +4 -0
  18. package/src/logging/logger.ts +44 -0
  19. package/src/logging/performance.ts +59 -0
  20. package/src/logging/rotation.ts +261 -0
  21. package/src/logging/types.ts +33 -0
  22. package/src/memory/capture-utils.ts +149 -0
  23. package/src/memory/capture.ts +16 -197
  24. package/src/memory/decay.ts +11 -2
  25. package/src/memory/injector.ts +4 -1
  26. package/src/memory/lessons.ts +85 -0
  27. package/src/memory/observations.ts +177 -0
  28. package/src/memory/preferences.ts +718 -0
  29. package/src/memory/projects.ts +83 -0
  30. package/src/memory/repository.ts +46 -1001
  31. package/src/memory/retrieval.ts +5 -1
  32. package/src/observability/context-display.ts +8 -0
  33. package/src/observability/event-handlers.ts +44 -6
  34. package/src/observability/forensic-log.ts +10 -2
  35. package/src/observability/forensic-schemas.ts +9 -1
  36. package/src/observability/log-reader.ts +20 -1
  37. package/src/orchestrator/error-context.ts +24 -0
  38. package/src/orchestrator/handlers/build-utils.ts +118 -0
  39. package/src/orchestrator/handlers/build.ts +13 -148
  40. package/src/orchestrator/handlers/retrospective.ts +0 -1
  41. package/src/orchestrator/lesson-memory.ts +7 -2
  42. package/src/orchestrator/orchestration-logger.ts +46 -31
  43. package/src/orchestrator/progress.ts +63 -0
  44. package/src/review/memory.ts +11 -3
  45. package/src/review/parse-findings.ts +116 -0
  46. package/src/review/pipeline.ts +3 -107
  47. package/src/review/selection.ts +38 -4
  48. package/src/scoring/time-provider.ts +23 -0
  49. package/src/tools/configure.ts +1 -1
  50. package/src/tools/doctor.ts +2 -2
  51. package/src/tools/logs.ts +32 -6
  52. package/src/tools/orchestrate.ts +11 -9
  53. package/src/tools/replay.ts +42 -0
  54. package/src/tools/review.ts +8 -2
  55. package/src/tools/summary.ts +43 -0
  56. package/src/types/background.ts +51 -0
  57. package/src/types/mcp.ts +27 -0
  58. package/src/types/recovery.ts +39 -0
  59. package/src/types/routing.ts +39 -0
  60. package/src/utils/random.ts +33 -0
  61. package/src/ux/session-summary.ts +56 -0
@@ -1,14 +1,16 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { join } from "node:path";
3
2
  import { tool } from "@opencode-ai/plugin";
3
+ import { getLogger } from "../logging/domains";
4
4
  import { parseTypedResultEnvelope } from "../orchestrator/contracts/legacy-result-adapter";
5
5
  import type { PendingDispatch, ResultEnvelope } from "../orchestrator/contracts/result-envelope";
6
+ import { enrichErrorMessage } from "../orchestrator/error-context";
6
7
  import { PHASE_HANDLERS } from "../orchestrator/handlers/index";
7
8
  import type { DispatchResult, PhaseHandlerContext } from "../orchestrator/handlers/types";
8
9
  import { buildLessonContext } from "../orchestrator/lesson-injection";
9
10
  import { loadLessonMemory } from "../orchestrator/lesson-memory";
10
11
  import { logOrchestrationEvent } from "../orchestrator/orchestration-logger";
11
12
  import { completePhase, getNextPhase, PHASE_INDEX, TOTAL_PHASES } from "../orchestrator/phase";
13
+ import { getPhaseProgressString } from "../orchestrator/progress";
12
14
  import { loadAdaptiveSkillContext } from "../orchestrator/skill-injection";
13
15
  import {
14
16
  createInitialState,
@@ -311,17 +313,16 @@ async function injectSkillContext(
311
313
  });
312
314
  if (ctx) return prompt + ctx;
313
315
  } catch (err) {
314
- console.warn("[opencode-autopilot] skill injection failed:", err);
316
+ getLogger("tool", "orchestrate").warn("skill injection failed", { err });
315
317
  }
316
318
  return prompt;
317
319
  }
318
320
 
319
321
  /** Build a human-readable progress string for user-facing display. */
320
- function buildUserProgress(phase: string, label?: string, attempt?: number): string {
321
- const idx = PHASE_INDEX[phase as Phase] ?? 0;
322
- const desc = label ?? "dispatching";
322
+ function buildUserProgress(state: PipelineState, label?: string, attempt?: number): string {
323
+ const baseProgress = getPhaseProgressString(state);
323
324
  const att = attempt != null ? ` (attempt ${attempt})` : "";
324
- return `Phase ${idx}/${TOTAL_PHASES}: ${phase} — ${desc}${att}`;
325
+ return `${baseProgress}${label ? ` — ${label}` : ""}${att}`;
325
326
  }
326
327
 
327
328
  /** Per-phase dispatch limits. BUILD is high because of multi-task waves. */
@@ -429,7 +430,7 @@ async function processHandlerResult(
429
430
  };
430
431
 
431
432
  // Log the dispatch event before any inline-review or context injection
432
- const progress = buildUserProgress(phase, normalizedResult.progress, attempt);
433
+ const progress = buildUserProgress(currentState, normalizedResult.progress, attempt);
433
434
  logOrchestrationEvent(artifactDir, {
434
435
  timestamp: new Date().toISOString(),
435
436
  phase,
@@ -544,7 +545,7 @@ async function processHandlerResult(
544
545
  taskId: entry.taskId ?? null,
545
546
  })) ?? [];
546
547
 
547
- const progress = buildUserProgress(phase, normalizedResult.progress, attempt);
548
+ const progress = buildUserProgress(currentState, normalizedResult.progress, attempt);
548
549
  logOrchestrationEvent(artifactDir, {
549
550
  timestamp: new Date().toISOString(),
550
551
  phase,
@@ -759,12 +760,13 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
759
760
  } catch (error: unknown) {
760
761
  const message = error instanceof Error ? error.message : String(error);
761
762
  const parsedErr = parseErrorCode(error);
762
- const safeMessage = message.replace(/[/\\][^\s"']+/g, "[PATH]").slice(0, 4096);
763
+ let safeMessage = message.replace(/[/\\][^\s"']+/g, "[PATH]").slice(0, 4096);
763
764
 
764
765
  // Persist failure metadata for forensics (best-effort)
765
766
  try {
766
767
  const currentState = await loadState(artifactDir);
767
768
  if (currentState?.currentPhase) {
769
+ safeMessage = enrichErrorMessage(safeMessage, currentState);
768
770
  const lastDone = currentState.phases.filter((p) => p.status === "DONE").pop();
769
771
  const failureContext = {
770
772
  failedPhase: currentState.currentPhase,
@@ -0,0 +1,42 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import type { ReviewState } from "../review/types";
3
+
4
+ export const ocReplay = tool({
5
+ description:
6
+ "Verify determinism by replaying a known sequence of inputs to the review pipeline and ensuring identical state output.",
7
+ args: {
8
+ runId: tool.schema.string().describe("The pipeline runId to use as the random seed for replay"),
9
+ inputs: tool.schema
10
+ .array(tool.schema.string())
11
+ .describe("Array of raw JSON findings inputs to feed sequentially into the pipeline"),
12
+ },
13
+ async execute(args) {
14
+ const { advancePipeline } = await import("../review/pipeline");
15
+
16
+ let currentState: ReviewState = {
17
+ stage: 1,
18
+ scope: "replay-scope",
19
+ selectedAgentNames: ["logic-auditor", "security-auditor"],
20
+ accumulatedFindings: [],
21
+ startedAt: "2026-04-05T00:00:00.000Z",
22
+ };
23
+
24
+ for (const input of args.inputs) {
25
+ const res = advancePipeline(input, currentState, undefined, args.runId, args.runId);
26
+ if (res.state) {
27
+ currentState = res.state;
28
+ }
29
+ }
30
+
31
+ return JSON.stringify(
32
+ {
33
+ success: true,
34
+ message: `Replayed ${args.inputs.length} inputs deterministically.`,
35
+ replayedRunId: args.runId,
36
+ finalState: currentState,
37
+ },
38
+ null,
39
+ 2,
40
+ );
41
+ },
42
+ });
@@ -22,6 +22,8 @@ import {
22
22
  loadActiveReviewStateFromKernel,
23
23
  saveActiveReviewStateToKernel,
24
24
  } from "../kernel/repository";
25
+ import { getLogger } from "../logging/domains";
26
+ import { loadState as loadPipelineState } from "../orchestrator/state";
25
27
  import { REVIEW_AGENTS, SPECIALIZED_AGENTS } from "../review/agents/index";
26
28
  import {
27
29
  createEmptyMemory,
@@ -133,7 +135,7 @@ async function syncLegacyReviewStateMirror(state: ReviewState, artifactDir: stri
133
135
  } catch (error: unknown) {
134
136
  if (!legacyReviewStateMirrorWarned) {
135
137
  legacyReviewStateMirrorWarned = true;
136
- console.warn("[opencode-autopilot] current-review.json mirror write failed:", error);
138
+ getLogger("tool", "review").warn("current-review.json mirror write failed", { error });
137
139
  }
138
140
  }
139
141
  }
@@ -190,7 +192,11 @@ async function startNewReview(
190
192
 
191
193
  // Select agents from all candidates (universal + specialized)
192
194
  const allCandidates = [...REVIEW_AGENTS, ...SPECIALIZED_AGENTS];
193
- const selection = selectAgents(detectedStacks, diffAnalysis, allCandidates);
195
+ const artifactDir = getProjectArtifactDir(projectRoot);
196
+ const pipelineState = await loadPipelineState(artifactDir);
197
+ const seed = pipelineState ? `${pipelineState.runId}-review-1` : undefined;
198
+
199
+ const selection = selectAgents(detectedStacks, diffAnalysis, allCandidates, { seed });
194
200
 
195
201
  const selectedNames = selection.selected.map((a) => a.name);
196
202
 
@@ -0,0 +1,43 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { z } from "zod";
3
+ import { readLatestSessionLog, readSessionLog } from "../observability/log-reader";
4
+ import { generateSessionSummary } from "../observability/summary-generator";
5
+
6
+ export async function summaryCore(sessionID?: string, logsDir?: string): Promise<string> {
7
+ const logsRoot = logsDir ?? process.cwd();
8
+ const log = sessionID
9
+ ? await readSessionLog(sessionID, logsRoot)
10
+ : await readLatestSessionLog(logsRoot);
11
+
12
+ if (!log) {
13
+ const target = sessionID ? `Session "${sessionID}" not found.` : "No session logs found.";
14
+ return JSON.stringify({
15
+ action: "error",
16
+ message: target,
17
+ });
18
+ }
19
+
20
+ const summary = generateSessionSummary(log);
21
+
22
+ return JSON.stringify({
23
+ action: "session_summary",
24
+ sessionId: log.sessionId,
25
+ summary,
26
+ displayText: summary,
27
+ });
28
+ }
29
+
30
+ export const ocSummary = tool({
31
+ description:
32
+ "Generate a markdown summary for the latest session or a specific session ID. Use this to review session outcomes, decisions, and errors.",
33
+ args: {
34
+ sessionID: z
35
+ .string()
36
+ .regex(/^[a-zA-Z0-9_-]{1,256}$/)
37
+ .optional()
38
+ .describe("Session ID to summarize (uses latest if omitted)"),
39
+ },
40
+ async execute({ sessionID }) {
41
+ return summaryCore(sessionID);
42
+ },
43
+ });
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+
3
+ export const TaskStatusSchema = z.enum(["pending", "running", "completed", "failed", "cancelled"]);
4
+ export type TaskStatus = z.infer<typeof TaskStatusSchema>;
5
+
6
+ export const TaskResultSchema = z.object({
7
+ taskId: z.string().min(1),
8
+ status: TaskStatusSchema,
9
+ output: z.string().optional(),
10
+ error: z.string().optional(),
11
+ startedAt: z.number().optional(),
12
+ completedAt: z.number().optional(),
13
+ durationMs: z.number().optional(),
14
+ });
15
+ export type TaskResult = z.infer<typeof TaskResultSchema>;
16
+
17
+ export const BackgroundTaskSchema = z.object({
18
+ id: z.string().min(1),
19
+ name: z.string().min(1),
20
+ status: TaskStatusSchema,
21
+ agentId: z.string().optional(),
22
+ priority: z.number().int().min(0).max(100).default(50),
23
+ createdAt: z.number(),
24
+ result: TaskResultSchema.optional(),
25
+ metadata: z.record(z.string(), z.unknown()).default({}),
26
+ });
27
+ export type BackgroundTask = z.infer<typeof BackgroundTaskSchema>;
28
+
29
+ export const AgentSlotSchema = z.object({
30
+ slotId: z.string().min(1),
31
+ agentId: z.string().min(1),
32
+ capacity: z.number().int().min(1).max(100).default(1),
33
+ activeTaskCount: z.number().int().min(0).default(0),
34
+ reserved: z.boolean().default(false),
35
+ });
36
+ export type AgentSlot = z.infer<typeof AgentSlotSchema>;
37
+
38
+ export const ConcurrencyLimitsSchema = z.object({
39
+ global: z.number().int().min(1).max(50).default(5),
40
+ perAgent: z.number().int().min(1).max(10).default(2),
41
+ perCategory: z.record(z.string(), z.number().int().min(1).max(20)).default({}),
42
+ });
43
+ export type ConcurrencyLimits = z.infer<typeof ConcurrencyLimitsSchema>;
44
+
45
+ export const backgroundConfigSchema = z.object({
46
+ enabled: z.boolean().default(false),
47
+ maxConcurrent: z.number().int().min(1).max(50).default(5),
48
+ persistence: z.boolean().default(true),
49
+ });
50
+ export type BackgroundConfig = z.infer<typeof backgroundConfigSchema>;
51
+ export const backgroundDefaults = backgroundConfigSchema.parse({});
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+
3
+ export const McpSkillSchema = z.object({
4
+ name: z.string().min(1),
5
+ description: z.string().optional(),
6
+ enabled: z.boolean().default(true),
7
+ version: z.string().optional(),
8
+ config: z.record(z.string(), z.unknown()).default({}),
9
+ });
10
+ export type McpSkill = z.infer<typeof McpSkillSchema>;
11
+
12
+ export const McpServerSchema = z.object({
13
+ id: z.string().min(1),
14
+ url: z.string().url().optional(),
15
+ transport: z.enum(["stdio", "http", "sse"]).default("stdio"),
16
+ enabled: z.boolean().default(true),
17
+ skills: z.array(z.string()).default([]),
18
+ metadata: z.record(z.string(), z.unknown()).default({}),
19
+ });
20
+ export type McpServer = z.infer<typeof McpServerSchema>;
21
+
22
+ export const mcpConfigSchema = z.object({
23
+ enabled: z.boolean().default(false),
24
+ skills: z.record(z.string(), z.unknown()).default({}),
25
+ });
26
+ export type McpConfig = z.infer<typeof mcpConfigSchema>;
27
+ export const mcpDefaults = mcpConfigSchema.parse({});
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export const ErrorCategorySchema = z.enum([
4
+ "rate_limit",
5
+ "auth_failure",
6
+ "quota_exceeded",
7
+ "service_unavailable",
8
+ "timeout",
9
+ "network",
10
+ "validation",
11
+ "unknown",
12
+ ]);
13
+ export type ErrorCategory = z.infer<typeof ErrorCategorySchema>;
14
+
15
+ export const RecoveryStrategySchema = z.enum([
16
+ "retry",
17
+ "fallback_model",
18
+ "skip",
19
+ "abort",
20
+ "user_prompt",
21
+ ]);
22
+ export type RecoveryStrategy = z.infer<typeof RecoveryStrategySchema>;
23
+
24
+ export const RecoveryActionSchema = z.object({
25
+ strategy: RecoveryStrategySchema,
26
+ errorCategory: ErrorCategorySchema,
27
+ maxAttempts: z.number().int().min(1).max(10).default(3),
28
+ backoffMs: z.number().int().min(0).default(1000),
29
+ fallbackAgentId: z.string().optional(),
30
+ metadata: z.record(z.string(), z.unknown()).default({}),
31
+ });
32
+ export type RecoveryAction = z.infer<typeof RecoveryActionSchema>;
33
+
34
+ export const recoveryConfigSchema = z.object({
35
+ enabled: z.boolean().default(true),
36
+ maxRetries: z.number().int().min(0).max(10).default(3),
37
+ });
38
+ export type RecoveryConfig = z.infer<typeof recoveryConfigSchema>;
39
+ export const recoveryDefaults = recoveryConfigSchema.parse({});
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export const CategorySchema = z.enum([
4
+ "quick",
5
+ "visual-engineering",
6
+ "ultrabrain",
7
+ "artistry",
8
+ "writing",
9
+ "unspecified-high",
10
+ "unspecified-low",
11
+ ]);
12
+ export type Category = z.infer<typeof CategorySchema>;
13
+
14
+ export const CategoryConfigSchema = z.object({
15
+ enabled: z.boolean().default(true),
16
+ agentId: z.string().optional(),
17
+ modelGroup: z.string().optional(),
18
+ maxTokenBudget: z.number().int().min(1000).optional(),
19
+ timeoutSeconds: z.number().int().min(1).max(3600).optional(),
20
+ skills: z.array(z.string()).default([]),
21
+ metadata: z.record(z.string(), z.unknown()).default({}),
22
+ });
23
+ export type CategoryConfig = z.infer<typeof CategoryConfigSchema>;
24
+
25
+ export const RoutingDecisionSchema = z.object({
26
+ category: CategorySchema,
27
+ confidence: z.number().min(0).max(1),
28
+ agentId: z.string().optional(),
29
+ reasoning: z.string().optional(),
30
+ appliedConfig: CategoryConfigSchema.optional(),
31
+ });
32
+ export type RoutingDecision = z.infer<typeof RoutingDecisionSchema>;
33
+
34
+ export const routingConfigSchema = z.object({
35
+ enabled: z.boolean().default(false),
36
+ categories: z.record(z.string(), CategoryConfigSchema).default({}),
37
+ });
38
+ export type RoutingConfig = z.infer<typeof routingConfigSchema>;
39
+ export const routingDefaults = routingConfigSchema.parse({});
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A simple seeded random number generator.
3
+ * Uses the Mulberry32 algorithm which provides decent quality and is very fast.
4
+ */
5
+ export function createSeededRandom(seedString: string) {
6
+ // Simple hash function (djb2) to convert string to 32-bit integer seed
7
+ let seed = 5381;
8
+ for (let i = 0; i < seedString.length; i++) {
9
+ seed = (seed * 33) ^ seedString.charCodeAt(i);
10
+ }
11
+ // Add an arbitrary constant to avoid passing 0 to mulberry32
12
+ let a = seed + 1831565813;
13
+
14
+ // Mulberry32 generator
15
+ return function random(): number {
16
+ a += 0x6d2b79f5;
17
+ let t = a;
18
+ t = Math.imul(t ^ (t >>> 15), t | 1);
19
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
20
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Shuffles an array in-place deterministically using the provided seeded RNG.
26
+ */
27
+ export function deterministicShuffle<T>(array: T[], rng: () => number): T[] {
28
+ for (let i = array.length - 1; i > 0; i--) {
29
+ const j = Math.floor(rng() * (i + 1));
30
+ [array[i], array[j]] = [array[j], array[i]];
31
+ }
32
+ return array;
33
+ }
@@ -0,0 +1,56 @@
1
+ import type { SessionEvents } from "../observability/event-store";
2
+ import type { PipelineState } from "../orchestrator/types";
3
+
4
+ export function generateSessionSummary(
5
+ sessionData: SessionEvents | undefined,
6
+ pipelineState: PipelineState | null,
7
+ ): string {
8
+ const sections: string[] = ["## Session Summary"];
9
+
10
+ if (pipelineState) {
11
+ const phaseInfo = pipelineState.currentPhase
12
+ ? ` (Current Phase: ${pipelineState.currentPhase})`
13
+ : "";
14
+ sections.push(`**Pipeline Status**: ${pipelineState.status}${phaseInfo}`);
15
+
16
+ const done = pipelineState.phases.filter((p) => p.status === "DONE").map((p) => p.name);
17
+
18
+ sections.push(`**Phases Completed**: ${done.length > 0 ? done.join(", ") : "None"}`);
19
+ } else {
20
+ sections.push("**Pipeline Status**: Unknown");
21
+ }
22
+
23
+ if (sessionData?.tokens) {
24
+ const { inputTokens, outputTokens, reasoningTokens } = sessionData.tokens;
25
+ sections.push(
26
+ "\n**Context Used**:",
27
+ `- Input: ${inputTokens.toLocaleString()} tokens`,
28
+ `- Output: ${outputTokens.toLocaleString()} tokens`,
29
+ ...(reasoningTokens > 0 ? [`- Reasoning: ${reasoningTokens.toLocaleString()} tokens`] : []),
30
+ );
31
+ }
32
+
33
+ const errors = (sessionData?.events ?? []).filter((e) => e.type === "error");
34
+ if (errors.length > 0) {
35
+ sections.push("\n**Errors Encountered**:");
36
+ for (const e of errors) {
37
+ if (e.type === "error") {
38
+ sections.push(`- ${e.errorType.toUpperCase()}: ${e.message}`);
39
+ }
40
+ }
41
+ }
42
+
43
+ if (sessionData) {
44
+ const endEvent = sessionData.events.find((e) => e.type === "session_end");
45
+ if (endEvent && endEvent.type === "session_end") {
46
+ const seconds = (endEvent.durationMs / 1000).toFixed(1);
47
+ sections.push(`\n**Duration**: ${seconds}s`);
48
+ } else {
49
+ const start = new Date(sessionData.startedAt).getTime();
50
+ const seconds = ((Date.now() - start) / 1000).toFixed(1);
51
+ sections.push(`\n**Duration (active)**: ${seconds}s`);
52
+ }
53
+ }
54
+
55
+ return sections.join("\n").trim();
56
+ }