@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.
- package/assets/commands/oc-brainstorm.md +1 -0
- package/assets/commands/oc-new-agent.md +1 -0
- package/assets/commands/oc-new-command.md +1 -0
- package/assets/commands/oc-new-skill.md +1 -0
- package/assets/commands/oc-quick.md +1 -0
- package/assets/commands/oc-refactor.md +26 -0
- package/assets/commands/oc-review-agents.md +1 -0
- package/assets/commands/oc-review-pr.md +1 -0
- package/assets/commands/oc-security-audit.md +20 -0
- package/assets/commands/oc-stocktake.md +1 -0
- package/assets/commands/oc-tdd.md +1 -0
- package/assets/commands/oc-update-docs.md +1 -0
- package/assets/commands/oc-write-plan.md +1 -0
- package/assets/skills/api-design/SKILL.md +391 -0
- package/assets/skills/brainstorming/SKILL.md +1 -0
- package/assets/skills/code-review/SKILL.md +1 -0
- package/assets/skills/coding-standards/SKILL.md +1 -0
- package/assets/skills/csharp-patterns/SKILL.md +1 -0
- package/assets/skills/database-patterns/SKILL.md +270 -0
- package/assets/skills/docker-deployment/SKILL.md +326 -0
- package/assets/skills/e2e-testing/SKILL.md +1 -0
- package/assets/skills/frontend-design/SKILL.md +1 -0
- package/assets/skills/git-worktrees/SKILL.md +1 -0
- package/assets/skills/go-patterns/SKILL.md +1 -0
- package/assets/skills/java-patterns/SKILL.md +1 -0
- package/assets/skills/plan-executing/SKILL.md +1 -0
- package/assets/skills/plan-writing/SKILL.md +1 -0
- package/assets/skills/python-patterns/SKILL.md +1 -0
- package/assets/skills/rust-patterns/SKILL.md +1 -0
- package/assets/skills/security-patterns/SKILL.md +312 -0
- package/assets/skills/strategic-compaction/SKILL.md +1 -0
- package/assets/skills/systematic-debugging/SKILL.md +1 -0
- package/assets/skills/tdd-workflow/SKILL.md +1 -0
- package/assets/skills/typescript-patterns/SKILL.md +1 -0
- package/assets/skills/verification/SKILL.md +1 -0
- package/package.json +1 -1
- package/src/agents/db-specialist.ts +295 -0
- package/src/agents/devops.ts +352 -0
- package/src/agents/frontend-engineer.ts +541 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/security-auditor.ts +348 -0
- package/src/hooks/anti-slop.ts +40 -1
- package/src/hooks/slop-patterns.ts +24 -4
- package/src/installer.ts +29 -2
- package/src/memory/capture.ts +9 -4
- package/src/memory/decay.ts +11 -0
- package/src/memory/retrieval.ts +31 -2
- package/src/orchestrator/artifacts.ts +7 -2
- package/src/orchestrator/confidence.ts +3 -2
- package/src/orchestrator/handlers/architect.ts +11 -8
- package/src/orchestrator/handlers/build.ts +12 -10
- package/src/orchestrator/handlers/challenge.ts +9 -3
- package/src/orchestrator/handlers/plan.ts +115 -9
- package/src/orchestrator/handlers/recon.ts +9 -4
- package/src/orchestrator/handlers/retrospective.ts +3 -1
- package/src/orchestrator/handlers/ship.ts +8 -7
- package/src/orchestrator/handlers/types.ts +1 -0
- package/src/orchestrator/lesson-memory.ts +2 -1
- package/src/orchestrator/orchestration-logger.ts +40 -0
- package/src/orchestrator/phase.ts +14 -0
- package/src/orchestrator/schemas.ts +1 -0
- package/src/orchestrator/skill-injection.ts +11 -6
- package/src/orchestrator/state.ts +2 -1
- package/src/review/selection.ts +4 -32
- package/src/skills/adaptive-injector.ts +96 -5
- package/src/skills/loader.ts +4 -1
- package/src/tools/orchestrate.ts +141 -18
- package/src/tools/review.ts +2 -1
|
@@ -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
|
});
|
|
@@ -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,
|
|
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
|
|
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",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|
|
@@ -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
|
}
|
package/src/review/selection.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Deterministic agent selection for the review pipeline.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
}
|