@kodrunhq/opencode-autopilot 1.15.2 → 1.16.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/bin/cli.ts +5 -0
- package/bin/inspect.ts +337 -0
- package/package.json +1 -1
- package/src/agents/autopilot.ts +7 -15
- package/src/health/checks.ts +29 -4
- package/src/index.ts +103 -11
- package/src/inspect/formatters.ts +225 -0
- package/src/inspect/repository.ts +882 -0
- package/src/kernel/database.ts +45 -0
- package/src/kernel/migrations.ts +62 -0
- package/src/kernel/repository.ts +571 -0
- package/src/kernel/schema.ts +122 -0
- package/src/kernel/types.ts +66 -0
- package/src/memory/capture.ts +221 -25
- package/src/memory/database.ts +74 -12
- package/src/memory/index.ts +17 -1
- package/src/memory/project-key.ts +6 -0
- package/src/memory/repository.ts +833 -42
- package/src/memory/retrieval.ts +83 -169
- package/src/memory/schemas.ts +39 -7
- package/src/memory/types.ts +4 -0
- package/src/observability/event-handlers.ts +28 -17
- package/src/observability/event-store.ts +29 -1
- package/src/observability/forensic-log.ts +159 -0
- package/src/observability/forensic-schemas.ts +69 -0
- package/src/observability/forensic-types.ts +10 -0
- package/src/observability/index.ts +21 -27
- package/src/observability/log-reader.ts +142 -111
- package/src/observability/log-writer.ts +41 -83
- package/src/observability/retention.ts +2 -2
- package/src/observability/session-logger.ts +36 -57
- package/src/observability/summary-generator.ts +31 -19
- package/src/observability/types.ts +12 -24
- package/src/orchestrator/contracts/invariants.ts +14 -0
- package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
- package/src/orchestrator/fallback/event-handler.ts +47 -3
- package/src/orchestrator/handlers/architect.ts +2 -1
- package/src/orchestrator/handlers/build.ts +55 -97
- package/src/orchestrator/handlers/retrospective.ts +2 -1
- package/src/orchestrator/handlers/types.ts +0 -1
- package/src/orchestrator/lesson-memory.ts +29 -9
- package/src/orchestrator/orchestration-logger.ts +37 -23
- package/src/orchestrator/phase.ts +8 -4
- package/src/orchestrator/state.ts +79 -17
- package/src/projects/database.ts +47 -0
- package/src/projects/repository.ts +264 -0
- package/src/projects/resolve.ts +301 -0
- package/src/projects/schemas.ts +30 -0
- package/src/projects/types.ts +12 -0
- package/src/review/memory.ts +29 -9
- package/src/tools/doctor.ts +26 -2
- package/src/tools/forensics.ts +7 -12
- package/src/tools/logs.ts +6 -5
- package/src/tools/memory-preferences.ts +157 -0
- package/src/tools/memory-status.ts +17 -96
- package/src/tools/orchestrate.ts +97 -81
- package/src/tools/pipeline-report.ts +3 -2
- package/src/tools/quick.ts +2 -2
- package/src/tools/review.ts +39 -6
- package/src/tools/session-stats.ts +3 -2
- package/src/utils/paths.ts +20 -1
package/src/tools/orchestrate.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { tool } from "@opencode-ai/plugin";
|
|
4
|
-
import {
|
|
5
|
-
import type { ResultEnvelope } from "../orchestrator/contracts/result-envelope";
|
|
4
|
+
import { parseTypedResultEnvelope } from "../orchestrator/contracts/legacy-result-adapter";
|
|
5
|
+
import type { PendingDispatch, ResultEnvelope } from "../orchestrator/contracts/result-envelope";
|
|
6
6
|
import { PHASE_HANDLERS } from "../orchestrator/handlers/index";
|
|
7
7
|
import type { DispatchResult, PhaseHandlerContext } from "../orchestrator/handlers/types";
|
|
8
8
|
import { buildLessonContext } from "../orchestrator/lesson-injection";
|
|
@@ -10,11 +10,22 @@ import { loadLessonMemory } from "../orchestrator/lesson-memory";
|
|
|
10
10
|
import { logOrchestrationEvent } from "../orchestrator/orchestration-logger";
|
|
11
11
|
import { completePhase, getNextPhase, PHASE_INDEX, TOTAL_PHASES } from "../orchestrator/phase";
|
|
12
12
|
import { loadAdaptiveSkillContext } from "../orchestrator/skill-injection";
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
createInitialState,
|
|
15
|
+
isStateConflictError,
|
|
16
|
+
loadState,
|
|
17
|
+
patchState,
|
|
18
|
+
saveState,
|
|
19
|
+
updatePersistedState,
|
|
20
|
+
} from "../orchestrator/state";
|
|
21
|
+
import type { Phase, PipelineState } from "../orchestrator/types";
|
|
15
22
|
import { isEnoentError } from "../utils/fs-helpers";
|
|
16
23
|
import { ensureGitignore } from "../utils/gitignore";
|
|
17
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getGlobalConfigDir,
|
|
26
|
+
getProjectArtifactDir,
|
|
27
|
+
getProjectRootFromArtifactDir,
|
|
28
|
+
} from "../utils/paths";
|
|
18
29
|
import { reviewCore } from "./review";
|
|
19
30
|
|
|
20
31
|
interface OrchestrateArgs {
|
|
@@ -28,6 +39,8 @@ const ORCHESTRATE_ERROR_CODES = Object.freeze({
|
|
|
28
39
|
PHASE_MISMATCH: "E_PHASE_MISMATCH",
|
|
29
40
|
UNKNOWN_DISPATCH: "E_UNKNOWN_DISPATCH",
|
|
30
41
|
DUPLICATE_RESULT: "E_DUPLICATE_RESULT",
|
|
42
|
+
PENDING_RESULT_REQUIRED: "E_PENDING_RESULT_REQUIRED",
|
|
43
|
+
RESULT_KIND_MISMATCH: "E_RESULT_KIND_MISMATCH",
|
|
31
44
|
});
|
|
32
45
|
|
|
33
46
|
function createDispatchId(): string {
|
|
@@ -56,6 +69,10 @@ function removePendingDispatch(state: Readonly<PipelineState>, dispatchId: strin
|
|
|
56
69
|
});
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
function expectedResultKindForPending(pending: Readonly<PendingDispatch>): string {
|
|
73
|
+
return pending.resultKind;
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
function markResultProcessed(
|
|
60
77
|
state: Readonly<PipelineState>,
|
|
61
78
|
envelope: ResultEnvelope,
|
|
@@ -172,6 +189,11 @@ function applyResultEnvelope(
|
|
|
172
189
|
`${ORCHESTRATE_ERROR_CODES.PHASE_MISMATCH}: result phase ${envelope.phase} != pending ${pending.phase}`,
|
|
173
190
|
);
|
|
174
191
|
}
|
|
192
|
+
if (expectedResultKindForPending(pending) !== envelope.kind) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`${ORCHESTRATE_ERROR_CODES.RESULT_KIND_MISMATCH}: result kind ${envelope.kind} != pending ${pending.resultKind}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
175
197
|
if (pending.taskId !== null && envelope.taskId !== pending.taskId) {
|
|
176
198
|
throw new Error(
|
|
177
199
|
`${ORCHESTRATE_ERROR_CODES.PHASE_MISMATCH}: taskId ${String(envelope.taskId)} != pending ${pending.taskId}`,
|
|
@@ -207,9 +229,7 @@ async function applyStateUpdates(
|
|
|
207
229
|
): Promise<PipelineState> {
|
|
208
230
|
const updates = handlerResult._stateUpdates;
|
|
209
231
|
if (updates) {
|
|
210
|
-
|
|
211
|
-
await saveState(updated, artifactDir);
|
|
212
|
-
return updated;
|
|
232
|
+
return updatePersistedState(artifactDir, state, (current) => patchState(current, updates));
|
|
213
233
|
}
|
|
214
234
|
return state;
|
|
215
235
|
}
|
|
@@ -228,7 +248,7 @@ async function maybeInlineReview(
|
|
|
228
248
|
handlerResult.agent === "oc-reviewer" &&
|
|
229
249
|
handlerResult.prompt
|
|
230
250
|
) {
|
|
231
|
-
const projectRoot =
|
|
251
|
+
const projectRoot = getProjectRootFromArtifactDir(artifactDir);
|
|
232
252
|
const reviewResult = await reviewCore({ scope: "branch" }, projectRoot);
|
|
233
253
|
return { inlined: true, reviewResult };
|
|
234
254
|
}
|
|
@@ -245,7 +265,7 @@ async function injectLessonContext(
|
|
|
245
265
|
artifactDir: string,
|
|
246
266
|
): Promise<string> {
|
|
247
267
|
try {
|
|
248
|
-
const projectRoot =
|
|
268
|
+
const projectRoot = getProjectRootFromArtifactDir(artifactDir);
|
|
249
269
|
const memory = await loadLessonMemory(projectRoot);
|
|
250
270
|
if (memory && memory.lessons.length > 0) {
|
|
251
271
|
const ctx = buildLessonContext(memory.lessons, phase as Phase);
|
|
@@ -332,26 +352,33 @@ async function checkCircuitBreaker(
|
|
|
332
352
|
}> {
|
|
333
353
|
const counts = { ...(currentState.phaseDispatchCounts ?? {}) };
|
|
334
354
|
counts[phase] = (counts[phase] ?? 0) + 1;
|
|
335
|
-
const
|
|
355
|
+
const candidateCount = counts[phase];
|
|
336
356
|
const limit = MAX_PHASE_DISPATCHES[phase] ?? 5;
|
|
337
|
-
if (
|
|
338
|
-
const msg = `Phase ${phase} exceeded max dispatches (${
|
|
357
|
+
if (candidateCount > limit) {
|
|
358
|
+
const msg = `Phase ${phase} exceeded max dispatches (${candidateCount}/${limit}) — possible infinite loop detected. Aborting.`;
|
|
339
359
|
logOrchestrationEvent(artifactDir, {
|
|
340
360
|
timestamp: new Date().toISOString(),
|
|
341
361
|
phase,
|
|
342
362
|
action: "loop_detected",
|
|
343
|
-
attempt:
|
|
363
|
+
attempt: candidateCount,
|
|
344
364
|
message: msg,
|
|
345
365
|
});
|
|
346
366
|
return {
|
|
347
367
|
abortMsg: JSON.stringify({ action: "error", message: msg }),
|
|
348
|
-
newCount,
|
|
368
|
+
newCount: candidateCount,
|
|
349
369
|
nextState: currentState,
|
|
350
370
|
};
|
|
351
371
|
}
|
|
352
|
-
const withCounts =
|
|
353
|
-
|
|
354
|
-
|
|
372
|
+
const withCounts = await updatePersistedState(artifactDir, currentState, (current) => {
|
|
373
|
+
const nextCounts = { ...(current.phaseDispatchCounts ?? {}) };
|
|
374
|
+
nextCounts[phase] = (nextCounts[phase] ?? 0) + 1;
|
|
375
|
+
return patchState(current, { phaseDispatchCounts: nextCounts });
|
|
376
|
+
});
|
|
377
|
+
return {
|
|
378
|
+
abortMsg: null,
|
|
379
|
+
newCount: withCounts.phaseDispatchCounts[phase] ?? candidateCount,
|
|
380
|
+
nextState: withCounts,
|
|
381
|
+
};
|
|
355
382
|
}
|
|
356
383
|
|
|
357
384
|
/**
|
|
@@ -400,8 +427,6 @@ async function processHandlerResult(
|
|
|
400
427
|
resultKind: normalizedResult.expectedResultKind ?? "phase_output",
|
|
401
428
|
taskId: normalizedResult.taskId ?? null,
|
|
402
429
|
};
|
|
403
|
-
const baseRevision = currentState.stateRevision;
|
|
404
|
-
const withPendingState = withPendingDispatch(currentState, pendingEntry);
|
|
405
430
|
|
|
406
431
|
// Log the dispatch event before any inline-review or context injection
|
|
407
432
|
const progress = buildUserProgress(phase, normalizedResult.progress, attempt);
|
|
@@ -417,7 +442,7 @@ async function processHandlerResult(
|
|
|
417
442
|
// Check if this is a review dispatch that should be inlined
|
|
418
443
|
const { inlined, reviewResult } = await maybeInlineReview(normalizedResult, artifactDir);
|
|
419
444
|
if (inlined && reviewResult) {
|
|
420
|
-
if (
|
|
445
|
+
if (currentState.currentPhase) {
|
|
421
446
|
let reviewPayloadText = reviewResult;
|
|
422
447
|
try {
|
|
423
448
|
const parsedReview = JSON.parse(reviewResult) as {
|
|
@@ -433,8 +458,8 @@ async function processHandlerResult(
|
|
|
433
458
|
const inlinedEnvelope: ResultEnvelope = {
|
|
434
459
|
schemaVersion: 1,
|
|
435
460
|
resultId: `inline-${createDispatchId()}`,
|
|
436
|
-
runId:
|
|
437
|
-
phase:
|
|
461
|
+
runId: currentState.runId,
|
|
462
|
+
phase: currentState.currentPhase,
|
|
438
463
|
dispatchId: pendingEntry.dispatchId,
|
|
439
464
|
agent: normalizedResult.agent ?? null,
|
|
440
465
|
kind: "review_findings",
|
|
@@ -443,13 +468,16 @@ async function processHandlerResult(
|
|
|
443
468
|
text: reviewPayloadText,
|
|
444
469
|
},
|
|
445
470
|
};
|
|
446
|
-
const withInlineResult =
|
|
447
|
-
|
|
471
|
+
const withInlineResult = await updatePersistedState(
|
|
472
|
+
artifactDir,
|
|
473
|
+
currentState,
|
|
474
|
+
(current) =>
|
|
475
|
+
applyResultEnvelope(withPendingDispatch(current, pendingEntry), inlinedEnvelope),
|
|
476
|
+
);
|
|
448
477
|
|
|
449
|
-
const handler = PHASE_HANDLERS[
|
|
478
|
+
const handler = PHASE_HANDLERS[currentState.currentPhase];
|
|
450
479
|
const nextResult = await handler(withInlineResult, artifactDir, reviewPayloadText, {
|
|
451
480
|
envelope: inlinedEnvelope,
|
|
452
|
-
legacy: false,
|
|
453
481
|
});
|
|
454
482
|
return processHandlerResult(nextResult, withInlineResult, artifactDir);
|
|
455
483
|
}
|
|
@@ -461,8 +489,9 @@ async function processHandlerResult(
|
|
|
461
489
|
});
|
|
462
490
|
}
|
|
463
491
|
|
|
464
|
-
await
|
|
465
|
-
|
|
492
|
+
currentState = await updatePersistedState(artifactDir, currentState, (current) =>
|
|
493
|
+
withPendingDispatch(current, pendingEntry),
|
|
494
|
+
);
|
|
466
495
|
|
|
467
496
|
// Inject lesson + skill context into dispatch prompt (best-effort)
|
|
468
497
|
if (normalizedResult.prompt && normalizedResult.phase) {
|
|
@@ -473,7 +502,7 @@ async function processHandlerResult(
|
|
|
473
502
|
);
|
|
474
503
|
const withSkills = await injectSkillContext(
|
|
475
504
|
enrichedPrompt,
|
|
476
|
-
|
|
505
|
+
getProjectRootFromArtifactDir(artifactDir),
|
|
477
506
|
normalizedResult.phase,
|
|
478
507
|
);
|
|
479
508
|
if (withSkills !== normalizedResult.prompt) {
|
|
@@ -514,11 +543,6 @@ async function processHandlerResult(
|
|
|
514
543
|
resultKind: entry.resultKind ?? inferExpectedResultKindForAgent(entry.agent),
|
|
515
544
|
taskId: entry.taskId ?? null,
|
|
516
545
|
})) ?? [];
|
|
517
|
-
const baseRevision = currentState.stateRevision;
|
|
518
|
-
let withPendingState = currentState;
|
|
519
|
-
for (const entry of pendingEntries) {
|
|
520
|
-
withPendingState = withPendingDispatch(withPendingState, entry);
|
|
521
|
-
}
|
|
522
546
|
|
|
523
547
|
const progress = buildUserProgress(phase, normalizedResult.progress, attempt);
|
|
524
548
|
logOrchestrationEvent(artifactDir, {
|
|
@@ -528,8 +552,13 @@ async function processHandlerResult(
|
|
|
528
552
|
agent: `${normalizedResult.agents?.length ?? 0} agents`,
|
|
529
553
|
attempt,
|
|
530
554
|
});
|
|
531
|
-
await
|
|
532
|
-
|
|
555
|
+
currentState = await updatePersistedState(artifactDir, currentState, (current) => {
|
|
556
|
+
let nextState = current;
|
|
557
|
+
for (const entry of pendingEntries) {
|
|
558
|
+
nextState = withPendingDispatch(nextState, entry);
|
|
559
|
+
}
|
|
560
|
+
return nextState;
|
|
561
|
+
});
|
|
533
562
|
|
|
534
563
|
// Inject lesson + skill context into each agent's prompt (best-effort)
|
|
535
564
|
// Load lesson and skill context once and reuse for all agents in the batch
|
|
@@ -537,7 +566,7 @@ async function processHandlerResult(
|
|
|
537
566
|
const lessonSuffix = await injectLessonContext("", normalizedResult.phase, artifactDir);
|
|
538
567
|
const skillSuffix = await injectSkillContext(
|
|
539
568
|
"",
|
|
540
|
-
|
|
569
|
+
getProjectRootFromArtifactDir(artifactDir),
|
|
541
570
|
normalizedResult.phase,
|
|
542
571
|
);
|
|
543
572
|
const combinedSuffix = lessonSuffix + (skillSuffix || "");
|
|
@@ -575,12 +604,11 @@ async function processHandlerResult(
|
|
|
575
604
|
action: "complete",
|
|
576
605
|
});
|
|
577
606
|
const nextPhase = getNextPhase(currentState.currentPhase);
|
|
578
|
-
const advanced =
|
|
607
|
+
const advanced = await updatePersistedState(artifactDir, currentState, (current) =>
|
|
608
|
+
completePhase(current),
|
|
609
|
+
);
|
|
579
610
|
|
|
580
611
|
if (nextPhase === null) {
|
|
581
|
-
// Terminal phase completed
|
|
582
|
-
const finished = { ...advanced, status: "COMPLETED" as const };
|
|
583
|
-
await saveState(finished, artifactDir);
|
|
584
612
|
const idx = PHASE_INDEX[currentState.currentPhase] ?? TOTAL_PHASES;
|
|
585
613
|
return JSON.stringify({
|
|
586
614
|
action: "complete",
|
|
@@ -589,8 +617,6 @@ async function processHandlerResult(
|
|
|
589
617
|
});
|
|
590
618
|
}
|
|
591
619
|
|
|
592
|
-
await saveState(advanced, artifactDir);
|
|
593
|
-
|
|
594
620
|
// Invoke the next phase handler immediately
|
|
595
621
|
const nextHandler = PHASE_HANDLERS[nextPhase];
|
|
596
622
|
const nextResult = await nextHandler(advanced, artifactDir, undefined, undefined);
|
|
@@ -624,7 +650,7 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
624
650
|
|
|
625
651
|
// Best-effort .gitignore update
|
|
626
652
|
try {
|
|
627
|
-
const projectRoot =
|
|
653
|
+
const projectRoot = getProjectRootFromArtifactDir(artifactDir);
|
|
628
654
|
await ensureGitignore(projectRoot);
|
|
629
655
|
} catch {
|
|
630
656
|
// Swallow gitignore errors -- non-critical
|
|
@@ -647,6 +673,18 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
647
673
|
});
|
|
648
674
|
}
|
|
649
675
|
|
|
676
|
+
if (args.result === undefined && state.pendingDispatches.length > 0) {
|
|
677
|
+
const pending = state.pendingDispatches.at(-1);
|
|
678
|
+
const msg = `Pending result required for dispatch ${pending?.dispatchId ?? "unknown"} (${pending?.agent ?? "unknown"} / ${pending?.phase ?? state.currentPhase}). Submit a typed result envelope before calling oc_orchestrate again.`;
|
|
679
|
+
logDeterministicError(
|
|
680
|
+
artifactDir,
|
|
681
|
+
pending?.phase ?? state.currentPhase,
|
|
682
|
+
ORCHESTRATE_ERROR_CODES.PENDING_RESULT_REQUIRED,
|
|
683
|
+
msg,
|
|
684
|
+
);
|
|
685
|
+
return asErrorJson(ORCHESTRATE_ERROR_CODES.PENDING_RESULT_REQUIRED, msg);
|
|
686
|
+
}
|
|
687
|
+
|
|
650
688
|
if (typeof args.result === "string") {
|
|
651
689
|
const phaseHint = detectPhaseFromPending(state);
|
|
652
690
|
if (phaseHint === null) {
|
|
@@ -661,7 +699,7 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
661
699
|
}
|
|
662
700
|
|
|
663
701
|
try {
|
|
664
|
-
const parsed =
|
|
702
|
+
const parsed = parseTypedResultEnvelope(args.result, {
|
|
665
703
|
runId: state.runId,
|
|
666
704
|
phase: phaseHint,
|
|
667
705
|
fallbackDispatchId: detectDispatchFromPending(state),
|
|
@@ -679,39 +717,13 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
679
717
|
return asErrorJson(ORCHESTRATE_ERROR_CODES.STALE_RESULT, msg);
|
|
680
718
|
}
|
|
681
719
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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);
|
|
720
|
+
const nextState = await updatePersistedState(artifactDir, state, (current) =>
|
|
721
|
+
applyResultEnvelope(current, parsed.envelope),
|
|
722
|
+
);
|
|
699
723
|
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
724
|
|
|
712
725
|
phaseHandlerContext = {
|
|
713
726
|
envelope: parsed.envelope,
|
|
714
|
-
legacy: parsed.legacy,
|
|
715
727
|
};
|
|
716
728
|
handlerInputResult = parsed.envelope.payload.text;
|
|
717
729
|
} catch (error: unknown) {
|
|
@@ -761,13 +773,17 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
761
773
|
timestamp: new Date().toISOString(),
|
|
762
774
|
lastSuccessfulPhase: lastDone?.name ?? null,
|
|
763
775
|
};
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
776
|
+
await updatePersistedState(artifactDir, currentState, (latest) =>
|
|
777
|
+
patchState(latest, {
|
|
778
|
+
status: "FAILED" as const,
|
|
779
|
+
failureContext,
|
|
780
|
+
}),
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
} catch (persistError: unknown) {
|
|
784
|
+
if (isStateConflictError(persistError)) {
|
|
785
|
+
// Swallow conflict after retry exhaustion -- original error takes priority
|
|
769
786
|
}
|
|
770
|
-
} catch {
|
|
771
787
|
// Swallow save errors -- original error takes priority
|
|
772
788
|
}
|
|
773
789
|
|
|
@@ -103,9 +103,10 @@ function buildDisplayText(
|
|
|
103
103
|
* @param logsDir - Optional override for logs directory (for testing)
|
|
104
104
|
*/
|
|
105
105
|
export async function pipelineReportCore(sessionID?: string, logsDir?: string): Promise<string> {
|
|
106
|
+
const logsRoot = logsDir ?? process.cwd();
|
|
106
107
|
const log = sessionID
|
|
107
|
-
? await readSessionLog(sessionID,
|
|
108
|
-
: await readLatestSessionLog(
|
|
108
|
+
? await readSessionLog(sessionID, logsRoot)
|
|
109
|
+
: await readLatestSessionLog(logsRoot);
|
|
109
110
|
|
|
110
111
|
if (!log) {
|
|
111
112
|
const target = sessionID ? `Session "${sessionID}" not found.` : "No session logs found.";
|
package/src/tools/quick.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { ensurePhaseDir } from "../orchestrator/artifacts";
|
|
|
5
5
|
import { PHASES, pipelineStateSchema } from "../orchestrator/schemas";
|
|
6
6
|
import { loadState, saveState } from "../orchestrator/state";
|
|
7
7
|
import { ensureGitignore } from "../utils/gitignore";
|
|
8
|
-
import { getProjectArtifactDir } from "../utils/paths";
|
|
8
|
+
import { getProjectArtifactDir, getProjectRootFromArtifactDir } from "../utils/paths";
|
|
9
9
|
import { orchestrateCore } from "./orchestrate";
|
|
10
10
|
|
|
11
11
|
/** Phases skipped in quick mode (per D-17: skip RECON, CHALLENGE, ARCHITECT, EXPLORE). */
|
|
@@ -109,7 +109,7 @@ export async function quickCore(args: QuickArgs, artifactDir: string): Promise<s
|
|
|
109
109
|
|
|
110
110
|
// 6. Best-effort .gitignore update (same pattern as orchestrateCore)
|
|
111
111
|
try {
|
|
112
|
-
await ensureGitignore(
|
|
112
|
+
await ensureGitignore(getProjectRootFromArtifactDir(artifactDir));
|
|
113
113
|
} catch {
|
|
114
114
|
// Non-critical -- swallow gitignore errors
|
|
115
115
|
}
|
package/src/tools/review.ts
CHANGED
|
@@ -17,6 +17,11 @@ import { readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
19
|
import { tool } from "@opencode-ai/plugin";
|
|
20
|
+
import {
|
|
21
|
+
clearActiveReviewStateInKernel,
|
|
22
|
+
loadActiveReviewStateFromKernel,
|
|
23
|
+
saveActiveReviewStateToKernel,
|
|
24
|
+
} from "../kernel/repository";
|
|
20
25
|
import { REVIEW_AGENTS, SPECIALIZED_AGENTS } from "../review/agents/index";
|
|
21
26
|
import {
|
|
22
27
|
createEmptyMemory,
|
|
@@ -42,6 +47,7 @@ interface ReviewArgs {
|
|
|
42
47
|
const execFileAsync = promisify(execFile);
|
|
43
48
|
|
|
44
49
|
const STATE_FILE = "current-review.json";
|
|
50
|
+
let legacyReviewStateMirrorWarned = false;
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
53
|
* Get changed file paths for the given review scope.
|
|
@@ -83,11 +89,18 @@ async function getChangedFiles(
|
|
|
83
89
|
* Load review state from disk. Returns null if no active review.
|
|
84
90
|
*/
|
|
85
91
|
async function loadReviewState(artifactDir: string): Promise<ReviewState | null> {
|
|
92
|
+
const kernelState = loadActiveReviewStateFromKernel(artifactDir);
|
|
93
|
+
if (kernelState !== null) {
|
|
94
|
+
return kernelState;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
const statePath = join(artifactDir, STATE_FILE);
|
|
87
98
|
try {
|
|
88
99
|
const raw = await readFile(statePath, "utf-8");
|
|
89
100
|
const parsed = JSON.parse(raw);
|
|
90
|
-
|
|
101
|
+
const validated = reviewStateSchema.parse(parsed) as ReviewState;
|
|
102
|
+
saveActiveReviewStateToKernel(artifactDir, validated);
|
|
103
|
+
return validated;
|
|
91
104
|
} catch (error: unknown) {
|
|
92
105
|
if (isEnoentError(error)) return null;
|
|
93
106
|
// Treat parse/schema errors as recoverable — delete corrupt file
|
|
@@ -103,23 +116,43 @@ async function loadReviewState(artifactDir: string): Promise<ReviewState | null>
|
|
|
103
116
|
}
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
async function writeLegacyReviewStateMirror(
|
|
120
|
+
state: ReviewState,
|
|
121
|
+
artifactDir: string,
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
await ensureDir(artifactDir);
|
|
124
|
+
const statePath = join(artifactDir, STATE_FILE);
|
|
125
|
+
const tmpPath = `${statePath}.tmp.${randomBytes(8).toString("hex")}`;
|
|
126
|
+
await writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
127
|
+
await rename(tmpPath, statePath);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function syncLegacyReviewStateMirror(state: ReviewState, artifactDir: string): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
await writeLegacyReviewStateMirror(state, artifactDir);
|
|
133
|
+
} catch (error: unknown) {
|
|
134
|
+
if (!legacyReviewStateMirrorWarned) {
|
|
135
|
+
legacyReviewStateMirrorWarned = true;
|
|
136
|
+
console.warn("[opencode-autopilot] current-review.json mirror write failed:", error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
106
141
|
/**
|
|
107
142
|
* Save review state atomically.
|
|
108
143
|
*/
|
|
109
144
|
async function saveReviewState(state: ReviewState, artifactDir: string): Promise<void> {
|
|
110
|
-
await ensureDir(artifactDir);
|
|
111
145
|
// Validate before writing (bidirectional validation, same as orchestrator state)
|
|
112
146
|
const validated = reviewStateSchema.parse(state);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await writeFile(tmpPath, JSON.stringify(validated, null, 2), "utf-8");
|
|
116
|
-
await rename(tmpPath, statePath);
|
|
147
|
+
saveActiveReviewStateToKernel(artifactDir, validated);
|
|
148
|
+
await syncLegacyReviewStateMirror(validated, artifactDir);
|
|
117
149
|
}
|
|
118
150
|
|
|
119
151
|
/**
|
|
120
152
|
* Delete review state file (pipeline complete or error cleanup).
|
|
121
153
|
*/
|
|
122
154
|
async function clearReviewState(artifactDir: string): Promise<void> {
|
|
155
|
+
clearActiveReviewStateInKernel(artifactDir);
|
|
123
156
|
const statePath = join(artifactDir, STATE_FILE);
|
|
124
157
|
try {
|
|
125
158
|
await unlink(statePath);
|
|
@@ -138,9 +138,10 @@ function buildDisplayText(
|
|
|
138
138
|
* @param logsDir - Optional override for logs directory (for testing)
|
|
139
139
|
*/
|
|
140
140
|
export async function sessionStatsCore(sessionID?: string, logsDir?: string): Promise<string> {
|
|
141
|
+
const logsRoot = logsDir ?? process.cwd();
|
|
141
142
|
const log = sessionID
|
|
142
|
-
? await readSessionLog(sessionID,
|
|
143
|
-
: await readLatestSessionLog(
|
|
143
|
+
? await readSessionLog(sessionID, logsRoot)
|
|
144
|
+
: await readLatestSessionLog(logsRoot);
|
|
144
145
|
|
|
145
146
|
if (!log) {
|
|
146
147
|
const target = sessionID ? `Session "${sessionID}" not found.` : "No session logs found.";
|
package/src/utils/paths.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
const __dirname = import.meta.dir ?? dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const AUTOPILOT_DB_FILE = "autopilot.db";
|
|
7
|
+
const LEGACY_MEMORY_DIR = "memory";
|
|
8
|
+
const LEGACY_MEMORY_DB_FILE = "memory.db";
|
|
6
9
|
|
|
7
10
|
export function getGlobalConfigDir(): string {
|
|
8
11
|
return join(homedir(), ".config", "opencode");
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
export function getAutopilotDbPath(baseDir?: string): string {
|
|
15
|
+
return join(baseDir ?? getGlobalConfigDir(), AUTOPILOT_DB_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getLegacyMemoryDbPath(baseDir?: string): string {
|
|
19
|
+
return join(baseDir ?? getGlobalConfigDir(), LEGACY_MEMORY_DIR, LEGACY_MEMORY_DB_FILE);
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export function getAssetsDir(): string {
|
|
12
23
|
return join(__dirname, "..", "..", "assets");
|
|
13
24
|
}
|
|
@@ -15,3 +26,11 @@ export function getAssetsDir(): string {
|
|
|
15
26
|
export function getProjectArtifactDir(projectRoot: string): string {
|
|
16
27
|
return join(projectRoot, ".opencode-autopilot");
|
|
17
28
|
}
|
|
29
|
+
|
|
30
|
+
export function isProjectArtifactDir(path: string): boolean {
|
|
31
|
+
return basename(path) === ".opencode-autopilot";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getProjectRootFromArtifactDir(artifactDir: string): string {
|
|
35
|
+
return isProjectArtifactDir(artifactDir) ? dirname(artifactDir) : artifactDir;
|
|
36
|
+
}
|