@jmanuelcorral/openteam 0.18.1 → 0.19.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 (111) hide show
  1. package/.opencode/openteam.example.json +23 -14
  2. package/AGENTS.md +16 -2
  3. package/README.es.md +291 -208
  4. package/README.md +292 -138
  5. package/dist/capabilities/classify.d.ts.map +1 -1
  6. package/dist/capabilities/normalize.d.ts.map +1 -1
  7. package/dist/certificates/graph-release-certificate.json +2 -2
  8. package/dist/certificates/graph-shadow-certificate.json +2 -2
  9. package/dist/cli.js +1348 -593
  10. package/dist/commands/agents.d.ts +16 -4
  11. package/dist/commands/agents.d.ts.map +1 -1
  12. package/dist/commands/dispatch.d.ts.map +1 -1
  13. package/dist/commands/doctor.d.ts.map +1 -1
  14. package/dist/commands/local.d.ts +4 -5
  15. package/dist/commands/local.d.ts.map +1 -1
  16. package/dist/commands/report.d.ts +6 -6
  17. package/dist/commands/report.d.ts.map +1 -1
  18. package/dist/commands/rolesInit.d.ts +10 -12
  19. package/dist/commands/rolesInit.d.ts.map +1 -1
  20. package/dist/commands/setup.d.ts +5 -13
  21. package/dist/commands/setup.d.ts.map +1 -1
  22. package/dist/commands/yolo.d.ts +2 -4
  23. package/dist/commands/yolo.d.ts.map +1 -1
  24. package/dist/config/errors.d.ts.map +1 -1
  25. package/dist/config/executionPolicyDomains.d.ts +18 -0
  26. package/dist/config/executionPolicyDomains.d.ts.map +1 -0
  27. package/dist/config/migrations.d.ts +9 -2
  28. package/dist/config/migrations.d.ts.map +1 -1
  29. package/dist/config/persist.d.ts.map +1 -1
  30. package/dist/config/runtime.d.ts +2 -0
  31. package/dist/config/runtime.d.ts.map +1 -1
  32. package/dist/config/schema.d.ts +97 -12
  33. package/dist/config/schema.d.ts.map +1 -1
  34. package/dist/console/configView.d.ts +2 -7
  35. package/dist/console/configView.d.ts.map +1 -1
  36. package/dist/console/render.d.ts.map +1 -1
  37. package/dist/console/types.d.ts +2 -2
  38. package/dist/console/types.d.ts.map +1 -1
  39. package/dist/context/schema.d.ts +13 -7
  40. package/dist/context/schema.d.ts.map +1 -1
  41. package/dist/graph/certificate.d.ts.map +1 -1
  42. package/dist/graph/langgraph/adapter.d.ts +7 -8
  43. package/dist/graph/langgraph/adapter.d.ts.map +1 -1
  44. package/dist/graph/langgraph/certification.d.ts +5 -0
  45. package/dist/graph/langgraph/certification.d.ts.map +1 -1
  46. package/dist/graph/langgraph/checkpointReplay.d.ts.map +1 -1
  47. package/dist/graph/langgraph/engine.d.ts +2 -2
  48. package/dist/graph/langgraph/engine.d.ts.map +1 -1
  49. package/dist/graph/langgraph/types.d.ts +11 -6
  50. package/dist/graph/langgraph/types.d.ts.map +1 -1
  51. package/dist/graph/privacyPolicy.d.ts +6 -30
  52. package/dist/graph/privacyPolicy.d.ts.map +1 -1
  53. package/dist/index.d.ts +1 -9
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3136 -1122
  56. package/dist/mcp/teamMemory.d.ts +2 -2
  57. package/dist/mcp/teamMemory.d.ts.map +1 -1
  58. package/dist/messages/commands.d.ts +22 -35
  59. package/dist/messages/commands.d.ts.map +1 -1
  60. package/dist/messages/executionSetup.d.ts +116 -0
  61. package/dist/messages/executionSetup.d.ts.map +1 -0
  62. package/dist/messages/index.d.ts +1 -0
  63. package/dist/messages/index.d.ts.map +1 -1
  64. package/dist/messages/memoryTool.d.ts +17 -0
  65. package/dist/messages/memoryTool.d.ts.map +1 -0
  66. package/dist/messages/orchestration.d.ts +16 -0
  67. package/dist/messages/orchestration.d.ts.map +1 -0
  68. package/dist/messages/routing.d.ts +9 -0
  69. package/dist/messages/routing.d.ts.map +1 -0
  70. package/dist/opencodeArtifacts/orchestratorAgent.d.ts +3 -5
  71. package/dist/opencodeArtifacts/orchestratorAgent.d.ts.map +1 -1
  72. package/dist/orchestrator/coordinator.d.ts +19 -12
  73. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  74. package/dist/orchestrator/graphEffects.d.ts +10 -11
  75. package/dist/orchestrator/graphEffects.d.ts.map +1 -1
  76. package/dist/orchestrator/outputContract.d.ts +5 -6
  77. package/dist/orchestrator/outputContract.d.ts.map +1 -1
  78. package/dist/orchestrator/roles.d.ts +25 -2
  79. package/dist/orchestrator/roles.d.ts.map +1 -1
  80. package/dist/orchestrator/runtimeSelection.d.ts.map +1 -1
  81. package/dist/orchestrator/subsessions.d.ts +49 -1
  82. package/dist/orchestrator/subsessions.d.ts.map +1 -1
  83. package/dist/plugin/availability.d.ts +15 -2
  84. package/dist/plugin/availability.d.ts.map +1 -1
  85. package/dist/plugin/diagnostics.d.ts +1 -0
  86. package/dist/plugin/diagnostics.d.ts.map +1 -1
  87. package/dist/plugin/hooks.d.ts +21 -2
  88. package/dist/plugin/hooks.d.ts.map +1 -1
  89. package/dist/plugin/memoryTool.d.ts +3 -3
  90. package/dist/plugin/memoryTool.d.ts.map +1 -1
  91. package/dist/plugin/modelOwnership.d.ts +15 -0
  92. package/dist/plugin/modelOwnership.d.ts.map +1 -0
  93. package/dist/plugin/orchestrateTool.d.ts +50 -3
  94. package/dist/plugin/orchestrateTool.d.ts.map +1 -1
  95. package/dist/router/chooseModel.d.ts.map +1 -1
  96. package/dist/router/cost.d.ts +1 -1
  97. package/dist/router/cost.d.ts.map +1 -1
  98. package/dist/router/executionPolicy.d.ts +44 -0
  99. package/dist/router/executionPolicy.d.ts.map +1 -0
  100. package/dist/router/frontierBaseline.d.ts.map +1 -1
  101. package/dist/router/types.d.ts +10 -4
  102. package/dist/router/types.d.ts.map +1 -1
  103. package/dist/telemetry/aggregate.d.ts +4 -4
  104. package/dist/telemetry/aggregate.d.ts.map +1 -1
  105. package/dist/telemetry/diagnostics.d.ts.map +1 -1
  106. package/dist/telemetry/events.d.ts +6 -6
  107. package/dist/telemetry/otel.d.ts.map +1 -1
  108. package/dist/telemetry/types.d.ts +3 -3
  109. package/dist/web/configApi.d.ts +3 -1
  110. package/dist/web/configApi.d.ts.map +1 -1
  111. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -47,6 +47,18 @@ import { z as z2 } from "zod";
47
47
 
48
48
  // src/orchestrator/roles.ts
49
49
  import { z } from "zod";
50
+
51
+ // src/messages/routing.ts
52
+ var routingMessages = {
53
+ conflictingLegacyModes: "router.localOnly and router.frontierOnly cannot both be true",
54
+ conflictingRouterModes: (executionMode, legacyMode) => `router.executionMode "${executionMode}" conflicts with legacy router mode "${legacyMode}"`,
55
+ agentModeBroadensGlobal: (agentMode, routerMode) => `agent execution mode "${agentMode}" cannot broaden global router execution mode "${routerMode}"`,
56
+ requiredLocalModeConflict: "requiresLocalRuntime=true conflicts with a non-local agent executionMode",
57
+ privacyModeDeprecated: 'privacyMode is deprecated and ignored; it no longer protects prompt content from frontier egress. Set router.executionMode to "local" to prohibit frontier routing.',
58
+ unknownCost: "unknown"
59
+ };
60
+
61
+ // src/orchestrator/roles.ts
50
62
  var modelSelectionSchema = z.object({
51
63
  providerID: z.string().min(1),
52
64
  modelID: z.string().min(1)
@@ -70,8 +82,19 @@ var AgentRoleProfileSchema = z.object({
70
82
  requiresLocalRuntime: z.boolean().optional(),
71
83
  localRuntimes: z.array(z.string().min(1)).optional(),
72
84
  localModels: z.record(z.string().min(1), z.string().min(1)).optional(),
73
- localModelClass: z.enum(["thinking", "coder"]).optional()
74
- }).strict();
85
+ localModelClass: z.enum(["thinking", "coder"]).optional(),
86
+ executionMode: z.enum(["local", "frontier", "mixed"]).optional(),
87
+ model: z.union([modelSelectionSchema, z.literal("auto")]).optional(),
88
+ fallbacks: z.array(modelSelectionSchema).optional()
89
+ }).strict().superRefine((profile, ctx) => {
90
+ if (profile.requiresLocalRuntime === true && profile.executionMode !== undefined && profile.executionMode !== "local") {
91
+ ctx.addIssue({
92
+ code: "custom",
93
+ path: ["executionMode"],
94
+ message: routingMessages.requiredLocalModeConflict
95
+ });
96
+ }
97
+ });
75
98
  var TEAM_ROLES = {
76
99
  architect: {
77
100
  roleID: "architect",
@@ -125,8 +148,7 @@ var TEAM_ROLES = {
125
148
  minCodeQualityTier: 1,
126
149
  needsToolCallingByDefault: false,
127
150
  preferredFrontierModels: [],
128
- localFirst: true,
129
- requiresLocalRuntime: true
151
+ localFirst: true
130
152
  },
131
153
  ralph: {
132
154
  roleID: "ralph",
@@ -136,8 +158,7 @@ var TEAM_ROLES = {
136
158
  minCodeQualityTier: 2,
137
159
  needsToolCallingByDefault: true,
138
160
  preferredFrontierModels: [],
139
- localFirst: true,
140
- requiresLocalRuntime: true
161
+ localFirst: true
141
162
  },
142
163
  guardian: {
143
164
  roleID: "guardian",
@@ -148,7 +169,6 @@ var TEAM_ROLES = {
148
169
  needsToolCallingByDefault: false,
149
170
  preferredFrontierModels: [],
150
171
  localFirst: true,
151
- requiresLocalRuntime: true,
152
172
  localModelClass: "thinking"
153
173
  },
154
174
  tester: {
@@ -216,9 +236,112 @@ function synthesiseFallbackProfile(roleID) {
216
236
  localFirst: true
217
237
  };
218
238
  }
239
+ function concreteFrontierPreferences(role) {
240
+ return role.preferredFrontierModels.filter((model) => model !== "auto");
241
+ }
242
+ function agentExecutionPolicyForRole(role) {
243
+ const policy = {};
244
+ if (role.executionMode !== undefined) {
245
+ policy.executionMode = role.executionMode;
246
+ } else if (role.requiresLocalRuntime === true) {
247
+ policy.executionMode = "local";
248
+ }
249
+ if (role.model !== undefined) {
250
+ policy.model = role.model;
251
+ }
252
+ if (role.fallbacks !== undefined) {
253
+ policy.fallbacks = [...role.fallbacks];
254
+ }
255
+ if (policy.model === undefined && policy.executionMode === undefined && policy.fallbacks === undefined) {
256
+ const preferences = concreteFrontierPreferences(role);
257
+ const primary = preferences[0];
258
+ if (primary === undefined) {
259
+ return;
260
+ }
261
+ policy.executionMode = "frontier";
262
+ policy.model = primary;
263
+ if (preferences.length > 1) {
264
+ policy.fallbacks = preferences.slice(1);
265
+ }
266
+ }
267
+ return policy;
268
+ }
269
+
270
+ // src/router/executionPolicy.ts
271
+ class ExecutionPolicyError extends Error {
272
+ code;
273
+ constructor(code, message) {
274
+ super(message);
275
+ this.name = "ExecutionPolicyError";
276
+ this.code = code;
277
+ }
278
+ }
279
+ function legacyExecutionMode(localOnly, frontierOnly) {
280
+ if (localOnly === true) {
281
+ return "local";
282
+ }
283
+ if (frontierOnly === true) {
284
+ return "frontier";
285
+ }
286
+ return;
287
+ }
288
+ function resolveExecutionModeResult(input) {
289
+ if (input.legacyLocalOnly === true && input.legacyFrontierOnly === true) {
290
+ return {
291
+ ok: false,
292
+ error: new ExecutionPolicyError("legacy-modes-conflict", routingMessages.conflictingLegacyModes)
293
+ };
294
+ }
295
+ const legacyMode = legacyExecutionMode(input.legacyLocalOnly, input.legacyFrontierOnly);
296
+ if (input.routerMode !== undefined && legacyMode !== undefined && input.routerMode !== legacyMode) {
297
+ return {
298
+ ok: false,
299
+ error: new ExecutionPolicyError("router-mode-conflict", routingMessages.conflictingRouterModes(input.routerMode, legacyMode))
300
+ };
301
+ }
302
+ const routerMode = input.routerMode ?? legacyMode ?? "mixed";
303
+ if (input.agentMode !== undefined && routerMode !== "mixed" && input.agentMode !== routerMode) {
304
+ return {
305
+ ok: false,
306
+ error: new ExecutionPolicyError("agent-mode-broadens-global", routingMessages.agentModeBroadensGlobal(input.agentMode, routerMode))
307
+ };
308
+ }
309
+ return { ok: true, mode: input.agentMode ?? routerMode };
310
+ }
311
+ function resolveExecutionMode(input) {
312
+ const resolution = resolveExecutionModeResult(input);
313
+ if (!resolution.ok) {
314
+ throw resolution.error;
315
+ }
316
+ return resolution.mode;
317
+ }
318
+ function resolveAgentExecutionPolicyResult(routerMode, override) {
319
+ const executionModeResolution = resolveExecutionModeResult({
320
+ agentMode: override?.executionMode,
321
+ routerMode
322
+ });
323
+ if (!executionModeResolution.ok) {
324
+ return executionModeResolution;
325
+ }
326
+ const policy = {
327
+ executionMode: executionModeResolution.mode,
328
+ model: override?.model ?? "auto"
329
+ };
330
+ if (override?.fallbacks !== undefined) {
331
+ policy.fallbacks = [...override.fallbacks];
332
+ }
333
+ return { ok: true, policy };
334
+ }
335
+ function resolveAgentExecutionPolicy(routerMode, override) {
336
+ const resolution = resolveAgentExecutionPolicyResult(routerMode, override);
337
+ if (!resolution.ok) {
338
+ throw resolution.error;
339
+ }
340
+ return resolution.policy;
341
+ }
219
342
 
220
343
  // src/config/migrations.ts
221
- var CURRENT_CONFIG_VERSION = 1;
344
+ var CURRENT_CONFIG_VERSION = 2;
222
345
 
223
346
  class ConfigMigrationError extends Error {
224
347
  code;
@@ -230,22 +353,69 @@ class ConfigMigrationError extends Error {
230
353
  this.detail = detail;
231
354
  }
232
355
  }
356
+ function isRecord(value) {
357
+ return typeof value === "object" && value !== null && !Array.isArray(value);
358
+ }
359
+ function detectConfigDeprecations(raw) {
360
+ if (!isRecord(raw) || raw.privacyMode === undefined) {
361
+ return [];
362
+ }
363
+ return [
364
+ {
365
+ code: "privacy-mode-ignored",
366
+ path: "privacyMode",
367
+ message: routingMessages.privacyModeDeprecated
368
+ }
369
+ ];
370
+ }
371
+ function migrateRouter(raw, fromLegacyVersion) {
372
+ if (!isRecord(raw)) {
373
+ return raw;
374
+ }
375
+ const validExecutionMode = raw.executionMode === "local" || raw.executionMode === "frontier" || raw.executionMode === "mixed" ? raw.executionMode : undefined;
376
+ let executionMode;
377
+ try {
378
+ executionMode = resolveExecutionMode({
379
+ routerMode: validExecutionMode,
380
+ legacyLocalOnly: typeof raw.localOnly === "boolean" ? raw.localOnly : undefined,
381
+ legacyFrontierOnly: typeof raw.frontierOnly === "boolean" ? raw.frontierOnly : undefined
382
+ });
383
+ } catch (error) {
384
+ if (!(error instanceof ExecutionPolicyError)) {
385
+ throw error;
386
+ }
387
+ throw new ConfigMigrationError("conflicting-execution-mode", error.message);
388
+ }
389
+ const { localOnly: _localOnly, frontierOnly: _frontierOnly, ...router } = raw;
390
+ if (raw.executionMode !== undefined && validExecutionMode === undefined) {
391
+ return raw;
392
+ }
393
+ if (fromLegacyVersion || raw.localOnly !== undefined || raw.frontierOnly !== undefined) {
394
+ return { ...router, executionMode };
395
+ }
396
+ return raw;
397
+ }
233
398
  function migrateConfig(raw) {
234
- if (typeof raw !== "object" || raw === null) {
399
+ if (!isRecord(raw)) {
235
400
  return raw;
236
401
  }
237
- const record = raw;
238
- const version = record.configVersion;
239
- if (version === undefined) {
240
- return { ...record, configVersion: CURRENT_CONFIG_VERSION };
402
+ const version = raw.configVersion;
403
+ const sourceVersion = version ?? 1;
404
+ if (typeof sourceVersion !== "number" || !Number.isInteger(sourceVersion) || sourceVersion < 1) {
405
+ throw new ConfigMigrationError("unknown-version", String(sourceVersion));
241
406
  }
242
- if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
243
- throw new ConfigMigrationError("unknown-version", String(version));
407
+ if (sourceVersion > CURRENT_CONFIG_VERSION) {
408
+ throw new ConfigMigrationError("unsupported-version", String(sourceVersion));
244
409
  }
245
- if (version > CURRENT_CONFIG_VERSION) {
246
- throw new ConfigMigrationError("unsupported-version", String(version));
410
+ const migratedRouter = migrateRouter(raw.router, sourceVersion < 2);
411
+ if (sourceVersion === CURRENT_CONFIG_VERSION && migratedRouter === raw.router) {
412
+ return raw;
247
413
  }
248
- return record;
414
+ return {
415
+ ...raw,
416
+ configVersion: CURRENT_CONFIG_VERSION,
417
+ ...migratedRouter === undefined ? {} : { router: migratedRouter }
418
+ };
249
419
  }
250
420
 
251
421
  // src/config/schema.ts
@@ -254,6 +424,7 @@ var ModelRefSchema = z2.object({
254
424
  modelID: z2.string().min(1)
255
425
  }).strict();
256
426
  var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
427
+ var ExecutionModeSchema = z2.enum(["local", "frontier", "mixed"]);
257
428
  var PrivacyModeSchema = z2.enum([
258
429
  "forceLocalOnSensitive",
259
430
  "consentBeforeFrontier",
@@ -332,7 +503,12 @@ var OrchestratorRolesSchema = z2.record(z2.string().min(1), z2.unknown()).defaul
332
503
  });
333
504
  continue;
334
505
  }
335
- const parsed = AgentRoleProfileSchema.safeParse({ ...value, roleID });
506
+ const base = getRoleProfile(roleID) ?? synthesiseFallbackProfile(roleID);
507
+ const parsed = AgentRoleProfileSchema.safeParse({
508
+ ...base,
509
+ ...value,
510
+ roleID
511
+ });
336
512
  if (!parsed.success) {
337
513
  for (const issue of parsed.error.issues) {
338
514
  ctx.addIssue({
@@ -366,17 +542,47 @@ var OrchestratorRolesSchema = z2.record(z2.string().min(1), z2.unknown()).defaul
366
542
  if (parsed.data.localModelClass !== undefined) {
367
543
  profile.localModelClass = parsed.data.localModelClass;
368
544
  }
545
+ if (parsed.data.executionMode !== undefined) {
546
+ profile.executionMode = parsed.data.executionMode;
547
+ }
548
+ if (parsed.data.model !== undefined) {
549
+ profile.model = parsed.data.model;
550
+ }
551
+ if (parsed.data.fallbacks !== undefined) {
552
+ profile.fallbacks = parsed.data.fallbacks;
553
+ }
554
+ if (profile.requiresLocalRuntime === true && profile.executionMode === undefined) {
555
+ profile.executionMode = "local";
556
+ }
557
+ const concretePreferences = profile.preferredFrontierModels.filter((model) => model !== "auto");
558
+ if (profile.requiresLocalRuntime !== true && profile.executionMode === undefined && profile.model === undefined && concretePreferences.length > 0) {
559
+ const primary = concretePreferences[0];
560
+ if (primary !== undefined) {
561
+ profile.executionMode = "frontier";
562
+ profile.model = primary;
563
+ if (profile.fallbacks === undefined && concretePreferences.length > 1) {
564
+ profile.fallbacks = concretePreferences.slice(1);
565
+ }
566
+ }
567
+ }
369
568
  parsedRoles[roleID] = profile;
370
569
  }
371
570
  return parsedRoles;
372
571
  });
572
+ var AgentExecutionPolicySchema = z2.object({
573
+ executionMode: ExecutionModeSchema.optional(),
574
+ model: z2.union([ModelRefSchema, z2.literal("auto")]).optional(),
575
+ fallbacks: z2.array(ModelRefSchema).optional()
576
+ }).strict();
373
577
  var OrchestratorConfigSchema = z2.object({
374
578
  roles: OrchestratorRolesSchema,
375
- maxRetries: z2.number().int().min(0).optional()
579
+ primary: AgentExecutionPolicySchema.optional(),
580
+ maxRetries: z2.number().int().min(0).optional(),
581
+ maxModelsPerNode: z2.number().int().positive().optional()
376
582
  }).strict();
377
583
  var defaultFrontierModel = {
378
- providerID: "anthropic",
379
- modelID: "claude-sonnet-4-5"
584
+ providerID: "google",
585
+ modelID: "gemini-3.5-flash"
380
586
  };
381
587
  var DEFAULT_LOCAL_MAX_CONCURRENCY = 4;
382
588
  var LocalRuntimeKindSchema = z2.enum([
@@ -469,26 +675,34 @@ var OpenTeamConfigObjectSchema = z2.object({
469
675
  }),
470
676
  router: z2.object({
471
677
  mode: RouterModeSchema.default("balanced"),
678
+ executionMode: ExecutionModeSchema.optional(),
472
679
  localDefault: ModelRefSchema.nullable().default(null),
473
680
  trivialPromptMaxChars: z2.number().int().positive().default(280),
474
681
  frontierPromptMinChars: z2.number().int().positive().default(2000),
475
- frontierOnly: z2.boolean().default(false),
476
- localOnly: z2.boolean().default(false)
682
+ frontierOnly: z2.boolean().optional(),
683
+ localOnly: z2.boolean().optional()
477
684
  }).strict().superRefine((router, ctx) => {
478
- if (router.frontierOnly && router.localOnly) {
685
+ try {
686
+ resolveExecutionMode({
687
+ routerMode: router.executionMode,
688
+ legacyLocalOnly: router.localOnly,
689
+ legacyFrontierOnly: router.frontierOnly
690
+ });
691
+ } catch (error) {
692
+ if (!(error instanceof ExecutionPolicyError)) {
693
+ throw error;
694
+ }
479
695
  ctx.addIssue({
480
696
  code: "custom",
481
- path: ["localOnly"],
482
- message: "router.localOnly and router.frontierOnly cannot both be true"
697
+ path: error.code === "legacy-modes-conflict" ? ["localOnly"] : ["executionMode"],
698
+ message: error.message
483
699
  });
484
700
  }
485
701
  }).default({
486
702
  mode: "balanced",
487
703
  localDefault: null,
488
704
  trivialPromptMaxChars: 280,
489
- frontierPromptMinChars: 2000,
490
- frontierOnly: false,
491
- localOnly: false
705
+ frontierPromptMinChars: 2000
492
706
  }),
493
707
  local: z2.object({
494
708
  runtimes: z2.array(LocalRuntimeSchema).default([]).superRefine((runtimes, ctx) => {
@@ -517,7 +731,7 @@ var OpenTeamConfigObjectSchema = z2.object({
517
731
  frontierTokensPerSession: z2.number().int().positive().optional(),
518
732
  hardStopOnBudgetExhaustion: z2.boolean().default(false)
519
733
  }).strict().default({ hardStopOnBudgetExhaustion: false }),
520
- privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
734
+ privacyMode: PrivacyModeSchema.optional(),
521
735
  console: ConsoleConfigSchema,
522
736
  storage: StorageConfigSchema,
523
737
  memory: MemoryConfigSchema.optional(),
@@ -529,7 +743,48 @@ var OpenTeamConfigObjectSchema = z2.object({
529
743
  worktrees: { enabled: false }
530
744
  }),
531
745
  orchestrator: OrchestratorConfigSchema.optional()
532
- }).strict();
746
+ }).strict().superRefine((config, ctx) => {
747
+ let routerMode;
748
+ try {
749
+ routerMode = resolveExecutionMode({
750
+ routerMode: config.router.executionMode,
751
+ legacyLocalOnly: config.router.localOnly,
752
+ legacyFrontierOnly: config.router.frontierOnly
753
+ });
754
+ } catch (error) {
755
+ if (!(error instanceof ExecutionPolicyError)) {
756
+ throw error;
757
+ }
758
+ return;
759
+ }
760
+ const policies = [
761
+ {
762
+ path: ["orchestrator", "primary"],
763
+ policy: config.orchestrator?.primary
764
+ },
765
+ ...Object.entries(config.orchestrator?.roles ?? {}).map(([roleID, role]) => ({
766
+ path: ["orchestrator", "roles", roleID],
767
+ policy: agentExecutionPolicyForRole(role)
768
+ }))
769
+ ];
770
+ for (const { path, policy } of policies) {
771
+ if (policy === undefined) {
772
+ continue;
773
+ }
774
+ try {
775
+ resolveAgentExecutionPolicy(routerMode, policy);
776
+ } catch (error) {
777
+ if (!(error instanceof ExecutionPolicyError)) {
778
+ throw error;
779
+ }
780
+ ctx.addIssue({
781
+ code: "custom",
782
+ path: [...path, "executionMode"],
783
+ message: error.message
784
+ });
785
+ }
786
+ }
787
+ });
533
788
  var OpenTeamConfigSchema = OpenTeamConfigObjectSchema;
534
789
  function resolveMemorySemantic(config) {
535
790
  return config.memory?.semantic ?? MemorySemanticSchema.parse(undefined);
@@ -649,7 +904,7 @@ function diagnosticDescription(code) {
649
904
  case "opencode-version-read-failed":
650
905
  return "openteam could not read the live opencode version; graph gates fail closed when this happens.";
651
906
  case "privacy-inert":
652
- return "forceLocalOnSensitive is configured but no local runtime is configured, so the setting cannot keep sensitive prompts local.";
907
+ return routingMessages.privacyModeDeprecated;
653
908
  case "soak-evidence-rejected":
654
909
  return "Graph soak evidence contained rejected observation lines; cutover gate evidence may be incomplete.";
655
910
  case "telemetry-warning":
@@ -676,9 +931,9 @@ var RouteEventSchema = EventBaseSchema.extend({
676
931
  routeKind: z4.enum(["local", "frontier", "blocked"]),
677
932
  selected: ModelRefSchema,
678
933
  rationale: z4.string(),
679
- estimatedCostUSD: z4.number().finite().min(0),
680
- baselineCostUSD: z4.number().finite().min(0),
681
- estimatedSavingsUSD: z4.number().finite(),
934
+ estimatedCostUSD: z4.number().finite().min(0).nullable(),
935
+ baselineCostUSD: z4.number().finite().min(0).nullable(),
936
+ estimatedSavingsUSD: z4.number().finite().nullable(),
682
937
  budgetAction: z4.string().min(1),
683
938
  tokensIn: z4.number().int().min(0).optional(),
684
939
  tokensOut: z4.number().int().min(0).optional(),
@@ -892,9 +1147,9 @@ var CostRecordSchema = z5.object({
892
1147
  routeKind: z5.enum(["local", "frontier", "blocked"]),
893
1148
  selected: ModelRefSchema,
894
1149
  rationale: z5.string(),
895
- estimatedCostUSD: z5.number().finite().min(0),
896
- baselineCostUSD: z5.number().finite().min(0),
897
- estimatedSavingsUSD: z5.number().finite(),
1150
+ estimatedCostUSD: z5.number().finite().min(0).nullable(),
1151
+ baselineCostUSD: z5.number().finite().min(0).nullable(),
1152
+ estimatedSavingsUSD: z5.number().finite().nullable(),
898
1153
  budgetAction: z5.string().min(1),
899
1154
  tokensIn: z5.number().int().min(0).optional(),
900
1155
  tokensOut: z5.number().int().min(0).optional()
@@ -1153,7 +1408,11 @@ var FAILURE_CAUSE_RULES = [
1153
1408
  "bad request",
1154
1409
  /(?<!\d)400(?!\d)/,
1155
1410
  "unprocessable",
1156
- "malformed"
1411
+ "malformed",
1412
+ "invalid model",
1413
+ "unknown model",
1414
+ "model not found",
1415
+ "model does not exist"
1157
1416
  ],
1158
1417
  "prompt-validation"
1159
1418
  ],
@@ -1599,6 +1858,9 @@ function pushUnique(list, value) {
1599
1858
  list.push(value);
1600
1859
  }
1601
1860
  }
1861
+ function addEstimate(current, value) {
1862
+ return current === null || value === null ? null : current + value;
1863
+ }
1602
1864
  function applyRoute(session, event) {
1603
1865
  session.routeCount += 1;
1604
1866
  if (event.routeKind === "local") {
@@ -1606,8 +1868,8 @@ function applyRoute(session, event) {
1606
1868
  } else if (event.routeKind === "frontier") {
1607
1869
  session.frontierCount += 1;
1608
1870
  }
1609
- session.estimatedCostUSD += event.estimatedCostUSD;
1610
- session.estimatedSavingsUSD += event.estimatedSavingsUSD;
1871
+ session.estimatedCostUSD = addEstimate(session.estimatedCostUSD, event.estimatedCostUSD);
1872
+ session.estimatedSavingsUSD = addEstimate(session.estimatedSavingsUSD, event.estimatedSavingsUSD);
1611
1873
  pushUnique(session.models, `${event.selected.providerID}/${event.selected.modelID}`);
1612
1874
  pushUnique(session.agents, event.agent);
1613
1875
  }
@@ -1673,8 +1935,8 @@ function accumulateTotals(totals, session) {
1673
1935
  totals.routeCount += session.routeCount;
1674
1936
  totals.localCount += session.localCount;
1675
1937
  totals.frontierCount += session.frontierCount;
1676
- totals.estimatedCostUSD += session.estimatedCostUSD;
1677
- totals.estimatedSavingsUSD += session.estimatedSavingsUSD;
1938
+ totals.estimatedCostUSD = addEstimate(totals.estimatedCostUSD, session.estimatedCostUSD);
1939
+ totals.estimatedSavingsUSD = addEstimate(totals.estimatedSavingsUSD, session.estimatedSavingsUSD);
1678
1940
  totals.realCostUSD += session.realCostUSD;
1679
1941
  totals.messageCount += session.messageCount;
1680
1942
  totals.tokensIn += session.tokensIn;
@@ -1977,6 +2239,7 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
1977
2239
  "router",
1978
2240
  [
1979
2241
  "mode",
2242
+ "executionMode",
1980
2243
  "localDefault",
1981
2244
  "trivialPromptMaxChars",
1982
2245
  "frontierPromptMinChars",
@@ -2063,7 +2326,9 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
2063
2326
  ]
2064
2327
  ],
2065
2328
  ["graph.worktrees", ["enabled"]],
2066
- ["orchestrator", ["roles", "maxRetries"]]
2329
+ ["orchestrator", ["roles", "primary", "maxRetries", "maxModelsPerNode"]],
2330
+ ["orchestrator.primary", ["executionMode", "model", "fallbacks"]],
2331
+ ["orchestrator.primary.fallbacks[]", MODEL_REF_KEYS]
2067
2332
  ]);
2068
2333
  function isUnrecognizedKeysIssue(issue) {
2069
2334
  return issue.code === "unrecognized_keys" && Array.isArray(issue.keys);
@@ -2169,6 +2434,211 @@ function formatConfigError(error) {
2169
2434
  });
2170
2435
  }
2171
2436
 
2437
+ // src/messages/executionSetup.ts
2438
+ var executionSetupMessages = {
2439
+ setupIntro: "openteam setup — configure local, frontier, or mixed team execution",
2440
+ runtimeQuestion: "Which local runtimes do you want to enable? (choose 'None' for frontier-only)",
2441
+ runtimeNoneLabel: "None — use frontier models only",
2442
+ runtimeNoneHint: "no local runtime (frontier execution)",
2443
+ executionModeQuestion: "Team execution mode",
2444
+ executionModeChoices: {
2445
+ local: {
2446
+ label: "local — every agent stays on configured local providers",
2447
+ hint: "requires at least one enabled local runtime"
2448
+ },
2449
+ frontier: {
2450
+ label: "frontier — every agent uses frontier providers",
2451
+ hint: "does not require a local runtime"
2452
+ },
2453
+ mixed: {
2454
+ label: "mixed — policies may select local or frontier models",
2455
+ hint: "the global mode is an upper bound; per-agent policies may narrow it"
2456
+ }
2457
+ },
2458
+ frontierOnlyNote: [
2459
+ "No local runtime is enabled, so the team will use frontier providers.",
2460
+ "The selected primary model is written consistently to opencode.json and",
2461
+ ".opencode/agent/openteam.md."
2462
+ ].join(`
2463
+ `),
2464
+ frontierOnlyTitle: "Frontier execution",
2465
+ localOnlyNote: (model) => [
2466
+ "Every team agent is restricted to configured local providers.",
2467
+ `The primary coordinator is pinned to ${model}.`,
2468
+ "No frontier provider is required for execution while this mode is active."
2469
+ ].join(`
2470
+ `),
2471
+ localOnlyTitle: "Local execution",
2472
+ localRequiredError: "local execution requires an enabled local runtime",
2473
+ mixedPrimaryQuestion: "Primary coordinator model",
2474
+ mixedPrimaryLocal: (model) => `Use local thinking model ${model}`,
2475
+ mixedPrimaryFrontier: "Choose a frontier model",
2476
+ frontierModelQuestion: "Frontier model (type to search; cheapest capable entries first)",
2477
+ primaryPolicySummary: (params) => [
2478
+ `Execution mode: ${params.executionMode}`,
2479
+ `Primary model: ${params.primaryModel}`,
2480
+ `Primary fallbacks: ${params.fallbacks}`,
2481
+ "Worker model policy: auto within the global execution mode"
2482
+ ],
2483
+ noFallbacks: "none",
2484
+ yoloQuestion: "Enable YOLO mode? (opencode auto-approves all permissions)",
2485
+ configuredSummary: (params) => [
2486
+ ...executionSetupMessages.primaryPolicySummary({
2487
+ executionMode: params.executionMode,
2488
+ primaryModel: params.primaryModel,
2489
+ fallbacks: executionSetupMessages.noFallbacks
2490
+ }),
2491
+ `Frontier baseline: ${params.frontierBaseline}`,
2492
+ `Local runtimes: ${params.localRuntimes}`,
2493
+ `YOLO mode: ${params.yolo ? "enabled (auto-approves permissions)" : "disabled"}`,
2494
+ "Web Console: launched separately with 'openteam console' (multi-session, loopback)",
2495
+ `Wrote: ${params.opencodePath}, ${params.configPath}, ${params.agentPath}, ${params.agentDir}/*.md (${params.roleAgentCount} standard subagents), ${params.commandDir}/*.md (${params.commandCount} commands); ensured ${params.gitignorePath}`,
2496
+ "",
2497
+ "Next steps:",
2498
+ params.executionMode === "local" ? " 1. No frontier authentication is required while local execution is active" : " 1. Authenticate the frontier provider: opencode auth login",
2499
+ params.executionMode === "local" ? executionSetupMessages.localRuntimeNextStep : params.executionMode === "mixed" ? executionSetupMessages.mixedNextStep : executionSetupMessages.frontierNextStep,
2500
+ " 3. Press Tab and pick the 'openteam' agent, or type / and pick an /openteam… command"
2501
+ ],
2502
+ configuredTitle: "openteam configured",
2503
+ complete: (paths) => `openteam setup complete: ${paths}`,
2504
+ error: (message) => `setup error: ${message}`,
2505
+ localRuntimeNextStep: " 2. Make sure every configured local runtime is reachable",
2506
+ frontierNextStep: " 2. Open opencode in this repo; openteam will use frontier models",
2507
+ mixedNextStep: " 2. Make sure local runtimes are reachable; unavailable candidates can use configured fallbacks",
2508
+ done: "Done. Open opencode and select the 'openteam' agent.",
2509
+ rolePolicy: {
2510
+ whyTitle: "Configure execution and model selection",
2511
+ why: (count) => `${count} roster role(s) have no explicit execution/model override. Configure a compact policy now, or leave them on the built-in automatic policy.`,
2512
+ modeQuestion: (roleID, agentName) => `Execution mode for "${roleID}" (${agentName})`,
2513
+ modeInheritLabel: "Inherit the team execution mode",
2514
+ modeLocalLabel: "Local only",
2515
+ modeFrontierLabel: "Frontier only",
2516
+ modelQuestion: (roleID) => `Model selection for "${roleID}"`,
2517
+ modelAutoLabel: "Auto — choose the cheapest capable eligible model",
2518
+ modelExactLabel: "Exact provider/model pin",
2519
+ modelExactQuestion: (roleID) => `Exact model for "${roleID}" as provider/model`,
2520
+ modelExactPlaceholder: "provider/model",
2521
+ fallbacksQuestion: (roleID) => `Ordered fallbacks for "${roleID}" (comma-separated provider/model, blank for none)`,
2522
+ fallbacksPlaceholder: "provider/model, provider/model",
2523
+ configured: (roleIDs, path) => ` ✓ execution/model policy: ${roleIDs} → ${path}`,
2524
+ invalidModelRef: (value) => `invalid model "${value}"; expected provider/model`
2525
+ }
2526
+ };
2527
+ var executionPolicyMessages = {
2528
+ doctorHelp: " openteam doctor Diagnose runtimes, execution policies, and config",
2529
+ agentsHelp: " openteam agents List agents, model identity, and execution policy",
2530
+ section: "Configured execution policies:",
2531
+ globalMode: (mode) => ` execution mode: ${mode}`,
2532
+ line: (policy) => ` ${policy.owner}: mode=${policy.executionMode ?? "inherit"} · model=${policy.model} · fallbacks=${policy.fallbacks.length > 0 ? policy.fallbacks.join(" → ") : "none"}`,
2533
+ retryLimits: (maxRetries, maxModelsPerNode) => ` retry limits: ${maxRetries} retries per model · ${maxModelsPerNode} model(s) per node`,
2534
+ inheritedDefaultUnset: "inherits the opencode.json default (not set); execution follows the configured team and agent policy",
2535
+ inheritedDefault: (subscription) => `inherits default → ${subscription}; execution follows the configured team and agent policy`,
2536
+ unprofiledRoleRouting: " routing: no explicit override — the role inherits its built-in or fallback policy within the global execution-mode upper bound. Roster prose is not executable configuration.",
2537
+ unprofiledRoleRemedy: " remedy: run `openteam roles init`, or add executionMode, model, and ordered fallbacks under orchestrator.roles in .opencode/openteam.json.",
2538
+ noLocalMixed: " ⚠ No local runtime reachable: mixed execution currently has only frontier candidates.",
2539
+ noLocalBlocked: " ✗ No local runtime reachable: local execution is blocked until a configured runtime is available.",
2540
+ primaryLocalMismatch: (name, providerID) => ` ✗ local execution is configured, but primary agent '${name}' points at provider '${providerID}', which opencode.json does not configure.`,
2541
+ primaryLocalMismatchCause: " cause: the primary agent artifact and opencode.json no longer agree on the selected provider/model identity.",
2542
+ primaryLocalMismatchConsequence: " consequence: opencode cannot start the coordinator, so it cannot read the roster or distribute work.",
2543
+ primaryLocalMismatchRemedy: (sourceFile) => ` remedy: re-run \`openteam setup\` to regenerate ${sourceFile} and opencode.json from the same primary policy, then re-run \`openteam doctor\`.`
2544
+ };
2545
+ var executionPolicyEditorMessages = {
2546
+ title: "Execution and model policy",
2547
+ globalMode: "Global execution mode",
2548
+ primaryModel: "Primary model",
2549
+ primaryFallbacks: "Primary fallbacks",
2550
+ retryLimits: "Retry limits",
2551
+ automatic: "auto",
2552
+ inherit: "inherit",
2553
+ none: "none",
2554
+ retryValue: (maxRetries, maxModelsPerNode) => `${maxRetries} retries per model; ${maxModelsPerNode} model(s) per node`
2555
+ };
2556
+ var localModeChangeMessages = {
2557
+ localProviderInFrontierDomain: (path, domainPath, providerID, modelID) => `${path}: model "${providerID}/${modelID}" uses provider "${providerID}", which is configured for local dispatch and cannot be selected when ${domainPath} is "frontier". Update this policy or use \`openteam local on\`.`,
2558
+ frontierProviderInLocalDomain: (path, domainPath, providerID, modelID) => `${path}: model "${providerID}/${modelID}" uses provider "${providerID}", which is not a configured local provider and cannot be selected when ${domainPath} is "local". Update this policy or use \`openteam local on\`.`
2559
+ };
2560
+
2561
+ // src/config/executionPolicyDomains.ts
2562
+ function compareRoleIDs([left], [right]) {
2563
+ return left === right ? 0 : left < right ? -1 : 1;
2564
+ }
2565
+ function policyModelReferences(policy, path) {
2566
+ const references = [];
2567
+ if (policy?.model !== undefined && policy.model !== "auto") {
2568
+ references.push({ path: `${path}.model`, model: policy.model });
2569
+ }
2570
+ for (const [index, fallback] of (policy?.fallbacks ?? []).entries()) {
2571
+ references.push({
2572
+ path: `${path}.fallbacks.${index}`,
2573
+ model: fallback
2574
+ });
2575
+ }
2576
+ return references;
2577
+ }
2578
+ function configuredPolicies(config) {
2579
+ const policies = [
2580
+ {
2581
+ path: "orchestrator.primary",
2582
+ policy: config.orchestrator?.primary
2583
+ }
2584
+ ];
2585
+ const roles = Object.entries(config.orchestrator?.roles ?? {}).sort(compareRoleIDs);
2586
+ for (const [roleID, role] of roles) {
2587
+ policies.push({
2588
+ path: `orchestrator.roles.${roleID}`,
2589
+ policy: agentExecutionPolicyForRole(role)
2590
+ });
2591
+ }
2592
+ return policies;
2593
+ }
2594
+ function effectiveConfigModelDomainViolations(config) {
2595
+ const routerMode = resolveExecutionMode({
2596
+ routerMode: config.router.executionMode,
2597
+ legacyLocalOnly: config.router.localOnly,
2598
+ legacyFrontierOnly: config.router.frontierOnly
2599
+ });
2600
+ const localProviderIDs = new Set(config.local.runtimes.map((runtime) => runtime.defaultModel.providerID));
2601
+ const violations = [];
2602
+ for (const { path, policy } of configuredPolicies(config)) {
2603
+ if (policy === undefined) {
2604
+ continue;
2605
+ }
2606
+ const executionMode = resolveAgentExecutionPolicy(routerMode, policy).executionMode;
2607
+ if (executionMode === "mixed") {
2608
+ continue;
2609
+ }
2610
+ const domainPath = policy.executionMode === undefined ? "router.executionMode" : `${path}.executionMode`;
2611
+ for (const reference of policyModelReferences(policy, path)) {
2612
+ const isLocalProvider = localProviderIDs.has(reference.model.providerID);
2613
+ if (executionMode === "local" && !isLocalProvider || executionMode === "frontier" && isLocalProvider) {
2614
+ violations.push({
2615
+ path: reference.path,
2616
+ domainPath,
2617
+ model: reference.model,
2618
+ executionMode
2619
+ });
2620
+ }
2621
+ }
2622
+ }
2623
+ return violations;
2624
+ }
2625
+
2626
+ class EffectiveConfigModelDomainError extends Error {
2627
+ violations;
2628
+ constructor(violations) {
2629
+ super(violations.map((violation) => violation.executionMode === "frontier" ? localModeChangeMessages.localProviderInFrontierDomain(violation.path, violation.domainPath, violation.model.providerID, violation.model.modelID) : localModeChangeMessages.frontierProviderInLocalDomain(violation.path, violation.domainPath, violation.model.providerID, violation.model.modelID)).join(`
2630
+ `));
2631
+ this.name = "EffectiveConfigModelDomainError";
2632
+ this.violations = [...violations];
2633
+ }
2634
+ }
2635
+ function assertEffectiveConfigModelDomains(config) {
2636
+ const violations = effectiveConfigModelDomainViolations(config);
2637
+ if (violations.length > 0) {
2638
+ throw new EffectiveConfigModelDomainError(violations);
2639
+ }
2640
+ }
2641
+
2172
2642
  // src/config/load.ts
2173
2643
  function loadOpenTeamConfig(raw) {
2174
2644
  return OpenTeamConfigSchema.parse(migrateConfig(raw ?? {}));
@@ -2189,9 +2659,76 @@ async function readOpenTeamConfigFile(path = ".opencode/openteam.json", storage
2189
2659
  // src/config/persist.ts
2190
2660
  var DEFAULT_CONFIG_PATH = ".opencode/openteam.json";
2191
2661
  var DEFAULT_LOCAL_OVERLAY_PATH = ".opencode/openteam-local/config.local.json";
2662
+ var ROLE_OVERRIDE_KEYS = [
2663
+ "opencodeAgent",
2664
+ "defaultTier",
2665
+ "minReasoningTier",
2666
+ "minCodeQualityTier",
2667
+ "needsToolCallingByDefault",
2668
+ "preferredFrontierModels",
2669
+ "localFirst",
2670
+ "requiresLocalRuntime",
2671
+ "localRuntimes",
2672
+ "localModels",
2673
+ "localModelClass",
2674
+ "executionMode",
2675
+ "model",
2676
+ "fallbacks"
2677
+ ];
2678
+ function sameConfigValue(left, right) {
2679
+ return left === right || JSON.stringify(left) === JSON.stringify(right);
2680
+ }
2681
+ function compactRoleOverride(roleID, role) {
2682
+ const base = getRoleProfile(roleID) ?? synthesiseFallbackProfile(roleID);
2683
+ const override = {};
2684
+ for (const key of ROLE_OVERRIDE_KEYS) {
2685
+ if (!sameConfigValue(role[key], base[key])) {
2686
+ override[key] = role[key];
2687
+ }
2688
+ }
2689
+ return override;
2690
+ }
2691
+ function compactRoles(roles) {
2692
+ return Object.fromEntries(Object.entries(roles).sort(([left], [right]) => left.localeCompare(right)).map(([roleID, role]) => [roleID, compactRoleOverride(roleID, role)]));
2693
+ }
2192
2694
  function serializeOpenTeamConfig(config) {
2193
2695
  const validated = OpenTeamConfigSchema.parse(config);
2194
- return `${JSON.stringify(validated, null, 2)}
2696
+ const {
2697
+ privacyMode: _privacyMode,
2698
+ router: validatedRouter,
2699
+ ...withoutLegacyPrivacy
2700
+ } = validated;
2701
+ const {
2702
+ localOnly: _localOnly,
2703
+ frontierOnly: _frontierOnly,
2704
+ mode,
2705
+ trivialPromptMaxChars,
2706
+ frontierPromptMinChars,
2707
+ ...canonicalRouter
2708
+ } = validatedRouter;
2709
+ const executionMode = resolveExecutionMode({
2710
+ routerMode: validatedRouter.executionMode,
2711
+ legacyLocalOnly: validatedRouter.localOnly,
2712
+ legacyFrontierOnly: validatedRouter.frontierOnly
2713
+ });
2714
+ const router = {
2715
+ ...canonicalRouter,
2716
+ ...mode === "balanced" ? {} : { mode },
2717
+ ...trivialPromptMaxChars === 280 ? {} : { trivialPromptMaxChars },
2718
+ ...frontierPromptMinChars === 2000 ? {} : { frontierPromptMinChars },
2719
+ executionMode
2720
+ };
2721
+ const orchestrator = validated.orchestrator === undefined ? undefined : {
2722
+ ...validated.orchestrator,
2723
+ roles: compactRoles(validated.orchestrator.roles)
2724
+ };
2725
+ const canonical = {
2726
+ ...withoutLegacyPrivacy,
2727
+ configVersion: CURRENT_CONFIG_VERSION,
2728
+ router,
2729
+ ...orchestrator === undefined ? {} : { orchestrator }
2730
+ };
2731
+ return `${JSON.stringify(canonical, null, 2)}
2195
2732
  `;
2196
2733
  }
2197
2734
  async function writeOpenTeamConfigFile(config, path = DEFAULT_CONFIG_PATH, storage = createFsStorageProvider(process.cwd())) {
@@ -2328,6 +2865,7 @@ async function loadRuntimeOpenTeamConfig(storage, options, path = DEFAULT_CONFIG
2328
2865
  ].filter((entry) => entry !== undefined);
2329
2866
  const label = mergedSources.length === 0 ? "runtime config" : `runtime config merged from ${mergedSources.join(", ")}`;
2330
2867
  const config = validateConfigInput(label, mergedRaw);
2868
+ const deprecations = detectConfigDeprecations(mergedRaw);
2331
2869
  const source = optionsFound ? "options" : fileFound ? "file" : "default";
2332
2870
  return {
2333
2871
  config,
@@ -2335,6 +2873,7 @@ async function loadRuntimeOpenTeamConfig(storage, options, path = DEFAULT_CONFIG
2335
2873
  fileFound,
2336
2874
  optionsFound,
2337
2875
  overlayFound,
2876
+ deprecations,
2338
2877
  searchedPaths: overlayPath === null ? [path] : [path, overlayPath]
2339
2878
  };
2340
2879
  }
@@ -2454,6 +2993,10 @@ function scrubSecretStrings(value, secrets) {
2454
2993
  };
2455
2994
  return walk(value);
2456
2995
  }
2996
+ function withoutDeprecatedPrivacyMode(config) {
2997
+ const { privacyMode: _privacyMode, ...next } = config;
2998
+ return next;
2999
+ }
2457
3000
  var DEFAULT_CONFIG_AUDIT_PATH = ".opencode/openteam-local/config-audit.jsonl";
2458
3001
  function editableSurface() {
2459
3002
  return [
@@ -2491,7 +3034,7 @@ function createConfigEditor(ports) {
2491
3034
  editableSurface,
2492
3035
  async read() {
2493
3036
  const resolution = await loadRuntimeOpenTeamConfig(ports.storage, undefined, basePath, overlayPath);
2494
- const scrubbed = scrubSecretStrings(resolution.config, secretValues());
3037
+ const scrubbed = withoutDeprecatedPrivacyMode(scrubSecretStrings(resolution.config, secretValues()));
2495
3038
  return {
2496
3039
  config: scrubbed,
2497
3040
  reload: {
@@ -2513,6 +3056,7 @@ function createConfigEditor(ports) {
2513
3056
  let submitted;
2514
3057
  try {
2515
3058
  submitted = OpenTeamConfigObjectSchema.parse(submittedRaw);
3059
+ assertEffectiveConfigModelDomains(submitted);
2516
3060
  } catch (error) {
2517
3061
  return {
2518
3062
  status: 400,
@@ -2578,7 +3122,6 @@ var RESTART_REQUIRED_KEYS = [
2578
3122
  "baseline",
2579
3123
  "router",
2580
3124
  "local",
2581
- "privacyMode",
2582
3125
  "graph",
2583
3126
  "orchestrator",
2584
3127
  "memory",
@@ -2586,6 +3129,110 @@ var RESTART_REQUIRED_KEYS = [
2586
3129
  "console.port"
2587
3130
  ];
2588
3131
 
3132
+ // src/messages/commands.ts
3133
+ var clearCacheMessages = {
3134
+ header: "openteam clear-cache — frozen plugin cache entries:",
3135
+ columns: {
3136
+ specDir: "spec dir",
3137
+ pinned: "spec-pinned",
3138
+ installed: "installed",
3139
+ mtime: "mtime"
3140
+ },
3141
+ reparseSkipSuffix: " [SKIP — reparse point]",
3142
+ deletedLabel: "deleted.",
3143
+ lockedLabel: (message) => `[LOCKED] ${message}`,
3144
+ pathOutsideWarning: (specDir, absolutePath) => ` ⚠ ${specDir}: path outside cacheRoot (${absolutePath}), skipped.`,
3145
+ processed: (count) => `${count} entry(ies) processed.`,
3146
+ found: (count) => `${count} entry(ies) found. Use --delete to remove them.`
3147
+ };
3148
+ var baselineMessages = {
3149
+ effectiveAuto: "cheapest-capable (auto)",
3150
+ pinnedSuffix: (ref) => `${ref} (pinned)`,
3151
+ summary: (params) => [
3152
+ "openteam baseline:",
3153
+ ` mode: ${params.mode}`,
3154
+ ` pinned: ${params.pinned}`,
3155
+ ` hardDefault: ${params.hardDefault}`,
3156
+ ` effective: ${params.effective}`
3157
+ ],
3158
+ invalidModel: (input) => `Invalid model "${input}". Use the provider/model format, e.g. anthropic/claude-sonnet-4-5.`,
3159
+ pinnedTo: (ref) => `Baseline pinned to ${ref} (pinned mode).`,
3160
+ autoMode: "Baseline set to auto mode (cheapest-capable)."
3161
+ };
3162
+ var localMessages = {
3163
+ help: {
3164
+ status: " openteam local status Show the execution mode (local / frontier / mixed)",
3165
+ off: " openteam local off Use frontier models only",
3166
+ only: " openteam local only Use local models only",
3167
+ on: " openteam local on Enable mixed local/frontier execution"
3168
+ },
3169
+ noRuntimes: "none",
3170
+ modeFrontier: "frontier",
3171
+ modeLocal: "local",
3172
+ modeMixed: "mixed",
3173
+ summary: (params) => [
3174
+ "openteam local:",
3175
+ ` execution mode: ${params.mode}`,
3176
+ ` local runtimes: ${params.runtimes}`
3177
+ ],
3178
+ frontierOnlyNoChange: "No change: execution mode is already frontier.",
3179
+ frontierOnlyEnabled: "Frontier execution enabled.",
3180
+ localOnlyNoRuntimes: "Cannot enable local execution: no enabled local runtime is configured. Run 'openteam setup' or enable a local runtime in .opencode/openteam.json first.",
3181
+ localOnlyNoChange: "No change: execution mode is already local.",
3182
+ localOnlyEnabled: "Local execution enabled.",
3183
+ localFirstNoChange: "No change: execution mode is already mixed.",
3184
+ localFirstReEnabled: "Mixed local/frontier execution enabled. Make sure each configured provider is reachable.",
3185
+ unknownSubcommand: (subcommand, help) => `Unknown local subcommand: ${subcommand}
3186
+
3187
+ ${help}`
3188
+ };
3189
+ var yoloMessages = {
3190
+ updated: (path, status, agentNote) => `${path} updated. ${status}${agentNote}
3191
+ Restart opencode (or reload) so agents pick up the new permissions.`
3192
+ };
3193
+ var frontierCatalogMessages = {
3194
+ free: "free",
3195
+ costPerMillion: (inputUSD, outputUSD) => `$${inputUSD}/$${outputUSD} per 1M`
3196
+ };
3197
+ var migrateMessages = {
3198
+ header: "openteam migrate:",
3199
+ nothingToMigrate: " nothing to migrate — the P15b layout is already in place.",
3200
+ counts: (params) => [
3201
+ ` moved: ${params.moved} file(s)`,
3202
+ ` deduped: ${params.deduped} file(s) (destination already identical)`,
3203
+ ` conflicts: ${params.conflicts} file(s)`
3204
+ ],
3205
+ relocatedHeader: " relocated:",
3206
+ relocatedEntry: (from, to, deduped) => ` · ${from} → ${to}${deduped ? " (deduped)" : ""}`,
3207
+ conflictsHeader: " ⚠ left in place (destination exists with different content — no data lost):",
3208
+ conflictEntry: (from, to) => ` · ${from} → ${to}`,
3209
+ manualWorktreesHeader: " ⚠ legacy git worktree(s) need a manual move (run `git worktree move`):",
3210
+ manualWorktreeEntry: (label, from, to) => ` · ${label}: ${from} → ${to}`,
3211
+ success: " ✓ machine-local runtime output relocated under .opencode/openteam-local/."
3212
+ };
3213
+ var rolesInitMessages = {
3214
+ help: " openteam roles init Compose per-role execution, model, and fallback policy",
3215
+ header: "openteam roles init:",
3216
+ title: "openteam roles init",
3217
+ noRoster: (path) => ` ✗ no roster at ${path}.`,
3218
+ noRosterRemedy: " Cast the team first: run `openteam setup`, then ask the orchestrator to register the cast.",
3219
+ unparseableRoster: (path, error) => ` ✗ ${path} is present but unparseable: ${error}`,
3220
+ unparseableRosterRemedy: " Run `openteam doctor` for the remedy, then re-run this command.",
3221
+ allProfiled: (count) => ` ✓ ${count} roster role(s) checked; no unprofiled roles — every role resolves a routing profile.`,
3222
+ skipLabel: "Skip for now",
3223
+ skipHint: "leaves the role on its built-in or fallback policy; doctor keeps reporting the missing explicit override",
3224
+ runtimeQuestion: (roleID) => `Preferred local runtime(s) for "${roleID}" (in failover order)`,
3225
+ runtimeHint: "machine-local — written to the git-ignored overlay",
3226
+ cancelled: (reason) => ` ✗ cancelled (${reason}); nothing was written.`,
3227
+ invalidResult: (reason) => ` ✗ refusing to write: the resulting config is invalid — ${reason}`,
3228
+ deprecation: (message) => ` ⚠ ${message}`,
3229
+ nothingWritten: (skipped) => ` · skipped ${skipped}; nothing written.`,
3230
+ nothingToWriteOutro: "Nothing to write.",
3231
+ runtimeBinding: (path) => ` ✓ runtime binding: ${path} (git-ignored, machine-local)`,
3232
+ skippedSummary: (roleIDs) => ` · skipped: ${roleIDs} — using built-in or fallback policy; re-run this command to add an explicit override.`,
3233
+ outro: "Role policy updated."
3234
+ };
3235
+
2589
3236
  // src/web/graphRoute.ts
2590
3237
  var GRAPH_DISABLED_REMEDIATION = 'Agent tree is off. In .opencode/openteam.json set graph.mode to a value other than "off" and console.graphView.enabled to true, then restart the console.';
2591
3238
  var GRAPH_DISABLED_ENVELOPE = {
@@ -2995,10 +3642,10 @@ function escapeHtml(value) {
2995
3642
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2996
3643
  }
2997
3644
  function usd(value) {
2998
- return `$${value.toFixed(5)}`;
3645
+ return value === null ? routingMessages.unknownCost : `$${value.toFixed(5)}`;
2999
3646
  }
3000
3647
  function pct(value) {
3001
- return `${value.toFixed(2)}%`;
3648
+ return value === null ? routingMessages.unknownCost : `${value.toFixed(2)}%`;
3002
3649
  }
3003
3650
  function ms(value) {
3004
3651
  return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
@@ -3694,6 +4341,42 @@ var OVERLAY_PATH = ".opencode/openteam-local/config.local.json";
3694
4341
  function codeList(keys) {
3695
4342
  return keys.map((key) => `<code>${escapeHtml(key)}</code>`).join(", ");
3696
4343
  }
4344
+ function record(value) {
4345
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
4346
+ }
4347
+ function modelSelection(value) {
4348
+ if (value === "auto" || value === undefined) {
4349
+ return executionPolicyEditorMessages.automatic;
4350
+ }
4351
+ const selection = record(value);
4352
+ return typeof selection.providerID === "string" && typeof selection.modelID === "string" ? `${selection.providerID}/${selection.modelID}` : executionPolicyEditorMessages.automatic;
4353
+ }
4354
+ function fallbackSelections(value) {
4355
+ if (!Array.isArray(value) || value.length === 0) {
4356
+ return executionPolicyEditorMessages.none;
4357
+ }
4358
+ return value.map(modelSelection).join(" → ");
4359
+ }
4360
+ function renderExecutionPolicyOverview(config) {
4361
+ const root = record(config);
4362
+ const router = record(root.router);
4363
+ const orchestrator = record(root.orchestrator);
4364
+ const primary = record(orchestrator.primary);
4365
+ const mode = typeof router.executionMode === "string" ? router.executionMode : router.localOnly === true ? "local" : router.frontierOnly === true ? "frontier" : "mixed";
4366
+ const maxRetries = typeof orchestrator.maxRetries === "number" ? orchestrator.maxRetries : 2;
4367
+ const maxModelsPerNode = typeof orchestrator.maxModelsPerNode === "number" ? orchestrator.maxModelsPerNode : 2;
4368
+ return [
4369
+ '<div class="config-policy">',
4370
+ `<h3>${escapeHtml(executionPolicyEditorMessages.title)}</h3>`,
4371
+ '<dl class="config-policy-list">',
4372
+ `<dt>${escapeHtml(executionPolicyEditorMessages.globalMode)}</dt><dd><code>${escapeHtml(mode)}</code></dd>`,
4373
+ `<dt>${escapeHtml(executionPolicyEditorMessages.primaryModel)}</dt><dd><code>${escapeHtml(modelSelection(primary.model))}</code></dd>`,
4374
+ `<dt>${escapeHtml(executionPolicyEditorMessages.primaryFallbacks)}</dt><dd><code>${escapeHtml(fallbackSelections(primary.fallbacks))}</code></dd>`,
4375
+ `<dt>${escapeHtml(executionPolicyEditorMessages.retryLimits)}</dt><dd>${escapeHtml(executionPolicyEditorMessages.retryValue(maxRetries, maxModelsPerNode))}</dd>`,
4376
+ "</dl>",
4377
+ "</div>"
4378
+ ].join("");
4379
+ }
3697
4380
  function renderConfigReloadTable(read) {
3698
4381
  const hot = read.reload.hotReloadable.length > 0 ? codeList(read.reload.hotReloadable) : '<span class="muted">none</span>';
3699
4382
  const restart = read.reload.restartRequired.length > 0 ? codeList(read.reload.restartRequired) : '<span class="muted">none</span>';
@@ -3713,6 +4396,7 @@ function renderConfigEditorForm(read) {
3713
4396
  const secretsNote = read.secrets.length > 0 ? `<p class="config-note config-secrets">Secrets are read from environment variables and are never shown or editable here: ${read.secrets.map((s) => `<code>${escapeHtml(s.key)}</code> (${s.set ? "set" : "not set"})`).join(", ")}.</p>` : "";
3714
4397
  return [
3715
4398
  '<form class="config-form" data-config-form>',
4399
+ renderExecutionPolicyOverview(read.config),
3716
4400
  renderConfigReloadTable(read),
3717
4401
  overlayNote,
3718
4402
  secretsNote,
@@ -3911,6 +4595,9 @@ function emptyTierSummary() {
3911
4595
  hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
3912
4596
  };
3913
4597
  }
4598
+ function addEstimate2(current, value) {
4599
+ return current === null || value === null ? null : current + value;
4600
+ }
3914
4601
  function summarizeCostRecords(records) {
3915
4602
  const report = {
3916
4603
  count: 0,
@@ -3925,30 +4612,33 @@ function summarizeCostRecords(records) {
3925
4612
  tokensOut: 0,
3926
4613
  byTier: emptyTierSummary()
3927
4614
  };
3928
- for (const record of records) {
4615
+ for (const record2 of records) {
3929
4616
  report.count += 1;
3930
- if (record.routeKind === "local") {
4617
+ if (record2.routeKind === "local") {
3931
4618
  report.localCount += 1;
3932
- } else if (record.routeKind === "blocked") {
4619
+ } else if (record2.routeKind === "blocked") {
3933
4620
  report.blockedCount += 1;
3934
4621
  } else {
3935
4622
  report.frontierCount += 1;
3936
4623
  }
3937
- report.totalEstimatedUSD += record.estimatedCostUSD;
3938
- report.totalBaselineUSD += record.baselineCostUSD;
3939
- report.totalSavingsUSD += record.estimatedSavingsUSD;
3940
- report.tokensIn += record.tokensIn ?? 0;
3941
- report.tokensOut += record.tokensOut ?? 0;
3942
- const tier = report.byTier[record.tier];
4624
+ report.totalEstimatedUSD = addEstimate2(report.totalEstimatedUSD, record2.estimatedCostUSD);
4625
+ report.totalBaselineUSD = addEstimate2(report.totalBaselineUSD, record2.baselineCostUSD);
4626
+ report.totalSavingsUSD = addEstimate2(report.totalSavingsUSD, record2.estimatedSavingsUSD);
4627
+ report.tokensIn += record2.tokensIn ?? 0;
4628
+ report.tokensOut += record2.tokensOut ?? 0;
4629
+ const tier = report.byTier[record2.tier];
3943
4630
  tier.count += 1;
3944
- tier.estimatedUSD += record.estimatedCostUSD;
3945
- tier.savingsUSD += record.estimatedSavingsUSD;
4631
+ tier.estimatedUSD = addEstimate2(tier.estimatedUSD, record2.estimatedCostUSD);
4632
+ tier.savingsUSD = addEstimate2(tier.savingsUSD, record2.estimatedSavingsUSD);
3946
4633
  }
3947
- report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
4634
+ report.savingsPct = report.totalBaselineUSD === null || report.totalSavingsUSD === null ? null : report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
3948
4635
  return report;
3949
4636
  }
3950
4637
  function usd2(value) {
3951
- return `$${value.toFixed(5)}`;
4638
+ return value === null ? routingMessages.unknownCost : `$${value.toFixed(5)}`;
4639
+ }
4640
+ function pct2(value) {
4641
+ return value === null ? routingMessages.unknownCost : `${value.toFixed(2)}%`;
3952
4642
  }
3953
4643
  function renderReport(report) {
3954
4644
  if (report.count === 0) {
@@ -3959,7 +4649,7 @@ function renderReport(report) {
3959
4649
  ` decisions: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount} · blocked ${report.blockedCount})`,
3960
4650
  ` real cost: ${usd2(report.totalEstimatedUSD)}`,
3961
4651
  ` baseline: ${usd2(report.totalBaselineUSD)}`,
3962
- ` savings: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
4652
+ ` savings: ${usd2(report.totalSavingsUSD)} (${pct2(report.savingsPct)})`,
3963
4653
  ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
3964
4654
  " by tier:"
3965
4655
  ];
@@ -4044,16 +4734,16 @@ var DEFAULT_RECENT_ROUTES = 50;
4044
4734
  var DEFAULT_ACTIVITY_LIMIT = 100;
4045
4735
  var DEFAULT_DECISIONS_LIMIT = 100;
4046
4736
  var DEFAULT_MEETINGS_LIMIT = 50;
4047
- function toRouteView(record) {
4737
+ function toRouteView(record2) {
4048
4738
  return {
4049
- ts: record.ts,
4050
- tier: record.tier,
4051
- routeKind: record.routeKind,
4052
- model: `${record.selected.providerID}/${record.selected.modelID}`,
4053
- promptHash: record.promptHash,
4054
- estimatedCostUSD: record.estimatedCostUSD,
4055
- estimatedSavingsUSD: record.estimatedSavingsUSD,
4056
- budgetAction: record.budgetAction
4739
+ ts: record2.ts,
4740
+ tier: record2.tier,
4741
+ routeKind: record2.routeKind,
4742
+ model: `${record2.selected.providerID}/${record2.selected.modelID}`,
4743
+ promptHash: record2.promptHash,
4744
+ estimatedCostUSD: record2.estimatedCostUSD,
4745
+ estimatedSavingsUSD: record2.estimatedSavingsUSD,
4746
+ budgetAction: record2.budgetAction
4057
4747
  };
4058
4748
  }
4059
4749
  function isRoute(event) {
@@ -4106,7 +4796,7 @@ function activityFeed(aggregate, limit) {
4106
4796
  function buildSessionTabs(aggregate, costRecords, nowMs, limits, options) {
4107
4797
  return aggregate.sessions.map((summary) => {
4108
4798
  const id = summary.sessionID;
4109
- const routes = costRecords.filter((record) => record.sessionID === id);
4799
+ const routes = costRecords.filter((record2) => record2.sessionID === id);
4110
4800
  const decisions = aggregate.decisions.filter((decision) => decision.sessionID === id).slice(0, Math.max(0, limits.decisionsLimit));
4111
4801
  const meetings = aggregate.meetings.filter((meeting) => meeting.sessionID === id).slice(0, Math.max(0, limits.meetingsLimit));
4112
4802
  const activity = composeActivity(aggregate.activity.filter((view) => view.sessionID === id), aggregate.decisions.filter((decision) => decision.sessionID === id), aggregate.meetings.filter((meeting) => meeting.sessionID === id), limits.activityLimit);
@@ -4284,9 +4974,9 @@ async function handlePermission(request, response, sessionID, deps) {
4284
4974
  return;
4285
4975
  }
4286
4976
  const body = await readJsonBody2(request, 4096);
4287
- const record = typeof body === "object" && body !== null ? body : {};
4288
- const permissionID = record.permissionID;
4289
- const responseValue = record.response;
4977
+ const record2 = typeof body === "object" && body !== null ? body : {};
4978
+ const permissionID = record2.permissionID;
4979
+ const responseValue = record2.response;
4290
4980
  if (typeof permissionID !== "string" || permissionID.length === 0 || responseValue !== "once" && responseValue !== "always" && responseValue !== "reject") {
4291
4981
  response.writeHead(400, JSON_HEADERS3).end(JSON.stringify({ error: "invalid permission" }));
4292
4982
  return;
@@ -4446,8 +5136,8 @@ async function handleWrite(request, response, op, deps) {
4446
5136
  return;
4447
5137
  }
4448
5138
  const body = await readJsonBody3(request, 8 * 1024 * 1024);
4449
- const record = typeof body === "object" && body !== null ? body : undefined;
4450
- const rawPath = record?.path;
5139
+ const record2 = typeof body === "object" && body !== null ? body : undefined;
5140
+ const rawPath = record2?.path;
4451
5141
  if (typeof rawPath !== "string") {
4452
5142
  sendJson(response, 400, { error: "invalid path" });
4453
5143
  return;
@@ -4467,7 +5157,7 @@ async function handleWrite(request, response, op, deps) {
4467
5157
  return;
4468
5158
  }
4469
5159
  if (op === "write" || op === "append") {
4470
- const content = record?.content;
5160
+ const content = record2?.content;
4471
5161
  if (typeof content !== "string") {
4472
5162
  sendJson(response, 400, { error: "invalid content" });
4473
5163
  return;
@@ -4845,7 +5535,7 @@ function classifyAgent(info, context) {
4845
5535
  name: info.name,
4846
5536
  mode: info.mode,
4847
5537
  kind: "inherited",
4848
- subscription: "inherits the opencode.json default (not set) · openteam routes local-first per message"
5538
+ subscription: executionPolicyMessages.inheritedDefaultUnset
4849
5539
  };
4850
5540
  }
4851
5541
  const { subscription } = subscriptionLabel(context.defaultModel, context.localProviders);
@@ -4854,7 +5544,7 @@ function classifyAgent(info, context) {
4854
5544
  mode: info.mode,
4855
5545
  kind: "inherited",
4856
5546
  model: context.defaultModel,
4857
- subscription: `inherits default → ${subscription} · openteam routes local-first per message`
5547
+ subscription: executionPolicyMessages.inheritedDefault(subscription)
4858
5548
  };
4859
5549
  }
4860
5550
  function modeRank(mode) {
@@ -4875,6 +5565,51 @@ function classifyAgents(files, context) {
4875
5565
  function modelText(classification) {
4876
5566
  return classification.model === undefined ? "" : ` (${classification.model.providerID}/${classification.model.modelID})`;
4877
5567
  }
5568
+ function asRecord3(value) {
5569
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
5570
+ }
5571
+ function selectionText(value) {
5572
+ if (value === "auto" || value === undefined) {
5573
+ return "auto";
5574
+ }
5575
+ const ref = asRecord3(value);
5576
+ return typeof ref?.providerID === "string" && typeof ref.modelID === "string" ? `${ref.providerID}/${ref.modelID}` : "auto";
5577
+ }
5578
+ function fallbackTexts(value) {
5579
+ if (!Array.isArray(value)) {
5580
+ return [];
5581
+ }
5582
+ return value.map(selectionText).filter((selection) => selection !== "auto");
5583
+ }
5584
+ function executionPolicySummaries(config) {
5585
+ const root = config;
5586
+ const router = asRecord3(root.router);
5587
+ const orchestrator = asRecord3(root.orchestrator);
5588
+ const primary = asRecord3(orchestrator?.primary);
5589
+ const roles = asRecord3(orchestrator?.roles);
5590
+ const globalMode = typeof router?.executionMode === "string" ? router.executionMode : router?.localOnly === true ? "local" : router?.frontierOnly === true ? "frontier" : "mixed";
5591
+ const summaries = [
5592
+ {
5593
+ owner: "primary",
5594
+ executionMode: typeof primary?.executionMode === "string" ? primary.executionMode : globalMode,
5595
+ model: selectionText(primary?.model),
5596
+ fallbacks: fallbackTexts(primary?.fallbacks)
5597
+ }
5598
+ ];
5599
+ for (const [roleID, value] of Object.entries(roles ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
5600
+ const policy = asRecord3(value);
5601
+ if (policy === undefined) {
5602
+ continue;
5603
+ }
5604
+ summaries.push({
5605
+ owner: roleID,
5606
+ ...typeof policy.executionMode === "string" ? { executionMode: policy.executionMode } : {},
5607
+ model: selectionText(policy.model),
5608
+ fallbacks: fallbackTexts(policy.fallbacks)
5609
+ });
5610
+ }
5611
+ return summaries;
5612
+ }
4878
5613
  function renderAgentList(classifications, context) {
4879
5614
  const lines = ["openteam agents — LLM per agent:", ""];
4880
5615
  if (classifications.length === 0) {
@@ -4901,6 +5636,15 @@ function renderAgentList(classifications, context) {
4901
5636
  lines.push("opencode.json default: (not set)");
4902
5637
  }
4903
5638
  lines.push("Legend: ● primary · ○ subagent · local = no token cost · frontier = uses your subscription.");
5639
+ if (context.executionPolicies !== undefined && context.executionPolicies.length > 0) {
5640
+ lines.push("", executionPolicyMessages.section);
5641
+ for (const policy of context.executionPolicies) {
5642
+ lines.push(executionPolicyMessages.line(policy));
5643
+ }
5644
+ if (context.retryLimits !== undefined) {
5645
+ lines.push(executionPolicyMessages.retryLimits(context.retryLimits.maxRetries, context.retryLimits.maxModelsPerNode));
5646
+ }
5647
+ }
4904
5648
  if (context.opencodeConfigError !== undefined) {
4905
5649
  lines.push("");
4906
5650
  lines.push(`⚠ opencode config parse error: ${context.opencodeConfigError}`);
@@ -4923,22 +5667,22 @@ var KNOWN_FRONTIER_PROVIDER_IDS = new Set([
4923
5667
  "openrouter"
4924
5668
  ]);
4925
5669
  var VIRTUAL_ROUTER_PROVIDER_ID = "openteam-router";
4926
- function isRecord(value) {
5670
+ function isRecord2(value) {
4927
5671
  return typeof value === "object" && value !== null && !Array.isArray(value);
4928
5672
  }
4929
5673
  function parseOpencodeProviders(opencodeConfig) {
4930
5674
  const result = new Map;
4931
5675
  const provider = opencodeConfig?.provider;
4932
- if (!isRecord(provider)) {
5676
+ if (!isRecord2(provider)) {
4933
5677
  return result;
4934
5678
  }
4935
5679
  for (const [id, value] of Object.entries(provider)) {
4936
- if (!isRecord(value)) {
5680
+ if (!isRecord2(value)) {
4937
5681
  result.set(id, {});
4938
5682
  continue;
4939
5683
  }
4940
5684
  const models = value.models;
4941
- result.set(id, isRecord(models) ? { models: new Set(Object.keys(models)) } : {});
5685
+ result.set(id, isRecord2(models) ? { models: new Set(Object.keys(models)) } : {});
4942
5686
  }
4943
5687
  return result;
4944
5688
  }
@@ -5680,8 +6424,8 @@ function canonical(value) {
5680
6424
  if (value === null || typeof value !== "object") {
5681
6425
  return JSON.stringify(value);
5682
6426
  }
5683
- const record = value;
5684
- const entries = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`);
6427
+ const record2 = value;
6428
+ const entries = Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonical(record2[key])}`);
5685
6429
  return `{${entries.join(",")}}`;
5686
6430
  }
5687
6431
  function frameDigest(prev, event) {
@@ -5942,7 +6686,7 @@ function createFsGraphJournal(root) {
5942
6686
 
5943
6687
  // src/telemetry/graphProjection.ts
5944
6688
  function projectGraphEvent(event) {
5945
- const record = {
6689
+ const record2 = {
5946
6690
  v: event.v,
5947
6691
  seq: event.seq,
5948
6692
  runID: event.runID,
@@ -5955,8 +6699,8 @@ function projectGraphEvent(event) {
5955
6699
  ..."specDigest" in event ? { specDigest: event.specDigest } : {},
5956
6700
  ..."artifact" in event ? { artifact: redactedRef(event.artifact) } : {}
5957
6701
  };
5958
- assertReferenceOnly(record);
5959
- return record;
6702
+ assertReferenceOnly(record2);
6703
+ return record2;
5960
6704
  }
5961
6705
 
5962
6706
  // src/telemetry/graphRuntimeHealth.ts
@@ -6047,18 +6791,18 @@ function indexTelemetry(telemetry) {
6047
6791
  const lastError = new Map;
6048
6792
  const lastFailure = new Map;
6049
6793
  const lastArtifact = new Map;
6050
- for (const record of telemetry) {
6051
- if (record.nodeID === undefined) {
6794
+ for (const record2 of telemetry) {
6795
+ if (record2.nodeID === undefined) {
6052
6796
  continue;
6053
6797
  }
6054
- if (record.type === "node.failed" && record.errorClass !== undefined) {
6055
- lastError.set(record.nodeID, record.errorClass);
6798
+ if (record2.type === "node.failed" && record2.errorClass !== undefined) {
6799
+ lastError.set(record2.nodeID, record2.errorClass);
6056
6800
  }
6057
- if (record.type === "node.failed" && record.failureClass !== undefined) {
6058
- lastFailure.set(record.nodeID, record.failureClass);
6801
+ if (record2.type === "node.failed" && record2.failureClass !== undefined) {
6802
+ lastFailure.set(record2.nodeID, record2.failureClass);
6059
6803
  }
6060
- if (record.type === "node.succeeded" && record.artifact !== undefined) {
6061
- lastArtifact.set(record.nodeID, record.artifact);
6804
+ if (record2.type === "node.succeeded" && record2.artifact !== undefined) {
6805
+ lastArtifact.set(record2.nodeID, record2.artifact);
6062
6806
  }
6063
6807
  }
6064
6808
  return { lastError, lastFailure, lastArtifact };
@@ -6249,8 +6993,8 @@ async function runConsoleServe(deps, options) {
6249
6993
  load: () => Promise.resolve(deps.loadConfig(deps.configPath)),
6250
6994
  configPath: deps.configPath
6251
6995
  });
6252
- const auditSink = async (record) => {
6253
- await deps.storage.append(DEFAULT_CONFIG_AUDIT_PATH, `${JSON.stringify(record)}
6996
+ const auditSink = async (record2) => {
6997
+ await deps.storage.append(DEFAULT_CONFIG_AUDIT_PATH, `${JSON.stringify(record2)}
6254
6998
  `);
6255
6999
  };
6256
7000
  const configEditor = createConfigEditor({
@@ -6343,11 +7087,6 @@ async function runConsoleServe(deps, options) {
6343
7087
  // src/cli/graphOperator.ts
6344
7088
  import { join as join8 } from "node:path";
6345
7089
 
6346
- // src/graph/privacyPolicy.ts
6347
- function frontierAllowed(privacyClass, mode) {
6348
- return !(privacyClass === "sensitive" && mode === "forceLocalOnSensitive");
6349
- }
6350
-
6351
7090
  // src/graph/langgraph/reducer.ts
6352
7091
  function reduceAdapterState(current, update) {
6353
7092
  const route = update.route !== undefined ? update.route : current.route;
@@ -6362,21 +7101,22 @@ function reduceAdapterState(current, update) {
6362
7101
 
6363
7102
  // src/graph/langgraph/adapter.ts
6364
7103
  var GRAPH_END = "__end__";
6365
- function resolveNodeRoute(privacyClass, mode, preferred) {
6366
- return preferred === "frontier" && !frontierAllowed(privacyClass, mode) ? "local" : preferred;
7104
+ function resolveNodeRoute(preferredOrPrivacyClass, _legacyMode, legacyPreferred) {
7105
+ return legacyPreferred ?? preferredOrPrivacyClass;
6367
7106
  }
6368
7107
  var routeNode = (state) => ({
6369
- route: resolveNodeRoute(state.input.privacyClass, state.input.mode, state.input.preferredRoute),
7108
+ route: resolveNodeRoute(state.input.preferredRoute),
6370
7109
  visited: ["route"]
6371
7110
  });
6372
7111
  var dispatchNode = async (state, deps) => {
6373
- const route = state.route ?? resolveNodeRoute(state.input.privacyClass, state.input.mode, state.input.preferredRoute);
6374
- const receipt = await deps.executor.execute({
7112
+ const route = state.route ?? resolveNodeRoute(state.input.preferredRoute);
7113
+ const request = {
6375
7114
  nodeID: state.input.nodeID,
6376
7115
  route,
6377
- privacyClass: state.input.privacyClass,
6378
- mode: state.input.mode
6379
- });
7116
+ ...state.input.privacyClass === undefined ? {} : { privacyClass: state.input.privacyClass },
7117
+ ...state.input.mode === undefined ? {} : { mode: state.input.mode }
7118
+ };
7119
+ const receipt = await deps.executor.execute(request);
6380
7120
  return { receipt, visited: ["dispatch"] };
6381
7121
  };
6382
7122
  function buildAdapterDefinition() {
@@ -6441,7 +7181,7 @@ function selectCheckpointer(location) {
6441
7181
  function resolveGraphEngine(config) {
6442
7182
  return config.graph?.engine ?? "builtin";
6443
7183
  }
6444
- function createLangGraphEffects(nodeExecutor) {
7184
+ function createLangGraphEffects(nodeExecutor, defaultRoute = "local") {
6445
7185
  const definition = buildAdapterDefinition();
6446
7186
  return {
6447
7187
  async execute(request) {
@@ -6450,11 +7190,9 @@ function createLangGraphEffects(nodeExecutor) {
6450
7190
  }
6451
7191
  const input = {
6452
7192
  nodeID: request.node.id,
6453
- privacyClass: "internal",
6454
- mode: "forceLocalOnSensitive",
6455
- preferredRoute: "local"
7193
+ preferredRoute: request.routeKind ?? defaultRoute
6456
7194
  };
6457
- const resolvedRoute = resolveNodeRoute(input.privacyClass, input.mode, input.preferredRoute);
7195
+ const resolvedRoute = resolveNodeRoute(input.preferredRoute);
6458
7196
  let state;
6459
7197
  try {
6460
7198
  state = await invokeStateGraph(definition, input, {
@@ -6647,8 +7385,8 @@ function createWorkspaceLease(root, registry = createRunRegistry(root)) {
6647
7385
  return text === undefined ? undefined : parse2(text);
6648
7386
  };
6649
7387
  const grant = async (runID, holder, token) => {
6650
- const record = { runID, holder, token, released: false };
6651
- writeDurable(leasePath(runID), JSON.stringify(record));
7388
+ const record2 = { runID, holder, token, released: false };
7389
+ writeDurable(leasePath(runID), JSON.stringify(record2));
6652
7390
  await registry.markActive(runID);
6653
7391
  return { runID, holder, token };
6654
7392
  };
@@ -7379,19 +8117,19 @@ function createWorktreeAdapter(config) {
7379
8117
  const base = `${canonicalizePath(worktreesDir)}/`;
7380
8118
  return canonicalizePath(worktreePath).startsWith(base);
7381
8119
  };
7382
- const toInfo = (record) => ({
7383
- runID: runIDFromPath(record.path),
7384
- path: record.path,
7385
- head: record.head,
7386
- detached: record.detached,
7387
- locked: record.locked
8120
+ const toInfo = (record2) => ({
8121
+ runID: runIDFromPath(record2.path),
8122
+ path: record2.path,
8123
+ head: record2.head,
8124
+ detached: record2.detached,
8125
+ locked: record2.locked
7388
8126
  });
7389
8127
  const list = async () => {
7390
8128
  const output = await runGit(["worktree", "list", "--porcelain"]);
7391
8129
  if (output.exitCode !== 0) {
7392
8130
  throw new WorktreeError("git-failed", `worktree list: ${output.stderr.trim() || output.exitCode}`);
7393
8131
  }
7394
- return parsePorcelain(output.stdout).filter((record) => isOurs(record.path)).map(toInfo);
8132
+ return parsePorcelain(output.stdout).filter((record2) => isOurs(record2.path)).map(toInfo);
7395
8133
  };
7396
8134
  const findExisting = async (expected) => (await list()).find((info) => samePath(info.path, expected));
7397
8135
  const addWorktree = async (runID, expected) => runGit([
@@ -7931,7 +8669,7 @@ function buildOrchestratorAgent(model, options = {}) {
7931
8669
  ];
7932
8670
  const frontmatter = [
7933
8671
  "---",
7934
- 'description: "openteam multi-agent orchestrator: on every request it checks whether a team exists; if not, it understands the tasks and creates the team of subagents on demand, plans dependencies, parallelizes independent work and keeps the todo list attributed to the owner of each task, with local-first routing."',
8672
+ 'description: "openteam multi-agent orchestrator: on every request it checks whether a team exists; if not, it understands the tasks and creates the team of subagents on demand, plans dependencies, parallelizes independent work and keeps the todo list attributed to the owner of each task, using the configured execution and model policies."',
7935
8673
  "mode: primary",
7936
8674
  `model: ${modelSpec}`,
7937
8675
  "temperature: 0.2",
@@ -8053,10 +8791,9 @@ function buildOrchestratorAgent(model, options = {}) {
8053
8791
  " global one, so an `ask`/`deny` in the subagent reintroduces the",
8054
8792
  " prompts. If YOLO is disabled, use minimal permissions (leave `bash`",
8055
8793
  " in ask mode).",
8056
- " - Leave `model` unset so it inherits the default and openteam routes it",
8057
- " local-first, or pin a local model for mechanical/bulk roles. Reserve",
8058
- " frontier only for architecture, security or ambiguous debugging:",
8059
- " openteam already routes cheapest-capable automatically.",
8794
+ " - Leave `model` unset in the agent file. Configure its execution domain,",
8795
+ " exact model or `auto`, and ordered fallbacks under `orchestrator.roles`",
8796
+ " in `.opencode/openteam.json`; the runtime policy is authoritative.",
8060
8797
  " - A focused, actionable role prompt.",
8061
8798
  "4. Register the cast with the `openteam-register-cast` tool and **hand out",
8062
8799
  " the tasks** to the newly created team with the `task` tool.",
@@ -8070,22 +8807,20 @@ function buildOrchestratorAgent(model, options = {}) {
8070
8807
  "",
8071
8808
  "- **scribe** — the team's silent memory. Records decisions and",
8072
8809
  " learnings in a shared log (`.opencode/openteam/decisions.md`) without",
8073
- " executing code changes. **Local** model; read-only permissions plus",
8074
- " editing of that log. **Parseable format** for the console: one decision",
8810
+ " executing code changes. Use read-only permissions plus editing of that",
8811
+ " log. **Parseable format** for the console: one decision",
8075
8812
  " per line as a Markdown list item, already **redacted** (no prompts or",
8076
- " sensitive data):",
8813
+ " credentials):",
8077
8814
  " `- YYYY-MM-DD [agent] Short summary of the decision #tag1 #tag2`",
8078
8815
  " The date, the `[agent]` and the `#tags` are optional; the summary is",
8079
8816
  " required.",
8080
8817
  "- **ralph** — automation and triage. Monitors pending work",
8081
8818
  " (issues/tasks), prioritizes it, coordinates execution by delegating to the",
8082
8819
  " specialists and **escalates to the human** on blockers, risks or approvals.",
8083
- "- **guardian** — security and privacy. **Detects and masks sensitive",
8084
- " data** (PII, secrets, API keys, tokens, credentials) before it",
8085
- " leaves the repo or goes to frontier, and does security review. Runs",
8086
- " **always locally** and respects `privacyMode`/`forceLocalOnSensitive`: never",
8087
- " lets sensitive data through to frontier. Reviews the inputs and outputs of the",
8088
- " other agents when they touch sensitive data.",
8820
+ "- **guardian** — an assignable security reviewer. Use it when a task calls",
8821
+ " for threat analysis, exploitability checks or security-focused review.",
8822
+ " It is not an invisible pre-dispatch gate and does not inspect or rewrite",
8823
+ " every other agent's input automatically.",
8089
8824
  "",
8090
8825
  "## Work planning and parallelization",
8091
8826
  "",
@@ -8105,10 +8840,9 @@ function buildOrchestratorAgent(model, options = {}) {
8105
8840
  "4. **Do not parallelize work that collides** on the same files or",
8106
8841
  " state: those are implicit dependencies; split it into separate waves to",
8107
8842
  " avoid write conflicts.",
8108
- "5. Distribute by **capability**: mechanical/bulk roles on local, and reserve",
8109
- " frontier for the hard parts; parallelism does not change openteam's routing.",
8110
- "6. The `guardian` is still the pre-frontier gate **even** in",
8111
- " parallel: nothing sensitive leaves without passing through it, even with several waves.",
8843
+ "5. Distribute by **capability** and let each configured execution/model",
8844
+ " policy select local, frontier or mixed candidates. Parallelism never",
8845
+ " broadens a role beyond its configured execution domain.",
8112
8846
  "",
8113
8847
  "Briefly explain your plan to the user (what runs in parallel and what in series, and",
8114
8848
  "why) before or while you launch the tasks.",
@@ -8202,8 +8936,8 @@ function buildOrchestratorAgent(model, options = {}) {
8202
8936
  "- Create subagents **exactly when the work calls for it**, never “just in case”.",
8203
8937
  ` Review \`.opencode/agent/\` and the \`${OPENTEAM_ROSTER_FILENAME}\`, and reuse the`,
8204
8938
  " existing ones before creating a new one.",
8205
- "- Respect openteam's routing and privacy; the `guardian` is the last",
8206
- " line before sending anything sensitive to frontier.",
8939
+ "- Respect openteam's configured execution domains, exact pins and ordered",
8940
+ " fallback chains. Assign `guardian` explicitly when security review is needed.",
8207
8941
  "- Summarize for the user which universe you chose, which team you created and why.",
8208
8942
  "",
8209
8943
  "## Limits",
@@ -8272,8 +9006,8 @@ function buildOpenteamCommands() {
8272
9006
  return [
8273
9007
  file("openteam", buildOpenteamCommand()),
8274
9008
  file("openteam-baseline", buildBaselineCommand()),
8275
- file("openteam-doctor", renderCommand("openteam · diagnostics for local runtimes and configuration", fixedActionBody("doctor"))),
8276
- file("openteam-agents", renderCommand("openteam · list the agents and the LLM (local/frontier + subscription) for each one", fixedActionBody("agents"))),
9009
+ file("openteam-doctor", renderCommand("openteam · diagnostics for runtimes, execution policies, and configuration", fixedActionBody("doctor"))),
9010
+ file("openteam-agents", renderCommand("openteam · list agents, model identity, execution policy, and subscription classification", fixedActionBody("agents"))),
8277
9011
  file("openteam-console", renderCommand("openteam · status and URL of the multi-session web Console", [
8278
9012
  "> ℹ️ The Console interface may still evolve between releases.",
8279
9013
  "",
@@ -8490,7 +9224,7 @@ var CURATED_FRONTIER_PROFILES = [
8490
9224
  ];
8491
9225
 
8492
9226
  // src/capabilities/normalize.ts
8493
- function isRecord2(value) {
9227
+ function isRecord3(value) {
8494
9228
  return typeof value === "object" && value !== null && !Array.isArray(value);
8495
9229
  }
8496
9230
  function stringValue(value) {
@@ -8511,7 +9245,7 @@ function boolValue(value) {
8511
9245
  }
8512
9246
  function nestedRecord(value, key) {
8513
9247
  const nested = value[key];
8514
- return isRecord2(nested) ? nested : undefined;
9248
+ return isRecord3(nested) ? nested : undefined;
8515
9249
  }
8516
9250
  function nestedBoolean(value, firstKey, secondKey) {
8517
9251
  const nested = nestedRecord(value, firstKey);
@@ -8524,7 +9258,7 @@ function includesImage(value) {
8524
9258
  if (Array.isArray(value)) {
8525
9259
  return value.some(includesImage);
8526
9260
  }
8527
- if (isRecord2(value)) {
9261
+ if (isRecord3(value)) {
8528
9262
  return Object.values(value).some(includesImage);
8529
9263
  }
8530
9264
  return false;
@@ -8543,7 +9277,7 @@ function tierFromModelName(modelID, reasoning) {
8543
9277
  if (typeof reasoning === "number") {
8544
9278
  return Math.max(0, Math.min(5, Math.trunc(reasoning)));
8545
9279
  }
8546
- if (reasoning === true || isRecord2(reasoning)) {
9280
+ if (reasoning === true || isRecord3(reasoning)) {
8547
9281
  return normalized.includes("flash") || normalized.includes("mini") ? 3 : 4;
8548
9282
  }
8549
9283
  if (normalized.includes("opus") || normalized.includes("codex")) {
@@ -8573,31 +9307,44 @@ function codeTierFromModelName(modelID) {
8573
9307
  function availability(value) {
8574
9308
  return value === "degraded" || value === "unavailable" ? value : "available";
8575
9309
  }
9310
+ function modelAvailability(availabilityValue, status) {
9311
+ if (availabilityValue === "degraded" || status === "deprecated") {
9312
+ return "degraded";
9313
+ }
9314
+ return availability(availabilityValue);
9315
+ }
8576
9316
  function modelEntriesFromProvider(providerID, provider) {
8577
9317
  const models = provider.models;
8578
9318
  if (Array.isArray(models)) {
8579
- return models.filter(isRecord2).map((model) => ({
8580
- providerID,
9319
+ return models.filter(isRecord3).map((model) => ({
9320
+ providerID: stringValue(model.providerID) ?? providerID,
8581
9321
  modelID: stringValue(model.id) ?? stringValue(model.modelID) ?? "",
8582
9322
  value: model
8583
9323
  })).filter((entry) => entry.modelID.length > 0);
8584
9324
  }
8585
- if (isRecord2(models)) {
8586
- return Object.entries(models).filter(([, model]) => isRecord2(model)).map(([modelID, model]) => ({
8587
- providerID,
8588
- modelID,
8589
- value: model
8590
- }));
9325
+ if (isRecord3(models)) {
9326
+ return Object.entries(models).flatMap(([modelID, model]) => {
9327
+ if (!isRecord3(model)) {
9328
+ return [];
9329
+ }
9330
+ return [
9331
+ {
9332
+ providerID: stringValue(model.providerID) ?? providerID,
9333
+ modelID: stringValue(model.id) ?? stringValue(model.modelID) ?? modelID,
9334
+ value: model
9335
+ }
9336
+ ];
9337
+ });
8591
9338
  }
8592
9339
  return [];
8593
9340
  }
8594
9341
  function catalogEntries(raw) {
8595
- if (!isRecord2(raw)) {
9342
+ if (!isRecord3(raw)) {
8596
9343
  return [];
8597
9344
  }
8598
- const providers = isRecord2(raw.providers) ? raw.providers : raw;
9345
+ const providers = isRecord3(raw.providers) ? raw.providers : raw;
8599
9346
  return Object.entries(providers).flatMap(([providerKey, provider]) => {
8600
- if (!isRecord2(provider)) {
9347
+ if (!isRecord3(provider)) {
8601
9348
  return [];
8602
9349
  }
8603
9350
  const providerID = stringValue(provider.id) ?? stringValue(provider.providerID) ?? providerKey;
@@ -8606,6 +9353,9 @@ function catalogEntries(raw) {
8606
9353
  }
8607
9354
  function normalizeEntry(entry) {
8608
9355
  const limit = nestedRecord(entry.value, "limit");
9356
+ const capabilities = nestedRecord(entry.value, "capabilities");
9357
+ const capabilityName = stringValue(entry.value.name) ?? entry.modelID;
9358
+ const inputCapabilities = capabilities === undefined ? undefined : nestedRecord(capabilities, "input");
8609
9359
  const contextWindow = numberValue(entry.value.contextWindow) ?? numberValue(entry.value.context_window) ?? (limit === undefined ? undefined : numberValue(limit.context));
8610
9360
  const maxOutputTokens = numberValue(entry.value.maxOutputTokens) ?? numberValue(entry.value.max_output_tokens) ?? (limit === undefined ? undefined : numberValue(limit.output));
8611
9361
  const costPer1M = costPair(entry.value);
@@ -8617,12 +9367,12 @@ function normalizeEntry(entry) {
8617
9367
  kind: "frontier",
8618
9368
  contextWindow,
8619
9369
  maxOutputTokens,
8620
- supportsToolCalling: boolValue(entry.value.tool_call) || boolValue(entry.value.toolCall) || nestedBoolean(entry.value, "features", "tool_call"),
8621
- supportsVision: boolValue(entry.value.vision) || includesImage(entry.value.modalities) || includesImage(entry.value.input_modalities),
8622
- reasoningTier: tierFromModelName(entry.modelID, entry.value.reasoning),
8623
- codeQualityTier: codeTierFromModelName(entry.modelID),
9370
+ supportsToolCalling: boolValue(entry.value.tool_call) || boolValue(entry.value.toolCall) || boolValue(capabilities?.toolcall) || nestedBoolean(entry.value, "features", "tool_call"),
9371
+ supportsVision: boolValue(entry.value.vision) || boolValue(inputCapabilities?.image) || includesImage(entry.value.modalities) || includesImage(entry.value.input_modalities),
9372
+ reasoningTier: tierFromModelName(capabilityName, entry.value.reasoning ?? capabilities?.reasoning),
9373
+ codeQualityTier: codeTierFromModelName(capabilityName),
8624
9374
  costPer1M,
8625
- availability: availability(entry.value.availability)
9375
+ availability: modelAvailability(entry.value.availability, entry.value.status)
8626
9376
  };
8627
9377
  const parsed = ModelCapabilityProfileSchema.safeParse(profile);
8628
9378
  return parsed.success ? parsed.data : undefined;
@@ -9459,123 +10209,6 @@ function isGateApplicable(mode) {
9459
10209
  return mode === "active";
9460
10210
  }
9461
10211
 
9462
- // src/messages/commands.ts
9463
- var clearCacheMessages = {
9464
- header: "openteam clear-cache — frozen plugin cache entries:",
9465
- columns: {
9466
- specDir: "spec dir",
9467
- pinned: "spec-pinned",
9468
- installed: "installed",
9469
- mtime: "mtime"
9470
- },
9471
- reparseSkipSuffix: " [SKIP — reparse point]",
9472
- deletedLabel: "deleted.",
9473
- lockedLabel: (message) => `[LOCKED] ${message}`,
9474
- pathOutsideWarning: (specDir, absolutePath) => ` ⚠ ${specDir}: path outside cacheRoot (${absolutePath}), skipped.`,
9475
- processed: (count) => `${count} entry(ies) processed.`,
9476
- found: (count) => `${count} entry(ies) found. Use --delete to remove them.`
9477
- };
9478
- var baselineMessages = {
9479
- effectiveAuto: "cheapest-capable (auto)",
9480
- pinnedSuffix: (ref) => `${ref} (pinned)`,
9481
- summary: (params) => [
9482
- "openteam baseline:",
9483
- ` mode: ${params.mode}`,
9484
- ` pinned: ${params.pinned}`,
9485
- ` hardDefault: ${params.hardDefault}`,
9486
- ` effective: ${params.effective}`
9487
- ],
9488
- invalidModel: (input) => `Invalid model "${input}". Use the provider/model format, e.g. anthropic/claude-sonnet-4-5.`,
9489
- pinnedTo: (ref) => `Baseline pinned to ${ref} (pinned mode).`,
9490
- autoMode: "Baseline set to auto mode (cheapest-capable)."
9491
- };
9492
- var localMessages = {
9493
- help: {
9494
- status: " openteam local status Show the routing mode (local-first / local-only / frontier-only)",
9495
- off: " openteam local off Use frontier models only (no local runtime)",
9496
- only: " openteam local only Use local models only (never frontier)",
9497
- on: " openteam local on Re-enable local-first routing"
9498
- },
9499
- noRuntimes: "none",
9500
- modeFrontierOnly: "frontier-only (frontierOnly)",
9501
- modeLocalOnly: "local-only (localOnly)",
9502
- modeLocalFirst: "local-first",
9503
- summary: (params) => [
9504
- "openteam local:",
9505
- ` mode: ${params.mode}`,
9506
- ` frontierOnly: ${params.frontierOnly}`,
9507
- ` localOnly: ${params.localOnly}`,
9508
- ` privacy: ${params.privacy}`,
9509
- ` runtimes: ${params.runtimes}`
9510
- ],
9511
- frontierOnlyNoChange: "No change: already in frontier-only mode.",
9512
- privacyAdjustedNote: " Privacy adjusted to 'consentBeforeFrontier' (forceLocalOnSensitive requires a local runtime).",
9513
- frontierOnlyEnabled: "Frontier-only mode enabled (frontierOnly=true, localOnly=false).",
9514
- localOnlyNoRuntimes: "Cannot enable local-only mode: no enabled local runtime is configured. Run 'openteam setup' or enable a local runtime in .opencode/openteam.json first.",
9515
- localOnlyNoChange: "No change: already in local-only mode.",
9516
- localOnlyEnabled: "Local-only mode enabled (localOnly=true, frontierOnly=false).",
9517
- localFirstNoChange: "No change: local-first routing is already active.",
9518
- localFirstReEnabled: "Local-first routing re-enabled (frontierOnly=false, localOnly=false). Make sure a local runtime is reachable.",
9519
- unknownSubcommand: (subcommand, help) => `Unknown local subcommand: ${subcommand}
9520
-
9521
- ${help}`
9522
- };
9523
- var frontierCatalogMessages = {
9524
- free: "free",
9525
- costPerMillion: (inputUSD, outputUSD) => `$${inputUSD}/$${outputUSD} per 1M`
9526
- };
9527
- var migrateMessages = {
9528
- header: "openteam migrate:",
9529
- nothingToMigrate: " nothing to migrate — the P15b layout is already in place.",
9530
- counts: (params) => [
9531
- ` moved: ${params.moved} file(s)`,
9532
- ` deduped: ${params.deduped} file(s) (destination already identical)`,
9533
- ` conflicts: ${params.conflicts} file(s)`
9534
- ],
9535
- relocatedHeader: " relocated:",
9536
- relocatedEntry: (from, to, deduped) => ` · ${from} → ${to}${deduped ? " (deduped)" : ""}`,
9537
- conflictsHeader: " ⚠ left in place (destination exists with different content — no data lost):",
9538
- conflictEntry: (from, to) => ` · ${from} → ${to}`,
9539
- manualWorktreesHeader: " ⚠ legacy git worktree(s) need a manual move (run `git worktree move`):",
9540
- manualWorktreeEntry: (label, from, to) => ` · ${label}: ${from} → ${to}`,
9541
- success: " ✓ machine-local runtime output relocated under .opencode/openteam-local/."
9542
- };
9543
- var rolesInitMessages = {
9544
- header: "openteam roles init:",
9545
- title: "openteam roles init",
9546
- noRoster: (path) => ` ✗ no roster at ${path}.`,
9547
- noRosterRemedy: " Cast the team first: run `openteam setup`, then ask the orchestrator to register the cast.",
9548
- unparseableRoster: (path, error) => ` ✗ ${path} is present but unparseable: ${error}`,
9549
- unparseableRosterRemedy: " Run `openteam doctor` for the remedy, then re-run this command.",
9550
- allProfiled: (count) => ` ✓ ${count} roster role(s) checked; no unprofiled roles — every role resolves a routing profile.`,
9551
- whyTitle: "Why you are being asked",
9552
- why: (count) => [
9553
- `${count} roster role(s) resolve no routing profile.`,
9554
- "Until a profile exists they are frontier-eligible: their work can escalate",
9555
- "to a frontier model. A 'local only' note in the roster prose does not",
9556
- "prevent this — prose outside the JSON fence is never read."
9557
- ].join(`
9558
- `),
9559
- policyQuestion: (roleID, agentName) => `Routing policy for "${roleID}" (${agentName})`,
9560
- localOnlyLabel: (roleID, agentName) => `Local only — ${roleID} (${agentName}) never leaves this machine`,
9561
- localOnlyHint: "sets requiresLocalRuntime; the role fails closed when no local runtime is reachable",
9562
- frontierOkLabel: "Local first, frontier allowed",
9563
- frontierOkHint: "records today's implicit behaviour: cheapest-capable frontier when the task warrants it",
9564
- skipLabel: "Skip for now",
9565
- skipHint: "leaves the role unprofiled, so it stays frontier-eligible and doctor keeps reporting it",
9566
- runtimeQuestion: (roleID) => `Preferred local runtime(s) for "${roleID}" (in failover order)`,
9567
- runtimeHint: "machine-local — written to the git-ignored overlay",
9568
- cancelled: (reason) => ` ✗ cancelled (${reason}); nothing was written.`,
9569
- invalidResult: (reason) => ` ✗ refusing to write: the resulting config is invalid — ${reason}`,
9570
- nothingWritten: (skipped) => ` · skipped ${skipped}; nothing written.`,
9571
- nothingToWriteOutro: "Nothing to write.",
9572
- pinnedLocal: (roleIDs, path) => ` ✓ pinned local-only: ${roleIDs} → ${path} (tracked, travels with the roster)`,
9573
- frontierAllowed: (roleIDs, path) => ` ✓ frontier allowed: ${roleIDs} → ${path} (model choice stays "auto": cheapest-capable per task)`,
9574
- runtimeBinding: (path) => ` ✓ runtime binding: ${path} (git-ignored, machine-local)`,
9575
- skippedSummary: (roleIDs) => ` · skipped: ${roleIDs} — still frontier-eligible; re-run this command to revisit.`,
9576
- outro: "Role policy updated."
9577
- };
9578
-
9579
10212
  // src/orchestrator/worktreeReconciler.ts
9580
10213
  function planOne(wt) {
9581
10214
  const base = { runID: wt.runID, path: wt.path };
@@ -10131,8 +10764,7 @@ function rosterHealthSection(audit, loadError, rosterRoleCount, rosterMissing) {
10131
10764
  if (unprofiled.length > 0) {
10132
10765
  const roleIDs = unprofiled.map((entry) => entry.roleID).join(", ");
10133
10766
  lines.push(` · unprofiled roleID(s): ${roleIDs}`);
10134
- lines.push(" routing: no profile means frontier-eligible — work for these roles can escalate to a frontier model. A 'local only' note in the roster prose does NOT prevent this: prose outside the JSON fence is never read.");
10135
- lines.push(' remedy: run `openteam roles init` to compose a profile interactively, or add one by hand under orchestrator.roles in .opencode/openteam.json with "requiresLocalRuntime": true to pin the role local; for curated routing, change roleID to a TEAM_ROLES key instead.');
10767
+ lines.push(executionPolicyMessages.unprofiledRoleRouting, executionPolicyMessages.unprofiledRoleRemedy);
10136
10768
  }
10137
10769
  return lines;
10138
10770
  }
@@ -10174,10 +10806,7 @@ function agentModelsSection(diagnostics, searchedPaths, localOnly) {
10174
10806
  }
10175
10807
  const localOnlyGap = localOnly ? diagnostics.find((d) => d.mode === "primary" && d.resolution?.status === "unresolvable" && d.resolution.reason === "provider-not-configured") : undefined;
10176
10808
  if (localOnlyGap?.model !== undefined) {
10177
- lines.push(` ✗ local-only mode, but the primary agent '${localOnlyGap.name}' points at provider '${localOnlyGap.model.providerID}', which opencode.json does not configure.`);
10178
- lines.push(" cause: `openteam setup` recorded the frontier baseline placeholder as this agent's `model:`. That value is correctly inert as baseline.hardDefault while router.localOnly is true, but an agent's own `model:` is load-bearing, so opencode cannot resolve the coordinator against your local-only providers.");
10179
- lines.push(" consequence: opencode cannot start the openteam coordinator, so it reads no roster and splits no work — even though every subagent resolves on your local runtime.");
10180
- lines.push(` remedy: re-run \`openteam setup\` and keep local-only mode to regenerate ${localOnlyGap.sourceFile} with your local thinking model, then re-run \`openteam doctor\`.`);
10809
+ lines.push(executionPolicyMessages.primaryLocalMismatch(localOnlyGap.name, localOnlyGap.model.providerID), executionPolicyMessages.primaryLocalMismatchCause, executionPolicyMessages.primaryLocalMismatchConsequence, executionPolicyMessages.primaryLocalMismatchRemedy(localOnlyGap.sourceFile));
10181
10810
  }
10182
10811
  lines.push(" note: 'declared' = model set in .opencode/agent/<name>.md frontmatter; 'inherited' = opencode.json default.");
10183
10812
  lines.push(" note: static checks are offline and deterministic; live checks need a reachable local runtime and can be inconclusive.");
@@ -10186,10 +10815,16 @@ function agentModelsSection(diagnostics, searchedPaths, localOnly) {
10186
10815
  function renderDoctor(input) {
10187
10816
  const enabledRuntimes = input.config.local.runtimes.filter((r) => r.enabled);
10188
10817
  const reachable = input.snapshots.filter((s) => s.reachable).length;
10818
+ const configRecord = input.config;
10819
+ const routerRecord = typeof configRecord.router === "object" && configRecord.router !== null && !Array.isArray(configRecord.router) ? configRecord.router : {};
10820
+ const executionMode = typeof routerRecord.executionMode === "string" ? routerRecord.executionMode : routerRecord.localOnly === true ? "local" : routerRecord.frontierOnly === true ? "frontier" : "mixed";
10821
+ const orchestratorRecord = typeof configRecord.orchestrator === "object" && configRecord.orchestrator !== null && !Array.isArray(configRecord.orchestrator) ? configRecord.orchestrator : {};
10822
+ const maxRetries = typeof orchestratorRecord.maxRetries === "number" ? orchestratorRecord.maxRetries : 2;
10823
+ const maxModelsPerNode = typeof orchestratorRecord.maxModelsPerNode === "number" ? orchestratorRecord.maxModelsPerNode : 2;
10189
10824
  const lines = [
10190
10825
  "openteam doctor:",
10191
10826
  ` baseline: ${input.config.baseline.mode}` + (input.config.baseline.mode === "pinned" && input.config.baseline.pinnedModel !== null ? ` (${input.config.baseline.pinnedModel.providerID}/${input.config.baseline.pinnedModel.modelID})` : ""),
10192
- ` privacy: ${input.config.privacyMode}`,
10827
+ executionPolicyMessages.globalMode(executionMode),
10193
10828
  ` local runtimes (${reachable}/${enabledRuntimes.length} reachable):`
10194
10829
  ];
10195
10830
  if (input.snapshots.length === 0) {
@@ -10217,6 +10852,11 @@ function renderDoctor(input) {
10217
10852
  lines.push(` ⚠ ${snapshot.id} is reached over the network: its ${cap}-slot cap is enforced per openteam process, so`, " two processes at once (an opencode session plus 'openteam console') can together exceed it.");
10218
10853
  }
10219
10854
  lines.push(` telemetry: ${input.telemetryPath} — ${input.telemetryRecords} record(s)`);
10855
+ lines.push(executionPolicyMessages.section);
10856
+ for (const policy of executionPolicySummaries(input.config)) {
10857
+ lines.push(executionPolicyMessages.line(policy));
10858
+ }
10859
+ lines.push(executionPolicyMessages.retryLimits(maxRetries, maxModelsPerNode));
10220
10860
  if (input.queueWait !== undefined) {
10221
10861
  lines.push(...queueWaitSection(input.queueWait));
10222
10862
  }
@@ -10244,8 +10884,10 @@ function renderDoctor(input) {
10244
10884
  lines.push(" console:");
10245
10885
  lines.push(" command: openteam console");
10246
10886
  lines.push(" launches: the multi-session web Console (status and URL)");
10247
- if (reachable === 0) {
10248
- lines.push(" ⚠ No local runtime reachable: all traffic will go to frontier.");
10887
+ if (reachable === 0 && executionMode === "mixed") {
10888
+ lines.push(executionPolicyMessages.noLocalMixed);
10889
+ } else if (reachable === 0 && executionMode === "local") {
10890
+ lines.push(executionPolicyMessages.noLocalBlocked);
10249
10891
  }
10250
10892
  if (input.graphHealth !== undefined) {
10251
10893
  const h = input.graphHealth;
@@ -10267,7 +10909,7 @@ function renderDoctor(input) {
10267
10909
  lines.push(" Run `openteam clear-cache --delete` to clean up.");
10268
10910
  }
10269
10911
  if (input.agentModels !== undefined) {
10270
- lines.push(...agentModelsSection(input.agentModels, input.opencodeConfigPaths ?? [], input.config.router.localOnly));
10912
+ lines.push(...agentModelsSection(input.agentModels, input.opencodeConfigPaths ?? [], executionMode === "local"));
10271
10913
  }
10272
10914
  if (input.opencodeConfigError !== undefined) {
10273
10915
  lines.push(` ⚠ opencode config parse error: ${input.opencodeConfigError}`);
@@ -10392,6 +11034,47 @@ function renderWorktreeReconciliation(report) {
10392
11034
  function hasEnabledLocalRuntime(config) {
10393
11035
  return config.local.runtimes.some((runtime) => runtime.enabled);
10394
11036
  }
11037
+ function executionMode(config) {
11038
+ const router = config.router;
11039
+ if (router.executionMode !== undefined) {
11040
+ return router.executionMode;
11041
+ }
11042
+ if (router.localOnly === true) {
11043
+ return "local";
11044
+ }
11045
+ if (router.frontierOnly === true) {
11046
+ return "frontier";
11047
+ }
11048
+ return "mixed";
11049
+ }
11050
+ function withExecutionMode(config, mode) {
11051
+ const {
11052
+ privacyMode: _legacyPrivacyMode,
11053
+ router: currentRouter,
11054
+ ...configWithoutLegacyPrivacy
11055
+ } = config;
11056
+ const {
11057
+ frontierOnly: _legacyFrontierOnly,
11058
+ localOnly: _legacyLocalOnly,
11059
+ ...currentRouterWithoutLegacyModes
11060
+ } = currentRouter;
11061
+ const candidate = {
11062
+ ...configWithoutLegacyPrivacy,
11063
+ router: {
11064
+ ...currentRouterWithoutLegacyModes,
11065
+ executionMode: mode
11066
+ }
11067
+ };
11068
+ const parsed = OpenTeamConfigSchema.safeParse(candidate);
11069
+ if (!parsed.success) {
11070
+ throw new Error(formatConfigError(parsed.error));
11071
+ }
11072
+ assertEffectiveConfigModelDomains(parsed.data);
11073
+ return parsed.data;
11074
+ }
11075
+ function requiresExecutionMigration(config) {
11076
+ return config.router.localOnly !== undefined || config.router.frontierOnly !== undefined || config.privacyMode !== undefined;
11077
+ }
10395
11078
  function enabledRuntimesSummary(config) {
10396
11079
  const enabled = config.local.runtimes.filter((runtime) => runtime.enabled);
10397
11080
  if (enabled.length === 0) {
@@ -10404,59 +11087,44 @@ function enabledRuntimesSummary(config) {
10404
11087
  }).join(", ");
10405
11088
  }
10406
11089
  function showLocal(config) {
10407
- const mode = config.router.localOnly ? localMessages.modeLocalOnly : config.router.frontierOnly ? localMessages.modeFrontierOnly : localMessages.modeLocalFirst;
11090
+ const mode = executionMode(config);
11091
+ const modeLabel = mode === "local" ? localMessages.modeLocal : mode === "frontier" ? localMessages.modeFrontier : localMessages.modeMixed;
10408
11092
  const lines = localMessages.summary({
10409
- mode,
10410
- frontierOnly: config.router.frontierOnly,
10411
- localOnly: config.router.localOnly,
10412
- privacy: config.privacyMode,
11093
+ mode: modeLabel,
10413
11094
  runtimes: enabledRuntimesSummary(config)
10414
11095
  });
10415
11096
  return { message: lines.join(`
10416
11097
  `) };
10417
11098
  }
10418
11099
  function setFrontierOnly(config) {
10419
- if (config.router.frontierOnly && !config.router.localOnly) {
11100
+ if (executionMode(config) === "frontier" && !requiresExecutionMigration(config)) {
11101
+ assertEffectiveConfigModelDomains(config);
10420
11102
  return { message: localMessages.frontierOnlyNoChange };
10421
11103
  }
10422
- const flipsPrivacy = config.privacyMode === "forceLocalOnSensitive";
10423
- const next = {
10424
- ...config,
10425
- router: { ...config.router, frontierOnly: true, localOnly: false },
10426
- privacyMode: flipsPrivacy ? "consentBeforeFrontier" : config.privacyMode
10427
- };
10428
- const privacyNote = flipsPrivacy ? localMessages.privacyAdjustedNote : "";
10429
11104
  return {
10430
- config: next,
10431
- message: `${localMessages.frontierOnlyEnabled}${privacyNote}`
11105
+ config: withExecutionMode(config, "frontier"),
11106
+ message: localMessages.frontierOnlyEnabled
10432
11107
  };
10433
11108
  }
10434
11109
  function setLocalOnly(config) {
10435
11110
  if (!hasEnabledLocalRuntime(config)) {
10436
11111
  return { message: localMessages.localOnlyNoRuntimes };
10437
11112
  }
10438
- if (config.router.localOnly && !config.router.frontierOnly) {
11113
+ if (executionMode(config) === "local" && !requiresExecutionMigration(config)) {
11114
+ assertEffectiveConfigModelDomains(config);
10439
11115
  return { message: localMessages.localOnlyNoChange };
10440
11116
  }
10441
- const next = {
10442
- ...config,
10443
- router: { ...config.router, frontierOnly: false, localOnly: true }
10444
- };
10445
11117
  return {
10446
- config: next,
11118
+ config: withExecutionMode(config, "local"),
10447
11119
  message: localMessages.localOnlyEnabled
10448
11120
  };
10449
11121
  }
10450
11122
  function setLocalFirst(config) {
10451
- if (!config.router.frontierOnly && !config.router.localOnly) {
11123
+ if (executionMode(config) === "mixed" && !requiresExecutionMigration(config)) {
10452
11124
  return { message: localMessages.localFirstNoChange };
10453
11125
  }
10454
- const next = {
10455
- ...config,
10456
- router: { ...config.router, frontierOnly: false, localOnly: false }
10457
- };
10458
11126
  return {
10459
- config: next,
11127
+ config: withExecutionMode(config, "mixed"),
10460
11128
  message: localMessages.localFirstReEnabled
10461
11129
  };
10462
11130
  }
@@ -10557,7 +11225,7 @@ function stripJsoncComments(src) {
10557
11225
  }
10558
11226
  return out.join("");
10559
11227
  }
10560
- function isRecord3(v) {
11228
+ function isRecord4(v) {
10561
11229
  return typeof v === "object" && v !== null && !Array.isArray(v);
10562
11230
  }
10563
11231
  var OPENCODE_CONFIG_CANDIDATES = [
@@ -10568,7 +11236,7 @@ function mergeOpencodeConfigs(base, override) {
10568
11236
  const result = { ...base };
10569
11237
  for (const [key, value] of Object.entries(override)) {
10570
11238
  const baseVal = result[key];
10571
- if (isRecord3(baseVal) && isRecord3(value)) {
11239
+ if (isRecord4(baseVal) && isRecord4(value)) {
10572
11240
  result[key] = mergeOpencodeConfigs(baseVal, value);
10573
11241
  } else {
10574
11242
  result[key] = value;
@@ -10769,17 +11437,17 @@ var GENERATED_OPENTEAM_STATE_GROUPS = [
10769
11437
  var OPENCODE_DIR = ".opencode";
10770
11438
  var AGENT_DIR = `${OPENCODE_DIR}/agent`;
10771
11439
  var ROLE_AGENT_DESCRIPTIONS = {
10772
- architect: "openteam architect subagent: high-level design, cross-cutting refactors and hard technical decisions; escalates to a frontier model when the task warrants it.",
11440
+ architect: "openteam architect subagent: high-level design, cross-cutting refactors and hard technical decisions.",
10773
11441
  integration: "openteam integration subagent: wires components together, owns end-to-end flows and cross-module glue.",
10774
- "local-runtime": "openteam local-runtime subagent: local model operation and runtime health; prefers local execution.",
11442
+ "local-runtime": "openteam local-runtime subagent: local model operation, provider binding and runtime health.",
10775
11443
  "routing-cost": "openteam routing-cost subagent: cost-aware routing analysis and token/budget optimisation.",
10776
- scribe: "openteam scribe subagent: the team's silent memory. Records decisions and learnings in .opencode/openteam/decisions.md without changing code. Runs locally only. When a conversation yields durable, reusable knowledge — such as facts, entities, and the typed relations between them (e.g. a decision that affects a component) — also call the `openteam-memory` tool with those messages to record it in the team's semantic graph; it never stores the raw conversation, and a reply that memory is disabled or that nothing was recorded is expected, so do not retry.",
10777
- ralph: "openteam ralph subagent: automation and triage driver. Monitors pending work, prioritises it and coordinates execution, escalating blockers to the human. Runs locally only.",
10778
- guardian: "openteam guardian subagent: security and privacy gate. Detects and masks sensitive data before it leaves the repo or reaches a frontier model. Runs locally only.",
11444
+ scribe: "openteam scribe subagent: the team's silent memory. Records decisions and learnings in .opencode/openteam/decisions.md without changing code. When a conversation yields durable, reusable knowledge — such as facts, entities, and the typed relations between them (e.g. a decision that affects a component) — also call the `openteam-memory` tool with those messages to record it in the team's semantic graph; it never stores the raw conversation, and a reply that memory is disabled or that nothing was recorded is expected, so do not retry.",
11445
+ ralph: "openteam ralph subagent: automation and triage driver. Monitors pending work, prioritises it and coordinates execution, escalating blockers to the human.",
11446
+ guardian: "openteam guardian subagent: an assignable security reviewer for threat analysis, exploitability checks and security-focused review.",
10779
11447
  tester: "openteam tester subagent: writes and runs tests, reproduces bugs and validates fixes.",
10780
11448
  reviewer: "openteam reviewer subagent: reviews diffs for correctness, security and quality."
10781
11449
  };
10782
- function buildRoleAgent(agentName, description, localOnly) {
11450
+ function buildRoleAgent(agentName, description) {
10783
11451
  const frontmatter = [
10784
11452
  "---",
10785
11453
  `description: ${JSON.stringify(description)}`,
@@ -10793,7 +11461,7 @@ function buildRoleAgent(agentName, description, localOnly) {
10793
11461
  "",
10794
11462
  description,
10795
11463
  "",
10796
- localOnly ? "You run on a local runtime only; openteam refuses to dispatch you when no local runtime is reachable, so sensitive work never reaches a frontier model." : "openteam routes your model per request (local-first, cheapest-capable frontier only when the task warrants it); leave `model` unset to inherit that routing.",
11464
+ "Your execution domain, selected model and ordered fallbacks come from the matching policy in `.opencode/openteam.json`; leave `model` unset here so the runtime policy remains authoritative.",
10797
11465
  "",
10798
11466
  "Stay focused on your role. Keep changes surgical and report what you did."
10799
11467
  ].join(`
@@ -10803,19 +11471,19 @@ function buildRoleAgent(agentName, description, localOnly) {
10803
11471
  ${body}
10804
11472
  `;
10805
11473
  }
10806
- function buildRoleAgentFiles() {
10807
- const seen = new Map;
11474
+ function buildRoleAgentFiles(_config) {
11475
+ const seen = new Set;
10808
11476
  for (const role of Object.values(TEAM_ROLES)) {
10809
11477
  const agentName = role.opencodeAgent;
10810
11478
  if (agentName === undefined || seen.has(agentName)) {
10811
11479
  continue;
10812
11480
  }
10813
- seen.set(agentName, role.requiresLocalRuntime === true);
11481
+ seen.add(agentName);
10814
11482
  }
10815
- return [...seen.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([agentName, localOnly]) => ({
11483
+ return [...seen].sort((a, b) => a.localeCompare(b)).map((agentName) => ({
10816
11484
  agentName,
10817
11485
  path: `${AGENT_DIR}/${agentName}.md`,
10818
- contents: buildRoleAgent(agentName, ROLE_AGENT_DESCRIPTIONS[agentName] ?? `openteam ${agentName} subagent.`, localOnly)
11486
+ contents: buildRoleAgent(agentName, ROLE_AGENT_DESCRIPTIONS[agentName] ?? `openteam ${agentName} subagent.`)
10819
11487
  }));
10820
11488
  }
10821
11489
  function splitSetupPath(path2) {
@@ -10961,40 +11629,42 @@ function buildOpenTeamConfig(answers) {
10961
11629
  return runtime;
10962
11630
  });
10963
11631
  const primaryLocal = firstEnabled(answers.runtimes);
11632
+ if (answers.executionMode === "local" && primaryLocal === undefined) {
11633
+ throw new Error(executionSetupMessages.localRequiredError);
11634
+ }
10964
11635
  const localDefault = primaryLocal !== undefined ? {
10965
11636
  providerID: choiceProviderID(primaryLocal),
10966
11637
  modelID: primaryLocal.defaultModelID
10967
11638
  } : null;
10968
- const localOnly = answers.localOnly === true && primaryLocal !== undefined;
10969
- return OpenTeamConfigSchema.parse({
11639
+ const roles = Object.fromEntries(Object.keys(TEAM_ROLES).sort().map((roleID) => [roleID, { model: "auto" }]));
11640
+ const config = OpenTeamConfigSchema.parse({
11641
+ configVersion: 2,
10970
11642
  baseline: {
10971
11643
  mode: "auto",
10972
11644
  pinnedModel: null,
10973
11645
  hardDefault: answers.frontier
10974
11646
  },
10975
11647
  router: {
10976
- mode: answers.routerMode,
10977
- localDefault,
10978
- frontierOnly: primaryLocal === undefined,
10979
- localOnly
11648
+ executionMode: answers.executionMode,
11649
+ localDefault
10980
11650
  },
10981
11651
  local: { runtimes },
10982
- privacyMode: answers.privacyMode,
11652
+ orchestrator: {
11653
+ primary: {
11654
+ model: answers.primaryModel,
11655
+ fallbacks: []
11656
+ },
11657
+ roles,
11658
+ maxRetries: 2,
11659
+ maxModelsPerNode: 2
11660
+ },
10983
11661
  graph: { mode: "active" }
10984
11662
  });
11663
+ assertEffectiveConfigModelDomains(config);
11664
+ return config;
10985
11665
  }
10986
11666
  function orchestratorAgentModel(answers) {
10987
- const primaryLocal = firstEnabled(answers.runtimes);
10988
- if (answers.localOnly === true && primaryLocal !== undefined) {
10989
- return {
10990
- providerID: choiceProviderID(primaryLocal),
10991
- modelID: primaryLocal.thinkingModelID ?? primaryLocal.defaultModelID
10992
- };
10993
- }
10994
- return {
10995
- providerID: answers.frontier.providerID,
10996
- modelID: answers.frontier.modelID
10997
- };
11667
+ return { ...answers.primaryModel };
10998
11668
  }
10999
11669
  function buildOpencodeConfig(answers) {
11000
11670
  const provider = {};
@@ -11008,12 +11678,22 @@ function buildOpencodeConfig(answers) {
11008
11678
  if (baseURL === undefined) {
11009
11679
  continue;
11010
11680
  }
11011
- const modelIDs = choice.models.length > 0 ? choice.models : [choice.defaultModelID];
11681
+ const providerID = choiceProviderID(choice);
11682
+ const modelIDs = new Set(choice.models);
11683
+ modelIDs.add(choice.defaultModelID);
11684
+ if (choice.thinkingModelID !== undefined) {
11685
+ modelIDs.add(choice.thinkingModelID);
11686
+ }
11687
+ if (choice.coderModelID !== undefined) {
11688
+ modelIDs.add(choice.coderModelID);
11689
+ }
11690
+ if (answers.primaryModel.providerID === providerID) {
11691
+ modelIDs.add(answers.primaryModel.modelID);
11692
+ }
11012
11693
  const models = {};
11013
11694
  for (const modelID of modelIDs) {
11014
11695
  models[modelID] = { name: modelID };
11015
11696
  }
11016
- const providerID = choiceProviderID(choice);
11017
11697
  const owner = providerOwner.get(providerID);
11018
11698
  if (owner !== undefined) {
11019
11699
  if (owner.baseURL !== baseURL) {
@@ -11038,8 +11718,7 @@ function buildOpencodeConfig(answers) {
11038
11718
  providerOwner.set(providerID, { instanceId: choice.id, baseURL });
11039
11719
  }
11040
11720
  const primaryLocal = firstEnabled(answers.runtimes);
11041
- const localOnly = answers.localOnly === true && primaryLocal !== undefined;
11042
- const mainModel = localOnly && primaryLocal !== undefined ? `${choiceProviderID(primaryLocal)}/${primaryLocal.defaultModelID}` : `${answers.frontier.providerID}/${answers.frontier.modelID}`;
11721
+ const mainModel = `${answers.primaryModel.providerID}/${answers.primaryModel.modelID}`;
11043
11722
  const config = {
11044
11723
  $schema: "https://opencode.ai/config.json",
11045
11724
  plugin: [OPENTEAM_PLUGIN_SPEC],
@@ -11119,7 +11798,7 @@ async function chooseFrontier(prompt, profiles) {
11119
11798
  const sorted = sortFrontierByCost(profiles);
11120
11799
  const first = sorted[0];
11121
11800
  const selected = await prompt.autocomplete({
11122
- message: "Baseline frontier model (type to search; cheapest first)",
11801
+ message: executionSetupMessages.frontierModelQuestion,
11123
11802
  placeholder: "search by provider or model…",
11124
11803
  choices: [
11125
11804
  ...sorted.map((profile) => ({
@@ -11405,7 +12084,7 @@ async function resolveRuntimeInstances(deps, meta, info, usedIds) {
11405
12084
  async function runSetup(deps) {
11406
12085
  const { prompt } = deps;
11407
12086
  try {
11408
- prompt.intro("openteam setup — configure opencode with local-first routing");
12087
+ prompt.intro(executionSetupMessages.setupIntro);
11409
12088
  const detected = await deps.detect();
11410
12089
  const detectedById = new Map(detected.map((item) => [item.id, item]));
11411
12090
  const runtimeChoices = KNOWN_RUNTIMES.map((meta) => {
@@ -11419,11 +12098,11 @@ async function runSetup(deps) {
11419
12098
  });
11420
12099
  runtimeChoices.push({
11421
12100
  value: RUNTIME_NONE,
11422
- label: "None — use frontier models only",
11423
- hint: "no local runtime (frontier-only)"
12101
+ label: executionSetupMessages.runtimeNoneLabel,
12102
+ hint: executionSetupMessages.runtimeNoneHint
11424
12103
  });
11425
12104
  const enabledIds = await prompt.multiselect({
11426
- message: "Which local runtimes do you want to enable? (choose 'None' for frontier-only)",
12105
+ message: executionSetupMessages.runtimeQuestion,
11427
12106
  choices: runtimeChoices,
11428
12107
  required: true
11429
12108
  });
@@ -11450,10 +12129,27 @@ async function runSetup(deps) {
11450
12129
  runtimes.push(...await resolveRuntimeInstances(deps, meta, info, usedIds));
11451
12130
  }
11452
12131
  const hasLocalEnabled = enabled.size > 0;
11453
- const localOnly = hasLocalEnabled ? await prompt.confirm({
11454
- message: "Run without frontier models? (local runtimes only; openteam never escalates)",
11455
- initial: false
11456
- }) : false;
12132
+ const executionMode2 = hasLocalEnabled ? await prompt.select({
12133
+ message: executionSetupMessages.executionModeQuestion,
12134
+ choices: [
12135
+ {
12136
+ value: "mixed",
12137
+ label: executionSetupMessages.executionModeChoices.mixed.label,
12138
+ hint: executionSetupMessages.executionModeChoices.mixed.hint
12139
+ },
12140
+ {
12141
+ value: "local",
12142
+ label: executionSetupMessages.executionModeChoices.local.label,
12143
+ hint: executionSetupMessages.executionModeChoices.local.hint
12144
+ },
12145
+ {
12146
+ value: "frontier",
12147
+ label: executionSetupMessages.executionModeChoices.frontier.label,
12148
+ hint: executionSetupMessages.executionModeChoices.frontier.hint
12149
+ }
12150
+ ],
12151
+ initial: "mixed"
12152
+ }) : "frontier";
11457
12153
  let frontierProfiles;
11458
12154
  try {
11459
12155
  const loaded = await deps.loadFrontierProfiles();
@@ -11461,68 +12157,55 @@ async function runSetup(deps) {
11461
12157
  } catch {
11462
12158
  frontierProfiles = [...CURATED_FRONTIER_PROFILES];
11463
12159
  }
11464
- const frontier = localOnly ? cheapestFrontier(frontierProfiles) : await chooseFrontier(prompt, frontierProfiles);
11465
- if (localOnly) {
11466
- prompt.note([
11467
- "openteam will never escalate to a frontier model.",
11468
- "opencode's main model is pointed at your local runtime too, so the",
11469
- "session itself does not reach a frontier provider either.",
11470
- "",
11471
- `A baseline is still recorded (${frontier.providerID}/${frontier.modelID})`,
11472
- "but stays unused. Set router.localOnly to false to re-enable it.",
11473
- "",
11474
- "Routing mode set to 'balanced' and privacy to 'forceLocalOnSensitive'",
11475
- "automatically — in local-only mode every task stays local regardless,",
11476
- "so these settings cannot change behaviour."
11477
- ].join(`
11478
- `), "Local-only mode");
11479
- }
11480
- const routerMode = localOnly ? "balanced" : await prompt.select({
11481
- message: "Routing mode",
11482
- choices: [
11483
- { value: "balanced", label: "balanced (recommended)" },
11484
- { value: "economy", label: "economy (max savings, prefers local)" },
11485
- { value: "quality", label: "quality (prefers frontier)" }
11486
- ],
11487
- initial: "balanced"
11488
- });
12160
+ const primaryLocal = firstEnabled(runtimes);
12161
+ const localPrimary = primaryLocal === undefined ? undefined : {
12162
+ providerID: choiceProviderID(primaryLocal),
12163
+ modelID: primaryLocal.thinkingModelID ?? primaryLocal.defaultModelID
12164
+ };
12165
+ let frontier;
12166
+ let primaryModel;
12167
+ if (executionMode2 === "local") {
12168
+ if (localPrimary === undefined) {
12169
+ throw new Error(executionSetupMessages.localRequiredError);
12170
+ }
12171
+ frontier = cheapestFrontier(frontierProfiles);
12172
+ primaryModel = localPrimary;
12173
+ prompt.note(executionSetupMessages.localOnlyNote(`${primaryModel.providerID}/${primaryModel.modelID}`), executionSetupMessages.localOnlyTitle);
12174
+ } else {
12175
+ frontier = await chooseFrontier(prompt, frontierProfiles);
12176
+ if (executionMode2 === "frontier" || localPrimary === undefined) {
12177
+ primaryModel = frontier;
12178
+ } else {
12179
+ const primaryDomain = await prompt.select({
12180
+ message: executionSetupMessages.mixedPrimaryQuestion,
12181
+ choices: [
12182
+ {
12183
+ value: "local",
12184
+ label: executionSetupMessages.mixedPrimaryLocal(`${localPrimary.providerID}/${localPrimary.modelID}`)
12185
+ },
12186
+ {
12187
+ value: "frontier",
12188
+ label: executionSetupMessages.mixedPrimaryFrontier
12189
+ }
12190
+ ],
12191
+ initial: "frontier"
12192
+ });
12193
+ primaryModel = primaryDomain === "local" ? localPrimary : frontier;
12194
+ }
12195
+ }
11489
12196
  if (!hasLocalEnabled) {
11490
- prompt.note([
11491
- "You have not enabled any local runtime: openteam will run only with",
11492
- "frontier models (the configured cheapest-capable).",
11493
- "",
11494
- "For sensitive prompts, 'forceLocalOnSensitive' would have no local",
11495
- "target, so 'consentBeforeFrontier' is recommended."
11496
- ].join(`
11497
- `), "Frontier-only mode");
11498
- }
11499
- const privacyMode = localOnly ? "forceLocalOnSensitive" : await prompt.select({
11500
- message: "Privacy for sensitive prompts",
11501
- choices: [
11502
- {
11503
- value: "forceLocalOnSensitive",
11504
- label: "forceLocalOnSensitive (never sends sensitive data to frontier)",
11505
- ...hasLocalEnabled ? {} : { hint: "requires an enabled local runtime" }
11506
- },
11507
- {
11508
- value: "consentBeforeFrontier",
11509
- label: "consentBeforeFrontier (asks for consent)"
11510
- },
11511
- { value: "off", label: "off (no protection)" }
11512
- ],
11513
- initial: hasLocalEnabled ? "forceLocalOnSensitive" : "consentBeforeFrontier"
11514
- });
12197
+ prompt.note(executionSetupMessages.frontierOnlyNote, executionSetupMessages.frontierOnlyTitle);
12198
+ }
11515
12199
  const yolo = await prompt.confirm({
11516
- message: "Enable YOLO mode? (opencode auto-approves all permissions; does not affect openteam privacy)",
12200
+ message: executionSetupMessages.yoloQuestion,
11517
12201
  initial: false
11518
12202
  });
11519
12203
  const answers = {
11520
12204
  runtimes,
11521
12205
  frontier,
11522
- routerMode,
11523
- privacyMode,
11524
- yolo,
11525
- localOnly
12206
+ executionMode: executionMode2,
12207
+ primaryModel,
12208
+ yolo
11526
12209
  };
11527
12210
  const openTeamConfig = buildOpenTeamConfig(answers);
11528
12211
  const opencodeConfig = buildOpencodeConfig(answers);
@@ -11532,7 +12215,7 @@ async function runSetup(deps) {
11532
12215
  const existingOpencodePath = await deps.resolveOpencodeWriteTarget(OPENCODE_CONFIG_CANDIDATES);
11533
12216
  const opencodePath = existingOpencodePath ?? OPENCODE_CONFIG_CANDIDATES[0];
11534
12217
  const agentPath = ORCHESTRATOR_AGENT_PATH;
11535
- const roleAgentFiles = buildRoleAgentFiles();
12218
+ const roleAgentFiles = buildRoleAgentFiles(openTeamConfig);
11536
12219
  const opencodeGitignorePath = OPENCODE_GITIGNORE_PATH;
11537
12220
  const existingGitignore = await deps.fileExists(opencodeGitignorePath) ? await deps.readFile(opencodeGitignorePath) ?? "" : "";
11538
12221
  const opencodeGitignore = buildOpencodeGitignore(existingGitignore);
@@ -11589,27 +12272,30 @@ async function runSetup(deps) {
11589
12272
  const remote = runtime.baseURL !== undefined && !runtime.baseURL.includes("localhost") && !runtime.baseURL.includes("127.0.0.1") ? ` @ ${runtime.baseURL}` : "";
11590
12273
  return `${runtime.id} (${runtime.defaultModelID})${remote}`;
11591
12274
  }).join(", ");
11592
- prompt.note([
11593
- `Baseline frontier: ${localOnly ? `${frontier.providerID}/${frontier.modelID} (recorded but unused — local-only)` : `${frontier.providerID}/${frontier.modelID}`}`,
11594
- `Local runtimes: ${enabledSummary || "none (frontier-only)"}`,
11595
- `YOLO mode: ${yolo ? "enabled (auto-approves permissions)" : "disabled"}`,
11596
- "Web Console: launched separately from the CLI with 'openteam console' (multi-session, loopback)",
11597
- `Wrote: ${opencodePath}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${AGENT_DIR}/*.md (${roleAgentFiles.length} standard subagents), ${OPENTEAM_COMMAND_DIR}/*.md (${slashCommands.length} commands); ensured ${OPENCODE_GITIGNORE_PATH}`,
11598
- "",
11599
- "Next steps:",
11600
- localOnly ? " 1. No frontier auth needed; openteam runs entirely on your local runtimes" : ` 1. Authenticate the frontier provider: opencode auth login`,
11601
- hasLocalEnabled ? " 2. Make sure your local runtime (local or on the network) is reachable; openteam routes local-first automatically" : " 2. Open opencode in this repo; openteam will use frontier models only",
11602
- " 3. Press Tab and pick the 'openteam' agent, or type / and pick an /openteam… command"
11603
- ].join(`
11604
- `), "openteam configured");
11605
- prompt.outro("Done. Open opencode and select the 'openteam' agent.");
12275
+ prompt.note(executionSetupMessages.configuredSummary({
12276
+ executionMode: executionMode2,
12277
+ primaryModel: `${primaryModel.providerID}/${primaryModel.modelID}`,
12278
+ frontierBaseline: `${frontier.providerID}/${frontier.modelID}`,
12279
+ localRuntimes: enabledSummary || executionSetupMessages.runtimeNoneHint,
12280
+ yolo,
12281
+ opencodePath,
12282
+ configPath: DEFAULT_CONFIG_PATH,
12283
+ agentPath: ORCHESTRATOR_AGENT_PATH,
12284
+ agentDir: AGENT_DIR,
12285
+ roleAgentCount: roleAgentFiles.length,
12286
+ commandDir: OPENTEAM_COMMAND_DIR,
12287
+ commandCount: slashCommands.length,
12288
+ gitignorePath: OPENCODE_GITIGNORE_PATH
12289
+ }).join(`
12290
+ `), executionSetupMessages.configuredTitle);
12291
+ prompt.outro(executionSetupMessages.done);
11606
12292
  return {
11607
12293
  exitCode: 0,
11608
- stdout: `openteam setup complete: ${opencodePath}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${AGENT_DIR}/*.md (${roleAgentFiles.length} subagents), ${OPENTEAM_COMMAND_DIR}/*.md, ${OPENCODE_GITIGNORE_PATH}`
12294
+ stdout: executionSetupMessages.complete(`${opencodePath}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${AGENT_DIR}/*.md (${roleAgentFiles.length} subagents), ${OPENTEAM_COMMAND_DIR}/*.md, ${OPENCODE_GITIGNORE_PATH}`)
11609
12295
  };
11610
12296
  } catch (error) {
11611
12297
  const message = error instanceof Error ? error.message : String(error);
11612
- return { exitCode: 1, stdout: `setup error: ${message}` };
12298
+ return { exitCode: 1, stdout: executionSetupMessages.error(message) };
11613
12299
  }
11614
12300
  }
11615
12301
 
@@ -11879,8 +12565,8 @@ function stripOpenteamFromOpencodeConfig(raw) {
11879
12565
  }
11880
12566
  return { status: "not-present" };
11881
12567
  }
11882
- const record = parsed;
11883
- const plugins = record.plugin;
12568
+ const record2 = parsed;
12569
+ const plugins = record2.plugin;
11884
12570
  if (!Array.isArray(plugins)) {
11885
12571
  if (raw.includes(OPENTEAM_PLUGIN_SPEC)) {
11886
12572
  return {
@@ -12294,10 +12980,10 @@ var HELP = [
12294
12980
  localMessages.help.off,
12295
12981
  localMessages.help.only,
12296
12982
  localMessages.help.on,
12297
- " openteam doctor Diagnose runtimes and config",
12298
- " openteam roles init Compose per-role routing policy for unprofiled roster roles",
12983
+ executionPolicyMessages.doctorHelp,
12984
+ rolesInitMessages.help,
12299
12985
  " openteam migrate Relocate pre-P15b artifacts to their split homes (idempotent)",
12300
- " openteam agents List agents and each one's LLM (local/frontier)",
12986
+ executionPolicyMessages.agentsHelp,
12301
12987
  " openteam console Launch the multi-session web Console (Ctrl+C to stop; --open opens the browser)",
12302
12988
  " openteam console --status Show the Console config without launching it",
12303
12989
  " openteam graph status Show the Graph journal and lease health",
@@ -12515,9 +13201,7 @@ ${HELP}`
12515
13201
  const agentNote = await regenerateOrchestratorAgent(deps, config, enable);
12516
13202
  return {
12517
13203
  exitCode: 0,
12518
- stdout: `${writePath} updated. ${renderYoloStatus(enable)}${agentNote}${enable ? `
12519
- Note: YOLO does not disable openteam privacy; sensitive prompts still stay off frontier with forceLocalOnSensitive.` : ""}
12520
- Restart opencode (or reload) so agents pick up the new permissions.`
13204
+ stdout: yoloMessages.updated(writePath, renderYoloStatus(enable), agentNote)
12521
13205
  };
12522
13206
  }
12523
13207
  async function regenerateOrchestratorAgent(deps, config, enable) {
@@ -12577,6 +13261,11 @@ async function runAgents(deps, configPath, opencodeConfigPaths, configResolution
12577
13261
  exitCode: 0,
12578
13262
  stdout: renderAgentList(classifications, {
12579
13263
  ...context,
13264
+ executionPolicies: executionPolicySummaries(config),
13265
+ retryLimits: {
13266
+ maxRetries: config.orchestrator?.maxRetries ?? 2,
13267
+ maxModelsPerNode: config.orchestrator?.maxModelsPerNode ?? 2
13268
+ },
12580
13269
  ...opencodeConfigError !== undefined ? { opencodeConfigError } : {}
12581
13270
  })
12582
13271
  };
@@ -13163,9 +13852,12 @@ function auditParsedRoster(roster, configuredRoles) {
13163
13852
  }
13164
13853
 
13165
13854
  // src/commands/rolesInit.ts
13166
- var ROLES_INIT_LOCAL_ONLY = "local-only";
13167
- var ROLES_INIT_FRONTIER_OK = "frontier-ok";
13855
+ var ROLES_INIT_AUTO = "auto";
13856
+ var ROLES_INIT_PIN = "pin";
13168
13857
  var ROLES_INIT_SKIP = "skip";
13858
+ var ROLES_INIT_INHERIT = "inherit";
13859
+ var ROLES_INIT_LOCAL_ONLY = "local";
13860
+ var ROLES_INIT_FRONTIER_ONLY = "frontier";
13169
13861
  function isJsonRecord2(value) {
13170
13862
  return typeof value === "object" && value !== null && !Array.isArray(value);
13171
13863
  }
@@ -13198,29 +13890,15 @@ function withRole(config, roleID, profile) {
13198
13890
  }
13199
13891
  };
13200
13892
  }
13201
- function localOnlyProfile(roleID) {
13202
- const base = synthesiseFallbackProfile(roleID);
13203
- return {
13204
- ...base,
13205
- preferredFrontierModels: [],
13206
- localFirst: true,
13207
- requiresLocalRuntime: true
13208
- };
13209
- }
13210
- function frontierEligibleProfile(roleID) {
13211
- return { ...synthesiseFallbackProfile(roleID) };
13212
- }
13213
- function policyChoices(roleID, agentName) {
13893
+ function policyChoices() {
13214
13894
  return [
13215
13895
  {
13216
- value: ROLES_INIT_LOCAL_ONLY,
13217
- label: rolesInitMessages.localOnlyLabel(roleID, agentName),
13218
- hint: rolesInitMessages.localOnlyHint
13896
+ value: ROLES_INIT_AUTO,
13897
+ label: executionSetupMessages.rolePolicy.modelAutoLabel
13219
13898
  },
13220
13899
  {
13221
- value: ROLES_INIT_FRONTIER_OK,
13222
- label: rolesInitMessages.frontierOkLabel,
13223
- hint: rolesInitMessages.frontierOkHint
13900
+ value: ROLES_INIT_PIN,
13901
+ label: executionSetupMessages.rolePolicy.modelExactLabel
13224
13902
  },
13225
13903
  {
13226
13904
  value: ROLES_INIT_SKIP,
@@ -13229,6 +13907,56 @@ function policyChoices(roleID, agentName) {
13229
13907
  }
13230
13908
  ];
13231
13909
  }
13910
+ function executionModeChoices() {
13911
+ return [
13912
+ {
13913
+ value: ROLES_INIT_INHERIT,
13914
+ label: executionSetupMessages.rolePolicy.modeInheritLabel
13915
+ },
13916
+ {
13917
+ value: ROLES_INIT_LOCAL_ONLY,
13918
+ label: executionSetupMessages.rolePolicy.modeLocalLabel
13919
+ },
13920
+ {
13921
+ value: ROLES_INIT_FRONTIER_ONLY,
13922
+ label: executionSetupMessages.rolePolicy.modeFrontierLabel
13923
+ }
13924
+ ];
13925
+ }
13926
+ function rawExecutionMode(config) {
13927
+ const router = isJsonRecord2(config.router) ? config.router : {};
13928
+ if (router.executionMode === "local" || router.executionMode === "frontier" || router.executionMode === "mixed") {
13929
+ return router.executionMode;
13930
+ }
13931
+ if (router.localOnly === true) {
13932
+ return "local";
13933
+ }
13934
+ if (router.frontierOnly === true) {
13935
+ return "frontier";
13936
+ }
13937
+ return "mixed";
13938
+ }
13939
+ function normalizePortableConfig(raw) {
13940
+ const migrated = migrateConfig(raw);
13941
+ const normalized = isJsonRecord2(migrated) ? { ...migrated } : {};
13942
+ delete normalized.privacyMode;
13943
+ return normalized;
13944
+ }
13945
+ function parseModelRef2(value) {
13946
+ const trimmed = value.trim();
13947
+ const slash = trimmed.indexOf("/");
13948
+ if (slash <= 0 || slash === trimmed.length - 1) {
13949
+ throw new Error(executionSetupMessages.rolePolicy.invalidModelRef(value));
13950
+ }
13951
+ return {
13952
+ providerID: trimmed.slice(0, slash),
13953
+ modelID: trimmed.slice(slash + 1)
13954
+ };
13955
+ }
13956
+ function parseFallbacks(value) {
13957
+ const trimmed = value.trim();
13958
+ return trimmed.length === 0 ? [] : trimmed.split(",").map((entry) => parseModelRef2(entry));
13959
+ }
13232
13960
  async function runRolesInit(deps) {
13233
13961
  const lines = [rolesInitMessages.header];
13234
13962
  const rosterContent = await deps.readFile(OPENTEAM_ROSTER_PATH);
@@ -13243,8 +13971,18 @@ async function runRolesInit(deps) {
13243
13971
  return { stdout: lines.join(`
13244
13972
  `), exitCode: 1 };
13245
13973
  }
13246
- const baseConfig = await readJsonFile(deps, DEFAULT_CONFIG_PATH);
13974
+ const rawBaseConfig = await readJsonFile(deps, DEFAULT_CONFIG_PATH);
13975
+ lines.push(...detectConfigDeprecations(rawBaseConfig).map((deprecation) => rolesInitMessages.deprecation(deprecation.message)));
13976
+ let baseConfig;
13977
+ try {
13978
+ baseConfig = normalizePortableConfig(rawBaseConfig);
13979
+ } catch (error) {
13980
+ lines.push(rolesInitMessages.invalidResult(error instanceof Error ? error.message : String(error)));
13981
+ return { stdout: lines.join(`
13982
+ `), exitCode: 1 };
13983
+ }
13247
13984
  const configuredRoles = existingRoles(baseConfig);
13985
+ const globalExecutionMode = rawExecutionMode(baseConfig);
13248
13986
  const unprofiled = parsed.roster.entries.filter((entry) => getRoleProfile(entry.roleID, configuredRoles) === undefined && !isKnownNonWorkerRole(entry.roleID));
13249
13987
  if (unprofiled.length === 0) {
13250
13988
  lines.push(rolesInitMessages.allProfiled(parsed.roster.entries.length));
@@ -13252,32 +13990,44 @@ async function runRolesInit(deps) {
13252
13990
  `), exitCode: 0 };
13253
13991
  }
13254
13992
  deps.prompt.intro(rolesInitMessages.title);
13255
- deps.prompt.note(rolesInitMessages.why(unprofiled.length), rolesInitMessages.whyTitle);
13993
+ deps.prompt.note(executionSetupMessages.rolePolicy.why(unprofiled.length), executionSetupMessages.rolePolicy.whyTitle);
13256
13994
  const runtimes = (await deps.detectRuntimes?.())?.filter((runtime) => runtime.available);
13257
13995
  let nextConfig = baseConfig;
13258
13996
  let nextOverlay;
13259
- const pinned = [];
13260
- const relaxed = [];
13997
+ const configured = [];
13261
13998
  const skipped = [];
13262
13999
  try {
13263
14000
  for (const entry of unprofiled) {
13264
14001
  const choice = await deps.prompt.select({
13265
- message: rolesInitMessages.policyQuestion(entry.roleID, entry.agentName),
13266
- choices: policyChoices(entry.roleID, entry.agentName),
13267
- initial: ROLES_INIT_LOCAL_ONLY
14002
+ message: executionSetupMessages.rolePolicy.modelQuestion(entry.roleID),
14003
+ choices: policyChoices(),
14004
+ initial: ROLES_INIT_AUTO
13268
14005
  });
13269
14006
  if (choice === ROLES_INIT_SKIP) {
13270
14007
  skipped.push(entry.roleID);
13271
14008
  continue;
13272
14009
  }
13273
- if (choice === ROLES_INIT_FRONTIER_OK) {
13274
- nextConfig = withRole(nextConfig, entry.roleID, frontierEligibleProfile(entry.roleID));
13275
- relaxed.push(entry.roleID);
13276
- continue;
13277
- }
13278
- nextConfig = withRole(nextConfig, entry.roleID, localOnlyProfile(entry.roleID));
13279
- pinned.push(entry.roleID);
13280
- if (runtimes !== undefined && runtimes.length > 0) {
14010
+ const modeChoice = globalExecutionMode === "mixed" ? await deps.prompt.select({
14011
+ message: executionSetupMessages.rolePolicy.modeQuestion(entry.roleID, entry.agentName),
14012
+ choices: executionModeChoices(),
14013
+ initial: ROLES_INIT_INHERIT
14014
+ }) : ROLES_INIT_INHERIT;
14015
+ const model = choice === ROLES_INIT_AUTO ? "auto" : parseModelRef2(await deps.prompt.text({
14016
+ message: executionSetupMessages.rolePolicy.modelExactQuestion(entry.roleID),
14017
+ placeholder: executionSetupMessages.rolePolicy.modelExactPlaceholder
14018
+ }));
14019
+ const fallbacks = parseFallbacks(await deps.prompt.text({
14020
+ message: executionSetupMessages.rolePolicy.fallbacksQuestion(entry.roleID),
14021
+ placeholder: executionSetupMessages.rolePolicy.fallbacksPlaceholder
14022
+ }));
14023
+ const policy = {
14024
+ model,
14025
+ fallbacks,
14026
+ ...modeChoice === ROLES_INIT_INHERIT ? {} : { executionMode: modeChoice }
14027
+ };
14028
+ nextConfig = withRole(nextConfig, entry.roleID, policy);
14029
+ configured.push(entry.roleID);
14030
+ if (modeChoice === ROLES_INIT_LOCAL_ONLY && runtimes !== undefined && runtimes.length > 0) {
13281
14031
  const selected = await deps.prompt.multiselect({
13282
14032
  message: rolesInitMessages.runtimeQuestion(entry.roleID),
13283
14033
  choices: runtimes.map((runtime) => ({
@@ -13299,15 +14049,21 @@ async function runRolesInit(deps) {
13299
14049
  return { stdout: lines.join(`
13300
14050
  `), exitCode: 1 };
13301
14051
  }
13302
- if (pinned.length === 0 && relaxed.length === 0) {
14052
+ if (configured.length === 0) {
13303
14053
  lines.push(rolesInitMessages.nothingWritten(skipped.join(", ")));
13304
14054
  deps.prompt.outro(rolesInitMessages.nothingToWriteOutro);
13305
14055
  return { stdout: lines.join(`
13306
14056
  `), exitCode: 0 };
13307
14057
  }
13308
- const merged = mergeConfigInputs(nextConfig, nextOverlay);
13309
14058
  try {
13310
- loadOpenTeamConfig(merged);
14059
+ const existingOverlay = await readJsonFile(deps, DEFAULT_LOCAL_OVERLAY_PATH);
14060
+ const effectiveOverlay = nextOverlay === undefined ? existingOverlay : mergeConfigInputs(existingOverlay, nextOverlay);
14061
+ const merged = mergeConfigInputs(nextConfig, effectiveOverlay);
14062
+ const effectiveConfig = loadOpenTeamConfig(merged);
14063
+ assertEffectiveConfigModelDomains(effectiveConfig);
14064
+ if (nextOverlay !== undefined) {
14065
+ nextOverlay = effectiveOverlay;
14066
+ }
13311
14067
  } catch (error) {
13312
14068
  lines.push(rolesInitMessages.invalidResult(error instanceof Error ? error.message : String(error)));
13313
14069
  return { stdout: lines.join(`
@@ -13317,12 +14073,7 @@ async function runRolesInit(deps) {
13317
14073
  if (nextOverlay !== undefined) {
13318
14074
  await deps.writeFile(DEFAULT_LOCAL_OVERLAY_PATH, serialize(nextOverlay));
13319
14075
  }
13320
- if (pinned.length > 0) {
13321
- lines.push(rolesInitMessages.pinnedLocal(pinned.join(", "), DEFAULT_CONFIG_PATH));
13322
- }
13323
- if (relaxed.length > 0) {
13324
- lines.push(rolesInitMessages.frontierAllowed(relaxed.join(", "), DEFAULT_CONFIG_PATH));
13325
- }
14076
+ lines.push(executionSetupMessages.rolePolicy.configured(configured.join(", "), DEFAULT_CONFIG_PATH));
13326
14077
  if (nextOverlay !== undefined) {
13327
14078
  lines.push(rolesInitMessages.runtimeBinding(DEFAULT_LOCAL_OVERLAY_PATH));
13328
14079
  }
@@ -14285,21 +15036,21 @@ var DEFAULT_RANK_SIGNALS = {
14285
15036
  accessWeight: 0.1,
14286
15037
  recencyHalfLifeMs: 14 * DAY_MS
14287
15038
  };
14288
- function recencyTimestamp(record) {
14289
- if (record.kind === "preference" && record.lastAccessedAt !== null) {
14290
- return record.lastAccessedAt;
15039
+ function recencyTimestamp(record2) {
15040
+ if (record2.kind === "preference" && record2.lastAccessedAt !== null) {
15041
+ return record2.lastAccessedAt;
14291
15042
  }
14292
- return record.createdAt;
15043
+ return record2.createdAt;
14293
15044
  }
14294
- function recencyScore(record, signals) {
14295
- const ageMs = Math.max(0, signals.now - recencyTimestamp(record));
15045
+ function recencyScore(record2, signals) {
15046
+ const ageMs = Math.max(0, signals.now - recencyTimestamp(record2));
14296
15047
  return 0.5 ** (ageMs / signals.recencyHalfLifeMs);
14297
15048
  }
14298
- function accessScore(record) {
14299
- if (record.kind !== "preference") {
15049
+ function accessScore(record2) {
15050
+ if (record2.kind !== "preference") {
14300
15051
  return 0;
14301
15052
  }
14302
- return 1 - 1 / (1 + record.accessCount);
15053
+ return 1 - 1 / (1 + record2.accessCount);
14303
15054
  }
14304
15055
  function scoreCandidate(candidate, signals) {
14305
15056
  return signals.similarityWeight * candidate.similarity + signals.recencyWeight * recencyScore(candidate.record, signals) + signals.accessWeight * accessScore(candidate.record);
@@ -14317,20 +15068,20 @@ function rankRecall(candidates, signals) {
14317
15068
  }
14318
15069
 
14319
15070
  // src/memory/temporal.ts
14320
- function isTransactionActiveAt(record, asOf) {
14321
- return record.invalidatedAt === null || record.invalidatedAt > asOf;
15071
+ function isTransactionActiveAt(record2, asOf) {
15072
+ return record2.invalidatedAt === null || record2.invalidatedAt > asOf;
14322
15073
  }
14323
- function isValidAt(record, asOf) {
14324
- if (record.validFrom > asOf) {
15074
+ function isValidAt(record2, asOf) {
15075
+ if (record2.validFrom > asOf) {
14325
15076
  return false;
14326
15077
  }
14327
- return record.validUntil === null || record.validUntil > asOf;
15078
+ return record2.validUntil === null || record2.validUntil > asOf;
14328
15079
  }
14329
- function isActiveAt(record, asOf) {
14330
- return isTransactionActiveAt(record, asOf) && isValidAt(record, asOf);
15080
+ function isActiveAt(record2, asOf) {
15081
+ return isTransactionActiveAt(record2, asOf) && isValidAt(record2, asOf);
14331
15082
  }
14332
15083
  function filterActive(records, asOf) {
14333
- return records.filter((record) => isActiveAt(record, asOf));
15084
+ return records.filter((record2) => isActiveAt(record2, asOf));
14334
15085
  }
14335
15086
  function coalesceWindow(a, b) {
14336
15087
  const validFrom = Math.min(a.validFrom, b.validFrom);
@@ -14366,21 +15117,21 @@ function cosineSimilarity(a, b) {
14366
15117
  }
14367
15118
 
14368
15119
  // src/memory/recall.ts
14369
- function matchesScope(record, query) {
14370
- if (record.ownerKey === query.ownerKey) {
15120
+ function matchesScope(record2, query) {
15121
+ if (record2.ownerKey === query.ownerKey) {
14371
15122
  return true;
14372
15123
  }
14373
- return query.includeShared && record.ownerKey === SHARED_OWNER_KEY;
15124
+ return query.includeShared && record2.ownerKey === SHARED_OWNER_KEY;
14374
15125
  }
14375
15126
  function recall(records, queryEmbedding, query, deps) {
14376
15127
  const asOf = query.asOf ?? deps.now;
14377
15128
  const kinds = new Set(query.kinds);
14378
15129
  const candidates = [];
14379
- for (const record of filterActive(records, asOf)) {
14380
- if (!kinds.has(record.kind) || !matchesScope(record, query)) {
15130
+ for (const record2 of filterActive(records, asOf)) {
15131
+ if (!kinds.has(record2.kind) || !matchesScope(record2, query)) {
14381
15132
  continue;
14382
15133
  }
14383
- const embedding = deps.embeddings.get(record.id);
15134
+ const embedding = deps.embeddings.get(record2.id);
14384
15135
  if (embedding === undefined) {
14385
15136
  continue;
14386
15137
  }
@@ -14388,7 +15139,7 @@ function recall(records, queryEmbedding, query, deps) {
14388
15139
  if (similarity < query.minSimilarity) {
14389
15140
  continue;
14390
15141
  }
14391
- candidates.push({ record, similarity });
15142
+ candidates.push({ record: record2, similarity });
14392
15143
  }
14393
15144
  const signals = {
14394
15145
  now: deps.now,
@@ -14578,11 +15329,11 @@ function emptyState() {
14578
15329
  return { facts: [], preferences: [], entities: [], relations: [] };
14579
15330
  }
14580
15331
  function applyTombstone(state, tombstone) {
14581
- const mark = (records) => records.map((record) => record.id === tombstone.id ? {
14582
- ...record,
15332
+ const mark = (records) => records.map((record2) => record2.id === tombstone.id ? {
15333
+ ...record2,
14583
15334
  invalidatedAt: tombstone.invalidatedAt,
14584
15335
  supersededBy: tombstone.supersededBy
14585
- } : record);
15336
+ } : record2);
14586
15337
  if (tombstone.kind === "fact") {
14587
15338
  state.facts = mark(state.facts);
14588
15339
  } else if (tombstone.kind === "preference") {
@@ -14622,8 +15373,8 @@ function allRecords(state) {
14622
15373
  }
14623
15374
 
14624
15375
  // src/memory/log.ts
14625
- function serializeAssertion(record) {
14626
- return JSON.stringify(record);
15376
+ function serializeAssertion(record2) {
15377
+ return JSON.stringify(record2);
14627
15378
  }
14628
15379
  function parseMemoryLog(text2) {
14629
15380
  const records = [];
@@ -14685,8 +15436,8 @@ function insertRecords(db, records) {
14685
15436
  const stmt = db.query(INSERT_SQL2);
14686
15437
  db.run("BEGIN");
14687
15438
  try {
14688
- for (const record of records) {
14689
- stmt.run(record.id, record.kind, record.ownerKey, record.createdAt, record.invalidatedAt, JSON.stringify(record));
15439
+ for (const record2 of records) {
15440
+ stmt.run(record2.id, record2.kind, record2.ownerKey, record2.createdAt, record2.invalidatedAt, JSON.stringify(record2));
14690
15441
  }
14691
15442
  db.run("COMMIT");
14692
15443
  } catch (error) {
@@ -14739,15 +15490,15 @@ function createMemoryIndex(db, warn) {
14739
15490
  const preferences = [];
14740
15491
  const entities = [];
14741
15492
  const relations = [];
14742
- for (const record of allRecordsWithWarning()) {
14743
- if (record.kind === "fact") {
14744
- facts.push(record);
14745
- } else if (record.kind === "preference") {
14746
- preferences.push(record);
14747
- } else if (record.kind === "entity") {
14748
- entities.push(record);
15493
+ for (const record2 of allRecordsWithWarning()) {
15494
+ if (record2.kind === "fact") {
15495
+ facts.push(record2);
15496
+ } else if (record2.kind === "preference") {
15497
+ preferences.push(record2);
15498
+ } else if (record2.kind === "entity") {
15499
+ entities.push(record2);
14749
15500
  } else {
14750
- relations.push(record);
15501
+ relations.push(record2);
14751
15502
  }
14752
15503
  }
14753
15504
  return { facts, preferences, entities, relations };
@@ -14886,31 +15637,31 @@ function createLocalEmbedder(request, deps) {
14886
15637
  function normalize(parts) {
14887
15638
  return parts.map((part) => part.trim()).filter((part) => part.length > 0).join(" ");
14888
15639
  }
14889
- function recordEmbeddingText(record) {
14890
- if (record.kind === "fact") {
15640
+ function recordEmbeddingText(record2) {
15641
+ if (record2.kind === "fact") {
14891
15642
  return normalize([
14892
- record.subject,
14893
- record.predicate,
14894
- record.object,
14895
- record.category ?? ""
15643
+ record2.subject,
15644
+ record2.predicate,
15645
+ record2.object,
15646
+ record2.category ?? ""
14896
15647
  ]);
14897
15648
  }
14898
- if (record.kind === "preference") {
15649
+ if (record2.kind === "preference") {
14899
15650
  return normalize([
14900
- record.category,
14901
- record.preference,
14902
- record.context ?? ""
15651
+ record2.category,
15652
+ record2.preference,
15653
+ record2.context ?? ""
14903
15654
  ]);
14904
15655
  }
14905
- if (record.kind === "relation") {
15656
+ if (record2.kind === "relation") {
14906
15657
  return normalize([
14907
- record.from,
14908
- record.predicate,
14909
- record.to,
14910
- record.annotation ?? ""
15658
+ record2.from,
15659
+ record2.predicate,
15660
+ record2.to,
15661
+ record2.annotation ?? ""
14911
15662
  ]);
14912
15663
  }
14913
- return normalize([record.canonicalName, record.type, ...record.aliases]);
15664
+ return normalize([record2.canonicalName, record2.type, ...record2.aliases]);
14914
15665
  }
14915
15666
 
14916
15667
  // src/storage/index/memoryEmbeddings.ts
@@ -14926,7 +15677,7 @@ async function ensureMemoryEmbeddings(deps) {
14926
15677
  const records = index.allRecords();
14927
15678
  index.pruneEmbeddings();
14928
15679
  const have = modelChanged ? new Set : new Set(index.embeddingMap().keys());
14929
- const todo = records.filter((record) => !have.has(record.id));
15680
+ const todo = records.filter((record2) => !have.has(record2.id));
14930
15681
  if (todo.length === 0) {
14931
15682
  return { embedded: 0, failed: 0, modelChanged };
14932
15683
  }
@@ -14942,8 +15693,8 @@ async function ensureMemoryEmbeddings(deps) {
14942
15693
  warn(`[openteam] could not compute embeddings for ${todo.length} memory record(s): ` + `the embedder returned ${vectors.length} vector(s). Recall over them is unavailable.`);
14943
15694
  return { embedded: 0, failed: todo.length, modelChanged };
14944
15695
  }
14945
- const entries = todo.map((record, position) => ({
14946
- recordId: record.id,
15696
+ const entries = todo.map((record2, position) => ({
15697
+ recordId: record2.id,
14947
15698
  vector: vectors[position]
14948
15699
  }));
14949
15700
  index.putEmbeddings(entries);
@@ -14992,7 +15743,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
14992
15743
  // package.json
14993
15744
  var package_default = {
14994
15745
  name: "@jmanuelcorral/openteam",
14995
- version: "0.18.1",
15746
+ version: "0.19.0",
14996
15747
  packageManager: "bun@1.3.14",
14997
15748
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
14998
15749
  license: "MIT",
@@ -15183,30 +15934,30 @@ function isMissingFile(error) {
15183
15934
  }
15184
15935
 
15185
15936
  // src/memory/traverse.ts
15186
- function inOwnerScope(record, ownerKey, includeShared) {
15187
- if (record.ownerKey === ownerKey) {
15937
+ function inOwnerScope(record2, ownerKey, includeShared) {
15938
+ if (record2.ownerKey === ownerKey) {
15188
15939
  return true;
15189
15940
  }
15190
- return includeShared && record.ownerKey === SHARED_OWNER_KEY;
15941
+ return includeShared && record2.ownerKey === SHARED_OWNER_KEY;
15191
15942
  }
15192
15943
  function traverseRelations(records, query) {
15193
15944
  const includeShared = query.includeShared ?? true;
15194
15945
  const adjacency = new Map;
15195
- for (const record of records) {
15196
- if (record.kind !== "relation") {
15946
+ for (const record2 of records) {
15947
+ if (record2.kind !== "relation") {
15197
15948
  continue;
15198
15949
  }
15199
- if (!isActiveAt(record, query.asOf)) {
15950
+ if (!isActiveAt(record2, query.asOf)) {
15200
15951
  continue;
15201
15952
  }
15202
- if (!inOwnerScope(record, query.ownerKey, includeShared)) {
15953
+ if (!inOwnerScope(record2, query.ownerKey, includeShared)) {
15203
15954
  continue;
15204
15955
  }
15205
- const list = adjacency.get(record.from);
15956
+ const list = adjacency.get(record2.from);
15206
15957
  if (list === undefined) {
15207
- adjacency.set(record.from, [record]);
15958
+ adjacency.set(record2.from, [record2]);
15208
15959
  } else {
15209
- list.push(record);
15960
+ list.push(record2);
15210
15961
  }
15211
15962
  }
15212
15963
  const visited = new Set([query.seed]);
@@ -15237,8 +15988,8 @@ function traverseRelations(records, query) {
15237
15988
  }
15238
15989
  return { seed: query.seed, nodes, edges };
15239
15990
  }
15240
- function seedNodeId(record) {
15241
- return record.kind === "entity" ? record.canonicalName : undefined;
15991
+ function seedNodeId(record2) {
15992
+ return record2.kind === "entity" ? record2.canonicalName : undefined;
15242
15993
  }
15243
15994
  function recallGraph(records, queryEmbedding, query, options, deps) {
15244
15995
  const seeds = recall(records, queryEmbedding, query, deps);
@@ -15534,32 +16285,32 @@ function projectRelationGraph(graph) {
15534
16285
  };
15535
16286
  }
15536
16287
  function projectRecall(results) {
15537
- return results.map(({ record, similarity, score }) => {
16288
+ return results.map(({ record: record2, similarity, score }) => {
15538
16289
  const base = {
15539
- id: record.id,
15540
- kind: record.kind,
15541
- ownerKey: record.ownerKey,
15542
- confidence: record.confidence,
16290
+ id: record2.id,
16291
+ kind: record2.kind,
16292
+ ownerKey: record2.ownerKey,
16293
+ confidence: record2.confidence,
15543
16294
  similarity,
15544
16295
  score
15545
16296
  };
15546
- if (record.kind === "fact") {
16297
+ if (record2.kind === "fact") {
15547
16298
  return {
15548
16299
  ...base,
15549
- subject: record.subject,
15550
- predicate: record.predicate,
15551
- object: record.object,
15552
- category: record.category
16300
+ subject: record2.subject,
16301
+ predicate: record2.predicate,
16302
+ object: record2.object,
16303
+ category: record2.category
15553
16304
  };
15554
16305
  }
15555
- if (record.kind === "preference") {
16306
+ if (record2.kind === "preference") {
15556
16307
  return {
15557
16308
  ...base,
15558
- category: record.category,
15559
- preference: record.preference
16309
+ category: record2.category,
16310
+ preference: record2.preference
15560
16311
  };
15561
16312
  }
15562
- const entity = record;
16313
+ const entity = record2;
15563
16314
  return { ...base, canonicalName: entity.canonicalName, type: entity.type };
15564
16315
  });
15565
16316
  }
@@ -15734,24 +16485,24 @@ var DEFAULT_DECAY_PARAMS = {
15734
16485
  halfLifeMs: 90 * DAY_MS2,
15735
16486
  forgetThreshold: 0.05
15736
16487
  };
15737
- function lastReinforcedAt(record) {
15738
- if (record.kind === "preference" && record.lastAccessedAt !== null) {
15739
- return record.lastAccessedAt;
16488
+ function lastReinforcedAt(record2) {
16489
+ if (record2.kind === "preference" && record2.lastAccessedAt !== null) {
16490
+ return record2.lastAccessedAt;
15740
16491
  }
15741
- return record.createdAt;
16492
+ return record2.createdAt;
15742
16493
  }
15743
- function decayedConfidence(record, now, params = DEFAULT_DECAY_PARAMS) {
15744
- const ageMs = Math.max(0, now - lastReinforcedAt(record));
15745
- return record.confidence * 0.5 ** (ageMs / params.halfLifeMs);
16494
+ function decayedConfidence(record2, now, params = DEFAULT_DECAY_PARAMS) {
16495
+ const ageMs = Math.max(0, now - lastReinforcedAt(record2));
16496
+ return record2.confidence * 0.5 ** (ageMs / params.halfLifeMs);
15746
16497
  }
15747
16498
  function consolidate(records, now, params = DEFAULT_DECAY_PARAMS) {
15748
16499
  const tombstones = [];
15749
- for (const record of records) {
15750
- if (!isTransactionActiveAt(record, now)) {
16500
+ for (const record2 of records) {
16501
+ if (!isTransactionActiveAt(record2, now)) {
15751
16502
  continue;
15752
16503
  }
15753
- if (decayedConfidence(record, now, params) < params.forgetThreshold) {
15754
- tombstones.push({ ...record, invalidatedAt: now, supersededBy: null });
16504
+ if (decayedConfidence(record2, now, params) < params.forgetThreshold) {
16505
+ tombstones.push({ ...record2, invalidatedAt: now, supersededBy: null });
15755
16506
  }
15756
16507
  }
15757
16508
  return tombstones;
@@ -15866,17 +16617,21 @@ async function loadConfig(path4, resolution = { explicit: false }) {
15866
16617
  read: (requestedPath) => requestedPath === path4 ? readConfigFile(path4, resolution) : storage.read(requestedPath)
15867
16618
  } : storage;
15868
16619
  const overlayPath = resolution.explicit ? null : DEFAULT_LOCAL_OVERLAY_PATH;
15869
- return (await loadRuntimeOpenTeamConfig(configStorage, undefined, path4, overlayPath)).config;
16620
+ const loaded = await loadRuntimeOpenTeamConfig(configStorage, undefined, path4, overlayPath);
16621
+ for (const deprecation of loaded.deprecations) {
16622
+ console.warn(deprecation.message);
16623
+ }
16624
+ return loaded.config;
15870
16625
  }
15871
16626
  async function saveConfig(config, path4, resolution = { explicit: false }) {
15872
16627
  if (!resolution.explicit) {
15873
16628
  await writeOpenTeamConfigFile(config, path4, storage);
15874
16629
  return;
15875
16630
  }
16631
+ const contents = serializeOpenTeamConfig(config);
15876
16632
  const diskPath = resolve6(path4);
15877
16633
  await mkdir2(dirname6(diskPath), { recursive: true });
15878
- await writeFile2(diskPath, `${JSON.stringify(config, null, 2)}
15879
- `, "utf8");
16634
+ await writeFile2(diskPath, contents, "utf8");
15880
16635
  }
15881
16636
  function parseConfigSelection(argv) {
15882
16637
  for (let i = 0;i < argv.length; i += 1) {