@kodrunhq/opencode-autopilot 1.12.2 → 1.14.1

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 (68) hide show
  1. package/assets/commands/oc-brainstorm.md +1 -0
  2. package/assets/commands/oc-new-agent.md +1 -0
  3. package/assets/commands/oc-new-command.md +1 -0
  4. package/assets/commands/oc-new-skill.md +1 -0
  5. package/assets/commands/oc-quick.md +1 -0
  6. package/assets/commands/oc-refactor.md +26 -0
  7. package/assets/commands/oc-review-agents.md +1 -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 +1 -0
  11. package/assets/commands/oc-tdd.md +1 -0
  12. package/assets/commands/oc-update-docs.md +1 -0
  13. package/assets/commands/oc-write-plan.md +1 -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 +1 -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/db-specialist.ts +295 -0
  38. package/src/agents/devops.ts +352 -0
  39. package/src/agents/frontend-engineer.ts +541 -0
  40. package/src/agents/index.ts +12 -0
  41. package/src/agents/security-auditor.ts +348 -0
  42. package/src/hooks/anti-slop.ts +40 -1
  43. package/src/hooks/slop-patterns.ts +24 -4
  44. package/src/installer.ts +29 -2
  45. package/src/memory/capture.ts +9 -4
  46. package/src/memory/decay.ts +11 -0
  47. package/src/memory/retrieval.ts +31 -2
  48. package/src/orchestrator/artifacts.ts +7 -2
  49. package/src/orchestrator/confidence.ts +3 -2
  50. package/src/orchestrator/handlers/architect.ts +11 -8
  51. package/src/orchestrator/handlers/build.ts +12 -10
  52. package/src/orchestrator/handlers/challenge.ts +9 -3
  53. package/src/orchestrator/handlers/plan.ts +115 -9
  54. package/src/orchestrator/handlers/recon.ts +9 -4
  55. package/src/orchestrator/handlers/retrospective.ts +3 -1
  56. package/src/orchestrator/handlers/ship.ts +8 -7
  57. package/src/orchestrator/handlers/types.ts +1 -0
  58. package/src/orchestrator/lesson-memory.ts +2 -1
  59. package/src/orchestrator/orchestration-logger.ts +40 -0
  60. package/src/orchestrator/phase.ts +14 -0
  61. package/src/orchestrator/schemas.ts +1 -0
  62. package/src/orchestrator/skill-injection.ts +11 -6
  63. package/src/orchestrator/state.ts +2 -1
  64. package/src/review/selection.ts +4 -32
  65. package/src/skills/adaptive-injector.ts +96 -5
  66. package/src/skills/loader.ts +4 -1
  67. package/src/tools/orchestrate.ts +141 -18
  68. package/src/tools/review.ts +2 -1
@@ -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
  });
@@ -55,9 +55,9 @@ function markTasksInProgress(tasks: readonly Task[], taskIds: readonly number[])
55
55
  /**
56
56
  * Build a prompt for a single task dispatch.
57
57
  */
58
- function buildTaskPrompt(task: Task): string {
59
- const planRef = getArtifactRef("PLAN", "tasks.md");
60
- 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");
61
61
  return [
62
62
  `Implement task ${task.id}: ${task.title}.`,
63
63
  `Reference the plan at ${planRef}`,
@@ -110,7 +110,7 @@ function hasCriticalFindings(resultStr: string): boolean {
110
110
  }
111
111
  }
112
112
 
113
- export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) => {
113
+ export const handleBuild: PhaseHandler = async (state, artifactDir, result?) => {
114
114
  const { tasks, buildProgress } = state;
115
115
 
116
116
  // Edge case: no tasks
@@ -159,6 +159,8 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
159
159
  const blockedIds = nonDoneTasks.map((t) => t.id).join(", ");
160
160
  return Object.freeze({
161
161
  action: "error" as const,
162
+ phase: "BUILD",
163
+ message: `All remaining tasks are BLOCKED due to dependency cycles: [${blockedIds}]`,
162
164
  progress: `All remaining tasks are BLOCKED due to dependency cycles: [${blockedIds}]`,
163
165
  _stateUpdates: Object.freeze({
164
166
  buildProgress: Object.freeze({
@@ -177,7 +179,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
177
179
  const prompt = [
178
180
  `CRITICAL review findings detected. Fix the following issues:`,
179
181
  safeResult,
180
- `Reference ${getArtifactRef("PLAN", "tasks.md")} for context.`,
182
+ `Reference ${getArtifactRef(artifactDir, "PLAN", "tasks.md")} for context.`,
181
183
  ].join(" ");
182
184
 
183
185
  return Object.freeze({
@@ -225,7 +227,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
225
227
  return Object.freeze({
226
228
  action: "dispatch",
227
229
  agent: AGENT_NAMES.BUILD,
228
- prompt: buildTaskPrompt(task),
230
+ prompt: buildTaskPrompt(task, artifactDir),
229
231
  phase: "BUILD",
230
232
  progress: `Wave ${nextWave} — task ${task.id}`,
231
233
  _stateUpdates: {
@@ -239,7 +241,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
239
241
  action: "dispatch_multi",
240
242
  agents: pendingTasks.map((task) => ({
241
243
  agent: AGENT_NAMES.BUILD,
242
- prompt: buildTaskPrompt(task),
244
+ prompt: buildTaskPrompt(task, artifactDir),
243
245
  })),
244
246
  phase: "BUILD",
245
247
  progress: `Wave ${nextWave} — ${pendingTasks.length} concurrent tasks`,
@@ -285,7 +287,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
285
287
  return Object.freeze({
286
288
  action: "dispatch",
287
289
  agent: AGENT_NAMES.BUILD,
288
- prompt: buildTaskPrompt(next),
290
+ prompt: buildTaskPrompt(next, artifactDir),
289
291
  phase: "BUILD",
290
292
  progress: `Wave ${currentWave} — task ${next.id}`,
291
293
  _stateUpdates: {
@@ -367,7 +369,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
367
369
  return Object.freeze({
368
370
  action: "dispatch",
369
371
  agent: AGENT_NAMES.BUILD,
370
- prompt: buildTaskPrompt(task),
372
+ prompt: buildTaskPrompt(task, artifactDir),
371
373
  phase: "BUILD",
372
374
  progress: `Wave ${currentWave} — task ${task.id}`,
373
375
  _stateUpdates: {
@@ -386,7 +388,7 @@ export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) =>
386
388
  action: "dispatch_multi",
387
389
  agents: pendingTasks.map((task) => ({
388
390
  agent: AGENT_NAMES.BUILD,
389
- prompt: buildTaskPrompt(task),
391
+ prompt: buildTaskPrompt(task, artifactDir),
390
392
  })),
391
393
  phase: "BUILD",
392
394
  progress: `Wave ${currentWave} — ${pendingTasks.length} concurrent tasks`,
@@ -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",
@@ -1,18 +1,124 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isEnoentError } from "../../utils/fs-helpers";
1
3
  import { getArtifactRef } from "../artifacts";
4
+ import { taskSchema } from "../schemas";
5
+ import type { Task } from "../types";
2
6
  import type { DispatchResult, PhaseHandler } from "./types";
3
7
  import { AGENT_NAMES } from "./types";
4
8
 
5
- export const handlePlan: PhaseHandler = async (_state, _artifactDir, result?) => {
9
+ const EXPECTED_COLUMN_COUNT = 6;
10
+ const taskIdPattern = /^W(\d+)-T(\d+)$/i;
11
+ const separatorCellPattern = /^:?-{3,}:?$/;
12
+
13
+ function parseTableColumns(line: string): readonly string[] | null {
14
+ const trimmed = line.trim();
15
+ if (!trimmed.includes("|")) {
16
+ return null;
17
+ }
18
+
19
+ const withoutLeadingBoundary = trimmed.startsWith("|") ? trimmed.slice(1) : trimmed;
20
+ const normalized = withoutLeadingBoundary.endsWith("|")
21
+ ? withoutLeadingBoundary.slice(0, -1)
22
+ : withoutLeadingBoundary;
23
+
24
+ return normalized.split("|").map((col) => col.trim());
25
+ }
26
+
27
+ function isSeparatorRow(columns: readonly string[]): boolean {
28
+ return columns.length > 0 && columns.every((col) => separatorCellPattern.test(col));
29
+ }
30
+
31
+ /**
32
+ * Parse tasks from markdown table in tasks.md.
33
+ * Expected format: | Task ID | Title | Description | Files | Wave | Criteria |
34
+ * Returns array of Task objects.
35
+ */
36
+ async function loadTasksFromMarkdown(tasksPath: string): Promise<Task[]> {
37
+ const content = await readFile(tasksPath, "utf-8");
38
+ const lines = content.split("\n");
39
+
40
+ const tasks: Task[] = [];
41
+ for (const line of lines) {
42
+ const columns = parseTableColumns(line);
43
+ if (columns === null || columns.length < EXPECTED_COLUMN_COUNT || isSeparatorRow(columns)) {
44
+ continue;
45
+ }
46
+
47
+ if (columns[0].toLowerCase() === "task id") {
48
+ continue;
49
+ }
50
+
51
+ const idMatch = taskIdPattern.exec(columns[0]);
52
+ if (idMatch === null) {
53
+ continue;
54
+ }
55
+
56
+ const waveFromId = Number.parseInt(idMatch[1], 10);
57
+ const title = columns[1];
58
+ const waveFromColumn = Number.parseInt(columns[4], 10);
59
+
60
+ if (!title || Number.isNaN(waveFromId) || Number.isNaN(waveFromColumn)) {
61
+ continue;
62
+ }
63
+
64
+ if (waveFromId !== waveFromColumn) {
65
+ continue;
66
+ }
67
+
68
+ tasks.push(
69
+ taskSchema.parse({
70
+ id: tasks.length + 1,
71
+ title,
72
+ status: "PENDING",
73
+ wave: waveFromColumn,
74
+ depends_on: [],
75
+ attempt: 0,
76
+ strike: 0,
77
+ }),
78
+ );
79
+ }
80
+
81
+ if (tasks.length === 0) {
82
+ throw new Error("No valid task rows found in PLAN tasks.md");
83
+ }
84
+
85
+ return tasks;
86
+ }
87
+
88
+ export const handlePlan: PhaseHandler = async (_state, artifactDir, result?) => {
89
+ // When result is provided, the planner has completed writing tasks
90
+ // Load them from tasks.md and populate state.tasks
6
91
  if (result) {
7
- return Object.freeze({
8
- action: "complete",
9
- phase: "PLAN",
10
- progress: "Planning complete — tasks written",
11
- } satisfies DispatchResult);
92
+ const tasksPath = getArtifactRef(artifactDir, "PLAN", "tasks.md");
93
+ try {
94
+ const loadedTasks = await loadTasksFromMarkdown(tasksPath);
95
+ return Object.freeze({
96
+ action: "complete",
97
+ phase: "PLAN",
98
+ progress: `Planning complete — loaded ${loadedTasks.length} task(s)`,
99
+ _stateUpdates: {
100
+ tasks: loadedTasks,
101
+ },
102
+ } satisfies DispatchResult);
103
+ } catch (error: unknown) {
104
+ const reason = isEnoentError(error)
105
+ ? "tasks.md not found after planner completion"
106
+ : error instanceof Error
107
+ ? error.message
108
+ : "Unknown parsing error";
109
+
110
+ return Object.freeze({
111
+ action: "error",
112
+ phase: "PLAN",
113
+ message: `Failed to load PLAN tasks: ${reason}`,
114
+ progress: "Planning failed — task extraction error",
115
+ } satisfies DispatchResult);
116
+ }
12
117
  }
13
118
 
14
- const architectRef = getArtifactRef("ARCHITECT", "design.md");
15
- const challengeRef = getArtifactRef("CHALLENGE", "brief.md");
119
+ const architectRef = getArtifactRef(artifactDir, "ARCHITECT", "design.md");
120
+ const challengeRef = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
121
+ const tasksPath = getArtifactRef(artifactDir, "PLAN", "tasks.md");
16
122
 
17
123
  const prompt = [
18
124
  "Read the architecture design at",
@@ -20,7 +126,7 @@ export const handlePlan: PhaseHandler = async (_state, _artifactDir, result?) =>
20
126
  "and the challenge brief at",
21
127
  challengeRef,
22
128
  "then produce a task plan.",
23
- "Write tasks to phases/PLAN/tasks.md.",
129
+ `Write tasks to ${tasksPath}.`,
24
130
  "Each task should have a 300-line diff max.",
25
131
  "Assign wave numbers for parallel execution.",
26
132
  ].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
  */
@@ -84,4 +84,5 @@ export const pipelineStateSchema = z.object({
84
84
  reviewPending: false,
85
85
  }),
86
86
  failureContext: failureContextSchema.nullable().default(null),
87
+ phaseDispatchCounts: z.record(z.string().max(32), z.number().int().min(0).max(1000)).default({}),
87
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
  }
@@ -1,10 +1,8 @@
1
1
  /**
2
- * Two-pass deterministic agent selection for the review pipeline.
2
+ * Deterministic agent selection for the review pipeline.
3
3
  *
4
- * Pass 1: Stack gate -- agents with empty relevantStacks always pass;
5
- * agents with non-empty relevantStacks require at least one match.
6
- * Pass 2: Diff relevance scoring -- currently used for future ordering,
7
- * all stack-passing agents run regardless of score.
4
+ * Stack gate: agents with empty relevantStacks always pass;
5
+ * agents with non-empty relevantStacks require at least one match.
8
6
  */
9
7
 
10
8
  /** Minimal agent shape needed for selection (compatible with ReviewAgent from agents/). */
@@ -38,7 +36,7 @@ export interface SelectionResult {
38
36
  */
39
37
  export function selectAgents(
40
38
  detectedStacks: readonly string[],
41
- diffAnalysis: DiffAnalysisInput,
39
+ _diffAnalysis: DiffAnalysisInput,
42
40
  agents: readonly SelectableAgent[],
43
41
  ): SelectionResult {
44
42
  const stackSet = new Set(detectedStacks);
@@ -65,34 +63,8 @@ export function selectAgents(
65
63
  }
66
64
  }
67
65
 
68
- // Pass 2: Compute relevance scores (stored for future ordering, no filtering)
69
- // Scores are intentionally not used for filtering yet
70
- for (const agent of selected) {
71
- computeDiffRelevance(agent, diffAnalysis);
72
- }
73
-
74
66
  return Object.freeze({
75
67
  selected: Object.freeze(selected),
76
68
  excluded: Object.freeze(excluded),
77
69
  });
78
70
  }
79
-
80
- /**
81
- * Compute diff-based relevance score for an agent.
82
- * Base score of 1.0 with bonuses for specific agent-analysis matches.
83
- * Used for future prioritization/ordering, not for filtering.
84
- */
85
- export function computeDiffRelevance(agent: SelectableAgent, analysis: DiffAnalysisInput): number {
86
- let score = 1.0;
87
-
88
- if (agent.name === "security-auditor") {
89
- if (analysis.hasAuth) score += 0.5;
90
- if (analysis.hasConfig) score += 0.3;
91
- }
92
-
93
- if (agent.name === "test-interrogator") {
94
- if (!analysis.hasTests) score += 0.5;
95
- }
96
-
97
- return score;
98
- }