@kodrunhq/opencode-autopilot 1.12.1 → 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.
Files changed (75) hide show
  1. package/assets/commands/oc-brainstorm.md +2 -0
  2. package/assets/commands/oc-new-agent.md +2 -0
  3. package/assets/commands/oc-new-command.md +2 -0
  4. package/assets/commands/oc-new-skill.md +2 -0
  5. package/assets/commands/oc-quick.md +2 -0
  6. package/assets/commands/oc-refactor.md +26 -0
  7. package/assets/commands/oc-review-agents.md +2 -0
  8. package/assets/commands/oc-review-pr.md +1 -0
  9. package/assets/commands/oc-security-audit.md +20 -0
  10. package/assets/commands/oc-stocktake.md +2 -0
  11. package/assets/commands/oc-tdd.md +2 -0
  12. package/assets/commands/oc-update-docs.md +2 -0
  13. package/assets/commands/oc-write-plan.md +2 -0
  14. package/assets/skills/api-design/SKILL.md +391 -0
  15. package/assets/skills/brainstorming/SKILL.md +1 -0
  16. package/assets/skills/code-review/SKILL.md +1 -0
  17. package/assets/skills/coding-standards/SKILL.md +3 -0
  18. package/assets/skills/csharp-patterns/SKILL.md +1 -0
  19. package/assets/skills/database-patterns/SKILL.md +270 -0
  20. package/assets/skills/docker-deployment/SKILL.md +326 -0
  21. package/assets/skills/e2e-testing/SKILL.md +1 -0
  22. package/assets/skills/frontend-design/SKILL.md +1 -0
  23. package/assets/skills/git-worktrees/SKILL.md +1 -0
  24. package/assets/skills/go-patterns/SKILL.md +1 -0
  25. package/assets/skills/java-patterns/SKILL.md +1 -0
  26. package/assets/skills/plan-executing/SKILL.md +1 -0
  27. package/assets/skills/plan-writing/SKILL.md +1 -0
  28. package/assets/skills/python-patterns/SKILL.md +1 -0
  29. package/assets/skills/rust-patterns/SKILL.md +1 -0
  30. package/assets/skills/security-patterns/SKILL.md +312 -0
  31. package/assets/skills/strategic-compaction/SKILL.md +1 -0
  32. package/assets/skills/systematic-debugging/SKILL.md +1 -0
  33. package/assets/skills/tdd-workflow/SKILL.md +1 -0
  34. package/assets/skills/typescript-patterns/SKILL.md +1 -0
  35. package/assets/skills/verification/SKILL.md +1 -0
  36. package/package.json +1 -1
  37. package/src/agents/autopilot.ts +4 -0
  38. package/src/agents/coder.ts +265 -0
  39. package/src/agents/db-specialist.ts +295 -0
  40. package/src/agents/debugger.ts +4 -0
  41. package/src/agents/devops.ts +352 -0
  42. package/src/agents/frontend-engineer.ts +541 -0
  43. package/src/agents/index.ts +31 -0
  44. package/src/agents/pipeline/oc-implementer.ts +4 -0
  45. package/src/agents/security-auditor.ts +348 -0
  46. package/src/hooks/anti-slop.ts +40 -1
  47. package/src/hooks/slop-patterns.ts +24 -4
  48. package/src/index.ts +2 -0
  49. package/src/installer.ts +29 -2
  50. package/src/memory/capture.ts +9 -4
  51. package/src/memory/decay.ts +11 -0
  52. package/src/memory/retrieval.ts +31 -2
  53. package/src/orchestrator/artifacts.ts +7 -2
  54. package/src/orchestrator/confidence.ts +3 -2
  55. package/src/orchestrator/handlers/architect.ts +11 -8
  56. package/src/orchestrator/handlers/build.ts +57 -16
  57. package/src/orchestrator/handlers/challenge.ts +9 -3
  58. package/src/orchestrator/handlers/plan.ts +5 -4
  59. package/src/orchestrator/handlers/recon.ts +9 -4
  60. package/src/orchestrator/handlers/retrospective.ts +3 -1
  61. package/src/orchestrator/handlers/ship.ts +8 -7
  62. package/src/orchestrator/handlers/types.ts +1 -0
  63. package/src/orchestrator/lesson-memory.ts +2 -1
  64. package/src/orchestrator/orchestration-logger.ts +40 -0
  65. package/src/orchestrator/phase.ts +14 -0
  66. package/src/orchestrator/schemas.ts +2 -0
  67. package/src/orchestrator/skill-injection.ts +11 -6
  68. package/src/orchestrator/state.ts +2 -1
  69. package/src/orchestrator/wave-assigner.ts +117 -0
  70. package/src/review/selection.ts +4 -32
  71. package/src/skills/adaptive-injector.ts +96 -5
  72. package/src/skills/loader.ts +4 -1
  73. package/src/tools/hashline-edit.ts +317 -0
  74. package/src/tools/orchestrate.ts +141 -18
  75. package/src/tools/review.ts +2 -1
@@ -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 { completePhase, getNextPhase } from "../orchestrator/phase";
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<import("../orchestrator/types").PipelineState>,
27
+ state: Readonly<PipelineState>,
27
28
  handlerResult: DispatchResult,
28
29
  artifactDir: string,
29
- ): Promise<import("../orchestrator/types").PipelineState> {
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(prompt: string, projectRoot?: string): Promise<string> {
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<import("../orchestrator/types").PipelineState>,
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(enrichedPrompt, join(artifactDir, ".."));
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
- handlerResult.phase as string,
163
- artifactDir,
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({ ...handlerResult, agents: enrichedAgents });
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 8 phases. Idea: ${currentState.idea}`,
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
 
@@ -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.${Date.now()}`;
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
  }