@jmanuelcorral/openteam 0.18.0 → 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 (113) 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 +1354 -594
  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 +3164 -1102
  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 +28 -9
  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 +25 -0
  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 +68 -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 +1 -1
  106. package/dist/telemetry/diagnostics.d.ts.map +1 -1
  107. package/dist/telemetry/events.d.ts +20 -6
  108. package/dist/telemetry/events.d.ts.map +1 -1
  109. package/dist/telemetry/otel.d.ts.map +1 -1
  110. package/dist/telemetry/types.d.ts +3 -3
  111. package/dist/web/configApi.d.ts +3 -1
  112. package/dist/web/configApi.d.ts.map +1 -1
  113. 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);
@@ -619,6 +874,7 @@ var DIAGNOSTIC_CODES = [
619
874
  "graph-gate-denied",
620
875
  "memory-extraction-warning",
621
876
  "memory-runtime-warning",
877
+ "node-budget-exhausted",
622
878
  "opencode-server-url",
623
879
  "opencode-version-read-failed",
624
880
  "privacy-inert",
@@ -641,12 +897,14 @@ function diagnosticDescription(code) {
641
897
  return "Semantic memory extraction reported a non-fatal local-runtime warning; no raw conversation content was recorded.";
642
898
  case "memory-runtime-warning":
643
899
  return "Semantic memory runtime setup reported a non-fatal warning; memory recall may be incomplete.";
900
+ case "node-budget-exhausted":
901
+ return "A graph node consumed its entire per-node pass budget without ever reporting a substantive done; the node's iteration stopped at the bound and the role produced no substantive report across its passes. Inspect the role's report artifact; the task likely needs a larger orchestrator.maxRetries or a clearer assignment.";
644
902
  case "opencode-server-url":
645
903
  return "openteam observed the opencode server URL during plugin initialization.";
646
904
  case "opencode-version-read-failed":
647
905
  return "openteam could not read the live opencode version; graph gates fail closed when this happens.";
648
906
  case "privacy-inert":
649
- return "forceLocalOnSensitive is configured but no local runtime is configured, so the setting cannot keep sensitive prompts local.";
907
+ return routingMessages.privacyModeDeprecated;
650
908
  case "soak-evidence-rejected":
651
909
  return "Graph soak evidence contained rejected observation lines; cutover gate evidence may be incomplete.";
652
910
  case "telemetry-warning":
@@ -673,9 +931,9 @@ var RouteEventSchema = EventBaseSchema.extend({
673
931
  routeKind: z4.enum(["local", "frontier", "blocked"]),
674
932
  selected: ModelRefSchema,
675
933
  rationale: z4.string(),
676
- estimatedCostUSD: z4.number().finite().min(0),
677
- baselineCostUSD: z4.number().finite().min(0),
678
- 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(),
679
937
  budgetAction: z4.string().min(1),
680
938
  tokensIn: z4.number().int().min(0).optional(),
681
939
  tokensOut: z4.number().int().min(0).optional(),
@@ -766,7 +1024,9 @@ var DiagnosticCodeSchema = z4.enum(DIAGNOSTIC_CODES);
766
1024
  var DiagnosticEventSchema = EventBaseSchema.extend({
767
1025
  type: z4.literal("diagnostic"),
768
1026
  level: DiagnosticLevelSchema,
769
- code: DiagnosticCodeSchema
1027
+ code: DiagnosticCodeSchema,
1028
+ roleID: z4.string().min(1).optional(),
1029
+ passCount: z4.number().int().min(0).optional()
770
1030
  });
771
1031
  var OpenTeamEventSchema = z4.discriminatedUnion("type", [
772
1032
  RouteEventSchema,
@@ -887,9 +1147,9 @@ var CostRecordSchema = z5.object({
887
1147
  routeKind: z5.enum(["local", "frontier", "blocked"]),
888
1148
  selected: ModelRefSchema,
889
1149
  rationale: z5.string(),
890
- estimatedCostUSD: z5.number().finite().min(0),
891
- baselineCostUSD: z5.number().finite().min(0),
892
- 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(),
893
1153
  budgetAction: z5.string().min(1),
894
1154
  tokensIn: z5.number().int().min(0).optional(),
895
1155
  tokensOut: z5.number().int().min(0).optional()
@@ -1148,7 +1408,11 @@ var FAILURE_CAUSE_RULES = [
1148
1408
  "bad request",
1149
1409
  /(?<!\d)400(?!\d)/,
1150
1410
  "unprocessable",
1151
- "malformed"
1411
+ "malformed",
1412
+ "invalid model",
1413
+ "unknown model",
1414
+ "model not found",
1415
+ "model does not exist"
1152
1416
  ],
1153
1417
  "prompt-validation"
1154
1418
  ],
@@ -1594,6 +1858,9 @@ function pushUnique(list, value) {
1594
1858
  list.push(value);
1595
1859
  }
1596
1860
  }
1861
+ function addEstimate(current, value) {
1862
+ return current === null || value === null ? null : current + value;
1863
+ }
1597
1864
  function applyRoute(session, event) {
1598
1865
  session.routeCount += 1;
1599
1866
  if (event.routeKind === "local") {
@@ -1601,8 +1868,8 @@ function applyRoute(session, event) {
1601
1868
  } else if (event.routeKind === "frontier") {
1602
1869
  session.frontierCount += 1;
1603
1870
  }
1604
- session.estimatedCostUSD += event.estimatedCostUSD;
1605
- session.estimatedSavingsUSD += event.estimatedSavingsUSD;
1871
+ session.estimatedCostUSD = addEstimate(session.estimatedCostUSD, event.estimatedCostUSD);
1872
+ session.estimatedSavingsUSD = addEstimate(session.estimatedSavingsUSD, event.estimatedSavingsUSD);
1606
1873
  pushUnique(session.models, `${event.selected.providerID}/${event.selected.modelID}`);
1607
1874
  pushUnique(session.agents, event.agent);
1608
1875
  }
@@ -1668,8 +1935,8 @@ function accumulateTotals(totals, session) {
1668
1935
  totals.routeCount += session.routeCount;
1669
1936
  totals.localCount += session.localCount;
1670
1937
  totals.frontierCount += session.frontierCount;
1671
- totals.estimatedCostUSD += session.estimatedCostUSD;
1672
- totals.estimatedSavingsUSD += session.estimatedSavingsUSD;
1938
+ totals.estimatedCostUSD = addEstimate(totals.estimatedCostUSD, session.estimatedCostUSD);
1939
+ totals.estimatedSavingsUSD = addEstimate(totals.estimatedSavingsUSD, session.estimatedSavingsUSD);
1673
1940
  totals.realCostUSD += session.realCostUSD;
1674
1941
  totals.messageCount += session.messageCount;
1675
1942
  totals.tokensIn += session.tokensIn;
@@ -1972,6 +2239,7 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
1972
2239
  "router",
1973
2240
  [
1974
2241
  "mode",
2242
+ "executionMode",
1975
2243
  "localDefault",
1976
2244
  "trivialPromptMaxChars",
1977
2245
  "frontierPromptMinChars",
@@ -2058,7 +2326,9 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
2058
2326
  ]
2059
2327
  ],
2060
2328
  ["graph.worktrees", ["enabled"]],
2061
- ["orchestrator", ["roles", "maxRetries"]]
2329
+ ["orchestrator", ["roles", "primary", "maxRetries", "maxModelsPerNode"]],
2330
+ ["orchestrator.primary", ["executionMode", "model", "fallbacks"]],
2331
+ ["orchestrator.primary.fallbacks[]", MODEL_REF_KEYS]
2062
2332
  ]);
2063
2333
  function isUnrecognizedKeysIssue(issue) {
2064
2334
  return issue.code === "unrecognized_keys" && Array.isArray(issue.keys);
@@ -2164,6 +2434,211 @@ function formatConfigError(error) {
2164
2434
  });
2165
2435
  }
2166
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
+
2167
2642
  // src/config/load.ts
2168
2643
  function loadOpenTeamConfig(raw) {
2169
2644
  return OpenTeamConfigSchema.parse(migrateConfig(raw ?? {}));
@@ -2184,9 +2659,76 @@ async function readOpenTeamConfigFile(path = ".opencode/openteam.json", storage
2184
2659
  // src/config/persist.ts
2185
2660
  var DEFAULT_CONFIG_PATH = ".opencode/openteam.json";
2186
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
+ }
2187
2694
  function serializeOpenTeamConfig(config) {
2188
2695
  const validated = OpenTeamConfigSchema.parse(config);
2189
- 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)}
2190
2732
  `;
2191
2733
  }
2192
2734
  async function writeOpenTeamConfigFile(config, path = DEFAULT_CONFIG_PATH, storage = createFsStorageProvider(process.cwd())) {
@@ -2323,6 +2865,7 @@ async function loadRuntimeOpenTeamConfig(storage, options, path = DEFAULT_CONFIG
2323
2865
  ].filter((entry) => entry !== undefined);
2324
2866
  const label = mergedSources.length === 0 ? "runtime config" : `runtime config merged from ${mergedSources.join(", ")}`;
2325
2867
  const config = validateConfigInput(label, mergedRaw);
2868
+ const deprecations = detectConfigDeprecations(mergedRaw);
2326
2869
  const source = optionsFound ? "options" : fileFound ? "file" : "default";
2327
2870
  return {
2328
2871
  config,
@@ -2330,6 +2873,7 @@ async function loadRuntimeOpenTeamConfig(storage, options, path = DEFAULT_CONFIG
2330
2873
  fileFound,
2331
2874
  optionsFound,
2332
2875
  overlayFound,
2876
+ deprecations,
2333
2877
  searchedPaths: overlayPath === null ? [path] : [path, overlayPath]
2334
2878
  };
2335
2879
  }
@@ -2449,6 +2993,10 @@ function scrubSecretStrings(value, secrets) {
2449
2993
  };
2450
2994
  return walk(value);
2451
2995
  }
2996
+ function withoutDeprecatedPrivacyMode(config) {
2997
+ const { privacyMode: _privacyMode, ...next } = config;
2998
+ return next;
2999
+ }
2452
3000
  var DEFAULT_CONFIG_AUDIT_PATH = ".opencode/openteam-local/config-audit.jsonl";
2453
3001
  function editableSurface() {
2454
3002
  return [
@@ -2486,7 +3034,7 @@ function createConfigEditor(ports) {
2486
3034
  editableSurface,
2487
3035
  async read() {
2488
3036
  const resolution = await loadRuntimeOpenTeamConfig(ports.storage, undefined, basePath, overlayPath);
2489
- const scrubbed = scrubSecretStrings(resolution.config, secretValues());
3037
+ const scrubbed = withoutDeprecatedPrivacyMode(scrubSecretStrings(resolution.config, secretValues()));
2490
3038
  return {
2491
3039
  config: scrubbed,
2492
3040
  reload: {
@@ -2508,6 +3056,7 @@ function createConfigEditor(ports) {
2508
3056
  let submitted;
2509
3057
  try {
2510
3058
  submitted = OpenTeamConfigObjectSchema.parse(submittedRaw);
3059
+ assertEffectiveConfigModelDomains(submitted);
2511
3060
  } catch (error) {
2512
3061
  return {
2513
3062
  status: 400,
@@ -2573,7 +3122,6 @@ var RESTART_REQUIRED_KEYS = [
2573
3122
  "baseline",
2574
3123
  "router",
2575
3124
  "local",
2576
- "privacyMode",
2577
3125
  "graph",
2578
3126
  "orchestrator",
2579
3127
  "memory",
@@ -2581,6 +3129,110 @@ var RESTART_REQUIRED_KEYS = [
2581
3129
  "console.port"
2582
3130
  ];
2583
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
+
2584
3236
  // src/web/graphRoute.ts
2585
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.';
2586
3238
  var GRAPH_DISABLED_ENVELOPE = {
@@ -2990,10 +3642,10 @@ function escapeHtml(value) {
2990
3642
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2991
3643
  }
2992
3644
  function usd(value) {
2993
- return `$${value.toFixed(5)}`;
3645
+ return value === null ? routingMessages.unknownCost : `$${value.toFixed(5)}`;
2994
3646
  }
2995
3647
  function pct(value) {
2996
- return `${value.toFixed(2)}%`;
3648
+ return value === null ? routingMessages.unknownCost : `${value.toFixed(2)}%`;
2997
3649
  }
2998
3650
  function ms(value) {
2999
3651
  return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
@@ -3689,6 +4341,42 @@ var OVERLAY_PATH = ".opencode/openteam-local/config.local.json";
3689
4341
  function codeList(keys) {
3690
4342
  return keys.map((key) => `<code>${escapeHtml(key)}</code>`).join(", ");
3691
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
+ }
3692
4380
  function renderConfigReloadTable(read) {
3693
4381
  const hot = read.reload.hotReloadable.length > 0 ? codeList(read.reload.hotReloadable) : '<span class="muted">none</span>';
3694
4382
  const restart = read.reload.restartRequired.length > 0 ? codeList(read.reload.restartRequired) : '<span class="muted">none</span>';
@@ -3708,6 +4396,7 @@ function renderConfigEditorForm(read) {
3708
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>` : "";
3709
4397
  return [
3710
4398
  '<form class="config-form" data-config-form>',
4399
+ renderExecutionPolicyOverview(read.config),
3711
4400
  renderConfigReloadTable(read),
3712
4401
  overlayNote,
3713
4402
  secretsNote,
@@ -3906,6 +4595,9 @@ function emptyTierSummary() {
3906
4595
  hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
3907
4596
  };
3908
4597
  }
4598
+ function addEstimate2(current, value) {
4599
+ return current === null || value === null ? null : current + value;
4600
+ }
3909
4601
  function summarizeCostRecords(records) {
3910
4602
  const report = {
3911
4603
  count: 0,
@@ -3920,30 +4612,33 @@ function summarizeCostRecords(records) {
3920
4612
  tokensOut: 0,
3921
4613
  byTier: emptyTierSummary()
3922
4614
  };
3923
- for (const record of records) {
4615
+ for (const record2 of records) {
3924
4616
  report.count += 1;
3925
- if (record.routeKind === "local") {
4617
+ if (record2.routeKind === "local") {
3926
4618
  report.localCount += 1;
3927
- } else if (record.routeKind === "blocked") {
4619
+ } else if (record2.routeKind === "blocked") {
3928
4620
  report.blockedCount += 1;
3929
4621
  } else {
3930
4622
  report.frontierCount += 1;
3931
4623
  }
3932
- report.totalEstimatedUSD += record.estimatedCostUSD;
3933
- report.totalBaselineUSD += record.baselineCostUSD;
3934
- report.totalSavingsUSD += record.estimatedSavingsUSD;
3935
- report.tokensIn += record.tokensIn ?? 0;
3936
- report.tokensOut += record.tokensOut ?? 0;
3937
- 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];
3938
4630
  tier.count += 1;
3939
- tier.estimatedUSD += record.estimatedCostUSD;
3940
- tier.savingsUSD += record.estimatedSavingsUSD;
4631
+ tier.estimatedUSD = addEstimate2(tier.estimatedUSD, record2.estimatedCostUSD);
4632
+ tier.savingsUSD = addEstimate2(tier.savingsUSD, record2.estimatedSavingsUSD);
3941
4633
  }
3942
- 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;
3943
4635
  return report;
3944
4636
  }
3945
4637
  function usd2(value) {
3946
- 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)}%`;
3947
4642
  }
3948
4643
  function renderReport(report) {
3949
4644
  if (report.count === 0) {
@@ -3954,7 +4649,7 @@ function renderReport(report) {
3954
4649
  ` decisions: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount} · blocked ${report.blockedCount})`,
3955
4650
  ` real cost: ${usd2(report.totalEstimatedUSD)}`,
3956
4651
  ` baseline: ${usd2(report.totalBaselineUSD)}`,
3957
- ` savings: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
4652
+ ` savings: ${usd2(report.totalSavingsUSD)} (${pct2(report.savingsPct)})`,
3958
4653
  ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
3959
4654
  " by tier:"
3960
4655
  ];
@@ -4039,16 +4734,16 @@ var DEFAULT_RECENT_ROUTES = 50;
4039
4734
  var DEFAULT_ACTIVITY_LIMIT = 100;
4040
4735
  var DEFAULT_DECISIONS_LIMIT = 100;
4041
4736
  var DEFAULT_MEETINGS_LIMIT = 50;
4042
- function toRouteView(record) {
4737
+ function toRouteView(record2) {
4043
4738
  return {
4044
- ts: record.ts,
4045
- tier: record.tier,
4046
- routeKind: record.routeKind,
4047
- model: `${record.selected.providerID}/${record.selected.modelID}`,
4048
- promptHash: record.promptHash,
4049
- estimatedCostUSD: record.estimatedCostUSD,
4050
- estimatedSavingsUSD: record.estimatedSavingsUSD,
4051
- 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
4052
4747
  };
4053
4748
  }
4054
4749
  function isRoute(event) {
@@ -4101,7 +4796,7 @@ function activityFeed(aggregate, limit) {
4101
4796
  function buildSessionTabs(aggregate, costRecords, nowMs, limits, options) {
4102
4797
  return aggregate.sessions.map((summary) => {
4103
4798
  const id = summary.sessionID;
4104
- const routes = costRecords.filter((record) => record.sessionID === id);
4799
+ const routes = costRecords.filter((record2) => record2.sessionID === id);
4105
4800
  const decisions = aggregate.decisions.filter((decision) => decision.sessionID === id).slice(0, Math.max(0, limits.decisionsLimit));
4106
4801
  const meetings = aggregate.meetings.filter((meeting) => meeting.sessionID === id).slice(0, Math.max(0, limits.meetingsLimit));
4107
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);
@@ -4279,9 +4974,9 @@ async function handlePermission(request, response, sessionID, deps) {
4279
4974
  return;
4280
4975
  }
4281
4976
  const body = await readJsonBody2(request, 4096);
4282
- const record = typeof body === "object" && body !== null ? body : {};
4283
- const permissionID = record.permissionID;
4284
- const responseValue = record.response;
4977
+ const record2 = typeof body === "object" && body !== null ? body : {};
4978
+ const permissionID = record2.permissionID;
4979
+ const responseValue = record2.response;
4285
4980
  if (typeof permissionID !== "string" || permissionID.length === 0 || responseValue !== "once" && responseValue !== "always" && responseValue !== "reject") {
4286
4981
  response.writeHead(400, JSON_HEADERS3).end(JSON.stringify({ error: "invalid permission" }));
4287
4982
  return;
@@ -4441,8 +5136,8 @@ async function handleWrite(request, response, op, deps) {
4441
5136
  return;
4442
5137
  }
4443
5138
  const body = await readJsonBody3(request, 8 * 1024 * 1024);
4444
- const record = typeof body === "object" && body !== null ? body : undefined;
4445
- const rawPath = record?.path;
5139
+ const record2 = typeof body === "object" && body !== null ? body : undefined;
5140
+ const rawPath = record2?.path;
4446
5141
  if (typeof rawPath !== "string") {
4447
5142
  sendJson(response, 400, { error: "invalid path" });
4448
5143
  return;
@@ -4462,7 +5157,7 @@ async function handleWrite(request, response, op, deps) {
4462
5157
  return;
4463
5158
  }
4464
5159
  if (op === "write" || op === "append") {
4465
- const content = record?.content;
5160
+ const content = record2?.content;
4466
5161
  if (typeof content !== "string") {
4467
5162
  sendJson(response, 400, { error: "invalid content" });
4468
5163
  return;
@@ -4840,7 +5535,7 @@ function classifyAgent(info, context) {
4840
5535
  name: info.name,
4841
5536
  mode: info.mode,
4842
5537
  kind: "inherited",
4843
- subscription: "inherits the opencode.json default (not set) · openteam routes local-first per message"
5538
+ subscription: executionPolicyMessages.inheritedDefaultUnset
4844
5539
  };
4845
5540
  }
4846
5541
  const { subscription } = subscriptionLabel(context.defaultModel, context.localProviders);
@@ -4849,7 +5544,7 @@ function classifyAgent(info, context) {
4849
5544
  mode: info.mode,
4850
5545
  kind: "inherited",
4851
5546
  model: context.defaultModel,
4852
- subscription: `inherits default → ${subscription} · openteam routes local-first per message`
5547
+ subscription: executionPolicyMessages.inheritedDefault(subscription)
4853
5548
  };
4854
5549
  }
4855
5550
  function modeRank(mode) {
@@ -4870,6 +5565,51 @@ function classifyAgents(files, context) {
4870
5565
  function modelText(classification) {
4871
5566
  return classification.model === undefined ? "" : ` (${classification.model.providerID}/${classification.model.modelID})`;
4872
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
+ }
4873
5613
  function renderAgentList(classifications, context) {
4874
5614
  const lines = ["openteam agents — LLM per agent:", ""];
4875
5615
  if (classifications.length === 0) {
@@ -4896,6 +5636,15 @@ function renderAgentList(classifications, context) {
4896
5636
  lines.push("opencode.json default: (not set)");
4897
5637
  }
4898
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
+ }
4899
5648
  if (context.opencodeConfigError !== undefined) {
4900
5649
  lines.push("");
4901
5650
  lines.push(`⚠ opencode config parse error: ${context.opencodeConfigError}`);
@@ -4918,22 +5667,22 @@ var KNOWN_FRONTIER_PROVIDER_IDS = new Set([
4918
5667
  "openrouter"
4919
5668
  ]);
4920
5669
  var VIRTUAL_ROUTER_PROVIDER_ID = "openteam-router";
4921
- function isRecord(value) {
5670
+ function isRecord2(value) {
4922
5671
  return typeof value === "object" && value !== null && !Array.isArray(value);
4923
5672
  }
4924
5673
  function parseOpencodeProviders(opencodeConfig) {
4925
5674
  const result = new Map;
4926
5675
  const provider = opencodeConfig?.provider;
4927
- if (!isRecord(provider)) {
5676
+ if (!isRecord2(provider)) {
4928
5677
  return result;
4929
5678
  }
4930
5679
  for (const [id, value] of Object.entries(provider)) {
4931
- if (!isRecord(value)) {
5680
+ if (!isRecord2(value)) {
4932
5681
  result.set(id, {});
4933
5682
  continue;
4934
5683
  }
4935
5684
  const models = value.models;
4936
- result.set(id, isRecord(models) ? { models: new Set(Object.keys(models)) } : {});
5685
+ result.set(id, isRecord2(models) ? { models: new Set(Object.keys(models)) } : {});
4937
5686
  }
4938
5687
  return result;
4939
5688
  }
@@ -5675,8 +6424,8 @@ function canonical(value) {
5675
6424
  if (value === null || typeof value !== "object") {
5676
6425
  return JSON.stringify(value);
5677
6426
  }
5678
- const record = value;
5679
- 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])}`);
5680
6429
  return `{${entries.join(",")}}`;
5681
6430
  }
5682
6431
  function frameDigest(prev, event) {
@@ -5937,7 +6686,7 @@ function createFsGraphJournal(root) {
5937
6686
 
5938
6687
  // src/telemetry/graphProjection.ts
5939
6688
  function projectGraphEvent(event) {
5940
- const record = {
6689
+ const record2 = {
5941
6690
  v: event.v,
5942
6691
  seq: event.seq,
5943
6692
  runID: event.runID,
@@ -5950,8 +6699,8 @@ function projectGraphEvent(event) {
5950
6699
  ..."specDigest" in event ? { specDigest: event.specDigest } : {},
5951
6700
  ..."artifact" in event ? { artifact: redactedRef(event.artifact) } : {}
5952
6701
  };
5953
- assertReferenceOnly(record);
5954
- return record;
6702
+ assertReferenceOnly(record2);
6703
+ return record2;
5955
6704
  }
5956
6705
 
5957
6706
  // src/telemetry/graphRuntimeHealth.ts
@@ -6042,18 +6791,18 @@ function indexTelemetry(telemetry) {
6042
6791
  const lastError = new Map;
6043
6792
  const lastFailure = new Map;
6044
6793
  const lastArtifact = new Map;
6045
- for (const record of telemetry) {
6046
- if (record.nodeID === undefined) {
6794
+ for (const record2 of telemetry) {
6795
+ if (record2.nodeID === undefined) {
6047
6796
  continue;
6048
6797
  }
6049
- if (record.type === "node.failed" && record.errorClass !== undefined) {
6050
- lastError.set(record.nodeID, record.errorClass);
6798
+ if (record2.type === "node.failed" && record2.errorClass !== undefined) {
6799
+ lastError.set(record2.nodeID, record2.errorClass);
6051
6800
  }
6052
- if (record.type === "node.failed" && record.failureClass !== undefined) {
6053
- lastFailure.set(record.nodeID, record.failureClass);
6801
+ if (record2.type === "node.failed" && record2.failureClass !== undefined) {
6802
+ lastFailure.set(record2.nodeID, record2.failureClass);
6054
6803
  }
6055
- if (record.type === "node.succeeded" && record.artifact !== undefined) {
6056
- lastArtifact.set(record.nodeID, record.artifact);
6804
+ if (record2.type === "node.succeeded" && record2.artifact !== undefined) {
6805
+ lastArtifact.set(record2.nodeID, record2.artifact);
6057
6806
  }
6058
6807
  }
6059
6808
  return { lastError, lastFailure, lastArtifact };
@@ -6244,8 +6993,8 @@ async function runConsoleServe(deps, options) {
6244
6993
  load: () => Promise.resolve(deps.loadConfig(deps.configPath)),
6245
6994
  configPath: deps.configPath
6246
6995
  });
6247
- const auditSink = async (record) => {
6248
- 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)}
6249
6998
  `);
6250
6999
  };
6251
7000
  const configEditor = createConfigEditor({
@@ -6338,11 +7087,6 @@ async function runConsoleServe(deps, options) {
6338
7087
  // src/cli/graphOperator.ts
6339
7088
  import { join as join8 } from "node:path";
6340
7089
 
6341
- // src/graph/privacyPolicy.ts
6342
- function frontierAllowed(privacyClass, mode) {
6343
- return !(privacyClass === "sensitive" && mode === "forceLocalOnSensitive");
6344
- }
6345
-
6346
7090
  // src/graph/langgraph/reducer.ts
6347
7091
  function reduceAdapterState(current, update) {
6348
7092
  const route = update.route !== undefined ? update.route : current.route;
@@ -6357,21 +7101,22 @@ function reduceAdapterState(current, update) {
6357
7101
 
6358
7102
  // src/graph/langgraph/adapter.ts
6359
7103
  var GRAPH_END = "__end__";
6360
- function resolveNodeRoute(privacyClass, mode, preferred) {
6361
- return preferred === "frontier" && !frontierAllowed(privacyClass, mode) ? "local" : preferred;
7104
+ function resolveNodeRoute(preferredOrPrivacyClass, _legacyMode, legacyPreferred) {
7105
+ return legacyPreferred ?? preferredOrPrivacyClass;
6362
7106
  }
6363
7107
  var routeNode = (state) => ({
6364
- route: resolveNodeRoute(state.input.privacyClass, state.input.mode, state.input.preferredRoute),
7108
+ route: resolveNodeRoute(state.input.preferredRoute),
6365
7109
  visited: ["route"]
6366
7110
  });
6367
7111
  var dispatchNode = async (state, deps) => {
6368
- const route = state.route ?? resolveNodeRoute(state.input.privacyClass, state.input.mode, state.input.preferredRoute);
6369
- const receipt = await deps.executor.execute({
7112
+ const route = state.route ?? resolveNodeRoute(state.input.preferredRoute);
7113
+ const request = {
6370
7114
  nodeID: state.input.nodeID,
6371
7115
  route,
6372
- privacyClass: state.input.privacyClass,
6373
- mode: state.input.mode
6374
- });
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);
6375
7120
  return { receipt, visited: ["dispatch"] };
6376
7121
  };
6377
7122
  function buildAdapterDefinition() {
@@ -6436,7 +7181,7 @@ function selectCheckpointer(location) {
6436
7181
  function resolveGraphEngine(config) {
6437
7182
  return config.graph?.engine ?? "builtin";
6438
7183
  }
6439
- function createLangGraphEffects(nodeExecutor) {
7184
+ function createLangGraphEffects(nodeExecutor, defaultRoute = "local") {
6440
7185
  const definition = buildAdapterDefinition();
6441
7186
  return {
6442
7187
  async execute(request) {
@@ -6445,11 +7190,9 @@ function createLangGraphEffects(nodeExecutor) {
6445
7190
  }
6446
7191
  const input = {
6447
7192
  nodeID: request.node.id,
6448
- privacyClass: "internal",
6449
- mode: "forceLocalOnSensitive",
6450
- preferredRoute: "local"
7193
+ preferredRoute: request.routeKind ?? defaultRoute
6451
7194
  };
6452
- const resolvedRoute = resolveNodeRoute(input.privacyClass, input.mode, input.preferredRoute);
7195
+ const resolvedRoute = resolveNodeRoute(input.preferredRoute);
6453
7196
  let state;
6454
7197
  try {
6455
7198
  state = await invokeStateGraph(definition, input, {
@@ -6642,8 +7385,8 @@ function createWorkspaceLease(root, registry = createRunRegistry(root)) {
6642
7385
  return text === undefined ? undefined : parse2(text);
6643
7386
  };
6644
7387
  const grant = async (runID, holder, token) => {
6645
- const record = { runID, holder, token, released: false };
6646
- writeDurable(leasePath(runID), JSON.stringify(record));
7388
+ const record2 = { runID, holder, token, released: false };
7389
+ writeDurable(leasePath(runID), JSON.stringify(record2));
6647
7390
  await registry.markActive(runID);
6648
7391
  return { runID, holder, token };
6649
7392
  };
@@ -7374,19 +8117,19 @@ function createWorktreeAdapter(config) {
7374
8117
  const base = `${canonicalizePath(worktreesDir)}/`;
7375
8118
  return canonicalizePath(worktreePath).startsWith(base);
7376
8119
  };
7377
- const toInfo = (record) => ({
7378
- runID: runIDFromPath(record.path),
7379
- path: record.path,
7380
- head: record.head,
7381
- detached: record.detached,
7382
- 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
7383
8126
  });
7384
8127
  const list = async () => {
7385
8128
  const output = await runGit(["worktree", "list", "--porcelain"]);
7386
8129
  if (output.exitCode !== 0) {
7387
8130
  throw new WorktreeError("git-failed", `worktree list: ${output.stderr.trim() || output.exitCode}`);
7388
8131
  }
7389
- return parsePorcelain(output.stdout).filter((record) => isOurs(record.path)).map(toInfo);
8132
+ return parsePorcelain(output.stdout).filter((record2) => isOurs(record2.path)).map(toInfo);
7390
8133
  };
7391
8134
  const findExisting = async (expected) => (await list()).find((info) => samePath(info.path, expected));
7392
8135
  const addWorktree = async (runID, expected) => runGit([
@@ -7926,7 +8669,7 @@ function buildOrchestratorAgent(model, options = {}) {
7926
8669
  ];
7927
8670
  const frontmatter = [
7928
8671
  "---",
7929
- '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."',
7930
8673
  "mode: primary",
7931
8674
  `model: ${modelSpec}`,
7932
8675
  "temperature: 0.2",
@@ -8048,10 +8791,9 @@ function buildOrchestratorAgent(model, options = {}) {
8048
8791
  " global one, so an `ask`/`deny` in the subagent reintroduces the",
8049
8792
  " prompts. If YOLO is disabled, use minimal permissions (leave `bash`",
8050
8793
  " in ask mode).",
8051
- " - Leave `model` unset so it inherits the default and openteam routes it",
8052
- " local-first, or pin a local model for mechanical/bulk roles. Reserve",
8053
- " frontier only for architecture, security or ambiguous debugging:",
8054
- " 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.",
8055
8797
  " - A focused, actionable role prompt.",
8056
8798
  "4. Register the cast with the `openteam-register-cast` tool and **hand out",
8057
8799
  " the tasks** to the newly created team with the `task` tool.",
@@ -8065,22 +8807,20 @@ function buildOrchestratorAgent(model, options = {}) {
8065
8807
  "",
8066
8808
  "- **scribe** — the team's silent memory. Records decisions and",
8067
8809
  " learnings in a shared log (`.opencode/openteam/decisions.md`) without",
8068
- " executing code changes. **Local** model; read-only permissions plus",
8069
- " 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",
8070
8812
  " per line as a Markdown list item, already **redacted** (no prompts or",
8071
- " sensitive data):",
8813
+ " credentials):",
8072
8814
  " `- YYYY-MM-DD [agent] Short summary of the decision #tag1 #tag2`",
8073
8815
  " The date, the `[agent]` and the `#tags` are optional; the summary is",
8074
8816
  " required.",
8075
8817
  "- **ralph** — automation and triage. Monitors pending work",
8076
8818
  " (issues/tasks), prioritizes it, coordinates execution by delegating to the",
8077
8819
  " specialists and **escalates to the human** on blockers, risks or approvals.",
8078
- "- **guardian** — security and privacy. **Detects and masks sensitive",
8079
- " data** (PII, secrets, API keys, tokens, credentials) before it",
8080
- " leaves the repo or goes to frontier, and does security review. Runs",
8081
- " **always locally** and respects `privacyMode`/`forceLocalOnSensitive`: never",
8082
- " lets sensitive data through to frontier. Reviews the inputs and outputs of the",
8083
- " 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.",
8084
8824
  "",
8085
8825
  "## Work planning and parallelization",
8086
8826
  "",
@@ -8100,10 +8840,9 @@ function buildOrchestratorAgent(model, options = {}) {
8100
8840
  "4. **Do not parallelize work that collides** on the same files or",
8101
8841
  " state: those are implicit dependencies; split it into separate waves to",
8102
8842
  " avoid write conflicts.",
8103
- "5. Distribute by **capability**: mechanical/bulk roles on local, and reserve",
8104
- " frontier for the hard parts; parallelism does not change openteam's routing.",
8105
- "6. The `guardian` is still the pre-frontier gate **even** in",
8106
- " 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.",
8107
8846
  "",
8108
8847
  "Briefly explain your plan to the user (what runs in parallel and what in series, and",
8109
8848
  "why) before or while you launch the tasks.",
@@ -8197,8 +8936,8 @@ function buildOrchestratorAgent(model, options = {}) {
8197
8936
  "- Create subagents **exactly when the work calls for it**, never “just in case”.",
8198
8937
  ` Review \`.opencode/agent/\` and the \`${OPENTEAM_ROSTER_FILENAME}\`, and reuse the`,
8199
8938
  " existing ones before creating a new one.",
8200
- "- Respect openteam's routing and privacy; the `guardian` is the last",
8201
- " 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.",
8202
8941
  "- Summarize for the user which universe you chose, which team you created and why.",
8203
8942
  "",
8204
8943
  "## Limits",
@@ -8267,8 +9006,8 @@ function buildOpenteamCommands() {
8267
9006
  return [
8268
9007
  file("openteam", buildOpenteamCommand()),
8269
9008
  file("openteam-baseline", buildBaselineCommand()),
8270
- file("openteam-doctor", renderCommand("openteam · diagnostics for local runtimes and configuration", fixedActionBody("doctor"))),
8271
- 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"))),
8272
9011
  file("openteam-console", renderCommand("openteam · status and URL of the multi-session web Console", [
8273
9012
  "> ℹ️ The Console interface may still evolve between releases.",
8274
9013
  "",
@@ -8485,7 +9224,7 @@ var CURATED_FRONTIER_PROFILES = [
8485
9224
  ];
8486
9225
 
8487
9226
  // src/capabilities/normalize.ts
8488
- function isRecord2(value) {
9227
+ function isRecord3(value) {
8489
9228
  return typeof value === "object" && value !== null && !Array.isArray(value);
8490
9229
  }
8491
9230
  function stringValue(value) {
@@ -8506,7 +9245,7 @@ function boolValue(value) {
8506
9245
  }
8507
9246
  function nestedRecord(value, key) {
8508
9247
  const nested = value[key];
8509
- return isRecord2(nested) ? nested : undefined;
9248
+ return isRecord3(nested) ? nested : undefined;
8510
9249
  }
8511
9250
  function nestedBoolean(value, firstKey, secondKey) {
8512
9251
  const nested = nestedRecord(value, firstKey);
@@ -8519,7 +9258,7 @@ function includesImage(value) {
8519
9258
  if (Array.isArray(value)) {
8520
9259
  return value.some(includesImage);
8521
9260
  }
8522
- if (isRecord2(value)) {
9261
+ if (isRecord3(value)) {
8523
9262
  return Object.values(value).some(includesImage);
8524
9263
  }
8525
9264
  return false;
@@ -8538,7 +9277,7 @@ function tierFromModelName(modelID, reasoning) {
8538
9277
  if (typeof reasoning === "number") {
8539
9278
  return Math.max(0, Math.min(5, Math.trunc(reasoning)));
8540
9279
  }
8541
- if (reasoning === true || isRecord2(reasoning)) {
9280
+ if (reasoning === true || isRecord3(reasoning)) {
8542
9281
  return normalized.includes("flash") || normalized.includes("mini") ? 3 : 4;
8543
9282
  }
8544
9283
  if (normalized.includes("opus") || normalized.includes("codex")) {
@@ -8568,31 +9307,44 @@ function codeTierFromModelName(modelID) {
8568
9307
  function availability(value) {
8569
9308
  return value === "degraded" || value === "unavailable" ? value : "available";
8570
9309
  }
9310
+ function modelAvailability(availabilityValue, status) {
9311
+ if (availabilityValue === "degraded" || status === "deprecated") {
9312
+ return "degraded";
9313
+ }
9314
+ return availability(availabilityValue);
9315
+ }
8571
9316
  function modelEntriesFromProvider(providerID, provider) {
8572
9317
  const models = provider.models;
8573
9318
  if (Array.isArray(models)) {
8574
- return models.filter(isRecord2).map((model) => ({
8575
- providerID,
9319
+ return models.filter(isRecord3).map((model) => ({
9320
+ providerID: stringValue(model.providerID) ?? providerID,
8576
9321
  modelID: stringValue(model.id) ?? stringValue(model.modelID) ?? "",
8577
9322
  value: model
8578
9323
  })).filter((entry) => entry.modelID.length > 0);
8579
9324
  }
8580
- if (isRecord2(models)) {
8581
- return Object.entries(models).filter(([, model]) => isRecord2(model)).map(([modelID, model]) => ({
8582
- providerID,
8583
- modelID,
8584
- value: model
8585
- }));
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
+ });
8586
9338
  }
8587
9339
  return [];
8588
9340
  }
8589
9341
  function catalogEntries(raw) {
8590
- if (!isRecord2(raw)) {
9342
+ if (!isRecord3(raw)) {
8591
9343
  return [];
8592
9344
  }
8593
- const providers = isRecord2(raw.providers) ? raw.providers : raw;
9345
+ const providers = isRecord3(raw.providers) ? raw.providers : raw;
8594
9346
  return Object.entries(providers).flatMap(([providerKey, provider]) => {
8595
- if (!isRecord2(provider)) {
9347
+ if (!isRecord3(provider)) {
8596
9348
  return [];
8597
9349
  }
8598
9350
  const providerID = stringValue(provider.id) ?? stringValue(provider.providerID) ?? providerKey;
@@ -8601,6 +9353,9 @@ function catalogEntries(raw) {
8601
9353
  }
8602
9354
  function normalizeEntry(entry) {
8603
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");
8604
9359
  const contextWindow = numberValue(entry.value.contextWindow) ?? numberValue(entry.value.context_window) ?? (limit === undefined ? undefined : numberValue(limit.context));
8605
9360
  const maxOutputTokens = numberValue(entry.value.maxOutputTokens) ?? numberValue(entry.value.max_output_tokens) ?? (limit === undefined ? undefined : numberValue(limit.output));
8606
9361
  const costPer1M = costPair(entry.value);
@@ -8612,12 +9367,12 @@ function normalizeEntry(entry) {
8612
9367
  kind: "frontier",
8613
9368
  contextWindow,
8614
9369
  maxOutputTokens,
8615
- supportsToolCalling: boolValue(entry.value.tool_call) || boolValue(entry.value.toolCall) || nestedBoolean(entry.value, "features", "tool_call"),
8616
- supportsVision: boolValue(entry.value.vision) || includesImage(entry.value.modalities) || includesImage(entry.value.input_modalities),
8617
- reasoningTier: tierFromModelName(entry.modelID, entry.value.reasoning),
8618
- 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),
8619
9374
  costPer1M,
8620
- availability: availability(entry.value.availability)
9375
+ availability: modelAvailability(entry.value.availability, entry.value.status)
8621
9376
  };
8622
9377
  const parsed = ModelCapabilityProfileSchema.safeParse(profile);
8623
9378
  return parsed.success ? parsed.data : undefined;
@@ -9454,123 +10209,6 @@ function isGateApplicable(mode) {
9454
10209
  return mode === "active";
9455
10210
  }
9456
10211
 
9457
- // src/messages/commands.ts
9458
- var clearCacheMessages = {
9459
- header: "openteam clear-cache — frozen plugin cache entries:",
9460
- columns: {
9461
- specDir: "spec dir",
9462
- pinned: "spec-pinned",
9463
- installed: "installed",
9464
- mtime: "mtime"
9465
- },
9466
- reparseSkipSuffix: " [SKIP — reparse point]",
9467
- deletedLabel: "deleted.",
9468
- lockedLabel: (message) => `[LOCKED] ${message}`,
9469
- pathOutsideWarning: (specDir, absolutePath) => ` ⚠ ${specDir}: path outside cacheRoot (${absolutePath}), skipped.`,
9470
- processed: (count) => `${count} entry(ies) processed.`,
9471
- found: (count) => `${count} entry(ies) found. Use --delete to remove them.`
9472
- };
9473
- var baselineMessages = {
9474
- effectiveAuto: "cheapest-capable (auto)",
9475
- pinnedSuffix: (ref) => `${ref} (pinned)`,
9476
- summary: (params) => [
9477
- "openteam baseline:",
9478
- ` mode: ${params.mode}`,
9479
- ` pinned: ${params.pinned}`,
9480
- ` hardDefault: ${params.hardDefault}`,
9481
- ` effective: ${params.effective}`
9482
- ],
9483
- invalidModel: (input) => `Invalid model "${input}". Use the provider/model format, e.g. anthropic/claude-sonnet-4-5.`,
9484
- pinnedTo: (ref) => `Baseline pinned to ${ref} (pinned mode).`,
9485
- autoMode: "Baseline set to auto mode (cheapest-capable)."
9486
- };
9487
- var localMessages = {
9488
- help: {
9489
- status: " openteam local status Show the routing mode (local-first / local-only / frontier-only)",
9490
- off: " openteam local off Use frontier models only (no local runtime)",
9491
- only: " openteam local only Use local models only (never frontier)",
9492
- on: " openteam local on Re-enable local-first routing"
9493
- },
9494
- noRuntimes: "none",
9495
- modeFrontierOnly: "frontier-only (frontierOnly)",
9496
- modeLocalOnly: "local-only (localOnly)",
9497
- modeLocalFirst: "local-first",
9498
- summary: (params) => [
9499
- "openteam local:",
9500
- ` mode: ${params.mode}`,
9501
- ` frontierOnly: ${params.frontierOnly}`,
9502
- ` localOnly: ${params.localOnly}`,
9503
- ` privacy: ${params.privacy}`,
9504
- ` runtimes: ${params.runtimes}`
9505
- ],
9506
- frontierOnlyNoChange: "No change: already in frontier-only mode.",
9507
- privacyAdjustedNote: " Privacy adjusted to 'consentBeforeFrontier' (forceLocalOnSensitive requires a local runtime).",
9508
- frontierOnlyEnabled: "Frontier-only mode enabled (frontierOnly=true, localOnly=false).",
9509
- 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.",
9510
- localOnlyNoChange: "No change: already in local-only mode.",
9511
- localOnlyEnabled: "Local-only mode enabled (localOnly=true, frontierOnly=false).",
9512
- localFirstNoChange: "No change: local-first routing is already active.",
9513
- localFirstReEnabled: "Local-first routing re-enabled (frontierOnly=false, localOnly=false). Make sure a local runtime is reachable.",
9514
- unknownSubcommand: (subcommand, help) => `Unknown local subcommand: ${subcommand}
9515
-
9516
- ${help}`
9517
- };
9518
- var frontierCatalogMessages = {
9519
- free: "free",
9520
- costPerMillion: (inputUSD, outputUSD) => `$${inputUSD}/$${outputUSD} per 1M`
9521
- };
9522
- var migrateMessages = {
9523
- header: "openteam migrate:",
9524
- nothingToMigrate: " nothing to migrate — the P15b layout is already in place.",
9525
- counts: (params) => [
9526
- ` moved: ${params.moved} file(s)`,
9527
- ` deduped: ${params.deduped} file(s) (destination already identical)`,
9528
- ` conflicts: ${params.conflicts} file(s)`
9529
- ],
9530
- relocatedHeader: " relocated:",
9531
- relocatedEntry: (from, to, deduped) => ` · ${from} → ${to}${deduped ? " (deduped)" : ""}`,
9532
- conflictsHeader: " ⚠ left in place (destination exists with different content — no data lost):",
9533
- conflictEntry: (from, to) => ` · ${from} → ${to}`,
9534
- manualWorktreesHeader: " ⚠ legacy git worktree(s) need a manual move (run `git worktree move`):",
9535
- manualWorktreeEntry: (label, from, to) => ` · ${label}: ${from} → ${to}`,
9536
- success: " ✓ machine-local runtime output relocated under .opencode/openteam-local/."
9537
- };
9538
- var rolesInitMessages = {
9539
- header: "openteam roles init:",
9540
- title: "openteam roles init",
9541
- noRoster: (path) => ` ✗ no roster at ${path}.`,
9542
- noRosterRemedy: " Cast the team first: run `openteam setup`, then ask the orchestrator to register the cast.",
9543
- unparseableRoster: (path, error) => ` ✗ ${path} is present but unparseable: ${error}`,
9544
- unparseableRosterRemedy: " Run `openteam doctor` for the remedy, then re-run this command.",
9545
- allProfiled: (count) => ` ✓ ${count} roster role(s) checked; no unprofiled roles — every role resolves a routing profile.`,
9546
- whyTitle: "Why you are being asked",
9547
- why: (count) => [
9548
- `${count} roster role(s) resolve no routing profile.`,
9549
- "Until a profile exists they are frontier-eligible: their work can escalate",
9550
- "to a frontier model. A 'local only' note in the roster prose does not",
9551
- "prevent this — prose outside the JSON fence is never read."
9552
- ].join(`
9553
- `),
9554
- policyQuestion: (roleID, agentName) => `Routing policy for "${roleID}" (${agentName})`,
9555
- localOnlyLabel: (roleID, agentName) => `Local only — ${roleID} (${agentName}) never leaves this machine`,
9556
- localOnlyHint: "sets requiresLocalRuntime; the role fails closed when no local runtime is reachable",
9557
- frontierOkLabel: "Local first, frontier allowed",
9558
- frontierOkHint: "records today's implicit behaviour: cheapest-capable frontier when the task warrants it",
9559
- skipLabel: "Skip for now",
9560
- skipHint: "leaves the role unprofiled, so it stays frontier-eligible and doctor keeps reporting it",
9561
- runtimeQuestion: (roleID) => `Preferred local runtime(s) for "${roleID}" (in failover order)`,
9562
- runtimeHint: "machine-local — written to the git-ignored overlay",
9563
- cancelled: (reason) => ` ✗ cancelled (${reason}); nothing was written.`,
9564
- invalidResult: (reason) => ` ✗ refusing to write: the resulting config is invalid — ${reason}`,
9565
- nothingWritten: (skipped) => ` · skipped ${skipped}; nothing written.`,
9566
- nothingToWriteOutro: "Nothing to write.",
9567
- pinnedLocal: (roleIDs, path) => ` ✓ pinned local-only: ${roleIDs} → ${path} (tracked, travels with the roster)`,
9568
- frontierAllowed: (roleIDs, path) => ` ✓ frontier allowed: ${roleIDs} → ${path} (model choice stays "auto": cheapest-capable per task)`,
9569
- runtimeBinding: (path) => ` ✓ runtime binding: ${path} (git-ignored, machine-local)`,
9570
- skippedSummary: (roleIDs) => ` · skipped: ${roleIDs} — still frontier-eligible; re-run this command to revisit.`,
9571
- outro: "Role policy updated."
9572
- };
9573
-
9574
10212
  // src/orchestrator/worktreeReconciler.ts
9575
10213
  function planOne(wt) {
9576
10214
  const base = { runID: wt.runID, path: wt.path };
@@ -10126,8 +10764,7 @@ function rosterHealthSection(audit, loadError, rosterRoleCount, rosterMissing) {
10126
10764
  if (unprofiled.length > 0) {
10127
10765
  const roleIDs = unprofiled.map((entry) => entry.roleID).join(", ");
10128
10766
  lines.push(` · unprofiled roleID(s): ${roleIDs}`);
10129
- 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.");
10130
- 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);
10131
10768
  }
10132
10769
  return lines;
10133
10770
  }
@@ -10169,10 +10806,7 @@ function agentModelsSection(diagnostics, searchedPaths, localOnly) {
10169
10806
  }
10170
10807
  const localOnlyGap = localOnly ? diagnostics.find((d) => d.mode === "primary" && d.resolution?.status === "unresolvable" && d.resolution.reason === "provider-not-configured") : undefined;
10171
10808
  if (localOnlyGap?.model !== undefined) {
10172
- lines.push(` ✗ local-only mode, but the primary agent '${localOnlyGap.name}' points at provider '${localOnlyGap.model.providerID}', which opencode.json does not configure.`);
10173
- 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.");
10174
- 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.");
10175
- 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));
10176
10810
  }
10177
10811
  lines.push(" note: 'declared' = model set in .opencode/agent/<name>.md frontmatter; 'inherited' = opencode.json default.");
10178
10812
  lines.push(" note: static checks are offline and deterministic; live checks need a reachable local runtime and can be inconclusive.");
@@ -10181,10 +10815,16 @@ function agentModelsSection(diagnostics, searchedPaths, localOnly) {
10181
10815
  function renderDoctor(input) {
10182
10816
  const enabledRuntimes = input.config.local.runtimes.filter((r) => r.enabled);
10183
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;
10184
10824
  const lines = [
10185
10825
  "openteam doctor:",
10186
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})` : ""),
10187
- ` privacy: ${input.config.privacyMode}`,
10827
+ executionPolicyMessages.globalMode(executionMode),
10188
10828
  ` local runtimes (${reachable}/${enabledRuntimes.length} reachable):`
10189
10829
  ];
10190
10830
  if (input.snapshots.length === 0) {
@@ -10212,6 +10852,11 @@ function renderDoctor(input) {
10212
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.");
10213
10853
  }
10214
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));
10215
10860
  if (input.queueWait !== undefined) {
10216
10861
  lines.push(...queueWaitSection(input.queueWait));
10217
10862
  }
@@ -10239,8 +10884,10 @@ function renderDoctor(input) {
10239
10884
  lines.push(" console:");
10240
10885
  lines.push(" command: openteam console");
10241
10886
  lines.push(" launches: the multi-session web Console (status and URL)");
10242
- if (reachable === 0) {
10243
- 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);
10244
10891
  }
10245
10892
  if (input.graphHealth !== undefined) {
10246
10893
  const h = input.graphHealth;
@@ -10262,7 +10909,7 @@ function renderDoctor(input) {
10262
10909
  lines.push(" Run `openteam clear-cache --delete` to clean up.");
10263
10910
  }
10264
10911
  if (input.agentModels !== undefined) {
10265
- lines.push(...agentModelsSection(input.agentModels, input.opencodeConfigPaths ?? [], input.config.router.localOnly));
10912
+ lines.push(...agentModelsSection(input.agentModels, input.opencodeConfigPaths ?? [], executionMode === "local"));
10266
10913
  }
10267
10914
  if (input.opencodeConfigError !== undefined) {
10268
10915
  lines.push(` ⚠ opencode config parse error: ${input.opencodeConfigError}`);
@@ -10387,6 +11034,47 @@ function renderWorktreeReconciliation(report) {
10387
11034
  function hasEnabledLocalRuntime(config) {
10388
11035
  return config.local.runtimes.some((runtime) => runtime.enabled);
10389
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
+ }
10390
11078
  function enabledRuntimesSummary(config) {
10391
11079
  const enabled = config.local.runtimes.filter((runtime) => runtime.enabled);
10392
11080
  if (enabled.length === 0) {
@@ -10399,59 +11087,44 @@ function enabledRuntimesSummary(config) {
10399
11087
  }).join(", ");
10400
11088
  }
10401
11089
  function showLocal(config) {
10402
- 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;
10403
11092
  const lines = localMessages.summary({
10404
- mode,
10405
- frontierOnly: config.router.frontierOnly,
10406
- localOnly: config.router.localOnly,
10407
- privacy: config.privacyMode,
11093
+ mode: modeLabel,
10408
11094
  runtimes: enabledRuntimesSummary(config)
10409
11095
  });
10410
11096
  return { message: lines.join(`
10411
11097
  `) };
10412
11098
  }
10413
11099
  function setFrontierOnly(config) {
10414
- if (config.router.frontierOnly && !config.router.localOnly) {
11100
+ if (executionMode(config) === "frontier" && !requiresExecutionMigration(config)) {
11101
+ assertEffectiveConfigModelDomains(config);
10415
11102
  return { message: localMessages.frontierOnlyNoChange };
10416
11103
  }
10417
- const flipsPrivacy = config.privacyMode === "forceLocalOnSensitive";
10418
- const next = {
10419
- ...config,
10420
- router: { ...config.router, frontierOnly: true, localOnly: false },
10421
- privacyMode: flipsPrivacy ? "consentBeforeFrontier" : config.privacyMode
10422
- };
10423
- const privacyNote = flipsPrivacy ? localMessages.privacyAdjustedNote : "";
10424
11104
  return {
10425
- config: next,
10426
- message: `${localMessages.frontierOnlyEnabled}${privacyNote}`
11105
+ config: withExecutionMode(config, "frontier"),
11106
+ message: localMessages.frontierOnlyEnabled
10427
11107
  };
10428
11108
  }
10429
11109
  function setLocalOnly(config) {
10430
11110
  if (!hasEnabledLocalRuntime(config)) {
10431
11111
  return { message: localMessages.localOnlyNoRuntimes };
10432
11112
  }
10433
- if (config.router.localOnly && !config.router.frontierOnly) {
11113
+ if (executionMode(config) === "local" && !requiresExecutionMigration(config)) {
11114
+ assertEffectiveConfigModelDomains(config);
10434
11115
  return { message: localMessages.localOnlyNoChange };
10435
11116
  }
10436
- const next = {
10437
- ...config,
10438
- router: { ...config.router, frontierOnly: false, localOnly: true }
10439
- };
10440
11117
  return {
10441
- config: next,
11118
+ config: withExecutionMode(config, "local"),
10442
11119
  message: localMessages.localOnlyEnabled
10443
11120
  };
10444
11121
  }
10445
11122
  function setLocalFirst(config) {
10446
- if (!config.router.frontierOnly && !config.router.localOnly) {
11123
+ if (executionMode(config) === "mixed" && !requiresExecutionMigration(config)) {
10447
11124
  return { message: localMessages.localFirstNoChange };
10448
11125
  }
10449
- const next = {
10450
- ...config,
10451
- router: { ...config.router, frontierOnly: false, localOnly: false }
10452
- };
10453
11126
  return {
10454
- config: next,
11127
+ config: withExecutionMode(config, "mixed"),
10455
11128
  message: localMessages.localFirstReEnabled
10456
11129
  };
10457
11130
  }
@@ -10552,7 +11225,7 @@ function stripJsoncComments(src) {
10552
11225
  }
10553
11226
  return out.join("");
10554
11227
  }
10555
- function isRecord3(v) {
11228
+ function isRecord4(v) {
10556
11229
  return typeof v === "object" && v !== null && !Array.isArray(v);
10557
11230
  }
10558
11231
  var OPENCODE_CONFIG_CANDIDATES = [
@@ -10563,7 +11236,7 @@ function mergeOpencodeConfigs(base, override) {
10563
11236
  const result = { ...base };
10564
11237
  for (const [key, value] of Object.entries(override)) {
10565
11238
  const baseVal = result[key];
10566
- if (isRecord3(baseVal) && isRecord3(value)) {
11239
+ if (isRecord4(baseVal) && isRecord4(value)) {
10567
11240
  result[key] = mergeOpencodeConfigs(baseVal, value);
10568
11241
  } else {
10569
11242
  result[key] = value;
@@ -10764,17 +11437,17 @@ var GENERATED_OPENTEAM_STATE_GROUPS = [
10764
11437
  var OPENCODE_DIR = ".opencode";
10765
11438
  var AGENT_DIR = `${OPENCODE_DIR}/agent`;
10766
11439
  var ROLE_AGENT_DESCRIPTIONS = {
10767
- 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.",
10768
11441
  integration: "openteam integration subagent: wires components together, owns end-to-end flows and cross-module glue.",
10769
- "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.",
10770
11443
  "routing-cost": "openteam routing-cost subagent: cost-aware routing analysis and token/budget optimisation.",
10771
- 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.",
10772
- ralph: "openteam ralph subagent: automation and triage driver. Monitors pending work, prioritises it and coordinates execution, escalating blockers to the human. Runs locally only.",
10773
- 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.",
10774
11447
  tester: "openteam tester subagent: writes and runs tests, reproduces bugs and validates fixes.",
10775
11448
  reviewer: "openteam reviewer subagent: reviews diffs for correctness, security and quality."
10776
11449
  };
10777
- function buildRoleAgent(agentName, description, localOnly) {
11450
+ function buildRoleAgent(agentName, description) {
10778
11451
  const frontmatter = [
10779
11452
  "---",
10780
11453
  `description: ${JSON.stringify(description)}`,
@@ -10788,7 +11461,7 @@ function buildRoleAgent(agentName, description, localOnly) {
10788
11461
  "",
10789
11462
  description,
10790
11463
  "",
10791
- 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.",
10792
11465
  "",
10793
11466
  "Stay focused on your role. Keep changes surgical and report what you did."
10794
11467
  ].join(`
@@ -10798,19 +11471,19 @@ function buildRoleAgent(agentName, description, localOnly) {
10798
11471
  ${body}
10799
11472
  `;
10800
11473
  }
10801
- function buildRoleAgentFiles() {
10802
- const seen = new Map;
11474
+ function buildRoleAgentFiles(_config) {
11475
+ const seen = new Set;
10803
11476
  for (const role of Object.values(TEAM_ROLES)) {
10804
11477
  const agentName = role.opencodeAgent;
10805
11478
  if (agentName === undefined || seen.has(agentName)) {
10806
11479
  continue;
10807
11480
  }
10808
- seen.set(agentName, role.requiresLocalRuntime === true);
11481
+ seen.add(agentName);
10809
11482
  }
10810
- return [...seen.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([agentName, localOnly]) => ({
11483
+ return [...seen].sort((a, b) => a.localeCompare(b)).map((agentName) => ({
10811
11484
  agentName,
10812
11485
  path: `${AGENT_DIR}/${agentName}.md`,
10813
- contents: buildRoleAgent(agentName, ROLE_AGENT_DESCRIPTIONS[agentName] ?? `openteam ${agentName} subagent.`, localOnly)
11486
+ contents: buildRoleAgent(agentName, ROLE_AGENT_DESCRIPTIONS[agentName] ?? `openteam ${agentName} subagent.`)
10814
11487
  }));
10815
11488
  }
10816
11489
  function splitSetupPath(path2) {
@@ -10956,40 +11629,42 @@ function buildOpenTeamConfig(answers) {
10956
11629
  return runtime;
10957
11630
  });
10958
11631
  const primaryLocal = firstEnabled(answers.runtimes);
11632
+ if (answers.executionMode === "local" && primaryLocal === undefined) {
11633
+ throw new Error(executionSetupMessages.localRequiredError);
11634
+ }
10959
11635
  const localDefault = primaryLocal !== undefined ? {
10960
11636
  providerID: choiceProviderID(primaryLocal),
10961
11637
  modelID: primaryLocal.defaultModelID
10962
11638
  } : null;
10963
- const localOnly = answers.localOnly === true && primaryLocal !== undefined;
10964
- 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,
10965
11642
  baseline: {
10966
11643
  mode: "auto",
10967
11644
  pinnedModel: null,
10968
11645
  hardDefault: answers.frontier
10969
11646
  },
10970
11647
  router: {
10971
- mode: answers.routerMode,
10972
- localDefault,
10973
- frontierOnly: primaryLocal === undefined,
10974
- localOnly
11648
+ executionMode: answers.executionMode,
11649
+ localDefault
10975
11650
  },
10976
11651
  local: { runtimes },
10977
- privacyMode: answers.privacyMode,
11652
+ orchestrator: {
11653
+ primary: {
11654
+ model: answers.primaryModel,
11655
+ fallbacks: []
11656
+ },
11657
+ roles,
11658
+ maxRetries: 2,
11659
+ maxModelsPerNode: 2
11660
+ },
10978
11661
  graph: { mode: "active" }
10979
11662
  });
11663
+ assertEffectiveConfigModelDomains(config);
11664
+ return config;
10980
11665
  }
10981
11666
  function orchestratorAgentModel(answers) {
10982
- const primaryLocal = firstEnabled(answers.runtimes);
10983
- if (answers.localOnly === true && primaryLocal !== undefined) {
10984
- return {
10985
- providerID: choiceProviderID(primaryLocal),
10986
- modelID: primaryLocal.thinkingModelID ?? primaryLocal.defaultModelID
10987
- };
10988
- }
10989
- return {
10990
- providerID: answers.frontier.providerID,
10991
- modelID: answers.frontier.modelID
10992
- };
11667
+ return { ...answers.primaryModel };
10993
11668
  }
10994
11669
  function buildOpencodeConfig(answers) {
10995
11670
  const provider = {};
@@ -11003,12 +11678,22 @@ function buildOpencodeConfig(answers) {
11003
11678
  if (baseURL === undefined) {
11004
11679
  continue;
11005
11680
  }
11006
- 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
+ }
11007
11693
  const models = {};
11008
11694
  for (const modelID of modelIDs) {
11009
11695
  models[modelID] = { name: modelID };
11010
11696
  }
11011
- const providerID = choiceProviderID(choice);
11012
11697
  const owner = providerOwner.get(providerID);
11013
11698
  if (owner !== undefined) {
11014
11699
  if (owner.baseURL !== baseURL) {
@@ -11033,8 +11718,7 @@ function buildOpencodeConfig(answers) {
11033
11718
  providerOwner.set(providerID, { instanceId: choice.id, baseURL });
11034
11719
  }
11035
11720
  const primaryLocal = firstEnabled(answers.runtimes);
11036
- const localOnly = answers.localOnly === true && primaryLocal !== undefined;
11037
- const mainModel = localOnly && primaryLocal !== undefined ? `${choiceProviderID(primaryLocal)}/${primaryLocal.defaultModelID}` : `${answers.frontier.providerID}/${answers.frontier.modelID}`;
11721
+ const mainModel = `${answers.primaryModel.providerID}/${answers.primaryModel.modelID}`;
11038
11722
  const config = {
11039
11723
  $schema: "https://opencode.ai/config.json",
11040
11724
  plugin: [OPENTEAM_PLUGIN_SPEC],
@@ -11114,7 +11798,7 @@ async function chooseFrontier(prompt, profiles) {
11114
11798
  const sorted = sortFrontierByCost(profiles);
11115
11799
  const first = sorted[0];
11116
11800
  const selected = await prompt.autocomplete({
11117
- message: "Baseline frontier model (type to search; cheapest first)",
11801
+ message: executionSetupMessages.frontierModelQuestion,
11118
11802
  placeholder: "search by provider or model…",
11119
11803
  choices: [
11120
11804
  ...sorted.map((profile) => ({
@@ -11400,7 +12084,7 @@ async function resolveRuntimeInstances(deps, meta, info, usedIds) {
11400
12084
  async function runSetup(deps) {
11401
12085
  const { prompt } = deps;
11402
12086
  try {
11403
- prompt.intro("openteam setup — configure opencode with local-first routing");
12087
+ prompt.intro(executionSetupMessages.setupIntro);
11404
12088
  const detected = await deps.detect();
11405
12089
  const detectedById = new Map(detected.map((item) => [item.id, item]));
11406
12090
  const runtimeChoices = KNOWN_RUNTIMES.map((meta) => {
@@ -11414,11 +12098,11 @@ async function runSetup(deps) {
11414
12098
  });
11415
12099
  runtimeChoices.push({
11416
12100
  value: RUNTIME_NONE,
11417
- label: "None — use frontier models only",
11418
- hint: "no local runtime (frontier-only)"
12101
+ label: executionSetupMessages.runtimeNoneLabel,
12102
+ hint: executionSetupMessages.runtimeNoneHint
11419
12103
  });
11420
12104
  const enabledIds = await prompt.multiselect({
11421
- message: "Which local runtimes do you want to enable? (choose 'None' for frontier-only)",
12105
+ message: executionSetupMessages.runtimeQuestion,
11422
12106
  choices: runtimeChoices,
11423
12107
  required: true
11424
12108
  });
@@ -11445,10 +12129,27 @@ async function runSetup(deps) {
11445
12129
  runtimes.push(...await resolveRuntimeInstances(deps, meta, info, usedIds));
11446
12130
  }
11447
12131
  const hasLocalEnabled = enabled.size > 0;
11448
- const localOnly = hasLocalEnabled ? await prompt.confirm({
11449
- message: "Run without frontier models? (local runtimes only; openteam never escalates)",
11450
- initial: false
11451
- }) : 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";
11452
12153
  let frontierProfiles;
11453
12154
  try {
11454
12155
  const loaded = await deps.loadFrontierProfiles();
@@ -11456,68 +12157,55 @@ async function runSetup(deps) {
11456
12157
  } catch {
11457
12158
  frontierProfiles = [...CURATED_FRONTIER_PROFILES];
11458
12159
  }
11459
- const frontier = localOnly ? cheapestFrontier(frontierProfiles) : await chooseFrontier(prompt, frontierProfiles);
11460
- if (localOnly) {
11461
- prompt.note([
11462
- "openteam will never escalate to a frontier model.",
11463
- "opencode's main model is pointed at your local runtime too, so the",
11464
- "session itself does not reach a frontier provider either.",
11465
- "",
11466
- `A baseline is still recorded (${frontier.providerID}/${frontier.modelID})`,
11467
- "but stays unused. Set router.localOnly to false to re-enable it.",
11468
- "",
11469
- "Routing mode set to 'balanced' and privacy to 'forceLocalOnSensitive'",
11470
- "automatically — in local-only mode every task stays local regardless,",
11471
- "so these settings cannot change behaviour."
11472
- ].join(`
11473
- `), "Local-only mode");
11474
- }
11475
- const routerMode = localOnly ? "balanced" : await prompt.select({
11476
- message: "Routing mode",
11477
- choices: [
11478
- { value: "balanced", label: "balanced (recommended)" },
11479
- { value: "economy", label: "economy (max savings, prefers local)" },
11480
- { value: "quality", label: "quality (prefers frontier)" }
11481
- ],
11482
- initial: "balanced"
11483
- });
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
+ }
11484
12196
  if (!hasLocalEnabled) {
11485
- prompt.note([
11486
- "You have not enabled any local runtime: openteam will run only with",
11487
- "frontier models (the configured cheapest-capable).",
11488
- "",
11489
- "For sensitive prompts, 'forceLocalOnSensitive' would have no local",
11490
- "target, so 'consentBeforeFrontier' is recommended."
11491
- ].join(`
11492
- `), "Frontier-only mode");
11493
- }
11494
- const privacyMode = localOnly ? "forceLocalOnSensitive" : await prompt.select({
11495
- message: "Privacy for sensitive prompts",
11496
- choices: [
11497
- {
11498
- value: "forceLocalOnSensitive",
11499
- label: "forceLocalOnSensitive (never sends sensitive data to frontier)",
11500
- ...hasLocalEnabled ? {} : { hint: "requires an enabled local runtime" }
11501
- },
11502
- {
11503
- value: "consentBeforeFrontier",
11504
- label: "consentBeforeFrontier (asks for consent)"
11505
- },
11506
- { value: "off", label: "off (no protection)" }
11507
- ],
11508
- initial: hasLocalEnabled ? "forceLocalOnSensitive" : "consentBeforeFrontier"
11509
- });
12197
+ prompt.note(executionSetupMessages.frontierOnlyNote, executionSetupMessages.frontierOnlyTitle);
12198
+ }
11510
12199
  const yolo = await prompt.confirm({
11511
- message: "Enable YOLO mode? (opencode auto-approves all permissions; does not affect openteam privacy)",
12200
+ message: executionSetupMessages.yoloQuestion,
11512
12201
  initial: false
11513
12202
  });
11514
12203
  const answers = {
11515
12204
  runtimes,
11516
12205
  frontier,
11517
- routerMode,
11518
- privacyMode,
11519
- yolo,
11520
- localOnly
12206
+ executionMode: executionMode2,
12207
+ primaryModel,
12208
+ yolo
11521
12209
  };
11522
12210
  const openTeamConfig = buildOpenTeamConfig(answers);
11523
12211
  const opencodeConfig = buildOpencodeConfig(answers);
@@ -11527,7 +12215,7 @@ async function runSetup(deps) {
11527
12215
  const existingOpencodePath = await deps.resolveOpencodeWriteTarget(OPENCODE_CONFIG_CANDIDATES);
11528
12216
  const opencodePath = existingOpencodePath ?? OPENCODE_CONFIG_CANDIDATES[0];
11529
12217
  const agentPath = ORCHESTRATOR_AGENT_PATH;
11530
- const roleAgentFiles = buildRoleAgentFiles();
12218
+ const roleAgentFiles = buildRoleAgentFiles(openTeamConfig);
11531
12219
  const opencodeGitignorePath = OPENCODE_GITIGNORE_PATH;
11532
12220
  const existingGitignore = await deps.fileExists(opencodeGitignorePath) ? await deps.readFile(opencodeGitignorePath) ?? "" : "";
11533
12221
  const opencodeGitignore = buildOpencodeGitignore(existingGitignore);
@@ -11584,27 +12272,30 @@ async function runSetup(deps) {
11584
12272
  const remote = runtime.baseURL !== undefined && !runtime.baseURL.includes("localhost") && !runtime.baseURL.includes("127.0.0.1") ? ` @ ${runtime.baseURL}` : "";
11585
12273
  return `${runtime.id} (${runtime.defaultModelID})${remote}`;
11586
12274
  }).join(", ");
11587
- prompt.note([
11588
- `Baseline frontier: ${localOnly ? `${frontier.providerID}/${frontier.modelID} (recorded but unused — local-only)` : `${frontier.providerID}/${frontier.modelID}`}`,
11589
- `Local runtimes: ${enabledSummary || "none (frontier-only)"}`,
11590
- `YOLO mode: ${yolo ? "enabled (auto-approves permissions)" : "disabled"}`,
11591
- "Web Console: launched separately from the CLI with 'openteam console' (multi-session, loopback)",
11592
- `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}`,
11593
- "",
11594
- "Next steps:",
11595
- localOnly ? " 1. No frontier auth needed; openteam runs entirely on your local runtimes" : ` 1. Authenticate the frontier provider: opencode auth login`,
11596
- 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",
11597
- " 3. Press Tab and pick the 'openteam' agent, or type / and pick an /openteam… command"
11598
- ].join(`
11599
- `), "openteam configured");
11600
- 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);
11601
12292
  return {
11602
12293
  exitCode: 0,
11603
- 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}`)
11604
12295
  };
11605
12296
  } catch (error) {
11606
12297
  const message = error instanceof Error ? error.message : String(error);
11607
- return { exitCode: 1, stdout: `setup error: ${message}` };
12298
+ return { exitCode: 1, stdout: executionSetupMessages.error(message) };
11608
12299
  }
11609
12300
  }
11610
12301
 
@@ -11874,8 +12565,8 @@ function stripOpenteamFromOpencodeConfig(raw) {
11874
12565
  }
11875
12566
  return { status: "not-present" };
11876
12567
  }
11877
- const record = parsed;
11878
- const plugins = record.plugin;
12568
+ const record2 = parsed;
12569
+ const plugins = record2.plugin;
11879
12570
  if (!Array.isArray(plugins)) {
11880
12571
  if (raw.includes(OPENTEAM_PLUGIN_SPEC)) {
11881
12572
  return {
@@ -12289,10 +12980,10 @@ var HELP = [
12289
12980
  localMessages.help.off,
12290
12981
  localMessages.help.only,
12291
12982
  localMessages.help.on,
12292
- " openteam doctor Diagnose runtimes and config",
12293
- " openteam roles init Compose per-role routing policy for unprofiled roster roles",
12983
+ executionPolicyMessages.doctorHelp,
12984
+ rolesInitMessages.help,
12294
12985
  " openteam migrate Relocate pre-P15b artifacts to their split homes (idempotent)",
12295
- " openteam agents List agents and each one's LLM (local/frontier)",
12986
+ executionPolicyMessages.agentsHelp,
12296
12987
  " openteam console Launch the multi-session web Console (Ctrl+C to stop; --open opens the browser)",
12297
12988
  " openteam console --status Show the Console config without launching it",
12298
12989
  " openteam graph status Show the Graph journal and lease health",
@@ -12510,9 +13201,7 @@ ${HELP}`
12510
13201
  const agentNote = await regenerateOrchestratorAgent(deps, config, enable);
12511
13202
  return {
12512
13203
  exitCode: 0,
12513
- stdout: `${writePath} updated. ${renderYoloStatus(enable)}${agentNote}${enable ? `
12514
- Note: YOLO does not disable openteam privacy; sensitive prompts still stay off frontier with forceLocalOnSensitive.` : ""}
12515
- Restart opencode (or reload) so agents pick up the new permissions.`
13204
+ stdout: yoloMessages.updated(writePath, renderYoloStatus(enable), agentNote)
12516
13205
  };
12517
13206
  }
12518
13207
  async function regenerateOrchestratorAgent(deps, config, enable) {
@@ -12572,6 +13261,11 @@ async function runAgents(deps, configPath, opencodeConfigPaths, configResolution
12572
13261
  exitCode: 0,
12573
13262
  stdout: renderAgentList(classifications, {
12574
13263
  ...context,
13264
+ executionPolicies: executionPolicySummaries(config),
13265
+ retryLimits: {
13266
+ maxRetries: config.orchestrator?.maxRetries ?? 2,
13267
+ maxModelsPerNode: config.orchestrator?.maxModelsPerNode ?? 2
13268
+ },
12575
13269
  ...opencodeConfigError !== undefined ? { opencodeConfigError } : {}
12576
13270
  })
12577
13271
  };
@@ -13158,9 +13852,12 @@ function auditParsedRoster(roster, configuredRoles) {
13158
13852
  }
13159
13853
 
13160
13854
  // src/commands/rolesInit.ts
13161
- var ROLES_INIT_LOCAL_ONLY = "local-only";
13162
- var ROLES_INIT_FRONTIER_OK = "frontier-ok";
13855
+ var ROLES_INIT_AUTO = "auto";
13856
+ var ROLES_INIT_PIN = "pin";
13163
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";
13164
13861
  function isJsonRecord2(value) {
13165
13862
  return typeof value === "object" && value !== null && !Array.isArray(value);
13166
13863
  }
@@ -13193,29 +13890,15 @@ function withRole(config, roleID, profile) {
13193
13890
  }
13194
13891
  };
13195
13892
  }
13196
- function localOnlyProfile(roleID) {
13197
- const base = synthesiseFallbackProfile(roleID);
13198
- return {
13199
- ...base,
13200
- preferredFrontierModels: [],
13201
- localFirst: true,
13202
- requiresLocalRuntime: true
13203
- };
13204
- }
13205
- function frontierEligibleProfile(roleID) {
13206
- return { ...synthesiseFallbackProfile(roleID) };
13207
- }
13208
- function policyChoices(roleID, agentName) {
13893
+ function policyChoices() {
13209
13894
  return [
13210
13895
  {
13211
- value: ROLES_INIT_LOCAL_ONLY,
13212
- label: rolesInitMessages.localOnlyLabel(roleID, agentName),
13213
- hint: rolesInitMessages.localOnlyHint
13896
+ value: ROLES_INIT_AUTO,
13897
+ label: executionSetupMessages.rolePolicy.modelAutoLabel
13214
13898
  },
13215
13899
  {
13216
- value: ROLES_INIT_FRONTIER_OK,
13217
- label: rolesInitMessages.frontierOkLabel,
13218
- hint: rolesInitMessages.frontierOkHint
13900
+ value: ROLES_INIT_PIN,
13901
+ label: executionSetupMessages.rolePolicy.modelExactLabel
13219
13902
  },
13220
13903
  {
13221
13904
  value: ROLES_INIT_SKIP,
@@ -13224,6 +13907,56 @@ function policyChoices(roleID, agentName) {
13224
13907
  }
13225
13908
  ];
13226
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
+ }
13227
13960
  async function runRolesInit(deps) {
13228
13961
  const lines = [rolesInitMessages.header];
13229
13962
  const rosterContent = await deps.readFile(OPENTEAM_ROSTER_PATH);
@@ -13238,8 +13971,18 @@ async function runRolesInit(deps) {
13238
13971
  return { stdout: lines.join(`
13239
13972
  `), exitCode: 1 };
13240
13973
  }
13241
- 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
+ }
13242
13984
  const configuredRoles = existingRoles(baseConfig);
13985
+ const globalExecutionMode = rawExecutionMode(baseConfig);
13243
13986
  const unprofiled = parsed.roster.entries.filter((entry) => getRoleProfile(entry.roleID, configuredRoles) === undefined && !isKnownNonWorkerRole(entry.roleID));
13244
13987
  if (unprofiled.length === 0) {
13245
13988
  lines.push(rolesInitMessages.allProfiled(parsed.roster.entries.length));
@@ -13247,32 +13990,44 @@ async function runRolesInit(deps) {
13247
13990
  `), exitCode: 0 };
13248
13991
  }
13249
13992
  deps.prompt.intro(rolesInitMessages.title);
13250
- deps.prompt.note(rolesInitMessages.why(unprofiled.length), rolesInitMessages.whyTitle);
13993
+ deps.prompt.note(executionSetupMessages.rolePolicy.why(unprofiled.length), executionSetupMessages.rolePolicy.whyTitle);
13251
13994
  const runtimes = (await deps.detectRuntimes?.())?.filter((runtime) => runtime.available);
13252
13995
  let nextConfig = baseConfig;
13253
13996
  let nextOverlay;
13254
- const pinned = [];
13255
- const relaxed = [];
13997
+ const configured = [];
13256
13998
  const skipped = [];
13257
13999
  try {
13258
14000
  for (const entry of unprofiled) {
13259
14001
  const choice = await deps.prompt.select({
13260
- message: rolesInitMessages.policyQuestion(entry.roleID, entry.agentName),
13261
- choices: policyChoices(entry.roleID, entry.agentName),
13262
- initial: ROLES_INIT_LOCAL_ONLY
14002
+ message: executionSetupMessages.rolePolicy.modelQuestion(entry.roleID),
14003
+ choices: policyChoices(),
14004
+ initial: ROLES_INIT_AUTO
13263
14005
  });
13264
14006
  if (choice === ROLES_INIT_SKIP) {
13265
14007
  skipped.push(entry.roleID);
13266
14008
  continue;
13267
14009
  }
13268
- if (choice === ROLES_INIT_FRONTIER_OK) {
13269
- nextConfig = withRole(nextConfig, entry.roleID, frontierEligibleProfile(entry.roleID));
13270
- relaxed.push(entry.roleID);
13271
- continue;
13272
- }
13273
- nextConfig = withRole(nextConfig, entry.roleID, localOnlyProfile(entry.roleID));
13274
- pinned.push(entry.roleID);
13275
- 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) {
13276
14031
  const selected = await deps.prompt.multiselect({
13277
14032
  message: rolesInitMessages.runtimeQuestion(entry.roleID),
13278
14033
  choices: runtimes.map((runtime) => ({
@@ -13294,15 +14049,21 @@ async function runRolesInit(deps) {
13294
14049
  return { stdout: lines.join(`
13295
14050
  `), exitCode: 1 };
13296
14051
  }
13297
- if (pinned.length === 0 && relaxed.length === 0) {
14052
+ if (configured.length === 0) {
13298
14053
  lines.push(rolesInitMessages.nothingWritten(skipped.join(", ")));
13299
14054
  deps.prompt.outro(rolesInitMessages.nothingToWriteOutro);
13300
14055
  return { stdout: lines.join(`
13301
14056
  `), exitCode: 0 };
13302
14057
  }
13303
- const merged = mergeConfigInputs(nextConfig, nextOverlay);
13304
14058
  try {
13305
- 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
+ }
13306
14067
  } catch (error) {
13307
14068
  lines.push(rolesInitMessages.invalidResult(error instanceof Error ? error.message : String(error)));
13308
14069
  return { stdout: lines.join(`
@@ -13312,12 +14073,7 @@ async function runRolesInit(deps) {
13312
14073
  if (nextOverlay !== undefined) {
13313
14074
  await deps.writeFile(DEFAULT_LOCAL_OVERLAY_PATH, serialize(nextOverlay));
13314
14075
  }
13315
- if (pinned.length > 0) {
13316
- lines.push(rolesInitMessages.pinnedLocal(pinned.join(", "), DEFAULT_CONFIG_PATH));
13317
- }
13318
- if (relaxed.length > 0) {
13319
- lines.push(rolesInitMessages.frontierAllowed(relaxed.join(", "), DEFAULT_CONFIG_PATH));
13320
- }
14076
+ lines.push(executionSetupMessages.rolePolicy.configured(configured.join(", "), DEFAULT_CONFIG_PATH));
13321
14077
  if (nextOverlay !== undefined) {
13322
14078
  lines.push(rolesInitMessages.runtimeBinding(DEFAULT_LOCAL_OVERLAY_PATH));
13323
14079
  }
@@ -14280,21 +15036,21 @@ var DEFAULT_RANK_SIGNALS = {
14280
15036
  accessWeight: 0.1,
14281
15037
  recencyHalfLifeMs: 14 * DAY_MS
14282
15038
  };
14283
- function recencyTimestamp(record) {
14284
- if (record.kind === "preference" && record.lastAccessedAt !== null) {
14285
- return record.lastAccessedAt;
15039
+ function recencyTimestamp(record2) {
15040
+ if (record2.kind === "preference" && record2.lastAccessedAt !== null) {
15041
+ return record2.lastAccessedAt;
14286
15042
  }
14287
- return record.createdAt;
15043
+ return record2.createdAt;
14288
15044
  }
14289
- function recencyScore(record, signals) {
14290
- const ageMs = Math.max(0, signals.now - recencyTimestamp(record));
15045
+ function recencyScore(record2, signals) {
15046
+ const ageMs = Math.max(0, signals.now - recencyTimestamp(record2));
14291
15047
  return 0.5 ** (ageMs / signals.recencyHalfLifeMs);
14292
15048
  }
14293
- function accessScore(record) {
14294
- if (record.kind !== "preference") {
15049
+ function accessScore(record2) {
15050
+ if (record2.kind !== "preference") {
14295
15051
  return 0;
14296
15052
  }
14297
- return 1 - 1 / (1 + record.accessCount);
15053
+ return 1 - 1 / (1 + record2.accessCount);
14298
15054
  }
14299
15055
  function scoreCandidate(candidate, signals) {
14300
15056
  return signals.similarityWeight * candidate.similarity + signals.recencyWeight * recencyScore(candidate.record, signals) + signals.accessWeight * accessScore(candidate.record);
@@ -14312,20 +15068,20 @@ function rankRecall(candidates, signals) {
14312
15068
  }
14313
15069
 
14314
15070
  // src/memory/temporal.ts
14315
- function isTransactionActiveAt(record, asOf) {
14316
- return record.invalidatedAt === null || record.invalidatedAt > asOf;
15071
+ function isTransactionActiveAt(record2, asOf) {
15072
+ return record2.invalidatedAt === null || record2.invalidatedAt > asOf;
14317
15073
  }
14318
- function isValidAt(record, asOf) {
14319
- if (record.validFrom > asOf) {
15074
+ function isValidAt(record2, asOf) {
15075
+ if (record2.validFrom > asOf) {
14320
15076
  return false;
14321
15077
  }
14322
- return record.validUntil === null || record.validUntil > asOf;
15078
+ return record2.validUntil === null || record2.validUntil > asOf;
14323
15079
  }
14324
- function isActiveAt(record, asOf) {
14325
- return isTransactionActiveAt(record, asOf) && isValidAt(record, asOf);
15080
+ function isActiveAt(record2, asOf) {
15081
+ return isTransactionActiveAt(record2, asOf) && isValidAt(record2, asOf);
14326
15082
  }
14327
15083
  function filterActive(records, asOf) {
14328
- return records.filter((record) => isActiveAt(record, asOf));
15084
+ return records.filter((record2) => isActiveAt(record2, asOf));
14329
15085
  }
14330
15086
  function coalesceWindow(a, b) {
14331
15087
  const validFrom = Math.min(a.validFrom, b.validFrom);
@@ -14361,21 +15117,21 @@ function cosineSimilarity(a, b) {
14361
15117
  }
14362
15118
 
14363
15119
  // src/memory/recall.ts
14364
- function matchesScope(record, query) {
14365
- if (record.ownerKey === query.ownerKey) {
15120
+ function matchesScope(record2, query) {
15121
+ if (record2.ownerKey === query.ownerKey) {
14366
15122
  return true;
14367
15123
  }
14368
- return query.includeShared && record.ownerKey === SHARED_OWNER_KEY;
15124
+ return query.includeShared && record2.ownerKey === SHARED_OWNER_KEY;
14369
15125
  }
14370
15126
  function recall(records, queryEmbedding, query, deps) {
14371
15127
  const asOf = query.asOf ?? deps.now;
14372
15128
  const kinds = new Set(query.kinds);
14373
15129
  const candidates = [];
14374
- for (const record of filterActive(records, asOf)) {
14375
- 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)) {
14376
15132
  continue;
14377
15133
  }
14378
- const embedding = deps.embeddings.get(record.id);
15134
+ const embedding = deps.embeddings.get(record2.id);
14379
15135
  if (embedding === undefined) {
14380
15136
  continue;
14381
15137
  }
@@ -14383,7 +15139,7 @@ function recall(records, queryEmbedding, query, deps) {
14383
15139
  if (similarity < query.minSimilarity) {
14384
15140
  continue;
14385
15141
  }
14386
- candidates.push({ record, similarity });
15142
+ candidates.push({ record: record2, similarity });
14387
15143
  }
14388
15144
  const signals = {
14389
15145
  now: deps.now,
@@ -14573,11 +15329,11 @@ function emptyState() {
14573
15329
  return { facts: [], preferences: [], entities: [], relations: [] };
14574
15330
  }
14575
15331
  function applyTombstone(state, tombstone) {
14576
- const mark = (records) => records.map((record) => record.id === tombstone.id ? {
14577
- ...record,
15332
+ const mark = (records) => records.map((record2) => record2.id === tombstone.id ? {
15333
+ ...record2,
14578
15334
  invalidatedAt: tombstone.invalidatedAt,
14579
15335
  supersededBy: tombstone.supersededBy
14580
- } : record);
15336
+ } : record2);
14581
15337
  if (tombstone.kind === "fact") {
14582
15338
  state.facts = mark(state.facts);
14583
15339
  } else if (tombstone.kind === "preference") {
@@ -14617,8 +15373,8 @@ function allRecords(state) {
14617
15373
  }
14618
15374
 
14619
15375
  // src/memory/log.ts
14620
- function serializeAssertion(record) {
14621
- return JSON.stringify(record);
15376
+ function serializeAssertion(record2) {
15377
+ return JSON.stringify(record2);
14622
15378
  }
14623
15379
  function parseMemoryLog(text2) {
14624
15380
  const records = [];
@@ -14680,8 +15436,8 @@ function insertRecords(db, records) {
14680
15436
  const stmt = db.query(INSERT_SQL2);
14681
15437
  db.run("BEGIN");
14682
15438
  try {
14683
- for (const record of records) {
14684
- 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));
14685
15441
  }
14686
15442
  db.run("COMMIT");
14687
15443
  } catch (error) {
@@ -14734,15 +15490,15 @@ function createMemoryIndex(db, warn) {
14734
15490
  const preferences = [];
14735
15491
  const entities = [];
14736
15492
  const relations = [];
14737
- for (const record of allRecordsWithWarning()) {
14738
- if (record.kind === "fact") {
14739
- facts.push(record);
14740
- } else if (record.kind === "preference") {
14741
- preferences.push(record);
14742
- } else if (record.kind === "entity") {
14743
- 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);
14744
15500
  } else {
14745
- relations.push(record);
15501
+ relations.push(record2);
14746
15502
  }
14747
15503
  }
14748
15504
  return { facts, preferences, entities, relations };
@@ -14881,31 +15637,31 @@ function createLocalEmbedder(request, deps) {
14881
15637
  function normalize(parts) {
14882
15638
  return parts.map((part) => part.trim()).filter((part) => part.length > 0).join(" ");
14883
15639
  }
14884
- function recordEmbeddingText(record) {
14885
- if (record.kind === "fact") {
15640
+ function recordEmbeddingText(record2) {
15641
+ if (record2.kind === "fact") {
14886
15642
  return normalize([
14887
- record.subject,
14888
- record.predicate,
14889
- record.object,
14890
- record.category ?? ""
15643
+ record2.subject,
15644
+ record2.predicate,
15645
+ record2.object,
15646
+ record2.category ?? ""
14891
15647
  ]);
14892
15648
  }
14893
- if (record.kind === "preference") {
15649
+ if (record2.kind === "preference") {
14894
15650
  return normalize([
14895
- record.category,
14896
- record.preference,
14897
- record.context ?? ""
15651
+ record2.category,
15652
+ record2.preference,
15653
+ record2.context ?? ""
14898
15654
  ]);
14899
15655
  }
14900
- if (record.kind === "relation") {
15656
+ if (record2.kind === "relation") {
14901
15657
  return normalize([
14902
- record.from,
14903
- record.predicate,
14904
- record.to,
14905
- record.annotation ?? ""
15658
+ record2.from,
15659
+ record2.predicate,
15660
+ record2.to,
15661
+ record2.annotation ?? ""
14906
15662
  ]);
14907
15663
  }
14908
- return normalize([record.canonicalName, record.type, ...record.aliases]);
15664
+ return normalize([record2.canonicalName, record2.type, ...record2.aliases]);
14909
15665
  }
14910
15666
 
14911
15667
  // src/storage/index/memoryEmbeddings.ts
@@ -14921,7 +15677,7 @@ async function ensureMemoryEmbeddings(deps) {
14921
15677
  const records = index.allRecords();
14922
15678
  index.pruneEmbeddings();
14923
15679
  const have = modelChanged ? new Set : new Set(index.embeddingMap().keys());
14924
- const todo = records.filter((record) => !have.has(record.id));
15680
+ const todo = records.filter((record2) => !have.has(record2.id));
14925
15681
  if (todo.length === 0) {
14926
15682
  return { embedded: 0, failed: 0, modelChanged };
14927
15683
  }
@@ -14937,8 +15693,8 @@ async function ensureMemoryEmbeddings(deps) {
14937
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.`);
14938
15694
  return { embedded: 0, failed: todo.length, modelChanged };
14939
15695
  }
14940
- const entries = todo.map((record, position) => ({
14941
- recordId: record.id,
15696
+ const entries = todo.map((record2, position) => ({
15697
+ recordId: record2.id,
14942
15698
  vector: vectors[position]
14943
15699
  }));
14944
15700
  index.putEmbeddings(entries);
@@ -14987,7 +15743,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
14987
15743
  // package.json
14988
15744
  var package_default = {
14989
15745
  name: "@jmanuelcorral/openteam",
14990
- version: "0.18.0",
15746
+ version: "0.19.0",
14991
15747
  packageManager: "bun@1.3.14",
14992
15748
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
14993
15749
  license: "MIT",
@@ -15178,30 +15934,30 @@ function isMissingFile(error) {
15178
15934
  }
15179
15935
 
15180
15936
  // src/memory/traverse.ts
15181
- function inOwnerScope(record, ownerKey, includeShared) {
15182
- if (record.ownerKey === ownerKey) {
15937
+ function inOwnerScope(record2, ownerKey, includeShared) {
15938
+ if (record2.ownerKey === ownerKey) {
15183
15939
  return true;
15184
15940
  }
15185
- return includeShared && record.ownerKey === SHARED_OWNER_KEY;
15941
+ return includeShared && record2.ownerKey === SHARED_OWNER_KEY;
15186
15942
  }
15187
15943
  function traverseRelations(records, query) {
15188
15944
  const includeShared = query.includeShared ?? true;
15189
15945
  const adjacency = new Map;
15190
- for (const record of records) {
15191
- if (record.kind !== "relation") {
15946
+ for (const record2 of records) {
15947
+ if (record2.kind !== "relation") {
15192
15948
  continue;
15193
15949
  }
15194
- if (!isActiveAt(record, query.asOf)) {
15950
+ if (!isActiveAt(record2, query.asOf)) {
15195
15951
  continue;
15196
15952
  }
15197
- if (!inOwnerScope(record, query.ownerKey, includeShared)) {
15953
+ if (!inOwnerScope(record2, query.ownerKey, includeShared)) {
15198
15954
  continue;
15199
15955
  }
15200
- const list = adjacency.get(record.from);
15956
+ const list = adjacency.get(record2.from);
15201
15957
  if (list === undefined) {
15202
- adjacency.set(record.from, [record]);
15958
+ adjacency.set(record2.from, [record2]);
15203
15959
  } else {
15204
- list.push(record);
15960
+ list.push(record2);
15205
15961
  }
15206
15962
  }
15207
15963
  const visited = new Set([query.seed]);
@@ -15232,8 +15988,8 @@ function traverseRelations(records, query) {
15232
15988
  }
15233
15989
  return { seed: query.seed, nodes, edges };
15234
15990
  }
15235
- function seedNodeId(record) {
15236
- return record.kind === "entity" ? record.canonicalName : undefined;
15991
+ function seedNodeId(record2) {
15992
+ return record2.kind === "entity" ? record2.canonicalName : undefined;
15237
15993
  }
15238
15994
  function recallGraph(records, queryEmbedding, query, options, deps) {
15239
15995
  const seeds = recall(records, queryEmbedding, query, deps);
@@ -15529,32 +16285,32 @@ function projectRelationGraph(graph) {
15529
16285
  };
15530
16286
  }
15531
16287
  function projectRecall(results) {
15532
- return results.map(({ record, similarity, score }) => {
16288
+ return results.map(({ record: record2, similarity, score }) => {
15533
16289
  const base = {
15534
- id: record.id,
15535
- kind: record.kind,
15536
- ownerKey: record.ownerKey,
15537
- confidence: record.confidence,
16290
+ id: record2.id,
16291
+ kind: record2.kind,
16292
+ ownerKey: record2.ownerKey,
16293
+ confidence: record2.confidence,
15538
16294
  similarity,
15539
16295
  score
15540
16296
  };
15541
- if (record.kind === "fact") {
16297
+ if (record2.kind === "fact") {
15542
16298
  return {
15543
16299
  ...base,
15544
- subject: record.subject,
15545
- predicate: record.predicate,
15546
- object: record.object,
15547
- category: record.category
16300
+ subject: record2.subject,
16301
+ predicate: record2.predicate,
16302
+ object: record2.object,
16303
+ category: record2.category
15548
16304
  };
15549
16305
  }
15550
- if (record.kind === "preference") {
16306
+ if (record2.kind === "preference") {
15551
16307
  return {
15552
16308
  ...base,
15553
- category: record.category,
15554
- preference: record.preference
16309
+ category: record2.category,
16310
+ preference: record2.preference
15555
16311
  };
15556
16312
  }
15557
- const entity = record;
16313
+ const entity = record2;
15558
16314
  return { ...base, canonicalName: entity.canonicalName, type: entity.type };
15559
16315
  });
15560
16316
  }
@@ -15729,24 +16485,24 @@ var DEFAULT_DECAY_PARAMS = {
15729
16485
  halfLifeMs: 90 * DAY_MS2,
15730
16486
  forgetThreshold: 0.05
15731
16487
  };
15732
- function lastReinforcedAt(record) {
15733
- if (record.kind === "preference" && record.lastAccessedAt !== null) {
15734
- return record.lastAccessedAt;
16488
+ function lastReinforcedAt(record2) {
16489
+ if (record2.kind === "preference" && record2.lastAccessedAt !== null) {
16490
+ return record2.lastAccessedAt;
15735
16491
  }
15736
- return record.createdAt;
16492
+ return record2.createdAt;
15737
16493
  }
15738
- function decayedConfidence(record, now, params = DEFAULT_DECAY_PARAMS) {
15739
- const ageMs = Math.max(0, now - lastReinforcedAt(record));
15740
- 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);
15741
16497
  }
15742
16498
  function consolidate(records, now, params = DEFAULT_DECAY_PARAMS) {
15743
16499
  const tombstones = [];
15744
- for (const record of records) {
15745
- if (!isTransactionActiveAt(record, now)) {
16500
+ for (const record2 of records) {
16501
+ if (!isTransactionActiveAt(record2, now)) {
15746
16502
  continue;
15747
16503
  }
15748
- if (decayedConfidence(record, now, params) < params.forgetThreshold) {
15749
- tombstones.push({ ...record, invalidatedAt: now, supersededBy: null });
16504
+ if (decayedConfidence(record2, now, params) < params.forgetThreshold) {
16505
+ tombstones.push({ ...record2, invalidatedAt: now, supersededBy: null });
15750
16506
  }
15751
16507
  }
15752
16508
  return tombstones;
@@ -15861,17 +16617,21 @@ async function loadConfig(path4, resolution = { explicit: false }) {
15861
16617
  read: (requestedPath) => requestedPath === path4 ? readConfigFile(path4, resolution) : storage.read(requestedPath)
15862
16618
  } : storage;
15863
16619
  const overlayPath = resolution.explicit ? null : DEFAULT_LOCAL_OVERLAY_PATH;
15864
- 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;
15865
16625
  }
15866
16626
  async function saveConfig(config, path4, resolution = { explicit: false }) {
15867
16627
  if (!resolution.explicit) {
15868
16628
  await writeOpenTeamConfigFile(config, path4, storage);
15869
16629
  return;
15870
16630
  }
16631
+ const contents = serializeOpenTeamConfig(config);
15871
16632
  const diskPath = resolve6(path4);
15872
16633
  await mkdir2(dirname6(diskPath), { recursive: true });
15873
- await writeFile2(diskPath, `${JSON.stringify(config, null, 2)}
15874
- `, "utf8");
16634
+ await writeFile2(diskPath, contents, "utf8");
15875
16635
  }
15876
16636
  function parseConfigSelection(argv) {
15877
16637
  for (let i = 0;i < argv.length; i += 1) {