@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.
- package/assets/commands/oc-brainstorm.md +2 -0
- package/assets/commands/oc-new-agent.md +2 -0
- package/assets/commands/oc-new-command.md +2 -0
- package/assets/commands/oc-new-skill.md +2 -0
- package/assets/commands/oc-quick.md +2 -0
- package/assets/commands/oc-refactor.md +26 -0
- package/assets/commands/oc-review-agents.md +2 -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 +2 -0
- package/assets/commands/oc-tdd.md +2 -0
- package/assets/commands/oc-update-docs.md +2 -0
- package/assets/commands/oc-write-plan.md +2 -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 +3 -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/autopilot.ts +4 -0
- package/src/agents/coder.ts +265 -0
- package/src/agents/db-specialist.ts +295 -0
- package/src/agents/debugger.ts +4 -0
- package/src/agents/devops.ts +352 -0
- package/src/agents/frontend-engineer.ts +541 -0
- package/src/agents/index.ts +31 -0
- package/src/agents/pipeline/oc-implementer.ts +4 -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/index.ts +2 -0
- 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 +57 -16
- 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 +2 -0
- package/src/orchestrator/skill-injection.ts +11 -6
- package/src/orchestrator/state.ts +2 -1
- package/src/orchestrator/wave-assigner.ts +117 -0
- 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/hashline-edit.ts +317 -0
- package/src/tools/orchestrate.ts +141 -18
- 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
|
-
//
|
|
40
|
-
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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 ??
|
|
258
|
+
buildProgress.currentTask ?? effectiveTasks.find((t) => t.status === "IN_PROGRESS")?.id ?? null;
|
|
218
259
|
if (result && !buildProgress.reviewPending && taskToComplete !== null) {
|
|
219
|
-
const updatedTasks = markTaskDone(
|
|
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(
|
|
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(
|
|
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
|
|
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 ${
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
const
|
|
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 ${
|
|
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]) =>
|
|
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,
|
|
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
|
-
|
|
29
|
+
`Write output to ${shipDir}/.`,
|
|
29
30
|
].join(" ");
|
|
30
31
|
|
|
31
32
|
return Object.freeze({
|
|
@@ -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.${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
} catch {
|
|
86
|
-
// Best-effort: all errors return empty string.
|
|
87
|
-
|
|
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.${
|
|
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
|
}
|