@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
@@ -36,8 +36,9 @@ export function summarizeConfidence(entries: readonly ConfidenceEntry[]): {
36
36
 
37
37
  const total = entries.length;
38
38
 
39
- // Tie-break: prefer higher confidence (HIGH > MEDIUM > LOW)
40
- let dominant: ConfidenceLevel = "MEDIUM"; // default for empty
39
+ // Default: no evidence of low confidence → assume HIGH (single-proposal fast path).
40
+ // This prevents empty ledgers from triggering expensive multi-proposal arena (depth=2).
41
+ let dominant: ConfidenceLevel = "HIGH";
41
42
  if (total > 0) {
42
43
  let maxCount = 0;
43
44
  for (const level of LEVEL_PRIORITY) {
@@ -27,11 +27,14 @@ export async function handleArchitect(
27
27
  artifactDir: string,
28
28
  _result?: string,
29
29
  ): Promise<DispatchResult> {
30
+ // _result is received from the orchestrator but completion is determined by
31
+ // artifact existence (design.md/critique.md), not by result truthiness.
32
+ // This preserves the three-step arena flow: proposals → critic → complete.
30
33
  const phaseDir = getPhaseDir(artifactDir, "ARCHITECT");
31
34
  const critiqueExists = await fileExists(join(phaseDir, "critique.md"));
32
35
  const designExists = await fileExists(join(phaseDir, "design.md"));
33
36
 
34
- // Step 3: critique or design exists -> complete
37
+ // Step 3: critique or design exists -> complete (idempotency on resume)
35
38
  if (critiqueExists || designExists) {
36
39
  return Object.freeze({
37
40
  action: "complete" as const,
@@ -49,9 +52,9 @@ export async function handleArchitect(
49
52
  action: "dispatch" as const,
50
53
  agent: AGENT_NAMES.CRITIC,
51
54
  prompt: [
52
- `Review architecture proposals in phases/ARCHITECT/proposals/`,
53
- `Read ${getArtifactRef("RECON", "report.md")} and ${getArtifactRef("CHALLENGE", "brief.md")} for context.`,
54
- `Write comparative critique to ${getArtifactRef("ARCHITECT", "critique.md")}`,
55
+ `Review architecture proposals in ${proposalsDir}/`,
56
+ `Read ${getArtifactRef(artifactDir, "RECON", "report.md")} and ${getArtifactRef(artifactDir, "CHALLENGE", "brief.md")} for context.`,
57
+ `Write comparative critique to ${getArtifactRef(artifactDir, "ARCHITECT", "critique.md")}`,
55
58
  `Include: strengths, weaknesses, recommendation, confidence (HIGH/MEDIUM/LOW).`,
56
59
  ].join("\n"),
57
60
  phase: "ARCHITECT",
@@ -63,8 +66,8 @@ export async function handleArchitect(
63
66
  await ensurePhaseDir(artifactDir, "ARCHITECT");
64
67
  const reconEntries = filterByPhase(state.confidence, "RECON");
65
68
  const depth = getMemoryTunedDepth(reconEntries, join(artifactDir, ".."));
66
- const reconRef = getArtifactRef("RECON", "report.md");
67
- const challengeRef = getArtifactRef("CHALLENGE", "brief.md");
69
+ const reconRef = getArtifactRef(artifactDir, "RECON", "report.md");
70
+ const challengeRef = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
68
71
  const safeIdea = sanitizeTemplateContent(state.idea).replace(/[\r\n]+/g, " ");
69
72
 
70
73
  if (depth === 1) {
@@ -74,7 +77,7 @@ export async function handleArchitect(
74
77
  prompt: [
75
78
  `Read ${reconRef} and ${challengeRef} for context.`,
76
79
  `Design architecture for: ${safeIdea}`,
77
- `Write design to ${getArtifactRef("ARCHITECT", "design.md")}`,
80
+ `Write design to ${getArtifactRef(artifactDir, "ARCHITECT", "design.md")}`,
78
81
  `Include: component diagram, data flow, technology choices, confidence (HIGH/MEDIUM/LOW).`,
79
82
  ].join("\n"),
80
83
  phase: "ARCHITECT",
@@ -90,7 +93,7 @@ export async function handleArchitect(
90
93
  `Read ${reconRef} and ${challengeRef} for context.`,
91
94
  `Design architecture for: ${safeIdea}`,
92
95
  `Constraint: ${constraint}`,
93
- `Write proposal to phases/ARCHITECT/proposals/proposal-${label}.md`,
96
+ `Write proposal to ${join(proposalsDir, `proposal-${label}.md`)}`,
94
97
  `Include: component diagram, data flow, technology choices, confidence (HIGH/MEDIUM/LOW).`,
95
98
  ].join("\n"),
96
99
  });
@@ -2,6 +2,7 @@ import { sanitizeTemplateContent } from "../../review/sanitize";
2
2
  import { getArtifactRef } from "../artifacts";
3
3
  import { groupByWave } from "../plan";
4
4
  import type { BuildProgress, Task } from "../types";
5
+ import { assignWaves } from "../wave-assigner";
5
6
  import type { DispatchResult, PhaseHandler } from "./types";
6
7
  import { AGENT_NAMES } from "./types";
7
8
 
@@ -54,9 +55,9 @@ function markTasksInProgress(tasks: readonly Task[], taskIds: readonly number[])
54
55
  /**
55
56
  * Build a prompt for a single task dispatch.
56
57
  */
57
- function buildTaskPrompt(task: Task): string {
58
- const planRef = getArtifactRef("PLAN", "tasks.md");
59
- const designRef = getArtifactRef("ARCHITECT", "design.md");
58
+ function buildTaskPrompt(task: Task, artifactDir: string): string {
59
+ const planRef = getArtifactRef(artifactDir, "PLAN", "tasks.md");
60
+ const designRef = getArtifactRef(artifactDir, "ARCHITECT", "design.md");
60
61
  return [
61
62
  `Implement task ${task.id}: ${task.title}.`,
62
63
  `Reference the plan at ${planRef}`,
@@ -109,7 +110,7 @@ function hasCriticalFindings(resultStr: string): boolean {
109
110
  }
110
111
  }
111
112
 
112
- export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) => {
113
+ export const handleBuild: PhaseHandler = async (state, artifactDir, result?) => {
113
114
  const { tasks, buildProgress } = state;
114
115
 
115
116
  // Edge case: no tasks
@@ -130,6 +131,46 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
130
131
  } satisfies DispatchResult);
131
132
  }
132
133
 
134
+ // Auto-assign waves from depends_on declarations (D-15)
135
+ let effectiveTasks = tasks;
136
+ const hasDependencies = tasks.some((t) => t.depends_on && t.depends_on.length > 0);
137
+ if (hasDependencies) {
138
+ const waveResult = assignWaves(
139
+ tasks.map((t) => ({ id: t.id, depends_on: t.depends_on ?? [] })),
140
+ );
141
+ if (waveResult.cycles.length > 0) {
142
+ const cycleSet = new Set(waveResult.cycles);
143
+ effectiveTasks = tasks.map((t) => {
144
+ if (cycleSet.has(t.id)) return { ...t, status: "BLOCKED" as const };
145
+ const assigned = waveResult.assignments.get(t.id);
146
+ return assigned !== undefined ? { ...t, wave: assigned } : t;
147
+ });
148
+ } else {
149
+ effectiveTasks = tasks.map((t) => {
150
+ const assigned = waveResult.assignments.get(t.id);
151
+ return assigned !== undefined ? { ...t, wave: assigned } : t;
152
+ });
153
+ }
154
+ }
155
+
156
+ // Check if all remaining tasks are BLOCKED (cycles or MAX_TASKS cap)
157
+ const nonDoneTasks = effectiveTasks.filter((t) => t.status !== "DONE" && t.status !== "SKIPPED");
158
+ if (nonDoneTasks.length > 0 && nonDoneTasks.every((t) => t.status === "BLOCKED")) {
159
+ const blockedIds = nonDoneTasks.map((t) => t.id).join(", ");
160
+ return Object.freeze({
161
+ action: "error" as const,
162
+ phase: "BUILD",
163
+ message: `All remaining tasks are BLOCKED due to dependency cycles: [${blockedIds}]`,
164
+ progress: `All remaining tasks are BLOCKED due to dependency cycles: [${blockedIds}]`,
165
+ _stateUpdates: Object.freeze({
166
+ buildProgress: Object.freeze({
167
+ ...buildProgress,
168
+ }),
169
+ tasks: effectiveTasks,
170
+ }),
171
+ });
172
+ }
173
+
133
174
  // Case 1: Review pending + result provided -> process review outcome
134
175
  if (buildProgress.reviewPending && result) {
135
176
  if (hasCriticalFindings(result)) {
@@ -138,7 +179,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
138
179
  const prompt = [
139
180
  `CRITICAL review findings detected. Fix the following issues:`,
140
181
  safeResult,
141
- `Reference ${getArtifactRef("PLAN", "tasks.md")} for context.`,
182
+ `Reference ${getArtifactRef(artifactDir, "PLAN", "tasks.md")} for context.`,
142
183
  ].join(" ");
143
184
 
144
185
  return Object.freeze({
@@ -157,7 +198,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
157
198
  }
158
199
 
159
200
  // No critical -> advance to next wave
160
- const waveMap = groupByWave(tasks);
201
+ const waveMap = groupByWave(effectiveTasks);
161
202
  const nextWave = findCurrentWave(waveMap);
162
203
 
163
204
  if (nextWave === null) {
@@ -186,7 +227,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
186
227
  return Object.freeze({
187
228
  action: "dispatch",
188
229
  agent: AGENT_NAMES.BUILD,
189
- prompt: buildTaskPrompt(task),
230
+ prompt: buildTaskPrompt(task, artifactDir),
190
231
  phase: "BUILD",
191
232
  progress: `Wave ${nextWave} — task ${task.id}`,
192
233
  _stateUpdates: {
@@ -200,12 +241,12 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
200
241
  action: "dispatch_multi",
201
242
  agents: pendingTasks.map((task) => ({
202
243
  agent: AGENT_NAMES.BUILD,
203
- prompt: buildTaskPrompt(task),
244
+ prompt: buildTaskPrompt(task, artifactDir),
204
245
  })),
205
246
  phase: "BUILD",
206
247
  progress: `Wave ${nextWave} — ${pendingTasks.length} concurrent tasks`,
207
248
  _stateUpdates: {
208
- tasks: [...markTasksInProgress(tasks, dispatchedIds)],
249
+ tasks: [...markTasksInProgress(effectiveTasks, dispatchedIds)],
209
250
  buildProgress: { ...updatedProgress, currentTask: null },
210
251
  },
211
252
  } satisfies DispatchResult);
@@ -214,9 +255,9 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
214
255
  // Case 2: Result provided + not review pending -> mark task done
215
256
  // For dispatch_multi, currentTask may be null — find the first IN_PROGRESS task instead
216
257
  const taskToComplete =
217
- buildProgress.currentTask ?? tasks.find((t) => t.status === "IN_PROGRESS")?.id ?? null;
258
+ buildProgress.currentTask ?? effectiveTasks.find((t) => t.status === "IN_PROGRESS")?.id ?? null;
218
259
  if (result && !buildProgress.reviewPending && taskToComplete !== null) {
219
- const updatedTasks = markTaskDone(tasks, taskToComplete);
260
+ const updatedTasks = markTaskDone(effectiveTasks, taskToComplete);
220
261
  const waveMap = groupByWave(updatedTasks);
221
262
  const currentWave = buildProgress.currentWave ?? 1;
222
263
 
@@ -246,7 +287,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
246
287
  return Object.freeze({
247
288
  action: "dispatch",
248
289
  agent: AGENT_NAMES.BUILD,
249
- prompt: buildTaskPrompt(next),
290
+ prompt: buildTaskPrompt(next, artifactDir),
250
291
  phase: "BUILD",
251
292
  progress: `Wave ${currentWave} — task ${next.id}`,
252
293
  _stateUpdates: {
@@ -280,7 +321,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
280
321
  }
281
322
 
282
323
  // Case 3: No result (first call or resume) -> find first pending wave
283
- const waveMap = groupByWave(tasks);
324
+ const waveMap = groupByWave(effectiveTasks);
284
325
  const currentWave = findCurrentWave(waveMap);
285
326
 
286
327
  if (currentWave === null) {
@@ -328,7 +369,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
328
369
  return Object.freeze({
329
370
  action: "dispatch",
330
371
  agent: AGENT_NAMES.BUILD,
331
- prompt: buildTaskPrompt(task),
372
+ prompt: buildTaskPrompt(task, artifactDir),
332
373
  phase: "BUILD",
333
374
  progress: `Wave ${currentWave} — task ${task.id}`,
334
375
  _stateUpdates: {
@@ -347,12 +388,12 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
347
388
  action: "dispatch_multi",
348
389
  agents: pendingTasks.map((task) => ({
349
390
  agent: AGENT_NAMES.BUILD,
350
- prompt: buildTaskPrompt(task),
391
+ prompt: buildTaskPrompt(task, artifactDir),
351
392
  })),
352
393
  phase: "BUILD",
353
394
  progress: `Wave ${currentWave} — ${pendingTasks.length} concurrent tasks`,
354
395
  _stateUpdates: {
355
- tasks: [...markTasksInProgress(tasks, dispatchedIds)],
396
+ tasks: [...markTasksInProgress(effectiveTasks, dispatchedIds)],
356
397
  buildProgress: {
357
398
  ...buildProgress,
358
399
  currentTask: null,
@@ -1,4 +1,5 @@
1
1
  import { sanitizeTemplateContent } from "../../review/sanitize";
2
+ import { fileExists } from "../../utils/fs-helpers";
2
3
  import { ensurePhaseDir, getArtifactRef } from "../artifacts";
3
4
  import type { PipelineState } from "../types";
4
5
  import { AGENT_NAMES, type DispatchResult } from "./types";
@@ -13,6 +14,11 @@ export async function handleChallenge(
13
14
  result?: string,
14
15
  ): Promise<DispatchResult> {
15
16
  if (result) {
17
+ // Warn if artifact wasn't written (best-effort — still complete the phase)
18
+ const artifactPath = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
19
+ if (!(await fileExists(artifactPath))) {
20
+ console.warn("[opencode-autopilot] CHALLENGE completed but artifact not found");
21
+ }
16
22
  return Object.freeze({
17
23
  action: "complete" as const,
18
24
  phase: "CHALLENGE",
@@ -21,8 +27,8 @@ export async function handleChallenge(
21
27
  }
22
28
 
23
29
  await ensurePhaseDir(artifactDir, "CHALLENGE");
24
- const reconRef = getArtifactRef("RECON", "report.md");
25
- const outputRef = getArtifactRef("CHALLENGE", "brief.md");
30
+ const reconRef = getArtifactRef(artifactDir, "RECON", "report.md");
31
+ const outputPath = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
26
32
 
27
33
  const safeIdea = sanitizeTemplateContent(state.idea).replace(/[\r\n]+/g, " ");
28
34
 
@@ -32,7 +38,7 @@ export async function handleChallenge(
32
38
  prompt: [
33
39
  `Read ${reconRef} for research context.`,
34
40
  `Original idea: ${safeIdea}`,
35
- `Propose up to 3 enhancements. Write ambitious brief to ${outputRef}`,
41
+ `Propose up to 3 enhancements. Write ambitious brief to ${outputPath}`,
36
42
  `For each: name, user value, complexity (LOW/MEDIUM/HIGH), accept/reject rationale.`,
37
43
  ].join("\n"),
38
44
  phase: "CHALLENGE",
@@ -2,7 +2,7 @@ import { getArtifactRef } from "../artifacts";
2
2
  import type { DispatchResult, PhaseHandler } from "./types";
3
3
  import { AGENT_NAMES } from "./types";
4
4
 
5
- export const handlePlan: PhaseHandler = async (_state, _artifactDir, result?) => {
5
+ export const handlePlan: PhaseHandler = async (_state, artifactDir, result?) => {
6
6
  if (result) {
7
7
  return Object.freeze({
8
8
  action: "complete",
@@ -11,8 +11,9 @@ export const handlePlan: PhaseHandler = async (_state, _artifactDir, result?) =>
11
11
  } satisfies DispatchResult);
12
12
  }
13
13
 
14
- const architectRef = getArtifactRef("ARCHITECT", "design.md");
15
- const challengeRef = getArtifactRef("CHALLENGE", "brief.md");
14
+ const architectRef = getArtifactRef(artifactDir, "ARCHITECT", "design.md");
15
+ const challengeRef = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
16
+ const tasksPath = getArtifactRef(artifactDir, "PLAN", "tasks.md");
16
17
 
17
18
  const prompt = [
18
19
  "Read the architecture design at",
@@ -20,7 +21,7 @@ export const handlePlan: PhaseHandler = async (_state, _artifactDir, result?) =>
20
21
  "and the challenge brief at",
21
22
  challengeRef,
22
23
  "then produce a task plan.",
23
- "Write tasks to phases/PLAN/tasks.md.",
24
+ `Write tasks to ${tasksPath}.`,
24
25
  "Each task should have a 300-line diff max.",
25
26
  "Assign wave numbers for parallel execution.",
26
27
  ].join(" ");
@@ -1,4 +1,5 @@
1
1
  import { sanitizeTemplateContent } from "../../review/sanitize";
2
+ import { fileExists } from "../../utils/fs-helpers";
2
3
  import { ensurePhaseDir, getArtifactRef } from "../artifacts";
3
4
  import type { PipelineState } from "../types";
4
5
  import { AGENT_NAMES, type DispatchResult } from "./types";
@@ -13,6 +14,11 @@ export async function handleRecon(
13
14
  result?: string,
14
15
  ): Promise<DispatchResult> {
15
16
  if (result) {
17
+ // Warn if artifact wasn't written (best-effort — still complete the phase)
18
+ const artifactPath = getArtifactRef(artifactDir, "RECON", "report.md");
19
+ if (!(await fileExists(artifactPath))) {
20
+ console.warn("[opencode-autopilot] RECON completed but artifact not found");
21
+ }
16
22
  return Object.freeze({
17
23
  action: "complete" as const,
18
24
  phase: "RECON",
@@ -20,8 +26,8 @@ export async function handleRecon(
20
26
  });
21
27
  }
22
28
 
23
- const phaseDir = await ensurePhaseDir(artifactDir, "RECON");
24
- const outputRef = getArtifactRef("RECON", "report.md");
29
+ await ensurePhaseDir(artifactDir, "RECON");
30
+ const outputPath = getArtifactRef(artifactDir, "RECON", "report.md");
25
31
 
26
32
  const safeIdea = sanitizeTemplateContent(state.idea).replace(/[\r\n]+/g, " ");
27
33
 
@@ -29,9 +35,8 @@ export async function handleRecon(
29
35
  action: "dispatch" as const,
30
36
  agent: AGENT_NAMES.RECON,
31
37
  prompt: [
32
- `Research the following idea and write findings to ${outputRef}`,
38
+ `Research the following idea and write findings to ${outputPath}`,
33
39
  `Idea: ${safeIdea}`,
34
- `Output: ${phaseDir}/report.md`,
35
40
  `Include: Market Analysis, Technology Options, UX Considerations, Feasibility Assessment, Confidence (HIGH/MEDIUM/LOW)`,
36
41
  ].join("\n"),
37
42
  phase: "RECON",
@@ -105,7 +105,9 @@ export const handleRetrospective: PhaseHandler = async (_state, artifactDir, res
105
105
 
106
106
  const artifactRefs = Object.entries(PHASE_ARTIFACTS)
107
107
  .filter(([phase, files]) => files.length > 0 && phase !== "RETROSPECTIVE")
108
- .flatMap(([phase, files]) => files.map((file) => getArtifactRef(phase as Phase, file)));
108
+ .flatMap(([phase, files]) =>
109
+ files.map((file) => getArtifactRef(artifactDir, phase as Phase, file)),
110
+ );
109
111
 
110
112
  const prompt = [
111
113
  "Analyze all phase artifacts:",
@@ -1,8 +1,8 @@
1
- import { getArtifactRef } from "../artifacts";
1
+ import { getArtifactRef, getPhaseDir } from "../artifacts";
2
2
  import type { DispatchResult, PhaseHandler } from "./types";
3
3
  import { AGENT_NAMES } from "./types";
4
4
 
5
- export const handleShip: PhaseHandler = async (_state, _artifactDir, result?) => {
5
+ export const handleShip: PhaseHandler = async (_state, artifactDir, result?) => {
6
6
  if (result) {
7
7
  return Object.freeze({
8
8
  action: "complete",
@@ -11,10 +11,11 @@ export const handleShip: PhaseHandler = async (_state, _artifactDir, result?) =>
11
11
  } satisfies DispatchResult);
12
12
  }
13
13
 
14
- const reconRef = getArtifactRef("RECON", "report.md");
15
- const challengeRef = getArtifactRef("CHALLENGE", "brief.md");
16
- const architectRef = getArtifactRef("ARCHITECT", "design.md");
17
- const planRef = getArtifactRef("PLAN", "tasks.md");
14
+ const reconRef = getArtifactRef(artifactDir, "RECON", "report.md");
15
+ const challengeRef = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
16
+ const architectRef = getArtifactRef(artifactDir, "ARCHITECT", "design.md");
17
+ const planRef = getArtifactRef(artifactDir, "PLAN", "tasks.md");
18
+ const shipDir = getPhaseDir(artifactDir, "SHIP");
18
19
 
19
20
  const prompt = [
20
21
  "Review all prior phase artifacts:",
@@ -25,7 +26,7 @@ export const handleShip: PhaseHandler = async (_state, _artifactDir, result?) =>
25
26
  "Produce walkthrough.md (architecture overview),",
26
27
  "decisions.md (key decisions with rationale),",
27
28
  "changelog.md (user-facing changes).",
28
- "Write output to phases/SHIP/.",
29
+ `Write output to ${shipDir}/.`,
29
30
  ].join(" ");
30
31
 
31
32
  return Object.freeze({
@@ -22,6 +22,7 @@ export interface DispatchResult {
22
22
  readonly progress?: string;
23
23
  readonly message?: string;
24
24
  readonly _stateUpdates?: Partial<PipelineState>;
25
+ readonly _userProgress?: string;
25
26
  }
26
27
 
27
28
  export type PhaseHandler = (
@@ -9,6 +9,7 @@
9
9
  * to prevent corruption.
10
10
  */
11
11
 
12
+ import { randomBytes } from "node:crypto";
12
13
  import { readFile, rename, writeFile } from "node:fs/promises";
13
14
  import { join } from "node:path";
14
15
  import { ensureDir, isEnoentError } from "../utils/fs-helpers";
@@ -76,7 +77,7 @@ export async function saveLessonMemory(memory: LessonMemory, projectRoot: string
76
77
  const dir = join(projectRoot, ".opencode-autopilot");
77
78
  await ensureDir(dir);
78
79
  const memoryPath = join(dir, LESSON_FILE);
79
- const tmpPath = `${memoryPath}.tmp.${Date.now()}`;
80
+ const tmpPath = `${memoryPath}.tmp.${randomBytes(8).toString("hex")}`;
80
81
  await writeFile(tmpPath, JSON.stringify(validated, null, 2), "utf-8");
81
82
  await rename(tmpPath, memoryPath);
82
83
  }
@@ -0,0 +1,40 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export interface OrchestrationEvent {
5
+ readonly timestamp: string;
6
+ readonly phase: string;
7
+ readonly action: "dispatch" | "dispatch_multi" | "complete" | "error" | "loop_detected";
8
+ readonly agent?: string;
9
+ readonly promptLength?: number;
10
+ readonly attempt?: number;
11
+ readonly message?: string;
12
+ }
13
+
14
+ const LOG_FILE = "orchestration.jsonl";
15
+
16
+ /** Rate-limit: warn about log failures at most once per process. */
17
+ let logWriteWarned = false;
18
+
19
+ /**
20
+ * Append an orchestration event to the project-local JSONL log.
21
+ * Uses synchronous append to survive crashes. Best-effort — errors are swallowed.
22
+ */
23
+ export function logOrchestrationEvent(artifactDir: string, event: OrchestrationEvent): void {
24
+ try {
25
+ mkdirSync(artifactDir, { recursive: true });
26
+ const logPath = join(artifactDir, LOG_FILE);
27
+ // Redact filesystem paths from message to avoid leaking sensitive directory info
28
+ const safe = event.message
29
+ ? { ...event, message: event.message.replace(/[/\\][^\s"']+/g, "[PATH]") }
30
+ : event;
31
+ const line = `${JSON.stringify(safe)}\n`;
32
+ appendFileSync(logPath, line, "utf-8");
33
+ } catch (err) {
34
+ // Best-effort — never block the pipeline. Warn once so operators know logging is broken.
35
+ if (!logWriteWarned) {
36
+ logWriteWarned = true;
37
+ console.warn("[opencode-autopilot] orchestration log write failed:", err);
38
+ }
39
+ }
40
+ }
@@ -1,5 +1,19 @@
1
1
  import type { Phase, PhaseStatus, PipelineState } from "./types";
2
2
 
3
+ /** Maps each phase to its 1-based position for user-facing progress display. */
4
+ export const PHASE_INDEX: Readonly<Record<Phase, number>> = Object.freeze({
5
+ RECON: 1,
6
+ CHALLENGE: 2,
7
+ ARCHITECT: 3,
8
+ EXPLORE: 4,
9
+ PLAN: 5,
10
+ BUILD: 6,
11
+ SHIP: 7,
12
+ RETROSPECTIVE: 8,
13
+ } satisfies Record<Phase, number>);
14
+
15
+ export const TOTAL_PHASES = 8;
16
+
3
17
  /**
4
18
  * Maps each phase to its valid successor. RETROSPECTIVE is terminal (null).
5
19
  */
@@ -42,6 +42,7 @@ export const taskSchema = z.object({
42
42
  title: z.string().max(2048),
43
43
  status: z.enum(["PENDING", "IN_PROGRESS", "DONE", "FAILED", "SKIPPED", "BLOCKED"]),
44
44
  wave: z.number(),
45
+ depends_on: z.array(z.number()).default([]),
45
46
  attempt: z.number().default(0),
46
47
  strike: z.number().default(0),
47
48
  });
@@ -83,4 +84,5 @@ export const pipelineStateSchema = z.object({
83
84
  reviewPending: false,
84
85
  }),
85
86
  failureContext: failureContextSchema.nullable().default(null),
87
+ phaseDispatchCounts: z.record(z.string().max(32), z.number().int().min(0).max(1000)).default({}),
86
88
  });
@@ -11,9 +11,10 @@ import { readFile } from "node:fs/promises";
11
11
  import { join } from "node:path";
12
12
  import { sanitizeTemplateContent } from "../review/sanitize";
13
13
  import {
14
- buildMultiSkillContext,
14
+ buildAdaptiveSkillContext,
15
15
  detectProjectStackTags,
16
16
  filterSkillsByStack,
17
+ type SkillMode,
17
18
  } from "../skills/adaptive-injector";
18
19
  import { loadAllSkills } from "../skills/loader";
19
20
  import { isEnoentError } from "../utils/fs-helpers";
@@ -69,7 +70,11 @@ export function buildSkillContext(skillContent: string): string {
69
70
  export async function loadAdaptiveSkillContext(
70
71
  baseDir: string,
71
72
  projectRoot: string,
72
- tokenBudget?: number,
73
+ options?: {
74
+ readonly phase?: string;
75
+ readonly budget?: number;
76
+ readonly mode?: SkillMode;
77
+ },
73
78
  ): Promise<string> {
74
79
  try {
75
80
  const skillsDir = join(baseDir, "skills");
@@ -81,10 +86,10 @@ export async function loadAdaptiveSkillContext(
81
86
  if (allSkills.size === 0) return "";
82
87
 
83
88
  const matchingSkills = filterSkillsByStack(allSkills, projectTags);
84
- return buildMultiSkillContext(matchingSkills, tokenBudget);
85
- } catch {
86
- // Best-effort: all errors return empty string. Caller (injectSkillContext)
87
- // logs the error — no need to re-throw since the call site is also best-effort.
89
+ return buildAdaptiveSkillContext(matchingSkills, options);
90
+ } catch (err) {
91
+ // Best-effort: all errors return empty string.
92
+ console.warn("[opencode-autopilot] adaptive skill load failed:", err);
88
93
  return "";
89
94
  }
90
95
  }
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { readFile, rename, writeFile } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { ensureDir, isEnoentError } from "../utils/fs-helpers";
@@ -45,7 +46,7 @@ export async function saveState(state: PipelineState, artifactDir: string): Prom
45
46
  const validated = pipelineStateSchema.parse(state);
46
47
  await ensureDir(artifactDir);
47
48
  const statePath = join(artifactDir, STATE_FILE);
48
- const tmpPath = `${statePath}.tmp.${Date.now()}`;
49
+ const tmpPath = `${statePath}.tmp.${randomBytes(8).toString("hex")}`;
49
50
  await writeFile(tmpPath, JSON.stringify(validated, null, 2), "utf-8");
50
51
  await rename(tmpPath, statePath);
51
52
  }