@kodrunhq/opencode-autopilot 1.16.0 → 1.17.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/inspect.ts +2 -2
- package/package.json +1 -1
- package/src/config/index.ts +29 -0
- package/src/config/migrations.ts +196 -0
- package/src/config/v7.ts +45 -0
- package/src/config.ts +3 -3
- package/src/health/checks.ts +97 -0
- package/src/health/types.ts +1 -1
- package/src/index.ts +25 -2
- package/src/kernel/transaction.ts +48 -0
- package/src/kernel/types.ts +1 -2
- package/src/logging/domains.ts +39 -0
- package/src/logging/forensic-writer.ts +177 -0
- package/src/logging/index.ts +4 -0
- package/src/logging/logger.ts +44 -0
- package/src/logging/performance.ts +59 -0
- package/src/logging/rotation.ts +261 -0
- package/src/logging/types.ts +33 -0
- package/src/memory/capture-utils.ts +149 -0
- package/src/memory/capture.ts +16 -197
- package/src/memory/decay.ts +11 -2
- package/src/memory/injector.ts +4 -1
- package/src/memory/lessons.ts +85 -0
- package/src/memory/observations.ts +177 -0
- package/src/memory/preferences.ts +718 -0
- package/src/memory/projects.ts +83 -0
- package/src/memory/repository.ts +46 -1001
- package/src/memory/retrieval.ts +5 -1
- package/src/observability/context-display.ts +8 -0
- package/src/observability/event-handlers.ts +44 -6
- package/src/observability/forensic-log.ts +10 -2
- package/src/observability/forensic-schemas.ts +9 -1
- package/src/observability/log-reader.ts +20 -1
- package/src/orchestrator/error-context.ts +24 -0
- package/src/orchestrator/handlers/build-utils.ts +118 -0
- package/src/orchestrator/handlers/build.ts +13 -148
- package/src/orchestrator/handlers/retrospective.ts +0 -1
- package/src/orchestrator/lesson-memory.ts +7 -2
- package/src/orchestrator/orchestration-logger.ts +46 -31
- package/src/orchestrator/progress.ts +63 -0
- package/src/review/memory.ts +11 -3
- package/src/review/parse-findings.ts +116 -0
- package/src/review/pipeline.ts +3 -107
- package/src/review/selection.ts +38 -4
- package/src/scoring/time-provider.ts +23 -0
- package/src/tools/doctor.ts +2 -2
- package/src/tools/logs.ts +32 -6
- package/src/tools/orchestrate.ts +11 -9
- package/src/tools/replay.ts +42 -0
- package/src/tools/review.ts +8 -2
- package/src/tools/summary.ts +43 -0
- package/src/utils/random.ts +33 -0
- package/src/ux/session-summary.ts +56 -0
package/src/tools/orchestrate.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { getLogger } from "../logging/domains";
|
|
4
4
|
import { parseTypedResultEnvelope } from "../orchestrator/contracts/legacy-result-adapter";
|
|
5
5
|
import type { PendingDispatch, ResultEnvelope } from "../orchestrator/contracts/result-envelope";
|
|
6
|
+
import { enrichErrorMessage } from "../orchestrator/error-context";
|
|
6
7
|
import { PHASE_HANDLERS } from "../orchestrator/handlers/index";
|
|
7
8
|
import type { DispatchResult, PhaseHandlerContext } from "../orchestrator/handlers/types";
|
|
8
9
|
import { buildLessonContext } from "../orchestrator/lesson-injection";
|
|
9
10
|
import { loadLessonMemory } from "../orchestrator/lesson-memory";
|
|
10
11
|
import { logOrchestrationEvent } from "../orchestrator/orchestration-logger";
|
|
11
12
|
import { completePhase, getNextPhase, PHASE_INDEX, TOTAL_PHASES } from "../orchestrator/phase";
|
|
13
|
+
import { getPhaseProgressString } from "../orchestrator/progress";
|
|
12
14
|
import { loadAdaptiveSkillContext } from "../orchestrator/skill-injection";
|
|
13
15
|
import {
|
|
14
16
|
createInitialState,
|
|
@@ -311,17 +313,16 @@ async function injectSkillContext(
|
|
|
311
313
|
});
|
|
312
314
|
if (ctx) return prompt + ctx;
|
|
313
315
|
} catch (err) {
|
|
314
|
-
|
|
316
|
+
getLogger("tool", "orchestrate").warn("skill injection failed", { err });
|
|
315
317
|
}
|
|
316
318
|
return prompt;
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
/** Build a human-readable progress string for user-facing display. */
|
|
320
|
-
function buildUserProgress(
|
|
321
|
-
const
|
|
322
|
-
const desc = label ?? "dispatching";
|
|
322
|
+
function buildUserProgress(state: PipelineState, label?: string, attempt?: number): string {
|
|
323
|
+
const baseProgress = getPhaseProgressString(state);
|
|
323
324
|
const att = attempt != null ? ` (attempt ${attempt})` : "";
|
|
324
|
-
return
|
|
325
|
+
return `${baseProgress}${label ? ` — ${label}` : ""}${att}`;
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
/** Per-phase dispatch limits. BUILD is high because of multi-task waves. */
|
|
@@ -429,7 +430,7 @@ async function processHandlerResult(
|
|
|
429
430
|
};
|
|
430
431
|
|
|
431
432
|
// Log the dispatch event before any inline-review or context injection
|
|
432
|
-
const progress = buildUserProgress(
|
|
433
|
+
const progress = buildUserProgress(currentState, normalizedResult.progress, attempt);
|
|
433
434
|
logOrchestrationEvent(artifactDir, {
|
|
434
435
|
timestamp: new Date().toISOString(),
|
|
435
436
|
phase,
|
|
@@ -544,7 +545,7 @@ async function processHandlerResult(
|
|
|
544
545
|
taskId: entry.taskId ?? null,
|
|
545
546
|
})) ?? [];
|
|
546
547
|
|
|
547
|
-
const progress = buildUserProgress(
|
|
548
|
+
const progress = buildUserProgress(currentState, normalizedResult.progress, attempt);
|
|
548
549
|
logOrchestrationEvent(artifactDir, {
|
|
549
550
|
timestamp: new Date().toISOString(),
|
|
550
551
|
phase,
|
|
@@ -759,12 +760,13 @@ export async function orchestrateCore(args: OrchestrateArgs, artifactDir: string
|
|
|
759
760
|
} catch (error: unknown) {
|
|
760
761
|
const message = error instanceof Error ? error.message : String(error);
|
|
761
762
|
const parsedErr = parseErrorCode(error);
|
|
762
|
-
|
|
763
|
+
let safeMessage = message.replace(/[/\\][^\s"']+/g, "[PATH]").slice(0, 4096);
|
|
763
764
|
|
|
764
765
|
// Persist failure metadata for forensics (best-effort)
|
|
765
766
|
try {
|
|
766
767
|
const currentState = await loadState(artifactDir);
|
|
767
768
|
if (currentState?.currentPhase) {
|
|
769
|
+
safeMessage = enrichErrorMessage(safeMessage, currentState);
|
|
768
770
|
const lastDone = currentState.phases.filter((p) => p.status === "DONE").pop();
|
|
769
771
|
const failureContext = {
|
|
770
772
|
failedPhase: currentState.currentPhase,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import type { ReviewState } from "../review/types";
|
|
3
|
+
|
|
4
|
+
export const ocReplay = tool({
|
|
5
|
+
description:
|
|
6
|
+
"Verify determinism by replaying a known sequence of inputs to the review pipeline and ensuring identical state output.",
|
|
7
|
+
args: {
|
|
8
|
+
runId: tool.schema.string().describe("The pipeline runId to use as the random seed for replay"),
|
|
9
|
+
inputs: tool.schema
|
|
10
|
+
.array(tool.schema.string())
|
|
11
|
+
.describe("Array of raw JSON findings inputs to feed sequentially into the pipeline"),
|
|
12
|
+
},
|
|
13
|
+
async execute(args) {
|
|
14
|
+
const { advancePipeline } = await import("../review/pipeline");
|
|
15
|
+
|
|
16
|
+
let currentState: ReviewState = {
|
|
17
|
+
stage: 1,
|
|
18
|
+
scope: "replay-scope",
|
|
19
|
+
selectedAgentNames: ["logic-auditor", "security-auditor"],
|
|
20
|
+
accumulatedFindings: [],
|
|
21
|
+
startedAt: "2026-04-05T00:00:00.000Z",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (const input of args.inputs) {
|
|
25
|
+
const res = advancePipeline(input, currentState, undefined, args.runId, args.runId);
|
|
26
|
+
if (res.state) {
|
|
27
|
+
currentState = res.state;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return JSON.stringify(
|
|
32
|
+
{
|
|
33
|
+
success: true,
|
|
34
|
+
message: `Replayed ${args.inputs.length} inputs deterministically.`,
|
|
35
|
+
replayedRunId: args.runId,
|
|
36
|
+
finalState: currentState,
|
|
37
|
+
},
|
|
38
|
+
null,
|
|
39
|
+
2,
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
});
|
package/src/tools/review.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
loadActiveReviewStateFromKernel,
|
|
23
23
|
saveActiveReviewStateToKernel,
|
|
24
24
|
} from "../kernel/repository";
|
|
25
|
+
import { getLogger } from "../logging/domains";
|
|
26
|
+
import { loadState as loadPipelineState } from "../orchestrator/state";
|
|
25
27
|
import { REVIEW_AGENTS, SPECIALIZED_AGENTS } from "../review/agents/index";
|
|
26
28
|
import {
|
|
27
29
|
createEmptyMemory,
|
|
@@ -133,7 +135,7 @@ async function syncLegacyReviewStateMirror(state: ReviewState, artifactDir: stri
|
|
|
133
135
|
} catch (error: unknown) {
|
|
134
136
|
if (!legacyReviewStateMirrorWarned) {
|
|
135
137
|
legacyReviewStateMirrorWarned = true;
|
|
136
|
-
|
|
138
|
+
getLogger("tool", "review").warn("current-review.json mirror write failed", { error });
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
}
|
|
@@ -190,7 +192,11 @@ async function startNewReview(
|
|
|
190
192
|
|
|
191
193
|
// Select agents from all candidates (universal + specialized)
|
|
192
194
|
const allCandidates = [...REVIEW_AGENTS, ...SPECIALIZED_AGENTS];
|
|
193
|
-
const
|
|
195
|
+
const artifactDir = getProjectArtifactDir(projectRoot);
|
|
196
|
+
const pipelineState = await loadPipelineState(artifactDir);
|
|
197
|
+
const seed = pipelineState ? `${pipelineState.runId}-review-1` : undefined;
|
|
198
|
+
|
|
199
|
+
const selection = selectAgents(detectedStacks, diffAnalysis, allCandidates, { seed });
|
|
194
200
|
|
|
195
201
|
const selectedNames = selection.selected.map((a) => a.name);
|
|
196
202
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readLatestSessionLog, readSessionLog } from "../observability/log-reader";
|
|
4
|
+
import { generateSessionSummary } from "../observability/summary-generator";
|
|
5
|
+
|
|
6
|
+
export async function summaryCore(sessionID?: string, logsDir?: string): Promise<string> {
|
|
7
|
+
const logsRoot = logsDir ?? process.cwd();
|
|
8
|
+
const log = sessionID
|
|
9
|
+
? await readSessionLog(sessionID, logsRoot)
|
|
10
|
+
: await readLatestSessionLog(logsRoot);
|
|
11
|
+
|
|
12
|
+
if (!log) {
|
|
13
|
+
const target = sessionID ? `Session "${sessionID}" not found.` : "No session logs found.";
|
|
14
|
+
return JSON.stringify({
|
|
15
|
+
action: "error",
|
|
16
|
+
message: target,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const summary = generateSessionSummary(log);
|
|
21
|
+
|
|
22
|
+
return JSON.stringify({
|
|
23
|
+
action: "session_summary",
|
|
24
|
+
sessionId: log.sessionId,
|
|
25
|
+
summary,
|
|
26
|
+
displayText: summary,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ocSummary = tool({
|
|
31
|
+
description:
|
|
32
|
+
"Generate a markdown summary for the latest session or a specific session ID. Use this to review session outcomes, decisions, and errors.",
|
|
33
|
+
args: {
|
|
34
|
+
sessionID: z
|
|
35
|
+
.string()
|
|
36
|
+
.regex(/^[a-zA-Z0-9_-]{1,256}$/)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Session ID to summarize (uses latest if omitted)"),
|
|
39
|
+
},
|
|
40
|
+
async execute({ sessionID }) {
|
|
41
|
+
return summaryCore(sessionID);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple seeded random number generator.
|
|
3
|
+
* Uses the Mulberry32 algorithm which provides decent quality and is very fast.
|
|
4
|
+
*/
|
|
5
|
+
export function createSeededRandom(seedString: string) {
|
|
6
|
+
// Simple hash function (djb2) to convert string to 32-bit integer seed
|
|
7
|
+
let seed = 5381;
|
|
8
|
+
for (let i = 0; i < seedString.length; i++) {
|
|
9
|
+
seed = (seed * 33) ^ seedString.charCodeAt(i);
|
|
10
|
+
}
|
|
11
|
+
// Add an arbitrary constant to avoid passing 0 to mulberry32
|
|
12
|
+
let a = seed + 1831565813;
|
|
13
|
+
|
|
14
|
+
// Mulberry32 generator
|
|
15
|
+
return function random(): number {
|
|
16
|
+
a += 0x6d2b79f5;
|
|
17
|
+
let t = a;
|
|
18
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
19
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
20
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Shuffles an array in-place deterministically using the provided seeded RNG.
|
|
26
|
+
*/
|
|
27
|
+
export function deterministicShuffle<T>(array: T[], rng: () => number): T[] {
|
|
28
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
29
|
+
const j = Math.floor(rng() * (i + 1));
|
|
30
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
31
|
+
}
|
|
32
|
+
return array;
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SessionEvents } from "../observability/event-store";
|
|
2
|
+
import type { PipelineState } from "../orchestrator/types";
|
|
3
|
+
|
|
4
|
+
export function generateSessionSummary(
|
|
5
|
+
sessionData: SessionEvents | undefined,
|
|
6
|
+
pipelineState: PipelineState | null,
|
|
7
|
+
): string {
|
|
8
|
+
const sections: string[] = ["## Session Summary"];
|
|
9
|
+
|
|
10
|
+
if (pipelineState) {
|
|
11
|
+
const phaseInfo = pipelineState.currentPhase
|
|
12
|
+
? ` (Current Phase: ${pipelineState.currentPhase})`
|
|
13
|
+
: "";
|
|
14
|
+
sections.push(`**Pipeline Status**: ${pipelineState.status}${phaseInfo}`);
|
|
15
|
+
|
|
16
|
+
const done = pipelineState.phases.filter((p) => p.status === "DONE").map((p) => p.name);
|
|
17
|
+
|
|
18
|
+
sections.push(`**Phases Completed**: ${done.length > 0 ? done.join(", ") : "None"}`);
|
|
19
|
+
} else {
|
|
20
|
+
sections.push("**Pipeline Status**: Unknown");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (sessionData?.tokens) {
|
|
24
|
+
const { inputTokens, outputTokens, reasoningTokens } = sessionData.tokens;
|
|
25
|
+
sections.push(
|
|
26
|
+
"\n**Context Used**:",
|
|
27
|
+
`- Input: ${inputTokens.toLocaleString()} tokens`,
|
|
28
|
+
`- Output: ${outputTokens.toLocaleString()} tokens`,
|
|
29
|
+
...(reasoningTokens > 0 ? [`- Reasoning: ${reasoningTokens.toLocaleString()} tokens`] : []),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const errors = (sessionData?.events ?? []).filter((e) => e.type === "error");
|
|
34
|
+
if (errors.length > 0) {
|
|
35
|
+
sections.push("\n**Errors Encountered**:");
|
|
36
|
+
for (const e of errors) {
|
|
37
|
+
if (e.type === "error") {
|
|
38
|
+
sections.push(`- ${e.errorType.toUpperCase()}: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (sessionData) {
|
|
44
|
+
const endEvent = sessionData.events.find((e) => e.type === "session_end");
|
|
45
|
+
if (endEvent && endEvent.type === "session_end") {
|
|
46
|
+
const seconds = (endEvent.durationMs / 1000).toFixed(1);
|
|
47
|
+
sections.push(`\n**Duration**: ${seconds}s`);
|
|
48
|
+
} else {
|
|
49
|
+
const start = new Date(sessionData.startedAt).getTime();
|
|
50
|
+
const seconds = ((Date.now() - start) / 1000).toFixed(1);
|
|
51
|
+
sections.push(`\n**Duration (active)**: ${seconds}s`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return sections.join("\n").trim();
|
|
56
|
+
}
|