@kodrunhq/opencode-autopilot 0.1.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/assets/agents/placeholder-agent.md +13 -0
  4. package/assets/commands/configure.md +17 -0
  5. package/assets/commands/new-agent.md +16 -0
  6. package/assets/commands/new-command.md +15 -0
  7. package/assets/commands/new-skill.md +15 -0
  8. package/assets/commands/review-pr.md +49 -0
  9. package/assets/skills/.gitkeep +0 -0
  10. package/assets/skills/coding-standards/SKILL.md +327 -0
  11. package/package.json +52 -0
  12. package/src/agents/autopilot.ts +42 -0
  13. package/src/agents/documenter.ts +44 -0
  14. package/src/agents/index.ts +49 -0
  15. package/src/agents/metaprompter.ts +50 -0
  16. package/src/agents/pipeline/index.ts +25 -0
  17. package/src/agents/pipeline/oc-architect.ts +49 -0
  18. package/src/agents/pipeline/oc-challenger.ts +44 -0
  19. package/src/agents/pipeline/oc-critic.ts +42 -0
  20. package/src/agents/pipeline/oc-explorer.ts +46 -0
  21. package/src/agents/pipeline/oc-implementer.ts +56 -0
  22. package/src/agents/pipeline/oc-planner.ts +45 -0
  23. package/src/agents/pipeline/oc-researcher.ts +46 -0
  24. package/src/agents/pipeline/oc-retrospector.ts +42 -0
  25. package/src/agents/pipeline/oc-reviewer.ts +44 -0
  26. package/src/agents/pipeline/oc-shipper.ts +42 -0
  27. package/src/agents/pr-reviewer.ts +74 -0
  28. package/src/agents/researcher.ts +43 -0
  29. package/src/config.ts +168 -0
  30. package/src/index.ts +152 -0
  31. package/src/installer.ts +130 -0
  32. package/src/orchestrator/arena.ts +41 -0
  33. package/src/orchestrator/artifacts.ts +28 -0
  34. package/src/orchestrator/confidence.ts +59 -0
  35. package/src/orchestrator/fallback/chat-message-handler.ts +49 -0
  36. package/src/orchestrator/fallback/error-classifier.ts +148 -0
  37. package/src/orchestrator/fallback/event-handler.ts +235 -0
  38. package/src/orchestrator/fallback/fallback-config.ts +16 -0
  39. package/src/orchestrator/fallback/fallback-manager.ts +323 -0
  40. package/src/orchestrator/fallback/fallback-state.ts +120 -0
  41. package/src/orchestrator/fallback/index.ts +11 -0
  42. package/src/orchestrator/fallback/message-replay.ts +40 -0
  43. package/src/orchestrator/fallback/resolve-chain.ts +34 -0
  44. package/src/orchestrator/fallback/tool-execute-handler.ts +44 -0
  45. package/src/orchestrator/fallback/types.ts +46 -0
  46. package/src/orchestrator/handlers/architect.ts +114 -0
  47. package/src/orchestrator/handlers/build.ts +363 -0
  48. package/src/orchestrator/handlers/challenge.ts +41 -0
  49. package/src/orchestrator/handlers/explore.ts +9 -0
  50. package/src/orchestrator/handlers/index.ts +21 -0
  51. package/src/orchestrator/handlers/plan.ts +35 -0
  52. package/src/orchestrator/handlers/recon.ts +40 -0
  53. package/src/orchestrator/handlers/retrospective.ts +123 -0
  54. package/src/orchestrator/handlers/ship.ts +38 -0
  55. package/src/orchestrator/handlers/types.ts +31 -0
  56. package/src/orchestrator/lesson-injection.ts +80 -0
  57. package/src/orchestrator/lesson-memory.ts +110 -0
  58. package/src/orchestrator/lesson-schemas.ts +24 -0
  59. package/src/orchestrator/lesson-types.ts +6 -0
  60. package/src/orchestrator/phase.ts +76 -0
  61. package/src/orchestrator/plan.ts +43 -0
  62. package/src/orchestrator/schemas.ts +86 -0
  63. package/src/orchestrator/skill-injection.ts +52 -0
  64. package/src/orchestrator/state.ts +80 -0
  65. package/src/orchestrator/types.ts +20 -0
  66. package/src/review/agent-catalog.ts +439 -0
  67. package/src/review/agents/auth-flow-verifier.ts +47 -0
  68. package/src/review/agents/code-quality-auditor.ts +51 -0
  69. package/src/review/agents/concurrency-checker.ts +47 -0
  70. package/src/review/agents/contract-verifier.ts +45 -0
  71. package/src/review/agents/database-auditor.ts +47 -0
  72. package/src/review/agents/dead-code-scanner.ts +47 -0
  73. package/src/review/agents/go-idioms-auditor.ts +46 -0
  74. package/src/review/agents/index.ts +82 -0
  75. package/src/review/agents/logic-auditor.ts +47 -0
  76. package/src/review/agents/product-thinker.ts +49 -0
  77. package/src/review/agents/python-django-auditor.ts +46 -0
  78. package/src/review/agents/react-patterns-auditor.ts +46 -0
  79. package/src/review/agents/red-team.ts +49 -0
  80. package/src/review/agents/rust-safety-auditor.ts +46 -0
  81. package/src/review/agents/scope-intent-verifier.ts +45 -0
  82. package/src/review/agents/security-auditor.ts +47 -0
  83. package/src/review/agents/silent-failure-hunter.ts +45 -0
  84. package/src/review/agents/spec-checker.ts +45 -0
  85. package/src/review/agents/state-mgmt-auditor.ts +46 -0
  86. package/src/review/agents/test-interrogator.ts +43 -0
  87. package/src/review/agents/type-soundness.ts +46 -0
  88. package/src/review/agents/wiring-inspector.ts +46 -0
  89. package/src/review/cross-verification.ts +71 -0
  90. package/src/review/finding-builder.ts +74 -0
  91. package/src/review/fix-cycle.ts +146 -0
  92. package/src/review/memory.ts +114 -0
  93. package/src/review/pipeline.ts +258 -0
  94. package/src/review/report.ts +141 -0
  95. package/src/review/sanitize.ts +8 -0
  96. package/src/review/schemas.ts +75 -0
  97. package/src/review/selection.ts +98 -0
  98. package/src/review/severity.ts +71 -0
  99. package/src/review/stack-gate.ts +127 -0
  100. package/src/review/types.ts +43 -0
  101. package/src/templates/agent-template.ts +47 -0
  102. package/src/templates/command-template.ts +29 -0
  103. package/src/templates/skill-template.ts +42 -0
  104. package/src/tools/confidence.ts +93 -0
  105. package/src/tools/create-agent.ts +81 -0
  106. package/src/tools/create-command.ts +74 -0
  107. package/src/tools/create-skill.ts +74 -0
  108. package/src/tools/forensics.ts +88 -0
  109. package/src/tools/orchestrate.ts +310 -0
  110. package/src/tools/phase.ts +92 -0
  111. package/src/tools/placeholder.ts +11 -0
  112. package/src/tools/plan.ts +56 -0
  113. package/src/tools/review.ts +295 -0
  114. package/src/tools/state.ts +112 -0
  115. package/src/utils/fs-helpers.ts +39 -0
  116. package/src/utils/gitignore.ts +27 -0
  117. package/src/utils/paths.ts +17 -0
  118. package/src/utils/validators.ts +57 -0
@@ -0,0 +1,148 @@
1
+ import type { ErrorType } from "./types";
2
+
3
+ export const RETRYABLE_ERROR_PATTERNS: readonly RegExp[] = Object.freeze([
4
+ /rate.?limit/i,
5
+ /too.?many.?requests/i,
6
+ /quota.?exceeded/i,
7
+ /quota.?protection/i,
8
+ /key.?limit.?exceeded/i,
9
+ /usage\s+limit\s+has\s+been\s+reached/i,
10
+ /service.?unavailable/i,
11
+ /overloaded/i,
12
+ /temporarily.?unavailable/i,
13
+ /try.?again/i,
14
+ /credit.*balance.*too.*low/i,
15
+ /insufficient.?(?:credits?|funds?|balance)/i,
16
+ /(?:^|\s)429(?:\s|$)/,
17
+ /(?:^|\s)503(?:\s|$)/,
18
+ /(?:^|\s)529(?:\s|$)/,
19
+ ]);
20
+
21
+ /**
22
+ * Extracts a human-readable error message from an unknown error value.
23
+ * Handles nested error.error.message, error.message, error.error, and string errors.
24
+ */
25
+ export function getErrorMessage(error: unknown): string {
26
+ if (error === null || error === undefined) return "";
27
+ if (typeof error === "string") return error;
28
+
29
+ if (typeof error === "object") {
30
+ const obj = error as Record<string, unknown>;
31
+
32
+ // Check nested error.error.message first
33
+ if (obj.error && typeof obj.error === "object") {
34
+ const nested = obj.error as Record<string, unknown>;
35
+ if (typeof nested.message === "string") return nested.message;
36
+ }
37
+
38
+ // Check error.message
39
+ if (typeof obj.message === "string") return obj.message;
40
+
41
+ // Check error.error as string
42
+ if (typeof obj.error === "string") return obj.error;
43
+ }
44
+
45
+ return "";
46
+ }
47
+
48
+ /**
49
+ * Extracts a status code from an error if it matches the retryable set.
50
+ * Checks error.status, error.statusCode, and message text.
51
+ */
52
+ export function extractStatusCode(error: unknown, retryOnErrors: readonly number[]): number | null {
53
+ if (error !== null && typeof error === "object") {
54
+ const obj = error as Record<string, unknown>;
55
+
56
+ if (typeof obj.status === "number" && retryOnErrors.includes(obj.status)) {
57
+ return obj.status;
58
+ }
59
+ if (typeof obj.statusCode === "number" && retryOnErrors.includes(obj.statusCode)) {
60
+ return obj.statusCode;
61
+ }
62
+ }
63
+
64
+ // Extract from message text — restrict to 4xx/5xx to avoid false positives
65
+ const message = getErrorMessage(error);
66
+ const matches = message.matchAll(/\b([45]\d{2})\b/g);
67
+ for (const m of matches) {
68
+ const code = Number.parseInt(m[1], 10);
69
+ if (retryOnErrors.includes(code)) return code;
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Classifies an error into a known ErrorType category based on message patterns and status codes.
77
+ */
78
+ export function classifyErrorType(error: unknown): ErrorType {
79
+ const message = getErrorMessage(error);
80
+ const lowerMessage = message.toLowerCase();
81
+
82
+ // Check status code first
83
+ if (error !== null && typeof error === "object") {
84
+ const obj = error as Record<string, unknown>;
85
+ if (obj.status === 429 || obj.statusCode === 429) return "rate_limit";
86
+ }
87
+
88
+ // Check message patterns
89
+ if (
90
+ /api.?key/i.test(message) &&
91
+ /missing|no\s|not\s+(?:provided|found|set|configured)/i.test(message)
92
+ )
93
+ return "missing_api_key";
94
+ if (/model.*not.*(?:found|exist)/i.test(message)) return "model_not_found";
95
+ if (/content.?filter/i.test(message)) return "content_filter";
96
+ if (/context.?length/i.test(message)) return "context_length";
97
+ if (/rate.?limit/i.test(lowerMessage) || /too.?many.?requests/i.test(lowerMessage))
98
+ return "rate_limit";
99
+ if (
100
+ /quota.?exceeded/i.test(lowerMessage) ||
101
+ /insufficient.?(?:credits?|funds?|balance)/i.test(lowerMessage)
102
+ )
103
+ return "quota_exceeded";
104
+ if (/service.?unavailable/i.test(lowerMessage) || /overloaded/i.test(lowerMessage))
105
+ return "service_unavailable";
106
+
107
+ return "unknown";
108
+ }
109
+
110
+ /**
111
+ * Determines whether an error is retryable by another model.
112
+ * Checks status codes, built-in patterns, error type, and user-provided patterns.
113
+ */
114
+ export function isRetryableError(
115
+ error: unknown,
116
+ retryOnErrors: readonly number[],
117
+ userPatterns?: readonly string[],
118
+ ): boolean {
119
+ const errorType = classifyErrorType(error);
120
+
121
+ // content_filter: same content will fail on any model.
122
+ // context_length: replay would fail without truncation; caller must truncate first.
123
+ if (errorType === "content_filter" || errorType === "context_length") return false;
124
+
125
+ if (errorType === "missing_api_key" || errorType === "model_not_found") return true;
126
+
127
+ const statusCode = extractStatusCode(error, retryOnErrors);
128
+ if (statusCode !== null && retryOnErrors.includes(statusCode)) return true;
129
+
130
+ const message = getErrorMessage(error);
131
+ if (RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(message))) return true;
132
+
133
+ if (userPatterns) {
134
+ for (const patternStr of userPatterns) {
135
+ // ReDoS protection: reject patterns with nested quantifiers or backtracking risk
136
+ if (/(\+|\*|\{)\s*\)(\+|\*|\{|\?)/.test(patternStr)) continue;
137
+ if (/\(.*\|.*\)(\+|\*|\{)/.test(patternStr)) continue;
138
+ try {
139
+ const re = new RegExp(patternStr, "i");
140
+ if (re.test(message)) return true;
141
+ } catch {
142
+ /* Invalid regex -- skip */
143
+ }
144
+ }
145
+ }
146
+
147
+ return false;
148
+ }
@@ -0,0 +1,235 @@
1
+ import type { FallbackConfig } from "./fallback-config";
2
+ import type { FallbackManager } from "./fallback-manager";
3
+ import { replayWithDegradation } from "./message-replay";
4
+ import type { MessagePart } from "./types";
5
+
6
+ /**
7
+ * SDK operations interface for dependency injection.
8
+ * Enables testing without the OpenCode runtime.
9
+ */
10
+ export interface SdkOperations {
11
+ readonly abortSession: (sessionID: string) => Promise<void>;
12
+ readonly getSessionMessages: (sessionID: string) => Promise<readonly MessagePart[]>;
13
+ readonly promptAsync: (
14
+ sessionID: string,
15
+ model: { readonly providerID: string; readonly modelID: string },
16
+ parts: readonly MessagePart[],
17
+ ) => Promise<void>;
18
+ readonly showToast: (
19
+ title: string,
20
+ message: string,
21
+ variant: "info" | "warning" | "error",
22
+ ) => Promise<void>;
23
+ }
24
+
25
+ export interface EventHandlerDeps {
26
+ readonly manager: FallbackManager;
27
+ readonly sdk: SdkOperations;
28
+ readonly config: FallbackConfig;
29
+ }
30
+
31
+ /**
32
+ * Parses a "provider/model" string into { providerID, modelID }.
33
+ * Splits on the first "/" only. Returns null if no "/" found.
34
+ */
35
+ export function parseModelString(
36
+ model: string,
37
+ ): { readonly providerID: string; readonly modelID: string } | null {
38
+ const slashIndex = model.indexOf("/");
39
+ if (slashIndex <= 0) return null;
40
+ return {
41
+ providerID: model.slice(0, slashIndex),
42
+ modelID: model.slice(slashIndex + 1),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Extracts a session ID from event properties.
48
+ * Supports both `properties.sessionID` and `properties.info.sessionID`.
49
+ */
50
+ function extractSessionID(properties: Record<string, unknown>): string | undefined {
51
+ if (typeof properties.sessionID === "string") return properties.sessionID;
52
+ if (
53
+ properties.info !== null &&
54
+ typeof properties.info === "object" &&
55
+ typeof (properties.info as Record<string, unknown>).sessionID === "string"
56
+ ) {
57
+ return (properties.info as Record<string, unknown>).sessionID as string;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ /**
63
+ * Factory that creates an event handler function bound to fallback dependencies.
64
+ * The returned handler routes OpenCode events to the FallbackManager.
65
+ */
66
+ export function createEventHandler(deps: EventHandlerDeps) {
67
+ const { manager, sdk, config } = deps;
68
+
69
+ return async (input: {
70
+ readonly event: { readonly type: string; readonly [key: string]: unknown };
71
+ }): Promise<void> => {
72
+ const { event } = input;
73
+ const properties = (event.properties ?? {}) as Record<string, unknown>;
74
+
75
+ switch (event.type) {
76
+ case "session.created": {
77
+ const info = properties.info as
78
+ | { id?: string; model?: string; parentID?: string | null; agent?: string }
79
+ | undefined;
80
+ if (!info?.id) return;
81
+
82
+ const model = typeof info.model === "string" ? info.model : "";
83
+ const parentID = info.parentID !== undefined ? info.parentID : undefined;
84
+ const agentName = typeof info.agent === "string" ? info.agent : undefined;
85
+ manager.initSession(info.id, model, parentID, agentName);
86
+
87
+ // Start TTFT timeout only when fallback is enabled and timeout configured.
88
+ // Without a fallback chain, a TTFT abort would just fail the session.
89
+ if (model && config.enabled && config.timeoutSeconds > 0) {
90
+ manager.startTtftTimeout(info.id, () => {
91
+ // Guard: skip if session was cleaned up before timer fires
92
+ if (!manager.getSessionState(info.id as string)) return;
93
+ // On TTFT timeout, abort session to trigger fallback via session.error
94
+ sdk.abortSession(info.id as string).catch(() => {
95
+ // Best-effort abort; session.error will handle the result
96
+ });
97
+ });
98
+ }
99
+ return;
100
+ }
101
+
102
+ case "session.deleted": {
103
+ const info = properties.info as { id?: string } | undefined;
104
+ if (info?.id) {
105
+ manager.cleanupSession(info.id);
106
+ }
107
+ return;
108
+ }
109
+
110
+ case "session.compacted": {
111
+ const sessionID = extractSessionID(properties);
112
+ if (sessionID) {
113
+ manager.clearCompactionInFlight(sessionID);
114
+ }
115
+ return;
116
+ }
117
+
118
+ case "message.part.delta":
119
+ case "session.diff": {
120
+ const sessionID = extractSessionID(properties);
121
+ if (sessionID) {
122
+ manager.recordFirstToken(sessionID);
123
+ manager.clearAwaitingResult(sessionID);
124
+ }
125
+ return;
126
+ }
127
+
128
+ case "session.error": {
129
+ const sessionID =
130
+ typeof properties.sessionID === "string" ? properties.sessionID : undefined;
131
+ if (!sessionID) return;
132
+
133
+ const error = properties.error;
134
+ const modelStr = typeof properties.model === "string" ? properties.model : undefined;
135
+
136
+ await handleFallbackError(manager, sdk, config, sessionID, error, modelStr);
137
+ return;
138
+ }
139
+
140
+ case "message.updated": {
141
+ const info = properties.info as
142
+ | { sessionID?: string; error?: unknown; model?: string }
143
+ | undefined;
144
+ if (!info?.sessionID || !info.error) return;
145
+
146
+ const modelStr = typeof info.model === "string" ? info.model : undefined;
147
+ await handleFallbackError(manager, sdk, config, info.sessionID, info.error, modelStr);
148
+ return;
149
+ }
150
+
151
+ default:
152
+ // Unknown event type -- ignore
153
+ return;
154
+ }
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Shared fallback error handling logic used by both session.error and message.updated.
160
+ */
161
+ async function handleFallbackError(
162
+ manager: FallbackManager,
163
+ sdk: SdkOperations,
164
+ config: FallbackConfig,
165
+ sessionID: string,
166
+ error: unknown,
167
+ modelStr?: string,
168
+ ): Promise<void> {
169
+ // All guards (self-abort, stale, retryable, lock) are inside manager.handleError
170
+ const plan = manager.handleError(sessionID, error, modelStr);
171
+ if (!plan) return;
172
+
173
+ try {
174
+ // Record self-abort before aborting (Pitfall 2)
175
+ manager.recordSelfAbort(sessionID);
176
+
177
+ // Abort current request
178
+ await sdk.abortSession(sessionID);
179
+
180
+ // Session may have been cleaned up during await — verify before continuing
181
+ if (!manager.getSessionState(sessionID)) {
182
+ manager.releaseRetryLock(sessionID);
183
+ return;
184
+ }
185
+
186
+ // Get messages for replay
187
+ const messages = await sdk.getSessionMessages(sessionID);
188
+
189
+ // Session existence check after second await
190
+ if (!manager.getSessionState(sessionID)) {
191
+ manager.releaseRetryLock(sessionID);
192
+ return;
193
+ }
194
+
195
+ // Get current state for attempt-based degradation
196
+ const state = manager.getSessionState(sessionID);
197
+ const attemptCount = state?.attemptCount ?? 0;
198
+ const { parts: replayedParts } = replayWithDegradation(messages, attemptCount);
199
+
200
+ // Commit fallback state — abort dispatch if commit fails (stale plan)
201
+ const committed = manager.commitAndUpdateState(sessionID, plan);
202
+ if (!committed) {
203
+ manager.releaseRetryLock(sessionID);
204
+ return;
205
+ }
206
+
207
+ // Parse the new model for the SDK call
208
+ const parsedModel = parseModelString(plan.newModel);
209
+ if (parsedModel) {
210
+ // Notify user if enabled
211
+ if (config.notifyOnFallback) {
212
+ await sdk
213
+ .showToast(
214
+ "Model Fallback",
215
+ `Switching from ${plan.failedModel} to ${plan.newModel}: ${plan.reason}`,
216
+ "warning",
217
+ )
218
+ .catch(() => {
219
+ // Best-effort notification
220
+ });
221
+ }
222
+
223
+ // Dispatch replay with fallback model
224
+ await sdk.promptAsync(sessionID, parsedModel, replayedParts);
225
+ // Mark awaiting result inside dispatch block — only when prompt was sent
226
+ manager.markAwaitingResult(sessionID);
227
+ }
228
+
229
+ // Release lock after dispatch (or skip if model unparseable)
230
+ manager.releaseRetryLock(sessionID);
231
+ } catch {
232
+ // On failure, release the lock to allow future retries
233
+ manager.releaseRetryLock(sessionID);
234
+ }
235
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+
3
+ export const fallbackConfigSchema = z.object({
4
+ enabled: z.boolean().default(true),
5
+ retryOnErrors: z.array(z.number()).default([401, 402, 429, 500, 502, 503, 504]),
6
+ retryableErrorPatterns: z.array(z.string().max(256)).max(50).default([]),
7
+ maxFallbackAttempts: z.number().min(1).max(100).default(10),
8
+ cooldownSeconds: z.number().min(1).max(3600).default(60),
9
+ timeoutSeconds: z.number().min(0).max(300).default(30),
10
+ notifyOnFallback: z.boolean().default(true),
11
+ });
12
+
13
+ export type FallbackConfig = z.infer<typeof fallbackConfigSchema>;
14
+
15
+ // Pre-compute defaults for Zod v4 nested default compatibility
16
+ export const fallbackDefaults = fallbackConfigSchema.parse({});