@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.
Files changed (53) hide show
  1. package/bin/inspect.ts +2 -2
  2. package/package.json +1 -1
  3. package/src/config/index.ts +29 -0
  4. package/src/config/migrations.ts +196 -0
  5. package/src/config/v7.ts +45 -0
  6. package/src/config.ts +3 -3
  7. package/src/health/checks.ts +97 -0
  8. package/src/health/types.ts +1 -1
  9. package/src/index.ts +25 -2
  10. package/src/kernel/transaction.ts +48 -0
  11. package/src/kernel/types.ts +1 -2
  12. package/src/logging/domains.ts +39 -0
  13. package/src/logging/forensic-writer.ts +177 -0
  14. package/src/logging/index.ts +4 -0
  15. package/src/logging/logger.ts +44 -0
  16. package/src/logging/performance.ts +59 -0
  17. package/src/logging/rotation.ts +261 -0
  18. package/src/logging/types.ts +33 -0
  19. package/src/memory/capture-utils.ts +149 -0
  20. package/src/memory/capture.ts +16 -197
  21. package/src/memory/decay.ts +11 -2
  22. package/src/memory/injector.ts +4 -1
  23. package/src/memory/lessons.ts +85 -0
  24. package/src/memory/observations.ts +177 -0
  25. package/src/memory/preferences.ts +718 -0
  26. package/src/memory/projects.ts +83 -0
  27. package/src/memory/repository.ts +46 -1001
  28. package/src/memory/retrieval.ts +5 -1
  29. package/src/observability/context-display.ts +8 -0
  30. package/src/observability/event-handlers.ts +44 -6
  31. package/src/observability/forensic-log.ts +10 -2
  32. package/src/observability/forensic-schemas.ts +9 -1
  33. package/src/observability/log-reader.ts +20 -1
  34. package/src/orchestrator/error-context.ts +24 -0
  35. package/src/orchestrator/handlers/build-utils.ts +118 -0
  36. package/src/orchestrator/handlers/build.ts +13 -148
  37. package/src/orchestrator/handlers/retrospective.ts +0 -1
  38. package/src/orchestrator/lesson-memory.ts +7 -2
  39. package/src/orchestrator/orchestration-logger.ts +46 -31
  40. package/src/orchestrator/progress.ts +63 -0
  41. package/src/review/memory.ts +11 -3
  42. package/src/review/parse-findings.ts +116 -0
  43. package/src/review/pipeline.ts +3 -107
  44. package/src/review/selection.ts +38 -4
  45. package/src/scoring/time-provider.ts +23 -0
  46. package/src/tools/doctor.ts +2 -2
  47. package/src/tools/logs.ts +32 -6
  48. package/src/tools/orchestrate.ts +11 -9
  49. package/src/tools/replay.ts +42 -0
  50. package/src/tools/review.ts +8 -2
  51. package/src/tools/summary.ts +43 -0
  52. package/src/utils/random.ts +33 -0
  53. package/src/ux/session-summary.ts +56 -0
package/bin/inspect.ts CHANGED
@@ -249,7 +249,7 @@ export async function inspectCliCore(
249
249
  );
250
250
  }
251
251
  case "project": {
252
- const details = getProjectDetails(parsed.projectRef!, dbInput);
252
+ const details = getProjectDetails(parsed.projectRef ?? "", dbInput);
253
253
  if (details === null) {
254
254
  return makeError(`Project not found: ${parsed.projectRef}`, parsed.json);
255
255
  }
@@ -260,7 +260,7 @@ export async function inspectCliCore(
260
260
  );
261
261
  }
262
262
  case "paths": {
263
- const details = getProjectDetails(parsed.projectRef!, dbInput);
263
+ const details = getProjectDetails(parsed.projectRef ?? "", dbInput);
264
264
  if (details === null) {
265
265
  return makeError(`Project not found: ${parsed.projectRef}`, parsed.json);
266
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodrunhq/opencode-autopilot",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Curated agents, skills, and commands for the OpenCode AI coding CLI — autonomous orchestrator, multi-agent code review, model fallback, and in-session asset creation tools.",
5
5
  "main": "src/index.ts",
6
6
  "keywords": [
@@ -0,0 +1,29 @@
1
+ export {
2
+ CONFIG_PATH,
3
+ confidenceConfigSchema,
4
+ confidenceDefaults,
5
+ createDefaultConfig,
6
+ isFirstLoad,
7
+ loadConfig,
8
+ memoryConfigSchema,
9
+ memoryDefaults,
10
+ orchestratorConfigSchema,
11
+ orchestratorDefaults,
12
+ type PluginConfig,
13
+ pluginConfigSchema,
14
+ saveConfig,
15
+ } from "../config";
16
+ // Re-export migration schemas and functions for backward compatibility
17
+ export {
18
+ migrateV1toV2,
19
+ migrateV2toV3,
20
+ migrateV3toV4,
21
+ migrateV4toV5,
22
+ migrateV5toV6,
23
+ pluginConfigSchemaV1,
24
+ pluginConfigSchemaV2,
25
+ pluginConfigSchemaV3,
26
+ pluginConfigSchemaV4,
27
+ pluginConfigSchemaV5,
28
+ } from "./migrations";
29
+ export { migrateV6toV7, type PluginConfigV7, v7ConfigDefaults } from "./v7";
@@ -0,0 +1,196 @@
1
+ import { z } from "zod";
2
+ import {
3
+ confidenceConfigSchema,
4
+ confidenceDefaults,
5
+ memoryConfigSchema,
6
+ memoryDefaults,
7
+ orchestratorConfigSchema,
8
+ orchestratorDefaults,
9
+ } from "../config";
10
+ import {
11
+ fallbackConfigSchema,
12
+ fallbackDefaults,
13
+ testModeDefaults,
14
+ } from "../orchestrator/fallback/fallback-config";
15
+ import { AGENT_REGISTRY, ALL_GROUP_IDS } from "../registry/model-groups";
16
+
17
+ export const pluginConfigSchemaV1 = z.object({
18
+ version: z.literal(1),
19
+ configured: z.boolean(),
20
+ models: z.record(z.string(), z.string()),
21
+ });
22
+
23
+ export type PluginConfigV1 = z.infer<typeof pluginConfigSchemaV1>;
24
+
25
+ export const pluginConfigSchemaV2 = z.object({
26
+ version: z.literal(2),
27
+ configured: z.boolean(),
28
+ models: z.record(z.string(), z.string()),
29
+ orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
30
+ confidence: confidenceConfigSchema.default(confidenceDefaults),
31
+ });
32
+
33
+ export type PluginConfigV2 = z.infer<typeof pluginConfigSchemaV2>;
34
+
35
+ export const pluginConfigSchemaV3 = z.object({
36
+ version: z.literal(3),
37
+ configured: z.boolean(),
38
+ models: z.record(z.string(), z.string()),
39
+ orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
40
+ confidence: confidenceConfigSchema.default(confidenceDefaults),
41
+ fallback: fallbackConfigSchema.default(fallbackDefaults),
42
+ fallback_models: z.union([z.string(), z.array(z.string())]).optional(),
43
+ });
44
+
45
+ export type PluginConfigV3 = z.infer<typeof pluginConfigSchemaV3>;
46
+
47
+ const groupModelAssignmentSchema = z.object({
48
+ primary: z.string().min(1),
49
+ fallbacks: z.array(z.string().min(1)).default([]),
50
+ });
51
+
52
+ const agentOverrideSchema = z.object({
53
+ primary: z.string().min(1),
54
+ fallbacks: z.array(z.string().min(1)).optional(),
55
+ });
56
+
57
+ export const pluginConfigSchemaV4 = z
58
+ .object({
59
+ version: z.literal(4),
60
+ configured: z.boolean(),
61
+ groups: z.record(z.string(), groupModelAssignmentSchema).default({}),
62
+ overrides: z.record(z.string(), agentOverrideSchema).default({}),
63
+ orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
64
+ confidence: confidenceConfigSchema.default(confidenceDefaults),
65
+ fallback: fallbackConfigSchema.default(fallbackDefaults),
66
+ })
67
+ .superRefine((config, ctx) => {
68
+ for (const groupId of Object.keys(config.groups)) {
69
+ if (!ALL_GROUP_IDS.includes(groupId as (typeof ALL_GROUP_IDS)[number])) {
70
+ ctx.addIssue({
71
+ code: z.ZodIssueCode.custom,
72
+ path: ["groups", groupId],
73
+ message: `Unknown group id "${groupId}". Expected one of: ${ALL_GROUP_IDS.join(", ")}`,
74
+ });
75
+ }
76
+ }
77
+ });
78
+
79
+ export type PluginConfigV4 = z.infer<typeof pluginConfigSchemaV4>;
80
+
81
+ export const pluginConfigSchemaV5 = z
82
+ .object({
83
+ version: z.literal(5),
84
+ configured: z.boolean(),
85
+ groups: z.record(z.string(), groupModelAssignmentSchema).default({}),
86
+ overrides: z.record(z.string(), agentOverrideSchema).default({}),
87
+ orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
88
+ confidence: confidenceConfigSchema.default(confidenceDefaults),
89
+ fallback: fallbackConfigSchema.default(fallbackDefaults),
90
+ memory: memoryConfigSchema.default(memoryDefaults),
91
+ })
92
+ .superRefine((config, ctx) => {
93
+ for (const groupId of Object.keys(config.groups)) {
94
+ if (!ALL_GROUP_IDS.includes(groupId as (typeof ALL_GROUP_IDS)[number])) {
95
+ ctx.addIssue({
96
+ code: z.ZodIssueCode.custom,
97
+ path: ["groups", groupId],
98
+ message: `Unknown group id "${groupId}". Expected one of: ${ALL_GROUP_IDS.join(", ")}`,
99
+ });
100
+ }
101
+ }
102
+ });
103
+
104
+ export type PluginConfigV5 = z.infer<typeof pluginConfigSchemaV5>;
105
+
106
+ export function migrateV1toV2(v1Config: PluginConfigV1): PluginConfigV2 {
107
+ return {
108
+ version: 2 as const,
109
+ configured: v1Config.configured,
110
+ models: v1Config.models,
111
+ orchestrator: orchestratorDefaults,
112
+ confidence: confidenceDefaults,
113
+ };
114
+ }
115
+
116
+ export function migrateV2toV3(v2Config: PluginConfigV2): PluginConfigV3 {
117
+ return {
118
+ version: 3 as const,
119
+ configured: v2Config.configured,
120
+ models: v2Config.models,
121
+ orchestrator: v2Config.orchestrator,
122
+ confidence: v2Config.confidence,
123
+ fallback: fallbackDefaults,
124
+ };
125
+ }
126
+
127
+ export function migrateV3toV4(v3Config: PluginConfigV3): PluginConfigV4 {
128
+ const groups: Record<string, { primary: string; fallbacks: string[] }> = {};
129
+ const overrides: Record<string, { primary: string }> = {};
130
+
131
+ for (const [agentName, modelId] of Object.entries(v3Config.models)) {
132
+ const entry = AGENT_REGISTRY[agentName];
133
+ if (!entry) {
134
+ overrides[agentName] = { primary: modelId };
135
+ continue;
136
+ }
137
+
138
+ const groupId = entry.group;
139
+ if (!groups[groupId]) {
140
+ groups[groupId] = { primary: modelId, fallbacks: [] };
141
+ } else if (groups[groupId].primary !== modelId) {
142
+ overrides[agentName] = { primary: modelId };
143
+ }
144
+ }
145
+
146
+ const globalFallbacks = v3Config.fallback_models
147
+ ? typeof v3Config.fallback_models === "string"
148
+ ? [v3Config.fallback_models]
149
+ : [...v3Config.fallback_models]
150
+ : [];
151
+
152
+ for (const group of Object.values(groups)) {
153
+ if (group.fallbacks.length === 0 && globalFallbacks.length > 0) {
154
+ group.fallbacks = [...globalFallbacks];
155
+ }
156
+ }
157
+
158
+ return {
159
+ version: 4 as const,
160
+ configured: v3Config.configured,
161
+ groups,
162
+ overrides,
163
+ orchestrator: v3Config.orchestrator,
164
+ confidence: v3Config.confidence,
165
+ fallback: v3Config.fallback,
166
+ };
167
+ }
168
+
169
+ export function migrateV4toV5(v4Config: PluginConfigV4): PluginConfigV5 {
170
+ return {
171
+ version: 5 as const,
172
+ configured: v4Config.configured,
173
+ groups: v4Config.groups,
174
+ overrides: v4Config.overrides,
175
+ orchestrator: v4Config.orchestrator,
176
+ confidence: v4Config.confidence,
177
+ fallback: v4Config.fallback,
178
+ memory: memoryDefaults,
179
+ };
180
+ }
181
+
182
+ export function migrateV5toV6(
183
+ v5Config: PluginConfigV5,
184
+ _fallbackDefaultsV6: typeof fallbackDefaults,
185
+ ) {
186
+ return {
187
+ version: 6 as const,
188
+ configured: v5Config.configured,
189
+ groups: v5Config.groups,
190
+ overrides: v5Config.overrides,
191
+ orchestrator: v5Config.orchestrator,
192
+ confidence: v5Config.confidence,
193
+ fallback: { ...v5Config.fallback, testMode: testModeDefaults },
194
+ memory: v5Config.memory,
195
+ };
196
+ }
@@ -0,0 +1,45 @@
1
+ import type { PluginConfig } from "../config";
2
+
3
+ export type PluginConfigV7 = Omit<PluginConfig, "version"> & {
4
+ readonly version: 7;
5
+ readonly background?: {
6
+ readonly enabled: boolean;
7
+ readonly maxConcurrent: number;
8
+ readonly defaultTimeout: number;
9
+ };
10
+ readonly autonomy?: {
11
+ readonly enabled: boolean;
12
+ readonly verification: "strict" | "normal" | "lenient";
13
+ readonly maxIterations: number;
14
+ };
15
+ };
16
+
17
+ export function migrateV6toV7(v6Config: PluginConfig): PluginConfigV7 {
18
+ return {
19
+ ...v6Config,
20
+ version: 7,
21
+ background: {
22
+ enabled: true,
23
+ maxConcurrent: 5,
24
+ defaultTimeout: 300000,
25
+ },
26
+ autonomy: {
27
+ enabled: false,
28
+ verification: "normal",
29
+ maxIterations: 10,
30
+ },
31
+ };
32
+ }
33
+
34
+ export const v7ConfigDefaults = {
35
+ background: {
36
+ enabled: true,
37
+ maxConcurrent: 5,
38
+ defaultTimeout: 300000,
39
+ },
40
+ autonomy: {
41
+ enabled: false,
42
+ verification: "normal",
43
+ maxIterations: 10,
44
+ },
45
+ } as const;
package/src/config.ts CHANGED
@@ -53,8 +53,8 @@ export const confidenceConfigSchema = z.object({
53
53
  });
54
54
 
55
55
  // Pre-compute full defaults for nested schema defaults
56
- const orchestratorDefaults = orchestratorConfigSchema.parse({});
57
- const confidenceDefaults = confidenceConfigSchema.parse({});
56
+ export const orchestratorDefaults = orchestratorConfigSchema.parse({});
57
+ export const confidenceDefaults = confidenceConfigSchema.parse({});
58
58
 
59
59
  // --- V2 schema (internal, for migration) ---
60
60
 
@@ -90,7 +90,7 @@ export const memoryConfigSchema = z.object({
90
90
  decayHalfLifeDays: z.number().min(7).max(365).default(90),
91
91
  });
92
92
 
93
- const memoryDefaults = memoryConfigSchema.parse({});
93
+ export const memoryDefaults = memoryConfigSchema.parse({});
94
94
 
95
95
  // --- V4 sub-schemas ---
96
96
 
@@ -44,6 +44,103 @@ export async function configHealthCheck(configPath?: string): Promise<HealthResu
44
44
  }
45
45
  }
46
46
 
47
+ const LATEST_CONFIG_VERSION = 6;
48
+
49
+ export async function configVersionCheck(configPath?: string): Promise<HealthResult> {
50
+ try {
51
+ const config = await loadConfig(configPath);
52
+ if (config === null) {
53
+ return Object.freeze({
54
+ name: "config-version",
55
+ status: "fail" as const,
56
+ message: "Config file not found",
57
+ });
58
+ }
59
+ if (config.version < LATEST_CONFIG_VERSION) {
60
+ return Object.freeze({
61
+ name: "config-version",
62
+ status: "warn" as const,
63
+ message: `Config v${config.version} is outdated (latest: v${LATEST_CONFIG_VERSION}). Auto-migration will upgrade on next load.`,
64
+ });
65
+ }
66
+ return Object.freeze({
67
+ name: "config-version",
68
+ status: "pass" as const,
69
+ message: `Config is on latest version (v${config.version})`,
70
+ });
71
+ } catch (error: unknown) {
72
+ const msg = error instanceof Error ? error.message : String(error);
73
+ return Object.freeze({
74
+ name: "config-version",
75
+ status: "fail" as const,
76
+ message: `Config version check failed: ${msg}`,
77
+ });
78
+ }
79
+ }
80
+
81
+ const REQUIRED_GROUPS: readonly string[] = Object.freeze([
82
+ "architects",
83
+ "challengers",
84
+ "builders",
85
+ "reviewers",
86
+ "red-team",
87
+ "researchers",
88
+ "communicators",
89
+ "utilities",
90
+ ]);
91
+
92
+ export async function configGroupsCheck(configPath?: string): Promise<HealthResult> {
93
+ try {
94
+ const config = await loadConfig(configPath);
95
+ if (config === null) {
96
+ return Object.freeze({
97
+ name: "config-groups",
98
+ status: "fail" as const,
99
+ message: "Config file not found",
100
+ });
101
+ }
102
+
103
+ const assignedGroups = Object.keys(config.groups);
104
+ const missingGroups = REQUIRED_GROUPS.filter((g) => !assignedGroups.includes(g));
105
+
106
+ if (missingGroups.length > 0) {
107
+ return Object.freeze({
108
+ name: "config-groups",
109
+ status: "warn" as const,
110
+ message: `Missing model assignments for groups: ${missingGroups.join(", ")}`,
111
+ details: Object.freeze(missingGroups),
112
+ });
113
+ }
114
+
115
+ const groupsWithoutPrimary = assignedGroups.filter((g) => {
116
+ const group = config.groups[g];
117
+ return !group?.primary;
118
+ });
119
+
120
+ if (groupsWithoutPrimary.length > 0) {
121
+ return Object.freeze({
122
+ name: "config-groups",
123
+ status: "warn" as const,
124
+ message: `Groups without primary model: ${groupsWithoutPrimary.join(", ")}`,
125
+ details: Object.freeze(groupsWithoutPrimary),
126
+ });
127
+ }
128
+
129
+ return Object.freeze({
130
+ name: "config-groups",
131
+ status: "pass" as const,
132
+ message: `All ${REQUIRED_GROUPS.length} required groups have primary models assigned`,
133
+ });
134
+ } catch (error: unknown) {
135
+ const msg = error instanceof Error ? error.message : String(error);
136
+ return Object.freeze({
137
+ name: "config-groups",
138
+ status: "fail" as const,
139
+ message: `Config groups check failed: ${msg}`,
140
+ });
141
+ }
142
+ }
143
+
47
144
  /** Standard agent names, derived from the agents barrel export. */
48
145
  const STANDARD_AGENT_NAMES: readonly string[] = Object.freeze([
49
146
  "researcher",
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export interface HealthResult {
6
6
  readonly name: string;
7
- readonly status: "pass" | "fail";
7
+ readonly status: "pass" | "warn" | "fail";
8
8
  readonly message: string;
9
9
  readonly details?: readonly string[];
10
10
  }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { isFirstLoad, loadConfig } from "./config";
4
4
  import { runHealthChecks } from "./health/runner";
5
5
  import { createAntiSlopHandler } from "./hooks/anti-slop";
6
6
  import { installAssets } from "./installer";
7
+ import { getLogger, initLoggers } from "./logging/domains";
7
8
  import {
8
9
  createMemoryCaptureHandler,
9
10
  createMemoryChatMessageHandler,
@@ -55,17 +56,36 @@ import { ocReview } from "./tools/review";
55
56
  import { ocSessionStats } from "./tools/session-stats";
56
57
  import { ocState } from "./tools/state";
57
58
  import { ocStocktake } from "./tools/stocktake";
59
+ import { ocSummary } from "./tools/summary";
58
60
  import { ocUpdateDocs } from "./tools/update-docs";
59
61
 
60
62
  let openCodeConfig: Config | null = null;
61
63
 
64
+ let processHandlersRegistered = false;
65
+ function registerProcessHandlers() {
66
+ if (processHandlersRegistered) return;
67
+ processHandlersRegistered = true;
68
+ process.on("uncaughtException", (error) => {
69
+ getLogger("system").error("Uncaught exception", {
70
+ error: error instanceof Error ? error.stack : String(error),
71
+ });
72
+ });
73
+ process.on("unhandledRejection", (reason) => {
74
+ getLogger("system").error("Unhandled rejection", {
75
+ reason: reason instanceof Error ? reason.stack : String(reason),
76
+ });
77
+ });
78
+ }
79
+
62
80
  const plugin: Plugin = async (input) => {
63
81
  const client = input.client;
82
+ initLoggers(process.cwd());
83
+ registerProcessHandlers();
64
84
 
65
85
  // Self-healing asset installation on every load
66
86
  const installResult = await installAssets();
67
87
  if (installResult.errors.length > 0) {
68
- console.error("[opencode-autopilot] Asset installation errors:", installResult.errors);
88
+ getLogger("system").warn("Asset installation errors", { errors: installResult.errors });
69
89
  }
70
90
 
71
91
  // Discover available providers/models in the background (non-blocking).
@@ -102,7 +122,9 @@ const plugin: Plugin = async (input) => {
102
122
 
103
123
  // Retention pruning on load (non-blocking per D-14)
104
124
  pruneOldLogs().catch((err) => {
105
- console.error("[opencode-autopilot]", err);
125
+ getLogger("system").error("Log retention pruning failed", {
126
+ error: err instanceof Error ? err.stack : String(err),
127
+ });
106
128
  });
107
129
 
108
130
  // --- Fallback subsystem initialization ---
@@ -307,6 +329,7 @@ const plugin: Plugin = async (input) => {
307
329
  oc_logs: ocLogs,
308
330
  oc_session_stats: ocSessionStats,
309
331
  oc_pipeline_report: ocPipelineReport,
332
+ oc_summary: ocSummary,
310
333
  oc_mock_fallback: ocMockFallback,
311
334
  oc_stocktake: ocStocktake,
312
335
  oc_update_docs: ocUpdateDocs,
@@ -0,0 +1,48 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ export interface TransactionOptions {
4
+ maxRetries?: number;
5
+ backoffMs?: number;
6
+ useImmediate?: boolean;
7
+ }
8
+
9
+ export function withTransaction<T>(db: Database, fn: () => T, options: TransactionOptions = {}): T {
10
+ const maxRetries = options.maxRetries ?? 5;
11
+ const backoffMs = options.backoffMs ?? 100;
12
+ const useImmediate = options.useImmediate ?? true;
13
+
14
+ let attempts = 0;
15
+ while (true) {
16
+ try {
17
+ if (useImmediate) {
18
+ db.run("BEGIN IMMEDIATE");
19
+ try {
20
+ const result = fn();
21
+ db.run("COMMIT");
22
+ return result;
23
+ } catch (innerError) {
24
+ db.run("ROLLBACK");
25
+ throw innerError;
26
+ }
27
+ }
28
+
29
+ const transaction = db.transaction(fn);
30
+ return transaction();
31
+ } catch (error: unknown) {
32
+ const e = error as Error;
33
+ const isBusyError =
34
+ e.message &&
35
+ (e.message.includes("database is locked") ||
36
+ e.message.includes("SQLITE_BUSY") ||
37
+ e.message.includes("database table is locked"));
38
+
39
+ if (isBusyError && attempts < maxRetries) {
40
+ attempts++;
41
+ const waitTime = backoffMs * attempts;
42
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitTime);
43
+ continue;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ }
@@ -1,7 +1,6 @@
1
1
  import type { ForensicEvent } from "../observability/forensic-types";
2
- import type { LessonMemory } from "../orchestrator/lesson-types";
3
2
  import type { PipelineState } from "../orchestrator/types";
4
- import type { ReviewMemory, ReviewState } from "../review/types";
3
+ import type { ReviewState } from "../review/types";
5
4
 
6
5
  export const KERNEL_STATE_CONFLICT_CODE = "E_STATE_CONFLICT";
7
6
 
@@ -0,0 +1,39 @@
1
+ import { createForensicSink } from "./forensic-writer";
2
+ import { BaseLogger } from "./logger";
3
+ import type { LogEntry, Logger, LogMetadata, LogSink } from "./types";
4
+
5
+ export class MultiplexSink implements LogSink {
6
+ constructor(private readonly sinks: readonly LogSink[]) {}
7
+
8
+ write(entry: LogEntry): void {
9
+ for (const sink of this.sinks) {
10
+ sink.write(entry);
11
+ }
12
+ }
13
+ }
14
+
15
+ let rootLogger: Logger | null = null;
16
+
17
+ export function initLoggers(projectRoot: string, sinks?: readonly LogSink[]): void {
18
+ const resolvedSinks = sinks ?? [createForensicSink(projectRoot)];
19
+ rootLogger = new BaseLogger(new MultiplexSink(resolvedSinks), { domain: "system" });
20
+ }
21
+
22
+ export function getLogger(domain: string, subsystem?: string): Logger {
23
+ if (!rootLogger) {
24
+ return new BaseLogger(
25
+ {
26
+ write(entry: LogEntry): void {
27
+ console.log(entry.level, entry.message);
28
+ },
29
+ },
30
+ compactMetadata(domain, subsystem),
31
+ );
32
+ }
33
+
34
+ return rootLogger.child(compactMetadata(domain, subsystem));
35
+ }
36
+
37
+ function compactMetadata(domain: string, subsystem?: string): LogMetadata {
38
+ return subsystem ? { domain, subsystem } : { domain };
39
+ }