@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
@@ -507,9 +507,14 @@ Options:
507
507
  --bundle <name> Limit deployment verification to one bundle
508
508
  --scope <scope> Verification scope: project or user
509
509
  --user Alias for --scope user
510
+ --qualify With --probe-workers, record installation and configured model/role approval
511
+ --yes Confirm worker qualification without a terminal
512
+ --dry-run With --probe-workers, preview qualification without writes
513
+ --cli NAME Limit worker probes/qualification to one configured CLI
514
+ --decision-offload Show offline advisory decision readiness and adoption report
510
515
  --probe-workers Run bounded worker CLI version probes (default: config-only, no subprocess)
511
516
  --project-local Include project-local artifact diagnostics
512
- --json Emit machine-readable output (with --probe-workers: one worker-probe record per CLI; with --deployment: deployment verification)
517
+ --json Emit machine-readable output (with --probe-workers: one worker-probe record per CLI; with --deployment: deployment verification; with --decision-offload: offline snapshot)
513
518
  --verbose, -v Include verbose diagnostics
514
519
  --help, -h Show this help
515
520
  `;
@@ -537,10 +542,10 @@ export const doctorHandler = {
537
542
  async execute(ctx) {
538
543
  // Validate before any early-return mode: deployment and user-scope doctor
539
544
  // never invoke doctor.mjs, so they must enforce the same flag contract.
540
- const valueFlags = ['--provider', '--bundle', '--scope', '--context-baseline', '--context-budget-tokens'];
545
+ const valueFlags = ['--provider', '--bundle', '--scope', '--context-baseline', '--context-budget-tokens', '--cli'];
541
546
  const knownFlags = new Set([
542
- ...valueFlags, '--all-providers', '--probe-workers', '--no-budget-check', '--strict-context', '--json',
543
- '--deployment', '--user', '--verbose', '-v', '--project-local', '--quiet', '--help', '-h',
547
+ ...valueFlags, '--all-providers', '--probe-workers', '--qualify', '--yes', '--dry-run', '--no-budget-check', '--strict-context', '--json',
548
+ '--decision-offload', '--deployment', '--user', '--verbose', '-v', '--project-local', '--quiet', '--help', '-h',
544
549
  ]);
545
550
  const hasSwitch = (name) => hasPositionalOrSwitch(ctx.args, name, valueFlags);
546
551
  const values = new Map();
@@ -558,8 +563,14 @@ export const doctorHandler = {
558
563
  throw new Error(`Unknown flag: ${name}`);
559
564
  }
560
565
  }
561
- if (hasSwitch('--json') && !hasSwitch('--probe-workers') && !hasSwitch('--deployment')) {
562
- throw new Error('--json requires --probe-workers or --deployment');
566
+ if (hasSwitch('--decision-offload') && (hasSwitch('--probe-workers') || hasSwitch('--deployment') || hasSwitch('--user') || values.get('--scope') !== undefined)) {
567
+ throw new Error('--decision-offload is a separate offline mode; do not combine it with worker, deployment or user-scope checks');
568
+ }
569
+ if (['--qualify', '--cli', '--yes', '--dry-run'].some(flag => hasSwitch(flag) || values.get(flag)) && !hasSwitch('--probe-workers')) {
570
+ throw new Error('--qualify, --cli, --yes and --dry-run require --probe-workers');
571
+ }
572
+ if (hasSwitch('--json') && !hasSwitch('--probe-workers') && !hasSwitch('--deployment') && !hasSwitch('--decision-offload')) {
573
+ throw new Error('--json requires --probe-workers or --deployment or --decision-offload');
563
574
  }
564
575
  }
565
576
  catch (error) {
@@ -578,11 +589,13 @@ export const doctorHandler = {
578
589
  bundle: values.get('--bundle'),
579
590
  scope: scopeValue,
580
591
  }, ctx.frameworkRoot);
592
+ const { rulePinState } = await import('../../rules/pin-state.js');
593
+ const pins = rulePinState(ctx.cwd, ctx.frameworkRoot);
581
594
  return {
582
595
  exitCode: result.exitCode,
583
596
  message: hasSwitch('--json')
584
- ? JSON.stringify(result, null, 2)
585
- : renderUseDeploymentResult(result),
597
+ ? JSON.stringify({ ...result, rule_pins: { rules: pins.rules, warnings: pins.warnings } }, null, 2)
598
+ : renderUseDeploymentResult(result) + '\n' + pins.rules.map(rule => `${rule.name}: pinned kernel (canonical ${rule.canonical_activation})`).concat(pins.warnings).join('\n'),
586
599
  rawOutput: hasSwitch('--json'),
587
600
  };
588
601
  }
@@ -595,7 +608,7 @@ export const doctorHandler = {
595
608
  if (userScopeRequested && !hasSwitch('--probe-workers')) {
596
609
  return await runUserScopeDoctor(hasSwitch('--verbose') || hasSwitch('-v'));
597
610
  }
598
- const frameworkRoot = await getFrameworkRoot();
611
+ const frameworkRoot = hasSwitch('--decision-offload') ? ctx.frameworkRoot : await getFrameworkRoot();
599
612
  const runner = createScriptRunner(frameworkRoot);
600
613
  // Run core doctor diagnostics
601
614
  const result = await runner.run('tools/cli/doctor.mjs', ctx.args, { cwd: ctx.cwd });
@@ -603,7 +616,7 @@ export const doctorHandler = {
603
616
  // printed exactly one JSON document (or a usage error) on its own
604
617
  // stdio — every section below writes human-readable text, so returning
605
618
  // here keeps stdout to the JSON document alone.
606
- if (hasSwitch('--json') || result.exitCode === 2)
619
+ if (hasSwitch('--decision-offload') || hasSwitch('--json') || hasSwitch('--qualify') || hasSwitch('--cli') || hasSwitch('--dry-run') || result.exitCode === 2)
607
620
  return result;
608
621
  let agentSkillsFailure = false;
609
622
  try {
@@ -801,6 +814,9 @@ export const contextFirewallHandler = {
801
814
  description: 'Audit provider context and manage its reviewed baseline',
802
815
  category: 'maintenance',
803
816
  aliases: ['-context-firewall', '--context-firewall'],
817
+ async help() {
818
+ return { exitCode: 0, message: CONTEXT_FIREWALL_HELP, rawOutput: true };
819
+ },
804
820
  async execute(ctx) {
805
821
  if (ctx.args.includes('--help') || ctx.args.includes('-h')) {
806
822
  return { exitCode: 0, message: CONTEXT_FIREWALL_HELP };
@@ -55,6 +55,9 @@ function collectFingerprint(versionInfo) {
55
55
  const loggerInfo = getLoggerInfo();
56
56
  const fp = {
57
57
  version: versionInfo.version,
58
+ launcherVersion: versionInfo.launcherVersion ?? versionInfo.version,
59
+ runtimeVersion: versionInfo.runtimeVersion ?? versionInfo.version,
60
+ runtimePath: versionInfo.runtimePath ?? versionInfo.packageRoot,
58
61
  channel: versionInfo.devMode ? 'dev' : versionInfo.channel,
59
62
  packageRoot: versionInfo.packageRoot,
60
63
  node: process.version,
@@ -132,6 +135,11 @@ async function displayVersion(opts) {
132
135
  }
133
136
  ui.blank();
134
137
  console.log(` ${ui.brandMark()} ${ui.bold('aiwg')} ${ui.bold(fp.version)} ${ui.channelLabel(fp.channel)}`);
138
+ if (info.devMode || fp.launcherVersion !== fp.runtimeVersion) {
139
+ ui.dim(` launcher: ${fp.launcherVersion}`);
140
+ ui.dim(` runtime: ${fp.runtimeVersion}`);
141
+ ui.dim(` runtime path: ${fp.runtimePath}`);
142
+ }
135
143
  if (!opts.verbose) {
136
144
  if (fp.git)
137
145
  ui.dim(` git: ${fp.git.sha} (${fp.git.branch})`);
@@ -75,6 +75,15 @@ export function getHookRegistry() {
75
75
  * ```
76
76
  */
77
77
  export async function run(args, options = {}) {
78
+ // Scheduled index maintenance has no hooks, telemetry, or deployment side effects.
79
+ if (args[0] === 'index' && args[1] === 'refresh'
80
+ && !args.includes('--help') && !args.includes('-h')) {
81
+ const { runIndexRefresh } = await import('../artifacts/index-refresh.js');
82
+ const report = await runIndexRefresh(args.slice(2), options.cwd ?? process.cwd(), options.signal);
83
+ if (report.totals.errors)
84
+ process.exit(1);
85
+ return;
86
+ }
78
87
  const started = process.hrtime.bigint();
79
88
  const registry = await initRouter();
80
89
  // Normalize `help <command>` into the same non-executing help path.
@@ -320,7 +329,12 @@ async function logCommandInvocation(input) {
320
329
  * @returns Handler context
321
330
  */
322
331
  async function buildContext(args, rawArgs, options) {
323
- const frameworkRoot = await getFrameworkRoot({ createIfMissing: !args.includes('--dry-run') });
332
+ const readOnlyRuleInventory = rawArgs[0] === 'rules' && ['inventory', 'list'].includes(args[0]);
333
+ const readOnlyOffloadReport = (['doctor', '-doctor', '--doctor'].includes(rawArgs[0] ?? '') && args.includes('--decision-offload'))
334
+ || (rawArgs[0] === 'steward' && args[0] === 'decision-offload');
335
+ const frameworkRoot = await getFrameworkRoot({
336
+ createIfMissing: !args.includes('--dry-run') && !readOnlyRuleInventory && !readOnlyOffloadReport,
337
+ });
324
338
  const ctx = {
325
339
  args,
326
340
  rawArgs,
@@ -1,5 +1,6 @@
1
1
  import { access, readFile, readdir, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { loadPrebuiltDeploymentIndex } from '../../artifacts/prebuilt-deployment.js';
3
4
  import { loadGraphIndexFile } from '../../artifacts/index-reader.js';
4
5
  import { readAiwgConfig } from '../../config/aiwg-config.js';
5
6
  import { readUserRegistry } from '../../config/user-registry.js';
@@ -102,7 +103,7 @@ export function normalizeFrameworkDiscoveryInventory(stats) {
102
103
  };
103
104
  }
104
105
  function frameworkDiscoveryInventory(frameworkRoot) {
105
- const stats = loadGraphIndexFile(frameworkRoot, 'stats.json', 'framework');
106
+ const stats = loadPrebuiltDeploymentIndex(frameworkRoot, { strictRoot: true })?.stats ?? loadGraphIndexFile(frameworkRoot, 'stats.json', 'framework');
106
107
  return stats ? normalizeFrameworkDiscoveryInventory(stats) : null;
107
108
  }
108
109
  const BUNDLE_INDEX_TOKENS = {
@@ -510,9 +511,11 @@ export async function verifyProviderDeployment(options) {
510
511
  const frameworkBundles = options.requestedBundles.filter((bundle) => !projectLocalBundles.includes(bundle));
511
512
  const indexesToVerify = [];
512
513
  if (frameworkBundles.length > 0) {
514
+ const prebuilt = loadPrebuiltDeploymentIndex(options.frameworkRoot, { strictRoot: true });
513
515
  indexesToVerify.push({
514
- index: loadGraphIndexFile(options.frameworkRoot, 'metadata.json', 'framework'),
515
- stats: loadGraphIndexFile(options.frameworkRoot, 'stats.json', 'framework'),
516
+ index: prebuilt?.index ?? loadGraphIndexFile(options.frameworkRoot, 'metadata.json', 'framework'),
517
+ stats: prebuilt?.stats ?? loadGraphIndexFile(options.frameworkRoot, 'stats.json', 'framework'),
518
+ prebuiltValidated: Boolean(prebuilt),
516
519
  graph: 'framework',
517
520
  bundles: frameworkBundles,
518
521
  });
@@ -521,11 +524,12 @@ export async function verifyProviderDeployment(options) {
521
524
  const graph = options.scope === 'user' ? 'user' : 'project';
522
525
  indexesToVerify.push({
523
526
  index: loadGraphIndexFile(options.projectRoot, 'metadata.json', graph),
527
+ stats: loadGraphIndexFile(options.projectRoot, 'stats.json', graph),
524
528
  graph,
525
529
  bundles: projectLocalBundles,
526
530
  });
527
531
  }
528
- for (const { index, stats, graph, bundles } of indexesToVerify) {
532
+ for (const { index, stats, graph, bundles, prebuiltValidated } of indexesToVerify) {
529
533
  if (!index || !index.entries || Object.keys(index.entries).length === 0) {
530
534
  findings.push(finding(normalized, `index-unreadable:${graph}`, 'blocking', `The ${graph} capability index is missing, unreadable, or empty.`, `Run aiwg index build --graph ${graph}, then re-run the same aiwg use command.`));
531
535
  continue;
@@ -550,11 +554,14 @@ export async function verifyProviderDeployment(options) {
550
554
  if (!surfacePresent) {
551
555
  findings.push(finding(normalized, `index-surface-missing:${graph}`, 'blocking', `The ${graph} capability index does not contain the requested bundle surface.`, `Rebuild the ${graph} index from the selected AIWG source and re-run aiwg use.`, { requestedBundles: bundles, entryCount: entryPaths.length }));
552
556
  }
553
- if (options.invocationStartedAt) {
554
- const builtAt = Date.parse(index.builtAt);
557
+ if (options.invocationStartedAt && !prebuiltValidated) {
558
+ // No-op builds preserve metadata bytes and record the refresh in stats.
559
+ const refreshedAt = Math.max(...[index.builtAt, stats?.builtAt]
560
+ .map((timestamp) => Date.parse(timestamp ?? ''))
561
+ .filter(Number.isFinite));
555
562
  const startedAt = Date.parse(options.invocationStartedAt);
556
- if (!Number.isFinite(builtAt) || builtAt + 2_000 < startedAt) {
557
- findings.push(finding(normalized, `index-stale:${graph}`, 'blocking', `The ${graph} capability index was not refreshed during this deployment.`, `Re-run aiwg use after correcting the ${graph} index build failure.`, { graph, builtAt: index.builtAt, invocationStartedAt: options.invocationStartedAt }));
563
+ if (!Number.isFinite(refreshedAt) || refreshedAt + 2_000 < startedAt) {
564
+ findings.push(finding(normalized, `index-stale:${graph}`, 'blocking', `The ${graph} capability index was not refreshed during this deployment.`, `Re-run aiwg use after correcting the ${graph} index build failure.`, { graph, builtAt: index.builtAt, statsBuiltAt: stats?.builtAt, invocationStartedAt: options.invocationStartedAt }));
558
565
  }
559
566
  }
560
567
  }
@@ -569,7 +576,7 @@ export async function verifyProviderDeployment(options) {
569
576
  }
570
577
  const requiredContext = [
571
578
  path.join(options.projectRoot, 'WORKSPACE.md'),
572
- path.join(options.projectRoot, 'AIWG.md'),
579
+ ...(definition?.paths.contextFiles.aiwgMd === false ? [] : [path.join(options.projectRoot, 'AIWG.md')]),
573
580
  path.join(options.projectRoot, '.aiwg', 'AIWG.md'),
574
581
  ];
575
582
  for (const contextPath of requiredContext) {
@@ -773,9 +780,12 @@ export async function buildDeploymentStatusProbe(projectRoot, frameworkRoot = pr
773
780
  const notConfigured = result.requestedBundles.length === 0
774
781
  && result.findings.length > 0
775
782
  && result.findings.every((item) => item.id === 'deployment-not-configured');
783
+ const { rulePinState } = await import('../../rules/pin-state.js');
784
+ const rulePins = rulePinState(projectRoot, frameworkRoot);
776
785
  const engaged = result.outcome === 'ready' || result.outcome === 'ready-restart-required' || result.outcome === 'degraded';
777
786
  return {
778
787
  schema: 'aiwg.status.probe.v1',
788
+ rule_pins: { rules: rulePins.rules, warnings: rulePins.warnings },
779
789
  generated_at: result.generatedAt,
780
790
  project_root: result.projectRoot,
781
791
  engaged,
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ export async function prepareProviderDeployments(frameworkRoot, jobs) {
4
+ const deployerUrl = pathToFileURL(path.join(frameworkRoot, 'tools', 'agents', 'deploy-agents.mjs')).href;
5
+ const deployer = await import(deployerUrl);
6
+ // Commands-to-skills migration is interactive and changes provider inputs.
7
+ // Run it serially in the coordinator before the shared snapshot is created;
8
+ // finalization then only applies post-deploy mirrors/pruning/config writes.
9
+ for (const job of jobs)
10
+ await deployer.main(job.args, { preflightOnly: true });
11
+ const url = pathToFileURL(path.join(frameworkRoot, 'tools', 'agents', 'providers', 'provider-prepare-coordinator.mjs')).href;
12
+ const coordinator = await import(url);
13
+ const sourceIndex = jobs[0]?.args.findIndex(arg => arg === '--source') ?? -1;
14
+ const srcRoot = sourceIndex >= 0 && jobs[0]?.args[sourceIndex + 1]
15
+ ? path.resolve(jobs[0].args[sourceIndex + 1])
16
+ : frameworkRoot;
17
+ return coordinator.prepareProviderDeployments({ srcRoot, jobs, maxConcurrency: 2 });
18
+ }
19
+ export async function finalizePreparedProvider(frameworkRoot, args) {
20
+ const url = pathToFileURL(path.join(frameworkRoot, 'tools', 'agents', 'deploy-agents.mjs')).href;
21
+ const deployer = await import(url);
22
+ await deployer.main(args, { finalizeOnly: true });
23
+ }
24
+ //# sourceMappingURL=provider-prepare-coordinator.js.map
@@ -3,7 +3,7 @@ import { createReadStream, existsSync } from 'fs';
3
3
  import { createHash } from 'crypto';
4
4
  import { createInterface } from 'readline';
5
5
  import path from 'path';
6
- import os from 'os';
6
+ import { getUserStateDir } from '../config/user-state.js';
7
7
  import { readAiwgConfig } from '../config/aiwg-config.js';
8
8
  import { PROJECT_AIWG_LOCATION_FILE, projectAiwgPath, projectControlPath, } from '../config/project-artifacts.js';
9
9
  const DEFAULT_MAX_BYTES = 1_048_576;
@@ -487,8 +487,7 @@ function resolveUsagePath(scope, projectRoot, env) {
487
487
  return null;
488
488
  return projectAiwgPath(projectRoot, 'telemetry', 'skill-usage.jsonl');
489
489
  }
490
- const stateHome = env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state');
491
- return path.join(stateHome, 'aiwg', 'skill-usage.jsonl');
490
+ return path.join(getUserStateDir(env), 'skill-usage.jsonl');
492
491
  }
493
492
  async function appendBoundedJsonl(filePath, event, maxBytes) {
494
493
  await mkdir(path.dirname(filePath), { recursive: true });
@@ -1,3 +1,4 @@
1
+ import { validateDecisionConfig } from './decision-config.js';
1
2
  /**
2
3
  * Project-Level AIWG Config
3
4
  *
@@ -8,8 +9,13 @@
8
9
  *
9
10
  * @implements #621
10
11
  */
12
+ import { validateImportExclusions } from '../sessions/import-exclusions.js';
11
13
  import { resolveDispatchHostThresholds } from './dispatch-thresholds-runtime.mjs';
12
14
  export { DEFAULT_DISPATCH_HOST_THRESHOLDS, resolveDispatchHostThresholds } from './dispatch-thresholds-runtime.mjs';
15
+ import { validateIndexGraphOverrides } from './index-graph-overrides.js';
16
+ import { DEFAULT_RULES_INLINE_BUDGET_TOKENS } from '../rules/inline-budget.mjs';
17
+ export { validateRulesConfig, resolveRulesInlineBudget } from '../rules/inline-budget.mjs';
18
+ import { loadRulePinConfigs, installedRuleCapabilities, validateRulePinCapabilities } from '../rules/pins.mjs';
13
19
  import Ajv from 'ajv';
14
20
  import addFormats from 'ajv-formats';
15
21
  import { readFileSync, existsSync } from 'node:fs';
@@ -297,7 +303,7 @@ export function resolveWorkerGroupInstallation(parallelism, installationId, regi
297
303
  * `options.aiwgHome`, when provided, is passed through to `ConfigLoader`
298
304
  * unchanged — it never reads or writes `process.env.AIWG_HOME`. Omitting it
299
305
  * keeps `ConfigLoader`'s own default precedence (`AIWG_HOME` env, else the
300
- * user-home install).
306
+ * running package root).
301
307
  */
302
308
  export async function validateWorkerConfigCandidate(projectDir, config, options) {
303
309
  const errors = validateParallelismConfig(config.parallelism);
@@ -456,37 +462,7 @@ export function validateIndexConfig(index) {
456
462
  errors.push(`index.graphBackend: must be one of ${GRAPH_BACKENDS.join(' | ')}`);
457
463
  }
458
464
  const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string');
459
- const graphOverrides = indexObject.graphOverrides;
460
- if (graphOverrides !== undefined) {
461
- if (typeof graphOverrides !== 'object' || graphOverrides === null || Array.isArray(graphOverrides)) {
462
- errors.push('index.graphOverrides: must be an object mapping supported built-in graph names to overrides');
463
- }
464
- else {
465
- for (const [name, rawOverride] of Object.entries(graphOverrides)) {
466
- const where = `index.graphOverrides.${name}`;
467
- if (name !== 'codebase') {
468
- errors.push(`${where}: unsupported built-in graph override (supported: codebase)`);
469
- continue;
470
- }
471
- if (typeof rawOverride !== 'object' || rawOverride === null || Array.isArray(rawOverride)) {
472
- errors.push(`${where}: must be an object`);
473
- continue;
474
- }
475
- const override = rawOverride;
476
- for (const field of Object.keys(override)) {
477
- if (field !== 'scanDirs' && field !== 'extensions') {
478
- errors.push(`${where}.${field}: unknown field (supported: scanDirs, extensions)`);
479
- }
480
- }
481
- if (override.scanDirs !== undefined && (!isStringArray(override.scanDirs) || override.scanDirs.length === 0)) {
482
- errors.push(`${where}.scanDirs: must be a non-empty array of strings`);
483
- }
484
- if (override.extensions !== undefined && (!isStringArray(override.extensions) || override.extensions.length === 0)) {
485
- errors.push(`${where}.extensions: must be a non-empty array of strings`);
486
- }
487
- }
488
- }
489
- }
465
+ errors.push(...validateIndexGraphOverrides(indexObject.graphOverrides));
490
466
  const graphs = indexObject.graphs;
491
467
  if (graphs === undefined)
492
468
  return errors; // index with no graphs is permissible
@@ -973,6 +949,7 @@ export function emptyConfig(providers = ['claude']) {
973
949
  threatAssessment: defaultThreatAssessmentConfig(),
974
950
  },
975
951
  artifact_outputs: defaultArtifactOutputs(),
952
+ rules: { inline_budget_tokens: DEFAULT_RULES_INLINE_BUDGET_TOKENS },
976
953
  delivery: {
977
954
  mode: 'pr-required',
978
955
  default_branch: 'main',
@@ -1058,6 +1035,30 @@ export async function readAiwgConfigCandidate(projectDir) {
1058
1035
  parsed.installed = {};
1059
1036
  if (!parsed.scripts)
1060
1037
  parsed.scripts = {};
1038
+ if (parsed.sessions !== undefined && (parsed.sessions === null || typeof parsed.sessions !== 'object' || Array.isArray(parsed.sessions)
1039
+ || (parsed.sessions.codex_roots !== undefined && (!Array.isArray(parsed.sessions.codex_roots)
1040
+ || !parsed.sessions.codex_roots.every((root) => typeof root === 'string' && root.trim().length > 0))))) {
1041
+ throw new Error('sessions.codex_roots must be an array of non-empty paths');
1042
+ }
1043
+ if (parsed.sessions?.discovery_manifests !== undefined && (!Array.isArray(parsed.sessions.discovery_manifests)
1044
+ || !parsed.sessions.discovery_manifests.every((file) => typeof file === 'string' && file.trim().length > 0))) {
1045
+ throw new Error('sessions.discovery_manifests must be an array of non-empty paths');
1046
+ }
1047
+ if (parsed.sessions?.import !== undefined) {
1048
+ const limits = parsed.sessions.import;
1049
+ if (limits === null || typeof limits !== 'object' || Array.isArray(limits)) {
1050
+ throw new Error('sessions.import must be an object');
1051
+ }
1052
+ if (limits.exclude !== undefined)
1053
+ validateImportExclusions(limits.exclude);
1054
+ for (const key of ['max_record_bytes', 'max_source_bytes']) {
1055
+ const value = limits[key];
1056
+ const minimum = key === 'max_source_bytes' ? 0 : 1;
1057
+ if (value !== undefined && (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum)) {
1058
+ throw new Error(`sessions.import.${key} must be a ${minimum ? 'positive' : 'non-negative'} integer`);
1059
+ }
1060
+ }
1061
+ }
1061
1062
  const externalLinkErrors = validateExternalLinks(parsed.externalLinks);
1062
1063
  if (externalLinkErrors.length > 0) {
1063
1064
  throw new Error(`Invalid .aiwg/aiwg.config:\n${externalLinkErrors.join('\n')}`);
@@ -1076,6 +1077,8 @@ export async function readAiwgConfigCandidate(projectDir) {
1076
1077
  ...validateThreatAssessmentConfig(parsed.security?.threatAssessment),
1077
1078
  ...validateProjectConfig(parsed.project),
1078
1079
  ...validateParallelismConfig(parsed.parallelism),
1080
+ ...validateDecisionConfig(parsed.decision),
1081
+ ...validateRulePinCapabilities(parsed.rules, installedRuleCapabilities(loadRulePinConfigs(projectDir, parsed)), loadRulePinConfigs(projectDir, parsed).user.rules?.pin_groups),
1079
1082
  ];
1080
1083
  if (threatAssessmentErrors.length > 0) {
1081
1084
  throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
@@ -1119,10 +1122,46 @@ export async function readAiwgConfig(projectDir, options) {
1119
1122
  * sync so split-root health remains deterministic.
1120
1123
  */
1121
1124
  export async function writeAiwgConfig(projectDir, config) {
1125
+ // Uninstall is an explicit capability change. Retire project selectors for
1126
+ // removed capabilities before strict validation, and tell the operator.
1127
+ const previousFile = getConfigPath(projectDir);
1128
+ let previous = null;
1129
+ if (existsSync(previousFile)) {
1130
+ try {
1131
+ previous = JSON.parse(readFileSync(previousFile, 'utf8'));
1132
+ }
1133
+ catch (error) {
1134
+ if (!(error instanceof SyntaxError))
1135
+ throw error;
1136
+ }
1137
+ }
1138
+ const pinConfigs = loadRulePinConfigs(projectDir, config);
1139
+ const currentCapabilities = new Set(installedRuleCapabilities(pinConfigs));
1140
+ const removed = previous ? installedRuleCapabilities({ ...pinConfigs, project: previous }).filter(id => !currentCapabilities.has(id)) : [];
1141
+ if (removed.length && (config.rules?.always_load?.length || Object.keys(config.rules?.pin_groups ?? {}).length)) {
1142
+ const removedSet = new Set(removed);
1143
+ const rules = config.rules;
1144
+ const userGroups = loadRulePinConfigs(projectDir, config).user.rules?.pin_groups ?? {};
1145
+ const pinGroups = Object.fromEntries(Object.entries(rules.pin_groups ?? {})
1146
+ .map(([name, ids]) => [name, ids.filter(id => !removedSet.has(id))]).filter(([, ids]) => ids.length));
1147
+ const selectors = (rules.always_load ?? []).flatMap(selector => {
1148
+ if (selector.group && ((!Object.hasOwn(pinGroups, selector.group) && Object.hasOwn(rules.pin_groups ?? {}, selector.group))
1149
+ || (!Object.hasOwn(pinGroups, selector.group) && userGroups[selector.group]?.some(id => removedSet.has(id)))))
1150
+ return [];
1151
+ if (!selector.capabilities)
1152
+ return [selector];
1153
+ const capabilities = selector.capabilities.filter(id => !removedSet.has(id));
1154
+ return capabilities.length ? [{ ...selector, capabilities }] : [];
1155
+ });
1156
+ config = { ...config, rules: { ...config.rules, pin_groups: pinGroups, always_load: selectors } };
1157
+ console.warn(`Uninstalled ${removed.join(', ')}: their rules leave the effective pinned set; retired project capability selectors/groups. User pins remain visible as inactive warnings.`);
1158
+ }
1122
1159
  const threatAssessmentErrors = [
1123
1160
  ...validateThreatAssessmentConfig(config.security?.threatAssessment),
1124
1161
  ...validateProjectConfig(config.project),
1125
1162
  ...validateParallelismConfig(config.parallelism),
1163
+ ...validateDecisionConfig(config.decision),
1164
+ ...validateRulePinCapabilities(config.rules, installedRuleCapabilities(loadRulePinConfigs(projectDir, config)), loadRulePinConfigs(projectDir, config).user.rules?.pin_groups),
1126
1165
  ];
1127
1166
  if (threatAssessmentErrors.length > 0) {
1128
1167
  throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
@@ -1379,13 +1418,24 @@ export function validateWorkerGroupReferences(parallelism, selection, qualifiedR
1379
1418
  errors.push(`OG_CONFIG_REFERENCE /defaults/workerSelection/roleGuidance/${role}/groups: Unknown group ${group}`);
1380
1419
  }
1381
1420
  if (qualifiedRoles)
1382
- for (const [id, group] of Object.entries(parallelism.groups ?? {})) {
1383
- for (const role of group.requiredRoles ?? [])
1384
- if (!group.members.some(m => m.enabled !== false && parallelism.clis?.[m.cli]?.enabled !== false && m.roles.includes(role) && qualifiedRoles[`${id}/${m.id}`]?.includes(role)))
1385
- errors.push(`OG_ADAPTER_UNQUALIFIED /parallelism/groups/${id}/requiredRoles: No qualified route covers ${role}`);
1386
- }
1421
+ errors.push(...validateWorkerGroupQualifications(parallelism, qualifiedRoles)
1422
+ .map(diagnostic => `${diagnostic.code} ${diagnostic.pointer}: ${diagnostic.message}`));
1387
1423
  return errors;
1388
1424
  }
1425
+ /** Structured qualification gaps retain role scope for readiness projections. */
1426
+ export function validateWorkerGroupQualifications(parallelism, qualifiedRoles) {
1427
+ const diagnostics = [];
1428
+ for (const [id, group] of Object.entries(parallelism.groups ?? {})) {
1429
+ for (const role of group.requiredRoles ?? []) {
1430
+ if (!group.members.some(m => m.enabled !== false && parallelism.clis?.[m.cli]?.enabled !== false
1431
+ && m.roles.includes(role) && qualifiedRoles[`${id}/${m.id}`]?.includes(role))) {
1432
+ diagnostics.push({ code: 'OG_ADAPTER_UNQUALIFIED', pointer: `/parallelism/groups/${id}/requiredRoles`,
1433
+ message: `No qualified route covers ${role}`, roles: [role] });
1434
+ }
1435
+ }
1436
+ }
1437
+ return diagnostics;
1438
+ }
1389
1439
  /** A configured hard ceiling is eligible only where a qualified control enforces it. */
1390
1440
  export function validateWorkerBudgetEnforcement(budgets, enforceable) {
1391
1441
  if (budgets?.enforcement !== 'hard')