@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
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getLogger } from "../logging/domains";
|
|
2
|
+
import { createForensicSinkForArtifactDir } from "../logging/forensic-writer";
|
|
3
|
+
import type { LogLevel } from "../logging/types";
|
|
2
4
|
|
|
3
5
|
export interface OrchestrationEvent {
|
|
4
6
|
readonly timestamp: string;
|
|
@@ -16,39 +18,52 @@ export interface OrchestrationEvent {
|
|
|
16
18
|
readonly payload?: Record<string, string | number | boolean | null>;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
function resolveOperation(event: OrchestrationEvent): string {
|
|
22
|
+
if (event.action === "dispatch") return "dispatch";
|
|
23
|
+
if (event.action === "dispatch_multi") return "dispatch_multi";
|
|
24
|
+
if (event.action === "complete") return "complete";
|
|
25
|
+
if (event.action === "loop_detected") return "loop_detected";
|
|
26
|
+
if (event.action === "error" && event.code?.startsWith("E_")) return "warning";
|
|
27
|
+
return "error";
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export function logOrchestrationEvent(artifactDir: string, event: OrchestrationEvent): void {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
event.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
: event.action === "loop_detected"
|
|
41
|
-
? "loop_detected"
|
|
42
|
-
: event.action === "error" && event.code?.startsWith("E_")
|
|
43
|
-
? "warning"
|
|
44
|
-
: "error",
|
|
45
|
-
code: event.code ?? null,
|
|
46
|
-
message: event.message ?? null,
|
|
47
|
-
payload: {
|
|
31
|
+
try {
|
|
32
|
+
const domain =
|
|
33
|
+
event.action === "error" && event.code?.startsWith("E_") ? "contract" : "orchestrator";
|
|
34
|
+
const operation = resolveOperation(event);
|
|
35
|
+
const level: LogLevel = event.action === "error" ? "ERROR" : "INFO";
|
|
36
|
+
|
|
37
|
+
const metadata = {
|
|
38
|
+
domain,
|
|
39
|
+
operation,
|
|
40
|
+
runId: event.runId ?? null,
|
|
41
|
+
sessionId: event.sessionId ?? null,
|
|
42
|
+
phase: event.phase,
|
|
43
|
+
dispatchId: event.dispatchId ?? null,
|
|
44
|
+
taskId: event.taskId ?? null,
|
|
45
|
+
agent: event.agent ?? null,
|
|
46
|
+
code: event.code ?? null,
|
|
48
47
|
action: event.action,
|
|
49
48
|
...(event.promptLength !== undefined ? { promptLength: event.promptLength } : {}),
|
|
50
49
|
...(event.attempt !== undefined ? { attempt: event.attempt } : {}),
|
|
51
50
|
...(event.payload ?? {}),
|
|
52
|
-
}
|
|
53
|
-
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
createForensicSinkForArtifactDir(artifactDir).write(
|
|
54
|
+
Object.freeze({
|
|
55
|
+
timestamp: event.timestamp,
|
|
56
|
+
level,
|
|
57
|
+
message: event.message ?? event.action,
|
|
58
|
+
metadata,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const globalLogger = getLogger(domain);
|
|
63
|
+
if (event.action === "error") {
|
|
64
|
+
globalLogger.error(event.message ?? event.action, { operation, phase: event.phase });
|
|
65
|
+
} else {
|
|
66
|
+
globalLogger.info(event.message ?? event.action, { operation, phase: event.phase });
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
54
69
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PHASES } from "./schemas";
|
|
2
|
+
import type { PipelineState } from "./types";
|
|
3
|
+
|
|
4
|
+
const PHASE_INDEX = Object.freeze(
|
|
5
|
+
Object.fromEntries(PHASES.map((phase, index) => [phase, index + 1])) as Record<
|
|
6
|
+
(typeof PHASES)[number],
|
|
7
|
+
number
|
|
8
|
+
>,
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a concise progress string for the user indicating current phase and progress.
|
|
13
|
+
* e.g., "[1/8] Analyzing requirements..." or "[6/8] Building wave 2 of 5..."
|
|
14
|
+
*/
|
|
15
|
+
export function getPhaseProgressString(state: PipelineState): string {
|
|
16
|
+
const currentPhase = state.currentPhase;
|
|
17
|
+
if (!currentPhase) {
|
|
18
|
+
if (state.status === "COMPLETED") return "[Done] Pipeline finished successfully.";
|
|
19
|
+
if (state.status === "FAILED") return "[Failed] Pipeline encountered an error.";
|
|
20
|
+
return `[0/${PHASES.length}] Not started`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const phaseIndex = PHASE_INDEX[currentPhase];
|
|
24
|
+
const totalPhases = PHASES.length;
|
|
25
|
+
const baseProgress = `[${phaseIndex}/${totalPhases}]`;
|
|
26
|
+
|
|
27
|
+
switch (currentPhase) {
|
|
28
|
+
case "RECON":
|
|
29
|
+
return `${baseProgress} Researching feasibility and codebase context...`;
|
|
30
|
+
case "CHALLENGE":
|
|
31
|
+
return `${baseProgress} Evaluating architecture enhancements...`;
|
|
32
|
+
case "ARCHITECT":
|
|
33
|
+
return `${baseProgress} Designing technical architecture...`;
|
|
34
|
+
case "EXPLORE":
|
|
35
|
+
return `${baseProgress} Exploring implementation paths...`;
|
|
36
|
+
case "PLAN":
|
|
37
|
+
return `${baseProgress} Planning implementation waves...`;
|
|
38
|
+
case "BUILD": {
|
|
39
|
+
const progress = state.buildProgress;
|
|
40
|
+
if (!progress || progress.currentWave === null) {
|
|
41
|
+
return `${baseProgress} Starting build phase...`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find max wave to show total waves
|
|
45
|
+
const allWaves = state.tasks.map((t) => t.wave);
|
|
46
|
+
const totalWaves = allWaves.length > 0 ? Math.max(...allWaves) : 0;
|
|
47
|
+
const totalTasksInWave = state.tasks.filter((t) => t.wave === progress.currentWave).length;
|
|
48
|
+
|
|
49
|
+
if (progress.reviewPending) {
|
|
50
|
+
return `${baseProgress} Reviewing wave ${progress.currentWave}/${totalWaves}...`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Just a sensible string for current build status
|
|
54
|
+
return `${baseProgress} Building wave ${progress.currentWave}/${totalWaves} (${totalTasksInWave} tasks)...`;
|
|
55
|
+
}
|
|
56
|
+
case "SHIP":
|
|
57
|
+
return `${baseProgress} Generating changelog and documentation...`;
|
|
58
|
+
case "RETROSPECTIVE":
|
|
59
|
+
return `${baseProgress} Extracting lessons learned...`;
|
|
60
|
+
default:
|
|
61
|
+
return `${baseProgress} Executing ${currentPhase}...`;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/review/memory.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { loadReviewMemoryFromKernel, saveReviewMemoryToKernel } from "../kernel/repository";
|
|
15
|
+
import { getLogger } from "../logging/domains";
|
|
15
16
|
import { ensureDir, isEnoentError } from "../utils/fs-helpers";
|
|
16
17
|
import { getProjectArtifactDir } from "../utils/paths";
|
|
17
18
|
import { reviewMemorySchema } from "./schemas";
|
|
@@ -93,7 +94,9 @@ export async function saveReviewMemory(memory: ReviewMemory, projectRoot: string
|
|
|
93
94
|
} catch (error: unknown) {
|
|
94
95
|
if (!legacyReviewMemoryMirrorWarned) {
|
|
95
96
|
legacyReviewMemoryMirrorWarned = true;
|
|
96
|
-
|
|
97
|
+
getLogger("review", "memory").warn("review-memory.json mirror write failed", {
|
|
98
|
+
error: String(error),
|
|
99
|
+
});
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
}
|
|
@@ -106,8 +109,13 @@ export async function saveReviewMemory(memory: ReviewMemory, projectRoot: string
|
|
|
106
109
|
* - falsePositives: cap at 50 (keep newest by markedAt date)
|
|
107
110
|
* - falsePositives: remove entries older than 30 days
|
|
108
111
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
import { systemTimeProvider, type TimeProvider } from "../scoring/time-provider";
|
|
113
|
+
|
|
114
|
+
export function pruneMemory(
|
|
115
|
+
memory: ReviewMemory,
|
|
116
|
+
timeProvider: TimeProvider = systemTimeProvider,
|
|
117
|
+
): ReviewMemory {
|
|
118
|
+
const now = timeProvider.now();
|
|
111
119
|
|
|
112
120
|
// Prune false positives older than 30 days first, then cap
|
|
113
121
|
const freshFalsePositives = memory.falsePositives.filter(
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { reviewFindingSchema, reviewFindingsEnvelopeSchema } from "./schemas";
|
|
2
|
+
import type { ReviewFinding, ReviewFindingsEnvelope } from "./types";
|
|
3
|
+
|
|
4
|
+
export function parseTypedFindingsEnvelope(raw: string): ReviewFindingsEnvelope | null {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(raw);
|
|
7
|
+
return reviewFindingsEnvelopeSchema.parse(parsed);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseAgentFindings(raw: string, agentName: string): readonly ReviewFinding[] {
|
|
14
|
+
const findings: ReviewFinding[] = [];
|
|
15
|
+
|
|
16
|
+
const jsonStr = extractJson(raw);
|
|
17
|
+
if (jsonStr === null) return Object.freeze(findings);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const cleanJson = sanitizeMalformedJson(jsonStr);
|
|
21
|
+
const parsed = JSON.parse(cleanJson);
|
|
22
|
+
const items = Array.isArray(parsed) ? parsed : parsed?.findings;
|
|
23
|
+
|
|
24
|
+
if (!Array.isArray(items)) return Object.freeze(findings);
|
|
25
|
+
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
if (typeof item !== "object" || item === null) continue;
|
|
28
|
+
|
|
29
|
+
const problem = item.problem || item.description || item.issue || "No description provided";
|
|
30
|
+
const normalized = {
|
|
31
|
+
...item,
|
|
32
|
+
agent: item.agent || agentName,
|
|
33
|
+
severity: normalizeSeverity(item.severity),
|
|
34
|
+
domain: item.domain || "general",
|
|
35
|
+
title:
|
|
36
|
+
item.title || item.name || (problem ? String(problem).slice(0, 50) : "Untitled finding"),
|
|
37
|
+
file: item.file || item.path || item.filename || "unknown",
|
|
38
|
+
source: item.source || "phase1",
|
|
39
|
+
evidence: item.evidence || item.snippet || item.context || "No evidence provided",
|
|
40
|
+
problem: problem,
|
|
41
|
+
fix: item.fix || item.recommendation || item.solution || "No fix provided",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = reviewFindingSchema.safeParse(normalized);
|
|
45
|
+
if (result.success) {
|
|
46
|
+
findings.push(result.data);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// JSON parse failed completely
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Object.freeze(findings);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeSeverity(sev: unknown): string {
|
|
57
|
+
if (typeof sev !== "string") return "LOW";
|
|
58
|
+
const upper = sev.toUpperCase();
|
|
59
|
+
if (["CRITICAL", "HIGH", "MEDIUM", "LOW"].includes(upper)) return upper;
|
|
60
|
+
return "LOW";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sanitizeMalformedJson(json: string): string {
|
|
64
|
+
let clean = json;
|
|
65
|
+
// Remove trailing commas in objects and arrays
|
|
66
|
+
clean = clean.replace(/,\s*([}\]])/g, "$1");
|
|
67
|
+
// Replace unescaped newlines in strings (basic attempt)
|
|
68
|
+
// This is risky with regex but catches common LLM markdown mistakes
|
|
69
|
+
return clean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function extractJson(raw: string): string | null {
|
|
73
|
+
const codeBlockMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
74
|
+
if (codeBlockMatch) {
|
|
75
|
+
return codeBlockMatch[1].trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const objectStart = raw.indexOf("{");
|
|
79
|
+
const arrayStart = raw.indexOf("[");
|
|
80
|
+
|
|
81
|
+
if (objectStart === -1 && arrayStart === -1) return null;
|
|
82
|
+
|
|
83
|
+
const start =
|
|
84
|
+
objectStart === -1
|
|
85
|
+
? arrayStart
|
|
86
|
+
: arrayStart === -1
|
|
87
|
+
? objectStart
|
|
88
|
+
: Math.min(objectStart, arrayStart);
|
|
89
|
+
|
|
90
|
+
let depth = 0;
|
|
91
|
+
let inString = false;
|
|
92
|
+
let escaped = false;
|
|
93
|
+
for (let i = start; i < raw.length; i++) {
|
|
94
|
+
const ch = raw[i];
|
|
95
|
+
if (escaped) {
|
|
96
|
+
escaped = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === "\\" && inString) {
|
|
100
|
+
escaped = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '"') {
|
|
104
|
+
inString = !inString;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (inString) continue;
|
|
108
|
+
if (ch === "{" || ch === "[") depth++;
|
|
109
|
+
if (ch === "}" || ch === "]") depth--;
|
|
110
|
+
if (depth === 0) {
|
|
111
|
+
return raw.slice(start, i + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
package/src/review/pipeline.ts
CHANGED
|
@@ -18,8 +18,8 @@ import { buildCrossVerificationPrompts, condenseFinding } from "./cross-verifica
|
|
|
18
18
|
const STAGE3_NAMES: ReadonlySet<string> = new Set(STAGE3_AGENTS.map((a) => a.name));
|
|
19
19
|
|
|
20
20
|
import { buildFixInstructions, determineFixableFindings } from "./fix-cycle";
|
|
21
|
+
import { parseAgentFindings, parseTypedFindingsEnvelope } from "./parse-findings";
|
|
21
22
|
import { buildReport } from "./report";
|
|
22
|
-
import { reviewFindingSchema, reviewFindingsEnvelopeSchema } from "./schemas";
|
|
23
23
|
import type { ReviewFinding, ReviewFindingsEnvelope, ReviewReport, ReviewState } from "./types";
|
|
24
24
|
|
|
25
25
|
export type { ReviewState };
|
|
@@ -38,112 +38,6 @@ export interface ReviewStageResult {
|
|
|
38
38
|
readonly parseMode?: "typed" | "legacy";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function parseTypedFindingsEnvelope(raw: string): ReviewFindingsEnvelope | null {
|
|
42
|
-
try {
|
|
43
|
-
const parsed = JSON.parse(raw);
|
|
44
|
-
return reviewFindingsEnvelopeSchema.parse(parsed);
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Parse findings from raw LLM output (which may contain markdown, prose, code blocks).
|
|
52
|
-
*
|
|
53
|
-
* Handles:
|
|
54
|
-
* - {"findings": [...]} wrapper
|
|
55
|
-
* - Raw array [{...}, ...]
|
|
56
|
-
* - JSON embedded in markdown code blocks
|
|
57
|
-
* - Prose with embedded JSON
|
|
58
|
-
*
|
|
59
|
-
* Sets agent field to agentName if missing from individual findings.
|
|
60
|
-
* Validates each finding through reviewFindingSchema, discards invalid ones.
|
|
61
|
-
*/
|
|
62
|
-
export function parseAgentFindings(raw: string, agentName: string): readonly ReviewFinding[] {
|
|
63
|
-
const findings: ReviewFinding[] = [];
|
|
64
|
-
|
|
65
|
-
// Try to extract JSON from the raw text
|
|
66
|
-
const jsonStr = extractJson(raw);
|
|
67
|
-
if (jsonStr === null) return Object.freeze(findings);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(jsonStr);
|
|
71
|
-
const items = Array.isArray(parsed) ? parsed : parsed?.findings;
|
|
72
|
-
|
|
73
|
-
if (!Array.isArray(items)) return Object.freeze(findings);
|
|
74
|
-
|
|
75
|
-
for (const item of items) {
|
|
76
|
-
// Set agent field if missing
|
|
77
|
-
const withAgent = item.agent ? item : { ...item, agent: agentName };
|
|
78
|
-
const result = reviewFindingSchema.safeParse(withAgent);
|
|
79
|
-
if (result.success) {
|
|
80
|
-
findings.push(result.data);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
// JSON parse failed -- return empty
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return Object.freeze(findings);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Extract the first JSON object or array from raw text.
|
|
92
|
-
* Looks for:
|
|
93
|
-
* 1. JSON inside markdown code blocks
|
|
94
|
-
* 2. {"findings": ...} pattern
|
|
95
|
-
* 3. Raw array [{...}]
|
|
96
|
-
*/
|
|
97
|
-
function extractJson(raw: string): string | null {
|
|
98
|
-
// Try markdown code block extraction first
|
|
99
|
-
const codeBlockMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
100
|
-
if (codeBlockMatch) {
|
|
101
|
-
return codeBlockMatch[1].trim();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Try to find {"findings": ...} or [{...}]
|
|
105
|
-
const objectStart = raw.indexOf("{");
|
|
106
|
-
const arrayStart = raw.indexOf("[");
|
|
107
|
-
|
|
108
|
-
if (objectStart === -1 && arrayStart === -1) return null;
|
|
109
|
-
|
|
110
|
-
// Pick whichever comes first
|
|
111
|
-
const start =
|
|
112
|
-
objectStart === -1
|
|
113
|
-
? arrayStart
|
|
114
|
-
: arrayStart === -1
|
|
115
|
-
? objectStart
|
|
116
|
-
: Math.min(objectStart, arrayStart);
|
|
117
|
-
|
|
118
|
-
// Find matching close bracket (string-literal-aware depth tracking)
|
|
119
|
-
let depth = 0;
|
|
120
|
-
let inString = false;
|
|
121
|
-
let escaped = false;
|
|
122
|
-
for (let i = start; i < raw.length; i++) {
|
|
123
|
-
const ch = raw[i];
|
|
124
|
-
if (escaped) {
|
|
125
|
-
escaped = false;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (ch === "\\" && inString) {
|
|
129
|
-
escaped = true;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (ch === '"') {
|
|
133
|
-
inString = !inString;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (inString) continue;
|
|
137
|
-
if (ch === "{" || ch === "[") depth++;
|
|
138
|
-
if (ch === "}" || ch === "]") depth--;
|
|
139
|
-
if (depth === 0) {
|
|
140
|
-
return raw.slice(start, i + 1);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
41
|
/**
|
|
148
42
|
* Advance the pipeline from the current stage to the next.
|
|
149
43
|
*
|
|
@@ -155,6 +49,8 @@ export function advancePipeline(
|
|
|
155
49
|
findingsJson: string,
|
|
156
50
|
currentState: ReviewState,
|
|
157
51
|
agentName = "unknown",
|
|
52
|
+
_runId?: string,
|
|
53
|
+
_seed?: string,
|
|
158
54
|
): ReviewStageResult {
|
|
159
55
|
const typedEnvelope = parseTypedFindingsEnvelope(findingsJson);
|
|
160
56
|
const parseMode = typedEnvelope ? "typed" : "legacy";
|
package/src/review/selection.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* agents with non-empty relevantStacks require at least one match.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { createSeededRandom, deterministicShuffle } from "../utils/random";
|
|
9
|
+
|
|
8
10
|
/** Minimal agent shape needed for selection (compatible with ReviewAgent from agents/). */
|
|
9
11
|
interface SelectableAgent {
|
|
10
12
|
readonly name: string;
|
|
@@ -21,6 +23,13 @@ export interface DiffAnalysisInput {
|
|
|
21
23
|
readonly fileCount: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export interface SelectionOptions {
|
|
27
|
+
/** Seed for reproducible agent ordering. If omitted, uses a fixed default. */
|
|
28
|
+
readonly seed?: string;
|
|
29
|
+
/** Maximum number of gated agents to select. Universal agents are always included. */
|
|
30
|
+
readonly limit?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
export interface SelectionResult {
|
|
25
34
|
readonly selected: readonly SelectableAgent[];
|
|
26
35
|
readonly excluded: readonly { readonly agent: string; readonly reason: string }[];
|
|
@@ -32,25 +41,28 @@ export interface SelectionResult {
|
|
|
32
41
|
* @param detectedStacks - Stack tags detected in the project (e.g., ["node", "typescript"])
|
|
33
42
|
* @param diffAnalysis - Analysis of changed files
|
|
34
43
|
* @param agents - All candidate agents
|
|
44
|
+
* @param options - Options for seeding and limiting the number of agents
|
|
35
45
|
* @returns Frozen SelectionResult with selected and excluded lists
|
|
36
46
|
*/
|
|
37
47
|
export function selectAgents(
|
|
38
48
|
detectedStacks: readonly string[],
|
|
39
49
|
_diffAnalysis: DiffAnalysisInput,
|
|
40
50
|
agents: readonly SelectableAgent[],
|
|
51
|
+
options: SelectionOptions = {},
|
|
41
52
|
): SelectionResult {
|
|
42
53
|
const stackSet = new Set(detectedStacks);
|
|
43
|
-
const
|
|
54
|
+
const universal: SelectableAgent[] = [];
|
|
55
|
+
const gatedCandidates: SelectableAgent[] = [];
|
|
44
56
|
const excluded: { readonly agent: string; readonly reason: string }[] = [];
|
|
45
57
|
|
|
46
58
|
for (const agent of agents) {
|
|
47
59
|
// Pass 1: Stack gate
|
|
48
60
|
if (agent.relevantStacks.length === 0) {
|
|
49
61
|
// Universal agent -- always passes
|
|
50
|
-
|
|
62
|
+
universal.push(agent);
|
|
51
63
|
} else if (agent.relevantStacks.some((s) => stackSet.has(s))) {
|
|
52
64
|
// Gated agent with at least one matching stack
|
|
53
|
-
|
|
65
|
+
gatedCandidates.push(agent);
|
|
54
66
|
} else {
|
|
55
67
|
// Gated agent with no matching stack
|
|
56
68
|
const stackList = detectedStacks.length > 0 ? detectedStacks.join(", ") : "none";
|
|
@@ -63,8 +75,30 @@ export function selectAgents(
|
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
const seed = options.seed ?? "default-selection-seed";
|
|
79
|
+
const rng = createSeededRandom(seed);
|
|
80
|
+
|
|
81
|
+
const shuffledGated = deterministicShuffle([...gatedCandidates], rng);
|
|
82
|
+
const finalGated =
|
|
83
|
+
options.limit !== undefined ? shuffledGated.slice(0, options.limit) : shuffledGated;
|
|
84
|
+
|
|
85
|
+
if (options.limit !== undefined && finalGated.length < shuffledGated.length) {
|
|
86
|
+
const dropped = shuffledGated.slice(options.limit);
|
|
87
|
+
for (const agent of dropped) {
|
|
88
|
+
excluded.push(
|
|
89
|
+
Object.freeze({
|
|
90
|
+
agent: agent.name,
|
|
91
|
+
reason: `Diversity limit: dropped to meet limit of ${options.limit}`,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const combined = [...universal, ...finalGated];
|
|
98
|
+
deterministicShuffle(combined, rng);
|
|
99
|
+
|
|
66
100
|
return Object.freeze({
|
|
67
|
-
selected: Object.freeze(
|
|
101
|
+
selected: Object.freeze(combined),
|
|
68
102
|
excluded: Object.freeze(excluded),
|
|
69
103
|
});
|
|
70
104
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface TimeProvider {
|
|
2
|
+
readonly now: () => number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const systemTimeProvider: TimeProvider = Object.freeze({
|
|
6
|
+
now: () => Date.now(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function createFixedTimeProvider(
|
|
10
|
+
initialTimeMs: number,
|
|
11
|
+
): TimeProvider & { advance: (ms: number) => void; set: (ms: number) => void } {
|
|
12
|
+
let currentTime = initialTimeMs;
|
|
13
|
+
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
now: () => currentTime,
|
|
16
|
+
advance: (ms: number) => {
|
|
17
|
+
currentTime += ms;
|
|
18
|
+
},
|
|
19
|
+
set: (ms: number) => {
|
|
20
|
+
currentTime = ms;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
package/src/tools/doctor.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function setOpenCodeConfig(config: Config | null): void {
|
|
|
17
17
|
*/
|
|
18
18
|
interface DoctorCheck {
|
|
19
19
|
readonly name: string;
|
|
20
|
-
readonly status: "pass" | "fail";
|
|
20
|
+
readonly status: "pass" | "warn" | "fail";
|
|
21
21
|
readonly message: string;
|
|
22
22
|
readonly fixSuggestion: string | null;
|
|
23
23
|
}
|
|
@@ -120,7 +120,7 @@ function formatCheck(result: HealthResult): DoctorCheck {
|
|
|
120
120
|
*/
|
|
121
121
|
function buildDisplayText(checks: readonly DoctorCheck[], duration: number): string {
|
|
122
122
|
const lines = checks.map((c) => {
|
|
123
|
-
const icon = c.status === "pass" ? "OK" : "FAIL";
|
|
123
|
+
const icon = c.status === "pass" ? "OK" : c.status === "warn" ? "WARN" : "FAIL";
|
|
124
124
|
const line = `[${icon}] ${c.name}: ${c.message}`;
|
|
125
125
|
return c.fixSuggestion ? `${line}\n Fix: ${c.fixSuggestion}` : line;
|
|
126
126
|
});
|
package/src/tools/logs.ts
CHANGED
|
@@ -22,14 +22,14 @@ import {
|
|
|
22
22
|
} from "../observability/log-reader";
|
|
23
23
|
import { generateSessionSummary } from "../observability/summary-generator";
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
* Options for logsCore search/detail modes.
|
|
27
|
-
*/
|
|
28
25
|
interface LogsOptions {
|
|
29
26
|
readonly sessionID?: string;
|
|
30
27
|
readonly eventType?: string;
|
|
31
28
|
readonly after?: string;
|
|
32
29
|
readonly before?: string;
|
|
30
|
+
readonly domain?: string;
|
|
31
|
+
readonly subsystem?: string;
|
|
32
|
+
readonly severity?: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -135,6 +135,9 @@ export async function logsCore(
|
|
|
135
135
|
type: options?.eventType,
|
|
136
136
|
after: options?.after,
|
|
137
137
|
before: options?.before,
|
|
138
|
+
domain: options?.domain,
|
|
139
|
+
subsystem: options?.subsystem,
|
|
140
|
+
severity: options?.severity,
|
|
138
141
|
});
|
|
139
142
|
|
|
140
143
|
const displayLines = [
|
|
@@ -146,6 +149,15 @@ export async function logsCore(
|
|
|
146
149
|
return JSON.stringify({
|
|
147
150
|
action: "logs_search",
|
|
148
151
|
sessionId: log.sessionId,
|
|
152
|
+
filters: {
|
|
153
|
+
eventType: options?.eventType,
|
|
154
|
+
after: options?.after,
|
|
155
|
+
before: options?.before,
|
|
156
|
+
domain: options?.domain,
|
|
157
|
+
subsystem: options?.subsystem,
|
|
158
|
+
severity: options?.severity,
|
|
159
|
+
},
|
|
160
|
+
matchCount: filtered.length,
|
|
149
161
|
events: filtered,
|
|
150
162
|
displayText: displayLines.join("\n"),
|
|
151
163
|
});
|
|
@@ -158,7 +170,7 @@ export async function logsCore(
|
|
|
158
170
|
export const ocLogs = tool({
|
|
159
171
|
description:
|
|
160
172
|
"View session logs. Modes: 'list' shows all sessions, 'detail' shows full log with " +
|
|
161
|
-
"summary, 'search' filters events by type/time. Use to inspect session history and errors.",
|
|
173
|
+
"summary, 'search' filters events by type/time/domain/subsystem/severity. Use to inspect session history and errors.",
|
|
162
174
|
args: {
|
|
163
175
|
mode: z.enum(["list", "detail", "search"]).describe("View mode: list, detail, or search"),
|
|
164
176
|
sessionID: z
|
|
@@ -172,8 +184,22 @@ export const ocLogs = tool({
|
|
|
172
184
|
.string()
|
|
173
185
|
.optional()
|
|
174
186
|
.describe("Only events before this ISO timestamp (for search mode)"),
|
|
187
|
+
domain: z
|
|
188
|
+
.string()
|
|
189
|
+
.optional()
|
|
190
|
+
.describe("Filter events by domain (e.g. 'session', 'orchestrator') (for search mode)"),
|
|
191
|
+
subsystem: z
|
|
192
|
+
.string()
|
|
193
|
+
.optional()
|
|
194
|
+
.describe("Filter events by payload.subsystem field (for search mode)"),
|
|
195
|
+
severity: z
|
|
196
|
+
.string()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe(
|
|
199
|
+
"Filter by severity: matches event.type (e.g. 'error', 'warning') or payload.severity/payload.level (for search mode)",
|
|
200
|
+
),
|
|
175
201
|
},
|
|
176
|
-
async execute({ mode, sessionID, eventType, after, before }) {
|
|
177
|
-
return logsCore(mode, { sessionID, eventType, after, before });
|
|
202
|
+
async execute({ mode, sessionID, eventType, after, before, domain, subsystem, severity }) {
|
|
203
|
+
return logsCore(mode, { sessionID, eventType, after, before, domain, subsystem, severity });
|
|
178
204
|
},
|
|
179
205
|
});
|