@kodrunhq/opencode-autopilot 1.17.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.
@@ -0,0 +1,17 @@
1
+ ---
2
+ # opencode-autopilot
3
+ description: Run plugin health diagnostics — config validity, agent injection, native suppression, assets, memory, commands, and v7 config fields (background, routing, recovery, mcp)
4
+ ---
5
+
6
+ Invoke the `oc_doctor` tool to run a full health check on the opencode-autopilot plugin.
7
+
8
+ `oc_doctor` runs the current built-in health checks and reports the results by diagnostic area. These checks cover:
9
+
10
+ - **Config health** — The plugin config exists, parses correctly, and required modern config sections such as `background`, `routing`, `recovery`, and `mcp` are present or reported as needing migration.
11
+ - **Agent setup** — Expected autopilot agents are present in the OpenCode config, and native `plan`/`build` agents are suppressed as subagents.
12
+ - **Assets and installation paths** — Bundled asset directories and the global `~/.config/opencode/` target are accessible.
13
+ - **Skill loading** — Skills load correctly and are filtered against the detected project stack.
14
+ - **Memory storage** — The memory SQLite database is available, readable, or cleanly reported as not yet initialized on first install.
15
+ - **Command files** — Expected slash command files exist and have valid YAML frontmatter.
16
+
17
+ Each failing check includes a **Fix** suggestion. Run this after installation, after upgrades, or whenever something feels off.
@@ -307,7 +307,7 @@ export async function runConfigure(configPath: string = CONFIG_PATH): Promise<vo
307
307
 
308
308
  const newConfig = {
309
309
  ...baseConfig,
310
- version: 6 as const,
310
+ version: 7 as const,
311
311
  configured: true,
312
312
  groups: groupsRecord,
313
313
  overrides: baseConfig.overrides ?? {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodrunhq/opencode-autopilot",
3
- "version": "1.17.0",
3
+ "version": "1.18.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": [
package/src/config/v7.ts CHANGED
@@ -5,7 +5,7 @@ export type PluginConfigV7 = Omit<PluginConfig, "version"> & {
5
5
  readonly background?: {
6
6
  readonly enabled: boolean;
7
7
  readonly maxConcurrent: number;
8
- readonly defaultTimeout: number;
8
+ readonly persistence: boolean;
9
9
  };
10
10
  readonly autonomy?: {
11
11
  readonly enabled: boolean;
@@ -21,7 +21,7 @@ export function migrateV6toV7(v6Config: PluginConfig): PluginConfigV7 {
21
21
  background: {
22
22
  enabled: true,
23
23
  maxConcurrent: 5,
24
- defaultTimeout: 300000,
24
+ persistence: true,
25
25
  },
26
26
  autonomy: {
27
27
  enabled: false,
@@ -35,7 +35,7 @@ export const v7ConfigDefaults = {
35
35
  background: {
36
36
  enabled: true,
37
37
  maxConcurrent: 5,
38
- defaultTimeout: 300000,
38
+ persistence: true,
39
39
  },
40
40
  autonomy: {
41
41
  enabled: false,
package/src/config.ts CHANGED
@@ -10,6 +10,10 @@ import {
10
10
  testModeDefaults,
11
11
  } from "./orchestrator/fallback/fallback-config";
12
12
  import { AGENT_REGISTRY, ALL_GROUP_IDS } from "./registry/model-groups";
13
+ import { backgroundConfigSchema, backgroundDefaults } from "./types/background";
14
+ import { mcpConfigSchema, mcpDefaults } from "./types/mcp";
15
+ import { recoveryConfigSchema, recoveryDefaults } from "./types/recovery";
16
+ import { routingConfigSchema, routingDefaults } from "./types/routing";
13
17
  import { ensureDir, isEnoentError } from "./utils/fs-helpers";
14
18
  import { getGlobalConfigDir } from "./utils/paths";
15
19
 
@@ -182,14 +186,47 @@ const pluginConfigSchemaV6 = z
182
186
  }
183
187
  });
184
188
 
185
- // Export aliases updated to v6
186
- export const pluginConfigSchema = pluginConfigSchemaV6;
189
+ type PluginConfigV6 = z.infer<typeof pluginConfigSchemaV6>;
187
190
 
188
- export type PluginConfig = z.infer<typeof pluginConfigSchemaV6>;
191
+ const pluginConfigSchemaV7 = z
192
+ .object({
193
+ version: z.literal(7),
194
+ configured: z.boolean(),
195
+ groups: z.record(z.string(), groupModelAssignmentSchema).default({}),
196
+ overrides: z.record(z.string(), agentOverrideSchema).default({}),
197
+ orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
198
+ confidence: confidenceConfigSchema.default(confidenceDefaults),
199
+ fallback: fallbackConfigSchemaV6.default(fallbackDefaultsV6),
200
+ memory: memoryConfigSchema.default(memoryDefaults),
201
+ background: backgroundConfigSchema.default(backgroundDefaults),
202
+ autonomy: z
203
+ .object({
204
+ enabled: z.boolean().default(false),
205
+ verification: z.enum(["strict", "normal", "lenient"]).default("normal"),
206
+ maxIterations: z.number().int().min(1).max(50).default(10),
207
+ })
208
+ .default({ enabled: false, verification: "normal", maxIterations: 10 }),
209
+ routing: routingConfigSchema.default(routingDefaults),
210
+ recovery: recoveryConfigSchema.default(recoveryDefaults),
211
+ mcp: mcpConfigSchema.default(mcpDefaults),
212
+ })
213
+ .superRefine((config, ctx) => {
214
+ for (const groupId of Object.keys(config.groups)) {
215
+ if (!ALL_GROUP_IDS.includes(groupId as (typeof ALL_GROUP_IDS)[number])) {
216
+ ctx.addIssue({
217
+ code: z.ZodIssueCode.custom,
218
+ path: ["groups", groupId],
219
+ message: `Unknown group id "${groupId}". Expected one of: ${ALL_GROUP_IDS.join(", ")}`,
220
+ });
221
+ }
222
+ }
223
+ });
189
224
 
190
- export const CONFIG_PATH = join(getGlobalConfigDir(), "opencode-autopilot.json");
225
+ export const pluginConfigSchema = pluginConfigSchemaV7;
226
+
227
+ export type PluginConfig = z.infer<typeof pluginConfigSchemaV7>;
191
228
 
192
- // --- Migration ---
229
+ export const CONFIG_PATH = join(getGlobalConfigDir(), "opencode-autopilot.json");
193
230
 
194
231
  function migrateV1toV2(v1Config: PluginConfigV1): PluginConfigV2 {
195
232
  return {
@@ -274,7 +311,7 @@ function migrateV4toV5(v4Config: PluginConfigV4): PluginConfigV5 {
274
311
  };
275
312
  }
276
313
 
277
- function migrateV5toV6(v5Config: PluginConfigV5): PluginConfig {
314
+ function migrateV5toV6(v5Config: PluginConfigV5): PluginConfigV6 {
278
315
  return {
279
316
  version: 6 as const,
280
317
  configured: v5Config.configured,
@@ -287,68 +324,106 @@ function migrateV5toV6(v5Config: PluginConfigV5): PluginConfig {
287
324
  };
288
325
  }
289
326
 
290
- // --- Public API ---
327
+ export const v7ConfigDefaults = {
328
+ background: backgroundDefaults,
329
+ autonomy: {
330
+ enabled: false,
331
+ verification: "normal" as const,
332
+ maxIterations: 10,
333
+ },
334
+ routing: routingDefaults,
335
+ recovery: recoveryDefaults,
336
+ mcp: mcpDefaults,
337
+ } as const;
338
+
339
+ export function migrateV6toV7(v6Config: PluginConfigV6): PluginConfig {
340
+ return {
341
+ version: 7 as const,
342
+ configured: v6Config.configured,
343
+ groups: v6Config.groups,
344
+ overrides: v6Config.overrides,
345
+ orchestrator: v6Config.orchestrator,
346
+ confidence: v6Config.confidence,
347
+ fallback: v6Config.fallback,
348
+ memory: v6Config.memory,
349
+ background: backgroundDefaults,
350
+ autonomy: {
351
+ enabled: false,
352
+ verification: "normal",
353
+ maxIterations: 10,
354
+ },
355
+ routing: routingDefaults,
356
+ recovery: recoveryDefaults,
357
+ mcp: mcpDefaults,
358
+ };
359
+ }
291
360
 
292
361
  export async function loadConfig(configPath: string = CONFIG_PATH): Promise<PluginConfig | null> {
293
362
  try {
294
363
  const raw = await readFile(configPath, "utf-8");
295
364
  const parsed = JSON.parse(raw);
296
365
 
297
- // Try v6 first
366
+ const v7Result = pluginConfigSchemaV7.safeParse(parsed);
367
+ if (v7Result.success) return v7Result.data;
368
+
298
369
  const v6Result = pluginConfigSchemaV6.safeParse(parsed);
299
- if (v6Result.success) return v6Result.data;
370
+ if (v6Result.success) {
371
+ const migrated = migrateV6toV7(v6Result.data);
372
+ await saveConfig(migrated, configPath);
373
+ return migrated;
374
+ }
300
375
 
301
- // Try v5 and migrate to v6
302
376
  const v5Result = pluginConfigSchemaV5.safeParse(parsed);
303
377
  if (v5Result.success) {
304
- const migrated = migrateV5toV6(v5Result.data);
378
+ const v6 = migrateV5toV6(v5Result.data);
379
+ const migrated = migrateV6toV7(v6);
305
380
  await saveConfig(migrated, configPath);
306
381
  return migrated;
307
382
  }
308
383
 
309
- // Try v4 → v5 → v6
310
384
  const v4Result = pluginConfigSchemaV4.safeParse(parsed);
311
385
  if (v4Result.success) {
312
386
  const v5 = migrateV4toV5(v4Result.data);
313
- const migrated = migrateV5toV6(v5);
387
+ const v6 = migrateV5toV6(v5);
388
+ const migrated = migrateV6toV7(v6);
314
389
  await saveConfig(migrated, configPath);
315
390
  return migrated;
316
391
  }
317
392
 
318
- // Try v3 → v4 → v5 → v6
319
393
  const v3Result = pluginConfigSchemaV3.safeParse(parsed);
320
394
  if (v3Result.success) {
321
395
  const v4 = migrateV3toV4(v3Result.data);
322
396
  const v5 = migrateV4toV5(v4);
323
- const migrated = migrateV5toV6(v5);
397
+ const v6 = migrateV5toV6(v5);
398
+ const migrated = migrateV6toV7(v6);
324
399
  await saveConfig(migrated, configPath);
325
400
  return migrated;
326
401
  }
327
402
 
328
- // Try v2 → v3 → v4 → v5 → v6
329
403
  const v2Result = pluginConfigSchemaV2.safeParse(parsed);
330
404
  if (v2Result.success) {
331
405
  const v3 = migrateV2toV3(v2Result.data);
332
406
  const v4 = migrateV3toV4(v3);
333
407
  const v5 = migrateV4toV5(v4);
334
- const migrated = migrateV5toV6(v5);
408
+ const v6 = migrateV5toV6(v5);
409
+ const migrated = migrateV6toV7(v6);
335
410
  await saveConfig(migrated, configPath);
336
411
  return migrated;
337
412
  }
338
413
 
339
- // Try v1 → v2 → v3 → v4 → v5 → v6
340
414
  const v1Result = pluginConfigSchemaV1.safeParse(parsed);
341
415
  if (v1Result.success) {
342
416
  const v2 = migrateV1toV2(v1Result.data);
343
417
  const v3 = migrateV2toV3(v2);
344
418
  const v4 = migrateV3toV4(v3);
345
419
  const v5 = migrateV4toV5(v4);
346
- const migrated = migrateV5toV6(v5);
420
+ const v6 = migrateV5toV6(v5);
421
+ const migrated = migrateV6toV7(v6);
347
422
  await saveConfig(migrated, configPath);
348
423
  return migrated;
349
424
  }
350
425
 
351
- return pluginConfigSchemaV6.parse(parsed); // throw with proper error
426
+ return pluginConfigSchemaV7.parse(parsed);
352
427
  } catch (error: unknown) {
353
428
  if (isEnoentError(error)) return null;
354
429
  throw error;
@@ -371,7 +446,7 @@ export function isFirstLoad(config: PluginConfig | null): boolean {
371
446
 
372
447
  export function createDefaultConfig(): PluginConfig {
373
448
  return {
374
- version: 6 as const,
449
+ version: 7 as const,
375
450
  configured: false,
376
451
  groups: {},
377
452
  overrides: {},
@@ -379,5 +454,14 @@ export function createDefaultConfig(): PluginConfig {
379
454
  confidence: confidenceDefaults,
380
455
  fallback: fallbackDefaultsV6,
381
456
  memory: memoryDefaults,
457
+ background: backgroundDefaults,
458
+ autonomy: {
459
+ enabled: false,
460
+ verification: "normal",
461
+ maxIterations: 10,
462
+ },
463
+ routing: routingDefaults,
464
+ recovery: recoveryDefaults,
465
+ mcp: mcpDefaults,
382
466
  };
383
467
  }
@@ -44,7 +44,7 @@ export async function configHealthCheck(configPath?: string): Promise<HealthResu
44
44
  }
45
45
  }
46
46
 
47
- const LATEST_CONFIG_VERSION = 6;
47
+ const LATEST_CONFIG_VERSION = 7;
48
48
 
49
49
  export async function configVersionCheck(configPath?: string): Promise<HealthResult> {
50
50
  try {
@@ -141,6 +141,74 @@ export async function configGroupsCheck(configPath?: string): Promise<HealthResu
141
141
  }
142
142
  }
143
143
 
144
+ /** v7 config fields that must be present on a v7 config. */
145
+ const V7_REQUIRED_FIELDS: readonly string[] = Object.freeze([
146
+ "background",
147
+ "routing",
148
+ "recovery",
149
+ "mcp",
150
+ ]);
151
+
152
+ /**
153
+ * Check that v7 configs contain all four new top-level fields introduced in v7:
154
+ * background, routing, recovery, and mcp.
155
+ * Inspects the raw on-disk JSON so that Zod default-filling does not mask
156
+ * actually-missing fields. Pre-v7 configs receive a pass with a migration notice.
157
+ */
158
+ export async function configV7FieldsCheck(configPath?: string): Promise<HealthResult> {
159
+ const resolvedPath = configPath ?? join(getGlobalConfigDir(), "opencode-autopilot.json");
160
+ try {
161
+ let raw: Record<string, unknown>;
162
+ try {
163
+ const content = await readFile(resolvedPath, "utf-8");
164
+ raw = JSON.parse(content) as Record<string, unknown>;
165
+ } catch (error: unknown) {
166
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
167
+ return Object.freeze({
168
+ name: "config-v7-fields",
169
+ status: "fail" as const,
170
+ message: "Config file not found",
171
+ });
172
+ }
173
+ throw error;
174
+ }
175
+
176
+ const version = typeof raw.version === "number" ? raw.version : 0;
177
+
178
+ if (version < 7) {
179
+ return Object.freeze({
180
+ name: "config-v7-fields",
181
+ status: "pass" as const,
182
+ message: `Config v${version} will gain v7 fields (background, routing, recovery, mcp) on next load`,
183
+ });
184
+ }
185
+
186
+ const missingFields = V7_REQUIRED_FIELDS.filter((field) => !(field in raw));
187
+
188
+ if (missingFields.length > 0) {
189
+ return Object.freeze({
190
+ name: "config-v7-fields",
191
+ status: "fail" as const,
192
+ message: `Config v7 is missing required fields: ${missingFields.join(", ")}`,
193
+ details: Object.freeze(missingFields),
194
+ });
195
+ }
196
+
197
+ return Object.freeze({
198
+ name: "config-v7-fields",
199
+ status: "pass" as const,
200
+ message: `Config v7 fields present: ${V7_REQUIRED_FIELDS.join(", ")}`,
201
+ });
202
+ } catch (error: unknown) {
203
+ const msg = error instanceof Error ? error.message : String(error);
204
+ return Object.freeze({
205
+ name: "config-v7-fields",
206
+ status: "fail" as const,
207
+ message: `Config v7 fields check failed: ${msg}`,
208
+ });
209
+ }
210
+ }
211
+
144
212
  /** Standard agent names, derived from the agents barrel export. */
145
213
  const STANDARD_AGENT_NAMES: readonly string[] = Object.freeze([
146
214
  "researcher",
@@ -4,6 +4,7 @@ import {
4
4
  assetHealthCheck,
5
5
  commandHealthCheck,
6
6
  configHealthCheck,
7
+ configV7FieldsCheck,
7
8
  memoryHealthCheck,
8
9
  nativeAgentSuppressionHealthCheck,
9
10
  skillHealthCheck,
@@ -43,16 +44,20 @@ export async function runHealthChecks(options?: {
43
44
  }): Promise<HealthReport> {
44
45
  const start = Date.now();
45
46
 
47
+ const configOutcome = await Promise.allSettled([configHealthCheck(options?.configPath)]);
48
+
46
49
  const settled = await Promise.allSettled([
47
- configHealthCheck(options?.configPath),
48
50
  agentHealthCheck(options?.openCodeConfig ?? null),
49
51
  nativeAgentSuppressionHealthCheck(options?.openCodeConfig ?? null),
50
52
  assetHealthCheck(options?.assetsDir, options?.targetDir),
51
53
  skillHealthCheck(options?.projectRoot ?? process.cwd()),
52
54
  memoryHealthCheck(options?.targetDir),
53
55
  commandHealthCheck(options?.targetDir),
56
+ configV7FieldsCheck(options?.configPath),
54
57
  ]);
55
58
 
59
+ const allSettled = [...configOutcome, ...settled];
60
+
56
61
  const fallbackNames = [
57
62
  "config-validity",
58
63
  "agent-injection",
@@ -61,9 +66,10 @@ export async function runHealthChecks(options?: {
61
66
  "skill-loading",
62
67
  "memory-db",
63
68
  "command-accessibility",
69
+ "config-v7-fields",
64
70
  ];
65
71
  const results: readonly HealthResult[] = Object.freeze(
66
- settled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
72
+ allSettled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
67
73
  );
68
74
 
69
75
  const allPassed = results.every((r) => r.status === "pass");
@@ -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 ?? {},
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+
3
+ export const TaskStatusSchema = z.enum(["pending", "running", "completed", "failed", "cancelled"]);
4
+ export type TaskStatus = z.infer<typeof TaskStatusSchema>;
5
+
6
+ export const TaskResultSchema = z.object({
7
+ taskId: z.string().min(1),
8
+ status: TaskStatusSchema,
9
+ output: z.string().optional(),
10
+ error: z.string().optional(),
11
+ startedAt: z.number().optional(),
12
+ completedAt: z.number().optional(),
13
+ durationMs: z.number().optional(),
14
+ });
15
+ export type TaskResult = z.infer<typeof TaskResultSchema>;
16
+
17
+ export const BackgroundTaskSchema = z.object({
18
+ id: z.string().min(1),
19
+ name: z.string().min(1),
20
+ status: TaskStatusSchema,
21
+ agentId: z.string().optional(),
22
+ priority: z.number().int().min(0).max(100).default(50),
23
+ createdAt: z.number(),
24
+ result: TaskResultSchema.optional(),
25
+ metadata: z.record(z.string(), z.unknown()).default({}),
26
+ });
27
+ export type BackgroundTask = z.infer<typeof BackgroundTaskSchema>;
28
+
29
+ export const AgentSlotSchema = z.object({
30
+ slotId: z.string().min(1),
31
+ agentId: z.string().min(1),
32
+ capacity: z.number().int().min(1).max(100).default(1),
33
+ activeTaskCount: z.number().int().min(0).default(0),
34
+ reserved: z.boolean().default(false),
35
+ });
36
+ export type AgentSlot = z.infer<typeof AgentSlotSchema>;
37
+
38
+ export const ConcurrencyLimitsSchema = z.object({
39
+ global: z.number().int().min(1).max(50).default(5),
40
+ perAgent: z.number().int().min(1).max(10).default(2),
41
+ perCategory: z.record(z.string(), z.number().int().min(1).max(20)).default({}),
42
+ });
43
+ export type ConcurrencyLimits = z.infer<typeof ConcurrencyLimitsSchema>;
44
+
45
+ export const backgroundConfigSchema = z.object({
46
+ enabled: z.boolean().default(false),
47
+ maxConcurrent: z.number().int().min(1).max(50).default(5),
48
+ persistence: z.boolean().default(true),
49
+ });
50
+ export type BackgroundConfig = z.infer<typeof backgroundConfigSchema>;
51
+ export const backgroundDefaults = backgroundConfigSchema.parse({});
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+
3
+ export const McpSkillSchema = z.object({
4
+ name: z.string().min(1),
5
+ description: z.string().optional(),
6
+ enabled: z.boolean().default(true),
7
+ version: z.string().optional(),
8
+ config: z.record(z.string(), z.unknown()).default({}),
9
+ });
10
+ export type McpSkill = z.infer<typeof McpSkillSchema>;
11
+
12
+ export const McpServerSchema = z.object({
13
+ id: z.string().min(1),
14
+ url: z.string().url().optional(),
15
+ transport: z.enum(["stdio", "http", "sse"]).default("stdio"),
16
+ enabled: z.boolean().default(true),
17
+ skills: z.array(z.string()).default([]),
18
+ metadata: z.record(z.string(), z.unknown()).default({}),
19
+ });
20
+ export type McpServer = z.infer<typeof McpServerSchema>;
21
+
22
+ export const mcpConfigSchema = z.object({
23
+ enabled: z.boolean().default(false),
24
+ skills: z.record(z.string(), z.unknown()).default({}),
25
+ });
26
+ export type McpConfig = z.infer<typeof mcpConfigSchema>;
27
+ export const mcpDefaults = mcpConfigSchema.parse({});
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export const ErrorCategorySchema = z.enum([
4
+ "rate_limit",
5
+ "auth_failure",
6
+ "quota_exceeded",
7
+ "service_unavailable",
8
+ "timeout",
9
+ "network",
10
+ "validation",
11
+ "unknown",
12
+ ]);
13
+ export type ErrorCategory = z.infer<typeof ErrorCategorySchema>;
14
+
15
+ export const RecoveryStrategySchema = z.enum([
16
+ "retry",
17
+ "fallback_model",
18
+ "skip",
19
+ "abort",
20
+ "user_prompt",
21
+ ]);
22
+ export type RecoveryStrategy = z.infer<typeof RecoveryStrategySchema>;
23
+
24
+ export const RecoveryActionSchema = z.object({
25
+ strategy: RecoveryStrategySchema,
26
+ errorCategory: ErrorCategorySchema,
27
+ maxAttempts: z.number().int().min(1).max(10).default(3),
28
+ backoffMs: z.number().int().min(0).default(1000),
29
+ fallbackAgentId: z.string().optional(),
30
+ metadata: z.record(z.string(), z.unknown()).default({}),
31
+ });
32
+ export type RecoveryAction = z.infer<typeof RecoveryActionSchema>;
33
+
34
+ export const recoveryConfigSchema = z.object({
35
+ enabled: z.boolean().default(true),
36
+ maxRetries: z.number().int().min(0).max(10).default(3),
37
+ });
38
+ export type RecoveryConfig = z.infer<typeof recoveryConfigSchema>;
39
+ export const recoveryDefaults = recoveryConfigSchema.parse({});
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export const CategorySchema = z.enum([
4
+ "quick",
5
+ "visual-engineering",
6
+ "ultrabrain",
7
+ "artistry",
8
+ "writing",
9
+ "unspecified-high",
10
+ "unspecified-low",
11
+ ]);
12
+ export type Category = z.infer<typeof CategorySchema>;
13
+
14
+ export const CategoryConfigSchema = z.object({
15
+ enabled: z.boolean().default(true),
16
+ agentId: z.string().optional(),
17
+ modelGroup: z.string().optional(),
18
+ maxTokenBudget: z.number().int().min(1000).optional(),
19
+ timeoutSeconds: z.number().int().min(1).max(3600).optional(),
20
+ skills: z.array(z.string()).default([]),
21
+ metadata: z.record(z.string(), z.unknown()).default({}),
22
+ });
23
+ export type CategoryConfig = z.infer<typeof CategoryConfigSchema>;
24
+
25
+ export const RoutingDecisionSchema = z.object({
26
+ category: CategorySchema,
27
+ confidence: z.number().min(0).max(1),
28
+ agentId: z.string().optional(),
29
+ reasoning: z.string().optional(),
30
+ appliedConfig: CategoryConfigSchema.optional(),
31
+ });
32
+ export type RoutingDecision = z.infer<typeof RoutingDecisionSchema>;
33
+
34
+ export const routingConfigSchema = z.object({
35
+ enabled: z.boolean().default(false),
36
+ categories: z.record(z.string(), CategoryConfigSchema).default({}),
37
+ });
38
+ export type RoutingConfig = z.infer<typeof routingConfigSchema>;
39
+ export const routingDefaults = routingConfigSchema.parse({});