@kodrunhq/opencode-autopilot 1.14.1 → 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 +85 -8
- 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
package/src/tools/orchestrate.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { tool } from "@opencode-ai/plugin";
|
|
4
|
+
import { parseResultEnvelope } from "../orchestrator/contracts/legacy-result-adapter";
|
|
5
|
+
import type { ResultEnvelope } from "../orchestrator/contracts/result-envelope";
|
|
3
6
|
import { PHASE_HANDLERS } from "../orchestrator/handlers/index";
|
|
4
|
-
import type { DispatchResult } from "../orchestrator/handlers/types";
|
|
7
|
+
import type { DispatchResult, PhaseHandlerContext } from "../orchestrator/handlers/types";
|
|
5
8
|
import { buildLessonContext } from "../orchestrator/lesson-injection";
|
|
6
9
|
import { loadLessonMemory } from "../orchestrator/lesson-memory";
|
|
7
10
|
import { logOrchestrationEvent } from "../orchestrator/orchestration-logger";
|
|
8
11
|
import { completePhase, getNextPhase, PHASE_INDEX, TOTAL_PHASES } from "../orchestrator/phase";
|
|
9
12
|
import { loadAdaptiveSkillContext } from "../orchestrator/skill-injection";
|
|
10
13
|
import { createInitialState, loadState, patchState, saveState } from "../orchestrator/state";
|
|
11
|
-
import type { Phase, PipelineState } from "../orchestrator/types";
|
|
14
|
+
import type { PendingDispatch, Phase, PipelineState } from "../orchestrator/types";
|
|
12
15
|
import { isEnoentError } from "../utils/fs-helpers";
|
|
13
16
|
import { ensureGitignore } from "../utils/gitignore";
|
|
14
17
|
import { getGlobalConfigDir, getProjectArtifactDir } from "../utils/paths";
|
|
@@ -19,6 +22,180 @@ interface OrchestrateArgs {
|
|
|
19
22
|
readonly result?: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
const ORCHESTRATE_ERROR_CODES = Object.freeze({
|
|
26
|
+
INVALID_RESULT: "E_INVALID_RESULT",
|
|
27
|
+
STALE_RESULT: "E_STALE_RESULT",
|
|
28
|
+
PHASE_MISMATCH: "E_PHASE_MISMATCH",
|
|
29
|
+
UNKNOWN_DISPATCH: "E_UNKNOWN_DISPATCH",
|
|
30
|
+
DUPLICATE_RESULT: "E_DUPLICATE_RESULT",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function createDispatchId(): string {
|
|
34
|
+
return `dispatch_${randomBytes(6).toString("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findPendingDispatch(
|
|
38
|
+
state: Readonly<PipelineState>,
|
|
39
|
+
dispatchId: string,
|
|
40
|
+
): PendingDispatch | null {
|
|
41
|
+
return state.pendingDispatches.find((entry) => entry.dispatchId === dispatchId) ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function withPendingDispatch(
|
|
45
|
+
state: Readonly<PipelineState>,
|
|
46
|
+
entry: PendingDispatch,
|
|
47
|
+
): PipelineState {
|
|
48
|
+
return patchState(state, {
|
|
49
|
+
pendingDispatches: [...state.pendingDispatches, entry],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removePendingDispatch(state: Readonly<PipelineState>, dispatchId: string): PipelineState {
|
|
54
|
+
return patchState(state, {
|
|
55
|
+
pendingDispatches: state.pendingDispatches.filter((entry) => entry.dispatchId !== dispatchId),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function markResultProcessed(
|
|
60
|
+
state: Readonly<PipelineState>,
|
|
61
|
+
envelope: ResultEnvelope,
|
|
62
|
+
): PipelineState {
|
|
63
|
+
if (state.processedResultIds.includes(envelope.resultId)) {
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
const capped = [...state.processedResultIds, envelope.resultId];
|
|
67
|
+
const nextIds = capped.length > 5000 ? capped.slice(capped.length - 5000) : capped;
|
|
68
|
+
return patchState(state, {
|
|
69
|
+
processedResultIds: nextIds,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function asErrorJson(code: string, message: string): string {
|
|
74
|
+
return JSON.stringify({ action: "error", code, message });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function logDeterministicError(
|
|
78
|
+
artifactDir: string,
|
|
79
|
+
phase: string,
|
|
80
|
+
code: string,
|
|
81
|
+
message: string,
|
|
82
|
+
): void {
|
|
83
|
+
logOrchestrationEvent(artifactDir, {
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
phase,
|
|
86
|
+
action: "error",
|
|
87
|
+
message: `${code}: ${message}`.slice(0, 500),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inferExpectedResultKindForAgent(
|
|
92
|
+
agent?: string,
|
|
93
|
+
): "phase_output" | "task_completion" | "review_findings" {
|
|
94
|
+
if (agent === "oc-reviewer") {
|
|
95
|
+
return "review_findings";
|
|
96
|
+
}
|
|
97
|
+
if (agent === "oc-implementer") {
|
|
98
|
+
return "task_completion";
|
|
99
|
+
}
|
|
100
|
+
return "phase_output";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ensureDispatchIdentity(
|
|
104
|
+
handlerResult: DispatchResult,
|
|
105
|
+
state: Readonly<PipelineState>,
|
|
106
|
+
): DispatchResult {
|
|
107
|
+
if (handlerResult.action === "dispatch") {
|
|
108
|
+
const dispatchId = handlerResult.dispatchId ?? createDispatchId();
|
|
109
|
+
return {
|
|
110
|
+
...handlerResult,
|
|
111
|
+
dispatchId,
|
|
112
|
+
runId: state.runId,
|
|
113
|
+
expectedResultKind:
|
|
114
|
+
handlerResult.expectedResultKind ??
|
|
115
|
+
handlerResult.resultKind ??
|
|
116
|
+
inferExpectedResultKindForAgent(handlerResult.agent),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (handlerResult.action === "dispatch_multi") {
|
|
121
|
+
return {
|
|
122
|
+
...handlerResult,
|
|
123
|
+
runId: state.runId,
|
|
124
|
+
agents: handlerResult.agents?.map((entry) => ({
|
|
125
|
+
...entry,
|
|
126
|
+
dispatchId: entry.dispatchId ?? createDispatchId(),
|
|
127
|
+
resultKind: entry.resultKind ?? inferExpectedResultKindForAgent(entry.agent),
|
|
128
|
+
})),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return handlerResult;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function detectPhaseFromPending(state: Readonly<PipelineState>): Phase | null {
|
|
136
|
+
const last = state.pendingDispatches.at(-1);
|
|
137
|
+
return (last?.phase as Phase | undefined) ?? state.currentPhase;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function detectAgentFromPending(state: Readonly<PipelineState>): string | null {
|
|
141
|
+
const last = state.pendingDispatches.at(-1);
|
|
142
|
+
return last?.agent ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function detectDispatchFromPending(state: Readonly<PipelineState>): string {
|
|
146
|
+
const last = state.pendingDispatches.at(-1);
|
|
147
|
+
return last?.dispatchId ?? "legacy-dispatch";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function applyResultEnvelope(
|
|
151
|
+
state: Readonly<PipelineState>,
|
|
152
|
+
envelope: ResultEnvelope,
|
|
153
|
+
options?: { readonly allowMissingPending?: boolean },
|
|
154
|
+
): PipelineState {
|
|
155
|
+
if (state.processedResultIds.includes(envelope.resultId)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`${ORCHESTRATE_ERROR_CODES.DUPLICATE_RESULT}: duplicate resultId ${envelope.resultId}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const pending = findPendingDispatch(state, envelope.dispatchId);
|
|
162
|
+
if (pending === null) {
|
|
163
|
+
if (options?.allowMissingPending) {
|
|
164
|
+
return markResultProcessed(state, envelope);
|
|
165
|
+
}
|
|
166
|
+
throw new Error(
|
|
167
|
+
`${ORCHESTRATE_ERROR_CODES.UNKNOWN_DISPATCH}: unknown dispatchId ${envelope.dispatchId}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (pending.phase !== envelope.phase) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`${ORCHESTRATE_ERROR_CODES.PHASE_MISMATCH}: result phase ${envelope.phase} != pending ${pending.phase}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (pending.taskId !== null && envelope.taskId !== pending.taskId) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`${ORCHESTRATE_ERROR_CODES.PHASE_MISMATCH}: taskId ${String(envelope.taskId)} != pending ${pending.taskId}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const withoutPending = removePendingDispatch(state, envelope.dispatchId);
|
|
182
|
+
return markResultProcessed(withoutPending, envelope);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseErrorCode(error: unknown): { readonly code: string; readonly message: string } {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
const idx = message.indexOf(":");
|
|
188
|
+
if (idx <= 0) {
|
|
189
|
+
return { code: ORCHESTRATE_ERROR_CODES.INVALID_RESULT, message };
|
|
190
|
+
}
|
|
191
|
+
const maybeCode = message.slice(0, idx);
|
|
192
|
+
const rest = message.slice(idx + 1).trim();
|
|
193
|
+
if (!maybeCode.startsWith("E_")) {
|
|
194
|
+
return { code: ORCHESTRATE_ERROR_CODES.INVALID_RESULT, message };
|
|
195
|
+
}
|
|
196
|
+
return { code: maybeCode, message: rest.length > 0 ? rest : message };
|
|
197
|
+
}
|
|
198
|
+
|
|
22
199
|
/**
|
|
23
200
|
* Apply state updates from a DispatchResult if present, then save.
|
|
24
201
|
* Returns the updated state.
|
|
@@ -148,7 +325,11 @@ async function checkCircuitBreaker(
|
|
|
148
325
|
currentState: Readonly<PipelineState>,
|
|
149
326
|
phase: string,
|
|
150
327
|
artifactDir: string,
|
|
151
|
-
): Promise<{
|
|
328
|
+
): Promise<{
|
|
329
|
+
readonly abortMsg: string | null;
|
|
330
|
+
readonly newCount: number;
|
|
331
|
+
readonly nextState: PipelineState;
|
|
332
|
+
}> {
|
|
152
333
|
const counts = { ...(currentState.phaseDispatchCounts ?? {}) };
|
|
153
334
|
counts[phase] = (counts[phase] ?? 0) + 1;
|
|
154
335
|
const newCount = counts[phase];
|
|
@@ -162,11 +343,15 @@ async function checkCircuitBreaker(
|
|
|
162
343
|
attempt: newCount,
|
|
163
344
|
message: msg,
|
|
164
345
|
});
|
|
165
|
-
return {
|
|
346
|
+
return {
|
|
347
|
+
abortMsg: JSON.stringify({ action: "error", message: msg }),
|
|
348
|
+
newCount,
|
|
349
|
+
nextState: currentState,
|
|
350
|
+
};
|
|
166
351
|
}
|
|
167
352
|
const withCounts = patchState(currentState, { phaseDispatchCounts: counts });
|
|
168
353
|
await saveState(withCounts, artifactDir);
|
|
169
|
-
return { abortMsg: null, newCount };
|
|
354
|
+
return { abortMsg: null, newCount, nextState: withCounts };
|
|
170
355
|
}
|
|
171
356
|
|
|
172
357
|
/**
|
|
@@ -178,48 +363,95 @@ async function processHandlerResult(
|
|
|
178
363
|
state: Readonly<PipelineState>,
|
|
179
364
|
artifactDir: string,
|
|
180
365
|
): Promise<string> {
|
|
366
|
+
const normalizedResult = ensureDispatchIdentity(handlerResult, state);
|
|
367
|
+
|
|
181
368
|
// Apply state updates from handler if present
|
|
182
|
-
|
|
369
|
+
let currentState = await applyStateUpdates(state, normalizedResult, artifactDir);
|
|
183
370
|
|
|
184
|
-
switch (
|
|
185
|
-
case "error":
|
|
371
|
+
switch (normalizedResult.action) {
|
|
372
|
+
case "error": {
|
|
373
|
+
const codePrefix = normalizedResult.code ? `${normalizedResult.code}: ` : "";
|
|
374
|
+
const messageBody = normalizedResult.message ?? "Handler returned error";
|
|
186
375
|
logOrchestrationEvent(artifactDir, {
|
|
187
376
|
timestamp: new Date().toISOString(),
|
|
188
|
-
phase:
|
|
377
|
+
phase: normalizedResult.phase ?? currentState.currentPhase ?? "UNKNOWN",
|
|
189
378
|
action: "error",
|
|
190
|
-
message:
|
|
379
|
+
message: `${codePrefix}${messageBody}`.slice(0, 500),
|
|
191
380
|
});
|
|
192
|
-
return JSON.stringify(
|
|
381
|
+
return JSON.stringify(normalizedResult);
|
|
382
|
+
}
|
|
193
383
|
|
|
194
384
|
case "dispatch": {
|
|
195
385
|
// Circuit breaker
|
|
196
|
-
const phase =
|
|
197
|
-
const {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
386
|
+
const phase = normalizedResult.phase ?? currentState.currentPhase ?? "UNKNOWN";
|
|
387
|
+
const {
|
|
388
|
+
abortMsg,
|
|
389
|
+
newCount: attempt,
|
|
390
|
+
nextState,
|
|
391
|
+
} = await checkCircuitBreaker(currentState, phase, artifactDir);
|
|
202
392
|
if (abortMsg) return abortMsg;
|
|
393
|
+
currentState = nextState;
|
|
394
|
+
|
|
395
|
+
const pendingEntry: PendingDispatch = {
|
|
396
|
+
dispatchId: normalizedResult.dispatchId ?? createDispatchId(),
|
|
397
|
+
phase: phase as Phase,
|
|
398
|
+
agent: normalizedResult.agent ?? "unknown",
|
|
399
|
+
issuedAt: new Date().toISOString(),
|
|
400
|
+
resultKind: normalizedResult.expectedResultKind ?? "phase_output",
|
|
401
|
+
taskId: normalizedResult.taskId ?? null,
|
|
402
|
+
};
|
|
403
|
+
const baseRevision = currentState.stateRevision;
|
|
404
|
+
const withPendingState = withPendingDispatch(currentState, pendingEntry);
|
|
203
405
|
|
|
204
406
|
// Log the dispatch event before any inline-review or context injection
|
|
205
|
-
const progress = buildUserProgress(phase,
|
|
407
|
+
const progress = buildUserProgress(phase, normalizedResult.progress, attempt);
|
|
206
408
|
logOrchestrationEvent(artifactDir, {
|
|
207
409
|
timestamp: new Date().toISOString(),
|
|
208
410
|
phase,
|
|
209
411
|
action: "dispatch",
|
|
210
|
-
agent:
|
|
211
|
-
promptLength:
|
|
412
|
+
agent: normalizedResult.agent,
|
|
413
|
+
promptLength: normalizedResult.prompt?.length,
|
|
212
414
|
attempt,
|
|
213
415
|
});
|
|
214
416
|
|
|
215
417
|
// Check if this is a review dispatch that should be inlined
|
|
216
|
-
const { inlined, reviewResult } = await maybeInlineReview(
|
|
418
|
+
const { inlined, reviewResult } = await maybeInlineReview(normalizedResult, artifactDir);
|
|
217
419
|
if (inlined && reviewResult) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
420
|
+
if (withPendingState.currentPhase) {
|
|
421
|
+
let reviewPayloadText = reviewResult;
|
|
422
|
+
try {
|
|
423
|
+
const parsedReview = JSON.parse(reviewResult) as {
|
|
424
|
+
findingsEnvelope?: unknown;
|
|
425
|
+
};
|
|
426
|
+
if (parsedReview.findingsEnvelope) {
|
|
427
|
+
reviewPayloadText = JSON.stringify(parsedReview.findingsEnvelope);
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
// keep raw review payload for legacy parser
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const inlinedEnvelope: ResultEnvelope = {
|
|
434
|
+
schemaVersion: 1,
|
|
435
|
+
resultId: `inline-${createDispatchId()}`,
|
|
436
|
+
runId: withPendingState.runId,
|
|
437
|
+
phase: withPendingState.currentPhase,
|
|
438
|
+
dispatchId: pendingEntry.dispatchId,
|
|
439
|
+
agent: normalizedResult.agent ?? null,
|
|
440
|
+
kind: "review_findings",
|
|
441
|
+
taskId: normalizedResult.taskId ?? null,
|
|
442
|
+
payload: {
|
|
443
|
+
text: reviewPayloadText,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
const withInlineResult = applyResultEnvelope(withPendingState, inlinedEnvelope);
|
|
447
|
+
await saveState(withInlineResult, artifactDir, baseRevision);
|
|
448
|
+
|
|
449
|
+
const handler = PHASE_HANDLERS[withPendingState.currentPhase];
|
|
450
|
+
const nextResult = await handler(withInlineResult, artifactDir, reviewPayloadText, {
|
|
451
|
+
envelope: inlinedEnvelope,
|
|
452
|
+
legacy: false,
|
|
453
|
+
});
|
|
454
|
+
return processHandlerResult(nextResult, withInlineResult, artifactDir);
|
|
223
455
|
}
|
|
224
456
|
// State unavailable or pipeline completed after inline review — return complete
|
|
225
457
|
return JSON.stringify({
|
|
@@ -228,67 +460,105 @@ async function processHandlerResult(
|
|
|
228
460
|
_userProgress: progress,
|
|
229
461
|
});
|
|
230
462
|
}
|
|
463
|
+
|
|
464
|
+
await saveState(withPendingState, artifactDir, baseRevision);
|
|
465
|
+
currentState = withPendingState;
|
|
466
|
+
|
|
231
467
|
// Inject lesson + skill context into dispatch prompt (best-effort)
|
|
232
|
-
if (
|
|
468
|
+
if (normalizedResult.prompt && normalizedResult.phase) {
|
|
233
469
|
const enrichedPrompt = await injectLessonContext(
|
|
234
|
-
|
|
235
|
-
|
|
470
|
+
normalizedResult.prompt,
|
|
471
|
+
normalizedResult.phase,
|
|
236
472
|
artifactDir,
|
|
237
473
|
);
|
|
238
474
|
const withSkills = await injectSkillContext(
|
|
239
475
|
enrichedPrompt,
|
|
240
476
|
join(artifactDir, ".."),
|
|
241
|
-
|
|
477
|
+
normalizedResult.phase,
|
|
242
478
|
);
|
|
243
|
-
if (withSkills !==
|
|
244
|
-
return JSON.stringify({
|
|
479
|
+
if (withSkills !== normalizedResult.prompt) {
|
|
480
|
+
return JSON.stringify({
|
|
481
|
+
...normalizedResult,
|
|
482
|
+
prompt: withSkills,
|
|
483
|
+
dispatchId: pendingEntry.dispatchId,
|
|
484
|
+
runId: currentState.runId,
|
|
485
|
+
_userProgress: progress,
|
|
486
|
+
});
|
|
245
487
|
}
|
|
246
488
|
}
|
|
247
|
-
return JSON.stringify({
|
|
489
|
+
return JSON.stringify({
|
|
490
|
+
...normalizedResult,
|
|
491
|
+
dispatchId: pendingEntry.dispatchId,
|
|
492
|
+
runId: currentState.runId,
|
|
493
|
+
_userProgress: progress,
|
|
494
|
+
});
|
|
248
495
|
}
|
|
249
496
|
|
|
250
497
|
case "dispatch_multi": {
|
|
251
498
|
// Circuit breaker
|
|
252
|
-
const phase =
|
|
253
|
-
const {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
499
|
+
const phase = normalizedResult.phase ?? currentState.currentPhase ?? "UNKNOWN";
|
|
500
|
+
const {
|
|
501
|
+
abortMsg,
|
|
502
|
+
newCount: attempt,
|
|
503
|
+
nextState,
|
|
504
|
+
} = await checkCircuitBreaker(currentState, phase, artifactDir);
|
|
258
505
|
if (abortMsg) return abortMsg;
|
|
506
|
+
currentState = nextState;
|
|
507
|
+
|
|
508
|
+
const pendingEntries: readonly PendingDispatch[] =
|
|
509
|
+
normalizedResult.agents?.map((entry) => ({
|
|
510
|
+
dispatchId: entry.dispatchId ?? createDispatchId(),
|
|
511
|
+
phase: phase as Phase,
|
|
512
|
+
agent: entry.agent,
|
|
513
|
+
issuedAt: new Date().toISOString(),
|
|
514
|
+
resultKind: entry.resultKind ?? inferExpectedResultKindForAgent(entry.agent),
|
|
515
|
+
taskId: entry.taskId ?? null,
|
|
516
|
+
})) ?? [];
|
|
517
|
+
const baseRevision = currentState.stateRevision;
|
|
518
|
+
let withPendingState = currentState;
|
|
519
|
+
for (const entry of pendingEntries) {
|
|
520
|
+
withPendingState = withPendingDispatch(withPendingState, entry);
|
|
521
|
+
}
|
|
259
522
|
|
|
260
|
-
const progress = buildUserProgress(phase,
|
|
523
|
+
const progress = buildUserProgress(phase, normalizedResult.progress, attempt);
|
|
261
524
|
logOrchestrationEvent(artifactDir, {
|
|
262
525
|
timestamp: new Date().toISOString(),
|
|
263
526
|
phase,
|
|
264
527
|
action: "dispatch_multi",
|
|
265
|
-
agent: `${
|
|
528
|
+
agent: `${normalizedResult.agents?.length ?? 0} agents`,
|
|
266
529
|
attempt,
|
|
267
530
|
});
|
|
531
|
+
await saveState(withPendingState, artifactDir, baseRevision);
|
|
532
|
+
currentState = withPendingState;
|
|
268
533
|
|
|
269
534
|
// Inject lesson + skill context into each agent's prompt (best-effort)
|
|
270
535
|
// Load lesson and skill context once and reuse for all agents in the batch
|
|
271
|
-
if (
|
|
272
|
-
const lessonSuffix = await injectLessonContext("",
|
|
536
|
+
if (normalizedResult.agents && normalizedResult.phase) {
|
|
537
|
+
const lessonSuffix = await injectLessonContext("", normalizedResult.phase, artifactDir);
|
|
273
538
|
const skillSuffix = await injectSkillContext(
|
|
274
539
|
"",
|
|
275
540
|
join(artifactDir, ".."),
|
|
276
|
-
|
|
541
|
+
normalizedResult.phase,
|
|
277
542
|
);
|
|
278
543
|
const combinedSuffix = lessonSuffix + (skillSuffix || "");
|
|
279
544
|
if (combinedSuffix) {
|
|
280
|
-
const enrichedAgents =
|
|
545
|
+
const enrichedAgents = normalizedResult.agents.map((entry) => ({
|
|
281
546
|
...entry,
|
|
282
547
|
prompt: entry.prompt + combinedSuffix,
|
|
283
548
|
}));
|
|
284
549
|
return JSON.stringify({
|
|
285
|
-
...
|
|
550
|
+
...normalizedResult,
|
|
286
551
|
agents: enrichedAgents,
|
|
552
|
+
runId: currentState.runId,
|
|
287
553
|
_userProgress: progress,
|
|
288
554
|
});
|
|
289
555
|
}
|
|
290
556
|
}
|
|
291
|
-
return JSON.stringify({
|
|
557
|
+
return JSON.stringify({
|
|
558
|
+
...normalizedResult,
|
|
559
|
+
runId: currentState.runId,
|
|
560
|
+
_userProgress: progress,
|
|
561
|
+
});
|
|
292
562
|
}
|
|
293
563
|
|
|
294
564
|
case "complete": {
|
|
@@ -306,7 +576,6 @@ async function processHandlerResult(
|
|
|
306
576
|
});
|
|
307
577
|
const nextPhase = getNextPhase(currentState.currentPhase);
|
|
308
578
|
const advanced = completePhase(currentState);
|
|
309
|
-
await saveState(advanced, artifactDir);
|
|
310
579
|
|
|
311
580
|
if (nextPhase === null) {
|
|
312
581
|
// Terminal phase completed
|
|
@@ -320,9 +589,11 @@ async function processHandlerResult(
|
|
|
320
589
|
});
|
|
321
590
|
}
|
|
322
591
|
|
|
592
|
+
await saveState(advanced, artifactDir);
|
|
593
|
+
|
|
323
594
|
// Invoke the next phase handler immediately
|
|
324
595
|
const nextHandler = PHASE_HANDLERS[nextPhase];
|
|
325
|
-
const nextResult = await nextHandler(advanced, artifactDir);
|
|
596
|
+
const nextResult = await nextHandler(advanced, artifactDir, undefined, undefined);
|
|
326
597
|
return processHandlerResult(nextResult, advanced, artifactDir);
|
|
327
598
|
}
|
|
328
599
|
|
|
@@ -336,7 +607,7 @@ async function processHandlerResult(
|
|
|
336
607
|
|
|
337
608
|
export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string): Promise<string> {
|
|
338
609
|
try {
|
|
339
|
-
|
|
610
|
+
let state = await loadState(artifactDir);
|
|
340
611
|
|
|
341
612
|
// No state and no idea -> error
|
|
342
613
|
if (state === null && !args.idea) {
|
|
@@ -360,13 +631,15 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
360
631
|
}
|
|
361
632
|
|
|
362
633
|
const handler = PHASE_HANDLERS[newState.currentPhase as Phase];
|
|
363
|
-
const handlerResult = await handler(newState, artifactDir);
|
|
634
|
+
const handlerResult = await handler(newState, artifactDir, undefined, undefined);
|
|
364
635
|
return processHandlerResult(handlerResult, newState, artifactDir);
|
|
365
636
|
}
|
|
366
637
|
|
|
367
638
|
// State exists
|
|
368
639
|
if (state !== null) {
|
|
369
|
-
|
|
640
|
+
let phaseHandlerContext: PhaseHandlerContext | undefined;
|
|
641
|
+
let handlerInputResult = args.result;
|
|
642
|
+
|
|
370
643
|
if (state.currentPhase === null) {
|
|
371
644
|
return JSON.stringify({
|
|
372
645
|
action: "complete",
|
|
@@ -374,15 +647,106 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
374
647
|
});
|
|
375
648
|
}
|
|
376
649
|
|
|
650
|
+
if (typeof args.result === "string") {
|
|
651
|
+
const phaseHint = detectPhaseFromPending(state);
|
|
652
|
+
if (phaseHint === null) {
|
|
653
|
+
const msg = "Received result but no pending dispatch exists.";
|
|
654
|
+
logDeterministicError(
|
|
655
|
+
artifactDir,
|
|
656
|
+
state.currentPhase ?? "UNKNOWN",
|
|
657
|
+
ORCHESTRATE_ERROR_CODES.STALE_RESULT,
|
|
658
|
+
msg,
|
|
659
|
+
);
|
|
660
|
+
return asErrorJson(ORCHESTRATE_ERROR_CODES.STALE_RESULT, msg);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
const parsed = parseResultEnvelope(args.result, {
|
|
665
|
+
runId: state.runId,
|
|
666
|
+
phase: phaseHint,
|
|
667
|
+
fallbackDispatchId: detectDispatchFromPending(state),
|
|
668
|
+
fallbackAgent: detectAgentFromPending(state),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
if (parsed.envelope.runId !== state.runId) {
|
|
672
|
+
const msg = `Result runId ${parsed.envelope.runId} does not match active run ${state.runId}.`;
|
|
673
|
+
logDeterministicError(
|
|
674
|
+
artifactDir,
|
|
675
|
+
state.currentPhase ?? phaseHint,
|
|
676
|
+
ORCHESTRATE_ERROR_CODES.STALE_RESULT,
|
|
677
|
+
msg,
|
|
678
|
+
);
|
|
679
|
+
return asErrorJson(ORCHESTRATE_ERROR_CODES.STALE_RESULT, msg);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (parsed.legacy && state.pendingDispatches.length > 1) {
|
|
683
|
+
const msg =
|
|
684
|
+
"Legacy result payload cannot be attributed with multiple pending dispatches. Provide typed envelope with dispatchId/taskId.";
|
|
685
|
+
logDeterministicError(
|
|
686
|
+
artifactDir,
|
|
687
|
+
state.currentPhase ?? phaseHint,
|
|
688
|
+
ORCHESTRATE_ERROR_CODES.INVALID_RESULT,
|
|
689
|
+
msg,
|
|
690
|
+
);
|
|
691
|
+
return asErrorJson(ORCHESTRATE_ERROR_CODES.INVALID_RESULT, msg);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const allowMissingPending = parsed.legacy && state.pendingDispatches.length === 0;
|
|
695
|
+
const nextState = applyResultEnvelope(state, parsed.envelope, {
|
|
696
|
+
allowMissingPending,
|
|
697
|
+
});
|
|
698
|
+
await saveState(nextState, artifactDir);
|
|
699
|
+
state = nextState;
|
|
700
|
+
if (!allowMissingPending && parsed.legacy) {
|
|
701
|
+
const legacyMsg =
|
|
702
|
+
"Legacy result parser path used. Submit typed envelopes for deterministic replay guarantees.";
|
|
703
|
+
logOrchestrationEvent(artifactDir, {
|
|
704
|
+
timestamp: new Date().toISOString(),
|
|
705
|
+
phase: state.currentPhase ?? phaseHint,
|
|
706
|
+
action: "error",
|
|
707
|
+
message: legacyMsg,
|
|
708
|
+
});
|
|
709
|
+
console.warn(`[opencode-autopilot] ${legacyMsg}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
phaseHandlerContext = {
|
|
713
|
+
envelope: parsed.envelope,
|
|
714
|
+
legacy: parsed.legacy,
|
|
715
|
+
};
|
|
716
|
+
handlerInputResult = parsed.envelope.payload.text;
|
|
717
|
+
} catch (error: unknown) {
|
|
718
|
+
const parsedErr = parseErrorCode(error);
|
|
719
|
+
logOrchestrationEvent(artifactDir, {
|
|
720
|
+
timestamp: new Date().toISOString(),
|
|
721
|
+
phase: state.currentPhase ?? "UNKNOWN",
|
|
722
|
+
action: "error",
|
|
723
|
+
message: `${parsedErr.code}: ${parsedErr.message}`,
|
|
724
|
+
});
|
|
725
|
+
return asErrorJson(parsedErr.code, parsedErr.message);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
377
729
|
// Delegate to current phase handler
|
|
730
|
+
if (state.currentPhase === null) {
|
|
731
|
+
return JSON.stringify({
|
|
732
|
+
action: "complete",
|
|
733
|
+
summary: `Pipeline already completed. Idea: ${state.idea}`,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
378
736
|
const handler = PHASE_HANDLERS[state.currentPhase];
|
|
379
|
-
const handlerResult = await handler(
|
|
737
|
+
const handlerResult = await handler(
|
|
738
|
+
state,
|
|
739
|
+
artifactDir,
|
|
740
|
+
handlerInputResult,
|
|
741
|
+
phaseHandlerContext,
|
|
742
|
+
);
|
|
380
743
|
return processHandlerResult(handlerResult, state, artifactDir);
|
|
381
744
|
}
|
|
382
745
|
|
|
383
746
|
return JSON.stringify({ action: "error", message: "Unexpected state" });
|
|
384
747
|
} catch (error: unknown) {
|
|
385
748
|
const message = error instanceof Error ? error.message : String(error);
|
|
749
|
+
const parsedErr = parseErrorCode(error);
|
|
386
750
|
const safeMessage = message.replace(/[/\\][^\s"']+/g, "[PATH]").slice(0, 4096);
|
|
387
751
|
|
|
388
752
|
// Persist failure metadata for forensics (best-effort)
|
|
@@ -407,7 +771,7 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
407
771
|
// Swallow save errors -- original error takes priority
|
|
408
772
|
}
|
|
409
773
|
|
|
410
|
-
return JSON.stringify({ action: "error", message: safeMessage });
|
|
774
|
+
return JSON.stringify({ action: "error", code: parsedErr.code, message: safeMessage });
|
|
411
775
|
}
|
|
412
776
|
}
|
|
413
777
|
|
package/src/tools/quick.ts
CHANGED
|
@@ -49,6 +49,8 @@ export async function quickCore(args: QuickArgs, artifactDir: string): Promise<s
|
|
|
49
49
|
const quickState = pipelineStateSchema.parse({
|
|
50
50
|
schemaVersion: 2,
|
|
51
51
|
status: "IN_PROGRESS",
|
|
52
|
+
runId: `quick-${Date.now()}`,
|
|
53
|
+
stateRevision: 0,
|
|
52
54
|
idea: args.idea,
|
|
53
55
|
currentPhase: "PLAN",
|
|
54
56
|
startedAt: now,
|
|
@@ -71,6 +73,8 @@ export async function quickCore(args: QuickArgs, artifactDir: string): Promise<s
|
|
|
71
73
|
tasks: [],
|
|
72
74
|
arenaConfidence: null,
|
|
73
75
|
exploreTriggered: false,
|
|
76
|
+
pendingDispatches: [],
|
|
77
|
+
processedResultIds: [],
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
// 4. Persist quick state to disk
|