@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,88 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { loadState } from "../orchestrator/state";
|
|
3
|
+
import { getProjectArtifactDir } from "../utils/paths";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BUILD failures are terminal (strike overflow). All other phases are recoverable.
|
|
7
|
+
*/
|
|
8
|
+
function isRecoverable(failedPhase: string): boolean {
|
|
9
|
+
return failedPhase !== "BUILD";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Suggest the best recovery action based on failed phase and recoverability.
|
|
14
|
+
* - Not recoverable -> "restart"
|
|
15
|
+
* - RECON (nothing completed to resume from) -> "restart"
|
|
16
|
+
* - All other recoverable phases -> "resume"
|
|
17
|
+
*/
|
|
18
|
+
function getSuggestedAction(failedPhase: string, recoverable: boolean): "resume" | "restart" {
|
|
19
|
+
if (!recoverable) return "restart";
|
|
20
|
+
if (failedPhase === "RECON") return "restart";
|
|
21
|
+
return "resume";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function forensicsCore(
|
|
25
|
+
_args: Record<string, never>,
|
|
26
|
+
projectRoot: string,
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
const artifactDir = getProjectArtifactDir(projectRoot);
|
|
29
|
+
let state: Awaited<ReturnType<typeof loadState>>;
|
|
30
|
+
try {
|
|
31
|
+
state = await loadState(artifactDir);
|
|
32
|
+
} catch (error: unknown) {
|
|
33
|
+
const detail =
|
|
34
|
+
error instanceof SyntaxError
|
|
35
|
+
? "state file contains invalid JSON"
|
|
36
|
+
: error !== null && typeof error === "object" && "issues" in error
|
|
37
|
+
? "state file failed schema validation"
|
|
38
|
+
: "state file is unreadable";
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
action: "error",
|
|
41
|
+
message: `Pipeline state is corrupt: ${detail}`,
|
|
42
|
+
recoverable: false,
|
|
43
|
+
suggestedAction: "manual",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (state === null) {
|
|
48
|
+
return JSON.stringify({ action: "error", message: "No pipeline state found" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (state.status !== "FAILED") {
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
action: "error",
|
|
54
|
+
message: `No failure to diagnose -- pipeline status: ${state.status}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!state.failureContext) {
|
|
59
|
+
return JSON.stringify({
|
|
60
|
+
action: "error",
|
|
61
|
+
message: "No failure metadata recorded",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { failureContext } = state;
|
|
66
|
+
const recoverable = isRecoverable(failureContext.failedPhase);
|
|
67
|
+
const suggestedAction = getSuggestedAction(failureContext.failedPhase, recoverable);
|
|
68
|
+
const phasesCompleted = state.phases.filter((p) => p.status === "DONE").map((p) => p.name);
|
|
69
|
+
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
failedPhase: failureContext.failedPhase,
|
|
72
|
+
failedAgent: failureContext.failedAgent,
|
|
73
|
+
errorMessage: failureContext.errorMessage,
|
|
74
|
+
lastSuccessfulPhase: failureContext.lastSuccessfulPhase,
|
|
75
|
+
recoverable,
|
|
76
|
+
suggestedAction,
|
|
77
|
+
phasesCompleted,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const ocForensics = tool({
|
|
82
|
+
description:
|
|
83
|
+
"Diagnose a failed orchestrator pipeline run. Returns structured JSON with failing phase, root cause, and whether the failure is recoverable. Invoke after a pipeline error.",
|
|
84
|
+
args: {},
|
|
85
|
+
async execute(_args) {
|
|
86
|
+
return forensicsCore({}, process.cwd());
|
|
87
|
+
},
|
|
88
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { PHASE_HANDLERS } from "../orchestrator/handlers/index";
|
|
4
|
+
import type { DispatchResult } from "../orchestrator/handlers/types";
|
|
5
|
+
import { buildLessonContext } from "../orchestrator/lesson-injection";
|
|
6
|
+
import { loadLessonMemory } from "../orchestrator/lesson-memory";
|
|
7
|
+
import { completePhase, getNextPhase } from "../orchestrator/phase";
|
|
8
|
+
import { buildSkillContext, loadSkillContent } from "../orchestrator/skill-injection";
|
|
9
|
+
import { createInitialState, loadState, patchState, saveState } from "../orchestrator/state";
|
|
10
|
+
import type { Phase } from "../orchestrator/types";
|
|
11
|
+
import { isEnoentError } from "../utils/fs-helpers";
|
|
12
|
+
import { ensureGitignore } from "../utils/gitignore";
|
|
13
|
+
import { getGlobalConfigDir, getProjectArtifactDir } from "../utils/paths";
|
|
14
|
+
import { reviewCore } from "./review";
|
|
15
|
+
|
|
16
|
+
interface OrchestrateArgs {
|
|
17
|
+
readonly idea?: string;
|
|
18
|
+
readonly result?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply state updates from a DispatchResult if present, then save.
|
|
23
|
+
* Returns the updated state.
|
|
24
|
+
*/
|
|
25
|
+
async function applyStateUpdates(
|
|
26
|
+
state: Readonly<import("../orchestrator/types").PipelineState>,
|
|
27
|
+
handlerResult: DispatchResult,
|
|
28
|
+
artifactDir: string,
|
|
29
|
+
): Promise<import("../orchestrator/types").PipelineState> {
|
|
30
|
+
const updates = handlerResult._stateUpdates;
|
|
31
|
+
if (updates) {
|
|
32
|
+
const updated = patchState(state, updates);
|
|
33
|
+
await saveState(updated, artifactDir);
|
|
34
|
+
return updated;
|
|
35
|
+
}
|
|
36
|
+
return state;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* When a handler dispatches "oc-reviewer", call reviewCore directly instead
|
|
41
|
+
* of returning the dispatch instruction. This avoids the JSON round-trip
|
|
42
|
+
* for the review integration in BUILD phase (per CONTEXT.md).
|
|
43
|
+
*/
|
|
44
|
+
async function maybeInlineReview(
|
|
45
|
+
handlerResult: DispatchResult,
|
|
46
|
+
artifactDir: string,
|
|
47
|
+
): Promise<{ readonly inlined: boolean; readonly reviewResult?: string }> {
|
|
48
|
+
if (
|
|
49
|
+
handlerResult.action === "dispatch" &&
|
|
50
|
+
handlerResult.agent === "oc-reviewer" &&
|
|
51
|
+
handlerResult.prompt
|
|
52
|
+
) {
|
|
53
|
+
const projectRoot = join(artifactDir, "..");
|
|
54
|
+
const reviewResult = await reviewCore({ scope: "branch" }, projectRoot);
|
|
55
|
+
return { inlined: true, reviewResult };
|
|
56
|
+
}
|
|
57
|
+
return { inlined: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Attempt to inject lesson context into a dispatch prompt.
|
|
62
|
+
* Best-effort: failures are silently swallowed to avoid breaking dispatch.
|
|
63
|
+
*/
|
|
64
|
+
async function injectLessonContext(
|
|
65
|
+
prompt: string,
|
|
66
|
+
phase: string,
|
|
67
|
+
artifactDir: string,
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
try {
|
|
70
|
+
const projectRoot = join(artifactDir, "..");
|
|
71
|
+
const memory = await loadLessonMemory(projectRoot);
|
|
72
|
+
if (memory && memory.lessons.length > 0) {
|
|
73
|
+
const ctx = buildLessonContext(memory.lessons, phase as Phase);
|
|
74
|
+
if (ctx) {
|
|
75
|
+
return prompt + ctx;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (error: unknown) {
|
|
79
|
+
if (
|
|
80
|
+
isEnoentError(error) ||
|
|
81
|
+
error instanceof SyntaxError ||
|
|
82
|
+
(error !== null && typeof error === "object" && "issues" in error)
|
|
83
|
+
) {
|
|
84
|
+
return prompt; // I/O, parse, or validation error -- non-critical
|
|
85
|
+
}
|
|
86
|
+
// Treat any NodeJS.ErrnoException (EACCES, EPERM, etc.) as non-critical
|
|
87
|
+
if (error !== null && typeof error === "object") {
|
|
88
|
+
const errWithCode = error as { code?: unknown };
|
|
89
|
+
if (typeof errWithCode.code === "string") {
|
|
90
|
+
return prompt;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw error; // re-throw programmer errors
|
|
94
|
+
}
|
|
95
|
+
return prompt;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Attempt to inject coding-standards skill context into a dispatch prompt.
|
|
100
|
+
* Best-effort: failures are silently swallowed to avoid breaking dispatch.
|
|
101
|
+
*/
|
|
102
|
+
async function injectSkillContext(prompt: string): Promise<string> {
|
|
103
|
+
try {
|
|
104
|
+
const baseDir = getGlobalConfigDir();
|
|
105
|
+
const content = await loadSkillContent(baseDir);
|
|
106
|
+
const ctx = buildSkillContext(content);
|
|
107
|
+
if (ctx) return prompt + ctx;
|
|
108
|
+
} catch {
|
|
109
|
+
// Best-effort: swallow all errors (same as lesson injection)
|
|
110
|
+
}
|
|
111
|
+
return prompt;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Process a handler's DispatchResult, handling complete/dispatch/dispatch_multi/error.
|
|
116
|
+
* On complete, advances the phase and invokes the next handler.
|
|
117
|
+
*/
|
|
118
|
+
async function processHandlerResult(
|
|
119
|
+
handlerResult: DispatchResult,
|
|
120
|
+
state: Readonly<import("../orchestrator/types").PipelineState>,
|
|
121
|
+
artifactDir: string,
|
|
122
|
+
): Promise<string> {
|
|
123
|
+
// Apply state updates from handler if present
|
|
124
|
+
const currentState = await applyStateUpdates(state, handlerResult, artifactDir);
|
|
125
|
+
|
|
126
|
+
switch (handlerResult.action) {
|
|
127
|
+
case "error":
|
|
128
|
+
return JSON.stringify(handlerResult);
|
|
129
|
+
|
|
130
|
+
case "dispatch": {
|
|
131
|
+
// Check if this is a review dispatch that should be inlined
|
|
132
|
+
const { inlined, reviewResult } = await maybeInlineReview(handlerResult, artifactDir);
|
|
133
|
+
if (inlined && reviewResult) {
|
|
134
|
+
// Feed the review result back into the current phase handler
|
|
135
|
+
const reloadedState = await loadState(artifactDir);
|
|
136
|
+
if (reloadedState?.currentPhase) {
|
|
137
|
+
const handler = PHASE_HANDLERS[reloadedState.currentPhase];
|
|
138
|
+
const nextResult = await handler(reloadedState, artifactDir, reviewResult);
|
|
139
|
+
return processHandlerResult(nextResult, reloadedState, artifactDir);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Inject lesson + skill context into dispatch prompt (best-effort)
|
|
143
|
+
if (handlerResult.prompt && handlerResult.phase) {
|
|
144
|
+
const enrichedPrompt = await injectLessonContext(
|
|
145
|
+
handlerResult.prompt,
|
|
146
|
+
handlerResult.phase,
|
|
147
|
+
artifactDir,
|
|
148
|
+
);
|
|
149
|
+
const withSkills = await injectSkillContext(enrichedPrompt);
|
|
150
|
+
if (withSkills !== handlerResult.prompt) {
|
|
151
|
+
return JSON.stringify({ ...handlerResult, prompt: withSkills });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return JSON.stringify(handlerResult);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "dispatch_multi": {
|
|
158
|
+
// Inject lesson + skill context into each agent's prompt (best-effort)
|
|
159
|
+
// Load lesson and skill context once and reuse for all agents in the batch
|
|
160
|
+
if (handlerResult.agents && handlerResult.phase) {
|
|
161
|
+
const lessonSuffix = await injectLessonContext(
|
|
162
|
+
"",
|
|
163
|
+
handlerResult.phase as string,
|
|
164
|
+
artifactDir,
|
|
165
|
+
);
|
|
166
|
+
const skillSuffix = await injectSkillContext("");
|
|
167
|
+
const combinedSuffix = lessonSuffix + (skillSuffix || "");
|
|
168
|
+
if (combinedSuffix) {
|
|
169
|
+
const enrichedAgents = handlerResult.agents.map((entry) => ({
|
|
170
|
+
...entry,
|
|
171
|
+
prompt: entry.prompt + combinedSuffix,
|
|
172
|
+
}));
|
|
173
|
+
return JSON.stringify({ ...handlerResult, agents: enrichedAgents });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return JSON.stringify(handlerResult);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case "complete": {
|
|
180
|
+
if (currentState.currentPhase === null) {
|
|
181
|
+
return JSON.stringify({
|
|
182
|
+
action: "complete",
|
|
183
|
+
summary: `Pipeline completed. Idea: ${currentState.idea}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const nextPhase = getNextPhase(currentState.currentPhase);
|
|
188
|
+
const advanced = completePhase(currentState);
|
|
189
|
+
await saveState(advanced, artifactDir);
|
|
190
|
+
|
|
191
|
+
if (nextPhase === null) {
|
|
192
|
+
// Terminal phase completed
|
|
193
|
+
const finished = { ...advanced, status: "COMPLETED" as const };
|
|
194
|
+
await saveState(finished, artifactDir);
|
|
195
|
+
return JSON.stringify({
|
|
196
|
+
action: "complete",
|
|
197
|
+
summary: `Pipeline completed all 8 phases. Idea: ${currentState.idea}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Invoke the next phase handler immediately
|
|
202
|
+
const nextHandler = PHASE_HANDLERS[nextPhase];
|
|
203
|
+
const nextResult = await nextHandler(advanced, artifactDir);
|
|
204
|
+
return processHandlerResult(nextResult, advanced, artifactDir);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
default:
|
|
208
|
+
return JSON.stringify({
|
|
209
|
+
action: "error",
|
|
210
|
+
message: `Unknown handler action: "${String((handlerResult as unknown as Record<string, unknown>).action)}"`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string): Promise<string> {
|
|
216
|
+
try {
|
|
217
|
+
const state = await loadState(artifactDir);
|
|
218
|
+
|
|
219
|
+
// No state and no idea -> error
|
|
220
|
+
if (state === null && !args.idea) {
|
|
221
|
+
return JSON.stringify({
|
|
222
|
+
action: "error",
|
|
223
|
+
message: "No active run. Provide an idea to start.",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// No state but idea provided -> create initial state and dispatch RECON via handler
|
|
228
|
+
if (state === null && args.idea) {
|
|
229
|
+
const newState = createInitialState(args.idea);
|
|
230
|
+
await saveState(newState, artifactDir);
|
|
231
|
+
|
|
232
|
+
// Best-effort .gitignore update
|
|
233
|
+
try {
|
|
234
|
+
const projectRoot = join(artifactDir, "..");
|
|
235
|
+
await ensureGitignore(projectRoot);
|
|
236
|
+
} catch {
|
|
237
|
+
// Swallow gitignore errors -- non-critical
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const handler = PHASE_HANDLERS[newState.currentPhase as Phase];
|
|
241
|
+
const handlerResult = await handler(newState, artifactDir);
|
|
242
|
+
return processHandlerResult(handlerResult, newState, artifactDir);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// State exists
|
|
246
|
+
if (state !== null) {
|
|
247
|
+
// Pipeline already completed
|
|
248
|
+
if (state.currentPhase === null) {
|
|
249
|
+
return JSON.stringify({
|
|
250
|
+
action: "complete",
|
|
251
|
+
summary: `Pipeline already completed. Idea: ${state.idea}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Delegate to current phase handler
|
|
256
|
+
const handler = PHASE_HANDLERS[state.currentPhase];
|
|
257
|
+
const handlerResult = await handler(state, artifactDir, args.result);
|
|
258
|
+
return processHandlerResult(handlerResult, state, artifactDir);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return JSON.stringify({ action: "error", message: "Unexpected state" });
|
|
262
|
+
} catch (error: unknown) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
const safeMessage = message.replace(/[/\\][^\s"']+/g, "[PATH]").slice(0, 4096);
|
|
265
|
+
|
|
266
|
+
// Persist failure metadata for forensics (best-effort)
|
|
267
|
+
try {
|
|
268
|
+
const currentState = await loadState(artifactDir);
|
|
269
|
+
if (currentState?.currentPhase) {
|
|
270
|
+
const lastDone = currentState.phases.filter((p) => p.status === "DONE").pop();
|
|
271
|
+
const failureContext = {
|
|
272
|
+
failedPhase: currentState.currentPhase,
|
|
273
|
+
failedAgent: null as string | null,
|
|
274
|
+
errorMessage: safeMessage,
|
|
275
|
+
timestamp: new Date().toISOString(),
|
|
276
|
+
lastSuccessfulPhase: lastDone?.name ?? null,
|
|
277
|
+
};
|
|
278
|
+
const failed = patchState(currentState, {
|
|
279
|
+
status: "FAILED" as const,
|
|
280
|
+
failureContext,
|
|
281
|
+
});
|
|
282
|
+
await saveState(failed, artifactDir);
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Swallow save errors -- original error takes priority
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return JSON.stringify({ action: "error", message: safeMessage });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const ocOrchestrate = tool({
|
|
293
|
+
description:
|
|
294
|
+
"Drive the orchestrator pipeline. Provide an idea to start a new run, or a result to advance the current phase. Returns JSON with action (dispatch/dispatch_multi/complete/error).",
|
|
295
|
+
args: {
|
|
296
|
+
idea: tool.schema
|
|
297
|
+
.string()
|
|
298
|
+
.max(4096)
|
|
299
|
+
.optional()
|
|
300
|
+
.describe("Idea to start a new orchestration run"),
|
|
301
|
+
result: tool.schema
|
|
302
|
+
.string()
|
|
303
|
+
.max(1_048_576)
|
|
304
|
+
.optional()
|
|
305
|
+
.describe("Result from previous agent to advance the pipeline"),
|
|
306
|
+
},
|
|
307
|
+
async execute(args) {
|
|
308
|
+
return orchestrateCore(args, getProjectArtifactDir(process.cwd()));
|
|
309
|
+
},
|
|
310
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { completePhase, getPhaseStatus, validateTransition } from "../orchestrator/phase";
|
|
3
|
+
import { phaseSchema } from "../orchestrator/schemas";
|
|
4
|
+
import { loadState, saveState } from "../orchestrator/state";
|
|
5
|
+
import { getProjectArtifactDir } from "../utils/paths";
|
|
6
|
+
|
|
7
|
+
interface PhaseArgs {
|
|
8
|
+
readonly subcommand: string;
|
|
9
|
+
readonly from?: string;
|
|
10
|
+
readonly to?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function phaseCore(args: PhaseArgs, artifactDir: string): Promise<string> {
|
|
14
|
+
try {
|
|
15
|
+
switch (args.subcommand) {
|
|
16
|
+
case "status": {
|
|
17
|
+
const state = await loadState(artifactDir);
|
|
18
|
+
if (state === null) {
|
|
19
|
+
return JSON.stringify({ error: "no_state" });
|
|
20
|
+
}
|
|
21
|
+
const currentPhase = state.currentPhase;
|
|
22
|
+
if (currentPhase === null) {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
currentPhase: null,
|
|
25
|
+
status: state.status,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const phaseStatus = getPhaseStatus(state, currentPhase);
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
currentPhase,
|
|
31
|
+
status: phaseStatus?.status ?? "UNKNOWN",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case "complete": {
|
|
36
|
+
const state = await loadState(artifactDir);
|
|
37
|
+
if (state === null) {
|
|
38
|
+
return JSON.stringify({ error: "no_state" });
|
|
39
|
+
}
|
|
40
|
+
const previousPhase = state.currentPhase;
|
|
41
|
+
const updated = completePhase(state);
|
|
42
|
+
await saveState(updated, artifactDir);
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
ok: true,
|
|
45
|
+
previousPhase,
|
|
46
|
+
currentPhase: updated.currentPhase,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case "validate": {
|
|
51
|
+
if (!args.from || !args.to) {
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
error: "from and to phases are required",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const fromPhase = phaseSchema.parse(args.from);
|
|
58
|
+
const toPhase = phaseSchema.parse(args.to);
|
|
59
|
+
validateTransition(fromPhase, toPhase);
|
|
60
|
+
return JSON.stringify({ valid: true });
|
|
61
|
+
} catch (validationError: unknown) {
|
|
62
|
+
const message =
|
|
63
|
+
validationError instanceof Error ? validationError.message : String(validationError);
|
|
64
|
+
return JSON.stringify({ valid: false, error: message });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
error: `unknown subcommand: ${args.subcommand}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
} catch (error: unknown) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return JSON.stringify({ error: message });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const ocPhase = tool({
|
|
80
|
+
description:
|
|
81
|
+
"Manage orchestrator phase transitions. Subcommands: status (current phase), complete (advance to next), validate (check transition validity).",
|
|
82
|
+
args: {
|
|
83
|
+
subcommand: tool.schema
|
|
84
|
+
.enum(["status", "complete", "validate"])
|
|
85
|
+
.describe("Operation to perform"),
|
|
86
|
+
from: tool.schema.string().optional().describe("Source phase for validate subcommand"),
|
|
87
|
+
to: tool.schema.string().optional().describe("Target phase for validate subcommand"),
|
|
88
|
+
},
|
|
89
|
+
async execute(args) {
|
|
90
|
+
return phaseCore(args, getProjectArtifactDir(process.cwd()));
|
|
91
|
+
},
|
|
92
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
export const ocPlaceholder = tool({
|
|
4
|
+
description: "Verifies that the OpenCode Assets plugin is loaded and working",
|
|
5
|
+
args: {
|
|
6
|
+
message: tool.schema.string().max(1000).describe("A test message to echo back"),
|
|
7
|
+
},
|
|
8
|
+
async execute(args) {
|
|
9
|
+
return `OpenCode Assets plugin is active. Your message: ${args.message}`;
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { countByStatus, groupByWave } from "../orchestrator/plan";
|
|
3
|
+
import { loadState } from "../orchestrator/state";
|
|
4
|
+
import { getProjectArtifactDir } from "../utils/paths";
|
|
5
|
+
|
|
6
|
+
interface PlanArgs {
|
|
7
|
+
readonly subcommand: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function planCore(args: PlanArgs, artifactDir: string): Promise<string> {
|
|
11
|
+
try {
|
|
12
|
+
switch (args.subcommand) {
|
|
13
|
+
case "waves": {
|
|
14
|
+
const state = await loadState(artifactDir);
|
|
15
|
+
if (state === null) {
|
|
16
|
+
return JSON.stringify({ error: "no_state" });
|
|
17
|
+
}
|
|
18
|
+
const waveMap = groupByWave(state.tasks);
|
|
19
|
+
// Convert Map to plain object for JSON serialization
|
|
20
|
+
const waves: Record<string, readonly unknown[]> = {};
|
|
21
|
+
for (const [wave, tasks] of waveMap) {
|
|
22
|
+
waves[String(wave)] = tasks;
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify({ waves });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case "status-count": {
|
|
28
|
+
const state = await loadState(artifactDir);
|
|
29
|
+
if (state === null) {
|
|
30
|
+
return JSON.stringify({ error: "no_state" });
|
|
31
|
+
}
|
|
32
|
+
const counts = countByStatus(state.tasks);
|
|
33
|
+
return JSON.stringify(counts);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
default:
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
error: `unknown subcommand: ${args.subcommand}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch (error: unknown) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
return JSON.stringify({ error: message });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const ocPlan = tool({
|
|
48
|
+
description:
|
|
49
|
+
"Query orchestrator plan data. Subcommands: waves (tasks grouped by wave), status-count (task counts by status).",
|
|
50
|
+
args: {
|
|
51
|
+
subcommand: tool.schema.enum(["waves", "status-count"]).describe("Operation to perform"),
|
|
52
|
+
},
|
|
53
|
+
async execute(args) {
|
|
54
|
+
return planCore(args, getProjectArtifactDir(process.cwd()));
|
|
55
|
+
},
|
|
56
|
+
});
|