@aiwg/cli 2026.10.11 → 2026.10.12

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 (132) hide show
  1. package/agentic/code/providers/model-catalog.v1.json +62 -12
  2. package/dist/src/artifacts/binding-target-identity.js +51 -0
  3. package/dist/src/artifacts/checksum-manifest.js +12 -0
  4. package/dist/src/artifacts/index-builder.js +11 -13
  5. package/dist/src/artifacts/query-engine.js +5 -11
  6. package/dist/src/cli/handlers/doctor-help.js +32 -0
  7. package/dist/src/cli/handlers/gates.js +19 -18
  8. package/dist/src/cli/handlers/models-help.js +7 -1
  9. package/dist/src/cli/handlers/models.js +3 -3
  10. package/dist/src/cli/handlers/script-runner.js +8 -5
  11. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  12. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  13. package/dist/src/cli/handlers/sessions.js +90 -17
  14. package/dist/src/cli/handlers/skill-lint.js +10 -3
  15. package/dist/src/cli/handlers/steward-format.js +100 -0
  16. package/dist/src/cli/handlers/steward-models.js +167 -0
  17. package/dist/src/cli/handlers/steward.js +14 -248
  18. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  19. package/dist/src/cli/handlers/use.js +6 -24
  20. package/dist/src/cli/handlers/utilities.js +1 -24
  21. package/dist/src/config/aiwg-config.js +7 -0
  22. package/dist/src/config/authorization-cli.js +89 -0
  23. package/dist/src/config/cli-help.js +1 -0
  24. package/dist/src/config/cli.js +10 -1
  25. package/dist/src/decision/context-pruning.js +3 -2
  26. package/dist/src/decision/ensemble/contract.js +15 -4
  27. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  28. package/dist/src/decision/ensemble-study/score.js +4 -3
  29. package/dist/src/decision/feature-export.js +3 -9
  30. package/dist/src/decision/heldout/collector.js +2 -2
  31. package/dist/src/decision/issue-triage/runtime.js +3 -9
  32. package/dist/src/decision/qualification/gates.js +2 -1
  33. package/dist/src/decision/qualification/release.js +2 -3
  34. package/dist/src/decision/routing/report.js +2 -0
  35. package/dist/src/decision/sdlc-screening.js +3 -5
  36. package/dist/src/decision/sensitivity/comparative.js +5 -3
  37. package/dist/src/gates/driver.js +6 -77
  38. package/dist/src/gates/evaluate.js +4 -11
  39. package/dist/src/gates/floors.js +4 -4
  40. package/dist/src/gates/index.js +3 -3
  41. package/dist/src/gates/lattice.js +26 -0
  42. package/dist/src/gates/registry.js +2 -1
  43. package/dist/src/gates/report.js +19 -7
  44. package/dist/src/issues/audit-cli.js +120 -0
  45. package/dist/src/issues/audit-contract.js +64 -0
  46. package/dist/src/issues/audit-local.js +82 -0
  47. package/dist/src/issues/audit.js +220 -0
  48. package/dist/src/lint/rule-activation.js +15 -1
  49. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  50. package/dist/src/mission-protocol/codecs.js +22 -550
  51. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  52. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  53. package/dist/src/mission-protocol/types.js +2 -42
  54. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  55. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  56. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  57. package/dist/src/models/catalog-policy.js +280 -0
  58. package/dist/src/models/index.js +1 -1
  59. package/dist/src/models/model-catalog.v1.json +62 -12
  60. package/dist/src/models/policy-resources.js +28 -0
  61. package/dist/src/models/provider-policy.js +10 -546
  62. package/dist/src/models/resolver.js +26 -8
  63. package/dist/src/models/router.js +15 -2
  64. package/dist/src/models/tiers.js +13 -0
  65. package/dist/src/models/types.js +0 -6
  66. package/dist/src/models/worker-ceilings.js +59 -0
  67. package/dist/src/models/worker-invocation-policy.js +243 -0
  68. package/dist/src/models/worker-plan.js +16 -20
  69. package/dist/src/models/worker-review-lineage.js +55 -0
  70. package/dist/src/models/worker-selection-policy.js +1 -1
  71. package/dist/src/models/wrapper-route.js +6 -3
  72. package/dist/src/policy/authorization.js +1 -0
  73. package/dist/src/policy/hitl-config.js +63 -0
  74. package/dist/src/policy/hitl-receipt.js +57 -0
  75. package/dist/src/policy/hitl-types.js +4 -0
  76. package/dist/src/policy/hitl-validation.js +39 -0
  77. package/dist/src/policy/hitl.js +117 -0
  78. package/dist/src/rules/activation.d.mts +1 -1
  79. package/dist/src/rules/activation.mjs +0 -2
  80. package/dist/src/rules/gate-coverage.js +51 -0
  81. package/dist/src/rules/gate-references.js +16 -0
  82. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  83. package/dist/src/serve/worker-command-request.js +21 -4
  84. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  85. package/dist/src/serve/worker-invocation.js +21 -10
  86. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  87. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  88. package/dist/src/sessions/adapters/grok-build.js +57 -15
  89. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  90. package/dist/src/sessions/adapters/hermes.js +42 -3
  91. package/dist/src/sessions/adapters/warp.js +1 -1
  92. package/dist/src/sessions/batch-import.js +10 -0
  93. package/dist/src/sessions/contracts.js +1 -1
  94. package/dist/src/sessions/discovery-capability.js +38 -0
  95. package/dist/src/sessions/importer.js +8 -251
  96. package/dist/src/sessions/index.js +4 -0
  97. package/dist/src/sessions/intake-dir.js +71 -0
  98. package/dist/src/sessions/normalization.js +251 -0
  99. package/dist/src/sessions/tail.js +91 -0
  100. package/dist/src/sessions/workspace-discovery.js +83 -7
  101. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  102. package/dist/src/skills/agent-skills.js +4 -23
  103. package/dist/src/skills/claude-surface-projection.js +25 -0
  104. package/dist/src/skills/deployer.js +8 -288
  105. package/dist/src/skills/deployment-policy.js +114 -0
  106. package/dist/src/skills/deployment-projection.js +152 -0
  107. package/dist/src/skills/deployment-types.js +20 -0
  108. package/dist/src/skills/exporter.js +8 -31
  109. package/dist/src/skills/provider-extensions.js +41 -0
  110. package/dist/src/skills/run.js +6 -0
  111. package/dist/src/skills/types.js +0 -8
  112. package/dist/src/skills/validator-resources.js +68 -0
  113. package/dist/src/skills/validator.js +6 -55
  114. package/dist/src/storage/cli-migrate-options.js +125 -0
  115. package/dist/src/storage/cli-migrate.js +111 -0
  116. package/dist/src/storage/cli.js +4 -279
  117. package/dist/src/storage/migration-adapter.js +79 -0
  118. package/dist/src/storage/migration-copy.js +108 -0
  119. package/dist/src/storage/migration-protocol.js +14 -197
  120. package/dist/src/storage/migration-records.js +186 -0
  121. package/dist/src/storage/migration-types.js +12 -0
  122. package/dist/src/tracker/forge-http.js +2 -1
  123. package/dist/src/tracker/issue-pages.js +110 -0
  124. package/package.json +1 -1
  125. package/schemas/mission-v1.schema.json +335 -0
  126. package/schemas/models/model-catalog.v1.schema.json +19 -1
  127. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  128. package/schemas/models/model-policy.v1.schema.json +1 -1
  129. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  130. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  131. package/tools/agents/providers/rule-delivery.mjs +7 -126
  132. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -5,43 +5,18 @@
5
5
  */
6
6
  import Ajv2020 from 'ajv/dist/2020.js';
7
7
  import addFormats from 'ajv-formats';
8
- import { existsSync, readFileSync } from 'node:fs';
9
- import { dirname, resolve } from 'node:path';
10
- import { fileURLToPath } from 'node:url';
11
8
  import { z } from 'zod';
12
- import { integer, dateTime, url, number } from '../schema/zod-validation.mjs';
13
- import { createRequire } from 'node:module';
14
- import { compileWorkerVersionPattern, workerVersionRangeMismatch } from './worker-installation.mjs';
15
- import { normalizeProviderDefinitionId } from '../providers/provider-definitions.js';
16
- import { qualificationDigest } from './qualification-digest.mjs';
17
- import { DEFAULT_WORKER_SELECTION, WorkerSelectionPolicySchema, workerModelKey, ownWorkerRoleGuidance } from './worker-selection-policy.js';
9
+ import { url } from '../schema/zod-validation.mjs';
10
+ import { compileWorkerVersionPattern } from './worker-installation.mjs';
11
+ import { ProviderSchema, requireModelResource } from './policy-resources.js';
12
+ import { resetCatalogPolicyCache, validateWorkerModelObservation } from './catalog-policy.js';
13
+ import { hasSelectionRelevantConflicts } from './worker-review-lineage.js';
14
+ export { hasSelectionRelevantConflicts } from './worker-review-lineage.js';
15
+ export * from './catalog-policy.js';
16
+ export * from './worker-invocation-policy.js';
17
+ import { DEFAULT_WORKER_SELECTION, workerModelKey, ownWorkerRoleGuidance } from './worker-selection-policy.js';
18
18
  export { DEFAULT_WORKER_SELECTION, WorkerSelectionPolicySchema, mergeWorkerSelection, workerModelKey, isCanonicalWorkerModelKey } from './worker-selection-policy.js';
19
- const require = createRequire(import.meta.url);
20
- function requireModelResource(filename) {
21
- const candidates = [
22
- `./${filename}`,
23
- `../../agentic/code/providers/${filename}`,
24
- `../../../agentic/code/providers/${filename}`,
25
- `../../schemas/models/${filename}`,
26
- `../../../schemas/models/${filename}`,
27
- ];
28
- for (const candidate of candidates) {
29
- try {
30
- return require(candidate);
31
- }
32
- catch (error) {
33
- if (error?.code !== 'MODULE_NOT_FOUND')
34
- throw error;
35
- }
36
- }
37
- throw new Error(`Model policy resource is missing: ${filename}`);
38
- }
39
19
  const capabilityData = requireModelResource('model-capabilities.v1.json');
40
- const catalogData = requireModelResource('model-catalog.v1.json');
41
- const ProviderSchema = z.enum([
42
- 'antigravity', 'claude', 'codex', 'copilot', 'cursor', 'deepseek-harness', 'factory', 'grokbot', 'grok-build', 'hermes',
43
- 'muse', 'opencode', 'openclaw', 'openhuman', 'omp', 'pi', 'warp', 'windsurf',
44
- ]);
45
20
  const OutcomeSchema = z.enum([
46
21
  'native', 'compiled', 'inherited', 'global-only', 'informational', 'unsupported',
47
22
  ]);
@@ -103,112 +78,7 @@ const CapabilityRegistrySchema = z.strictObject({
103
78
  workerHealthAdapters: z.record(z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), WorkerAdapterSchema)
104
79
  .refine(value => Object.keys(value).length <= 128, 'At most 128 worker health descriptors').optional(),
105
80
  });
106
- const CatalogModelSchema = z.object({
107
- id: z.string().min(1),
108
- status: z.enum(['active', 'deprecated', 'unverified']),
109
- fallback: z.string().min(1).optional(),
110
- observed: z.boolean().optional(),
111
- });
112
- const CatalogProviderSchema = z.object({
113
- roles: z.object({
114
- reasoning: CatalogModelSchema,
115
- coding: CatalogModelSchema,
116
- efficiency: CatalogModelSchema,
117
- }),
118
- sourceUrl: url(),
119
- verifiedAt: z.string().date(),
120
- });
121
- let catalogValidators;
122
- function getCatalogValidators() {
123
- if (!catalogValidators) {
124
- const here = dirname(fileURLToPath(import.meta.url));
125
- const file = [resolve(here, '../../schemas/models/model-catalog.v1.schema.json'), resolve(here, '../../../schemas/models/model-catalog.v1.schema.json')].find(existsSync);
126
- if (!file)
127
- throw new Error('Model catalog schema is unavailable');
128
- const schema = JSON.parse(readFileSync(file, 'utf8'));
129
- const ajv = addFormats(new Ajv2020({ strict: false, allErrors: true }));
130
- ajv.addSchema(schema);
131
- catalogValidators = { catalog: ajv.getSchema(schema.$id), observation: ajv.compile({ $ref: `${schema.$id}#/$defs/workerObservation` }), discovery: ajv.compile({ $ref: `${schema.$id}#/properties/discovery` }) };
132
- }
133
- return catalogValidators;
134
- }
135
- export function validateWorkerModelObservation(input) {
136
- const validate = getCatalogValidators().observation;
137
- return validate(input) ? [] : (validate.errors ?? []).map(error => `${error.instancePath}: ${error.message}`);
138
- }
139
- const WorkerObservationSchema = z.custom(input => validateWorkerModelObservation(input).length === 0);
140
- const DiscoveredWorkerModelSchema = z.looseObject({
141
- id: z.string().min(1), apiModelId: z.string().optional(), llmProvider: z.string().optional(),
142
- hidden: z.boolean().optional(), reasoningEfforts: z.array(z.string()).optional(), worker: WorkerObservationSchema.optional(),
143
- }).superRefine((model, context) => {
144
- if (!model.worker)
145
- return;
146
- if (model.hidden !== undefined && model.worker.visibility !== (model.hidden ? 'hidden' : 'listed'))
147
- context.addIssue({ code: 'custom', path: ['worker', 'visibility'], message: 'Conflicting visibility evidence' });
148
- const efforts = model.reasoningEfforts ?? [];
149
- const observed = model.worker.efforts.map(e => e.id);
150
- if (efforts.length !== observed.length || new Set(observed).size !== observed.length || efforts.some(e => !observed.includes(e)))
151
- context.addIssue({ code: 'custom', path: ['worker', 'efforts'], message: 'Conflicting effort evidence' });
152
- });
153
- const ProviderDiscoverySchema = z.looseObject({
154
- provider: z.string(), source: z.enum(['native', 'cache', 'remote', 'static']), observedAt: dateTime({ offset: true }),
155
- runtimeVersion: z.string().optional(), accountScope: z.enum(['local-account', 'local-runtime', 'public-feed', 'static']),
156
- accountBinding: z.string().min(1).optional(), models: z.array(DiscoveredWorkerModelSchema),
157
- });
158
- const DiscoverySchema = z.strictObject({
159
- source: z.enum(['native', 'cache', 'remote', 'static']), upstreamSource: z.enum(['native', 'remote', 'static']).optional(),
160
- fetchedAt: dateTime({ offset: true }), remoteUrl: url().optional(), remoteError: z.string().optional(),
161
- inventorySignature: z.string().optional(), providers: z.record(z.string(), ProviderDiscoverySchema),
162
- instances: z.record(z.string(), ProviderDiscoverySchema).optional(),
163
- });
164
- /** Optional enrichment is checked without tightening legacy catalog rows. */
165
- export function validateWorkerCatalogDiscovery(input) {
166
- if (input === undefined)
167
- return [];
168
- const validate = getCatalogValidators().discovery;
169
- if (!validate(input))
170
- return (validate.errors ?? []).map(error => `${error.instancePath}: ${error.message}`);
171
- const parsed = DiscoverySchema.safeParse(input);
172
- return parsed.success ? [] : parsed.error.issues.map(issue => `${issue.path.join('/')}: ${issue.message}`);
173
- }
174
- const CatalogSchema = z.strictObject({
175
- version: z.literal('1.0.0'),
176
- $schema: z.literal('../../../schemas/models/model-catalog.v1.schema.json').optional(),
177
- discovery: DiscoverySchema.optional(),
178
- refreshedAt: z.string().date(),
179
- staleAfterDays: integer().positive(),
180
- providers: z.partialRecord(ProviderSchema, CatalogProviderSchema),
181
- });
182
- const PolicySchema = z.strictObject({
183
- role: z.enum(['reasoning', 'coding', 'efficiency']),
184
- tier: z.enum(['economy', 'standard', 'premium']),
185
- effort: z.enum(['minimal', 'low', 'medium', 'high', 'xhigh']).optional(),
186
- override: z.string().min(1).optional(),
187
- });
188
- const UserProjectModelConfigSchema = z.looseObject({
189
- defaults: z.strictObject({
190
- tier: z.enum(['economy', 'standard', 'premium', 'max-quality']).optional(),
191
- provider: ProviderSchema.optional(),
192
- workerSelection: WorkerSelectionPolicySchema.optional(),
193
- }).optional(),
194
- agentOverrides: z.record(z.string(), z.strictObject({
195
- 'model-tier': z.enum(['economy', 'standard', 'premium', 'max-quality']).optional(),
196
- 'model-override': z.string().min(1).optional(),
197
- rationale: z.string().min(1).optional(),
198
- })).optional(),
199
- customTiers: z.record(z.string(), z.strictObject({
200
- description: z.string().min(1),
201
- costMultiplier: number().check(z.gte(0)).meta({ type: 'number', minimum: 0 }),
202
- roleMapping: z.strictObject({
203
- reasoning: z.enum(['reasoning', 'coding', 'efficiency']),
204
- coding: z.enum(['reasoning', 'coding', 'efficiency']),
205
- efficiency: z.enum(['reasoning', 'coding', 'efficiency']),
206
- }),
207
- })).optional(),
208
- rationale: z.string().optional(),
209
- });
210
81
  let registryCache = null;
211
- let catalogCache = null;
212
82
  export function loadProviderModelCapabilities() {
213
83
  if (!registryCache) {
214
84
  registryCache = parseProviderModelCapabilities(capabilityData);
@@ -345,410 +215,9 @@ export function assertWorkerSelectionAllowed(selected, context) {
345
215
  }
346
216
  }
347
217
  }
348
- const WORKER_CONTROLS = ['model', 'effort', 'sandbox', 'approval', 'workdir',
349
- 'finalOutput', 'stdin', 'structuredOutput', 'stepCap', 'trust', 'children', 'fallback'];
350
- /** Caller controls only add restrictions. Absent resolved authority, require all controls. */
351
- export function requiredWorkerControls(context) {
352
- if (context.requiredControls !== undefined && (!Array.isArray(context.requiredControls)
353
- || context.requiredControls.some(name => typeof name !== 'string' || !name))) {
354
- throw new Error('Worker required controls must be an array of control names');
355
- }
356
- const controls = new Set(WORKER_CONTROLS);
357
- if (context.policy && context.permissions) {
358
- if (context.effort === null)
359
- controls.delete('effort');
360
- // Step limits are observed unless explicitly required by the trusted route.
361
- controls.delete('stepCap');
362
- if (context.policy.guards?.denySelfDelegation === false)
363
- controls.delete('children');
364
- }
365
- for (const name of context.requiredControls ?? [])
366
- controls.add(name);
367
- return [...controls];
368
- }
369
- /** The same sandbox requirement governs inspection, resolution and rendering. */
370
- export function requiresWorkerExternalBoundary(adapter, selection) {
371
- if (hasOperatorWorkerNativeRenderer(adapter)
372
- && (selection.permissions?.sandbox !== 'external' || selection.transport === 'shell'))
373
- return false;
374
- const providers = [adapter.id, adapter.provider].map(normalizeProviderDefinitionId);
375
- return requiredWorkerControls(selection).includes('sandbox')
376
- && (providers.some(provider => provider && ['claude', 'muse', 'hermes'].includes(provider))
377
- || adapter.invocationProfiles?.[selection.profile]?.controls.sandbox?.support === 'external'
378
- || selection.permissions?.sandbox === 'external');
379
- }
380
- /** Explicit local operator approval for the direct CLI baseline (decision 17).
381
- * This is not backend attestation or a live negative-fixture result. */
382
- export const WORKER_OPERATOR_APPROVAL = 'aiwg doctor --probe-workers --qualify';
383
- export function isOperatorQualifiedWorkerAdapter(adapter) {
384
- return adapter.qualification.status === 'qualified'
385
- && adapter.qualification.evidenceRef === WORKER_OPERATOR_APPROVAL;
386
- }
387
- /** Bundled renderers with an independently checked native direct baseline. */
388
- export function hasOperatorWorkerNativeRenderer(adapter) {
389
- return isOperatorQualifiedWorkerAdapter(adapter) && ['codex', 'muse'].includes(adapter.provider)
390
- && adapter.invocation.kind === 'process';
391
- }
392
- /** The process-group launch boundary requires the independently verified native renderer. */
393
- export function assertWorkerLaunchMode(adapter, permissions = {}) {
394
- const mode = permissions.launchMode ?? 'systemd-unit';
395
- if (!['systemd-unit', 'native-sandbox'].includes(mode)) {
396
- throw new WorkerPolicyError('OG_CONFIG_REFERENCE', 'Unknown worker launch mode');
397
- }
398
- if (mode === 'native-sandbox' && (!hasOperatorWorkerNativeRenderer(adapter)
399
- || !['read-only', 'workspace-write'].includes(permissions.sandbox ?? '')
400
- || (permissions.transport ?? 'shell') !== 'shell')) {
401
- throw new WorkerPolicyError('OG_POLICY_RELAXATION', 'native-sandbox requires a verified native read-only or workspace-write shell renderer');
402
- }
403
- }
404
- /** Check the actual argv before granting the direct renderer exception. */
405
- export function assertOperatorWorkerNativeArgv(adapter, argv, selected, permissions) {
406
- if (!hasOperatorWorkerNativeRenderer(adapter))
407
- return;
408
- const args = argv.slice(1);
409
- const requirePair = (flag, value) => {
410
- if (args.filter(token => token === flag).length !== 1 || args[args.indexOf(flag) + 1] !== value) {
411
- throw new Error(`Requested native worker control is absent or ambiguous: ${flag}`);
412
- }
413
- };
414
- const sandbox = permissions?.sandbox ?? 'workspace-write';
415
- if (!['read-only', 'workspace-write', 'external'].includes(sandbox))
416
- throw new Error('Requested sandbox has no verified native renderer');
417
- if (adapter.provider === 'muse') {
418
- requirePair('--provider', 'meta');
419
- requirePair('--model', selected.model);
420
- if (selected.effort.value === null)
421
- throw new Error('Muse requires explicit effort');
422
- requirePair('--reasoning-effort', selected.effort.value);
423
- if ((permissions?.approval ?? 'never') !== 'never')
424
- throw new Error('Muse direct worker approval requires never');
425
- requirePair('--approval-mode', 'never');
426
- requirePair('--approval-judge', 'off');
427
- // #3685: Tier 0 Muse workers run in conductor-prepared worker trees, so
428
- // project context must load. An argv without exactly one
429
- // --trust-workspace would run with project_sources=0 (no project
430
- // rules/skills/hooks) and is refused rather than silently degraded.
431
- if (args.filter(token => token === '--trust-workspace').length !== 1) {
432
- throw new Error('Requested native worker control is absent or ambiguous: --trust-workspace');
433
- }
434
- if (args.includes('--disable-sandbox') !== (sandbox === 'external')) {
435
- throw new Error('Requested native worker sandbox control does not match');
436
- }
437
- if (sandbox === 'read-only' && (!args.includes('--disable-write') || !args.includes('--disable-shell'))) {
438
- throw new Error('Requested native worker read-only control is absent');
439
- }
440
- if (args[0] !== 'exec')
441
- throw new Error('Native worker controls could be overridden');
442
- const seen = new Set();
443
- for (let index = 1; index < args.length; index++) {
444
- const flag = args[index];
445
- if (seen.has(flag))
446
- throw new Error('Native worker controls could be overridden');
447
- seen.add(flag);
448
- if (['--json', '--trust-workspace', '--disable-write', '--disable-shell', '--disable-sandbox'].includes(flag))
449
- continue;
450
- if (!['--provider', '--model', '--reasoning-effort', '--workspace', '--approval-mode', '--approval-judge',
451
- '--max-model-steps', '--output-schema', '--prompt-file'].includes(flag) || index + 1 >= args.length) {
452
- throw new Error('Native worker controls could be overridden');
453
- }
454
- index++;
455
- }
456
- return;
457
- }
458
- requirePair('-m', selected.model);
459
- requirePair('-s', sandbox === 'external' ? 'danger-full-access' : sandbox);
460
- if ((permissions?.approval ?? 'never') !== 'never')
461
- throw new Error('Codex direct worker approval requires never');
462
- for (const [name, value] of [['model_reasoning_effort', selected.effort.value], ['approval_policy', permissions?.approval ?? 'never']]) {
463
- const overrides = args.flatMap((token, index) => token === '-c' ? [args[index + 1]] : [])
464
- .filter(token => token?.split('=')[0] === name);
465
- if (value === null || overrides.length !== 1 || overrides[0] !== `${name}=${JSON.stringify(value)}`) {
466
- throw new Error(`Requested native worker control is absent or ambiguous: ${name}`);
467
- }
468
- }
469
- // Permit only the bounded native command grammar. Alternate/combined flags,
470
- // profiles and additional config overrides must not undo the checked pairs.
471
- if (args[0] !== '--no-daemon' || args[1] !== 'exec' || !args.includes('--ignore-user-config')) {
472
- throw new Error('Native worker controls could be overridden');
473
- }
474
- for (let index = 2; index < args.length; index++) {
475
- const flag = args[index];
476
- if (['--ignore-user-config', '--skip-git-repo-check', '--json'].includes(flag))
477
- continue;
478
- if (flag === '-' && index === args.length - 1)
479
- continue;
480
- if (!['-m', '-s', '-c', '-C', '-o'].includes(flag) || index + 1 >= args.length) {
481
- throw new Error('Native worker controls could be overridden');
482
- }
483
- const value = args[++index];
484
- if (flag === '-c' && !['model_reasoning_effort', 'approval_policy'].includes(value.split('=')[0])) {
485
- throw new Error('Native worker controls could be overridden');
486
- }
487
- }
488
- }
489
- /** Inspect declared controls before installation evidence can be produced. This
490
- * does not authorize invocation; consumers must use qualifyWorkerInvocationProfile. */
491
- export function inspectWorkerInvocationProfile(adapter, selection) {
492
- WorkerAdapterSchema.parse(adapter);
493
- const profile = adapter.invocationProfiles?.[selection.profile];
494
- if (adapter.qualification.status !== 'qualified' || !profile || profile.transport !== selection.transport) {
495
- throw new Error('Worker transport/profile is unqualified');
496
- }
497
- // Shared range predicate (worker-installation.mjs): read-only surfaces
498
- // (worker-direct-readiness.ts) reuse the same helper instead of
499
- // re-deriving this inspector's range logic.
500
- if (workerVersionRangeMismatch(selection.cliVersion, [adapter.supportedVersions, profile.supportedVersions]) !== null) {
501
- throw new Error('Worker CLI version is outside qualified profile');
502
- }
503
- // The direct CLI baseline trusts the installed driver's bounded renderer.
504
- // Explicit extra controls still require the existing qualified mappings.
505
- if (hasOperatorWorkerNativeRenderer(adapter) && profile.transport === 'shell') {
506
- if (selection.permissions?.sandbox && !['read-only', 'workspace-write', 'external'].includes(selection.permissions.sandbox)) {
507
- throw new Error('Requested sandbox has no verified native renderer');
508
- }
509
- for (const name of selection.requiredControls ?? []) {
510
- if (!['native', 'external'].includes(profile.controls[name]?.support)) {
511
- throw new Error(`Required worker control is unqualified: ${name}`);
512
- }
513
- }
514
- return profile;
515
- }
516
- const requiredControls = requiredWorkerControls(selection);
517
- if (requiresWorkerExternalBoundary(adapter, selection)
518
- && (!selection.boundary || !['os', 'executor'].includes(selection.boundary.kind)
519
- || !selection.boundary.identity || !/^sha256:[a-f0-9]{64}$/.test(selection.boundary.digest)
520
- || !selection.boundary.controls?.includes('sandbox'))) {
521
- throw new Error('Required worker control sandbox needs a qualified external enforcing boundary');
522
- }
523
- for (const name of requiredControls) {
524
- if ((name === 'children' && !adapter.capabilities.nestedAgentsControllable)
525
- || (name === 'fallback' && !adapter.capabilities.nativeFallbackControllable)) {
526
- throw new Error(`Required worker control is unqualified: ${name}`);
527
- }
528
- const control = profile.controls[name];
529
- if (!control || !['native', 'external'].includes(control.support) || !control.mapping || !control.evidence) {
530
- throw new Error(`Required worker control is unqualified: ${name}`);
531
- }
532
- }
533
- if (selection.transport === 'mcp' && (!selection.mcp || !profile.mcp
534
- || Object.keys(profile.mcp).some(key => profile.mcp[key]
535
- !== selection.mcp[key]))) {
536
- throw new Error('MCP server/tool/schema/session qualification changed');
537
- }
538
- return profile;
539
- }
540
- /** Qualification is transport/version scoped; declaration or boundary claims
541
- * alone cannot qualify an external sandbox. The invocation owner rechecks the
542
- * installation scope, including this boundary, against the recorded digest. */
543
- export function qualifyWorkerInvocationProfile(adapter, selection) {
544
- const profile = inspectWorkerInvocationProfile(adapter, selection);
545
- if (isOperatorQualifiedWorkerAdapter(adapter) && selection.policy?.guards?.denySelfDelegation !== false
546
- && !adapter.capabilities.nestedAgentsControllable) {
547
- throw new Error('Required worker control is unqualified: children');
548
- }
549
- if (requiresWorkerExternalBoundary(adapter, selection) && !profile.installationQualification) {
550
- throw new Error('Required worker control sandbox needs matching installation qualification for its external boundary');
551
- }
552
- return profile;
553
- }
554
- /** Health timing evidence belongs to the exact execution profile and tuple.
555
- * Fixture qualification does not become authenticated/live qualification. */
556
- export function workerHealthScopeDigest(profile, scope) {
557
- const { installationQualification: _installation, health, ...executionProfile } = profile;
558
- const { qualification: _qualification, ...signals } = health ?? {};
559
- return qualificationDigest({ profile: executionProfile, health: signals,
560
- profileRef: scope.profileRef, cliVersion: scope.cliVersion, accountScope: scope.accountScope,
561
- modelKey: workerModelKey(scope.inferenceProvider, scope.model), effort: scope.effort });
562
- }
563
- /** Reused by observation and invocation owners before using qualified health.
564
- * Older profiles have no qualification record and retain degraded observation. */
565
- export function assertWorkerHealthQualification(profile, scope) {
566
- const evidence = profile.health?.qualification;
567
- if (!evidence)
568
- return;
569
- if (evidence.status === 'unqualified') {
570
- if (profile.health?.coverage === 'full' || profile.health?.handshake === 'semantic') {
571
- throw new Error('Unqualified worker health cannot assert full or semantic coverage');
572
- }
573
- return;
574
- }
575
- const modelKey = workerModelKey(scope.inferenceProvider, scope.model);
576
- if (evidence.cliVersion !== scope.cliVersion || evidence.accountScope !== scope.accountScope
577
- || evidence.profileRef !== scope.profileRef || evidence.modelKey !== modelKey || evidence.effort !== scope.effort
578
- || !profile.health?.qualifiedAt || !Number.isFinite(Date.parse(profile.health.qualifiedAt))
579
- || evidence.scopeDigest !== workerHealthScopeDigest(profile, scope)
580
- || (evidence.status === 'live-qualified' && evidence.authorizedLive !== true)) {
581
- throw new Error('Worker health qualification scope changed or lacks live authorization');
582
- }
583
- }
584
- export function parseProviderModelCatalog(input) {
585
- const validate = getCatalogValidators().catalog;
586
- if (!validate(input))
587
- throw new Error(`MODEL_POLICY_INVALID: ${validate.errors?.map(e => `${e.instancePath} ${e.message}`).join('; ')}`);
588
- return CatalogSchema.parse(input);
589
- }
590
- export function loadProviderModelCatalog() {
591
- if (!catalogCache)
592
- catalogCache = parseProviderModelCatalog(catalogData);
593
- return catalogCache;
594
- }
595
- export function validateCanonicalModelPolicy(input) {
596
- const parsed = PolicySchema.safeParse(input);
597
- if (!parsed.success) {
598
- return {
599
- valid: false,
600
- diagnostics: parsed.error.issues.map(issue => ({
601
- code: 'MODEL_POLICY_INVALID',
602
- severity: 'error',
603
- message: `${issue.path.join('.') || 'policy'}: ${issue.message}`,
604
- })),
605
- };
606
- }
607
- return { valid: true, policy: parsed.data, diagnostics: [] };
608
- }
609
- export function validateUserProjectModelConfig(input) {
610
- const parsed = UserProjectModelConfigSchema.safeParse(input);
611
- if (parsed.success)
612
- return { valid: true, diagnostics: [] };
613
- return {
614
- valid: false,
615
- diagnostics: parsed.error.issues.map(issue => ({
616
- code: issue.path.includes('workerSelection') ? 'OG_CONFIG_REFERENCE' : 'MODEL_POLICY_INVALID',
617
- pointer: `/${issue.path.map(p => String(p).replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`,
618
- severity: 'error',
619
- message: `${issue.path.join('.') || 'config'}: ${issue.message}`,
620
- })),
621
- };
622
- }
623
- function catalogRole(policy) {
624
- if (policy.tier === 'economy')
625
- return 'efficiency';
626
- if (policy.tier === 'premium')
627
- return 'reasoning';
628
- return policy.role;
629
- }
630
- function fieldNames(provider) {
631
- switch (provider) {
632
- case 'antigravity': return { model: 'model', effort: 'effort' };
633
- case 'codex': return { model: 'model', effort: 'model_reasoning_effort' };
634
- case 'factory': return { model: 'model', effort: 'reasoningEffort' };
635
- case 'claude': return { model: 'model', effort: 'effort' };
636
- case 'copilot':
637
- case 'cursor':
638
- case 'opencode': return { model: 'model' };
639
- case 'grok-build': return { model: 'model', effort: 'effort' };
640
- case 'omp': return { model: 'model', effort: 'thinkingLevel' };
641
- case 'pi': return { model: 'model', effort: 'thinking' };
642
- case 'openhuman': return { model: 'model_hint' };
643
- case 'openclaw': return { model: 'subagents.model' };
644
- default: return {};
645
- }
646
- }
647
- export function compileModelPolicy(input) {
648
- const registry = loadProviderModelCapabilities();
649
- const catalog = input.catalog ?? loadProviderModelCatalog();
650
- const capability = registry.providers[input.provider];
651
- if (!capability)
652
- throw new Error(`Unknown provider model capability: ${input.provider}`);
653
- const surface = capability[input.artifact];
654
- const diagnostics = [];
655
- const fields = {};
656
- const names = fieldNames(input.provider);
657
- const catalogProvider = catalog.providers[input.provider];
658
- if (!catalogProvider)
659
- throw new Error(`Unknown provider model catalog: ${input.provider}`);
660
- const catalogEntry = catalogProvider.roles[catalogRole(input.policy)];
661
- const effectiveModel = input.policy.override ?? catalogEntry.id;
662
- if (surface === 'unsupported' || surface === 'informational') {
663
- diagnostics.push({
664
- code: surface === 'unsupported' ? 'MODEL_SURFACE_UNSUPPORTED' : 'MODEL_SURFACE_DEGRADED',
665
- severity: surface === 'unsupported' ? 'warning' : 'info',
666
- provider: input.provider,
667
- message: `${input.provider} cannot enforce ${input.artifact}-scoped model policy (${surface})`,
668
- });
669
- }
670
- else if (surface === 'global-only' || surface === 'inherited') {
671
- diagnostics.push({
672
- code: 'MODEL_SURFACE_DEGRADED', severity: 'warning', provider: input.provider,
673
- message: `${input.provider} compiles ${input.artifact} policy as ${surface}`,
674
- });
675
- }
676
- else if (names.model) {
677
- fields[names.model] = effectiveModel;
678
- }
679
- if (input.policy.effort) {
680
- if (names.effort && capability.effortValues?.includes(input.policy.effort)
681
- && surface !== 'unsupported' && surface !== 'informational') {
682
- fields[names.effort] = input.policy.effort;
683
- }
684
- else {
685
- diagnostics.push({
686
- code: 'MODEL_EFFORT_UNSUPPORTED', severity: 'warning', provider: input.provider,
687
- message: `${input.provider} does not enforce effort=${input.policy.effort} on ${input.artifact} artifacts`,
688
- });
689
- }
690
- }
691
- if (input.policy.override) {
692
- diagnostics.push({
693
- code: 'MODEL_OVERRIDE_UNVERIFIED', severity: 'warning', provider: input.provider,
694
- message: `Exact override ${input.policy.override} requires provider validation`,
695
- });
696
- }
697
- else if (catalogEntry.status === 'deprecated') {
698
- diagnostics.push({
699
- code: 'MODEL_CATALOG_DEPRECATED', severity: 'error', provider: input.provider,
700
- message: `${catalogEntry.id} is deprecated`,
701
- });
702
- }
703
- else if (catalogEntry.status === 'unverified') {
704
- diagnostics.push({
705
- code: 'MODEL_CATALOG_UNVERIFIED', severity: 'warning', provider: input.provider,
706
- message: `${catalogEntry.id} has not been observed on ${input.provider}`,
707
- });
708
- }
709
- const now = input.now ?? new Date();
710
- const refreshedAt = new Date(`${catalog.refreshedAt}T00:00:00Z`);
711
- const ageDays = Math.floor((now.getTime() - refreshedAt.getTime()) / 86_400_000);
712
- if (ageDays > catalog.staleAfterDays) {
713
- diagnostics.push({
714
- code: 'MODEL_CATALOG_STALE', severity: 'warning', provider: input.provider,
715
- message: `Model catalog is ${ageDays} days old (limit ${catalog.staleAfterDays})`,
716
- });
717
- }
718
- return {
719
- provider: input.provider,
720
- artifact: input.artifact,
721
- outcome: surface,
722
- effectiveModel: surface === 'unsupported' || surface === 'informational' ? undefined : effectiveModel,
723
- effectiveEffort: fields[names.effort ?? ''] ? input.policy.effort : undefined,
724
- fields,
725
- target: capability.configTarget,
726
- source: input.policy.override ? 'override' : (surface === 'inherited' ? 'inheritance' : 'catalog'),
727
- diagnostics,
728
- };
729
- }
730
- function tomlString(value) { return JSON.stringify(value); }
731
- export function renderCodexAgentToml(definition) {
732
- const required = [
733
- ['name', definition.name],
734
- ['description', definition.description],
735
- ['developer_instructions', definition.developerInstructions],
736
- ];
737
- for (const [field, value] of required) {
738
- if (!value.trim())
739
- throw new Error(`Codex custom agent requires ${field}`);
740
- }
741
- const lines = required.map(([field, value]) => `${field} = ${tomlString(value)}`);
742
- if (definition.model)
743
- lines.push(`model = ${tomlString(definition.model)}`);
744
- if (definition.modelReasoningEffort) {
745
- lines.push(`model_reasoning_effort = ${tomlString(definition.modelReasoningEffort)}`);
746
- }
747
- return `${lines.join('\n')}\n`;
748
- }
749
218
  export function resetModelPolicyCachesForTests() {
750
219
  registryCache = null;
751
- catalogCache = null;
220
+ resetCatalogPolicyCache();
752
221
  }
753
222
  // Dispatch rechecks consume the effective policy from resolution. Keep these
754
223
  // predicates shared with route validation so defaults and restrictions agree.
@@ -794,11 +263,6 @@ function isWorkerEvidenceStale(observation, policy, now) {
794
263
  return observed > now || now - observed > blockHours * 3600000
795
264
  || (verified !== undefined && (verified > now || now - verified > policyDays * 86400000));
796
265
  }
797
- /** Optional catalog facts do not veto otherwise qualified selection. */
798
- export function hasSelectionRelevantConflicts(observation) {
799
- const relevant = /^\/(?:lifecycle|availability|visibility|hidden|efforts|effortSupport|reasoningEfforts|sunsetAt|retiredAt|replacementModelId|supersededBy|dataUse|id|apiModelId|llmProvider|cliId|family|runtimeVersion|accountScope|endpointFingerprint|identityEvidence|contextTokens|maxOutputTokens|qualification)(?:\/|$)/;
800
- return observation.conflicts?.some(conflict => relevant.test(conflict.field)) ?? false;
801
- }
802
266
  /** Validate a resolved route without launching or creating managed state. */
803
267
  export function validateWorkerPolicyRoute(input) {
804
268
  const diagnostics = [];
@@ -5,6 +5,8 @@
5
5
  * @implements @.aiwg/architecture/ADR-015-enhanced-model-selection.md
6
6
  * @architecture @.aiwg/architecture/enhanced-model-selection-design.md
7
7
  */
8
+ import { normalizeModelTier, TIER_ORDER, flagshipElected, isFlagshipModel } from './tiers.js';
9
+ import { compileModelPolicy } from './provider-policy.js';
8
10
  import { ConfigLoader } from './config-loader.js';
9
11
  import { resolveWorkerPlan } from './worker-plan.js';
10
12
  export * from './worker-plan.js';
@@ -39,23 +41,39 @@ export class ModelResolver {
39
41
  const config = await this.getConfig();
40
42
  // Determine effective tier and role
41
43
  const { tier, role, source } = this.determineEffectiveTierAndRole(agentName, agentRole, agentTier, config);
44
+ if (normalizeModelTier(tier) === 'flagship' && !flagshipElected({ ...this.options, agentName })) {
45
+ throw new Error('Flagship models are user-elected only');
46
+ }
42
47
  // Check for direct model override (bypasses tier system)
43
48
  const override = config.agentOverrides[agentName];
44
49
  if (override?.['model-override']) {
50
+ if (isFlagshipModel(override['model-override']) && !flagshipElected({ ...this.options, agentName }))
51
+ throw new Error('Flagship models are user-elected only');
45
52
  return {
46
53
  modelId: override['model-override'],
47
54
  provider: this.options.provider || config.defaults.provider,
48
- tier,
55
+ tier: normalizeModelTier(tier),
49
56
  role,
50
57
  source: 'agent',
51
58
  };
52
59
  }
53
- // Resolve tier + role to concrete model
54
- const modelId = this.resolveModelId(tier, role, config);
60
+ const provider = this.options.provider || config.defaults.provider;
61
+ const modelId = normalizeModelTier(tier) === 'flagship'
62
+ ? compileModelPolicy({
63
+ provider: provider === 'openai' ? 'codex' : provider,
64
+ artifact: 'agent', policy: { tier: 'flagship', role },
65
+ election: this.options.election, flagshipAllowlist: this.options.flagshipAllowlist, agentName,
66
+ }).effectiveModel
67
+ : this.resolveModelId(tier, role, config);
68
+ if (!modelId)
69
+ throw new Error(`No supported model binding for ${provider}/${tier}`);
70
+ if (isFlagshipModel(modelId) && !flagshipElected({ ...this.options, agentName })) {
71
+ throw new Error('Flagship models are user-elected only');
72
+ }
55
73
  return {
56
74
  modelId,
57
75
  provider: this.options.provider || config.defaults.provider,
58
- tier,
76
+ tier: normalizeModelTier(tier),
59
77
  role,
60
78
  source,
61
79
  };
@@ -132,9 +150,9 @@ export class ModelResolver {
132
150
  * Return the higher of two tiers
133
151
  */
134
152
  maxTier(tier1, tier2) {
135
- const tierOrder = ['economy', 'standard', 'premium', 'max-quality'];
136
- const index1 = tierOrder.indexOf(tier1);
137
- const index2 = tierOrder.indexOf(tier2);
153
+ const tierOrder = TIER_ORDER;
154
+ const index1 = tierOrder.indexOf(normalizeModelTier(tier1));
155
+ const index2 = tierOrder.indexOf(normalizeModelTier(tier2));
138
156
  return tierOrder[Math.max(index1, index2)];
139
157
  }
140
158
  /**
@@ -227,7 +245,7 @@ export class ModelResolver {
227
245
  async listTiers() {
228
246
  const config = await this.getConfig();
229
247
  return Object.entries(config.tiers).map(([tierName, tierDef]) => ({
230
- tier: tierName,
248
+ tier: normalizeModelTier(tierName),
231
249
  description: tierDef.description,
232
250
  costMultiplier: tierDef.costMultiplier,
233
251
  }));