@kodrunhq/opencode-autopilot 1.16.0 → 1.18.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.
Files changed (61) hide show
  1. package/assets/commands/oc-doctor.md +17 -0
  2. package/bin/configure-tui.ts +1 -1
  3. package/bin/inspect.ts +2 -2
  4. package/package.json +1 -1
  5. package/src/config/index.ts +29 -0
  6. package/src/config/migrations.ts +196 -0
  7. package/src/config/v7.ts +45 -0
  8. package/src/config.ts +108 -24
  9. package/src/health/checks.ts +165 -0
  10. package/src/health/runner.ts +8 -2
  11. package/src/health/types.ts +1 -1
  12. package/src/index.ts +25 -2
  13. package/src/kernel/transaction.ts +48 -0
  14. package/src/kernel/types.ts +1 -2
  15. package/src/logging/domains.ts +39 -0
  16. package/src/logging/forensic-writer.ts +177 -0
  17. package/src/logging/index.ts +4 -0
  18. package/src/logging/logger.ts +44 -0
  19. package/src/logging/performance.ts +59 -0
  20. package/src/logging/rotation.ts +261 -0
  21. package/src/logging/types.ts +33 -0
  22. package/src/memory/capture-utils.ts +149 -0
  23. package/src/memory/capture.ts +16 -197
  24. package/src/memory/decay.ts +11 -2
  25. package/src/memory/injector.ts +4 -1
  26. package/src/memory/lessons.ts +85 -0
  27. package/src/memory/observations.ts +177 -0
  28. package/src/memory/preferences.ts +718 -0
  29. package/src/memory/projects.ts +83 -0
  30. package/src/memory/repository.ts +46 -1001
  31. package/src/memory/retrieval.ts +5 -1
  32. package/src/observability/context-display.ts +8 -0
  33. package/src/observability/event-handlers.ts +44 -6
  34. package/src/observability/forensic-log.ts +10 -2
  35. package/src/observability/forensic-schemas.ts +9 -1
  36. package/src/observability/log-reader.ts +20 -1
  37. package/src/orchestrator/error-context.ts +24 -0
  38. package/src/orchestrator/handlers/build-utils.ts +118 -0
  39. package/src/orchestrator/handlers/build.ts +13 -148
  40. package/src/orchestrator/handlers/retrospective.ts +0 -1
  41. package/src/orchestrator/lesson-memory.ts +7 -2
  42. package/src/orchestrator/orchestration-logger.ts +46 -31
  43. package/src/orchestrator/progress.ts +63 -0
  44. package/src/review/memory.ts +11 -3
  45. package/src/review/parse-findings.ts +116 -0
  46. package/src/review/pipeline.ts +3 -107
  47. package/src/review/selection.ts +38 -4
  48. package/src/scoring/time-provider.ts +23 -0
  49. package/src/tools/configure.ts +1 -1
  50. package/src/tools/doctor.ts +2 -2
  51. package/src/tools/logs.ts +32 -6
  52. package/src/tools/orchestrate.ts +11 -9
  53. package/src/tools/replay.ts +42 -0
  54. package/src/tools/review.ts +8 -2
  55. package/src/tools/summary.ts +43 -0
  56. package/src/types/background.ts +51 -0
  57. package/src/types/mcp.ts +27 -0
  58. package/src/types/recovery.ts +39 -0
  59. package/src/types/routing.ts +39 -0
  60. package/src/utils/random.ts +33 -0
  61. package/src/ux/session-summary.ts +56 -0
@@ -1,4 +1,6 @@
1
- import { appendForensicEventForArtifactDir } from "../observability/forensic-log";
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
- * Append an orchestration event to the project-local JSONL log.
21
- * Uses synchronous append to survive crashes. Best-effort errors are swallowed.
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
- appendForensicEventForArtifactDir(artifactDir, {
25
- timestamp: event.timestamp,
26
- domain: event.action === "error" && event.code?.startsWith("E_") ? "contract" : "orchestrator",
27
- runId: event.runId ?? null,
28
- sessionId: event.sessionId ?? null,
29
- phase: event.phase,
30
- dispatchId: event.dispatchId ?? null,
31
- taskId: event.taskId ?? null,
32
- agent: event.agent ?? null,
33
- type:
34
- event.action === "dispatch"
35
- ? "dispatch"
36
- : event.action === "dispatch_multi"
37
- ? "dispatch_multi"
38
- : event.action === "complete"
39
- ? "complete"
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
+ }
@@ -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
- console.warn("[opencode-autopilot] review-memory.json mirror write failed:", error);
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
- export function pruneMemory(memory: ReviewMemory): ReviewMemory {
110
- const now = Date.now();
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
+ }
@@ -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";
@@ -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 selected: SelectableAgent[] = [];
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
- selected.push(agent);
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
- selected.push(agent);
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(selected),
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
+ }
@@ -314,7 +314,7 @@ async function handleCommit(configPath?: string): Promise<string> {
314
314
  }
315
315
  const newConfig = {
316
316
  ...currentConfig,
317
- version: 6 as const,
317
+ version: 7 as const,
318
318
  configured: true,
319
319
  groups: groupsRecord,
320
320
  overrides: currentConfig.overrides ?? {},
@@ -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
  });