@kodrunhq/opencode-autopilot 1.12.2 → 1.14.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/assets/commands/oc-brainstorm.md +1 -0
- package/assets/commands/oc-new-agent.md +1 -0
- package/assets/commands/oc-new-command.md +1 -0
- package/assets/commands/oc-new-skill.md +1 -0
- package/assets/commands/oc-quick.md +1 -0
- package/assets/commands/oc-refactor.md +26 -0
- package/assets/commands/oc-review-agents.md +1 -0
- package/assets/commands/oc-review-pr.md +1 -0
- package/assets/commands/oc-security-audit.md +20 -0
- package/assets/commands/oc-stocktake.md +1 -0
- package/assets/commands/oc-tdd.md +1 -0
- package/assets/commands/oc-update-docs.md +1 -0
- package/assets/commands/oc-write-plan.md +1 -0
- package/assets/skills/api-design/SKILL.md +391 -0
- package/assets/skills/brainstorming/SKILL.md +1 -0
- package/assets/skills/code-review/SKILL.md +1 -0
- package/assets/skills/coding-standards/SKILL.md +1 -0
- package/assets/skills/csharp-patterns/SKILL.md +1 -0
- package/assets/skills/database-patterns/SKILL.md +270 -0
- package/assets/skills/docker-deployment/SKILL.md +326 -0
- package/assets/skills/e2e-testing/SKILL.md +1 -0
- package/assets/skills/frontend-design/SKILL.md +1 -0
- package/assets/skills/git-worktrees/SKILL.md +1 -0
- package/assets/skills/go-patterns/SKILL.md +1 -0
- package/assets/skills/java-patterns/SKILL.md +1 -0
- package/assets/skills/plan-executing/SKILL.md +1 -0
- package/assets/skills/plan-writing/SKILL.md +1 -0
- package/assets/skills/python-patterns/SKILL.md +1 -0
- package/assets/skills/rust-patterns/SKILL.md +1 -0
- package/assets/skills/security-patterns/SKILL.md +312 -0
- package/assets/skills/strategic-compaction/SKILL.md +1 -0
- package/assets/skills/systematic-debugging/SKILL.md +1 -0
- package/assets/skills/tdd-workflow/SKILL.md +1 -0
- package/assets/skills/typescript-patterns/SKILL.md +1 -0
- package/assets/skills/verification/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/agents/db-specialist.ts +295 -0
- package/src/agents/devops.ts +352 -0
- package/src/agents/frontend-engineer.ts +541 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/security-auditor.ts +348 -0
- package/src/hooks/anti-slop.ts +40 -1
- package/src/hooks/slop-patterns.ts +24 -4
- package/src/installer.ts +29 -2
- package/src/memory/capture.ts +9 -4
- package/src/memory/decay.ts +11 -0
- package/src/memory/retrieval.ts +31 -2
- package/src/orchestrator/artifacts.ts +7 -2
- package/src/orchestrator/confidence.ts +3 -2
- package/src/orchestrator/handlers/architect.ts +11 -8
- package/src/orchestrator/handlers/build.ts +12 -10
- package/src/orchestrator/handlers/challenge.ts +9 -3
- package/src/orchestrator/handlers/plan.ts +5 -4
- package/src/orchestrator/handlers/recon.ts +9 -4
- package/src/orchestrator/handlers/retrospective.ts +3 -1
- package/src/orchestrator/handlers/ship.ts +8 -7
- package/src/orchestrator/handlers/types.ts +1 -0
- package/src/orchestrator/lesson-memory.ts +2 -1
- package/src/orchestrator/orchestration-logger.ts +40 -0
- package/src/orchestrator/phase.ts +14 -0
- package/src/orchestrator/schemas.ts +1 -0
- package/src/orchestrator/skill-injection.ts +11 -6
- package/src/orchestrator/state.ts +2 -1
- package/src/review/selection.ts +4 -32
- package/src/skills/adaptive-injector.ts +96 -5
- package/src/skills/loader.ts +4 -1
- package/src/tools/orchestrate.ts +141 -18
- package/src/tools/review.ts +2 -1
package/src/tools/orchestrate.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { PHASE_HANDLERS } from "../orchestrator/handlers/index";
|
|
|
4
4
|
import type { DispatchResult } from "../orchestrator/handlers/types";
|
|
5
5
|
import { buildLessonContext } from "../orchestrator/lesson-injection";
|
|
6
6
|
import { loadLessonMemory } from "../orchestrator/lesson-memory";
|
|
7
|
-
import {
|
|
7
|
+
import { logOrchestrationEvent } from "../orchestrator/orchestration-logger";
|
|
8
|
+
import { completePhase, getNextPhase, PHASE_INDEX, TOTAL_PHASES } from "../orchestrator/phase";
|
|
8
9
|
import { loadAdaptiveSkillContext } from "../orchestrator/skill-injection";
|
|
9
10
|
import { createInitialState, loadState, patchState, saveState } from "../orchestrator/state";
|
|
10
|
-
import type { Phase } from "../orchestrator/types";
|
|
11
|
+
import type { Phase, PipelineState } from "../orchestrator/types";
|
|
11
12
|
import { isEnoentError } from "../utils/fs-helpers";
|
|
12
13
|
import { ensureGitignore } from "../utils/gitignore";
|
|
13
14
|
import { getGlobalConfigDir, getProjectArtifactDir } from "../utils/paths";
|
|
@@ -23,10 +24,10 @@ interface OrchestrateArgs {
|
|
|
23
24
|
* Returns the updated state.
|
|
24
25
|
*/
|
|
25
26
|
async function applyStateUpdates(
|
|
26
|
-
state: Readonly<
|
|
27
|
+
state: Readonly<PipelineState>,
|
|
27
28
|
handlerResult: DispatchResult,
|
|
28
29
|
artifactDir: string,
|
|
29
|
-
): Promise<
|
|
30
|
+
): Promise<PipelineState> {
|
|
30
31
|
const updates = handlerResult._stateUpdates;
|
|
31
32
|
if (updates) {
|
|
32
33
|
const updated = patchState(state, updates);
|
|
@@ -99,10 +100,18 @@ async function injectLessonContext(
|
|
|
99
100
|
* Attempt to inject stack-filtered adaptive skill context into a dispatch prompt.
|
|
100
101
|
* Best-effort: failures are silently swallowed to avoid breaking dispatch.
|
|
101
102
|
*/
|
|
102
|
-
async function injectSkillContext(
|
|
103
|
+
async function injectSkillContext(
|
|
104
|
+
prompt: string,
|
|
105
|
+
projectRoot?: string,
|
|
106
|
+
phase?: string,
|
|
107
|
+
): Promise<string> {
|
|
103
108
|
try {
|
|
104
109
|
const baseDir = getGlobalConfigDir();
|
|
105
|
-
const ctx = await loadAdaptiveSkillContext(baseDir, projectRoot ?? process.cwd()
|
|
110
|
+
const ctx = await loadAdaptiveSkillContext(baseDir, projectRoot ?? process.cwd(), {
|
|
111
|
+
phase,
|
|
112
|
+
budget: 1500,
|
|
113
|
+
mode: "summary",
|
|
114
|
+
});
|
|
106
115
|
if (ctx) return prompt + ctx;
|
|
107
116
|
} catch (err) {
|
|
108
117
|
console.warn("[opencode-autopilot] skill injection failed:", err);
|
|
@@ -110,13 +119,63 @@ async function injectSkillContext(prompt: string, projectRoot?: string): Promise
|
|
|
110
119
|
return prompt;
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
/** Build a human-readable progress string for user-facing display. */
|
|
123
|
+
function buildUserProgress(phase: string, label?: string, attempt?: number): string {
|
|
124
|
+
const idx = PHASE_INDEX[phase as Phase] ?? 0;
|
|
125
|
+
const desc = label ?? "dispatching";
|
|
126
|
+
const att = attempt != null ? ` (attempt ${attempt})` : "";
|
|
127
|
+
return `Phase ${idx}/${TOTAL_PHASES}: ${phase} — ${desc}${att}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Per-phase dispatch limits. BUILD is high because of multi-task waves. */
|
|
131
|
+
const MAX_PHASE_DISPATCHES: Readonly<Record<string, number>> = Object.freeze({
|
|
132
|
+
RECON: 3,
|
|
133
|
+
CHALLENGE: 3,
|
|
134
|
+
ARCHITECT: 10,
|
|
135
|
+
EXPLORE: 3,
|
|
136
|
+
PLAN: 5,
|
|
137
|
+
BUILD: 100,
|
|
138
|
+
SHIP: 5,
|
|
139
|
+
RETROSPECTIVE: 3,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Circuit breaker: increment per-phase dispatch count and abort if limit exceeded.
|
|
144
|
+
* Returns `{ abortMsg, newCount }`. When `abortMsg` is non-null the caller must
|
|
145
|
+
* return it immediately. `newCount` is the authoritative post-increment value.
|
|
146
|
+
*/
|
|
147
|
+
async function checkCircuitBreaker(
|
|
148
|
+
currentState: Readonly<PipelineState>,
|
|
149
|
+
phase: string,
|
|
150
|
+
artifactDir: string,
|
|
151
|
+
): Promise<{ readonly abortMsg: string | null; readonly newCount: number }> {
|
|
152
|
+
const counts = { ...(currentState.phaseDispatchCounts ?? {}) };
|
|
153
|
+
counts[phase] = (counts[phase] ?? 0) + 1;
|
|
154
|
+
const newCount = counts[phase];
|
|
155
|
+
const limit = MAX_PHASE_DISPATCHES[phase] ?? 5;
|
|
156
|
+
if (newCount > limit) {
|
|
157
|
+
const msg = `Phase ${phase} exceeded max dispatches (${newCount}/${limit}) — possible infinite loop detected. Aborting.`;
|
|
158
|
+
logOrchestrationEvent(artifactDir, {
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
phase,
|
|
161
|
+
action: "loop_detected",
|
|
162
|
+
attempt: newCount,
|
|
163
|
+
message: msg,
|
|
164
|
+
});
|
|
165
|
+
return { abortMsg: JSON.stringify({ action: "error", message: msg }), newCount };
|
|
166
|
+
}
|
|
167
|
+
const withCounts = patchState(currentState, { phaseDispatchCounts: counts });
|
|
168
|
+
await saveState(withCounts, artifactDir);
|
|
169
|
+
return { abortMsg: null, newCount };
|
|
170
|
+
}
|
|
171
|
+
|
|
113
172
|
/**
|
|
114
173
|
* Process a handler's DispatchResult, handling complete/dispatch/dispatch_multi/error.
|
|
115
174
|
* On complete, advances the phase and invokes the next handler.
|
|
116
175
|
*/
|
|
117
176
|
async function processHandlerResult(
|
|
118
177
|
handlerResult: DispatchResult,
|
|
119
|
-
state: Readonly<
|
|
178
|
+
state: Readonly<PipelineState>,
|
|
120
179
|
artifactDir: string,
|
|
121
180
|
): Promise<string> {
|
|
122
181
|
// Apply state updates from handler if present
|
|
@@ -124,19 +183,50 @@ async function processHandlerResult(
|
|
|
124
183
|
|
|
125
184
|
switch (handlerResult.action) {
|
|
126
185
|
case "error":
|
|
186
|
+
logOrchestrationEvent(artifactDir, {
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
phase: handlerResult.phase ?? currentState.currentPhase ?? "UNKNOWN",
|
|
189
|
+
action: "error",
|
|
190
|
+
message: handlerResult.message?.slice(0, 500),
|
|
191
|
+
});
|
|
127
192
|
return JSON.stringify(handlerResult);
|
|
128
193
|
|
|
129
194
|
case "dispatch": {
|
|
195
|
+
// Circuit breaker
|
|
196
|
+
const phase = handlerResult.phase ?? currentState.currentPhase ?? "UNKNOWN";
|
|
197
|
+
const { abortMsg, newCount: attempt } = await checkCircuitBreaker(
|
|
198
|
+
currentState,
|
|
199
|
+
phase,
|
|
200
|
+
artifactDir,
|
|
201
|
+
);
|
|
202
|
+
if (abortMsg) return abortMsg;
|
|
203
|
+
|
|
204
|
+
// Log the dispatch event before any inline-review or context injection
|
|
205
|
+
const progress = buildUserProgress(phase, handlerResult.progress, attempt);
|
|
206
|
+
logOrchestrationEvent(artifactDir, {
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
phase,
|
|
209
|
+
action: "dispatch",
|
|
210
|
+
agent: handlerResult.agent,
|
|
211
|
+
promptLength: handlerResult.prompt?.length,
|
|
212
|
+
attempt,
|
|
213
|
+
});
|
|
214
|
+
|
|
130
215
|
// Check if this is a review dispatch that should be inlined
|
|
131
216
|
const { inlined, reviewResult } = await maybeInlineReview(handlerResult, artifactDir);
|
|
132
217
|
if (inlined && reviewResult) {
|
|
133
|
-
// Feed the review result back into the current phase handler
|
|
134
218
|
const reloadedState = await loadState(artifactDir);
|
|
135
219
|
if (reloadedState?.currentPhase) {
|
|
136
220
|
const handler = PHASE_HANDLERS[reloadedState.currentPhase];
|
|
137
221
|
const nextResult = await handler(reloadedState, artifactDir, reviewResult);
|
|
138
222
|
return processHandlerResult(nextResult, reloadedState, artifactDir);
|
|
139
223
|
}
|
|
224
|
+
// State unavailable or pipeline completed after inline review — return complete
|
|
225
|
+
return JSON.stringify({
|
|
226
|
+
action: "complete",
|
|
227
|
+
summary: "Inline review completed; no active phase.",
|
|
228
|
+
_userProgress: progress,
|
|
229
|
+
});
|
|
140
230
|
}
|
|
141
231
|
// Inject lesson + skill context into dispatch prompt (best-effort)
|
|
142
232
|
if (handlerResult.prompt && handlerResult.phase) {
|
|
@@ -145,34 +235,60 @@ async function processHandlerResult(
|
|
|
145
235
|
handlerResult.phase,
|
|
146
236
|
artifactDir,
|
|
147
237
|
);
|
|
148
|
-
const withSkills = await injectSkillContext(
|
|
238
|
+
const withSkills = await injectSkillContext(
|
|
239
|
+
enrichedPrompt,
|
|
240
|
+
join(artifactDir, ".."),
|
|
241
|
+
handlerResult.phase,
|
|
242
|
+
);
|
|
149
243
|
if (withSkills !== handlerResult.prompt) {
|
|
150
|
-
return JSON.stringify({ ...handlerResult, prompt: withSkills });
|
|
244
|
+
return JSON.stringify({ ...handlerResult, prompt: withSkills, _userProgress: progress });
|
|
151
245
|
}
|
|
152
246
|
}
|
|
153
|
-
return JSON.stringify(handlerResult);
|
|
247
|
+
return JSON.stringify({ ...handlerResult, _userProgress: progress });
|
|
154
248
|
}
|
|
155
249
|
|
|
156
250
|
case "dispatch_multi": {
|
|
251
|
+
// Circuit breaker
|
|
252
|
+
const phase = handlerResult.phase ?? currentState.currentPhase ?? "UNKNOWN";
|
|
253
|
+
const { abortMsg, newCount: attempt } = await checkCircuitBreaker(
|
|
254
|
+
currentState,
|
|
255
|
+
phase,
|
|
256
|
+
artifactDir,
|
|
257
|
+
);
|
|
258
|
+
if (abortMsg) return abortMsg;
|
|
259
|
+
|
|
260
|
+
const progress = buildUserProgress(phase, handlerResult.progress, attempt);
|
|
261
|
+
logOrchestrationEvent(artifactDir, {
|
|
262
|
+
timestamp: new Date().toISOString(),
|
|
263
|
+
phase,
|
|
264
|
+
action: "dispatch_multi",
|
|
265
|
+
agent: `${handlerResult.agents?.length ?? 0} agents`,
|
|
266
|
+
attempt,
|
|
267
|
+
});
|
|
268
|
+
|
|
157
269
|
// Inject lesson + skill context into each agent's prompt (best-effort)
|
|
158
270
|
// Load lesson and skill context once and reuse for all agents in the batch
|
|
159
271
|
if (handlerResult.agents && handlerResult.phase) {
|
|
160
|
-
const lessonSuffix = await injectLessonContext(
|
|
272
|
+
const lessonSuffix = await injectLessonContext("", handlerResult.phase, artifactDir);
|
|
273
|
+
const skillSuffix = await injectSkillContext(
|
|
161
274
|
"",
|
|
162
|
-
|
|
163
|
-
|
|
275
|
+
join(artifactDir, ".."),
|
|
276
|
+
handlerResult.phase,
|
|
164
277
|
);
|
|
165
|
-
const skillSuffix = await injectSkillContext("", join(artifactDir, ".."));
|
|
166
278
|
const combinedSuffix = lessonSuffix + (skillSuffix || "");
|
|
167
279
|
if (combinedSuffix) {
|
|
168
280
|
const enrichedAgents = handlerResult.agents.map((entry) => ({
|
|
169
281
|
...entry,
|
|
170
282
|
prompt: entry.prompt + combinedSuffix,
|
|
171
283
|
}));
|
|
172
|
-
return JSON.stringify({
|
|
284
|
+
return JSON.stringify({
|
|
285
|
+
...handlerResult,
|
|
286
|
+
agents: enrichedAgents,
|
|
287
|
+
_userProgress: progress,
|
|
288
|
+
});
|
|
173
289
|
}
|
|
174
290
|
}
|
|
175
|
-
return JSON.stringify(handlerResult);
|
|
291
|
+
return JSON.stringify({ ...handlerResult, _userProgress: progress });
|
|
176
292
|
}
|
|
177
293
|
|
|
178
294
|
case "complete": {
|
|
@@ -183,6 +299,11 @@ async function processHandlerResult(
|
|
|
183
299
|
});
|
|
184
300
|
}
|
|
185
301
|
|
|
302
|
+
logOrchestrationEvent(artifactDir, {
|
|
303
|
+
timestamp: new Date().toISOString(),
|
|
304
|
+
phase: currentState.currentPhase,
|
|
305
|
+
action: "complete",
|
|
306
|
+
});
|
|
186
307
|
const nextPhase = getNextPhase(currentState.currentPhase);
|
|
187
308
|
const advanced = completePhase(currentState);
|
|
188
309
|
await saveState(advanced, artifactDir);
|
|
@@ -191,9 +312,11 @@ async function processHandlerResult(
|
|
|
191
312
|
// Terminal phase completed
|
|
192
313
|
const finished = { ...advanced, status: "COMPLETED" as const };
|
|
193
314
|
await saveState(finished, artifactDir);
|
|
315
|
+
const idx = PHASE_INDEX[currentState.currentPhase] ?? TOTAL_PHASES;
|
|
194
316
|
return JSON.stringify({
|
|
195
317
|
action: "complete",
|
|
196
|
-
summary: `Pipeline completed all
|
|
318
|
+
summary: `Pipeline completed all ${TOTAL_PHASES} phases. Idea: ${currentState.idea}`,
|
|
319
|
+
_userProgress: `Completed ${currentState.currentPhase} (${idx}/${TOTAL_PHASES}), pipeline finished`,
|
|
197
320
|
});
|
|
198
321
|
}
|
|
199
322
|
|
package/src/tools/review.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { execFile } from "node:child_process";
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
15
16
|
import { readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
16
17
|
import { join } from "node:path";
|
|
17
18
|
import { promisify } from "node:util";
|
|
@@ -110,7 +111,7 @@ async function saveReviewState(state: ReviewState, artifactDir: string): Promise
|
|
|
110
111
|
// Validate before writing (bidirectional validation, same as orchestrator state)
|
|
111
112
|
const validated = reviewStateSchema.parse(state);
|
|
112
113
|
const statePath = join(artifactDir, STATE_FILE);
|
|
113
|
-
const tmpPath = `${statePath}.tmp.${
|
|
114
|
+
const tmpPath = `${statePath}.tmp.${randomBytes(8).toString("hex")}`;
|
|
114
115
|
await writeFile(tmpPath, JSON.stringify(validated, null, 2), "utf-8");
|
|
115
116
|
await rename(tmpPath, statePath);
|
|
116
117
|
}
|