@aiwg/cli 2026.10.3 → 2026.10.5

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 (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +69 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +286 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +130 -0
  23. package/dist/src/artifacts/index-refresh.js +591 -0
  24. package/dist/src/artifacts/index-worker.mjs +11 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +137 -43
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +95 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +252 -12
  207. package/dist/src/sessions/contracts.js +50 -3
  208. package/dist/src/sessions/coverage-preflight.js +27 -2
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +145 -26
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +105 -0
  218. package/dist/src/sessions/repository.js +151 -54
  219. package/dist/src/sessions/workspace-discovery.js +292 -60
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -64,6 +64,7 @@ export const WorkerAdapterSchema = z.custom().superRefine((value, context) => {
64
64
  }
65
65
  });
66
66
  const CapabilityEntrySchema = z.object({
67
+ sessionEnvAllowlist: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).optional(),
67
68
  agent: OutcomeSchema,
68
69
  skill: OutcomeSchema,
69
70
  globalChild: OutcomeSchema,
@@ -497,12 +498,68 @@ export function requiredWorkerControls(context) {
497
498
  }
498
499
  /** The same sandbox requirement governs inspection, resolution and rendering. */
499
500
  export function requiresWorkerExternalBoundary(adapter, selection) {
501
+ if (hasOperatorWorkerNativeRenderer(adapter) && selection.permissions?.sandbox !== 'external')
502
+ return false;
500
503
  const providers = [adapter.id, adapter.provider].map(normalizeProviderDefinitionId);
501
504
  return requiredWorkerControls(selection).includes('sandbox')
502
505
  && (providers.some(provider => provider && ['claude', 'muse', 'hermes'].includes(provider))
503
506
  || adapter.invocationProfiles?.[selection.profile]?.controls.sandbox?.support === 'external'
504
507
  || selection.permissions?.sandbox === 'external');
505
508
  }
509
+ /** Explicit local operator approval for the direct CLI baseline (decision 17).
510
+ * This is not backend attestation or a live negative-fixture result. */
511
+ export const WORKER_OPERATOR_APPROVAL = 'aiwg doctor --probe-workers --qualify';
512
+ export function isOperatorQualifiedWorkerAdapter(adapter) {
513
+ return adapter.qualification.status === 'qualified'
514
+ && adapter.qualification.evidenceRef === WORKER_OPERATOR_APPROVAL;
515
+ }
516
+ /** Only this bundled renderer has an independently checked native direct baseline. */
517
+ export function hasOperatorWorkerNativeRenderer(adapter) {
518
+ return isOperatorQualifiedWorkerAdapter(adapter) && adapter.provider === 'codex'
519
+ && adapter.invocation.kind === 'process';
520
+ }
521
+ /** Check the actual argv before granting the direct renderer exception. */
522
+ export function assertOperatorWorkerNativeArgv(adapter, argv, selected, permissions) {
523
+ if (!hasOperatorWorkerNativeRenderer(adapter))
524
+ return;
525
+ const args = argv.slice(1);
526
+ const requirePair = (flag, value) => {
527
+ if (args.filter(token => token === flag).length !== 1 || args[args.indexOf(flag) + 1] !== value) {
528
+ throw new Error(`Requested native worker control is absent or ambiguous: ${flag}`);
529
+ }
530
+ };
531
+ const sandbox = permissions?.sandbox ?? 'workspace-write';
532
+ if (!['read-only', 'workspace-write'].includes(sandbox))
533
+ throw new Error('Requested sandbox has no verified native renderer');
534
+ requirePair('-m', selected.model);
535
+ requirePair('-s', sandbox);
536
+ for (const [name, value] of [['model_reasoning_effort', selected.effort.value], ['approval_policy', permissions?.approval ?? 'never']]) {
537
+ const overrides = args.flatMap((token, index) => token === '-c' ? [args[index + 1]] : [])
538
+ .filter(token => token?.split('=')[0] === name);
539
+ if (value === null || overrides.length !== 1 || overrides[0] !== `${name}=${JSON.stringify(value)}`) {
540
+ throw new Error(`Requested native worker control is absent or ambiguous: ${name}`);
541
+ }
542
+ }
543
+ // Permit only the bounded native command grammar. Alternate/combined flags,
544
+ // profiles and additional config overrides must not undo the checked pairs.
545
+ if (args[0] !== '--no-daemon' || args[1] !== 'exec' || !args.includes('--ignore-user-config')) {
546
+ throw new Error('Native worker controls could be overridden');
547
+ }
548
+ for (let index = 2; index < args.length; index++) {
549
+ const flag = args[index];
550
+ if (['--ignore-user-config', '--skip-git-repo-check', '--json'].includes(flag))
551
+ continue;
552
+ if (flag === '-' && index === args.length - 1)
553
+ continue;
554
+ if (!['-m', '-s', '-c', '-C', '-o'].includes(flag) || index + 1 >= args.length) {
555
+ throw new Error('Native worker controls could be overridden');
556
+ }
557
+ const value = args[++index];
558
+ if (flag === '-c' && !['model_reasoning_effort', 'approval_policy'].includes(value.split('=')[0])) {
559
+ throw new Error('Native worker controls could be overridden');
560
+ }
561
+ }
562
+ }
506
563
  /** Inspect declared controls before installation evidence can be produced. This
507
564
  * does not authorize invocation; consumers must use qualifyWorkerInvocationProfile. */
508
565
  export function inspectWorkerInvocationProfile(adapter, selection) {
@@ -517,6 +574,23 @@ export function inspectWorkerInvocationProfile(adapter, selection) {
517
574
  if (workerVersionRangeMismatch(selection.cliVersion, [adapter.supportedVersions, profile.supportedVersions]) !== null) {
518
575
  throw new Error('Worker CLI version is outside qualified profile');
519
576
  }
577
+ // The direct CLI baseline trusts the installed driver's bounded renderer.
578
+ // Explicit extra controls still require the existing qualified mappings.
579
+ if (hasOperatorWorkerNativeRenderer(adapter) && profile.transport === 'shell') {
580
+ if (selection.permissions?.sandbox && !['read-only', 'workspace-write'].includes(selection.permissions.sandbox)) {
581
+ throw new Error('Requested sandbox has no verified native renderer');
582
+ }
583
+ if (selection.permissions?.sandbox === 'external' && (!selection.boundary
584
+ || !['os', 'executor'].includes(selection.boundary.kind) || !selection.boundary.controls.includes('sandbox'))) {
585
+ throw new Error('Requested external sandbox requires an enforcing boundary');
586
+ }
587
+ for (const name of selection.requiredControls ?? []) {
588
+ if (!['native', 'external'].includes(profile.controls[name]?.support)) {
589
+ throw new Error(`Required worker control is unqualified: ${name}`);
590
+ }
591
+ }
592
+ return profile;
593
+ }
520
594
  const requiredControls = requiredWorkerControls(selection);
521
595
  if (requiresWorkerExternalBoundary(adapter, selection)
522
596
  && (!selection.boundary || !['os', 'executor'].includes(selection.boundary.kind)
@@ -546,6 +620,10 @@ export function inspectWorkerInvocationProfile(adapter, selection) {
546
620
  * installation scope, including this boundary, against the recorded digest. */
547
621
  export function qualifyWorkerInvocationProfile(adapter, selection) {
548
622
  const profile = inspectWorkerInvocationProfile(adapter, selection);
623
+ if (isOperatorQualifiedWorkerAdapter(adapter) && selection.policy?.guards?.denySelfDelegation !== false
624
+ && !adapter.capabilities.nestedAgentsControllable) {
625
+ throw new Error('Required worker control is unqualified: children');
626
+ }
549
627
  if (requiresWorkerExternalBoundary(adapter, selection) && !profile.installationQualification) {
550
628
  throw new Error('Required worker control sandbox needs matching installation qualification for its external boundary');
551
629
  }
@@ -817,7 +895,7 @@ export function validateWorkerPolicyRoute(input) {
817
895
  const guards = input.policy.guards ?? DEFAULT_WORKER_SELECTION.guards;
818
896
  const guidance = input.policy.roleGuidance?.[input.role];
819
897
  if (observation.qualification.status !== 'qualified' || !observation.qualification.roles.includes(input.role))
820
- deny('OG_ADAPTER_UNQUALIFIED', '/worker/qualification', 'Role lacks qualified evidence');
898
+ deny('OG_ADAPTER_UNQUALIFIED', '/worker/qualification', 'Role lacks qualified evidence. Run aiwg doctor --probe-workers --qualify.');
821
899
  if (!guidance)
822
900
  deny('OG_CONFIG_REFERENCE', `/defaults/workerSelection/roleGuidance/${input.role}`, 'Role guidance is unavailable');
823
901
  const substitution = input.substitution?.model === input.selectedModel
@@ -855,13 +933,22 @@ export function validateWorkerPolicyRoute(input) {
855
933
  else {
856
934
  const effort = observation.efforts.find(e => e.id === input.effort);
857
935
  if (observation.effortSupport !== 'explicit' || !effort || !(input.effort === 'none' ? guidance?.initialEffort === 'none' && guidance.effortLadder.length === 0 : guidance?.effortLadder.includes(input.effort)) || isWorkerEffortDenied(input.effort, input.policy) || (guards.denySelfDelegation !== false && effort.selfDelegates !== false))
858
- deny('OG_EFFORT_DENIED', '/worker/efforts', 'Unsupported, denied or self-delegating effort');
936
+ deny('OG_EFFORT_DENIED', '/worker/efforts', 'Unsupported, denied or self-delegating effort. Check catalog effort evidence and models.json defaults.workerSelection.roleGuidance, guards.deniedEfforts and guards.denySelfDelegation.');
859
937
  }
860
938
  const { data, unknownDataClass } = workerRouteDataPolicy(input.policy, input.dataClass);
861
939
  if (unknownDataClass)
862
940
  deny('OG_DATA_POLICY', '/defaults/workerSelection/dataClasses', 'Unknown data class');
863
- if (isWorkerDataUseDenied(observation.dataUse, data))
864
- deny('OG_DATA_POLICY', '/worker/dataUse', 'Execution path data facts do not satisfy policy');
941
+ if (isWorkerDataUseDenied(observation.dataUse, data)) {
942
+ const facts = observation.dataUse;
943
+ const keys = [
944
+ ...((facts.processing === 'unknown' || facts.productImprovement === null
945
+ || data.maxRetentionDays !== undefined && facts.retentionDays === null) && data.allowUnknown !== true ? ['allowUnknown'] : []),
946
+ ...(facts.processing !== 'unknown' && !data.allowedProcessing?.includes(facts.processing) ? ['allowedProcessing'] : []),
947
+ ...(facts.productImprovement === true && data.allowProductImprovement !== true ? ['allowProductImprovement'] : []),
948
+ ...(data.maxRetentionDays !== undefined && facts.retentionDays !== null && facts.retentionDays > data.maxRetentionDays ? ['maxRetentionDays'] : []),
949
+ ];
950
+ deny('OG_DATA_POLICY', '/worker/dataUse', `Execution path data facts do not satisfy policy. Review models.json ${keys.map(key => `defaults.workerSelection.dataPolicy.${key}`).join(', ')}${input.dataClass ? ` and defaults.workerSelection.dataClasses.${input.dataClass}` : ''}; unknown facts remain unknown.`);
951
+ }
865
952
  const now = (input.now ?? new Date()).getTime();
866
953
  if (observation.retiredAt != null && Date.parse(observation.retiredAt) <= now
867
954
  || observation.sunsetAt && Date.parse(observation.sunsetAt) <= now) {
@@ -873,7 +960,14 @@ export function validateWorkerPolicyRoute(input) {
873
960
  diagnostics.push({ code: 'OG_ADAPTER_UNQUALIFIED', pointer: '/worker/observedAt', blocking: false, message: 'Model observation exceeds the freshness warning threshold' });
874
961
  }
875
962
  if (input.context && !workerContextHeadroom(input.context, observation).fits) {
876
- deny('OG_MODEL_UNAPPROVED', '/worker/contextTokens', 'Bounded context and output do not fit qualified limits');
963
+ const missing = [observation.contextTokens === null ? 'contextTokens' : ''].filter(Boolean);
964
+ deny('OG_MODEL_UNAPPROVED', '/worker/contextTokens', missing.length
965
+ ? `Bounded context and output cannot be checked: catalog does not report a qualified ${missing.join(', ')} limit. Refresh discovery with reported limits; operator approval does not invent limits.`
966
+ : 'Bounded context and output do not fit qualified limits; reduce the request context or reserved output.');
967
+ }
968
+ if (input.context && observation.contextTokens !== null && observation.maxOutputTokens === null) {
969
+ diagnostics.push({ code: 'OG_MODEL_UNAPPROVED', pointer: '/worker/maxOutputTokens', blocking: false,
970
+ message: 'Output limit is unreported; only the context window was checked.' });
877
971
  }
878
972
  return diagnostics;
879
973
  }
@@ -883,8 +977,8 @@ export function workerContextHeadroom(estimate, observation) {
883
977
  const total = terms.reduce((sum, value) => sum + value, 0);
884
978
  const fits = Boolean(estimate.estimator.trim()) && terms.every(value => Number.isSafeInteger(value) && value >= 0)
885
979
  && (estimate.safetyMargin === undefined || Number.isSafeInteger(estimate.safetyMargin) && estimate.safetyMargin >= 0)
886
- && Number.isSafeInteger(total) && observation.contextTokens !== null && observation.maxOutputTokens !== null
887
- && total <= observation.contextTokens && estimate.reservedOutput <= observation.maxOutputTokens;
980
+ && Number.isSafeInteger(total) && observation.contextTokens !== null
981
+ && total <= observation.contextTokens && (observation.maxOutputTokens === null || estimate.reservedOutput <= observation.maxOutputTokens);
888
982
  return { ...estimate, safetyMargin, total, qualifiedContextTokens: observation.contextTokens,
889
983
  qualifiedMaxOutputTokens: observation.maxOutputTokens, fits };
890
984
  }
@@ -0,0 +1,2 @@
1
+ /** Canonical JSON binding with a sha256: prefix; shared by TS and runtime ESM. */
2
+ export function qualificationDigest(value: unknown): string;
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import { ConfigLoader } from './config-loader.js';
9
9
  import { WORKER_DISPATCH_ENABLE_COMMAND, WORKER_DISPATCH_READINESS_COMMAND } from './worker-dispatch-offer.js';
10
- import { resolveWorkerGroupInstallation, resolveParallelism, validateWorkerGroupReferences, validateWorkerBudgetEnforcement, workerActivationDigests, workerConfigDigest, } from '../config/aiwg-config.js';
11
- import { qualifyWorkerInvocationProfile, validateWorkerPolicyRoute, validateWorkerModelObservation, hasSelectionRelevantConflicts, workerContextHeadroom, workerModelKey, isCanonicalWorkerModelKey, WorkerSelectionPolicySchema, validateWorkerCatalogDiscovery, } from './provider-policy.js';
10
+ import { resolveWorkerGroupInstallation, resolveParallelism, validateWorkerGroupQualifications, validateWorkerGroupReferences, validateWorkerBudgetEnforcement, workerActivationDigests, workerConfigDigest, } from '../config/aiwg-config.js';
11
+ import { qualifyWorkerInvocationProfile, validateWorkerPolicyRoute, validateWorkerModelObservation, hasOperatorWorkerNativeRenderer, hasSelectionRelevantConflicts, workerContextHeadroom, workerModelKey, isCanonicalWorkerModelKey, WorkerSelectionPolicySchema, validateWorkerCatalogDiscovery, } from './provider-policy.js';
12
12
  import { MODEL_WRAPPERS } from './provider-models.js';
13
13
  import { routeModelTier } from './router.js';
14
14
  /**
@@ -344,8 +344,9 @@ export function resolveWorkerPlan(snapshot, request) {
344
344
  ? observed.qualification.roles.filter(role => policy.roleGuidance?.[role]?.groups.includes(groupId)) : [];
345
345
  }
346
346
  }
347
- for (const message of validateWorkerGroupReferences(parallelism, policy, qualifiedRoles))
348
- block('OG_ADAPTER_UNQUALIFIED', '/parallelism/groups', message);
347
+ plan.diagnostics.push(...validateWorkerGroupQualifications(parallelism, qualifiedRoles).map(diagnostic => ({
348
+ ...diagnostic, message: `${diagnostic.message}. Run aiwg doctor --probe-workers --qualify.`,
349
+ })));
349
350
  const taskRole = request.taskClass ? policy.taskClasses?.[request.taskClass] : undefined;
350
351
  const role = request.role ?? taskRole;
351
352
  plan.role = role ?? null;
@@ -397,7 +398,7 @@ export function resolveWorkerPlan(snapshot, request) {
397
398
  if (pinMismatch && !substitution)
398
399
  deny('OG_PIN_MISMATCH', pointer, 'Exact request or wrapper pin requires explicit substitution');
399
400
  if (!observation || !binding || !row?.llmProvider || !row.apiModelId)
400
- deny('OG_ADAPTER_UNQUALIFIED', pointer, 'Missing exact model, inference provider, observation or current installation evidence');
401
+ deny('OG_ADAPTER_UNQUALIFIED', pointer, 'Missing exact model, inference provider, observation or current installation evidence. Run aiwg doctor --probe-workers --qualify.');
401
402
  if (observation && binding && row?.llmProvider && row.apiModelId) {
402
403
  const shapeErrors = validateWorkerModelObservation(observation);
403
404
  if (shapeErrors.length) {
@@ -424,12 +425,13 @@ export function resolveWorkerPlan(snapshot, request) {
424
425
  const configuredServer = parallelism.clis?.[member.cli]?.mcpServerRef;
425
426
  if (transport === 'mcp' && configuredServer && configuredServer !== binding.mcp?.serverRef)
426
427
  throw new Error('Configured MCP server changed');
427
- if (adapter.capabilities.modelPin === 'unsupported' || !adapter.capabilities.nativeFallbackControllable
428
- || policy.guards?.denySelfDelegation !== false && !adapter.capabilities.nestedAgentsControllable)
429
- throw new Error('Exact model or child/fallback control is unqualified');
428
+ if (!hasOperatorWorkerNativeRenderer(adapter) && (adapter.capabilities.modelPin === 'unsupported' || !adapter.capabilities.nativeFallbackControllable))
429
+ throw new Error('Exact model or fallback control is unqualified; qualify an adapter with these controls');
430
+ if (policy.guards?.denySelfDelegation !== false && !adapter.capabilities.nestedAgentsControllable)
431
+ throw new Error('Child-agent control is unqualified. Review models.json defaults.workerSelection.guards.denySelfDelegation; false permits uncontrolled child agents without qualifying their behavior.');
430
432
  if (binding.identityEvidence === 'backend-attested' && adapter.capabilities.modelPin !== 'observed')
431
433
  throw new Error('Requested-only mapping cannot attest backend identity');
432
- if (group.execution?.sandbox && !adapter.capabilities.sandboxModes.includes(group.execution.sandbox))
434
+ if (!hasOperatorWorkerNativeRenderer(adapter) && group.execution?.sandbox && !adapter.capabilities.sandboxModes.includes(group.execution.sandbox))
433
435
  throw new Error('Requested sandbox mode is unqualified');
434
436
  qualifyWorkerInvocationProfile(adapter, { profile: binding.profile, transport, cliVersion: binding.cliVersion,
435
437
  policy, permissions, effort, boundary: binding.boundary,
@@ -460,7 +462,7 @@ export function resolveWorkerPlan(snapshot, request) {
460
462
  const authority = routeModelTier({ ...request.authority, deterministic: false, routine: false,
461
463
  defaultTier: guidance.tier === 'economy' ? 1 : 2, requestedPremium: premium || request.authority?.requestedPremium });
462
464
  if (authority.requiresConfirmation)
463
- deny('OG_MODEL_UNAPPROVED', '/request/authority', authority.rationale.join('; '));
465
+ deny('OG_MODEL_UNAPPROVED', '/request/authority', `${authority.rationale.join('; ')}; agents may pass --authorize-premium only after the human explicitly confirmed the premium escalation (escalation-discipline rule)`);
464
466
  const installation = parallelism.clis?.[member.cli];
465
467
  const caps = [parallelism.max_parallel_subagents, group.maxParallel, parallelism.admission?.groupCaps?.[groupId],
466
468
  parallelism.admission?.memberCaps?.[`${groupId}/${member.id}`], parallelism.admission?.cliCaps?.[member.cli],
@@ -47,8 +47,8 @@
47
47
  * is a binding the public config validator also accepts, and vice versa.
48
48
  *
49
49
  * The shipped direct profiles are declarations, not live qualification: no
50
- * finding ever calls a profile "qualified". Profiles stay unqualified until
51
- * an authenticated smoke run (tracker #3005), and an installed, in-range CLI
50
+ * finding ever calls a profile "qualified". Explicit operator approval uses `aiwg doctor --probe-workers --qualify`;
51
+ * a probe alone does not qualify a profile, and an installed, in-range CLI
52
52
  * can still be refused by the resolver — the dispatch finding says so
53
53
  * plainly with the resolver's own reason.
54
54
  *
@@ -61,7 +61,7 @@ import { isAbsolute, resolve as resolvePath } from 'node:path';
61
61
  import { boundedVersionProbe, normalizeWorkerVersionBanner, workerVersionProbeDefaults, workerVersionProbeEnvironment, workerVersionRangeMismatch, } from './worker-installation.mjs';
62
62
  import { resolveWorkerGroupInstallation } from '../config/aiwg-config.js';
63
63
  import { resolveWorkerPlan } from './resolver.js';
64
- const SMOKE_NOTE = 'Profiles are unqualified until an authenticated smoke run (see #3005).';
64
+ const SMOKE_NOTE = 'A version probe alone leaves profiles unqualified. To record direct CLI operator approval, run aiwg doctor --probe-workers --qualify; a version probe is not an authenticated smoke run (see #3005).';
65
65
  function truncateBanner(banner, limit = 200) {
66
66
  const singleLine = banner.replace(/[\r\n]+/g, '\\n');
67
67
  return singleLine.length > limit ? `${singleLine.slice(0, limit)}…` : singleLine;
@@ -166,13 +166,13 @@ function describeConfigContext(members) {
166
166
  * `parallelism.clis` entries plus any cli id referenced only implicitly by a
167
167
  * group member (canonical default binding, no explicit installation entry).
168
168
  */
169
- function allConfiguredCliIds(parallelism) {
169
+ function allConfiguredCliIds(parallelism, cli) {
170
170
  const ids = new Set(Object.keys(parallelism.clis ?? {}));
171
171
  for (const group of Object.values(parallelism.groups ?? {})) {
172
172
  for (const member of group?.members ?? [])
173
173
  ids.add(member.cli);
174
174
  }
175
- return [...ids].sort();
175
+ return [...ids].filter(id => cli === undefined || id === cli).sort();
176
176
  }
177
177
  /**
178
178
  * A cli is "required" only when an enabled group member referencing it also
@@ -202,12 +202,6 @@ function advisoryStatus(status, required) {
202
202
  return required || status !== 'error' ? status : 'warn';
203
203
  }
204
204
  const offlineDirectContext = () => ({ estimator: 'doctor-offline-direct-context-v1', briefTokens: 0, inputTokens: 0, toolOutputAllowance: 0, reservedOutput: 0 });
205
- function formatDiagnostics(diagnostics) {
206
- const seen = new Map();
207
- for (const diagnostic of diagnostics)
208
- seen.set(`${diagnostic.code}:${diagnostic.pointer}:${diagnostic.message}`, diagnostic);
209
- return [...seen.values()].map(diagnostic => `${diagnostic.code} ${diagnostic.pointer}: ${diagnostic.message}`).join('; ');
210
- }
211
205
  /**
212
206
  * Dispatch eligibility from the actual offline dispatch resolver
213
207
  * (`resolveWorkerPlan`): called once per role for every group member
@@ -222,7 +216,8 @@ function resolveEligibilityFromDispatchResolver(resolverSnapshot, registry, cli,
222
216
  config: resolverSnapshot.config, models: resolverSnapshot.models, modelDigests: resolverSnapshot.modelDigests,
223
217
  catalog: resolverSnapshot.catalog, registry, installations: {},
224
218
  };
225
- const refusals = [];
219
+ const diagnostics = new Map();
220
+ const uncovered = [];
226
221
  for (const ref of refs) {
227
222
  if (!ref.enabled)
228
223
  continue;
@@ -239,23 +234,48 @@ function resolveEligibilityFromDispatchResolver(resolverSnapshot, registry, cli,
239
234
  break;
240
235
  }
241
236
  const exclusion = plan.exclusions.find(matches);
242
- seen.push(...(exclusion?.diagnostics ?? plan.diagnostics));
237
+ for (const diagnostic of exclusion?.diagnostics ?? plan.diagnostics) {
238
+ // Only an exact member pointer is generalized; other config pointers
239
+ // remain distinct even when their diagnostic text happens to match.
240
+ const memberPointer = `/parallelism/groups/${ref.groupId}/members/${ref.memberId}`;
241
+ const pointer = diagnostic.pointer === memberPointer || diagnostic.pointer.startsWith(`${memberPointer}/`)
242
+ ? `/parallelism/groups/${ref.groupId}/members${diagnostic.pointer.slice(memberPointer.length)}`
243
+ : diagnostic.pointer;
244
+ seen.push({ ...diagnostic, pointer, members: [`${ref.groupId}/${ref.memberId}`],
245
+ roles: diagnostic.roles ?? [role] });
246
+ }
243
247
  }
244
248
  if (eligibleHere) {
245
249
  return { eligibility: 'eligible',
246
250
  detail: `Dispatch resolver accepts cli '${cli}' for group '${ref.groupId}' member '${ref.memberId}'. `
247
251
  + `Dispatch eligibility: eligible. ${SMOKE_NOTE}` };
248
252
  }
249
- const reasons = formatDiagnostics(seen);
250
- refusals.push(`group '${ref.groupId}' member '${ref.memberId}'${reasons ? `: ${reasons}` : ' (no qualified route)'}`);
253
+ if (!seen.length)
254
+ uncovered.push(`${ref.groupId}/${ref.memberId}`);
255
+ for (const diagnostic of seen) {
256
+ const key = JSON.stringify([diagnostic.code, diagnostic.pointer, diagnostic.message,
257
+ diagnostic.blocking, diagnostic.origins]);
258
+ const shared = diagnostics.get(key);
259
+ if (shared) {
260
+ shared.members = [...new Set([...shared.members, ...diagnostic.members])];
261
+ shared.roles = [...new Set([...shared.roles, ...diagnostic.roles])];
262
+ }
263
+ else
264
+ diagnostics.set(key, diagnostic);
265
+ }
251
266
  }
252
- if (!refusals.length) {
267
+ if (!diagnostics.size && !uncovered.length) {
253
268
  return { eligibility: 'blocked',
254
269
  detail: `Dispatch resolver has no enabled group member to evaluate for cli '${cli}'. `
255
270
  + `Dispatch eligibility: blocked. ${SMOKE_NOTE}` };
256
271
  }
257
- return { eligibility: 'blocked',
258
- detail: `Dispatch resolver refuses cli '${cli}': ${refusals.join('; ')}. `
272
+ const distinct = [...diagnostics.values()];
273
+ const reasons = distinct.map(diagnostic => `${diagnostic.code} ${diagnostic.pointer}: ${diagnostic.message}`
274
+ + ` [roles: ${diagnostic.roles.join(', ')}; members: ${diagnostic.members.join(', ')}]`);
275
+ if (uncovered.length)
276
+ reasons.push(`No qualified route [members: ${uncovered.join(', ')}]`);
277
+ return { eligibility: 'blocked', diagnostics: distinct,
278
+ detail: `Dispatch resolver refuses cli '${cli}': ${reasons.join('; ')}. `
259
279
  + `Dispatch eligibility: blocked. ${SMOKE_NOTE}` };
260
280
  }
261
281
  /**
@@ -277,7 +297,7 @@ function resolveEligibilityFromBindingOnly(parallelism, registry, cli, installRe
277
297
  const reason = error instanceof Error ? error.message : String(error);
278
298
  const readyNote = installReady
279
299
  ? ' The CLI is installed with an in-range version, but dispatch stays refused for the reason above; '
280
- + 'an authenticated smoke run (see #3005) alone does not satisfy the resolver gates.'
300
+ + `an authenticated smoke run (see #3005) alone does not satisfy the resolver gates. ${SMOKE_NOTE}`
281
301
  : ` ${SMOKE_NOTE}`;
282
302
  return { eligibility: 'blocked',
283
303
  detail: `Resolver refuses cli '${cli}': ${reason}. Dispatch eligibility: blocked.${readyNote}` };
@@ -300,7 +320,7 @@ export async function diagnoseDirectWorkerReadiness(input) {
300
320
  const { parallelism, registry, probe, announce, timeoutMs = workerVersionProbeDefaults.timeoutMs, probeCwd = tmpdir(), configFile = '.aiwg/aiwg.config', projectRoot = process.cwd(), resolverSnapshot } = input;
301
321
  const report = {
302
322
  enabled: parallelism.worker_dispatch === true,
303
- workersConfigured: allConfiguredCliIds(parallelism).length > 0,
323
+ workersConfigured: allConfiguredCliIds(parallelism, input.cli).length > 0,
304
324
  ready: true,
305
325
  findings: [],
306
326
  };
@@ -315,7 +335,7 @@ export async function diagnoseDirectWorkerReadiness(input) {
315
335
  members: [] });
316
336
  return report;
317
337
  }
318
- const cliIds = allConfiguredCliIds(parallelism);
338
+ const cliIds = allConfiguredCliIds(parallelism, input.cli);
319
339
  if (!cliIds.length) {
320
340
  // No workers configured: no findings at all so doctor stays silent.
321
341
  return report;
@@ -325,11 +345,11 @@ export async function diagnoseDirectWorkerReadiness(input) {
325
345
  if (finding.status === 'error' || finding.status === 'warn')
326
346
  report.ready = false;
327
347
  };
328
- const pushDispatch = (cli, profile, status, code, eligibility, detail, configKey, members, nextAction) => {
348
+ const pushDispatch = (cli, profile, status, code, eligibility, detail, configKey, members, nextAction, diagnostics) => {
329
349
  report.findings.push({ cli, kind: 'dispatch', profile, executable: null, installedVersion: null,
330
350
  status, code, message: `${detail} (config ${configFile} key ${configKey}; ${describeConfigContext(members)})`,
331
351
  nextAction, configFile, configKey,
332
- readiness: null, eligibility, eligibilityDetail: detail, members });
352
+ readiness: null, eligibility, eligibilityDetail: detail, members, ...(diagnostics ? { diagnostics } : {}) });
333
353
  };
334
354
  await Promise.all(cliIds.map(async (cli) => {
335
355
  const configured = parallelism.clis?.[cli];
@@ -344,8 +364,8 @@ export async function diagnoseDirectWorkerReadiness(input) {
344
364
  message: `Dispatch is disabled for cli '${cli}' (config ${configFile} key ${configKey}/enabled); the version probe was skipped. Installation readiness: skipped. (${describeConfigContext(members)})`,
345
365
  nextAction: 'enable-cli-to-onboard',
346
366
  configFile, configKey, readiness: 'skipped', members });
347
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
348
- pushDispatch(cli, configured.invocationProfile ?? null, 'info', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'enable-cli-to-onboard');
367
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
368
+ pushDispatch(cli, configured.invocationProfile ?? null, 'info', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'enable-cli-to-onboard', diagnostics);
349
369
  return;
350
370
  }
351
371
  const resolved = resolveDirectBinding(registry, configured?.adapter, cli);
@@ -355,8 +375,8 @@ export async function diagnoseDirectWorkerReadiness(input) {
355
375
  message: `${resolved.error} (config ${configFile} key ${configKey}/adapter); the profile cannot be onboarded. Installation readiness: not-ready. (${describeConfigContext(members)})`,
356
376
  nextAction: 'review-worker-configuration',
357
377
  configFile, configKey: `${configKey}/adapter`, readiness: 'not-ready', members });
358
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
359
- pushDispatch(cli, configured?.invocationProfile ?? null, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'review-worker-configuration');
378
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
379
+ pushDispatch(cli, configured?.invocationProfile ?? null, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'review-worker-configuration', diagnostics);
360
380
  return;
361
381
  }
362
382
  const { adapter, adapterRef } = resolved;
@@ -366,8 +386,8 @@ export async function diagnoseDirectWorkerReadiness(input) {
366
386
  message: `Cli '${cli}' binds '${adapterRef}' (config ${configFile} key ${configKey}/adapter), which uses an HTTP adapter; there is no CLI version probe — check endpoint health instead. Installation readiness: skipped. (${describeConfigContext(members)})`,
367
387
  nextAction: 'check-endpoint-health',
368
388
  configFile, configKey, readiness: 'skipped', members });
369
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
370
- pushDispatch(cli, null, eligibility === 'eligible' ? 'ok' : 'warn', eligibility === 'eligible' ? 'WORKER_DIRECT_DISPATCH_ELIGIBLE' : 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, eligibility === 'eligible' ? 'check-endpoint-health' : 'review-worker-configuration');
389
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
390
+ pushDispatch(cli, null, eligibility === 'eligible' ? 'ok' : 'warn', eligibility === 'eligible' ? 'WORKER_DIRECT_DISPATCH_ELIGIBLE' : 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, eligibility === 'eligible' ? 'check-endpoint-health' : 'review-worker-configuration', diagnostics);
371
391
  return;
372
392
  }
373
393
  const profileIds = Object.keys(adapter.invocationProfiles ?? {});
@@ -382,8 +402,8 @@ export async function diagnoseDirectWorkerReadiness(input) {
382
402
  : `Cli '${cli}' binding '${adapterRef}' has no single direct profile to check (config ${configFile} key ${configKey}); configure invocationProfile explicitly. Installation readiness: not-ready. (${describeConfigContext(members)})`,
383
403
  nextAction: 'review-worker-configuration',
384
404
  configFile, configKey, readiness: 'not-ready', members });
385
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
386
- pushDispatch(cli, configured?.invocationProfile ?? null, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'review-worker-configuration');
405
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
406
+ pushDispatch(cli, configured?.invocationProfile ?? null, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'review-worker-configuration', diagnostics);
387
407
  return;
388
408
  }
389
409
  const profile = adapter.invocationProfiles[profileRef];
@@ -394,8 +414,8 @@ export async function diagnoseDirectWorkerReadiness(input) {
394
414
  message: `Cli '${cli}' binds '${adapterRef}' profile '${profileRef}' (config ${configFile} key ${configKey}); version probe not run — no subprocess was started. Installation readiness: unprobed. Rerun with probes to check install/version. (${describeConfigContext(members)})`,
395
415
  nextAction: 'rerun-with-probe-workers',
396
416
  configFile, configKey, readiness: 'unprobed', members });
397
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
398
- pushDispatch(cli, profileRef, eligibility === 'eligible' ? 'ok' : 'warn', eligibility === 'eligible' ? 'WORKER_DIRECT_DISPATCH_ELIGIBLE' : 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, eligibility === 'eligible' ? 'rerun-with-probe-workers' : 'review-worker-configuration');
417
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
418
+ pushDispatch(cli, profileRef, eligibility === 'eligible' ? 'ok' : 'warn', eligibility === 'eligible' ? 'WORKER_DIRECT_DISPATCH_ELIGIBLE' : 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, eligibility === 'eligible' ? 'rerun-with-probe-workers' : 'review-worker-configuration', diagnostics);
399
419
  return;
400
420
  }
401
421
  announce?.(cli, executable, adapter.invocation.versionArgv);
@@ -416,8 +436,8 @@ export async function diagnoseDirectWorkerReadiness(input) {
416
436
  : `Version probe for cli '${cli}' ('${executable}', config ${configFile} key ${configKey}) failed; retry once, then check PATH and execute permission. Installation readiness: not-ready. (${describeConfigContext(members)})`,
417
437
  nextAction: notInstalled ? 'install-cli' : 'retry-version-probe',
418
438
  configFile, configKey, readiness: 'not-ready', members });
419
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
420
- pushDispatch(cli, profileRef, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, notInstalled ? 'install-cli' : 'retry-version-probe');
439
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
440
+ pushDispatch(cli, profileRef, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, notInstalled ? 'install-cli' : 'retry-version-probe', diagnostics);
421
441
  return;
422
442
  }
423
443
  const rawBanner = outcome.rawBanner.trim();
@@ -431,8 +451,8 @@ export async function diagnoseDirectWorkerReadiness(input) {
431
451
  message: `CLI '${executable}' banner ${JSON.stringify(truncateBanner(rawBanner))} does not match the registered version pattern (config ${configFile} key ${configKey}); the install is unexpected — reinstall a supported release. Installation readiness: not-ready. (${describeConfigContext(members)})`,
432
452
  nextAction: 'reinstall-supported-cli',
433
453
  configFile, configKey, readiness: 'not-ready', members });
434
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
435
- pushDispatch(cli, profileRef, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'reinstall-supported-cli');
454
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
455
+ pushDispatch(cli, profileRef, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'reinstall-supported-cli', diagnostics);
436
456
  return;
437
457
  }
438
458
  const mismatch = workerVersionRangeMismatch(installedVersion, [profile.supportedVersions, adapter.supportedVersions]);
@@ -442,17 +462,17 @@ export async function diagnoseDirectWorkerReadiness(input) {
442
462
  message: `CLI '${executable}' version ${installedVersion} is outside supported range '${mismatch}' for cli '${cli}' (profile '${profileRef}', config ${configFile} key ${configKey}); upgrade or pin a supported release. Installation readiness: not-ready. (${describeConfigContext(members)})`,
443
463
  nextAction: 'upgrade-or-pin-cli',
444
464
  configFile, configKey, readiness: 'not-ready', members });
445
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
446
- pushDispatch(cli, profileRef, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'upgrade-or-pin-cli');
465
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, false, resolverSnapshot, refs);
466
+ pushDispatch(cli, profileRef, 'warn', 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'upgrade-or-pin-cli', diagnostics);
447
467
  return;
448
468
  }
449
469
  pushInstall({ cli, profile: profileRef, executable, installedVersion,
450
470
  status: 'ok', code: 'WORKER_DIRECT_READY',
451
471
  message: `CLI '${executable}' version ${installedVersion} matches profile '${profileRef}' for cli '${cli}' (config ${configFile} key ${configKey}). Installation readiness: ready. ${SMOKE_NOTE} See the dispatch finding for whether the resolver would dispatch. (${describeConfigContext(members)})`,
452
- nextAction: 'run-authenticated-smoke-run',
472
+ nextAction: 'qualify-installed-cli',
453
473
  configFile, configKey, readiness: 'ready', members });
454
- const { eligibility, detail } = checkDispatchEligibility(parallelism, registry, cli, true, resolverSnapshot, refs);
455
- pushDispatch(cli, profileRef, eligibility === 'eligible' ? 'ok' : 'warn', eligibility === 'eligible' ? 'WORKER_DIRECT_DISPATCH_ELIGIBLE' : 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'run-authenticated-smoke-run');
474
+ const { eligibility, detail, diagnostics } = checkDispatchEligibility(parallelism, registry, cli, true, resolverSnapshot, refs);
475
+ pushDispatch(cli, profileRef, eligibility === 'eligible' ? 'ok' : 'warn', eligibility === 'eligible' ? 'WORKER_DIRECT_DISPATCH_ELIGIBLE' : 'WORKER_DIRECT_DISPATCH_BLOCKED', eligibility, detail, configKey, members, 'qualify-installed-cli', diagnostics);
456
476
  }));
457
477
  report.findings.sort((a, b) => a.cli.localeCompare(b.cli) || a.kind.localeCompare(b.kind));
458
478
  return report;
@@ -478,7 +498,7 @@ export async function diagnoseDirectWorkerReadiness(input) {
478
498
  */
479
499
  export async function buildDirectWorkerDoctorSection(input) {
480
500
  const { parallelism, registry, probeWorkers } = input;
481
- const cliIds = allConfiguredCliIds(parallelism ?? {});
501
+ const cliIds = allConfiguredCliIds(parallelism ?? {}, input.cli);
482
502
  if (!cliIds.length)
483
503
  return { lines: [], probed: false, workersConfigured: false, installFindings: [] };
484
504
  const lines = [];
@@ -493,6 +513,7 @@ export async function buildDirectWorkerDoctorSection(input) {
493
513
  const report = await diagnoseDirectWorkerReadiness({
494
514
  parallelism: parallelism,
495
515
  registry,
516
+ cli: input.cli,
496
517
  ...(probeWorkers ? { probe: input.probe ?? probeDirectWorkerVersion, ...(input.announce ? { announce: input.announce } : {}) } : {}),
497
518
  ...(input.configFile ? { configFile: input.configFile } : {}),
498
519
  ...(input.projectRoot ? { projectRoot: input.projectRoot } : {}),
@@ -509,7 +530,7 @@ export async function buildDirectWorkerDoctorSection(input) {
509
530
  }
510
531
  if (probeWorkers && !report.findings.some(finding => finding.code === 'WORKER_DIRECT_READY')) {
511
532
  lines.push({ name: 'Direct Worker', status: 'info',
512
- message: 'no profile is onboarded; profiles are unqualified until an authenticated smoke run (see #3005)' });
533
+ message: `no profile is onboarded; ${SMOKE_NOTE}` });
513
534
  }
514
535
  return {
515
536
  lines,
@@ -32,7 +32,7 @@ import { loadProviderModelCapabilities } from './provider-policy.js';
32
32
  */
33
33
  export const WORKER_DISPATCH_ENABLE_COMMAND = 'aiwg config set --project parallelism.worker_dispatch true';
34
34
  /** Readiness check shared by setup hints and dispatch refusals. */
35
- export const WORKER_DISPATCH_READINESS_COMMAND = 'aiwg doctor --probe-workers';
35
+ export const WORKER_DISPATCH_READINESS_COMMAND = 'aiwg doctor --probe-workers --qualify';
36
36
  /**
37
37
  * Dotted config path a decline is recorded at. A setup/onboarding
38
38
  * preference, not worker policy — kept out of `parallelism`, which is a
@@ -117,7 +117,7 @@ export function evaluateWorkerDispatchOffer(parallelism, setup, detected) {
117
117
  */
118
118
  export function workerDispatchOfferQuestion(detected) {
119
119
  return ` Detected worker CLIs (${detected.join(', ')}). Enable direct worker dispatch so AIWG `
120
- + `can dispatch tasks to them directly, using their own accounts' quota? [y/N]: `;
120
+ + `can dispatch tasks to them directly, using their own accounts' quota? After enabling, run aiwg doctor --probe-workers --qualify. [y/N]: `;
121
121
  }
122
122
  /**
123
123
  * One-line non-interactive / regenerate / refresh / doctor hint. Never
@@ -127,7 +127,7 @@ export function workerDispatchOfferQuestion(detected) {
127
127
  */
128
128
  export function workerDispatchOfferHint(detected) {
129
129
  return `Direct worker dispatch is off; detected worker CLIs: ${detected.join(', ')}. `
130
- + `Enable with \`${WORKER_DISPATCH_ENABLE_COMMAND}\` (then check readiness with \`${WORKER_DISPATCH_READINESS_COMMAND}\`).`;
130
+ + `Enable with \`${WORKER_DISPATCH_ENABLE_COMMAND}\` (then qualify installed CLIs with \`${WORKER_DISPATCH_READINESS_COMMAND}\`).`;
131
131
  }
132
132
  /**
133
133
  * Accepted values for the `--worker-dispatch` argument (#3062). Only
@@ -61,18 +61,31 @@ export function diagnoseWorkerHealthReadiness(snapshot, request) {
61
61
  message: 'No explicit quota/account correlation scope; shared aliases cannot establish independent upstream capacity.',
62
62
  nextAction: 'configure-qualified-quota-scope' });
63
63
  }
64
+ const sharedDiagnostics = new Map();
64
65
  for (const role of roles) {
65
66
  const plan = resolveWorkerPlan(snapshot, { ...request, role: role });
66
67
  const isRequired = required.has(role);
67
68
  const diagnostics = [...plan.diagnostics, ...plan.exclusions.flatMap(item => item.diagnostics)];
68
69
  const seen = new Set();
69
70
  for (const diagnostic of diagnostics) {
70
- const key = `${diagnostic.code}:${diagnostic.pointer}:${diagnostic.message}`;
71
+ if (diagnostic.roles && !diagnostic.roles.includes(role))
72
+ continue;
73
+ const key = JSON.stringify([diagnostic.code, diagnostic.pointer, diagnostic.message, diagnostic.blocking, diagnostic.origins]);
71
74
  if (seen.has(key))
72
75
  continue;
73
76
  seen.add(key);
74
- add({ ...diagnostic, status: diagnostic.blocking === false ? 'info' : 'warn', role,
75
- nextAction: repairAction(diagnostic.code, diagnostic.pointer) });
77
+ const finding = { ...diagnostic,
78
+ status: diagnostic.blocking === false ? 'info' : 'warn', role,
79
+ nextAction: repairAction(diagnostic.code, diagnostic.pointer) };
80
+ const shared = sharedDiagnostics.get(key);
81
+ if (shared) {
82
+ shared.roles = [...new Set([...(shared.roles ?? [shared.role]), role])];
83
+ shared.role = null;
84
+ }
85
+ else {
86
+ sharedDiagnostics.set(key, finding);
87
+ add(finding);
88
+ }
76
89
  }
77
90
  if (!plan.selected) {
78
91
  if (isRequired)
@@ -0,0 +1,39 @@
1
+ import type { ProviderModelCapabilityRegistry } from './provider-policy.js';
2
+ import type { WorkerSelectionPolicy } from './types.js';
3
+ import type { WorkerResolutionBinding } from './resolver.js';
4
+ import type { WorkerInvocationDriver } from '../serve/worker-invocation.js';
5
+ export function loadRecordedWorkerInstallations(input: {
6
+ registry: ProviderModelCapabilityRegistry;
7
+ cwd: string;
8
+ policy?: WorkerSelectionPolicy;
9
+ parallelism?: import('../config/aiwg-config.js').ParallelismConfig;
10
+ storePath?: string;
11
+ storeDigest?: string;
12
+ }): Promise<{ registry: ProviderModelCapabilityRegistry; installations: Record<string, WorkerResolutionBinding>; drivers: Record<string, WorkerInvocationDriver>; observations: Record<string, import('./model-discovery.js').ProviderModelDiscovery> }>;
13
+
14
+ export function hasProvider(name: string): boolean;
15
+
16
+ export function compileWorkerVersionPattern(pattern: string): RegExp;
17
+ export function normalizeWorkerVersionBanner(banner: string, pattern?: string): string;
18
+ export const workerVersionProbeEnvAllowlist: readonly string[];
19
+ export function workerVersionProbeEnvironment(source?: Record<string, string | undefined>): Record<string, string>;
20
+ export const workerVersionProbeDefaults: { readonly timeoutMs: number; readonly maxBytes: number };
21
+ export function workerVersionRangeMismatch(version: string, ranges: string[]): string | null;
22
+ export function boundedVersionProbe(
23
+ executable: string,
24
+ argv: string[],
25
+ environment: Record<string, string>,
26
+ cwd: string,
27
+ timeoutMs: number,
28
+ maxBytes: number,
29
+ versionPattern: string | undefined,
30
+ ): Promise<{ rawBanner: string; version: string }>;
31
+
32
+ export interface WorkerRecoveryCapabilities {
33
+ canVerifyAttempt?: boolean;
34
+ parseWorkerAttemptFinal?(output: string | Uint8Array, options: Record<string, unknown>): Promise<unknown>;
35
+ }
36
+ export function registerWorkerRecoveryCapabilities<T extends object>(driver: T, capabilities: WorkerRecoveryCapabilities): T;
37
+ export function getWorkerRecoveryCapabilities(driver: object | undefined | null): Readonly<WorkerRecoveryCapabilities> | null;
38
+
39
+ export function prepareWorkerInstallationStore(input: { workerWritableRoots: string[]; apply?: boolean }): Promise<{ path: string; action: string }[]>;