@kodrunhq/opencode-autopilot 0.1.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/LICENSE +21 -0
- package/README.md +1 -0
- package/assets/agents/placeholder-agent.md +13 -0
- package/assets/commands/configure.md +17 -0
- package/assets/commands/new-agent.md +16 -0
- package/assets/commands/new-command.md +15 -0
- package/assets/commands/new-skill.md +15 -0
- package/assets/commands/review-pr.md +49 -0
- package/assets/skills/.gitkeep +0 -0
- package/assets/skills/coding-standards/SKILL.md +327 -0
- package/package.json +52 -0
- package/src/agents/autopilot.ts +42 -0
- package/src/agents/documenter.ts +44 -0
- package/src/agents/index.ts +49 -0
- package/src/agents/metaprompter.ts +50 -0
- package/src/agents/pipeline/index.ts +25 -0
- package/src/agents/pipeline/oc-architect.ts +49 -0
- package/src/agents/pipeline/oc-challenger.ts +44 -0
- package/src/agents/pipeline/oc-critic.ts +42 -0
- package/src/agents/pipeline/oc-explorer.ts +46 -0
- package/src/agents/pipeline/oc-implementer.ts +56 -0
- package/src/agents/pipeline/oc-planner.ts +45 -0
- package/src/agents/pipeline/oc-researcher.ts +46 -0
- package/src/agents/pipeline/oc-retrospector.ts +42 -0
- package/src/agents/pipeline/oc-reviewer.ts +44 -0
- package/src/agents/pipeline/oc-shipper.ts +42 -0
- package/src/agents/pr-reviewer.ts +74 -0
- package/src/agents/researcher.ts +43 -0
- package/src/config.ts +168 -0
- package/src/index.ts +152 -0
- package/src/installer.ts +130 -0
- package/src/orchestrator/arena.ts +41 -0
- package/src/orchestrator/artifacts.ts +28 -0
- package/src/orchestrator/confidence.ts +59 -0
- package/src/orchestrator/fallback/chat-message-handler.ts +49 -0
- package/src/orchestrator/fallback/error-classifier.ts +148 -0
- package/src/orchestrator/fallback/event-handler.ts +235 -0
- package/src/orchestrator/fallback/fallback-config.ts +16 -0
- package/src/orchestrator/fallback/fallback-manager.ts +323 -0
- package/src/orchestrator/fallback/fallback-state.ts +120 -0
- package/src/orchestrator/fallback/index.ts +11 -0
- package/src/orchestrator/fallback/message-replay.ts +40 -0
- package/src/orchestrator/fallback/resolve-chain.ts +34 -0
- package/src/orchestrator/fallback/tool-execute-handler.ts +44 -0
- package/src/orchestrator/fallback/types.ts +46 -0
- package/src/orchestrator/handlers/architect.ts +114 -0
- package/src/orchestrator/handlers/build.ts +363 -0
- package/src/orchestrator/handlers/challenge.ts +41 -0
- package/src/orchestrator/handlers/explore.ts +9 -0
- package/src/orchestrator/handlers/index.ts +21 -0
- package/src/orchestrator/handlers/plan.ts +35 -0
- package/src/orchestrator/handlers/recon.ts +40 -0
- package/src/orchestrator/handlers/retrospective.ts +123 -0
- package/src/orchestrator/handlers/ship.ts +38 -0
- package/src/orchestrator/handlers/types.ts +31 -0
- package/src/orchestrator/lesson-injection.ts +80 -0
- package/src/orchestrator/lesson-memory.ts +110 -0
- package/src/orchestrator/lesson-schemas.ts +24 -0
- package/src/orchestrator/lesson-types.ts +6 -0
- package/src/orchestrator/phase.ts +76 -0
- package/src/orchestrator/plan.ts +43 -0
- package/src/orchestrator/schemas.ts +86 -0
- package/src/orchestrator/skill-injection.ts +52 -0
- package/src/orchestrator/state.ts +80 -0
- package/src/orchestrator/types.ts +20 -0
- package/src/review/agent-catalog.ts +439 -0
- package/src/review/agents/auth-flow-verifier.ts +47 -0
- package/src/review/agents/code-quality-auditor.ts +51 -0
- package/src/review/agents/concurrency-checker.ts +47 -0
- package/src/review/agents/contract-verifier.ts +45 -0
- package/src/review/agents/database-auditor.ts +47 -0
- package/src/review/agents/dead-code-scanner.ts +47 -0
- package/src/review/agents/go-idioms-auditor.ts +46 -0
- package/src/review/agents/index.ts +82 -0
- package/src/review/agents/logic-auditor.ts +47 -0
- package/src/review/agents/product-thinker.ts +49 -0
- package/src/review/agents/python-django-auditor.ts +46 -0
- package/src/review/agents/react-patterns-auditor.ts +46 -0
- package/src/review/agents/red-team.ts +49 -0
- package/src/review/agents/rust-safety-auditor.ts +46 -0
- package/src/review/agents/scope-intent-verifier.ts +45 -0
- package/src/review/agents/security-auditor.ts +47 -0
- package/src/review/agents/silent-failure-hunter.ts +45 -0
- package/src/review/agents/spec-checker.ts +45 -0
- package/src/review/agents/state-mgmt-auditor.ts +46 -0
- package/src/review/agents/test-interrogator.ts +43 -0
- package/src/review/agents/type-soundness.ts +46 -0
- package/src/review/agents/wiring-inspector.ts +46 -0
- package/src/review/cross-verification.ts +71 -0
- package/src/review/finding-builder.ts +74 -0
- package/src/review/fix-cycle.ts +146 -0
- package/src/review/memory.ts +114 -0
- package/src/review/pipeline.ts +258 -0
- package/src/review/report.ts +141 -0
- package/src/review/sanitize.ts +8 -0
- package/src/review/schemas.ts +75 -0
- package/src/review/selection.ts +98 -0
- package/src/review/severity.ts +71 -0
- package/src/review/stack-gate.ts +127 -0
- package/src/review/types.ts +43 -0
- package/src/templates/agent-template.ts +47 -0
- package/src/templates/command-template.ts +29 -0
- package/src/templates/skill-template.ts +42 -0
- package/src/tools/confidence.ts +93 -0
- package/src/tools/create-agent.ts +81 -0
- package/src/tools/create-command.ts +74 -0
- package/src/tools/create-skill.ts +74 -0
- package/src/tools/forensics.ts +88 -0
- package/src/tools/orchestrate.ts +310 -0
- package/src/tools/phase.ts +92 -0
- package/src/tools/placeholder.ts +11 -0
- package/src/tools/plan.ts +56 -0
- package/src/tools/review.ts +295 -0
- package/src/tools/state.ts +112 -0
- package/src/utils/fs-helpers.ts +39 -0
- package/src/utils/gitignore.ts +27 -0
- package/src/utils/paths.ts +17 -0
- package/src/utils/validators.ts +57 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sanitizeTemplateContent } from "../../review/sanitize";
|
|
4
|
+
import { fileExists } from "../../utils/fs-helpers";
|
|
5
|
+
import { getDebateDepth } from "../arena";
|
|
6
|
+
import { ensurePhaseDir, getArtifactRef, getPhaseDir } from "../artifacts";
|
|
7
|
+
import { filterByPhase } from "../confidence";
|
|
8
|
+
import type { PipelineState } from "../types";
|
|
9
|
+
import { AGENT_NAMES, type DispatchResult } from "./types";
|
|
10
|
+
|
|
11
|
+
const CONSTRAINT_FRAMINGS: readonly string[] = Object.freeze([
|
|
12
|
+
"Optimize for simplicity and minimal dependencies",
|
|
13
|
+
"Optimize for extensibility and future growth",
|
|
14
|
+
"Optimize for performance and resource efficiency",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ARCHITECT phase handler with Arena multi-proposal logic.
|
|
19
|
+
*
|
|
20
|
+
* Three-step flow using artifact-existence for idempotency:
|
|
21
|
+
* 1. No proposals/design -> dispatch architect(s) based on debate depth
|
|
22
|
+
* 2. Proposals exist but no critique -> dispatch critic
|
|
23
|
+
* 3. Critique or design exists -> complete
|
|
24
|
+
*/
|
|
25
|
+
export async function handleArchitect(
|
|
26
|
+
state: Readonly<PipelineState>,
|
|
27
|
+
artifactDir: string,
|
|
28
|
+
_result?: string,
|
|
29
|
+
): Promise<DispatchResult> {
|
|
30
|
+
const phaseDir = getPhaseDir(artifactDir, "ARCHITECT");
|
|
31
|
+
const critiqueExists = await fileExists(join(phaseDir, "critique.md"));
|
|
32
|
+
const designExists = await fileExists(join(phaseDir, "design.md"));
|
|
33
|
+
|
|
34
|
+
// Step 3: critique or design exists -> complete
|
|
35
|
+
if (critiqueExists || designExists) {
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
action: "complete" as const,
|
|
38
|
+
phase: "ARCHITECT",
|
|
39
|
+
progress: "ARCHITECT complete",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check for existing proposals (Step 2)
|
|
44
|
+
const proposalsDir = join(phaseDir, "proposals");
|
|
45
|
+
const hasProposals = await hasProposalFiles(proposalsDir);
|
|
46
|
+
|
|
47
|
+
if (hasProposals) {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
action: "dispatch" as const,
|
|
50
|
+
agent: AGENT_NAMES.CRITIC,
|
|
51
|
+
prompt: [
|
|
52
|
+
`Review architecture proposals in phases/ARCHITECT/proposals/`,
|
|
53
|
+
`Read ${getArtifactRef("RECON", "report.md")} and ${getArtifactRef("CHALLENGE", "brief.md")} for context.`,
|
|
54
|
+
`Write comparative critique to ${getArtifactRef("ARCHITECT", "critique.md")}`,
|
|
55
|
+
`Include: strengths, weaknesses, recommendation, confidence (HIGH/MEDIUM/LOW).`,
|
|
56
|
+
].join("\n"),
|
|
57
|
+
phase: "ARCHITECT",
|
|
58
|
+
progress: "Dispatching critic for proposal review",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Step 1: Dispatch architect(s) based on confidence depth
|
|
63
|
+
await ensurePhaseDir(artifactDir, "ARCHITECT");
|
|
64
|
+
const reconEntries = filterByPhase(state.confidence, "RECON");
|
|
65
|
+
const depth = getDebateDepth(reconEntries);
|
|
66
|
+
const reconRef = getArtifactRef("RECON", "report.md");
|
|
67
|
+
const challengeRef = getArtifactRef("CHALLENGE", "brief.md");
|
|
68
|
+
const safeIdea = sanitizeTemplateContent(state.idea).replace(/[\r\n]+/g, " ");
|
|
69
|
+
|
|
70
|
+
if (depth === 1) {
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
action: "dispatch" as const,
|
|
73
|
+
agent: AGENT_NAMES.ARCHITECT,
|
|
74
|
+
prompt: [
|
|
75
|
+
`Read ${reconRef} and ${challengeRef} for context.`,
|
|
76
|
+
`Design architecture for: ${safeIdea}`,
|
|
77
|
+
`Write design to ${getArtifactRef("ARCHITECT", "design.md")}`,
|
|
78
|
+
`Include: component diagram, data flow, technology choices, confidence (HIGH/MEDIUM/LOW).`,
|
|
79
|
+
].join("\n"),
|
|
80
|
+
phase: "ARCHITECT",
|
|
81
|
+
progress: "Dispatching architect for design",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const agents = CONSTRAINT_FRAMINGS.slice(0, depth).map((constraint, i) => {
|
|
86
|
+
const label = String.fromCharCode(65 + i); // A, B, C
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
agent: AGENT_NAMES.ARCHITECT,
|
|
89
|
+
prompt: [
|
|
90
|
+
`Read ${reconRef} and ${challengeRef} for context.`,
|
|
91
|
+
`Design architecture for: ${safeIdea}`,
|
|
92
|
+
`Constraint: ${constraint}`,
|
|
93
|
+
`Write proposal to phases/ARCHITECT/proposals/proposal-${label}.md`,
|
|
94
|
+
`Include: component diagram, data flow, technology choices, confidence (HIGH/MEDIUM/LOW).`,
|
|
95
|
+
].join("\n"),
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
action: "dispatch_multi" as const,
|
|
101
|
+
agents: Object.freeze(agents),
|
|
102
|
+
phase: "ARCHITECT",
|
|
103
|
+
progress: `Dispatching ${depth} architects for Arena proposals`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function hasProposalFiles(proposalsDir: string): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
const entries = await readdir(proposalsDir);
|
|
110
|
+
return entries.some((e) => e.startsWith("proposal-") && e.endsWith(".md"));
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { sanitizeTemplateContent } from "../../review/sanitize";
|
|
2
|
+
import { getArtifactRef } from "../artifacts";
|
|
3
|
+
import { groupByWave } from "../plan";
|
|
4
|
+
import type { BuildProgress, Task } from "../types";
|
|
5
|
+
import type { DispatchResult, PhaseHandler } from "./types";
|
|
6
|
+
import { AGENT_NAMES } from "./types";
|
|
7
|
+
|
|
8
|
+
const MAX_STRIKES = 3;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the first wave number that has PENDING tasks.
|
|
12
|
+
*/
|
|
13
|
+
function findCurrentWave(waveMap: ReadonlyMap<number, readonly Task[]>): number | null {
|
|
14
|
+
const sortedWaves = [...waveMap.keys()].sort((a, b) => a - b);
|
|
15
|
+
for (const wave of sortedWaves) {
|
|
16
|
+
const tasks = waveMap.get(wave) ?? [];
|
|
17
|
+
if (tasks.some((t) => t.status === "PENDING" || t.status === "IN_PROGRESS")) {
|
|
18
|
+
return wave;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get pending tasks for a specific wave.
|
|
26
|
+
*/
|
|
27
|
+
function findPendingTasks(
|
|
28
|
+
waveMap: ReadonlyMap<number, readonly Task[]>,
|
|
29
|
+
wave: number,
|
|
30
|
+
): readonly Task[] {
|
|
31
|
+
const tasks = waveMap.get(wave) ?? [];
|
|
32
|
+
return tasks.filter((t) => t.status === "PENDING");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get in-progress tasks for a specific wave.
|
|
37
|
+
*/
|
|
38
|
+
function findInProgressTasks(
|
|
39
|
+
waveMap: ReadonlyMap<number, readonly Task[]>,
|
|
40
|
+
wave: number,
|
|
41
|
+
): readonly Task[] {
|
|
42
|
+
const tasks = waveMap.get(wave) ?? [];
|
|
43
|
+
return tasks.filter((t) => t.status === "IN_PROGRESS");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mark multiple tasks as IN_PROGRESS immutably.
|
|
48
|
+
*/
|
|
49
|
+
function markTasksInProgress(tasks: readonly Task[], taskIds: readonly number[]): readonly Task[] {
|
|
50
|
+
const idSet = new Set(taskIds);
|
|
51
|
+
return tasks.map((t) => (idSet.has(t.id) ? { ...t, status: "IN_PROGRESS" as const } : t));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a prompt for a single task dispatch.
|
|
56
|
+
*/
|
|
57
|
+
function buildTaskPrompt(task: Task): string {
|
|
58
|
+
const planRef = getArtifactRef("PLAN", "tasks.md");
|
|
59
|
+
const designRef = getArtifactRef("ARCHITECT", "design.md");
|
|
60
|
+
return [
|
|
61
|
+
`Implement task ${task.id}: ${task.title}.`,
|
|
62
|
+
`Reference the plan at ${planRef}`,
|
|
63
|
+
`and architecture at ${designRef}.`,
|
|
64
|
+
`If a CLAUDE.md file exists in the project root, read it for project-specific conventions.`,
|
|
65
|
+
`Check ~/.config/opencode/skills/coding-standards/SKILL.md for coding standards.`,
|
|
66
|
+
`Report completion when done.`,
|
|
67
|
+
].join(" ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mark a task as DONE immutably and return the updated tasks array.
|
|
72
|
+
*/
|
|
73
|
+
function markTaskDone(tasks: readonly Task[], taskId: number): readonly Task[] {
|
|
74
|
+
return tasks.map((t) => (t.id === taskId ? { ...t, status: "DONE" as const } : t));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check whether all tasks in a given wave are DONE.
|
|
79
|
+
*/
|
|
80
|
+
function isWaveComplete(waveMap: ReadonlyMap<number, readonly Task[]>, wave: number): boolean {
|
|
81
|
+
const tasks = waveMap.get(wave) ?? [];
|
|
82
|
+
return tasks.every((t) => t.status === "DONE");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse review result to check for CRITICAL findings.
|
|
87
|
+
*/
|
|
88
|
+
function hasCriticalFindings(resultStr: string): boolean {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(resultStr);
|
|
91
|
+
if (parsed.severity === "CRITICAL") return true;
|
|
92
|
+
const hasCritical = (arr: unknown[]): boolean =>
|
|
93
|
+
arr.some(
|
|
94
|
+
(f: unknown) =>
|
|
95
|
+
typeof f === "object" &&
|
|
96
|
+
f !== null &&
|
|
97
|
+
"severity" in f &&
|
|
98
|
+
(f as { severity: string }).severity === "CRITICAL",
|
|
99
|
+
);
|
|
100
|
+
if (Array.isArray(parsed.findings)) {
|
|
101
|
+
return hasCritical(parsed.findings);
|
|
102
|
+
}
|
|
103
|
+
if (parsed.report?.findings && Array.isArray(parsed.report.findings)) {
|
|
104
|
+
return hasCritical(parsed.report.findings);
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const handleBuild: PhaseHandler = async (state, _artifactDir, result?) => {
|
|
113
|
+
const { tasks, buildProgress } = state;
|
|
114
|
+
|
|
115
|
+
// Edge case: no tasks
|
|
116
|
+
if (tasks.length === 0) {
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
action: "error",
|
|
119
|
+
phase: "BUILD",
|
|
120
|
+
message: "No tasks found",
|
|
121
|
+
} satisfies DispatchResult);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Edge case: strike count exceeded
|
|
125
|
+
if (buildProgress.strikeCount > MAX_STRIKES && buildProgress.reviewPending && result) {
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
action: "error",
|
|
128
|
+
phase: "BUILD",
|
|
129
|
+
message: "Max retries exceeded — too many CRITICAL review findings",
|
|
130
|
+
} satisfies DispatchResult);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Case 1: Review pending + result provided -> process review outcome
|
|
134
|
+
if (buildProgress.reviewPending && result) {
|
|
135
|
+
if (hasCriticalFindings(result)) {
|
|
136
|
+
// Re-dispatch implementer with fix instructions
|
|
137
|
+
const safeResult = sanitizeTemplateContent(result).slice(0, 4000);
|
|
138
|
+
const prompt = [
|
|
139
|
+
`CRITICAL review findings detected. Fix the following issues:`,
|
|
140
|
+
safeResult,
|
|
141
|
+
`Reference ${getArtifactRef("PLAN", "tasks.md")} for context.`,
|
|
142
|
+
].join(" ");
|
|
143
|
+
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
action: "dispatch",
|
|
146
|
+
agent: AGENT_NAMES.BUILD,
|
|
147
|
+
prompt,
|
|
148
|
+
phase: "BUILD",
|
|
149
|
+
progress: "Fix dispatch — CRITICAL findings",
|
|
150
|
+
_stateUpdates: {
|
|
151
|
+
buildProgress: {
|
|
152
|
+
...buildProgress,
|
|
153
|
+
strikeCount: buildProgress.strikeCount + 1,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
} satisfies DispatchResult);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// No critical -> advance to next wave
|
|
160
|
+
const waveMap = groupByWave(tasks);
|
|
161
|
+
const nextWave = findCurrentWave(waveMap);
|
|
162
|
+
|
|
163
|
+
if (nextWave === null) {
|
|
164
|
+
return Object.freeze({
|
|
165
|
+
action: "complete",
|
|
166
|
+
phase: "BUILD",
|
|
167
|
+
progress: "All tasks and reviews complete",
|
|
168
|
+
_stateUpdates: {
|
|
169
|
+
buildProgress: {
|
|
170
|
+
...buildProgress,
|
|
171
|
+
reviewPending: false,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
} satisfies DispatchResult);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const pendingTasks = findPendingTasks(waveMap, nextWave);
|
|
178
|
+
const updatedProgress: BuildProgress = {
|
|
179
|
+
...buildProgress,
|
|
180
|
+
reviewPending: false,
|
|
181
|
+
currentWave: nextWave,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (pendingTasks.length === 1) {
|
|
185
|
+
const task = pendingTasks[0];
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
action: "dispatch",
|
|
188
|
+
agent: AGENT_NAMES.BUILD,
|
|
189
|
+
prompt: buildTaskPrompt(task),
|
|
190
|
+
phase: "BUILD",
|
|
191
|
+
progress: `Wave ${nextWave} — task ${task.id}`,
|
|
192
|
+
_stateUpdates: {
|
|
193
|
+
buildProgress: { ...updatedProgress, currentTask: task.id },
|
|
194
|
+
},
|
|
195
|
+
} satisfies DispatchResult);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const dispatchedIds = pendingTasks.map((t) => t.id);
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
action: "dispatch_multi",
|
|
201
|
+
agents: pendingTasks.map((task) => ({
|
|
202
|
+
agent: AGENT_NAMES.BUILD,
|
|
203
|
+
prompt: buildTaskPrompt(task),
|
|
204
|
+
})),
|
|
205
|
+
phase: "BUILD",
|
|
206
|
+
progress: `Wave ${nextWave} — ${pendingTasks.length} concurrent tasks`,
|
|
207
|
+
_stateUpdates: {
|
|
208
|
+
tasks: [...markTasksInProgress(tasks, dispatchedIds)],
|
|
209
|
+
buildProgress: { ...updatedProgress, currentTask: null },
|
|
210
|
+
},
|
|
211
|
+
} satisfies DispatchResult);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Case 2: Result provided + not review pending -> mark task done
|
|
215
|
+
// For dispatch_multi, currentTask may be null — find the first IN_PROGRESS task instead
|
|
216
|
+
const taskToComplete =
|
|
217
|
+
buildProgress.currentTask ?? tasks.find((t) => t.status === "IN_PROGRESS")?.id ?? null;
|
|
218
|
+
if (result && !buildProgress.reviewPending && taskToComplete !== null) {
|
|
219
|
+
const updatedTasks = markTaskDone(tasks, taskToComplete);
|
|
220
|
+
const waveMap = groupByWave(updatedTasks);
|
|
221
|
+
const currentWave = buildProgress.currentWave ?? 1;
|
|
222
|
+
|
|
223
|
+
if (isWaveComplete(waveMap, currentWave)) {
|
|
224
|
+
// Wave complete -> trigger review (same for final wave or intermediate)
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
action: "dispatch",
|
|
227
|
+
agent: AGENT_NAMES.REVIEW,
|
|
228
|
+
prompt: "Review completed wave. Scope: branch. Report any CRITICAL findings.",
|
|
229
|
+
phase: "BUILD",
|
|
230
|
+
progress: `Wave ${currentWave} complete — review pending`,
|
|
231
|
+
_stateUpdates: {
|
|
232
|
+
tasks: [...updatedTasks],
|
|
233
|
+
buildProgress: {
|
|
234
|
+
...buildProgress,
|
|
235
|
+
currentTask: null,
|
|
236
|
+
reviewPending: true,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
} satisfies DispatchResult);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Wave not complete -> dispatch next pending task or wait for in-progress
|
|
243
|
+
const pendingInWave = findPendingTasks(waveMap, currentWave);
|
|
244
|
+
if (pendingInWave.length > 0) {
|
|
245
|
+
const next = pendingInWave[0];
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
action: "dispatch",
|
|
248
|
+
agent: AGENT_NAMES.BUILD,
|
|
249
|
+
prompt: buildTaskPrompt(next),
|
|
250
|
+
phase: "BUILD",
|
|
251
|
+
progress: `Wave ${currentWave} — task ${next.id}`,
|
|
252
|
+
_stateUpdates: {
|
|
253
|
+
tasks: [...updatedTasks],
|
|
254
|
+
buildProgress: {
|
|
255
|
+
...buildProgress,
|
|
256
|
+
currentTask: next.id,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
} satisfies DispatchResult);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// No pending tasks but wave not complete — other tasks are still IN_PROGRESS
|
|
263
|
+
const inProgressInWave = findInProgressTasks(waveMap, currentWave);
|
|
264
|
+
if (inProgressInWave.length > 0) {
|
|
265
|
+
return Object.freeze({
|
|
266
|
+
action: "dispatch",
|
|
267
|
+
agent: AGENT_NAMES.BUILD,
|
|
268
|
+
prompt: `Wave ${currentWave} has ${inProgressInWave.length} task(s) still in progress. Continue working on remaining tasks.`,
|
|
269
|
+
phase: "BUILD",
|
|
270
|
+
progress: `Wave ${currentWave} — waiting for ${inProgressInWave.length} in-progress task(s)`,
|
|
271
|
+
_stateUpdates: {
|
|
272
|
+
tasks: [...updatedTasks],
|
|
273
|
+
buildProgress: {
|
|
274
|
+
...buildProgress,
|
|
275
|
+
currentTask: null,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
} satisfies DispatchResult);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Case 3: No result (first call or resume) -> find first pending wave
|
|
283
|
+
const waveMap = groupByWave(tasks);
|
|
284
|
+
const currentWave = findCurrentWave(waveMap);
|
|
285
|
+
|
|
286
|
+
if (currentWave === null) {
|
|
287
|
+
// All tasks already DONE
|
|
288
|
+
return Object.freeze({
|
|
289
|
+
action: "complete",
|
|
290
|
+
phase: "BUILD",
|
|
291
|
+
progress: "All tasks complete",
|
|
292
|
+
} satisfies DispatchResult);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const pendingTasks = findPendingTasks(waveMap, currentWave);
|
|
296
|
+
const inProgressTasks = findInProgressTasks(waveMap, currentWave);
|
|
297
|
+
|
|
298
|
+
if (pendingTasks.length === 0 && inProgressTasks.length > 0) {
|
|
299
|
+
// Wave has only IN_PROGRESS tasks (e.g., resume after dispatch_multi).
|
|
300
|
+
// Return a dispatch instruction so the orchestrator knows work is underway.
|
|
301
|
+
return Object.freeze({
|
|
302
|
+
action: "dispatch",
|
|
303
|
+
agent: AGENT_NAMES.BUILD,
|
|
304
|
+
prompt: `Resume: wave ${currentWave} has ${inProgressTasks.length} task(s) still in progress. Wait for agent results and pass them back.`,
|
|
305
|
+
phase: "BUILD",
|
|
306
|
+
progress: `Wave ${currentWave} — waiting for ${inProgressTasks.length} in-progress task(s)`,
|
|
307
|
+
_stateUpdates: {
|
|
308
|
+
buildProgress: {
|
|
309
|
+
...buildProgress,
|
|
310
|
+
currentWave,
|
|
311
|
+
currentTask: null,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
} satisfies DispatchResult);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (pendingTasks.length === 0) {
|
|
318
|
+
// All tasks in all waves DONE (findCurrentWave already checked PENDING + IN_PROGRESS)
|
|
319
|
+
return Object.freeze({
|
|
320
|
+
action: "complete",
|
|
321
|
+
phase: "BUILD",
|
|
322
|
+
progress: "All tasks complete",
|
|
323
|
+
} satisfies DispatchResult);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (pendingTasks.length === 1) {
|
|
327
|
+
const task = pendingTasks[0];
|
|
328
|
+
return Object.freeze({
|
|
329
|
+
action: "dispatch",
|
|
330
|
+
agent: AGENT_NAMES.BUILD,
|
|
331
|
+
prompt: buildTaskPrompt(task),
|
|
332
|
+
phase: "BUILD",
|
|
333
|
+
progress: `Wave ${currentWave} — task ${task.id}`,
|
|
334
|
+
_stateUpdates: {
|
|
335
|
+
buildProgress: {
|
|
336
|
+
...buildProgress,
|
|
337
|
+
currentTask: task.id,
|
|
338
|
+
currentWave,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
} satisfies DispatchResult);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Multiple pending tasks in wave -> dispatch_multi
|
|
345
|
+
const dispatchedIds = pendingTasks.map((t) => t.id);
|
|
346
|
+
return Object.freeze({
|
|
347
|
+
action: "dispatch_multi",
|
|
348
|
+
agents: pendingTasks.map((task) => ({
|
|
349
|
+
agent: AGENT_NAMES.BUILD,
|
|
350
|
+
prompt: buildTaskPrompt(task),
|
|
351
|
+
})),
|
|
352
|
+
phase: "BUILD",
|
|
353
|
+
progress: `Wave ${currentWave} — ${pendingTasks.length} concurrent tasks`,
|
|
354
|
+
_stateUpdates: {
|
|
355
|
+
tasks: [...markTasksInProgress(tasks, dispatchedIds)],
|
|
356
|
+
buildProgress: {
|
|
357
|
+
...buildProgress,
|
|
358
|
+
currentTask: null,
|
|
359
|
+
currentWave,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
} satisfies DispatchResult);
|
|
363
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { sanitizeTemplateContent } from "../../review/sanitize";
|
|
2
|
+
import { ensurePhaseDir, getArtifactRef } from "../artifacts";
|
|
3
|
+
import type { PipelineState } from "../types";
|
|
4
|
+
import { AGENT_NAMES, type DispatchResult } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CHALLENGE phase handler — dispatches oc-challenger with RECON artifact references.
|
|
8
|
+
* References files by path (not content injection) per D-11.
|
|
9
|
+
*/
|
|
10
|
+
export async function handleChallenge(
|
|
11
|
+
state: Readonly<PipelineState>,
|
|
12
|
+
artifactDir: string,
|
|
13
|
+
result?: string,
|
|
14
|
+
): Promise<DispatchResult> {
|
|
15
|
+
if (result) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
action: "complete" as const,
|
|
18
|
+
phase: "CHALLENGE",
|
|
19
|
+
progress: "CHALLENGE complete",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await ensurePhaseDir(artifactDir, "CHALLENGE");
|
|
24
|
+
const reconRef = getArtifactRef("RECON", "report.md");
|
|
25
|
+
const outputRef = getArtifactRef("CHALLENGE", "brief.md");
|
|
26
|
+
|
|
27
|
+
const safeIdea = sanitizeTemplateContent(state.idea).replace(/[\r\n]+/g, " ");
|
|
28
|
+
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
action: "dispatch" as const,
|
|
31
|
+
agent: AGENT_NAMES.CHALLENGE,
|
|
32
|
+
prompt: [
|
|
33
|
+
`Read ${reconRef} for research context.`,
|
|
34
|
+
`Original idea: ${safeIdea}`,
|
|
35
|
+
`Propose up to 3 enhancements. Write ambitious brief to ${outputRef}`,
|
|
36
|
+
`For each: name, user value, complexity (LOW/MEDIUM/HIGH), accept/reject rationale.`,
|
|
37
|
+
].join("\n"),
|
|
38
|
+
phase: "CHALLENGE",
|
|
39
|
+
progress: "Dispatching challenger for product enhancements",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DispatchResult, PhaseHandler } from "./types";
|
|
2
|
+
|
|
3
|
+
export const handleExplore: PhaseHandler = async (_state, _artifactDir, _result?) => {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
action: "complete",
|
|
6
|
+
phase: "EXPLORE",
|
|
7
|
+
progress: "EXPLORE skipped (not yet implemented)",
|
|
8
|
+
} satisfies DispatchResult);
|
|
9
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Phase } from "../types";
|
|
2
|
+
import { handleArchitect } from "./architect";
|
|
3
|
+
import { handleBuild } from "./build";
|
|
4
|
+
import { handleChallenge } from "./challenge";
|
|
5
|
+
import { handleExplore } from "./explore";
|
|
6
|
+
import { handlePlan } from "./plan";
|
|
7
|
+
import { handleRecon } from "./recon";
|
|
8
|
+
import { handleRetrospective } from "./retrospective";
|
|
9
|
+
import { handleShip } from "./ship";
|
|
10
|
+
import type { PhaseHandler } from "./types";
|
|
11
|
+
|
|
12
|
+
export const PHASE_HANDLERS: Readonly<Record<Phase, PhaseHandler>> = Object.freeze({
|
|
13
|
+
RECON: handleRecon,
|
|
14
|
+
CHALLENGE: handleChallenge,
|
|
15
|
+
ARCHITECT: handleArchitect,
|
|
16
|
+
EXPLORE: handleExplore,
|
|
17
|
+
PLAN: handlePlan,
|
|
18
|
+
BUILD: handleBuild,
|
|
19
|
+
SHIP: handleShip,
|
|
20
|
+
RETROSPECTIVE: handleRetrospective,
|
|
21
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getArtifactRef } from "../artifacts";
|
|
2
|
+
import type { DispatchResult, PhaseHandler } from "./types";
|
|
3
|
+
import { AGENT_NAMES } from "./types";
|
|
4
|
+
|
|
5
|
+
export const handlePlan: PhaseHandler = async (_state, _artifactDir, result?) => {
|
|
6
|
+
if (result) {
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
action: "complete",
|
|
9
|
+
phase: "PLAN",
|
|
10
|
+
progress: "Planning complete — tasks written",
|
|
11
|
+
} satisfies DispatchResult);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const architectRef = getArtifactRef("ARCHITECT", "design.md");
|
|
15
|
+
const challengeRef = getArtifactRef("CHALLENGE", "brief.md");
|
|
16
|
+
|
|
17
|
+
const prompt = [
|
|
18
|
+
"Read the architecture design at",
|
|
19
|
+
architectRef,
|
|
20
|
+
"and the challenge brief at",
|
|
21
|
+
challengeRef,
|
|
22
|
+
"then produce a task plan.",
|
|
23
|
+
"Write tasks to phases/PLAN/tasks.md.",
|
|
24
|
+
"Each task should have a 300-line diff max.",
|
|
25
|
+
"Assign wave numbers for parallel execution.",
|
|
26
|
+
].join(" ");
|
|
27
|
+
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
action: "dispatch",
|
|
30
|
+
agent: AGENT_NAMES.PLAN,
|
|
31
|
+
prompt,
|
|
32
|
+
phase: "PLAN",
|
|
33
|
+
progress: "Dispatching planner",
|
|
34
|
+
} satisfies DispatchResult);
|
|
35
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { sanitizeTemplateContent } from "../../review/sanitize";
|
|
2
|
+
import { ensurePhaseDir, getArtifactRef } from "../artifacts";
|
|
3
|
+
import type { PipelineState } from "../types";
|
|
4
|
+
import { AGENT_NAMES, type DispatchResult } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RECON phase handler — dispatches oc-researcher with idea and artifact path.
|
|
8
|
+
* Uses file references (not content injection) per D-11.
|
|
9
|
+
*/
|
|
10
|
+
export async function handleRecon(
|
|
11
|
+
state: Readonly<PipelineState>,
|
|
12
|
+
artifactDir: string,
|
|
13
|
+
result?: string,
|
|
14
|
+
): Promise<DispatchResult> {
|
|
15
|
+
if (result) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
action: "complete" as const,
|
|
18
|
+
phase: "RECON",
|
|
19
|
+
progress: "RECON complete",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const phaseDir = await ensurePhaseDir(artifactDir, "RECON");
|
|
24
|
+
const outputRef = getArtifactRef("RECON", "report.md");
|
|
25
|
+
|
|
26
|
+
const safeIdea = sanitizeTemplateContent(state.idea).replace(/[\r\n]+/g, " ");
|
|
27
|
+
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
action: "dispatch" as const,
|
|
30
|
+
agent: AGENT_NAMES.RECON,
|
|
31
|
+
prompt: [
|
|
32
|
+
`Research the following idea and write findings to ${outputRef}`,
|
|
33
|
+
`Idea: ${safeIdea}`,
|
|
34
|
+
`Output: ${phaseDir}/report.md`,
|
|
35
|
+
`Include: Market Analysis, Technology Options, UX Considerations, Feasibility Assessment, Confidence (HIGH/MEDIUM/LOW)`,
|
|
36
|
+
].join("\n"),
|
|
37
|
+
phase: "RECON",
|
|
38
|
+
progress: "Dispatching researcher for domain analysis",
|
|
39
|
+
});
|
|
40
|
+
}
|