@aiwg/cli 2026.10.1 → 2026.10.4

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 (361) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/capability-matrix.yaml +270 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +3183 -24
  4. package/bin/aiwg.mjs +135 -47
  5. package/dist/src/a2a/codecs.js +9 -0
  6. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  7. package/dist/src/artifacts/browser-export.js +6 -0
  8. package/dist/src/artifacts/checksum-manifest.js +1 -0
  9. package/dist/src/artifacts/citation-parser.js +173 -92
  10. package/dist/src/artifacts/citation-table-schema.js +59 -0
  11. package/dist/src/artifacts/cli.js +57 -18
  12. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  13. package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
  14. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  15. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  16. package/dist/src/artifacts/discovery-eval.js +6 -4
  17. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  18. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  19. package/dist/src/artifacts/framework-capabilities.js +171 -0
  20. package/dist/src/artifacts/framework-index-paths.js +73 -0
  21. package/dist/src/artifacts/framework-source-digest.js +75 -0
  22. package/dist/src/artifacts/index-builder.js +285 -131
  23. package/dist/src/artifacts/index-files.js +132 -18
  24. package/dist/src/artifacts/index-preparation.js +65 -0
  25. package/dist/src/artifacts/index-refresh.js +576 -0
  26. package/dist/src/artifacts/index-worker.mjs +9 -0
  27. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  28. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  29. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  30. package/dist/src/artifacts/query-engine.js +410 -88
  31. package/dist/src/artifacts/relations-builder.js +168 -0
  32. package/dist/src/artifacts/relations.js +148 -0
  33. package/dist/src/artifacts/repair.js +9 -4
  34. package/dist/src/artifacts/stats.js +5 -2
  35. package/dist/src/artifacts/types.js +30 -10
  36. package/dist/src/artifacts/watcher.js +1 -1
  37. package/dist/src/auth/credential-store.js +2 -1
  38. package/dist/src/catalog/cli.d.mts +5 -0
  39. package/dist/src/channel/manager.d.mts +20 -0
  40. package/dist/src/channel/manager.mjs +78 -12
  41. package/dist/src/cli/child-process-result.js +40 -0
  42. package/dist/src/cli/command-log.js +2 -3
  43. package/dist/src/cli/env.js +38 -16
  44. package/dist/src/cli/flag-parsing.js +236 -0
  45. package/dist/src/cli/handlers/artifacts.js +3 -0
  46. package/dist/src/cli/handlers/auth.js +3 -0
  47. package/dist/src/cli/handlers/decision.js +218 -5
  48. package/dist/src/cli/handlers/diagnose.js +13 -5
  49. package/dist/src/cli/handlers/effect.js +4 -2
  50. package/dist/src/cli/handlers/evidence.js +3 -0
  51. package/dist/src/cli/handlers/feedback.js +14 -5
  52. package/dist/src/cli/handlers/help.js +1 -0
  53. package/dist/src/cli/handlers/index.js +3 -1
  54. package/dist/src/cli/handlers/init.js +248 -30
  55. package/dist/src/cli/handlers/lint.js +5 -0
  56. package/dist/src/cli/handlers/mc.js +23 -0
  57. package/dist/src/cli/handlers/models.js +169 -10
  58. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  59. package/dist/src/cli/handlers/ralph.js +3 -0
  60. package/dist/src/cli/handlers/refresh.js +28 -0
  61. package/dist/src/cli/handlers/regenerate.js +14 -0
  62. package/dist/src/cli/handlers/resource-versions.js +3 -0
  63. package/dist/src/cli/handlers/rules.js +195 -0
  64. package/dist/src/cli/handlers/run.js +4 -0
  65. package/dist/src/cli/handlers/runtime-info.js +25 -11
  66. package/dist/src/cli/handlers/script-runner.js +19 -8
  67. package/dist/src/cli/handlers/session.js +406 -62
  68. package/dist/src/cli/handlers/sessions.js +243 -76
  69. package/dist/src/cli/handlers/setup-manifest.js +474 -79
  70. package/dist/src/cli/handlers/setup.js +180 -39
  71. package/dist/src/cli/handlers/steward.js +11 -0
  72. package/dist/src/cli/handlers/subcommands.js +60 -3
  73. package/dist/src/cli/handlers/team.js +18 -8
  74. package/dist/src/cli/handlers/uhp.js +3 -0
  75. package/dist/src/cli/handlers/use.js +307 -55
  76. package/dist/src/cli/handlers/utilities.js +125 -26
  77. package/dist/src/cli/handlers/version.js +8 -0
  78. package/dist/src/cli/prompt-utils.js +72 -0
  79. package/dist/src/cli/provider-resolution.js +11 -0
  80. package/dist/src/cli/router.js +15 -1
  81. package/dist/src/cli/services/deployment-verification.js +19 -9
  82. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  83. package/dist/src/cli/skill-usage.js +2 -3
  84. package/dist/src/config/aiwg-config.js +349 -35
  85. package/dist/src/config/cli.js +256 -23
  86. package/dist/src/config/decision-config.js +106 -0
  87. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  88. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  89. package/dist/src/config/gitignore.js +50 -0
  90. package/dist/src/config/index-graph-overrides.js +36 -0
  91. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  92. package/dist/src/config/project-artifacts-health.mjs +14 -3
  93. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  94. package/dist/src/config/user-config-dir.d.mts +9 -0
  95. package/dist/src/config/user-config.js +38 -11
  96. package/dist/src/config/user-registry.js +23 -15
  97. package/dist/src/config/user-state.js +7 -0
  98. package/dist/src/decision/adapters/jev.js +75 -27
  99. package/dist/src/decision/ask.js +203 -43
  100. package/dist/src/decision/context-live-qualification.js +2 -2
  101. package/dist/src/decision/driver.js +14 -7
  102. package/dist/src/decision/ensemble/aggregate.js +23 -10
  103. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  104. package/dist/src/decision/entry.js +25 -6
  105. package/dist/src/decision/graph-live-qualification.js +1 -1
  106. package/dist/src/decision/graph-run.js +9 -0
  107. package/dist/src/decision/heldout/collector.js +15 -14
  108. package/dist/src/decision/heldout/contract.js +30 -7
  109. package/dist/src/decision/index.js +1 -0
  110. package/dist/src/decision/interop.js +60 -8
  111. package/dist/src/decision/jev-credentials.js +15 -1
  112. package/dist/src/decision/ledger.js +942 -0
  113. package/dist/src/decision/offload-activation.js +137 -0
  114. package/dist/src/decision/offload.js +550 -0
  115. package/dist/src/decision/qualification/load.js +4 -6
  116. package/dist/src/decision/savings.js +28 -0
  117. package/dist/src/decision/validate.js +6 -3
  118. package/dist/src/effects/dispatch-verifier.js +252 -0
  119. package/dist/src/effects/index.js +1 -0
  120. package/dist/src/effects/types.js +5 -1
  121. package/dist/src/effects/verifiers/git.js +2 -1
  122. package/dist/src/effects/verifiers/tracker.js +26 -14
  123. package/dist/src/extensions/commands/definitions.js +19 -0
  124. package/dist/src/extensions/project-local-remove.js +36 -1
  125. package/dist/src/features/installer.js +2 -2
  126. package/dist/src/flow/graph-metadata.js +5 -0
  127. package/dist/src/gates/providers/loader.js +64 -18
  128. package/dist/src/installation/manager-command.d.mts +18 -0
  129. package/dist/src/installation/manager-command.mjs +15 -3
  130. package/dist/src/installation/manager.d.mts +49 -0
  131. package/dist/src/installation/manager.mjs +95 -5
  132. package/dist/src/issues/cli.js +4 -4
  133. package/dist/src/issues/local.js +24 -5
  134. package/dist/src/jobs/runner.js +1 -1
  135. package/dist/src/lint/cli.js +27 -1
  136. package/dist/src/lint/loader.js +2 -1
  137. package/dist/src/lint/reporters.js +25 -1
  138. package/dist/src/lint/rule-activation.js +43 -0
  139. package/dist/src/lint/runner.js +137 -62
  140. package/dist/src/mcp/adapters/codex-runtime.js +366 -56
  141. package/dist/src/mcp/claude-install.d.mts +4 -0
  142. package/dist/src/mcp/claude-install.mjs +41 -0
  143. package/dist/src/mcp/cli.d.mts +1 -0
  144. package/dist/src/mcp/cli.mjs +620 -85
  145. package/dist/src/mcp/config-file.d.mts +28 -0
  146. package/dist/src/mcp/config-file.mjs +279 -0
  147. package/dist/src/mcp/config-layers.d.mts +36 -0
  148. package/dist/src/mcp/config-layers.mjs +348 -0
  149. package/dist/src/mcp/credentials.d.mts +51 -0
  150. package/dist/src/mcp/credentials.mjs +484 -0
  151. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  152. package/dist/src/mcp/grok-build-config.mjs +17 -27
  153. package/dist/src/mcp/helpers.mjs +109 -4
  154. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  155. package/dist/src/mcp/muse-native-config.mjs +1 -0
  156. package/dist/src/mcp/omp-config.d.mts +4 -0
  157. package/dist/src/mcp/omp-config.mjs +50 -29
  158. package/dist/src/mcp/profiles.js +97 -23
  159. package/dist/src/mcp/profiles.mjs +99 -27
  160. package/dist/src/mcp/registry.js +248 -131
  161. package/dist/src/mcp/registry.mjs +169 -100
  162. package/dist/src/mcp/server-config.d.mts +3 -0
  163. package/dist/src/mcp/server-config.mjs +89 -0
  164. package/dist/src/mcp/server.mjs +127 -82
  165. package/dist/src/mcp/test-simple.mjs +2 -1
  166. package/dist/src/mcp/toml-editor.d.mts +4 -0
  167. package/dist/src/mcp/toml-string.d.mts +1 -0
  168. package/dist/src/mcp/toml-string.mjs +9 -0
  169. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  170. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  171. package/dist/src/mcp/tool-filters.d.mts +46 -0
  172. package/dist/src/mcp/tool-filters.mjs +357 -0
  173. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  174. package/dist/src/mcp/tools/decision.mjs +22 -3
  175. package/dist/src/mcp/tools/discovery.mjs +62 -55
  176. package/dist/src/mcp/tools/orchestration.mjs +50 -2
  177. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  178. package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
  179. package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
  180. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  181. package/dist/src/memory/project-registry.js +2 -2
  182. package/dist/src/mission-protocol/codecs.js +647 -1
  183. package/dist/src/mission-protocol/types.js +110 -0
  184. package/dist/src/mission-protocol/worker-status.js +2 -0
  185. package/dist/src/models/config-loader.js +76 -14
  186. package/dist/src/models/grok-build-models.js +93 -41
  187. package/dist/src/models/model-capabilities.v1.json +3183 -24
  188. package/dist/src/models/model-discovery.js +931 -67
  189. package/dist/src/models/provider-policy.js +719 -11
  190. package/dist/src/models/qualification-digest.d.mts +2 -0
  191. package/dist/src/models/qualification-digest.mjs +17 -0
  192. package/dist/src/models/resolver.js +261 -0
  193. package/dist/src/models/worker-direct-readiness.js +550 -0
  194. package/dist/src/models/worker-dispatch-offer.js +169 -0
  195. package/dist/src/models/worker-health-readiness.js +144 -0
  196. package/dist/src/models/worker-installation.d.mts +39 -0
  197. package/dist/src/models/worker-installation.mjs +986 -0
  198. package/dist/src/models/wrapper-route.js +18 -0
  199. package/dist/src/ops/registry.js +9 -14
  200. package/dist/src/plugins/standalone-packager.js +4 -3
  201. package/dist/src/process/bounded-child-process.d.mts +84 -0
  202. package/dist/src/process/bounded-child-process.mjs +430 -0
  203. package/dist/src/process/cli-command.d.mts +16 -0
  204. package/dist/src/process/cli-command.mjs +75 -0
  205. package/dist/src/process/signal-child.d.mts +6 -0
  206. package/dist/src/process/signal-child.mjs +7 -0
  207. package/dist/src/providers/capability-matrix.js +8 -0
  208. package/dist/src/providers/capability-matrix.yaml +270 -0
  209. package/dist/src/providers/deployed-skill-roots.js +20 -0
  210. package/dist/src/providers/grok-build-acp.js +3 -2
  211. package/dist/src/providers/grok-build-headless.js +53 -23
  212. package/dist/src/providers/grok-build-inspect.js +1 -0
  213. package/dist/src/providers/omp-agent.d.mts +8 -0
  214. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  215. package/dist/src/providers/omp-paths.d.mts +13 -0
  216. package/dist/src/providers/provider-definitions.js +9 -1
  217. package/dist/src/providers/provider-definitions.mjs +9 -2
  218. package/dist/src/providers/skill-ownership.d.mts +9 -0
  219. package/dist/src/providers/skill-ownership.mjs +88 -0
  220. package/dist/src/rules/activation.d.mts +21 -0
  221. package/dist/src/rules/activation.mjs +62 -0
  222. package/dist/src/rules/inline-budget.d.mts +26 -0
  223. package/dist/src/rules/inline-budget.mjs +66 -0
  224. package/dist/src/rules/inventory.js +129 -0
  225. package/dist/src/rules/pin-state.js +73 -0
  226. package/dist/src/rules/pins.d.mts +14 -0
  227. package/dist/src/rules/pins.mjs +112 -0
  228. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  229. package/dist/src/serve/executor-registry.js +16 -24
  230. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  231. package/dist/src/serve/mission-conductor.js +47 -12
  232. package/dist/src/serve/shared-host-scheduler.js +2 -0
  233. package/dist/src/serve/worker-invocation.js +558 -0
  234. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  235. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  236. package/dist/src/sessions/adapter-registry.js +49 -0
  237. package/dist/src/sessions/adapters/claude.js +39 -54
  238. package/dist/src/sessions/adapters/codex.js +82 -67
  239. package/dist/src/sessions/adapters/copilot.js +4 -4
  240. package/dist/src/sessions/adapters/cursor.js +40 -57
  241. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  242. package/dist/src/sessions/adapters/factory.js +31 -27
  243. package/dist/src/sessions/adapters/generic.js +43 -9
  244. package/dist/src/sessions/adapters/grok-build.js +11 -11
  245. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  246. package/dist/src/sessions/adapters/grokbot.js +434 -24
  247. package/dist/src/sessions/adapters/hermes.js +27 -29
  248. package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
  249. package/dist/src/sessions/adapters/muse.js +200 -104
  250. package/dist/src/sessions/adapters/omp.js +28 -14
  251. package/dist/src/sessions/adapters/openclaw.js +89 -79
  252. package/dist/src/sessions/adapters/opencode.js +27 -24
  253. package/dist/src/sessions/adapters/openhuman.js +18 -60
  254. package/dist/src/sessions/adapters/pi.js +30 -14
  255. package/dist/src/sessions/adapters/warp.js +3 -3
  256. package/dist/src/sessions/adapters/windsurf.js +19 -48
  257. package/dist/src/sessions/batch-contracts.js +25 -5
  258. package/dist/src/sessions/batch-import.js +72 -51
  259. package/dist/src/sessions/candidates.js +47 -11
  260. package/dist/src/sessions/contracts.js +75 -2
  261. package/dist/src/sessions/coverage-preflight.js +134 -0
  262. package/dist/src/sessions/discovery.js +3 -0
  263. package/dist/src/sessions/export-plan.js +4 -3
  264. package/dist/src/sessions/grok-build-cli.js +67 -5
  265. package/dist/src/sessions/import-exclusions.js +72 -0
  266. package/dist/src/sessions/import-lease.js +34 -18
  267. package/dist/src/sessions/import-limits.js +37 -0
  268. package/dist/src/sessions/importer.js +84 -46
  269. package/dist/src/sessions/index.js +2 -0
  270. package/dist/src/sessions/lifecycle.js +45 -0
  271. package/dist/src/sessions/policy.js +133 -25
  272. package/dist/src/sessions/readers.js +106 -53
  273. package/dist/src/sessions/refresh.js +106 -0
  274. package/dist/src/sessions/repository.js +199 -63
  275. package/dist/src/sessions/workspace-discovery.js +315 -88
  276. package/dist/src/sessions/workspace-matching.js +107 -0
  277. package/dist/src/skills/adapters/clawhub.js +10 -8
  278. package/dist/src/skills/adapters/openclaw.js +10 -7
  279. package/dist/src/skills/importer.js +1 -0
  280. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  281. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  282. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  283. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  284. package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
  285. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  286. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  287. package/dist/src/smiths/mcpsmith/example.js +1 -1
  288. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  289. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  290. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
  291. package/dist/src/storage/backends/fs.js +18 -76
  292. package/dist/src/storage/backends/logseq.js +68 -7
  293. package/dist/src/storage/backends/obsidian.js +1 -0
  294. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  295. package/dist/src/testing/generators/test-code-generator.js +57 -30
  296. package/dist/src/tracker/forge-http.js +2 -1
  297. package/dist/src/uhp/client.js +11 -1
  298. package/dist/src/uhp/mission.js +5 -0
  299. package/dist/src/update/checker.d.mts +2 -0
  300. package/dist/src/update/checker.mjs +5 -5
  301. package/dist/src/update/notifier.mjs +54 -2
  302. package/dist/src/update/service.d.mts +22 -0
  303. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  304. package/dist/src/utils/contained-filesystem.mjs +683 -0
  305. package/dist/src/utils/temporary-directory.js +36 -0
  306. package/dist/src/writing/example-generator.js +1 -1
  307. package/dist/src/writing/fidelity.js +18 -1
  308. package/package.json +32 -2
  309. package/schemas/mission-v1.schema.json +2429 -0
  310. package/schemas/models/model-catalog.v1.schema.json +301 -0
  311. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  312. package/schemas/models/model-policy.v1.schema.json +14 -0
  313. package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
  314. package/tools/_resolve-impl.mjs +39 -2
  315. package/tools/agents/deploy-agents.mjs +64 -23
  316. package/tools/agents/providers/base.mjs +153 -166
  317. package/tools/agents/providers/claude.mjs +8 -24
  318. package/tools/agents/providers/codex.mjs +18 -11
  319. package/tools/agents/providers/cursor.mjs +19 -18
  320. package/tools/agents/providers/grok-build.mjs +3 -1
  321. package/tools/agents/providers/grokbot.mjs +11 -12
  322. package/tools/agents/providers/hermes.mjs +62 -49
  323. package/tools/agents/providers/omp.mjs +5 -2
  324. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  325. package/tools/agents/providers/openclaw.mjs +9 -4
  326. package/tools/agents/providers/pi.mjs +5 -0
  327. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  328. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  329. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  330. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  331. package/tools/agents/providers/warp.mjs +20 -26
  332. package/tools/cli/decision-offload-report.mjs +40 -0
  333. package/tools/cli/workspace-status.mjs +862 -0
  334. package/tools/plugin/package-plugins.mjs +186 -8
  335. package/tools/plugin/plugin-links.mjs +114 -0
  336. package/tools/providers/antigravity-transport.mjs +3 -1
  337. package/tools/providers/deepseek-harness-transport.mjs +360 -0
  338. package/tools/providers/omp-transport.mjs +194 -0
  339. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  340. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  341. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  342. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  343. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  344. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  345. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  346. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  347. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  348. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  349. package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
  350. package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
  351. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  352. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  353. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  354. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  355. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  356. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  357. package/tools/ralph-external/process-monitor.mjs +1017 -0
  358. package/tools/ralph-external/session-launcher.mjs +5057 -0
  359. package/tools/scripts/resource-preflight.mjs +541 -0
  360. package/tools/skills/deploy-skills-codex.mjs +2 -1
  361. package/vscode-extension/schemas/aiwg.config.v1.json +2916 -0
@@ -1,8 +1,13 @@
1
+ import { signalChild } from '../process/signal-child.mjs';
2
+ import { hasSelectionRelevantConflicts, validateWorkerCatalogDiscovery } from './provider-policy.js';
1
3
  import { spawn } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
2
5
  import { createInterface } from 'node:readline';
3
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
- import { dirname, join } from 'node:path';
6
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, writeFile } from 'node:fs/promises';
7
+ import { dirname, join, relative } from 'node:path';
8
+ import { constants } from 'node:fs';
5
9
  import { homedir, tmpdir } from 'node:os';
10
+ import { stripVTControlCharacters } from 'node:util';
6
11
  export const PROVIDER_DISCOVERY_DECISIONS = {
7
12
  claude: {
8
13
  provider: 'claude',
@@ -14,8 +19,8 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
14
19
  codex: {
15
20
  provider: 'codex',
16
21
  status: 'native',
17
- interface: 'codex app-server model/list',
18
- reason: 'The app-server JSON-RPC model/list method returns models available to the current Codex account without running a model turn.',
22
+ interface: 'codex app-server model/list; codex debug models (version-scoped fixture)',
23
+ reason: 'The app-server JSON-RPC model/list method returns models available to the current Codex account without running a model turn. The version-scoped codex debug models JSON shape normalizes through the same observation contract.',
19
24
  documentation: 'https://developers.openai.com/codex/app-server/',
20
25
  },
21
26
  copilot: {
@@ -62,9 +67,9 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
62
67
  },
63
68
  muse: {
64
69
  provider: 'muse',
65
- status: 'unsupported',
66
- interface: null,
67
- reason: 'Muse Code is a terminal/CI coding agent with no verified non-interactive model-list command; AIWG does not invent Meta Model API enumeration as Muse Code entitlement.',
70
+ status: 'native',
71
+ interface: 'model-catalog JSON rows (~/.local/share/muse/model-catalog/*.json)',
72
+ reason: 'Bounded model-catalog JSON rows report configured model facts for the current Muse installation without probing model APIs or consuming tokens. Entitlement stays account-scoped and contributor data-use facts remain config-scoped, never row-derived.',
68
73
  documentation: 'https://github.com/jmagly/aiwg/blob/main/docs/architecture/adr-muse-provider-target.md',
69
74
  },
70
75
  opencode: {
@@ -88,6 +93,13 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
88
93
  reason: 'OpenHuman profiles accept semantic model hints but expose no standardized local model-list command.',
89
94
  documentation: 'https://github.com/roctinam/openhuman',
90
95
  },
96
+ ollama: {
97
+ provider: 'ollama',
98
+ status: 'native',
99
+ interface: 'GET /api/tags on the configured local endpoint',
100
+ reason: 'The read-only HTTP model-list endpoint reports the locally present models and per-model digests for the configured Ollama server without probing a remote registry or consuming inference tokens. Declared here (roctinam/aiwg#2899, B-OG-09); no fetcher is implemented against this map by this task — see the local-http worker-adapter registry entry and its adapter docstring for what is actually dispatchable today.',
101
+ documentation: 'https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models',
102
+ },
91
103
  omp: { provider: 'omp', status: 'native', interface: 'omp models --json --no-extensions',
92
104
  reason: 'OMP JSON catalog reports credential-available LLM models; ambient extensions are disabled by default.',
93
105
  documentation: 'https://github.com/can1357/oh-my-pi/blob/5964a0f7649275bcde818f20073193fd032451f2/packages/coding-agent/src/commands/models.ts' },
@@ -115,6 +127,12 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
115
127
  };
116
128
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
117
129
  export function classifyDiscoveryError(message) {
130
+ message = stripVTControlCharacters(message);
131
+ if (/\b(?:EROFS|EACCES|EPERM)\b|read-only file system/i.test(message)
132
+ || (/FileSystem\.open|permission denied|operation not permitted/i.test(message)
133
+ && /[\\/]opencode[\\/]log[\\/]|[\\/]\.pi[\\/]agent(?:[\\/]|$)/i.test(message))) {
134
+ return 'environment';
135
+ }
118
136
  if (/(?:401|403|unauth|forbidden|credential|login|required auth)/i.test(message)) {
119
137
  return 'authentication';
120
138
  }
@@ -128,7 +146,7 @@ const SECRET_ASSIGNMENT = /((?:api[_-]?key|authorization|token|secret|password|c
128
146
  const BEARER_SECRET = /(bearer\s+)[A-Za-z0-9._~+/=-]+/gi;
129
147
  /** Keep native diagnostics useful without ever echoing credential material. */
130
148
  export function redactModelDiscoveryText(value) {
131
- return value
149
+ return stripVTControlCharacters(value)
132
150
  .replace(SECRET_ASSIGNMENT, '$1[REDACTED]')
133
151
  .replace(BEARER_SECRET, '$1[REDACTED]')
134
152
  .slice(0, 4096);
@@ -152,7 +170,7 @@ export const runModelDiscoveryCommand = (command, args, options = {}) => new Pro
152
170
  child.stdout.on('data', chunk => { stdout += String(chunk); if (Buffer.byteLength(stdout) > 8 * 1024 * 1024) {
153
171
  stdout = '';
154
172
  stderr = 'Model catalog exceeded 8 MiB limit';
155
- child.kill('SIGKILL');
173
+ signalChild(child, 'SIGKILL');
156
174
  finish(1);
157
175
  } });
158
176
  child.stderr.on('data', chunk => { stderr = (stderr + String(chunk)).slice(-4096); });
@@ -163,7 +181,7 @@ export const runModelDiscoveryCommand = (command, args, options = {}) => new Pro
163
181
  child.on('close', code => finish(code ?? 1));
164
182
  const timer = setTimeout(() => {
165
183
  stderr = `${stderr}${stderr ? '\n' : ''}${command} timed out`;
166
- child.kill();
184
+ signalChild(child);
167
185
  finish(124);
168
186
  }, options.timeoutMs ?? 10_000);
169
187
  });
@@ -178,7 +196,7 @@ export async function discoverOpenCodeModels(command = 'opencode', runner = runM
178
196
  runner(command, ['models', '--pure'], { cwd: tmpdir(), timeoutMs: 15_000 }),
179
197
  ]);
180
198
  if (result.exitCode !== 0) {
181
- const error = result.stderr.trim() || `OpenCode models exited ${result.exitCode}`;
199
+ const error = redactModelDiscoveryText(result.stderr.trim()) || `OpenCode models exited ${result.exitCode}`;
182
200
  return {
183
201
  provider: 'opencode',
184
202
  source: 'native',
@@ -190,10 +208,16 @@ export async function discoverOpenCodeModels(command = 'opencode', runner = runM
190
208
  error,
191
209
  };
192
210
  }
193
- const models = [...new Set(result.stdout.split(/\r?\n/)
194
- .map(line => line.trim())
195
- .filter(line => /^[a-z0-9][a-z0-9._-]*\/\S+$/i.test(line)))]
196
- .map(id => ({ id }));
211
+ const diagnostics = [];
212
+ const models = [...new Set(result.stdout.split(/\r?\n/).flatMap((raw, index) => {
213
+ const line = raw.trim();
214
+ if (!line)
215
+ return [];
216
+ if (/^[a-z0-9][a-z0-9._-]*\/\S+$/i.test(line))
217
+ return [line];
218
+ diagnostics.push(`OpenCode models line ${index + 1}: malformed provider/model row`);
219
+ return [];
220
+ }))].map(id => ({ id }));
197
221
  return {
198
222
  provider: 'opencode',
199
223
  source: 'native',
@@ -201,8 +225,8 @@ export async function discoverOpenCodeModels(command = 'opencode', runner = runM
201
225
  ...(version ? { runtimeVersion: version } : {}),
202
226
  accountScope: 'local-runtime',
203
227
  models,
204
- ...(models.length === 0
205
- ? { errorKind: 'invalid-output', error: 'OpenCode returned no normalized provider/model rows.' }
228
+ ...(diagnostics.length || models.length === 0
229
+ ? { errorKind: 'invalid-output', error: diagnostics.join('; ') || 'OpenCode returned no normalized provider/model rows.' }
206
230
  : {}),
207
231
  };
208
232
  }
@@ -213,22 +237,33 @@ export async function discoverPiModels(command = 'pi', runner = runModelDiscover
213
237
  runner(command, ['--list-models'], { cwd: tmpdir(), timeoutMs: 15_000 }),
214
238
  ]);
215
239
  if (result.exitCode !== 0) {
216
- const error = result.stderr.trim() || `Pi --list-models exited ${result.exitCode}`;
240
+ const error = redactModelDiscoveryText(result.stderr.trim()) || `Pi --list-models exited ${result.exitCode}`;
217
241
  return { provider: 'pi', source: 'native', observedAt,
218
242
  ...(version ? { runtimeVersion: version } : {}), accountScope: 'local-runtime',
219
243
  models: [], errorKind: classifyDiscoveryError(error), error };
220
244
  }
221
- const lines = result.stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
222
- const models = lines.slice(1).flatMap(line => {
245
+ const diagnostics = [];
246
+ const lines = result.stdout.split(/\r?\n/);
247
+ let headerSeen = false;
248
+ const models = lines.flatMap((raw, index) => {
249
+ const line = raw.trim();
250
+ if (!line)
251
+ return [];
252
+ if (!headerSeen && /^provider\s+model(?:\s|$)/i.test(line)) {
253
+ headerSeen = true;
254
+ return [];
255
+ }
223
256
  const columns = line.split(/\s{2,}/);
224
- if (columns.length < 2 || !/^[a-z0-9][a-z0-9._-]*$/i.test(columns[0]))
257
+ if (columns.length < 2 || !/^[a-z0-9][a-z0-9._-]*$/i.test(columns[0]) || !/^\S+$/.test(columns[1])) {
258
+ diagnostics.push(`Pi --list-models line ${index + 1}: malformed provider/model row`);
225
259
  return [];
260
+ }
226
261
  return [{ id: `${columns[0]}/${columns[1]}` }];
227
262
  });
228
263
  return { provider: 'pi', source: 'native', observedAt,
229
264
  ...(version ? { runtimeVersion: version } : {}), accountScope: 'local-runtime', models,
230
- ...(models.length === 0 ? { errorKind: 'invalid-output',
231
- error: 'Pi returned no parseable provider/model rows.' } : {}) };
265
+ ...(diagnostics.length || models.length === 0 ? { errorKind: 'invalid-output',
266
+ error: diagnostics.join('; ') || 'Pi returned no parseable provider/model rows.' } : {}) };
232
267
  }
233
268
  export async function discoverOpenClawModels(command = 'openclaw', runner = runModelDiscoveryCommand) {
234
269
  const observedAt = new Date().toISOString();
@@ -237,7 +272,7 @@ export async function discoverOpenClawModels(command = 'openclaw', runner = runM
237
272
  runner(command, ['models', 'list', '--json'], { timeoutMs: 15_000 }),
238
273
  ]);
239
274
  if (result.exitCode !== 0) {
240
- const error = result.stderr.trim() || `OpenClaw models list exited ${result.exitCode}`;
275
+ const error = redactModelDiscoveryText(result.stderr.trim()) || `OpenClaw models list exited ${result.exitCode}`;
241
276
  return {
242
277
  provider: 'openclaw',
243
278
  source: 'native',
@@ -251,13 +286,25 @@ export async function discoverOpenClawModels(command = 'openclaw', runner = runM
251
286
  }
252
287
  try {
253
288
  const payload = JSON.parse(result.stdout);
254
- const models = (payload.models ?? []).flatMap(model => typeof model.key === 'string' && model.key.includes('/') && model.available !== false
255
- ? [{
289
+ if (!Array.isArray(payload?.models))
290
+ throw new Error('expected /models array');
291
+ const diagnostics = [];
292
+ const models = payload.models.flatMap((model, index) => {
293
+ if (!model || typeof model !== 'object' || Array.isArray(model)
294
+ || typeof model.key !== 'string' || !/^[^\s/]+\/\S+$/.test(model.key)
295
+ || (model.available !== undefined && typeof model.available !== 'boolean')
296
+ || (model.name !== undefined && typeof model.name !== 'string')
297
+ || (model.tags !== undefined && (!Array.isArray(model.tags) || model.tags.some((tag) => typeof tag !== 'string')))) {
298
+ diagnostics.push(`OpenClaw /models/${index}: malformed model row`);
299
+ return [];
300
+ }
301
+ return model.available !== false ? [{
256
302
  id: model.key,
257
303
  ...(typeof model.name === 'string' ? { displayName: model.name } : {}),
258
304
  isDefault: Array.isArray(model.tags) && model.tags.includes('default'),
259
305
  }]
260
- : []);
306
+ : [];
307
+ });
261
308
  return {
262
309
  provider: 'openclaw',
263
310
  source: 'native',
@@ -265,8 +312,8 @@ export async function discoverOpenClawModels(command = 'openclaw', runner = runM
265
312
  ...(version ? { runtimeVersion: version } : {}),
266
313
  accountScope: 'local-runtime',
267
314
  models,
268
- ...(models.length === 0
269
- ? { errorKind: 'invalid-output', error: 'OpenClaw returned no normalized model keys.' }
315
+ ...(diagnostics.length || models.length === 0
316
+ ? { errorKind: 'invalid-output', error: diagnostics.join('; ') || 'OpenClaw returned no normalized model keys.' }
270
317
  : {}),
271
318
  };
272
319
  }
@@ -294,6 +341,8 @@ async function readCatalog(file) {
294
341
  const parsed = JSON.parse(await readFile(file, 'utf8'));
295
342
  if (!parsed.version || !parsed.providers || typeof parsed.providers !== 'object')
296
343
  return null;
344
+ if (validateWorkerCatalogDiscovery(parsed.discovery).length)
345
+ return null;
297
346
  return parsed;
298
347
  }
299
348
  catch {
@@ -328,7 +377,27 @@ const STANDARD_FAMILY = /(?:^|[/:._-])(?:sonnet|terra)(?:$|[/:._[-]|\d)/i;
328
377
  export function selectRoleModels(models) {
329
378
  // Flagship families are user-elected only (epic #2642): discovery never
330
379
  // assigns them to a role, even when the provider marks one as its default.
331
- const visible = models.filter(model => !model.hidden && !FLAGSHIP_FAMILY.test(model.id));
380
+ const visible = models.filter(model => {
381
+ // Keep the legacy name-based selection only for unenriched providers.
382
+ const worker = model.worker;
383
+ return !retirementGuard(null, model).blocked && !model.hidden && !FLAGSHIP_FAMILY.test(model.id)
384
+ && (!worker || (worker.qualification.status === 'qualified'
385
+ && worker.visibility === 'listed' && worker.availability === 'accessible'
386
+ && (worker.lifecycle === 'active' || worker.lifecycle === 'deprecated')
387
+ && worker.effortSupport !== 'unknown' && !hasSelectionRelevantConflicts(worker)));
388
+ });
389
+ if (visible.length === 0)
390
+ return {};
391
+ const selected = {};
392
+ for (const role of ['reasoning', 'coding', 'efficiency']) {
393
+ const candidates = visible.filter(model => !model.worker || model.worker.qualification.roles.includes(role));
394
+ const model = rankRoleModels(candidates)[role];
395
+ if (model)
396
+ selected[role] = model;
397
+ }
398
+ return selected;
399
+ }
400
+ function rankRoleModels(visible) {
332
401
  if (visible.length === 0)
333
402
  return {};
334
403
  const coding = visible.find(model => STANDARD_FAMILY.test(model.id))
@@ -379,6 +448,8 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
379
448
  const lines = createInterface({ input: child.stdout });
380
449
  let settled = false;
381
450
  let stderr = '';
451
+ const diagnostics = [];
452
+ let lineNumber = 0;
382
453
  let detectedRuntimeVersion = version;
383
454
  let timer;
384
455
  const finish = (result) => {
@@ -387,10 +458,10 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
387
458
  settled = true;
388
459
  clearTimeout(timer);
389
460
  lines.close();
390
- child.kill();
461
+ signalChild(child);
391
462
  resolveDiscovery(result);
392
463
  };
393
- child.stderr.on('data', chunk => { stderr += String(chunk); });
464
+ child.stderr.on('data', chunk => { stderr = (stderr + String(chunk)).slice(-4096); });
394
465
  child.on('error', error => finish({
395
466
  provider: 'codex',
396
467
  source: 'native',
@@ -398,14 +469,20 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
398
469
  accountScope: 'local-account',
399
470
  models: [],
400
471
  errorKind: classifyDiscoveryError(error.message),
401
- error: error.message,
472
+ error: redactModelDiscoveryText(error.message),
402
473
  }));
403
474
  lines.on('line', line => {
475
+ lineNumber++;
404
476
  let message;
405
477
  try {
406
478
  message = JSON.parse(line);
407
479
  }
408
480
  catch {
481
+ diagnostics.push(`Codex app-server line ${lineNumber}: invalid JSON`);
482
+ return;
483
+ }
484
+ if (!message || typeof message !== 'object') {
485
+ diagnostics.push(`Codex app-server line ${lineNumber}: expected JSON-RPC object`);
409
486
  return;
410
487
  }
411
488
  if (message.id === 1 && message.result) {
@@ -425,16 +502,34 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
425
502
  })}\n`);
426
503
  }
427
504
  if (message.id === 2) {
505
+ if (!Array.isArray(message.result?.data))
506
+ diagnostics.push('Codex model/list /result/data: expected array');
507
+ if (message.result?.nextCursor)
508
+ diagnostics.push('Codex model/list /result/nextCursor: truncated catalog (more pages remain)');
428
509
  const models = Array.isArray(message.result?.data)
429
- ? message.result.data.map((model) => ({
430
- id: String(model.model ?? model.id),
431
- displayName: typeof model.displayName === 'string' ? model.displayName : undefined,
432
- hidden: Boolean(model.hidden),
433
- isDefault: Boolean(model.isDefault),
434
- reasoningEfforts: Array.isArray(model.supportedReasoningEfforts)
435
- ? model.supportedReasoningEfforts.map((effort) => typeof effort === 'string' ? effort : String(effort.reasoningEffort ?? effort.effort ?? effort))
436
- : undefined,
437
- }))
510
+ ? message.result.data.flatMap((model, index) => {
511
+ const id = model?.model ?? model?.id;
512
+ const efforts = model?.supportedReasoningEfforts;
513
+ if (!model || typeof model !== 'object' || Array.isArray(model) || typeof id !== 'string' || !id.trim()
514
+ || ['hidden', 'isDefault'].some(key => model[key] !== undefined && typeof model[key] !== 'boolean')
515
+ || (model.displayName !== undefined && typeof model.displayName !== 'string')
516
+ || (efforts !== undefined && (!Array.isArray(efforts) || efforts.some((effort) => {
517
+ const value = typeof effort === 'string' ? effort : effort?.reasoningEffort ?? effort?.effort;
518
+ return typeof value !== 'string' || !value.trim();
519
+ })))) {
520
+ diagnostics.push(`Codex model/list /result/data/${index}: malformed model row`);
521
+ return [];
522
+ }
523
+ return [{
524
+ id,
525
+ displayName: typeof model.displayName === 'string' ? model.displayName : undefined,
526
+ hidden: Boolean(model.hidden),
527
+ isDefault: Boolean(model.isDefault),
528
+ reasoningEfforts: Array.isArray(model.supportedReasoningEfforts)
529
+ ? model.supportedReasoningEfforts.map((effort) => typeof effort === 'string' ? effort : String(effort.reasoningEffort ?? effort.effort ?? effort))
530
+ : undefined,
531
+ }];
532
+ })
438
533
  : [];
439
534
  finish({
440
535
  provider: 'codex',
@@ -443,7 +538,8 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
443
538
  ...(detectedRuntimeVersion ? { runtimeVersion: detectedRuntimeVersion } : {}),
444
539
  accountScope: 'local-account',
445
540
  models,
446
- ...(message.error ? { error: JSON.stringify(message.error) } : {}),
541
+ ...(message.error ? { errorKind: 'command', error: redactModelDiscoveryText(JSON.stringify(message.error)) }
542
+ : diagnostics.length ? { errorKind: 'invalid-output', error: diagnostics.join('; ') } : {}),
447
543
  });
448
544
  }
449
545
  });
@@ -463,7 +559,7 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
463
559
  accountScope: 'local-account',
464
560
  models: [],
465
561
  errorKind: 'timeout',
466
- error: `Codex app-server model/list timed out${stderr ? `: ${stderr.trim()}` : ''}`,
562
+ error: `Codex app-server model/list timed out${stderr ? `: ${redactModelDiscoveryText(stderr.trim())}` : ''}`,
467
563
  }), timeoutMs);
468
564
  });
469
565
  }
@@ -472,7 +568,7 @@ export async function resolveDynamicModelCatalog(options) {
472
568
  const homeDir = options.homeDir ?? homedir();
473
569
  const cacheFile = options.cacheFile ?? join(homeDir, '.cache/aiwg/model-catalog.v1.json');
474
570
  const staticFile = join(options.aiwgRoot, 'agentic/code/providers/model-catalog.v1.json');
475
- const signature = inventorySignature(options.inventory) + JSON.stringify({ omp: options.omp, profile: process.env.OMP_PROFILE ?? process.env.PI_PROFILE, config: process.env.PI_CONFIG_DIR, data: process.env.XDG_DATA_HOME, cache: process.env.XDG_CACHE_HOME, agent: process.env.PI_CODING_AGENT_DIR });
571
+ const signature = inventorySignature(options.inventory) + JSON.stringify({ codex: options.codex, muse: options.muse, omp: options.omp, profile: process.env.OMP_PROFILE ?? process.env.PI_PROFILE, config: process.env.PI_CONFIG_DIR, data: process.env.XDG_DATA_HOME, cache: process.env.XDG_CACHE_HOME, agent: process.env.PI_CODING_AGENT_DIR });
476
572
  const staticCatalog = await readCatalog(staticFile);
477
573
  if (!staticCatalog)
478
574
  throw new Error(`Invalid or missing static model catalog: ${staticFile}`);
@@ -480,6 +576,8 @@ export async function resolveDynamicModelCatalog(options) {
480
576
  const cached = useCache ? await readCatalog(cacheFile) : null;
481
577
  if (!options.forceRefresh
482
578
  && cached
579
+ && (!options.codex?.accountBinding || cached.discovery?.providers.codex?.accountBinding === options.codex.accountBinding)
580
+ && (!options.muse?.accountBinding || cached.discovery?.providers.muse?.accountBinding === options.muse.accountBinding)
483
581
  && isFresh(cached, now, options.ttlMs ?? DEFAULT_TTL_MS, signature)) {
484
582
  return {
485
583
  ...cached,
@@ -502,6 +600,7 @@ export async function resolveDynamicModelCatalog(options) {
502
600
  fetchedAt: now.toISOString(),
503
601
  inventorySignature: signature,
504
602
  providers: {},
603
+ ...(staticCatalog.discovery?.instances ? { instances: staticCatalog.discovery.instances } : {}),
505
604
  },
506
605
  };
507
606
  }
@@ -518,7 +617,7 @@ export async function resolveDynamicModelCatalog(options) {
518
617
  if (!response.ok)
519
618
  throw new Error(`HTTP ${response.status}`);
520
619
  const remote = await response.json();
521
- if (!remote.version || !remote.providers)
620
+ if (!remote.version || !remote.providers || validateWorkerCatalogDiscovery(remote.discovery).length)
522
621
  throw new Error('invalid catalog shape');
523
622
  catalog = remote;
524
623
  source = 'remote';
@@ -530,7 +629,12 @@ export async function resolveDynamicModelCatalog(options) {
530
629
  }
531
630
  const available = new Set(options.inventory.providers.filter(provider => provider.available).map(provider => provider.id));
532
631
  const nativeDiscoverers = options.nativeDiscoverers ?? {
533
- codex: () => discoverCodexModels(),
632
+ codex: () => discoverCodexDebugModels(options.codex?.command ?? 'codex', options.runner, {
633
+ accountBinding: options.codex?.accountBinding, now: options.now,
634
+ }),
635
+ muse: () => discoverMuseCatalogModels({
636
+ ...options.muse, catalogDir: options.muse?.catalogDir ?? join(homeDir, MUSE_CATALOG_DIR), now: options.now,
637
+ }),
534
638
  opencode: () => discoverOpenCodeModels(),
535
639
  openclaw: () => discoverOpenClawModels(),
536
640
  pi: () => discoverPiModels(),
@@ -546,21 +650,6 @@ export async function resolveDynamicModelCatalog(options) {
546
650
  if (discoverer) {
547
651
  try {
548
652
  providerDiscovery[provider] = await discoverer();
549
- const selected = provider === 'omp' ? {} : selectRoleModels(providerDiscovery[provider].models);
550
- const providerCatalog = catalog.providers[provider];
551
- if (providerCatalog) {
552
- for (const role of ['reasoning', 'coding', 'efficiency']) {
553
- if (!selected[role])
554
- continue;
555
- providerCatalog.roles[role] = {
556
- ...(providerCatalog.roles[role] ?? {}),
557
- id: selected[role].id,
558
- status: 'active',
559
- observed: true,
560
- };
561
- }
562
- providerCatalog.verifiedAt = providerDiscovery[provider].observedAt.slice(0, 10);
563
- }
564
653
  }
565
654
  catch (error) {
566
655
  providerDiscovery[provider] = {
@@ -571,8 +660,67 @@ export async function resolveDynamicModelCatalog(options) {
571
660
  models: [],
572
661
  errorKind: classifyDiscoveryError(error.message),
573
662
  error: error.message,
663
+ accountBinding: provider === 'codex' ? options.codex?.accountBinding : provider === 'muse' ? options.muse?.accountBinding : undefined,
574
664
  };
575
665
  }
666
+ const next = providerDiscovery[provider];
667
+ if (next.error)
668
+ next.error = stripVTControlCharacters(next.error);
669
+ const instanceId = discoveryInstanceId(next);
670
+ const prior = cached?.discovery?.instances?.[instanceId]
671
+ ?? cached?.discovery?.providers[provider];
672
+ if (prior)
673
+ providerDiscovery[provider] = mergeProviderDiscovery(prior, next);
674
+ const entry = providerDiscovery[provider];
675
+ if (entry.errorKind && prior && prior.accountBinding === next.accountBinding
676
+ && cached?.providers[provider])
677
+ catalog.providers[provider] = structuredClone(cached.providers[provider]);
678
+ const providerCatalog = catalog.providers[provider];
679
+ const preservePins = provider === 'omp' || entry.models.some(model => model.worker)
680
+ || prior?.models.some(model => model.worker);
681
+ if (!next.errorKind && prior && prior.accountBinding === next.accountBinding
682
+ && prior.accountScope === next.accountScope) {
683
+ for (const role of Object.values(providerCatalog?.roles ?? {})) {
684
+ const alreadyUnavailable = Object.values(cached?.providers[provider]?.roles ?? {}).some(previous => previous.id === role.id && previous.status === 'unverified' && previous.observed === false);
685
+ if (!(alreadyUnavailable || (preservePins && prior.models.some(model => model.id === role.id)))
686
+ || entry.models.some(model => model.id === role.id))
687
+ continue;
688
+ const guard = retirementGuard(role.id);
689
+ role.status = 'unverified';
690
+ role.observed = false;
691
+ entry.policy ??= { layers: [], selected: {}, constraints: [], diagnostics: [] };
692
+ if (!entry.policy.diagnostics.includes(guard.reason))
693
+ entry.policy.diagnostics.push(guard.reason);
694
+ }
695
+ }
696
+ for (const model of entry.models) {
697
+ if (!model.worker)
698
+ continue;
699
+ const pinnedRoles = Object.values(providerCatalog?.roles ?? {}).filter(role => role.id === model.id);
700
+ const guard = retirementGuard(pinnedRoles.length ? model.id : null, model);
701
+ if (guard.blocked) {
702
+ for (const role of pinnedRoles)
703
+ role.status = 'unverified';
704
+ entry.policy ??= { layers: [], selected: {}, constraints: [], diagnostics: [] };
705
+ if (!entry.policy.diagnostics.includes(guard.reason))
706
+ entry.policy.diagnostics.push(guard.reason);
707
+ }
708
+ }
709
+ if (providerCatalog) {
710
+ // Observations are facts, not approval to adopt a model. Preserve pins.
711
+ const selected = preservePins || entry.errorKind ? {}
712
+ : selectRoleModels(entry.models);
713
+ for (const role of ['reasoning', 'coding', 'efficiency']) {
714
+ if (!selected[role])
715
+ continue;
716
+ providerCatalog.roles[role] = {
717
+ ...(providerCatalog.roles[role] ?? {}), id: selected[role].id,
718
+ status: 'active', observed: true,
719
+ };
720
+ }
721
+ if (Object.keys(selected).length)
722
+ providerCatalog.verifiedAt = entry.observedAt.slice(0, 10);
723
+ }
576
724
  }
577
725
  else {
578
726
  const decision = PROVIDER_DISCOVERY_DECISIONS[provider];
@@ -588,7 +736,7 @@ export async function resolveDynamicModelCatalog(options) {
588
736
  };
589
737
  }
590
738
  }
591
- const resolved = {
739
+ let resolved = {
592
740
  ...catalog,
593
741
  discovery: {
594
742
  source,
@@ -597,8 +745,12 @@ export async function resolveDynamicModelCatalog(options) {
597
745
  ...(remoteError ? { remoteError } : {}),
598
746
  inventorySignature: signature,
599
747
  providers: providerDiscovery,
748
+ instances: { ...cached?.discovery?.instances, ...catalog.discovery?.instances },
600
749
  },
601
750
  };
751
+ for (const entry of Object.values(providerDiscovery)) {
752
+ resolved = recordAccountInstance(resolved, discoveryInstanceId(entry), entry);
753
+ }
602
754
  await atomicWrite(cacheFile, `${JSON.stringify(resolved, null, 2)}\n`);
603
755
  return resolved;
604
756
  }
@@ -616,10 +768,19 @@ export async function discoverOmpModels(command = 'omp', runner = runModelDiscov
616
768
  if (result.exitCode !== 0)
617
769
  return { ...base, errorKind: result.exitCode === 124 ? 'timeout' : /unknown|unsupported|not found/i.test(result.stderr) ? 'unsupported' : classifyDiscoveryError(result.stderr), error: 'OMP model discovery failed; check CLI version, selected profile/config, and credential availability.' };
618
770
  const payload = JSON.parse(result.stdout);
619
- if (!Array.isArray(payload.models) || payload.models.some((m) => !m || typeof m.id !== 'string' || !m.id || typeof m.provider !== 'string' || !m.provider))
620
- throw new Error('shape');
621
- base.models = payload.models.map((m) => ({ id: `${m.provider}/${m.id}`, llmProvider: m.provider, displayName: m.name, reasoningEfforts: Array.isArray(m.thinking) ? m.thinking : undefined }));
622
- return base;
771
+ if (!Array.isArray(payload?.models))
772
+ return { ...base, errorKind: 'invalid-output', error: 'OMP /models: expected array' };
773
+ const diagnostics = [];
774
+ base.models = payload.models.flatMap((m, index) => {
775
+ if (!m || typeof m.id !== 'string' || !m.id.trim() || typeof m.provider !== 'string' || !m.provider.trim()
776
+ || (m.name !== undefined && typeof m.name !== 'string')
777
+ || (m.thinking !== undefined && (!Array.isArray(m.thinking) || m.thinking.some((effort) => typeof effort !== 'string')))) {
778
+ diagnostics.push(`OMP /models/${index}: malformed model row`);
779
+ return [];
780
+ }
781
+ return [{ id: `${m.provider}/${m.id}`, llmProvider: m.provider, displayName: m.name, reasoningEfforts: m.thinking }];
782
+ });
783
+ return { ...base, ...(diagnostics.length ? { errorKind: 'invalid-output', error: diagnostics.join('; ') } : {}) };
623
784
  }
624
785
  catch {
625
786
  return { ...base, errorKind: 'invalid-output', error: 'OMP returned invalid model catalog JSON or discovery could not run.' };
@@ -634,4 +795,707 @@ export function resolveOmpRoleModel(catalog, role, mappings, explicit) {
634
795
  throw new Error(`OMP ${role} model unavailable; configure a model from omp models --json or supply an explicit model.`);
635
796
  return id;
636
797
  }
798
+ /**
799
+ * B-OG-03 shared discovery refinements (S1 direct/shared checkpoint).
800
+ *
801
+ * Codex `debug models` JSON and Muse model-catalog rows normalize through one
802
+ * observation contract: per-CLI sources, field provenance, coherent snapshots
803
+ * and freshness. Native and bridged MCP use identical facts. Synthetic fixture
804
+ * copies under test fixtures exercise these paths; synthetic seeds never ship
805
+ * as defaults.
806
+ */
807
+ /** Evidence dated beyond this skew past now is rejected, never trusted. */
808
+ export const MAX_DISCOVERY_CLOCK_SKEW_MS = 5 * 60 * 1000;
809
+ const MUSE_CATALOG_DIR = '.local/share/muse/model-catalog';
810
+ const MAX_MUSE_CATALOG_FILES = 64;
811
+ const MAX_MUSE_CATALOG_BYTES = 1024 * 1024;
812
+ /** Sanitized provenance digest for a normalized evidence entry. */
813
+ export function discoveryDigest(value) {
814
+ return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
815
+ }
816
+ /** True when an evidence timestamp is materially in the future. Unknown/unparseable stays explicit, not future. */
817
+ export function isFutureDated(value, now = new Date(), skewMs = MAX_DISCOVERY_CLOCK_SKEW_MS) {
818
+ if (typeof value !== 'string')
819
+ return false;
820
+ const time = Date.parse(value);
821
+ return Number.isFinite(time) && time - now.getTime() > skewMs;
822
+ }
823
+ function normalizeVisibility(value) {
824
+ if (value === true || value === 'listed' || value === 'list' || value === 'visible' || value === 'shown')
825
+ return 'listed';
826
+ if (value === false || value === 'hidden')
827
+ return 'hidden';
828
+ return 'unknown';
829
+ }
830
+ function normalizeLifecycle(entry) {
831
+ const lifecycle = entry.lifecycle ?? entry.status ?? entry.state;
832
+ if (lifecycle === 'retired' || entry.retired === true) {
833
+ return {
834
+ lifecycle: 'retired',
835
+ sunsetAt: typeof entry.sunset_at === 'string' || entry.sunset_at === null ? entry.sunset_at : null,
836
+ ...(typeof entry.replacement === 'string' && entry.replacement ? { replacementModelId: entry.replacement } : {}),
837
+ };
838
+ }
839
+ if (lifecycle === 'deprecated' || entry.deprecated === true) {
840
+ return {
841
+ lifecycle: 'deprecated',
842
+ sunsetAt: typeof entry.sunset_at === 'string' || entry.sunset_at === null ? entry.sunset_at : null,
843
+ ...(typeof entry.replacement === 'string' && entry.replacement ? { replacementModelId: entry.replacement } : {}),
844
+ };
845
+ }
846
+ return { lifecycle: 'active', sunsetAt: null };
847
+ }
848
+ function normalizePositiveInt(value) {
849
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 ? value : null;
850
+ }
851
+ function observationTimestamps(options) {
852
+ const observedAt = options.observedAt ?? (options.now ?? new Date()).toISOString();
853
+ return {
854
+ observedAt,
855
+ firstObservedAt: options.firstObservedAt ?? observedAt,
856
+ fetchedAt: options.fetchedAt ?? observedAt,
857
+ };
858
+ }
859
+ /** Build one explicit observation: unknowns stay unknown, every field carries provenance. */
860
+ export function buildWorkerObservation(options, input) {
861
+ const { observedAt, firstObservedAt, fetchedAt } = observationTimestamps(options);
862
+ const releaseDate = input.releaseDate ?? null;
863
+ const efforts = input.efforts ?? [];
864
+ const effortSupport = input.effortSupport
865
+ ?? (efforts.length > 0 ? 'explicit' : 'unknown');
866
+ const lifecycle = input.lifecycle;
867
+ const availability = input.availability
868
+ ?? (lifecycle === 'retired' ? 'unavailable' : 'accessible');
869
+ const provenanceFields = input.dataUseNote
870
+ ? [...input.provenanceFields, '/dataUse:unknown(config-scoped)']
871
+ : input.provenanceFields;
872
+ const observation = {
873
+ version: 1,
874
+ recordId: `${options.cliId}:${input.modelId}:${options.accountBinding ?? options.accountScope}`,
875
+ cliId: options.cliId,
876
+ family: input.family,
877
+ runtimeVersion: options.runtimeVersion,
878
+ accountScope: options.accountBinding ?? options.accountScope,
879
+ endpointFingerprint: `local-cli:${options.cliId}`,
880
+ observedAt,
881
+ verifiedAt: observedAt,
882
+ releaseDate,
883
+ releaseDateKind: releaseDate === null ? 'unknown' : (input.releaseDateKind ?? 'reported'),
884
+ visibility: input.visibility,
885
+ availability,
886
+ lifecycle,
887
+ sunsetAt: input.sunsetAt ?? null,
888
+ ...(input.replacementModelId ? { replacementModelId: input.replacementModelId } : {}),
889
+ contextTokens: input.contextTokens ?? null,
890
+ maxOutputTokens: input.maxOutputTokens ?? null,
891
+ modalities: ['text'],
892
+ efforts: effortSupport === 'explicit' ? efforts : [],
893
+ effortSupport,
894
+ pricing: { costClass: 'unknown', billing: 'unknown', currency: 'USD', inputPerMillion: null, outputPerMillion: null },
895
+ dataUse: { processing: 'unknown', productImprovement: null, retentionDays: null },
896
+ qualification: { status: 'unqualified', roles: [] },
897
+ provenance: [],
898
+ firstObservedAt,
899
+ fetchedAt,
900
+ retiredAt: lifecycle === 'retired' ? observedAt : null,
901
+ };
902
+ const { provenance: _provenance, observedAt: _observedAt, verifiedAt: _verifiedAt, firstObservedAt: _firstObservedAt, fetchedAt: _fetchedAt, retiredAt: _retiredAt, ...facts } = observation;
903
+ observation.evidenceDigest = discoveryDigest(JSON.stringify({
904
+ facts, modelId: input.modelId, source: options.source,
905
+ sourceEvidence: options.contentDigest ?? observationTimestamps(options),
906
+ }));
907
+ observation.provenance = [{
908
+ fields: [...new Set([...observationPointers(observation).filter(field => field !== '/provenance'),
909
+ '/id', '/hidden', '/isDefault', '/reasoningEfforts', ...provenanceFields])],
910
+ source: `${options.source}${options.pointer ? `#${options.pointer}` : ''}`,
911
+ kind: 'native', observedAt,
912
+ digest: discoveryDigest(JSON.stringify(observation)), confidence: 'reported',
913
+ }];
914
+ return observation;
915
+ }
916
+ /** Include container and leaf JSON Pointers so explicit unknown/default leaves stay traceable. */
917
+ function observationPointers(value, prefix = '') {
918
+ if (!value || typeof value !== 'object')
919
+ return [];
920
+ return Object.entries(value).flatMap(([key, entry]) => {
921
+ const pointer = `${prefix}/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`;
922
+ return [pointer, ...observationPointers(entry, pointer)];
923
+ });
924
+ }
925
+ function stringList(value) {
926
+ return Array.isArray(value) ? value.flatMap(entry => {
927
+ if (typeof entry === 'string')
928
+ return entry ? [entry] : [];
929
+ if (entry && typeof entry === 'object' && typeof entry.effort === 'string' && entry.effort)
930
+ return [entry.effort];
931
+ return [];
932
+ }) : [];
933
+ }
934
+ /**
935
+ * Normalize `codex debug models` JSON (`.models[]` with slug, visibility,
936
+ * supported_reasoning_levels, default_reasoning_level, context_window).
937
+ * Observed on Codex 0.160.0; hidden CLI defaults are never treated as eligible.
938
+ */
939
+ export function normalizeCodexDebugModels(payload, options) {
940
+ const raw = payload?.models;
941
+ if (!Array.isArray(raw))
942
+ return { models: [], diagnostics: [`${options.source}: expected .models[] in codex debug models JSON`] };
943
+ return normalizeObservationRows(raw, options, 'codex');
944
+ }
945
+ function museEfforts(row) {
946
+ const variants = row.reasoning_effort_variants;
947
+ if (!Array.isArray(variants))
948
+ return [];
949
+ return [...new Set(variants.flatMap(variant => {
950
+ if (typeof variant === 'string')
951
+ return variant ? [variant] : [];
952
+ if (variant && typeof variant === 'object') {
953
+ const tier = variant.tier;
954
+ return typeof tier === 'string' && tier ? [tier] : [];
955
+ }
956
+ return [];
957
+ }))];
958
+ }
959
+ /**
960
+ * Normalize Muse model-catalog rows (`.rows[]` with model_id, visibility,
961
+ * is_default, is_current, reasoning_effort_variants[].tier, context_limit,
962
+ * output_limit, release_date). Contributor data-use facts stay config-scoped
963
+ * and are never derived from a row's name or default bit.
964
+ */
965
+ export function normalizeMuseCatalogRows(payload, options) {
966
+ const rows = Array.isArray(payload) ? payload
967
+ : Array.isArray(payload?.rows) ? payload.rows : [];
968
+ if (!rows.length)
969
+ return { models: [], diagnostics: [`${options.source}: expected .rows[] in Muse model-catalog JSON`] };
970
+ return normalizeObservationRows(rows, options, 'muse');
971
+ }
972
+ function validDate(value) {
973
+ return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)
974
+ && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
975
+ }
976
+ function validTimestamp(value) {
977
+ return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i.test(value)
978
+ && validDate(value.slice(0, 10)) && Number.isFinite(Date.parse(value));
979
+ }
980
+ /** Use the active window, capped by the reported maximum, with native headroom. */
981
+ function codexContextTokens(row) {
982
+ const window = normalizePositiveInt(row.context_window);
983
+ const maximum = normalizePositiveInt(row.max_context_window);
984
+ const percent = row.effective_context_window_percent;
985
+ // A malformed supplied limit must not silently enlarge the qualified window.
986
+ if (window === null || row.max_context_window != null && maximum === null
987
+ || percent != null && (typeof percent !== 'number' || !Number.isFinite(percent) || percent <= 0 || percent > 100))
988
+ return null;
989
+ return normalizePositiveInt(Math.floor(Math.min(window, maximum ?? window) * (typeof percent === 'number' ? percent : 100) / 100));
990
+ }
991
+ function catalogEfforts(ids, raw) {
992
+ return ids.map(id => {
993
+ const entry = Array.isArray(raw) ? raw.find(value => value && typeof value === 'object'
994
+ && (value.effort ?? value.tier) === id) : undefined;
995
+ // Conductor-verified Codex/Muse catalog rule (#3083): only ultra is
996
+ // "Maximum reasoning with automatic task delegation". Prefer that explicit
997
+ // description when present; other listed levels do not self-delegate.
998
+ // multi_agent_reasoning_effort/version describe child execution, not which
999
+ // selected effort activates delegation (e.g. xhigh is not self-delegating).
1000
+ return { id, selfDelegates: id === 'ultra'
1001
+ || typeof entry?.description === 'string' && /automatic task delegation/i.test(entry.description) };
1002
+ });
1003
+ }
1004
+ function normalizeObservationRows(rows, options, cli) {
1005
+ const diagnostics = [];
1006
+ const models = [];
1007
+ const sourcePointers = new Map();
1008
+ for (const [index, entry] of rows.entries()) {
1009
+ const rowOptions = options.rowOptions?.[index] ?? options;
1010
+ const now = rowOptions.now ?? new Date();
1011
+ const sourceOptions = { ...rowOptions, pointer: rowOptions.pointer ?? `/${cli === 'muse' ? 'rows' : 'models'}/${index}` };
1012
+ const source = `${sourceOptions.source}#${sourceOptions.pointer}`;
1013
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
1014
+ diagnostics.push(`${source}: skipped non-object model entry`);
1015
+ continue;
1016
+ }
1017
+ const row = entry;
1018
+ const id = cli === 'muse' ? row.model_id : row.slug;
1019
+ if (typeof id !== 'string' || !id.trim()) {
1020
+ diagnostics.push(`${source}: skipped entry without a ${cli === 'muse' ? 'model_id' : 'slug'}`);
1021
+ continue;
1022
+ }
1023
+ const times = observationTimestamps(sourceOptions);
1024
+ for (const field of [...(cli === 'muse' ? ['context_limit'] : ['context_window', 'max_context_window']), 'output_limit']) {
1025
+ if (row[field] != null && (typeof row[field] !== 'number' || !Number.isSafeInteger(row[field]) || row[field] < 1)) {
1026
+ diagnostics.push(`${source}: malformed ${field} for ${id}; retained as unknown`);
1027
+ // Do not round or coerce malformed evidence into a valid observation.
1028
+ }
1029
+ }
1030
+ if (cli === 'codex' && row.effective_context_window_percent != null
1031
+ && (typeof row.effective_context_window_percent !== 'number' || !Number.isFinite(row.effective_context_window_percent)
1032
+ || row.effective_context_window_percent <= 0 || row.effective_context_window_percent > 100)) {
1033
+ diagnostics.push(`${source}: malformed effective_context_window_percent for ${id}; context retained as unknown`);
1034
+ }
1035
+ for (const field of ['is_default', 'is_current', 'retired', 'deprecated']) {
1036
+ if (row[field] != null && typeof row[field] !== 'boolean')
1037
+ diagnostics.push(`${source}: malformed ${field} for ${id}`);
1038
+ }
1039
+ const effortField = cli === 'muse' ? 'reasoning_effort_variants' : 'supported_reasoning_levels';
1040
+ const rawEfforts = row[effortField];
1041
+ if (rawEfforts != null) {
1042
+ if (!Array.isArray(rawEfforts))
1043
+ diagnostics.push(`${source}/${effortField}: expected effort array`);
1044
+ else
1045
+ for (const [effortIndex, effort] of rawEfforts.entries()) {
1046
+ const value = typeof effort === 'string' ? effort : effort?.[cli === 'muse' ? 'tier' : 'effort'];
1047
+ if (typeof value !== 'string' || !value.trim())
1048
+ diagnostics.push(`${source}/${effortField}/${effortIndex}: malformed effort`);
1049
+ }
1050
+ }
1051
+ if (row.default_reasoning_level != null && (typeof row.default_reasoning_level !== 'string' || !row.default_reasoning_level.trim())) {
1052
+ diagnostics.push(`${source}: malformed default_reasoning_level for ${id}`);
1053
+ }
1054
+ let invalidTime = false;
1055
+ for (const [field, value] of Object.entries(times)) {
1056
+ if (!validTimestamp(value) || isFutureDated(value, now)) {
1057
+ diagnostics.push(`${source}: ${field} is unknown (invalid or future-dated evidence); rejected ${id}`);
1058
+ invalidTime = true;
1059
+ }
1060
+ }
1061
+ if (invalidTime)
1062
+ continue;
1063
+ let releaseDate = null;
1064
+ if (row.release_date !== null && row.release_date !== undefined) {
1065
+ if (!validDate(row.release_date))
1066
+ diagnostics.push(`${source}: malformed release_date for ${id}; retained as unknown`);
1067
+ else if (isFutureDated(row.release_date, now)) {
1068
+ diagnostics.push(`${source}: rejected future-dated release_date for ${id}; date is unknown`);
1069
+ continue;
1070
+ }
1071
+ else
1072
+ releaseDate = row.release_date;
1073
+ }
1074
+ const visibility = normalizeVisibility(row.visibility);
1075
+ const lifecycle = normalizeLifecycle(row);
1076
+ if (row.sunset_at != null && !validTimestamp(row.sunset_at)) {
1077
+ diagnostics.push(`${source}: malformed sunset_at for ${id}; retained as unknown`);
1078
+ lifecycle.sunsetAt = null;
1079
+ }
1080
+ const efforts = cli === 'muse' ? museEfforts(row) : [...new Set([...stringList(row.supported_reasoning_levels),
1081
+ ...(typeof row.default_reasoning_level === 'string' && row.default_reasoning_level ? [row.default_reasoning_level] : [])])];
1082
+ // Codex's registry provider and catalogAdapter contain no inference-provider
1083
+ // identity. Its providerless native catalog is OpenAI; never infer from a slug.
1084
+ // Muse's real local catalogs use provider_id, not provider.
1085
+ const llmProvider = typeof row.provider === 'string' && row.provider ? row.provider
1086
+ : cli === 'muse' && typeof row.provider_id === 'string' && row.provider_id ? row.provider_id
1087
+ : cli === 'codex' ? 'openai' : undefined;
1088
+ const model = {
1089
+ id, apiModelId: typeof row.api_model_id === 'string' ? row.api_model_id : id,
1090
+ ...(llmProvider ? { llmProvider } : {}),
1091
+ ...(visibility === 'unknown' ? {} : { hidden: visibility === 'hidden' }),
1092
+ ...(cli === 'muse' ? { isDefault: row.is_default === true } : {}),
1093
+ ...(efforts.length ? { reasoningEfforts: efforts } : {}),
1094
+ worker: buildWorkerObservation(sourceOptions, {
1095
+ modelId: id, family: typeof row.family === 'string' && row.family ? row.family : cli,
1096
+ visibility, ...lifecycle, releaseDate,
1097
+ availability: lifecycle.lifecycle === 'retired' ? 'unavailable'
1098
+ : cli === 'muse' ? (row.is_current === true ? 'accessible' : 'unknown') : 'accessible',
1099
+ contextTokens: cli === 'muse' ? normalizePositiveInt(row.context_limit) : codexContextTokens(row),
1100
+ maxOutputTokens: normalizePositiveInt(row.output_limit),
1101
+ efforts: catalogEfforts(efforts, rawEfforts),
1102
+ // The native renderer passes model_reasoning_effort (Muse: --reasoning-effort).
1103
+ effortSupport: efforts.length ? 'explicit' : 'unknown',
1104
+ ...(cli === 'muse' ? { dataUseNote: 'contributor policy is config-scoped' } : {}),
1105
+ provenanceFields: cli === 'muse'
1106
+ ? ['/id:model_id', '/isDefault:is_default', '/availability:is_current', '/efforts:reasoning_effort_variants[].tier',
1107
+ '/efforts/selfDelegates:catalog-description+verified-ultra-rule', '/contextTokens:context_limit', '/maxOutputTokens:output_limit', '/releaseDate:release_date']
1108
+ : ['/id:slug', '/efforts:supported_reasoning_levels+default_reasoning_level',
1109
+ '/efforts/selfDelegates:catalog-description+verified-ultra-rule',
1110
+ '/contextTokens:context_window+max_context_window+effective_context_window_percent'],
1111
+ }),
1112
+ };
1113
+ // Bind compatibility-row values without including unrelated credential-bearing row keys.
1114
+ model.worker.provenance[0].fields.push('/apiModelId');
1115
+ if (model.llmProvider)
1116
+ model.worker.provenance[0].fields.push('/llmProvider', `/llmProvider:${typeof row.provider === 'string' && row.provider ? 'provider' : cli === 'muse' ? 'provider_id' : 'codex-openai-default'}`);
1117
+ const compatibility = { id: model.id, apiModelId: model.apiModelId, llmProvider: model.llmProvider, hidden: model.hidden, isDefault: model.isDefault, reasoningEfforts: model.reasoningEfforts };
1118
+ model.worker.evidenceDigest = discoveryDigest(JSON.stringify({
1119
+ workerDigest: model.worker.evidenceDigest, ...compatibility,
1120
+ }));
1121
+ const { provenance: _provenance, ...values } = model.worker;
1122
+ model.worker.provenance[0].digest = discoveryDigest(JSON.stringify({ worker: values, ...compatibility }));
1123
+ sourcePointers.set(model, {
1124
+ visibility: '/visibility', releaseDate: '/release_date', sunsetAt: '/sunset_at', replacementModelId: '/replacement',
1125
+ lifecycle: row.retired === true ? '/retired' : row.deprecated === true ? '/deprecated'
1126
+ : row.lifecycle !== undefined ? '/lifecycle' : row.status !== undefined ? '/status' : '/state',
1127
+ availability: cli === 'muse' ? '/is_current' : '/visibility',
1128
+ efforts: cli === 'muse' ? '/reasoning_effort_variants' : '/supported_reasoning_levels',
1129
+ effortSupport: cli === 'muse' ? '/reasoning_effort_variants' : '/supported_reasoning_levels',
1130
+ contextTokens: cli === 'muse' ? '/context_limit' : '/context_window', maxOutputTokens: '/output_limit', family: '/family',
1131
+ });
1132
+ models.push(model);
1133
+ }
1134
+ return { models: reconcileDuplicateObservations(models, diagnostics, sourcePointers), diagnostics };
1135
+ }
1136
+ /** Retain every conflicting value and source; never let the first row erase retirement. */
1137
+ function reconcileDuplicateObservations(models, diagnostics, sourcePointers) {
1138
+ const groups = new Map();
1139
+ for (const model of models)
1140
+ groups.set(model.id, [...(groups.get(model.id) ?? []), model]);
1141
+ return [...groups.values()].map(group => {
1142
+ if (group.length === 1)
1143
+ return group[0];
1144
+ const model = { ...group[0], worker: { ...group[0].worker } };
1145
+ const worker = model.worker;
1146
+ const fields = ['family', 'visibility', 'lifecycle', 'availability', 'sunsetAt', 'replacementModelId',
1147
+ 'efforts', 'effortSupport', 'releaseDate', 'contextTokens', 'maxOutputTokens'];
1148
+ const conflicts = [];
1149
+ for (const field of fields) {
1150
+ const evidence = group.map(m => ({ value: m.worker[field] ?? null,
1151
+ source: m.worker.provenance[0].source, pointer: sourcePointers.get(m)?.[field] ?? `/${field}`,
1152
+ observedAt: m.worker.observedAt, digest: m.worker.provenance[0].digest }));
1153
+ if (new Set(evidence.map(e => JSON.stringify(e.value))).size < 2)
1154
+ continue;
1155
+ conflicts.push({ field: `/${field}`, evidence });
1156
+ diagnostics.push(`${model.id}: conflicting ${field === 'releaseDate' ? 'release_date' : field} values; retained values with individual sources: ${evidence.map(item => item.source).join(', ')}`);
1157
+ switch (field) {
1158
+ case 'lifecycle':
1159
+ worker.lifecycle = group.some(m => m.worker.lifecycle === 'retired') ? 'retired' : 'unverified';
1160
+ break;
1161
+ case 'visibility':
1162
+ worker.visibility = group.some(m => m.worker.visibility === 'hidden') ? 'hidden' : 'unknown';
1163
+ break;
1164
+ case 'availability':
1165
+ worker.availability = 'unknown';
1166
+ break;
1167
+ case 'efforts':
1168
+ case 'effortSupport':
1169
+ worker.efforts = [];
1170
+ worker.effortSupport = 'unknown';
1171
+ break;
1172
+ case 'releaseDate':
1173
+ worker.releaseDate = null;
1174
+ worker.releaseDateKind = 'unknown';
1175
+ break;
1176
+ case 'sunsetAt':
1177
+ case 'contextTokens':
1178
+ case 'maxOutputTokens':
1179
+ worker[field] = null;
1180
+ break;
1181
+ case 'replacementModelId':
1182
+ delete worker.replacementModelId;
1183
+ break;
1184
+ case 'family':
1185
+ worker.family = 'unknown';
1186
+ break;
1187
+ }
1188
+ }
1189
+ // Provider/API identity is selection evidence too. Conflicting rows must
1190
+ // not acquire a qualified route merely because one row appeared first.
1191
+ for (const field of ['apiModelId', 'llmProvider']) {
1192
+ if (new Set(group.map(row => row[field])).size < 2)
1193
+ continue;
1194
+ conflicts.push({ field: `/${field}`, evidence: group.map(row => ({ value: row[field] ?? null,
1195
+ source: row.worker.provenance[0].source, pointer: field === 'apiModelId' ? '/api_model_id' : '/provider',
1196
+ observedAt: row.worker.observedAt, digest: row.worker.provenance[0].digest })) });
1197
+ diagnostics.push(`${model.id}: conflicting ${field}`);
1198
+ delete model[field];
1199
+ }
1200
+ // Compatibility defaults also remain evidence rather than silently keeping the first.
1201
+ if (new Set(group.map(m => m.isDefault)).size > 1) {
1202
+ diagnostics.push(`${model.id}: conflicting isDefault values from ${group.map(m => m.worker.provenance[0].source).join(', ')}`);
1203
+ conflicts.push({ field: '/isDefault', evidence: group.map(m => ({ value: m.isDefault ?? null,
1204
+ source: m.worker.provenance[0].source, pointer: '/is_default',
1205
+ observedAt: m.worker.observedAt, digest: m.worker.provenance[0].digest })) });
1206
+ model.isDefault = false;
1207
+ }
1208
+ if (worker.lifecycle === 'retired') {
1209
+ worker.availability = 'unavailable';
1210
+ worker.retiredAt = group.find(m => m.worker.lifecycle === 'retired').worker.retiredAt;
1211
+ }
1212
+ model.hidden = worker.visibility === 'hidden';
1213
+ if (worker.visibility === 'unknown')
1214
+ delete model.hidden;
1215
+ model.reasoningEfforts = worker.efforts.map(e => e.id);
1216
+ worker.provenance = group.flatMap(m => m.worker.provenance);
1217
+ worker.evidenceDigest = discoveryDigest(JSON.stringify(group.map(m => m.worker.evidenceDigest)));
1218
+ if (conflicts.length)
1219
+ worker.conflicts = conflicts;
1220
+ const { provenance: _provenance, ...resolved } = worker;
1221
+ worker.provenance.push({ fields: observationPointers(resolved), source: 'src/models/model-discovery.ts#reconcileDuplicateObservations',
1222
+ kind: 'derived', observedAt: worker.observedAt,
1223
+ digest: discoveryDigest(JSON.stringify(resolved)), confidence: 'derived' });
1224
+ return model;
1225
+ });
1226
+ }
1227
+ /**
1228
+ * Merge a refresh into its prior snapshot. A failed refresh never renews age:
1229
+ * prior models and prior observation time are kept while the new error and
1230
+ * fetch time stay visible. A partial success retains prior-only rows with
1231
+ * their original age; per-model firstObservedAt is the earliest evidence.
1232
+ * A complete success contains exactly the returned identities.
1233
+ */
1234
+ export function mergeProviderDiscovery(prior, next) {
1235
+ if (prior.provider !== next.provider || prior.accountBinding !== next.accountBinding
1236
+ || prior.accountScope !== next.accountScope)
1237
+ return next;
1238
+ if (next.errorKind && next.models.length === 0) {
1239
+ return {
1240
+ ...next,
1241
+ models: prior.models,
1242
+ observedAt: prior.observedAt,
1243
+ ...(prior.accountBinding !== undefined ? { accountBinding: prior.accountBinding } : {}),
1244
+ };
1245
+ }
1246
+ const priorById = new Map(prior.models.map(model => [model.id, model]));
1247
+ const seen = new Set();
1248
+ const merged = next.models.map((model) => {
1249
+ seen.add(model.id);
1250
+ const previous = priorById.get(model.id);
1251
+ if (!previous?.worker || !model.worker)
1252
+ return model;
1253
+ if (next.provider === 'muse' && previous.worker.evidenceDigest
1254
+ && previous.worker.evidenceDigest === model.worker.evidenceDigest)
1255
+ return previous;
1256
+ const firstObservedAt = Date.parse(previous.worker.firstObservedAt) < Date.parse(model.worker.firstObservedAt)
1257
+ ? previous.worker.firstObservedAt
1258
+ : model.worker.firstObservedAt;
1259
+ if (firstObservedAt === model.worker.firstObservedAt)
1260
+ return model;
1261
+ return { ...model, worker: {
1262
+ ...model.worker, firstObservedAt,
1263
+ provenance: [
1264
+ ...model.worker.provenance.map(p => ({ ...p, fields: p.fields.filter(field => field !== '/firstObservedAt') })),
1265
+ { fields: ['/firstObservedAt'], source: previous.worker.provenance[0].source,
1266
+ kind: 'derived', observedAt: previous.worker.observedAt,
1267
+ digest: discoveryDigest(JSON.stringify({ firstObservedAt, evidence: previous.worker.evidenceDigest })), confidence: 'derived' },
1268
+ ],
1269
+ } };
1270
+ });
1271
+ if (next.errorKind) {
1272
+ for (const model of prior.models) {
1273
+ if (!seen.has(model.id))
1274
+ merged.push(model);
1275
+ }
1276
+ }
1277
+ const observedAt = next.errorKind ? prior.observedAt
1278
+ : next.provider === 'muse' && merged.some(model => model.worker)
1279
+ ? merged.flatMap(model => model.worker ? [model.worker.observedAt] : []).sort((a, b) => Date.parse(a) - Date.parse(b))[0]
1280
+ : next.observedAt;
1281
+ return { ...next, observedAt, models: merged };
1282
+ }
1283
+ /** Retirement blocks dispatch without rewriting pins; replacement is a proposal, never substitution. */
1284
+ export function retirementGuard(pinnedId, model) {
1285
+ if (!model && pinnedId)
1286
+ return {
1287
+ blocked: true, pin: pinnedId,
1288
+ reason: `Unavailable model ${pinnedId} is absent from the complete catalog and blocks dispatch; pin is preserved, never rewritten.`,
1289
+ };
1290
+ if (model?.worker?.lifecycle === 'retired') {
1291
+ const replacement = model.worker.supersededBy ?? model.worker.replacementModelId;
1292
+ return {
1293
+ blocked: true,
1294
+ pin: pinnedId,
1295
+ reason: `Retired model ${model.id} blocks dispatch; pin ${pinnedId ?? '(none)'} is preserved, never rewritten`
1296
+ + `${replacement ? ` (suggested replacement ${replacement} requires operator approval)` : ''}.`,
1297
+ };
1298
+ }
1299
+ return { blocked: false, pin: pinnedId };
1300
+ }
1301
+ /**
1302
+ * Record an account-scoped discovery under `discovery.instances` without
1303
+ * merging account entitlements into the provider projection.
1304
+ */
1305
+ function discoveryInstanceId(entry) {
1306
+ return `${entry.provider}:${entry.accountBinding ?? entry.accountScope}`;
1307
+ }
1308
+ export function recordAccountInstance(catalog, instanceId, entry) {
1309
+ const discovery = catalog.discovery ?? {
1310
+ source: 'static',
1311
+ fetchedAt: entry.observedAt,
1312
+ providers: {},
1313
+ };
1314
+ return {
1315
+ ...catalog,
1316
+ discovery: {
1317
+ ...discovery,
1318
+ instances: { ...(discovery.instances ?? {}), [instanceId]: entry },
1319
+ },
1320
+ };
1321
+ }
1322
+ /** Run the version-scoped `codex debug models` strategy and normalize its JSON. */
1323
+ export async function discoverCodexDebugModels(command = 'codex', runner = runModelDiscoveryCommand, options = {}) {
1324
+ const observedAt = (options.now ?? (() => new Date()))().toISOString();
1325
+ const version = options.runtimeVersion ?? await runtimeVersion(command, runner);
1326
+ const base = {
1327
+ provider: 'codex',
1328
+ source: 'native',
1329
+ observedAt,
1330
+ ...(version ? { runtimeVersion: version } : {}),
1331
+ accountScope: 'local-account',
1332
+ ...(options.accountBinding ? { accountBinding: options.accountBinding } : {}),
1333
+ models: [],
1334
+ };
1335
+ const result = await runner(command, ['debug', 'models'], { cwd: tmpdir(), timeoutMs: 10_000 });
1336
+ if (result.exitCode !== 0) {
1337
+ const error = redactModelDiscoveryText(result.stderr.trim()) || `codex debug models exited ${result.exitCode}`;
1338
+ return { ...base, errorKind: classifyDiscoveryError(error), error };
1339
+ }
1340
+ let payload;
1341
+ try {
1342
+ payload = JSON.parse(result.stdout);
1343
+ }
1344
+ catch (error) {
1345
+ return { ...base, errorKind: 'invalid-output', error: `Invalid codex debug models JSON: ${error.message}` };
1346
+ }
1347
+ const normalized = normalizeCodexDebugModels(payload, {
1348
+ cliId: 'codex',
1349
+ source: `${command} debug models`,
1350
+ runtimeVersion: version ?? 'unknown',
1351
+ accountScope: 'local-account',
1352
+ ...(options.accountBinding ? { accountBinding: options.accountBinding } : {}),
1353
+ observedAt,
1354
+ now: (options.now ?? (() => new Date()))(),
1355
+ });
1356
+ return {
1357
+ ...base,
1358
+ models: normalized.models,
1359
+ ...(normalized.diagnostics.length > 0 || normalized.models.length === 0
1360
+ ? {
1361
+ errorKind: 'invalid-output',
1362
+ error: normalized.diagnostics.join('; ')
1363
+ || 'codex debug models returned no normalized model rows.',
1364
+ }
1365
+ : {}),
1366
+ policy: { layers: [], selected: {}, constraints: [], diagnostics: normalized.diagnostics },
1367
+ };
1368
+ }
1369
+ /** Read bounded Muse model-catalog rows. Reads only declared catalog keys, never credential files. */
1370
+ export async function discoverMuseCatalogModels(options = {}) {
1371
+ const now = (options.now ?? (() => new Date()))();
1372
+ const catalogDir = options.catalogDir ?? join(homedir(), MUSE_CATALOG_DIR);
1373
+ const base = {
1374
+ provider: 'muse', source: 'native', observedAt: now.toISOString(),
1375
+ ...(options.runtimeVersion ? { runtimeVersion: options.runtimeVersion } : {}),
1376
+ accountScope: 'local-account', ...(options.accountBinding ? { accountBinding: options.accountBinding } : {}), models: [],
1377
+ };
1378
+ const diagnostics = [];
1379
+ const rows = [];
1380
+ const rowOptions = [];
1381
+ const deadline = Date.now() + (options.timeoutMs ?? 5_000);
1382
+ const bounded = async (operation) => {
1383
+ let timer;
1384
+ try {
1385
+ return await Promise.race([operation, new Promise((_resolve, reject) => {
1386
+ timer = setTimeout(() => reject(new Error('Muse catalog discovery deadline exceeded')), Math.max(0, deadline - Date.now()));
1387
+ })]);
1388
+ }
1389
+ finally {
1390
+ clearTimeout(timer);
1391
+ }
1392
+ };
1393
+ try {
1394
+ const root = await bounded(realpath(catalogDir));
1395
+ const files = (await bounded(readdir(root))).filter(file => file.endsWith('.json')).sort();
1396
+ if (!files.length)
1397
+ diagnostics.push(`${root}: holds no JSON rows`);
1398
+ if (files.length > MAX_MUSE_CATALOG_FILES)
1399
+ diagnostics.push(`${join(root, files[MAX_MUSE_CATALOG_FILES])}: exceeds bounded file count; this and remaining files skipped`);
1400
+ for (const file of files.slice(0, MAX_MUSE_CATALOG_FILES)) {
1401
+ if (Date.now() >= deadline) {
1402
+ diagnostics.push(`${join(root, file)}: discovery deadline exceeded; this and remaining files skipped`);
1403
+ break;
1404
+ }
1405
+ const path = join(root, file);
1406
+ try {
1407
+ const link = await bounded(lstat(path));
1408
+ let target = path;
1409
+ if (link.isSymbolicLink()) {
1410
+ target = await bounded(realpath(path));
1411
+ const subpath = relative(root, target);
1412
+ if (subpath === '..' || subpath.startsWith('../'))
1413
+ throw new Error('symlink escapes catalog directory; regular file required');
1414
+ }
1415
+ else if (!link.isFile())
1416
+ throw new Error('not a regular file; skipped');
1417
+ const stat = link.isSymbolicLink() ? await bounded(lstat(target)) : link;
1418
+ if (!stat.isFile())
1419
+ throw new Error('not a regular file; skipped');
1420
+ if (stat.size > MAX_MUSE_CATALOG_BYTES)
1421
+ throw new Error('exceeds bounded catalog size; skipped before reading');
1422
+ // O_NONBLOCK prevents a stat/open race to a FIFO from blocking. O_NOFOLLOW
1423
+ // rejects a replacement symlink; fstat rechecks the opened descriptor.
1424
+ const pending = open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1425
+ // If open itself outlives the deadline, close any late descriptor.
1426
+ let expired = false;
1427
+ let lateHandle;
1428
+ void pending.then(handle => {
1429
+ lateHandle = handle;
1430
+ if (expired)
1431
+ void handle.close().catch(() => { });
1432
+ }, () => { });
1433
+ const handle = await bounded(pending).catch(error => {
1434
+ expired = true;
1435
+ if (lateHandle)
1436
+ void lateHandle.close().catch(() => { });
1437
+ throw error;
1438
+ });
1439
+ try {
1440
+ const opened = await bounded(handle.stat());
1441
+ if (!opened.isFile())
1442
+ throw new Error('not a regular file; skipped');
1443
+ if (opened.size > MAX_MUSE_CATALOG_BYTES)
1444
+ throw new Error('exceeds bounded catalog size; skipped before reading');
1445
+ const buffer = Buffer.alloc(MAX_MUSE_CATALOG_BYTES + 1);
1446
+ let bytes = 0;
1447
+ while (bytes < buffer.length) {
1448
+ const result = await bounded(handle.read(buffer, bytes, buffer.length - bytes, bytes));
1449
+ if (!result.bytesRead)
1450
+ break;
1451
+ bytes += result.bytesRead;
1452
+ }
1453
+ if (bytes > MAX_MUSE_CATALOG_BYTES)
1454
+ throw new Error('exceeds bounded catalog size; skipped');
1455
+ const payload = JSON.parse(buffer.subarray(0, bytes).toString('utf8'));
1456
+ const fileRows = Array.isArray(payload) ? payload : payload?.rows;
1457
+ if (!Array.isArray(fileRows))
1458
+ throw new Error('expected .rows[]; rejected');
1459
+ const metadata = Array.isArray(payload) ? {} : payload;
1460
+ // Re-reading a cache does not create new evidence. Prefer its provider
1461
+ // timestamp, falling back to the file's own modification time.
1462
+ const evidenceTime = metadata.fetchedAt ?? metadata.fetched_at ?? metadata.refreshedAt ?? metadata.refreshed_at ?? opened.mtime.toISOString();
1463
+ const normalizedOptions = {
1464
+ cliId: 'muse', source: path, contentDigest: discoveryDigest(buffer.subarray(0, bytes).toString('utf8')), runtimeVersion: options.runtimeVersion ?? 'unknown',
1465
+ accountScope: 'local-account', accountBinding: options.accountBinding, now,
1466
+ observedAt: metadata.observedAt ?? evidenceTime,
1467
+ fetchedAt: evidenceTime,
1468
+ firstObservedAt: metadata.firstObservedAt ?? evidenceTime,
1469
+ };
1470
+ for (const [index, row] of fileRows.entries()) {
1471
+ rows.push(row);
1472
+ rowOptions.push({ ...normalizedOptions, pointer: `/${Array.isArray(payload) ? '' : 'rows/'}${index}` });
1473
+ }
1474
+ }
1475
+ finally {
1476
+ await bounded(handle.close());
1477
+ }
1478
+ }
1479
+ catch (error) {
1480
+ diagnostics.push(`${path}: ${error.message}; rejected`);
1481
+ }
1482
+ }
1483
+ }
1484
+ catch (error) {
1485
+ diagnostics.push(`Muse model catalog is unavailable at ${catalogDir}: ${error.message}`);
1486
+ }
1487
+ const normalized = normalizeMuseCatalogRows(rows, {
1488
+ cliId: 'muse', source: catalogDir, runtimeVersion: options.runtimeVersion ?? 'unknown',
1489
+ accountScope: 'local-account', now, rowOptions,
1490
+ });
1491
+ diagnostics.push(...normalized.diagnostics);
1492
+ const observedAt = normalized.models.length
1493
+ ? normalized.models.map(model => model.worker.observedAt).sort((a, b) => Date.parse(a) - Date.parse(b))[0] : base.observedAt;
1494
+ return {
1495
+ ...base, observedAt, models: normalized.models,
1496
+ ...(diagnostics.length || normalized.models.length === 0
1497
+ ? { errorKind: 'invalid-output', error: diagnostics.join('; ') || 'Muse model catalog returned no usable rows.' } : {}),
1498
+ policy: { layers: [], selected: {}, constraints: [], diagnostics },
1499
+ };
1500
+ }
637
1501
  //# sourceMappingURL=model-discovery.js.map