@kodrunhq/opencode-autopilot 1.14.0 → 1.15.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/package.json +1 -1
- package/src/orchestrator/artifacts.ts +1 -1
- package/src/orchestrator/contracts/invariants.ts +121 -0
- package/src/orchestrator/contracts/legacy-result-adapter.ts +47 -0
- package/src/orchestrator/contracts/phase-artifacts.ts +90 -0
- package/src/orchestrator/contracts/result-envelope.ts +23 -0
- package/src/orchestrator/handlers/architect.ts +5 -1
- package/src/orchestrator/handlers/build.ts +110 -18
- package/src/orchestrator/handlers/challenge.ts +3 -1
- package/src/orchestrator/handlers/explore.ts +1 -0
- package/src/orchestrator/handlers/plan.ts +189 -7
- package/src/orchestrator/handlers/recon.ts +3 -1
- package/src/orchestrator/handlers/retrospective.ts +8 -0
- package/src/orchestrator/handlers/ship.ts +6 -1
- package/src/orchestrator/handlers/types.ts +21 -2
- package/src/orchestrator/renderers/tasks-markdown.ts +22 -0
- package/src/orchestrator/replay.ts +14 -0
- package/src/orchestrator/schemas.ts +19 -0
- package/src/orchestrator/state.ts +48 -7
- package/src/orchestrator/types.ts +4 -0
- package/src/review/pipeline.ts +41 -6
- package/src/review/schemas.ts +6 -0
- package/src/review/types.ts +2 -0
- package/src/tools/doctor.ts +34 -0
- package/src/tools/forensics.ts +34 -0
- package/src/tools/orchestrate.ts +418 -54
- package/src/tools/quick.ts +4 -0
- package/src/tools/review.ts +27 -2
- package/src/types/inquirer-shims.d.ts +42 -0
|
@@ -1,19 +1,200 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { isEnoentError } from "../../utils/fs-helpers";
|
|
1
3
|
import { getArtifactRef } from "../artifacts";
|
|
4
|
+
import { normalizePlanTasks, planTasksArtifactSchema } from "../contracts/phase-artifacts";
|
|
5
|
+
import { logOrchestrationEvent } from "../orchestration-logger";
|
|
6
|
+
import { renderTasksMarkdown } from "../renderers/tasks-markdown";
|
|
7
|
+
import { taskSchema } from "../schemas";
|
|
8
|
+
import type { Task } from "../types";
|
|
2
9
|
import type { DispatchResult, PhaseHandler } from "./types";
|
|
3
10
|
import { AGENT_NAMES } from "./types";
|
|
4
11
|
|
|
12
|
+
const EXPECTED_COLUMN_COUNT = 6;
|
|
13
|
+
const taskIdPattern = /^W(\d+)-T(\d+)$/i;
|
|
14
|
+
const separatorCellPattern = /^:?-{3,}:?$/;
|
|
15
|
+
|
|
16
|
+
function parseTableColumns(line: string): readonly string[] | null {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed.includes("|")) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const withoutLeadingBoundary = trimmed.startsWith("|") ? trimmed.slice(1) : trimmed;
|
|
23
|
+
const normalized = withoutLeadingBoundary.endsWith("|")
|
|
24
|
+
? withoutLeadingBoundary.slice(0, -1)
|
|
25
|
+
: withoutLeadingBoundary;
|
|
26
|
+
|
|
27
|
+
return normalized.split("|").map((col) => col.trim());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isSeparatorRow(columns: readonly string[]): boolean {
|
|
31
|
+
return columns.length > 0 && columns.every((col) => separatorCellPattern.test(col));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse tasks from markdown table in tasks.md.
|
|
36
|
+
* Legacy fallback only -- canonical source is tasks.json.
|
|
37
|
+
*/
|
|
38
|
+
async function loadTasksFromMarkdown(tasksPath: string): Promise<Task[]> {
|
|
39
|
+
const content = await readFile(tasksPath, "utf-8");
|
|
40
|
+
const lines = content.split("\n");
|
|
41
|
+
|
|
42
|
+
const tasks: Task[] = [];
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const columns = parseTableColumns(line);
|
|
45
|
+
if (columns === null || columns.length < EXPECTED_COLUMN_COUNT || isSeparatorRow(columns)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (columns[0].toLowerCase() === "task id") {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const idMatch = taskIdPattern.exec(columns[0]);
|
|
54
|
+
if (idMatch === null) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const waveFromId = Number.parseInt(idMatch[1], 10);
|
|
59
|
+
const title = columns[1];
|
|
60
|
+
const waveFromColumn = Number.parseInt(columns[4], 10);
|
|
61
|
+
|
|
62
|
+
if (!title || Number.isNaN(waveFromId) || Number.isNaN(waveFromColumn)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (waveFromId !== waveFromColumn) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
tasks.push(
|
|
71
|
+
taskSchema.parse({
|
|
72
|
+
id: tasks.length + 1,
|
|
73
|
+
title,
|
|
74
|
+
status: "PENDING",
|
|
75
|
+
wave: waveFromColumn,
|
|
76
|
+
depends_on: [],
|
|
77
|
+
attempt: 0,
|
|
78
|
+
strike: 0,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (tasks.length === 0) {
|
|
84
|
+
throw new Error("No valid task rows found in PLAN tasks.md");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return tasks;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadTasksFromJson(tasksPath: string): Promise<Task[]> {
|
|
91
|
+
const raw = await readFile(tasksPath, "utf-8");
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
const artifact = planTasksArtifactSchema.parse(parsed);
|
|
94
|
+
const normalized = normalizePlanTasks(artifact);
|
|
95
|
+
return normalized.map((task) =>
|
|
96
|
+
taskSchema.parse({
|
|
97
|
+
id: task.id,
|
|
98
|
+
title: task.title,
|
|
99
|
+
status: "PENDING",
|
|
100
|
+
wave: task.wave,
|
|
101
|
+
depends_on: task.dependsOnIndexes,
|
|
102
|
+
attempt: 0,
|
|
103
|
+
strike: 0,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildTasksArtifactFromLegacyTasks(tasks: readonly Task[]) {
|
|
109
|
+
const countersByWave = new Map<number, number>();
|
|
110
|
+
return planTasksArtifactSchema.parse({
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
tasks: tasks.map((task) => {
|
|
113
|
+
const nextIndex = (countersByWave.get(task.wave) ?? 0) + 1;
|
|
114
|
+
countersByWave.set(task.wave, nextIndex);
|
|
115
|
+
return {
|
|
116
|
+
taskId: `W${task.wave}-T${String(nextIndex).padStart(2, "0")}`,
|
|
117
|
+
title: task.title,
|
|
118
|
+
wave: task.wave,
|
|
119
|
+
depends_on: [] as string[],
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
5
125
|
export const handlePlan: PhaseHandler = async (_state, artifactDir, result?) => {
|
|
126
|
+
// When result is provided, the planner has completed writing tasks
|
|
127
|
+
// Load them from tasks.json (canonical) and populate state.tasks.
|
|
128
|
+
// Fall back to tasks.md for compatibility with legacy planners.
|
|
6
129
|
if (result) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
130
|
+
const tasksJsonPath = getArtifactRef(artifactDir, "PLAN", "tasks.json");
|
|
131
|
+
const tasksPath = getArtifactRef(artifactDir, "PLAN", "tasks.md");
|
|
132
|
+
try {
|
|
133
|
+
let loadedTasks: Task[];
|
|
134
|
+
let usedLegacyMarkdown = false;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
loadedTasks = await loadTasksFromJson(tasksJsonPath);
|
|
138
|
+
} catch (jsonError: unknown) {
|
|
139
|
+
if (!isEnoentError(jsonError)) {
|
|
140
|
+
throw jsonError;
|
|
141
|
+
}
|
|
142
|
+
loadedTasks = await loadTasksFromMarkdown(tasksPath);
|
|
143
|
+
usedLegacyMarkdown = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (usedLegacyMarkdown) {
|
|
147
|
+
const msg =
|
|
148
|
+
"PLAN fallback: parsed legacy tasks.md (tasks.json missing). Migrate planner output to tasks.json.";
|
|
149
|
+
console.warn(`[opencode-autopilot] ${msg}`);
|
|
150
|
+
logOrchestrationEvent(artifactDir, {
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
phase: "PLAN",
|
|
153
|
+
action: "error",
|
|
154
|
+
message: msg,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const artifact = buildTasksArtifactFromLegacyTasks(loadedTasks);
|
|
158
|
+
await writeFile(tasksJsonPath, JSON.stringify(artifact, null, 2), "utf-8");
|
|
159
|
+
} else {
|
|
160
|
+
const artifact = planTasksArtifactSchema.parse(
|
|
161
|
+
JSON.parse(await readFile(tasksJsonPath, "utf-8")),
|
|
162
|
+
);
|
|
163
|
+
const markdown = renderTasksMarkdown(artifact);
|
|
164
|
+
await writeFile(tasksPath, markdown, "utf-8");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
action: "complete",
|
|
169
|
+
phase: "PLAN",
|
|
170
|
+
resultKind: "phase_output",
|
|
171
|
+
progress: usedLegacyMarkdown
|
|
172
|
+
? `Planning complete — loaded ${loadedTasks.length} task(s) via legacy markdown fallback`
|
|
173
|
+
: `Planning complete — loaded ${loadedTasks.length} task(s) from tasks.json`,
|
|
174
|
+
_stateUpdates: {
|
|
175
|
+
tasks: loadedTasks,
|
|
176
|
+
},
|
|
177
|
+
} satisfies DispatchResult);
|
|
178
|
+
} catch (error: unknown) {
|
|
179
|
+
const reason = isEnoentError(error)
|
|
180
|
+
? "tasks.md not found after planner completion"
|
|
181
|
+
: error instanceof Error
|
|
182
|
+
? error.message
|
|
183
|
+
: "Unknown parsing error";
|
|
184
|
+
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
action: "error",
|
|
187
|
+
code: "E_PLAN_TASK_LOAD",
|
|
188
|
+
phase: "PLAN",
|
|
189
|
+
message: `Failed to load PLAN tasks: ${reason}`,
|
|
190
|
+
progress: "Planning failed — task extraction error",
|
|
191
|
+
} satisfies DispatchResult);
|
|
192
|
+
}
|
|
12
193
|
}
|
|
13
194
|
|
|
14
195
|
const architectRef = getArtifactRef(artifactDir, "ARCHITECT", "design.md");
|
|
15
196
|
const challengeRef = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
|
|
16
|
-
const tasksPath = getArtifactRef(artifactDir, "PLAN", "tasks.
|
|
197
|
+
const tasksPath = getArtifactRef(artifactDir, "PLAN", "tasks.json");
|
|
17
198
|
|
|
18
199
|
const prompt = [
|
|
19
200
|
"Read the architecture design at",
|
|
@@ -21,7 +202,7 @@ export const handlePlan: PhaseHandler = async (_state, artifactDir, result?) =>
|
|
|
21
202
|
"and the challenge brief at",
|
|
22
203
|
challengeRef,
|
|
23
204
|
"then produce a task plan.",
|
|
24
|
-
`Write tasks to ${tasksPath}.`,
|
|
205
|
+
`Write tasks to ${tasksPath} as strict JSON with shape {"schemaVersion":1,"tasks":[{"taskId":"W1-T01","title":"...","wave":1,"depends_on":[]}]}.`,
|
|
25
206
|
"Each task should have a 300-line diff max.",
|
|
26
207
|
"Assign wave numbers for parallel execution.",
|
|
27
208
|
].join(" ");
|
|
@@ -29,6 +210,7 @@ export const handlePlan: PhaseHandler = async (_state, artifactDir, result?) =>
|
|
|
29
210
|
return Object.freeze({
|
|
30
211
|
action: "dispatch",
|
|
31
212
|
agent: AGENT_NAMES.PLAN,
|
|
213
|
+
resultKind: "phase_output",
|
|
32
214
|
prompt,
|
|
33
215
|
phase: "PLAN",
|
|
34
216
|
progress: "Dispatching planner",
|
|
@@ -2,7 +2,7 @@ import { sanitizeTemplateContent } from "../../review/sanitize";
|
|
|
2
2
|
import { fileExists } from "../../utils/fs-helpers";
|
|
3
3
|
import { ensurePhaseDir, getArtifactRef } from "../artifacts";
|
|
4
4
|
import type { PipelineState } from "../types";
|
|
5
|
-
import { AGENT_NAMES, type DispatchResult } from "./types";
|
|
5
|
+
import { AGENT_NAMES, type DispatchResult, type PhaseHandlerContext } from "./types";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* RECON phase handler — dispatches oc-researcher with idea and artifact path.
|
|
@@ -12,6 +12,7 @@ export async function handleRecon(
|
|
|
12
12
|
state: Readonly<PipelineState>,
|
|
13
13
|
artifactDir: string,
|
|
14
14
|
result?: string,
|
|
15
|
+
_context?: PhaseHandlerContext,
|
|
15
16
|
): Promise<DispatchResult> {
|
|
16
17
|
if (result) {
|
|
17
18
|
// Warn if artifact wasn't written (best-effort — still complete the phase)
|
|
@@ -34,6 +35,7 @@ export async function handleRecon(
|
|
|
34
35
|
return Object.freeze({
|
|
35
36
|
action: "dispatch" as const,
|
|
36
37
|
agent: AGENT_NAMES.RECON,
|
|
38
|
+
resultKind: "phase_output",
|
|
37
39
|
prompt: [
|
|
38
40
|
`Research the following idea and write findings to ${outputPath}`,
|
|
39
41
|
`Idea: ${safeIdea}`,
|
|
@@ -12,6 +12,8 @@ import type { Phase } from "../types";
|
|
|
12
12
|
import type { DispatchResult, PhaseHandler } from "./types";
|
|
13
13
|
import { AGENT_NAMES } from "./types";
|
|
14
14
|
|
|
15
|
+
export const LESSONS_PARSE_ERROR_CODE = "E_RETRO_PARSE";
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Parse and validate lessons from the agent's JSON output.
|
|
17
19
|
* Returns only valid lessons; invalid entries are silently skipped (graceful degradation).
|
|
@@ -61,6 +63,8 @@ export const handleRetrospective: PhaseHandler = async (_state, artifactDir, res
|
|
|
61
63
|
if (parseError) {
|
|
62
64
|
return Object.freeze({
|
|
63
65
|
action: "complete",
|
|
66
|
+
code: LESSONS_PARSE_ERROR_CODE,
|
|
67
|
+
resultKind: "phase_output",
|
|
64
68
|
phase: "RETROSPECTIVE",
|
|
65
69
|
progress: "Retrospective complete -- no lessons extracted (parse error)",
|
|
66
70
|
} satisfies DispatchResult);
|
|
@@ -69,6 +73,7 @@ export const handleRetrospective: PhaseHandler = async (_state, artifactDir, res
|
|
|
69
73
|
if (valid.length === 0) {
|
|
70
74
|
return Object.freeze({
|
|
71
75
|
action: "complete",
|
|
76
|
+
resultKind: "phase_output",
|
|
72
77
|
phase: "RETROSPECTIVE",
|
|
73
78
|
progress: "Retrospective complete -- 0 lessons extracted",
|
|
74
79
|
} satisfies DispatchResult);
|
|
@@ -91,6 +96,7 @@ export const handleRetrospective: PhaseHandler = async (_state, artifactDir, res
|
|
|
91
96
|
const msg = raw.replace(/[/\\][^\s"']+/g, "[PATH]").slice(0, 256);
|
|
92
97
|
return Object.freeze({
|
|
93
98
|
action: "complete",
|
|
99
|
+
resultKind: "phase_output",
|
|
94
100
|
phase: "RETROSPECTIVE",
|
|
95
101
|
progress: `Retrospective complete — ${valid.length} lessons extracted (persistence failed: ${msg})`,
|
|
96
102
|
} satisfies DispatchResult);
|
|
@@ -98,6 +104,7 @@ export const handleRetrospective: PhaseHandler = async (_state, artifactDir, res
|
|
|
98
104
|
|
|
99
105
|
return Object.freeze({
|
|
100
106
|
action: "complete",
|
|
107
|
+
resultKind: "phase_output",
|
|
101
108
|
phase: "RETROSPECTIVE",
|
|
102
109
|
progress: `Retrospective complete -- ${valid.length} lessons extracted`,
|
|
103
110
|
} satisfies DispatchResult);
|
|
@@ -118,6 +125,7 @@ export const handleRetrospective: PhaseHandler = async (_state, artifactDir, res
|
|
|
118
125
|
return Object.freeze({
|
|
119
126
|
action: "dispatch",
|
|
120
127
|
agent: AGENT_NAMES.RETROSPECTIVE,
|
|
128
|
+
resultKind: "phase_output",
|
|
121
129
|
prompt,
|
|
122
130
|
phase: "RETROSPECTIVE",
|
|
123
131
|
progress: "Dispatching retrospector",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fileExists } from "../../utils/fs-helpers";
|
|
1
2
|
import { getArtifactRef, getPhaseDir } from "../artifacts";
|
|
2
3
|
import type { DispatchResult, PhaseHandler } from "./types";
|
|
3
4
|
import { AGENT_NAMES } from "./types";
|
|
@@ -6,6 +7,7 @@ export const handleShip: PhaseHandler = async (_state, artifactDir, result?) =>
|
|
|
6
7
|
if (result) {
|
|
7
8
|
return Object.freeze({
|
|
8
9
|
action: "complete",
|
|
10
|
+
resultKind: "phase_output",
|
|
9
11
|
phase: "SHIP",
|
|
10
12
|
progress: "Shipping complete — documentation written",
|
|
11
13
|
} satisfies DispatchResult);
|
|
@@ -14,7 +16,9 @@ export const handleShip: PhaseHandler = async (_state, artifactDir, result?) =>
|
|
|
14
16
|
const reconRef = getArtifactRef(artifactDir, "RECON", "report.md");
|
|
15
17
|
const challengeRef = getArtifactRef(artifactDir, "CHALLENGE", "brief.md");
|
|
16
18
|
const architectRef = getArtifactRef(artifactDir, "ARCHITECT", "design.md");
|
|
17
|
-
const
|
|
19
|
+
const tasksJsonRef = getArtifactRef(artifactDir, "PLAN", "tasks.json");
|
|
20
|
+
const tasksMarkdownRef = getArtifactRef(artifactDir, "PLAN", "tasks.md");
|
|
21
|
+
const planRef = (await fileExists(tasksJsonRef)) ? tasksJsonRef : tasksMarkdownRef;
|
|
18
22
|
const shipDir = getPhaseDir(artifactDir, "SHIP");
|
|
19
23
|
|
|
20
24
|
const prompt = [
|
|
@@ -32,6 +36,7 @@ export const handleShip: PhaseHandler = async (_state, artifactDir, result?) =>
|
|
|
32
36
|
return Object.freeze({
|
|
33
37
|
action: "dispatch",
|
|
34
38
|
agent: AGENT_NAMES.SHIP,
|
|
39
|
+
resultKind: "phase_output",
|
|
35
40
|
prompt,
|
|
36
41
|
phase: "SHIP",
|
|
37
42
|
progress: "Dispatching shipper",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResultEnvelope } from "../contracts/result-envelope";
|
|
2
|
+
import type { DispatchResultKind, PipelineState } from "../types";
|
|
2
3
|
|
|
3
4
|
export const AGENT_NAMES = Object.freeze({
|
|
4
5
|
RECON: "oc-researcher",
|
|
@@ -15,18 +16,36 @@ export const AGENT_NAMES = Object.freeze({
|
|
|
15
16
|
|
|
16
17
|
export interface DispatchResult {
|
|
17
18
|
readonly action: "dispatch" | "dispatch_multi" | "complete" | "error";
|
|
19
|
+
readonly code?: string;
|
|
18
20
|
readonly agent?: string;
|
|
19
|
-
readonly agents?: readonly {
|
|
21
|
+
readonly agents?: readonly {
|
|
22
|
+
readonly agent: string;
|
|
23
|
+
readonly prompt: string;
|
|
24
|
+
readonly dispatchId?: string;
|
|
25
|
+
readonly taskId?: number | null;
|
|
26
|
+
readonly resultKind?: DispatchResultKind;
|
|
27
|
+
}[];
|
|
20
28
|
readonly prompt?: string;
|
|
21
29
|
readonly phase?: string;
|
|
22
30
|
readonly progress?: string;
|
|
23
31
|
readonly message?: string;
|
|
32
|
+
readonly resultKind?: DispatchResultKind;
|
|
33
|
+
readonly taskId?: number | null;
|
|
34
|
+
readonly dispatchId?: string;
|
|
35
|
+
readonly runId?: string;
|
|
36
|
+
readonly expectedResultKind?: DispatchResultKind;
|
|
24
37
|
readonly _stateUpdates?: Partial<PipelineState>;
|
|
25
38
|
readonly _userProgress?: string;
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
export interface PhaseHandlerContext {
|
|
42
|
+
readonly envelope: ResultEnvelope;
|
|
43
|
+
readonly legacy: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
export type PhaseHandler = (
|
|
29
47
|
state: Readonly<PipelineState>,
|
|
30
48
|
artifactDir: string,
|
|
31
49
|
result?: string,
|
|
50
|
+
context?: PhaseHandlerContext,
|
|
32
51
|
) => Promise<DispatchResult>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { planTasksArtifactSchema } from "../contracts/phase-artifacts";
|
|
3
|
+
|
|
4
|
+
type PlanTasksArtifact = z.infer<typeof planTasksArtifactSchema>;
|
|
5
|
+
|
|
6
|
+
export function renderTasksMarkdown(artifact: PlanTasksArtifact): string {
|
|
7
|
+
const header = [
|
|
8
|
+
"# Implementation Task Plan",
|
|
9
|
+
"",
|
|
10
|
+
"## Task Table",
|
|
11
|
+
"",
|
|
12
|
+
"| Task ID | Title | Description | Files to Modify | Wave Number | Acceptance Criteria |",
|
|
13
|
+
"|---|---|---|---|---:|---|",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const rows = artifact.tasks.map((task) => {
|
|
17
|
+
const deps = task.depends_on.length > 0 ? `Depends on: ${task.depends_on.join(", ")}` : "None";
|
|
18
|
+
return `| ${task.taskId} | ${task.title.replace(/\|/g, "\\|")} | ${deps} | TBD | ${task.wave} | TBD |`;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return [...header, ...rows, ""].join("\n");
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { orchestrateCore } from "../tools/orchestrate";
|
|
2
|
+
import type { ResultEnvelope } from "./contracts/result-envelope";
|
|
3
|
+
|
|
4
|
+
export async function replayEnvelopes(
|
|
5
|
+
artifactDir: string,
|
|
6
|
+
envelopes: readonly ResultEnvelope[],
|
|
7
|
+
): Promise<readonly string[]> {
|
|
8
|
+
const outputs: string[] = [];
|
|
9
|
+
for (const envelope of envelopes) {
|
|
10
|
+
const result = await orchestrateCore({ result: JSON.stringify(envelope) }, artifactDir);
|
|
11
|
+
outputs.push(result);
|
|
12
|
+
}
|
|
13
|
+
return Object.freeze(outputs);
|
|
14
|
+
}
|
|
@@ -55,6 +55,21 @@ export const buildProgressSchema = z.object({
|
|
|
55
55
|
reviewPending: z.boolean().default(false),
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
export const dispatchResultKindSchema = z.enum([
|
|
59
|
+
"phase_output",
|
|
60
|
+
"task_completion",
|
|
61
|
+
"review_findings",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export const pendingDispatchSchema = z.object({
|
|
65
|
+
dispatchId: z.string().min(1).max(128),
|
|
66
|
+
phase: phaseSchema,
|
|
67
|
+
agent: z.string().min(1).max(128),
|
|
68
|
+
issuedAt: z.string().max(128),
|
|
69
|
+
resultKind: dispatchResultKindSchema.default("phase_output"),
|
|
70
|
+
taskId: z.number().int().positive().nullable().default(null),
|
|
71
|
+
});
|
|
72
|
+
|
|
58
73
|
export const failureContextSchema = z.object({
|
|
59
74
|
failedPhase: phaseSchema,
|
|
60
75
|
failedAgent: z.string().max(128).nullable(),
|
|
@@ -66,6 +81,8 @@ export const failureContextSchema = z.object({
|
|
|
66
81
|
export const pipelineStateSchema = z.object({
|
|
67
82
|
schemaVersion: z.literal(2),
|
|
68
83
|
status: z.enum(["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "FAILED"]),
|
|
84
|
+
runId: z.string().max(128).default("legacy-run"),
|
|
85
|
+
stateRevision: z.number().int().min(0).default(0),
|
|
69
86
|
idea: z.string().max(4096),
|
|
70
87
|
currentPhase: phaseSchema.nullable(),
|
|
71
88
|
startedAt: z.string().max(128),
|
|
@@ -83,6 +100,8 @@ export const pipelineStateSchema = z.object({
|
|
|
83
100
|
strikeCount: 0,
|
|
84
101
|
reviewPending: false,
|
|
85
102
|
}),
|
|
103
|
+
pendingDispatches: z.array(pendingDispatchSchema).max(2000).default([]),
|
|
104
|
+
processedResultIds: z.array(z.string().max(128)).max(10_000).default([]),
|
|
86
105
|
failureContext: failureContextSchema.nullable().default(null),
|
|
87
106
|
phaseDispatchCounts: z.record(z.string().max(32), z.number().int().min(0).max(1000)).default({}),
|
|
88
107
|
});
|
|
@@ -2,16 +2,23 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ensureDir, isEnoentError } from "../utils/fs-helpers";
|
|
5
|
+
import { assertStateInvariants } from "./contracts/invariants";
|
|
5
6
|
import { PHASES, pipelineStateSchema } from "./schemas";
|
|
6
7
|
import type { PipelineState } from "./types";
|
|
7
8
|
|
|
8
9
|
const STATE_FILE = "state.json";
|
|
9
10
|
|
|
11
|
+
function generateRunId(): string {
|
|
12
|
+
return `run_${randomBytes(8).toString("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function createInitialState(idea: string): PipelineState {
|
|
11
16
|
const now = new Date().toISOString();
|
|
12
17
|
return pipelineStateSchema.parse({
|
|
13
18
|
schemaVersion: 2,
|
|
14
19
|
status: "IN_PROGRESS",
|
|
20
|
+
runId: generateRunId(),
|
|
21
|
+
stateRevision: 0,
|
|
15
22
|
idea,
|
|
16
23
|
currentPhase: "RECON",
|
|
17
24
|
startedAt: now,
|
|
@@ -25,6 +32,8 @@ export function createInitialState(idea: string): PipelineState {
|
|
|
25
32
|
tasks: [],
|
|
26
33
|
arenaConfidence: null,
|
|
27
34
|
exploreTriggered: false,
|
|
35
|
+
pendingDispatches: [],
|
|
36
|
+
processedResultIds: [],
|
|
28
37
|
});
|
|
29
38
|
}
|
|
30
39
|
|
|
@@ -42,8 +51,23 @@ export async function loadState(artifactDir: string): Promise<PipelineState | nu
|
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
export async function saveState(
|
|
54
|
+
export async function saveState(
|
|
55
|
+
state: PipelineState,
|
|
56
|
+
artifactDir: string,
|
|
57
|
+
expectedRevision?: number,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (typeof expectedRevision === "number") {
|
|
60
|
+
const current = await loadState(artifactDir);
|
|
61
|
+
const currentRevision = current?.stateRevision ?? -1;
|
|
62
|
+
if (currentRevision !== expectedRevision) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`E_STATE_CONFLICT: expected stateRevision ${expectedRevision}, found ${currentRevision}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
const validated = pipelineStateSchema.parse(state);
|
|
70
|
+
assertStateInvariants(validated);
|
|
47
71
|
await ensureDir(artifactDir);
|
|
48
72
|
const statePath = join(artifactDir, STATE_FILE);
|
|
49
73
|
const tmpPath = `${statePath}.tmp.${randomBytes(8).toString("hex")}`;
|
|
@@ -55,20 +79,38 @@ export function patchState(
|
|
|
55
79
|
current: Readonly<PipelineState>,
|
|
56
80
|
updates: Partial<PipelineState>,
|
|
57
81
|
): PipelineState {
|
|
82
|
+
const now = new Date().toISOString();
|
|
58
83
|
const merged = {
|
|
59
84
|
...current,
|
|
60
85
|
...updates,
|
|
61
|
-
|
|
86
|
+
stateRevision: current.stateRevision + 1,
|
|
87
|
+
lastUpdatedAt: now,
|
|
62
88
|
};
|
|
63
|
-
|
|
89
|
+
|
|
90
|
+
if (merged.status === "COMPLETED") {
|
|
91
|
+
merged.currentPhase = null;
|
|
92
|
+
merged.phases = merged.phases.map((phase) => {
|
|
93
|
+
if (phase.status === "IN_PROGRESS") {
|
|
94
|
+
return {
|
|
95
|
+
...phase,
|
|
96
|
+
status: "DONE" as const,
|
|
97
|
+
completedAt: phase.completedAt ?? now,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return phase;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const validated = pipelineStateSchema.parse(merged);
|
|
105
|
+
assertStateInvariants(validated);
|
|
106
|
+
return validated;
|
|
64
107
|
}
|
|
65
108
|
|
|
66
109
|
export function appendDecision(
|
|
67
110
|
current: Readonly<PipelineState>,
|
|
68
111
|
decision: { phase: string; agent: string; decision: string; rationale: string },
|
|
69
112
|
): PipelineState {
|
|
70
|
-
return {
|
|
71
|
-
...current,
|
|
113
|
+
return patchState(current, {
|
|
72
114
|
decisions: [
|
|
73
115
|
...current.decisions,
|
|
74
116
|
{
|
|
@@ -76,6 +118,5 @@ export function appendDecision(
|
|
|
76
118
|
timestamp: new Date().toISOString(),
|
|
77
119
|
},
|
|
78
120
|
],
|
|
79
|
-
|
|
80
|
-
};
|
|
121
|
+
});
|
|
81
122
|
}
|
|
@@ -3,7 +3,9 @@ import type {
|
|
|
3
3
|
buildProgressSchema,
|
|
4
4
|
confidenceEntrySchema,
|
|
5
5
|
decisionEntrySchema,
|
|
6
|
+
dispatchResultKindSchema,
|
|
6
7
|
failureContextSchema,
|
|
8
|
+
pendingDispatchSchema,
|
|
7
9
|
phaseSchema,
|
|
8
10
|
phaseStatusSchema,
|
|
9
11
|
pipelineStateSchema,
|
|
@@ -18,3 +20,5 @@ export type ConfidenceEntry = z.infer<typeof confidenceEntrySchema>;
|
|
|
18
20
|
export type Task = z.infer<typeof taskSchema>;
|
|
19
21
|
export type BuildProgress = z.infer<typeof buildProgressSchema>;
|
|
20
22
|
export type FailureContext = z.infer<typeof failureContextSchema>;
|
|
23
|
+
export type PendingDispatch = z.infer<typeof pendingDispatchSchema>;
|
|
24
|
+
export type DispatchResultKind = z.infer<typeof dispatchResultKindSchema>;
|
package/src/review/pipeline.ts
CHANGED
|
@@ -19,8 +19,8 @@ const STAGE3_NAMES: ReadonlySet<string> = new Set(STAGE3_AGENTS.map((a) => a.nam
|
|
|
19
19
|
|
|
20
20
|
import { buildFixInstructions, determineFixableFindings } from "./fix-cycle";
|
|
21
21
|
import { buildReport } from "./report";
|
|
22
|
-
import { reviewFindingSchema } from "./schemas";
|
|
23
|
-
import type { ReviewFinding, ReviewReport, ReviewState } from "./types";
|
|
22
|
+
import { reviewFindingSchema, reviewFindingsEnvelopeSchema } from "./schemas";
|
|
23
|
+
import type { ReviewFinding, ReviewFindingsEnvelope, ReviewReport, ReviewState } from "./types";
|
|
24
24
|
|
|
25
25
|
export type { ReviewState };
|
|
26
26
|
|
|
@@ -32,8 +32,19 @@ export interface ReviewStageResult {
|
|
|
32
32
|
readonly stage?: number;
|
|
33
33
|
readonly agents?: readonly { readonly name: string; readonly prompt: string }[];
|
|
34
34
|
readonly report?: ReviewReport;
|
|
35
|
+
readonly findingsEnvelope?: ReviewFindingsEnvelope;
|
|
35
36
|
readonly message?: string;
|
|
36
37
|
readonly state?: ReviewState;
|
|
38
|
+
readonly parseMode?: "typed" | "legacy";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseTypedFindingsEnvelope(raw: string): ReviewFindingsEnvelope | null {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
return reviewFindingsEnvelopeSchema.parse(parsed);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
@@ -145,8 +156,11 @@ export function advancePipeline(
|
|
|
145
156
|
currentState: ReviewState,
|
|
146
157
|
agentName = "unknown",
|
|
147
158
|
): ReviewStageResult {
|
|
148
|
-
|
|
149
|
-
const
|
|
159
|
+
const typedEnvelope = parseTypedFindingsEnvelope(findingsJson);
|
|
160
|
+
const parseMode = typedEnvelope ? "typed" : "legacy";
|
|
161
|
+
const newFindings = typedEnvelope
|
|
162
|
+
? typedEnvelope.findings
|
|
163
|
+
: parseAgentFindings(findingsJson, agentName);
|
|
150
164
|
const accumulated = [...currentState.accumulatedFindings, ...newFindings];
|
|
151
165
|
|
|
152
166
|
const nextStage = currentState.stage + 1;
|
|
@@ -169,6 +183,7 @@ export function advancePipeline(
|
|
|
169
183
|
action: "dispatch" as const,
|
|
170
184
|
stage: nextStage,
|
|
171
185
|
agents: prompts,
|
|
186
|
+
parseMode,
|
|
172
187
|
state: newState,
|
|
173
188
|
});
|
|
174
189
|
}
|
|
@@ -193,6 +208,7 @@ export function advancePipeline(
|
|
|
193
208
|
action: "dispatch" as const,
|
|
194
209
|
stage: nextStage,
|
|
195
210
|
agents: Object.freeze(stage3Prompts.map((p) => Object.freeze(p))),
|
|
211
|
+
parseMode,
|
|
196
212
|
state: newState,
|
|
197
213
|
});
|
|
198
214
|
}
|
|
@@ -217,18 +233,37 @@ export function advancePipeline(
|
|
|
217
233
|
stage: nextStage,
|
|
218
234
|
message: "Fix cycle: CRITICAL findings with actionable suggestions detected.",
|
|
219
235
|
agents: Object.freeze(fixAgents),
|
|
236
|
+
parseMode,
|
|
220
237
|
state: newState,
|
|
221
238
|
});
|
|
222
239
|
}
|
|
223
240
|
// No fix cycle needed -- complete
|
|
224
241
|
const report = buildReport(accumulated, currentState.scope, currentState.selectedAgentNames);
|
|
225
|
-
return Object.freeze({
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
action: "complete" as const,
|
|
244
|
+
report,
|
|
245
|
+
findingsEnvelope: Object.freeze({
|
|
246
|
+
schemaVersion: 1 as const,
|
|
247
|
+
kind: "review_findings" as const,
|
|
248
|
+
findings: accumulated,
|
|
249
|
+
}),
|
|
250
|
+
parseMode,
|
|
251
|
+
});
|
|
226
252
|
}
|
|
227
253
|
|
|
228
254
|
case 4: {
|
|
229
255
|
// Stage 4 -> complete: Build final report with all findings
|
|
230
256
|
const report = buildReport(accumulated, currentState.scope, currentState.selectedAgentNames);
|
|
231
|
-
return Object.freeze({
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
action: "complete" as const,
|
|
259
|
+
report,
|
|
260
|
+
findingsEnvelope: Object.freeze({
|
|
261
|
+
schemaVersion: 1 as const,
|
|
262
|
+
kind: "review_findings" as const,
|
|
263
|
+
findings: accumulated,
|
|
264
|
+
}),
|
|
265
|
+
parseMode,
|
|
266
|
+
});
|
|
232
267
|
}
|
|
233
268
|
|
|
234
269
|
default:
|
package/src/review/schemas.ts
CHANGED
|
@@ -65,6 +65,12 @@ export const reviewStateSchema = z.object({
|
|
|
65
65
|
startedAt: z.string().max(128),
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
export const reviewFindingsEnvelopeSchema = z.object({
|
|
69
|
+
schemaVersion: z.literal(1).default(1),
|
|
70
|
+
kind: z.literal("review_findings"),
|
|
71
|
+
findings: z.array(reviewFindingSchema).max(500).default([]),
|
|
72
|
+
});
|
|
73
|
+
|
|
68
74
|
export const reviewConfigSchema = z.object({
|
|
69
75
|
parallel: z.boolean().default(true),
|
|
70
76
|
maxFixAttempts: z.number().int().min(0).max(10).default(3),
|