@geraldmaron/construct 1.2.3 → 1.3.1

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 (505) hide show
  1. package/README.md +7 -10
  2. package/bin/construct +235 -209
  3. package/bin/construct-postinstall.mjs +0 -21
  4. package/commands/work/optimize-prompts.md +1 -1
  5. package/examples/distribution/sources/adr.md +2 -2
  6. package/examples/seed-observations/decisions.md +1 -1
  7. package/lib/artifact-loop-core.mjs +412 -0
  8. package/lib/artifact-manifest.mjs +6 -0
  9. package/lib/artifact-release-gate.mjs +115 -49
  10. package/lib/audit-rules.mjs +2 -4
  11. package/lib/audit-skills.mjs +32 -20
  12. package/lib/audit-specialists.mjs +44 -26
  13. package/lib/auto-docs.mjs +6 -5
  14. package/lib/brand-prose.mjs +3 -3
  15. package/lib/brand-tokens.mjs +2 -2
  16. package/lib/certification/artifact-fixtures.mjs +4 -1
  17. package/lib/certification/demo-parity.mjs +6 -32
  18. package/lib/certification/real-llm-scenarios.mjs +58 -13
  19. package/lib/certification/role-cards.mjs +7 -6
  20. package/lib/certification/role-overlays.mjs +4 -4
  21. package/lib/certification/runner.mjs +9 -2
  22. package/lib/certification/skill-inventory.mjs +34 -22
  23. package/lib/certification/skill-scenarios.mjs +21 -8
  24. package/lib/certification/specialist-contracts.mjs +4 -3
  25. package/lib/certification/specialist-scenarios.mjs +7 -6
  26. package/lib/certification/status.mjs +5 -4
  27. package/lib/cli-commands.mjs +55 -54
  28. package/lib/comment-lint.mjs +1 -5
  29. package/lib/completions.mjs +20 -2
  30. package/lib/config/legacy-config-migration.mjs +90 -24
  31. package/lib/config/schema.mjs +5 -5
  32. package/lib/config/source-targets.mjs +80 -0
  33. package/lib/contracts/validate.mjs +119 -51
  34. package/lib/decisions/golden.mjs +3 -2
  35. package/lib/decisions/registry.mjs +11 -6
  36. package/lib/demo-project.mjs +188 -0
  37. package/lib/demo-recording.mjs +9 -34
  38. package/lib/demo-script.mjs +2 -2
  39. package/lib/demo-surface.mjs +13 -131
  40. package/lib/demo-tour-renderer.mjs +92 -0
  41. package/lib/demo.mjs +69 -54
  42. package/lib/diagram-export.mjs +63 -14
  43. package/lib/doctor/cli.mjs +12 -2
  44. package/lib/doctor/source-checkout.mjs +3 -4
  45. package/lib/doctor/watchers/consistency.mjs +148 -43
  46. package/lib/doctor/watchers/credential-parity.mjs +97 -0
  47. package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
  48. package/lib/doctor/watchers/service-health.mjs +4 -32
  49. package/lib/document-export.mjs +73 -9
  50. package/lib/document-extract.mjs +10 -198
  51. package/lib/embed/artifact.mjs +2 -3
  52. package/lib/embed/cli.mjs +1 -1
  53. package/lib/embed/config.mjs +1 -2
  54. package/lib/embed/customer-profiles.mjs +1 -1
  55. package/lib/embed/daemon.mjs +5 -6
  56. package/lib/embed/demand-fetch.mjs +114 -2
  57. package/lib/embed/docs-lifecycle.mjs +9 -5
  58. package/lib/embed/intake-metrics.mjs +2 -2
  59. package/lib/embed/notifications.mjs +2 -3
  60. package/lib/embed/recommendation-store.mjs +25 -22
  61. package/lib/embed/role-framing.mjs +4 -9
  62. package/lib/embedded-contract/capability.mjs +4 -2
  63. package/lib/embedded-contract/contract-version.mjs +1 -1
  64. package/lib/env-config.mjs +2 -2
  65. package/lib/flavors/loader.mjs +21 -19
  66. package/lib/graph/build-from-registry.mjs +31 -7
  67. package/lib/graph/staleness.mjs +1 -1
  68. package/lib/headhunt.mjs +82 -35
  69. package/lib/hooks/agent-tracker.mjs +2 -2
  70. package/lib/hooks/config-protection.mjs +1 -1
  71. package/lib/hooks/registry-sync.mjs +3 -3
  72. package/lib/host-capabilities.mjs +5 -5
  73. package/lib/improvement/controller.mjs +2 -2
  74. package/lib/init-unified.mjs +19 -21
  75. package/lib/intake/classify.mjs +26 -0
  76. package/lib/intake/daemon.mjs +2 -3
  77. package/lib/intake/prepare.mjs +10 -3
  78. package/lib/intake/session-prelude.mjs +1 -1
  79. package/lib/intake/tables/creative.mjs +9 -9
  80. package/lib/intake/tables/operations.mjs +4 -4
  81. package/lib/intake/tables/research.mjs +2 -2
  82. package/lib/integrations/intake-integrations.mjs +9 -10
  83. package/lib/knowledge/research-store.mjs +2 -2
  84. package/lib/mcp/server.mjs +104 -61
  85. package/lib/mcp/tool-budget.mjs +1 -46
  86. package/lib/mcp/tool-recovery.mjs +44 -0
  87. package/lib/mcp/tools/artifact-author.mjs +36 -0
  88. package/lib/mcp/tools/find-tool.mjs +86 -0
  89. package/lib/mcp/tools/orchestration-run.mjs +107 -69
  90. package/lib/mcp/tools/project.mjs +4 -3
  91. package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
  92. package/lib/mcp/tools/skills.mjs +39 -22
  93. package/lib/mcp/tools/telemetry.mjs +2 -1
  94. package/lib/mcp/tools/workflow.mjs +1 -1
  95. package/lib/mcp-platform-config.mjs +7 -5
  96. package/lib/migrations/index.mjs +4 -2
  97. package/lib/migrations/v2-unified-registry.mjs +35 -0
  98. package/lib/model-router.mjs +203 -36
  99. package/lib/models/catalog.mjs +25 -9
  100. package/lib/models/execution-capability-profile.mjs +3 -3
  101. package/lib/models/execution-policy.mjs +7 -6
  102. package/lib/models/provider-poll.mjs +18 -4
  103. package/lib/opencode-config.mjs +56 -1
  104. package/lib/opencode-runtime-plugin.mjs +13 -10
  105. package/lib/oracle/artifact-gate.mjs +14 -4
  106. package/lib/oracle/cli.mjs +2 -0
  107. package/lib/oracle/dispatch.mjs +18 -1
  108. package/lib/oracle/execute.mjs +39 -7
  109. package/lib/oracle/index.mjs +1 -1
  110. package/lib/oracle/org-graph.mjs +10 -5
  111. package/lib/oracle/policy.mjs +6 -0
  112. package/lib/oracle/read-model.mjs +103 -6
  113. package/lib/oracle/reconcile.mjs +31 -4
  114. package/lib/oracle/remediation-dispatch.mjs +53 -0
  115. package/lib/oracle/routing.mjs +5 -0
  116. package/lib/oracle/synthesize.mjs +80 -3
  117. package/lib/orchestration/routing-tables.mjs +44 -10
  118. package/lib/orchestration/runtime.mjs +2 -0
  119. package/lib/orchestration/worker.mjs +1 -1
  120. package/lib/orchestration-policy.mjs +351 -62
  121. package/lib/overrides/resolver.mjs +1 -12
  122. package/lib/parity.mjs +37 -11
  123. package/lib/policy/engine.mjs +80 -15
  124. package/lib/prompt-composer.js +51 -15
  125. package/lib/prompt-metadata.mjs +3 -2
  126. package/lib/providers/connection-probe.mjs +58 -0
  127. package/lib/providers/copilot-auth.mjs +1 -1
  128. package/lib/providers/credential-bootstrap.mjs +1 -1
  129. package/lib/providers/op-run.mjs +3 -4
  130. package/lib/providers/secret-resolver.mjs +31 -0
  131. package/lib/publish-tooling.mjs +3 -11
  132. package/lib/publish.mjs +12 -29
  133. package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
  134. package/lib/reflect.mjs +2 -2
  135. package/lib/registry/agent-manifest.mjs +190 -0
  136. package/lib/registry/assemble.mjs +133 -0
  137. package/lib/registry/catalog.mjs +171 -0
  138. package/lib/registry/cli.mjs +590 -6
  139. package/lib/registry/consolidation.mjs +5 -4
  140. package/lib/registry/docs-sync.mjs +67 -0
  141. package/lib/registry/loader.mjs +147 -0
  142. package/lib/registry/org-io.mjs +76 -0
  143. package/lib/registry/retired-paths.mjs +71 -0
  144. package/lib/registry/surface-map.mjs +5 -7
  145. package/lib/registry/validator.mjs +438 -0
  146. package/lib/research-execution-policy.mjs +173 -0
  147. package/lib/roles/catalog.mjs +4 -9
  148. package/lib/roles/fence.mjs +107 -1
  149. package/lib/roles/flavor-bindings.mjs +68 -0
  150. package/lib/roles/gateway.mjs +140 -29
  151. package/lib/roles/manifest.mjs +22 -13
  152. package/lib/roles/router.mjs +1 -1
  153. package/lib/scopes/enrich.mjs +67 -0
  154. package/lib/scopes/hooks.mjs +12 -0
  155. package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
  156. package/lib/scopes/loader.mjs +104 -0
  157. package/lib/scopes/rebrand.mjs +32 -0
  158. package/lib/scopes/research-profile.mjs +16 -0
  159. package/lib/scopes/teams.mjs +98 -0
  160. package/lib/service-manager.mjs +1 -117
  161. package/lib/setup-prompts.mjs +1 -1
  162. package/lib/setup.mjs +6 -26
  163. package/lib/skills/composition-graph.mjs +98 -0
  164. package/lib/skills/router.mjs +80 -0
  165. package/lib/specialist-contracts.mjs +17 -18
  166. package/lib/specialists/postconditions.mjs +2 -2
  167. package/lib/specialists/prompt-schema.mjs +6 -5
  168. package/lib/specialists/roster.mjs +7 -12
  169. package/lib/specialists/schema.mjs +9 -6
  170. package/lib/status.mjs +25 -90
  171. package/lib/storage/file-lock.mjs +6 -3
  172. package/lib/telemetry/skill-calls.mjs +1 -1
  173. package/lib/templates/doc-presentation.mjs +63 -0
  174. package/lib/templates/visual-requirements.mjs +35 -2
  175. package/lib/term-format.mjs +107 -2
  176. package/lib/test-env-setup.mjs +21 -0
  177. package/lib/ui/components.mjs +42 -0
  178. package/lib/ui/glyphs.mjs +58 -0
  179. package/lib/ui/links.mjs +115 -0
  180. package/lib/ui/theme.mjs +108 -0
  181. package/lib/uninstall/uninstall.mjs +2 -1
  182. package/lib/validator.mjs +45 -8
  183. package/lib/validators/skill-effectiveness.mjs +174 -0
  184. package/lib/validators/skills.mjs +1 -1
  185. package/package.json +12 -8
  186. package/personas/construct.md +12 -3
  187. package/platforms/claude/CLAUDE.md +1 -1
  188. package/rules/common/beads-hygiene.md +52 -0
  189. package/rules/common/neurodivergent-output.md +4 -7
  190. package/rules/common/research.md +2 -0
  191. package/scripts/sync-specialists.mjs +269 -51
  192. package/skills/ai/prompt-optimizer.md +3 -3
  193. package/skills/brand/output-vibe.md +48 -0
  194. package/skills/docs/adr-workflow.md +3 -0
  195. package/skills/docs/backlog-proposal-workflow.md +3 -0
  196. package/skills/docs/codebase-research-workflow.md +5 -2
  197. package/skills/docs/customer-profile-workflow.md +3 -0
  198. package/skills/docs/document-ingest-workflow.md +3 -0
  199. package/skills/docs/evidence-ingest-workflow.md +4 -1
  200. package/skills/docs/init-project.md +1 -1
  201. package/skills/docs/prd-workflow.md +1 -1
  202. package/skills/docs/prfaq-workflow.md +3 -0
  203. package/skills/docs/product-intelligence-workflow.md +4 -1
  204. package/skills/docs/product-signal-workflow.md +3 -0
  205. package/skills/docs/research-workflow.md +13 -6
  206. package/skills/docs/runbook-workflow.md +3 -0
  207. package/skills/docs/strategy-workflow.md +3 -0
  208. package/skills/docs/user-research-workflow.md +6 -3
  209. package/skills/operating/orchestration-reference.md +1 -1
  210. package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
  211. package/skills/roles/architect.ai-systems.md +1 -1
  212. package/skills/roles/architect.data.md +1 -1
  213. package/skills/roles/architect.enterprise.md +1 -1
  214. package/skills/roles/architect.integration.md +1 -1
  215. package/skills/roles/architect.md +1 -1
  216. package/skills/roles/architect.platform.md +1 -1
  217. package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
  218. package/skills/roles/data-analyst.experiment.md +1 -1
  219. package/skills/roles/data-analyst.md +1 -1
  220. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  221. package/skills/roles/data-analyst.product.md +1 -1
  222. package/skills/roles/data-analyst.telemetry.md +1 -1
  223. package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
  224. package/skills/roles/data-engineer.pipeline.md +2 -2
  225. package/skills/roles/data-engineer.vector-retrieval.md +2 -2
  226. package/skills/roles/data-engineer.warehouse.md +2 -2
  227. package/skills/roles/debugger.md +1 -1
  228. package/skills/roles/designer.accessibility.md +1 -1
  229. package/skills/roles/designer.md +1 -1
  230. package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
  231. package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
  232. package/skills/roles/engineer.md +3 -7
  233. package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
  234. package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
  235. package/skills/roles/{operator.md → operations.md} +2 -5
  236. package/skills/roles/orchestrator.md +1 -1
  237. package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
  238. package/skills/roles/product-manager.ai-product.md +1 -1
  239. package/skills/roles/product-manager.enterprise.md +1 -1
  240. package/skills/roles/product-manager.growth.md +1 -2
  241. package/skills/roles/product-manager.md +1 -2
  242. package/skills/roles/product-manager.platform.md +1 -1
  243. package/skills/roles/product-manager.product.md +1 -1
  244. package/skills/roles/qa.ai-eval.md +1 -1
  245. package/skills/roles/qa.api-contract.md +1 -1
  246. package/skills/roles/qa.data-pipeline.md +1 -1
  247. package/skills/roles/qa.md +1 -1
  248. package/skills/roles/qa.web-ui.md +1 -1
  249. package/skills/roles/{operator.release.md → release-manager.md} +3 -3
  250. package/skills/roles/researcher.md +1 -1
  251. package/skills/roles/reviewer.md +1 -1
  252. package/skills/roles/security.ai.md +1 -1
  253. package/skills/roles/security.appsec.md +1 -1
  254. package/skills/roles/security.cloud.md +1 -1
  255. package/skills/roles/security.legal-compliance.md +1 -1
  256. package/skills/roles/security.md +1 -1
  257. package/skills/roles/security.privacy.md +1 -1
  258. package/skills/roles/security.supply-chain.md +1 -1
  259. package/skills/roles/{operator.sre.md → sre.md} +3 -3
  260. package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
  261. package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
  262. package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
  263. package/skills/routing.json +18 -0
  264. package/skills/routing.md +1 -1
  265. package/specialists/artifact-manifest.json +2 -1
  266. package/specialists/audit-enrichments.json +14 -14
  267. package/specialists/org/contracts/accessibility-to-qa.json +37 -0
  268. package/specialists/org/contracts/any-to-business-strategist.json +57 -0
  269. package/specialists/org/contracts/any-to-debugger.json +38 -0
  270. package/specialists/org/contracts/any-to-designer.json +33 -0
  271. package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
  272. package/specialists/org/contracts/any-to-explorer.json +39 -0
  273. package/specialists/org/contracts/any-to-sre-incident.json +33 -0
  274. package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
  275. package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
  276. package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
  277. package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
  278. package/specialists/org/contracts/architect-to-engineer.json +34 -0
  279. package/specialists/org/contracts/architect-to-evaluator.json +59 -0
  280. package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
  281. package/specialists/org/contracts/architect-to-operations.json +45 -0
  282. package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
  283. package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
  284. package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
  285. package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
  286. package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
  287. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
  288. package/specialists/org/contracts/designer-to-accessibility.json +36 -0
  289. package/specialists/org/contracts/engineer-to-qa.json +34 -0
  290. package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
  291. package/specialists/org/contracts/explorer-to-engineer.json +38 -0
  292. package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
  293. package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
  294. package/specialists/org/contracts/product-manager-to-architect.json +71 -0
  295. package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
  296. package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
  297. package/specialists/org/contracts/qa-to-release-manager.json +42 -0
  298. package/specialists/org/contracts/qa-to-test-automation.json +42 -0
  299. package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
  300. package/specialists/org/contracts/researcher-to-architect.json +40 -0
  301. package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
  302. package/specialists/org/contracts/reviewer-to-security.json +41 -0
  303. package/specialists/org/contracts/sre-to-release-manager.json +36 -0
  304. package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
  305. package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
  306. package/specialists/org/contracts/user-to-construct.json +30 -0
  307. package/specialists/org/groups/engineering-group.json +43 -0
  308. package/specialists/org/groups/governance-group.json +38 -0
  309. package/specialists/org/groups/operations-group.json +41 -0
  310. package/specialists/org/groups/product-group.json +46 -0
  311. package/specialists/org/groups/quality-group.json +42 -0
  312. package/specialists/org/groups/strategy-group.json +41 -0
  313. package/specialists/org/policies/action-approval.json +13 -0
  314. package/specialists/org/policies/agents-routing.json +13 -0
  315. package/specialists/org/policies/architecture.json +20 -0
  316. package/specialists/org/policies/bash-safety.json +13 -0
  317. package/specialists/org/policies/beads-hygiene.json +13 -0
  318. package/specialists/org/policies/bootstrap-state.json +13 -0
  319. package/specialists/org/policies/code-review.json +13 -0
  320. package/specialists/org/policies/coding-style.json +13 -0
  321. package/specialists/org/policies/comments.json +13 -0
  322. package/specialists/org/policies/commit-approval.json +13 -0
  323. package/specialists/org/policies/contract-preconditions.json +13 -0
  324. package/specialists/org/policies/deployment.json +20 -0
  325. package/specialists/org/policies/description.json +14 -0
  326. package/specialists/org/policies/design-approval.json +19 -0
  327. package/specialists/org/policies/doc-ownership.json +13 -0
  328. package/specialists/org/policies/file-path-fence.json +13 -0
  329. package/specialists/org/policies/framing.json +13 -0
  330. package/specialists/org/policies/git-workflow.json +13 -0
  331. package/specialists/org/policies/incident-response.json +15 -0
  332. package/specialists/org/policies/intake-triage.json +15 -0
  333. package/specialists/org/policies/neurodivergent-output.json +13 -0
  334. package/specialists/org/policies/no-fabrication.json +13 -0
  335. package/specialists/org/policies/patterns.json +13 -0
  336. package/specialists/org/policies/quality-gate-approval.json +17 -0
  337. package/specialists/org/policies/release-gates.json +13 -0
  338. package/specialists/org/policies/research-evidence.json +13 -0
  339. package/specialists/org/policies/review-before-change.json +13 -0
  340. package/specialists/org/policies/rollback.json +15 -0
  341. package/specialists/org/policies/scope-change.json +20 -0
  342. package/specialists/org/policies/secret-scan.json +13 -0
  343. package/specialists/org/policies/security-approval.json +17 -0
  344. package/specialists/org/policies/security.json +13 -0
  345. package/specialists/org/policies/session-efficiency.json +13 -0
  346. package/specialists/org/policies/skill-routing.json +13 -0
  347. package/specialists/org/policies/strategic-prioritization.json +17 -0
  348. package/specialists/org/policies/testing.json +13 -0
  349. package/specialists/org/policies/tool-invisibility.json +14 -0
  350. package/specialists/org/scopes/creative.json +54 -0
  351. package/specialists/org/scopes/operations.json +51 -0
  352. package/specialists/org/scopes/research.json +60 -0
  353. package/specialists/org/scopes/rnd.json +81 -0
  354. package/specialists/org/specialists/cx-accessibility.json +69 -0
  355. package/specialists/org/specialists/cx-ai-engineer.json +75 -0
  356. package/specialists/org/specialists/cx-architect.json +89 -0
  357. package/specialists/org/specialists/cx-business-strategist.json +72 -0
  358. package/specialists/org/specialists/cx-data-analyst.json +68 -0
  359. package/specialists/org/specialists/cx-data-engineer.json +71 -0
  360. package/specialists/org/specialists/cx-debugger.json +75 -0
  361. package/specialists/org/specialists/cx-designer.json +85 -0
  362. package/specialists/org/specialists/cx-devil-advocate.json +71 -0
  363. package/specialists/org/specialists/cx-docs-keeper.json +82 -0
  364. package/specialists/org/specialists/cx-engineer.json +106 -0
  365. package/specialists/org/specialists/cx-evaluator.json +69 -0
  366. package/specialists/org/specialists/cx-explorer.json +69 -0
  367. package/specialists/org/specialists/cx-legal-compliance.json +74 -0
  368. package/specialists/org/specialists/cx-operations.json +72 -0
  369. package/specialists/org/specialists/cx-oracle.json +46 -0
  370. package/specialists/org/specialists/cx-orchestrator.json +81 -0
  371. package/specialists/org/specialists/cx-platform-engineer.json +80 -0
  372. package/specialists/org/specialists/cx-product-manager.json +102 -0
  373. package/specialists/org/specialists/cx-qa.json +79 -0
  374. package/specialists/org/specialists/cx-rd-lead.json +73 -0
  375. package/specialists/org/specialists/cx-release-manager.json +77 -0
  376. package/specialists/org/specialists/cx-researcher.json +82 -0
  377. package/specialists/org/specialists/cx-reviewer.json +71 -0
  378. package/specialists/org/specialists/cx-security.json +91 -0
  379. package/specialists/org/specialists/cx-sre.json +94 -0
  380. package/specialists/org/specialists/cx-test-automation.json +69 -0
  381. package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
  382. package/specialists/org/specialists/cx-ux-researcher.json +68 -0
  383. package/specialists/org/teams/accessibility-team.json +39 -0
  384. package/specialists/org/teams/design-team.json +40 -0
  385. package/specialists/org/teams/engineering-team.json +50 -0
  386. package/specialists/org/teams/governance-team.json +41 -0
  387. package/specialists/org/teams/operations-team.json +46 -0
  388. package/specialists/org/teams/product-management-team.json +45 -0
  389. package/specialists/org/teams/quality-team.json +49 -0
  390. package/specialists/org/teams/research-team.json +39 -0
  391. package/specialists/org/teams/strategy-team.json +48 -0
  392. package/specialists/org/teams/ux-research-team.json +39 -0
  393. package/specialists/prompts/_shared/validation-contract.md +1 -1
  394. package/specialists/prompts/_team-template.md +10 -0
  395. package/specialists/prompts/cx-accessibility.md +1 -0
  396. package/specialists/prompts/cx-ai-engineer.md +1 -1
  397. package/specialists/prompts/cx-business-strategist.md +1 -1
  398. package/specialists/prompts/cx-data-engineer.md +1 -1
  399. package/specialists/prompts/cx-designer.md +1 -0
  400. package/specialists/prompts/cx-devil-advocate.md +1 -1
  401. package/specialists/prompts/cx-docs-keeper.md +1 -1
  402. package/specialists/prompts/cx-evaluator.md +1 -1
  403. package/specialists/prompts/cx-explorer.md +1 -1
  404. package/specialists/prompts/cx-operations.md +1 -1
  405. package/specialists/prompts/cx-oracle.md +7 -3
  406. package/specialists/prompts/cx-orchestrator.md +17 -1
  407. package/specialists/prompts/cx-platform-engineer.md +1 -1
  408. package/specialists/prompts/cx-product-manager.md +2 -0
  409. package/specialists/prompts/cx-release-manager.md +1 -1
  410. package/specialists/prompts/cx-researcher.md +7 -4
  411. package/specialists/prompts/cx-sre.md +1 -1
  412. package/specialists/prompts/cx-test-automation.md +2 -2
  413. package/specialists/prompts/cx-trace-reviewer.md +3 -3
  414. package/specialists/prompts/cx-ux-researcher.md +2 -1
  415. package/templates/demos/scripts/architecture-review-adr.json +30 -0
  416. package/templates/demos/scripts/capability-contract.json +25 -0
  417. package/templates/demos/scripts/intake-triage.json +25 -0
  418. package/templates/demos/scripts/profile-doctor-health.json +25 -0
  419. package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
  420. package/templates/demos/tapes/architecture-review-adr.tape +44 -0
  421. package/templates/demos/tapes/capability-contract.tape +39 -0
  422. package/templates/demos/tapes/intake-triage.tape +39 -0
  423. package/templates/demos/tapes/profile-doctor-health.tape +39 -0
  424. package/templates/distribution/construct-brand.typ +23 -18
  425. package/templates/distribution/construct-decision.typ +1 -1
  426. package/templates/distribution/construct-pdf.typ +1 -1
  427. package/templates/distribution/construct-prd.typ +1 -1
  428. package/templates/distribution/construct-research.typ +1 -1
  429. package/templates/distribution/mermaid-puppeteer.json +8 -0
  430. package/templates/docs/persona-artifact.md +1 -1
  431. package/templates/docs/prd.md +6 -0
  432. package/apps/chat/engine/ai-sdk-agent.mjs +0 -183
  433. package/apps/chat/engine/loop-driver.mjs +0 -211
  434. package/apps/chat/engine/models.mjs +0 -122
  435. package/apps/chat/engine/provider-adapters.mjs +0 -171
  436. package/apps/chat/engine/tools/permission.mjs +0 -54
  437. package/apps/chat/engine/tools/primitives.mjs +0 -180
  438. package/apps/chat/engine/tools/registry.mjs +0 -122
  439. package/apps/chat/engine/turn-controls.mjs +0 -70
  440. package/lib/boundary.mjs +0 -127
  441. package/lib/certification/dashboard-api.mjs +0 -71
  442. package/lib/chat/cli.mjs +0 -333
  443. package/lib/chat/command-suggest.mjs +0 -161
  444. package/lib/chat/commands.mjs +0 -215
  445. package/lib/chat/config.mjs +0 -142
  446. package/lib/chat/context-compactor.mjs +0 -250
  447. package/lib/chat/context-continuation.mjs +0 -253
  448. package/lib/chat/continuation-source.mjs +0 -58
  449. package/lib/chat/demo-guide.mjs +0 -61
  450. package/lib/chat/design-tokens.mjs +0 -91
  451. package/lib/chat/desktop-binary.mjs +0 -81
  452. package/lib/chat/desktop-build.mjs +0 -130
  453. package/lib/chat/desktop-launcher.mjs +0 -133
  454. package/lib/chat/evidence.mjs +0 -145
  455. package/lib/chat/export.mjs +0 -74
  456. package/lib/chat/harness/driver.mjs +0 -91
  457. package/lib/chat/list-picker.mjs +0 -112
  458. package/lib/chat/model-picker.mjs +0 -356
  459. package/lib/chat/openrouter-fallback.mjs +0 -151
  460. package/lib/chat/permission-prompt.mjs +0 -33
  461. package/lib/chat/picker-catalog.mjs +0 -45
  462. package/lib/chat/policy-telemetry.mjs +0 -34
  463. package/lib/chat/present.mjs +0 -246
  464. package/lib/chat/session-context.mjs +0 -39
  465. package/lib/chat/session-persist.mjs +0 -73
  466. package/lib/chat/session-restore.mjs +0 -71
  467. package/lib/chat/session-settings.mjs +0 -53
  468. package/lib/chat/system-prompt.mjs +0 -52
  469. package/lib/chat/transparency.mjs +0 -93
  470. package/lib/chat/tui/color-scheme.mjs +0 -42
  471. package/lib/chat/tui/markdown.mjs +0 -123
  472. package/lib/chat/tui/presentation.mjs +0 -100
  473. package/lib/chat/tui/render.mjs +0 -500
  474. package/lib/chat/tui/turn-block.mjs +0 -284
  475. package/lib/chat/tui/turn-present.mjs +0 -18
  476. package/lib/chat/tui/turn-state.mjs +0 -88
  477. package/lib/chat/tui/usage.mjs +0 -122
  478. package/lib/chat/web-commands.mjs +0 -146
  479. package/lib/chat/web-launcher.mjs +0 -63
  480. package/lib/chat/web-picker-keys.mjs +0 -46
  481. package/lib/chat/web-session.mjs +0 -159
  482. package/lib/config/alias.mjs +0 -57
  483. package/lib/dashboard-demo.mjs +0 -71
  484. package/lib/dashboard-static.mjs +0 -175
  485. package/lib/install/desktop-binary-download.mjs +0 -88
  486. package/lib/profiles/loader.mjs +0 -123
  487. package/lib/profiles/rebrand.mjs +0 -46
  488. package/lib/server/auth.mjs +0 -169
  489. package/lib/server/chat-loop.mjs +0 -622
  490. package/lib/server/chat.mjs +0 -336
  491. package/lib/server/cors.mjs +0 -77
  492. package/lib/server/csrf.mjs +0 -103
  493. package/lib/server/demo-preview.mjs +0 -63
  494. package/lib/server/index.mjs +0 -2641
  495. package/lib/server/insights.mjs +0 -780
  496. package/lib/server/langfuse-login.mjs +0 -58
  497. package/lib/server/rate-limit.mjs +0 -93
  498. package/lib/server/telemetry-login.mjs +0 -100
  499. package/lib/server/webhook.mjs +0 -512
  500. package/specialists/contracts.json +0 -1032
  501. package/specialists/contracts.schema.json +0 -83
  502. package/specialists/policy-inventory.json +0 -188
  503. package/specialists/registry.json +0 -1412
  504. package/specialists/role-manifests.json +0 -217
  505. package/specialists/teams.json +0 -94
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * lib/migrations/index.mjs — Schema migration registry and runner.
3
3
  *
4
- * Construct's machine-readable artifacts (specialists/registry.json, contracts.json,
5
- * role-manifests.json, .cx/config.json, profile JSON) carry an integer
4
+ * Construct's machine-readable artifacts (specialists/org/, .cx/config.json,
5
+ * .cx/config.json, profile JSON) carry an integer
6
6
  * `version` field. When the installed binary's expected schema version is
7
7
  * higher than the on-disk version, migrations apply in order. When the on-disk
8
8
  * version is higher, the runner refuses to load and instructs the operator to
@@ -18,9 +18,11 @@
18
18
  */
19
19
 
20
20
  import v1Baseline from './v1-baseline.mjs';
21
+ import v2UnifiedRegistry from './v2-unified-registry.mjs';
21
22
 
22
23
  const REGISTRY = [
23
24
  v1Baseline,
25
+ v2UnifiedRegistry,
24
26
  ];
25
27
 
26
28
  export const CURRENT_SCHEMA_VERSION = Math.max(...REGISTRY.map((m) => m.to));
@@ -0,0 +1,35 @@
1
+ /**
2
+ * lib/migrations/v2-unified-registry.mjs — v1 → v2 schema stamp.
3
+ *
4
+ * v2 is the version that unified-registry.json was born at when the legacy
5
+ * specialists/registry.json + contracts.json + teams.json were consolidated
6
+ * into a single unified file. Artifacts already at v2 need no shape change.
7
+ * For any v1 artifact carrying the old flat-registry shape, this stamps the
8
+ * version field so the migrate runner reports "already at v2" on re-run.
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
12
+
13
+ export default {
14
+ from: 1,
15
+ to: 2,
16
+ description: 'Stamp legacy v1 artifacts with version: 2 (no shape changes required)',
17
+ async apply(artifactPath, { dryRun = false } = {}) {
18
+ if (!existsSync(artifactPath)) {
19
+ return { changed: false, summary: `${artifactPath}: not present (skip)` };
20
+ }
21
+ let data;
22
+ try { data = JSON.parse(readFileSync(artifactPath, 'utf8')); }
23
+ catch (err) { throw new Error(`failed to parse ${artifactPath}: ${err.message}`); }
24
+
25
+ if (data && typeof data === 'object' && data.version === 2) {
26
+ return { changed: false, summary: `${artifactPath}: already at v2` };
27
+ }
28
+
29
+ const next = { ...data, version: 2 };
30
+ if (!dryRun) {
31
+ writeFileSync(artifactPath, JSON.stringify(next, null, 2) + '\n');
32
+ }
33
+ return { changed: true, summary: `${artifactPath}: stamped version: 2` };
34
+ },
35
+ };
@@ -38,10 +38,13 @@ import {
38
38
  } from "./model-free-selector.mjs";
39
39
  import {
40
40
  applyModelVisibilityFilter,
41
+ collectTierDefaultIds,
41
42
  loadModelsCatalogContext,
42
43
  mergeLiveModelsIntoProviders,
44
+ providerFamilyEnabled,
43
45
  } from "./models/catalog.mjs";
44
46
  import { configDir } from "./config/xdg.mjs";
47
+ import { getUserEnvPath } from "./env-config.mjs";
45
48
 
46
49
  function uniqueStrings(values = []) {
47
50
  return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
@@ -448,7 +451,7 @@ export function getProviderModelCatalog({
448
451
  } = {}) {
449
452
  const baseProviders = PROVIDER_FAMILY_TIERS.map((family) => {
450
453
  const tiers = family.resolve({});
451
- const options = {
454
+ const staticOptions = {
452
455
  reasoning: uniqueStrings([...(family.options?.reasoning ?? []), tiers.reasoning]),
453
456
  standard: uniqueStrings([...(family.options?.standard ?? []), tiers.standard]),
454
457
  fast: uniqueStrings([...(family.options?.fast ?? []), tiers.fast]),
@@ -457,7 +460,8 @@ export function getProviderModelCatalog({
457
460
  id: family.id,
458
461
  label: family.label,
459
462
  tiers,
460
- options,
463
+ staticOptions,
464
+ options: staticOptions,
461
465
  local: family.local === true,
462
466
  requiresEnv: Array.isArray(family.requiresEnv) ? family.requiresEnv : [],
463
467
  pricingHint: family.pricingHint ?? null,
@@ -488,6 +492,54 @@ export function getProviderModelCatalog({
488
492
  );
489
493
  }
490
494
 
495
+ /**
496
+ * Tier pins and option lists for configured provider families limit live
497
+ * OpenRouter polls in model-selection surfaces without dumping the full catalog.
498
+ */
499
+ export function getPickerModelAllowlist({
500
+ env = process.env,
501
+ cwd = process.cwd(),
502
+ currentModel = null,
503
+ modelsConfig = null,
504
+ registryModels = null,
505
+ } = {}) {
506
+ const ctx = loadModelsCatalogContext({ cwd, env });
507
+ const vis = modelsConfig?.visibility ?? ctx.modelsConfig.visibility;
508
+ const registry = registryModels ?? ctx.registryModels;
509
+ const tierDefaults = collectTierDefaultIds(registry);
510
+ const allowed = new Set();
511
+
512
+ if (currentModel) allowed.add(currentModel);
513
+
514
+ for (const family of PROVIDER_FAMILY_TIERS) {
515
+ if (!isProviderConfigured(family.id, env)) continue;
516
+ if (!providerFamilyEnabled(family.id, vis)) continue;
517
+
518
+ const tiers = family.resolve({});
519
+ const pins = uniqueStrings(['reasoning', 'standard', 'fast'].map((tier) => tiers[tier]));
520
+ const shipped = uniqueStrings(
521
+ ['reasoning', 'standard', 'fast'].flatMap((tier) => family.options?.[tier] || []),
522
+ );
523
+
524
+ if (vis.mode === 'explicit') {
525
+ for (const id of vis.include || []) allowed.add(id);
526
+ continue;
527
+ }
528
+ if (vis.mode === 'all_configured') {
529
+ for (const id of [...pins, ...shipped]) allowed.add(id);
530
+ continue;
531
+ }
532
+ if (tierDefaults.length > 0) {
533
+ for (const id of [...tierDefaults, ...pins]) allowed.add(id);
534
+ } else {
535
+ for (const id of [...pins, ...shipped]) allowed.add(id);
536
+ }
537
+ }
538
+
539
+ for (const id of vis.exclude || []) allowed.delete(id);
540
+ return allowed;
541
+ }
542
+
491
543
  // Provider families with open-ended model catalogs: credential presence is the
492
544
  // gate; static option lists are hints, not exhaustive allowlists.
493
545
 
@@ -504,16 +556,44 @@ const LENIENT_MODEL_FAMILIES = new Set([
504
556
  'local',
505
557
  ]);
506
558
 
559
+ export const RETIRED_MODEL_SLUGS = new Set([
560
+ 'anthropic/claude-3.5-sonnet',
561
+ 'openrouter/anthropic/claude-3.5-sonnet',
562
+ ]);
563
+
564
+ function shippedModelIdsForFamily(family) {
565
+ if (!family) return [];
566
+ const tiers = family.resolve({});
567
+ return uniqueStrings([
568
+ tiers.reasoning,
569
+ tiers.standard,
570
+ tiers.fast,
571
+ ...(family.options?.reasoning ?? []),
572
+ ...(family.options?.standard ?? []),
573
+ ...(family.options?.fast ?? []),
574
+ ]);
575
+ }
576
+
577
+ function isLenientPollSlug(modelId) {
578
+ return modelId === 'openrouter/free'
579
+ || modelId === 'openrouter/openrouter/free'
580
+ || /:free$/i.test(modelId);
581
+ }
582
+
507
583
  /**
508
584
  * Check whether a model id can be used on this machine right now: the provider
509
585
  * family must match, credentials must be present (including op:// and Copilot
510
586
  * auth-store), and for fixed-catalog families (e.g. github-copilot) the id must
511
587
  * appear in the shipped option list or tier defaults — not a stale CX_MODEL pin.
512
588
  */
513
- export function isChatModelAvailable(modelId, { env = process.env, excludeFamilies = [] } = {}) {
589
+ export function isModelAvailable(modelId, { env = process.env, excludeFamilies = [], strictPin = false } = {}) {
514
590
  if (!modelId || typeof modelId !== 'string') {
515
591
  return { ok: false, reason: 'missing', modelId: modelId || null };
516
592
  }
593
+ if (RETIRED_MODEL_SLUGS.has(modelId)) {
594
+ const family = matchProviderFamily(modelId);
595
+ return { ok: false, reason: 'model_retired', modelId, provider: family?.id || null };
596
+ }
517
597
  const family = matchProviderFamily(modelId);
518
598
  if (!family) {
519
599
  return { ok: false, reason: 'unknown_family', modelId };
@@ -543,7 +623,14 @@ export function isChatModelAvailable(modelId, { env = process.env, excludeFamili
543
623
  return { ok: true, modelId, provider: family.id };
544
624
  }
545
625
  if (LENIENT_MODEL_FAMILIES.has(family.id)) {
546
- return { ok: true, modelId, provider: family.id };
626
+ if (!strictPin || isLenientPollSlug(modelId)) {
627
+ return { ok: true, modelId, provider: family.id };
628
+ }
629
+ const shipped = shippedModelIdsForFamily(family);
630
+ if (shipped.includes(modelId)) {
631
+ return { ok: true, modelId, provider: family.id };
632
+ }
633
+ return { ok: false, reason: 'model_not_available', modelId, provider: family.id };
547
634
  }
548
635
  const { providers } = getProviderModelCatalog({ env });
549
636
  const provider = providers.find((p) => p.id === family.id);
@@ -564,27 +651,105 @@ export function isChatModelAvailable(modelId, { env = process.env, excludeFamili
564
651
  return { ok: false, reason: 'model_not_available', modelId, provider: family.id };
565
652
  }
566
653
 
567
- function availabilityNotice(rejected) {
568
- if (!rejected?.modelId) return null;
569
- const label = rejected.modelId;
654
+ export function listAvailableModels({ env = process.env, cwd = process.cwd(), activeModelId = null } = {}) {
655
+ const { providers } = getProviderModelCatalog({ env, cwd, activeModelId });
656
+ const models = [];
657
+ const seen = new Set();
658
+ for (const provider of providers) {
659
+ for (const tier of ['reasoning', 'standard', 'fast']) {
660
+ for (const id of provider.options?.[tier] || []) {
661
+ if (seen.has(id)) continue;
662
+ seen.add(id);
663
+ models.push({
664
+ id,
665
+ label: id,
666
+ provider: provider.id,
667
+ configured: provider.configured,
668
+ local: provider.local === true,
669
+ suitable: true,
670
+ tier,
671
+ available: isModelAvailable(id, { env }).ok,
672
+ });
673
+ }
674
+ }
675
+ }
676
+ return models.sort((a, b) => Number(b.configured) - Number(a.configured) || a.id.localeCompare(b.id));
677
+ }
678
+
679
+ /**
680
+ * Structured notices when a pinned model cannot run and a fallback is used.
681
+ * Returns multiple lines; `notice` on the resolution object joins them for
682
+ * single-string consumers.
683
+ */
684
+ export function formatModelResolutionNotices({
685
+ rejected,
686
+ activeId = null,
687
+ activeProviderLabel = null,
688
+ configEnvPath = null,
689
+ } = {}) {
690
+ if (!rejected?.modelId) return [];
691
+ const pinned = rejected.modelId;
692
+ const configPath = configEnvPath || getUserEnvPath();
693
+ const lines = [];
694
+
570
695
  if (rejected.reason === 'provider_not_configured') {
571
696
  const family = rejected.provider || '';
697
+ lines.push(`Pinned ${pinned}`);
572
698
  if (family.startsWith('openrouter')) {
573
- return `Saved ${label} OpenRouter not configured. Set OPENROUTER_API_KEY in ~/.config/construct/config.env or run \`construct creds set openrouter\`.`;
699
+ lines.push(`OpenRouter unavailable: OPENROUTER_API_KEY not found in ${configPath}`);
700
+ lines.push('Set a value or op:// ref in config.env, run `node scripts/setup-credentials.mjs`, or launch with `op run --env-file=…`');
701
+ } else {
702
+ lines.push(`Provider unavailable: credentials not configured for ${family || 'requested provider'}`);
703
+ lines.push(`Set keys in ${configPath} or run \`construct creds set <provider>\``);
574
704
  }
575
- return `Saved ${label} — provider not configured.`;
705
+ if (activeId) {
706
+ const suffix = activeProviderLabel ? ` (${activeProviderLabel})` : '';
707
+ lines.push(`Active model: ${activeId}${suffix}`);
708
+ }
709
+ return lines;
576
710
  }
577
- if (rejected.reason === 'model_not_available') {
578
- return `Pinned ${label} not available on your account.`;
711
+
712
+ if (rejected.reason === 'model_not_available' || rejected.reason === 'model_retired') {
713
+ lines.push(`Pinned ${pinned} — not available on your account.`);
714
+ if (activeId) {
715
+ const suffix = activeProviderLabel ? ` (${activeProviderLabel})` : '';
716
+ lines.push(`Active model: ${activeId}${suffix}`);
717
+ }
718
+ return lines;
579
719
  }
720
+
580
721
  if (rejected.reason === 'model_not_pulled') {
581
- const native = rejected.nativeModel || toOllamaNativeModelId(label);
582
- return `Pinned ${label} — not installed locally. Run \`ollama pull ${native}\` or \`construct ollama pull ${native}\`.`;
722
+ const native = rejected.nativeModel || toOllamaNativeModelId(pinned);
723
+ lines.push(`Pinned ${pinned} — not installed locally. Run \`ollama pull ${native}\` or \`construct ollama pull ${native}\`.`);
724
+ if (activeId) {
725
+ const suffix = activeProviderLabel ? ` (${activeProviderLabel})` : '';
726
+ lines.push(`Active model: ${activeId}${suffix}`);
727
+ }
728
+ return lines;
583
729
  }
730
+
584
731
  if (rejected.reason === 'unknown_family') {
585
- return `Pinned ${label} — unrecognized model id.`;
732
+ lines.push(`Pinned ${pinned} — unrecognized model id.`);
733
+ return lines;
734
+ }
735
+
736
+ lines.push(`Pinned ${pinned} — unavailable.`);
737
+ if (activeId) {
738
+ const suffix = activeProviderLabel ? ` (${activeProviderLabel})` : '';
739
+ lines.push(`Active model: ${activeId}${suffix}`);
586
740
  }
587
- return `Pinned ${label} — unavailable.`;
741
+ return lines;
742
+ }
743
+
744
+ function resolutionNotices(rejections, activeId, activeProviderLabel) {
745
+ if (!rejections.length) return { notice: null, notices: [] };
746
+ const notices = formatModelResolutionNotices({
747
+ rejected: rejections[0],
748
+ activeId,
749
+ activeProviderLabel,
750
+ configEnvPath: getUserEnvPath(),
751
+ });
752
+ return { notice: notices.length ? notices.join(' ') : null, notices };
588
753
  }
589
754
 
590
755
  function recommendTierModel(tier, { env = process.env, excludeFamilies = [] } = {}) {
@@ -596,7 +761,7 @@ function recommendTierModel(tier, { env = process.env, excludeFamilies = [] } =
596
761
  ...(provider.options?.[tier] ?? []),
597
762
  ]);
598
763
  for (const id of candidates) {
599
- const check = isChatModelAvailable(id, { env, excludeFamilies });
764
+ const check = isModelAvailable(id, { env, excludeFamilies });
600
765
  if (check.ok) return { id, provider: provider.label, tier };
601
766
  }
602
767
  }
@@ -604,17 +769,17 @@ function recommendTierModel(tier, { env = process.env, excludeFamilies = [] } =
604
769
  }
605
770
 
606
771
  /**
607
- * Resolve the chat model with validation: explicit request and CX_MODEL pins
772
+ * Resolve a model with validation: explicit request and CX_MODEL pins
608
773
  * are preferences, not blind truth. Stale or unconfigured pins fall through to
609
774
  * the next configured provider for the tier, with a human-readable notice.
610
775
  */
611
- export function resolveValidatedChatModel({ env = process.env, requested = null, excludeFamilies = [] } = {}) {
776
+ export function resolveValidatedModel({ env = process.env, requested = null, excludeFamilies = [] } = {}) {
612
777
  const rejections = [];
613
778
 
614
779
  if (requested) {
615
- const check = isChatModelAvailable(requested, { env, excludeFamilies });
780
+ const check = isModelAvailable(requested, { env, excludeFamilies, strictPin: true });
616
781
  if (check.ok) {
617
- return { id: requested, source: 'explicit', notice: null, rejected: [] };
782
+ return { id: requested, source: 'explicit', notice: null, notices: [], rejected: [] };
618
783
  }
619
784
  rejections.push(check);
620
785
  }
@@ -626,34 +791,36 @@ export function resolveValidatedChatModel({ env = process.env, requested = null,
626
791
  ];
627
792
  for (const [tier, pin] of pinOrder) {
628
793
  if (!pin) continue;
629
- const check = isChatModelAvailable(pin, { env, excludeFamilies });
794
+ const check = isModelAvailable(pin, { env, excludeFamilies, strictPin: true });
630
795
  if (check.ok) {
631
- const notice = rejections.length
632
- ? `${availabilityNotice(rejections[0])} Using ${pin}.`
633
- : null;
634
- return { id: pin, source: 'pin', tier, notice, rejected: rejections };
796
+ const { notice, notices } = rejections.length
797
+ ? resolutionNotices(rejections, pin, null)
798
+ : { notice: null, notices: [] };
799
+ return { id: pin, source: 'pin', tier, notice, notices, rejected: rejections };
635
800
  }
636
801
  rejections.push(check);
637
802
  }
638
803
 
639
804
  const standard = recommendTierModel('standard', { env, excludeFamilies });
640
805
  if (standard) {
641
- const notice = rejections.length
642
- ? `${availabilityNotice(rejections[0])} Using ${standard.id} (${standard.provider}).`
643
- : null;
644
- return { id: standard.id, source: 'recommended', tier: 'standard', notice, rejected: rejections };
806
+ const { notice, notices } = rejections.length
807
+ ? resolutionNotices(rejections, standard.id, standard.provider)
808
+ : { notice: null, notices: [] };
809
+ return { id: standard.id, source: 'recommended', tier: 'standard', notice, notices, rejected: rejections };
645
810
  }
646
811
 
647
812
  const anyTier = recommendTierModel('reasoning', { env, excludeFamilies }) || recommendTierModel('fast', { env, excludeFamilies });
648
813
  if (anyTier) {
649
- const notice = rejections.length
650
- ? `${availabilityNotice(rejections[0])} Using ${anyTier.id} (${anyTier.provider}).`
651
- : null;
652
- return { id: anyTier.id, source: 'recommended', tier: anyTier.tier, notice, rejected: rejections };
814
+ const { notice, notices } = rejections.length
815
+ ? resolutionNotices(rejections, anyTier.id, anyTier.provider)
816
+ : { notice: null, notices: [] };
817
+ return { id: anyTier.id, source: 'recommended', tier: anyTier.tier, notice, notices, rejected: rejections };
653
818
  }
654
819
 
655
- const notice = rejections.length ? availabilityNotice(rejections[0]) : null;
656
- return { id: null, source: null, notice, rejected: rejections };
820
+ const { notice, notices } = rejections.length
821
+ ? resolutionNotices(rejections, null, null)
822
+ : { notice: null, notices: [] };
823
+ return { id: null, source: null, notice, notices, rejected: rejections };
657
824
  }
658
825
 
659
826
  /**
@@ -893,7 +1060,7 @@ export function classifyProviderFailure(input) {
893
1060
 
894
1061
  const patterns = [
895
1062
  { kind: "rate_limit", retryable: true, test: /\b429\b|rate limit|usage limits?|too many requests|quota exceeded|weekly limit|monthly limit|daily limit/i },
896
- { kind: "provider_unavailable", retryable: true, test: /model unavailable|model.*overloaded|ProviderModelNotFoundError|model.*not found|no such model/i },
1063
+ { kind: "provider_unavailable", retryable: true, test: /model unavailable|model.*overloaded|ProviderModelNotFoundError|model.*not found|no such model|no endpoints found/i },
897
1064
  { kind: "provider_unavailable", retryable: true, test: /service unavailable|temporarily unavailable|upstream error|server error|\b5\d\d\b/i },
898
1065
  { kind: "transient_network", retryable: true, test: /timeout|timed out|ETIMEDOUT|ECONNRESET|network error|fetch failed/i },
899
1066
  { kind: "auth_error", retryable: false, test: /unauthorized|forbidden|invalid api key|authentication failed/i },
@@ -2,11 +2,12 @@
2
2
  * lib/models/catalog.mjs — live model catalog merge, cache, and visibility filter.
3
3
  *
4
4
  * Merges static provider-family hints with cached OpenRouter free-model polls,
5
- * then applies construct.config.json models.visibility rules so chat, CLI, and
5
+ * then applies construct.config.json models.visibility rules so CLI, hosts, and
6
6
  * dashboard share one filtered catalog surface.
7
7
  */
8
8
 
9
9
  import fs from 'node:fs';
10
+ import { loadRegistry } from '../registry/loader.mjs';
10
11
  import path from 'node:path';
11
12
  import os from 'node:os';
12
13
  import { pollFreeModels } from '../model-free-selector.mjs';
@@ -18,7 +19,7 @@ export const MODEL_VISIBILITY_MODES = ['all_configured', 'tier_defaults', 'expli
18
19
 
19
20
  export const DEFAULT_MODELS_CONFIG = Object.freeze({
20
21
  visibility: Object.freeze({
21
- mode: 'all_configured',
22
+ mode: 'tier_defaults',
22
23
  include: [],
23
24
  exclude: [],
24
25
  providers: {},
@@ -93,7 +94,7 @@ export async function refreshLiveOpenRouterCatalog({ env = process.env, homeDir
93
94
  return models;
94
95
  }
95
96
 
96
- function providerFamilyEnabled(providerId, visibility) {
97
+ export function providerFamilyEnabled(providerId, visibility) {
97
98
  const map = visibility.providers || {};
98
99
  if (Object.keys(map).length === 0) return true;
99
100
  if (map[providerId] === false) return false;
@@ -101,7 +102,7 @@ function providerFamilyEnabled(providerId, visibility) {
101
102
  return map[providerId] !== false;
102
103
  }
103
104
 
104
- function collectTierDefaultIds(registryModels = {}) {
105
+ export function collectTierDefaultIds(registryModels = {}) {
105
106
  const ids = [];
106
107
  for (const tier of ['reasoning', 'standard', 'fast']) {
107
108
  const def = registryModels[tier];
@@ -130,6 +131,18 @@ export function mergeLiveModelsIntoProviders(providers, liveModels = [], { maxLi
130
131
  });
131
132
  }
132
133
 
134
+ function collectShippedModelIds(provider) {
135
+ if (!provider) return [];
136
+ return uniqueStrings([
137
+ provider.tiers?.reasoning,
138
+ provider.tiers?.standard,
139
+ provider.tiers?.fast,
140
+ ...(provider.staticOptions?.reasoning ?? []),
141
+ ...(provider.staticOptions?.standard ?? []),
142
+ ...(provider.staticOptions?.fast ?? []),
143
+ ]);
144
+ }
145
+
133
146
  export function applyModelVisibilityFilter(catalog, {
134
147
  visibility = DEFAULT_MODELS_CONFIG.visibility,
135
148
  registryModels = {},
@@ -138,6 +151,7 @@ export function applyModelVisibilityFilter(catalog, {
138
151
  const includeSet = new Set(visibility.include || []);
139
152
  const excludeSet = new Set(visibility.exclude || []);
140
153
  const tierDefaults = collectTierDefaultIds(registryModels);
154
+ const providerById = new Map((catalog.providers || []).map((provider) => [provider.id, provider]));
141
155
 
142
156
  const modelAllowed = (modelId, providerId) => {
143
157
  if (!modelId) return false;
@@ -148,7 +162,11 @@ export function applyModelVisibilityFilter(catalog, {
148
162
  return includeSet.has(modelId);
149
163
  }
150
164
  if (visibility.mode === 'tier_defaults') {
151
- return tierDefaults.includes(modelId);
165
+ const shipped = collectShippedModelIds(providerById.get(providerId));
166
+ if (tierDefaults.length > 0) {
167
+ return tierDefaults.includes(modelId) || shipped.includes(modelId);
168
+ }
169
+ return shipped.includes(modelId);
152
170
  }
153
171
  return true;
154
172
  };
@@ -183,10 +201,8 @@ export function loadModelsCatalogContext({ cwd = process.cwd(), env = process.en
183
201
  const modelsConfig = resolveModelsConfig(config);
184
202
  let registryModels = {};
185
203
  try {
186
- const registryPath = path.join(cwd, 'specialists', 'registry.json');
187
- if (fs.existsSync(registryPath)) {
188
- registryModels = JSON.parse(fs.readFileSync(registryPath, 'utf8')).models ?? {};
189
- }
204
+ const registry = loadRegistry({ rootDir: cwd, skipValidation: true });
205
+ registryModels = registry?.models ?? {};
190
206
  } catch { /* registry read is best-effort outside repo root */ }
191
207
  const liveModels = modelsConfig.catalog.liveOpenRouter
192
208
  ? (readLiveCatalogCache({ homeDir }) ?? [])
@@ -3,7 +3,7 @@
3
3
  * record (construct-6zga.1.8).
4
4
  *
5
5
  * Consolidates the four formerly-scattered capability producers into one
6
- * serializable, versioned record that chat (resolveCapabilityTier) and specialist
6
+ * serializable, versioned record that model routing (resolveCapabilityTier) and specialist
7
7
  * prompt composition (resolveModelOperatingProfile via lib/prompt-composer.js) both
8
8
  * consume. It is keyed by provider, requested/resolved model, adapter protocol, and
9
9
  * observation time. Every capability declares an evidence source
@@ -57,8 +57,8 @@ export function providerGroupForModel(model) {
57
57
  return seg || 'unknown';
58
58
  }
59
59
 
60
- // Adapter protocol mirrors the dispatch in apps/chat/engine/ai-sdk-agent.mjs:
61
- // Anthropic uses its native messages API, OpenAI and Copilot use chat completions,
60
+ // Adapter protocol mirrors provider worker dispatch:
61
+ // Anthropic uses its native messages API, OpenAI and Copilot use completions,
62
62
  // and every other provider goes through the OpenAI-compatible shim.
63
63
 
64
64
  function adapterProtocolForGroup(group) {
@@ -81,11 +81,10 @@ function oneOf(allowed, value, fallback) {
81
81
  return allowed.includes(value) ? value : fallback;
82
82
  }
83
83
 
84
- // The chat turn overlay (lib/chat/transparency.mjs) speaks the orchestration
85
- // vocabulary (INTENT_CLASSES, risk flags, externalResearch). These helpers map it
84
+ // Routing overlays speak the orchestration vocabulary (INTENT_CLASSES, risk flags,
85
+ // externalResearch). These helpers map that vocabulary
86
86
  // onto the compiler's stable input enums so the compiler stays decoupled from
87
- // routing changes. A turn that follows a failed tool result is the recovery case
88
- // the loop signals with toolFailure.
87
+ // routing changes. A turn that follows a failed tool result is the recovery case.
89
88
 
90
89
  export function normalizePolicyIntent({ intent = null, assumptionsBlocked = false, toolFailure = false } = {}) {
91
90
  if (toolFailure === true) return 'tool-failure';
@@ -240,10 +239,10 @@ export function compileExecutionPolicy({
240
239
  }
241
240
 
242
241
  /**
243
- * Compile a policy directly from a chat turn overlay (lib/chat/transparency.mjs),
242
+ * Compile a policy directly from a routing overlay,
244
243
  * normalizing its routing vocabulary into the compiler's input enums first.
245
244
  */
246
- export function compilePolicyFromOverlay({ profile = null, overlay = null, toolFailure = false } = {}) {
245
+ export function compilePolicyFromRoutingOverlay({ profile = null, overlay = null, toolFailure = false } = {}) {
247
246
  const intent = normalizePolicyIntent({
248
247
  intent: overlay?.intent,
249
248
  assumptionsBlocked: overlay?.assumptionsBlocked === true,
@@ -257,6 +256,8 @@ export function compilePolicyFromOverlay({ profile = null, overlay = null, toolF
257
256
  return compileExecutionPolicy({ profile, intent, risk, evidenceRequirement });
258
257
  }
259
258
 
259
+ export const compilePolicyFromOverlay = compilePolicyFromRoutingOverlay;
260
+
260
261
  /**
261
262
  * Hand-rolled validator (no ajv — Construct stays dependency-free at startup).
262
263
  * Returns { valid, errors } against schemas/execution-policy.schema.json.
@@ -71,8 +71,16 @@ function openRouterCapabilities(model) {
71
71
  };
72
72
  }
73
73
 
74
+ function resolveSecretOrNull(varNames, opts) {
75
+ try {
76
+ return resolveFirstSecret(varNames, opts);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
74
82
  export async function pollOpenRouter({ env = process.env } = {}) {
75
- const apiKey = resolveFirstSecret(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env });
83
+ const apiKey = resolveSecretOrNull(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env, allowAmbient: env === process.env });
76
84
  const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
77
85
  const data = await fetchJson('https://openrouter.ai/api/v1/models', { headers });
78
86
  if (!data) return null;
@@ -80,10 +88,15 @@ export async function pollOpenRouter({ env = process.env } = {}) {
80
88
  const models = [];
81
89
  for (const m of list) {
82
90
  if (!m?.id) continue;
91
+ const outputs = m.architecture?.output_modalities;
92
+ if (Array.isArray(outputs) && outputs.length && !outputs.includes('text')) continue;
93
+ if (/(embed|rerank|whisper|tts|dall-e|moderation|transcribe|sora)/i.test(m.id)) continue;
83
94
  const free = isFreePricing(m.pricing);
84
95
  const input = Number(m.pricing?.prompt);
85
96
  const output = Number(m.pricing?.completion);
86
97
  const caps = openRouterCapabilities(m);
98
+ const context = Number(m.context_length) || null;
99
+ if (!free && context && context < 8000) continue;
87
100
  models.push({
88
101
  id: `openrouter/${m.id}`,
89
102
  label: m.name || m.id,
@@ -92,11 +105,12 @@ export async function pollOpenRouter({ env = process.env } = {}) {
92
105
  pricing: Number.isFinite(input) && Number.isFinite(output)
93
106
  ? { input: input * 1_000_000, output: output * 1_000_000 }
94
107
  : null,
95
- context: Number(m.context_length) || null,
108
+ context,
96
109
  reasoning: caps.reasoning,
97
110
  tools: caps.tools,
98
111
  toolsKnown: true,
99
112
  vision: caps.vision,
113
+ architecture: m.architecture || null,
100
114
  source: 'live',
101
115
  });
102
116
  }
@@ -110,7 +124,7 @@ export async function pollOpenRouter({ env = process.env } = {}) {
110
124
  const OPENAI_NON_CHAT = /(embedding|whisper|tts|audio|realtime|image|dall-e|moderation|search|babbage|davinci|transcribe|sora)/i;
111
125
 
112
126
  export async function pollOpenAI({ env = process.env } = {}) {
113
- const apiKey = resolveFirstSecret(['OPENAI_API_KEY'], { env });
127
+ const apiKey = resolveSecretOrNull(['OPENAI_API_KEY'], { env, allowAmbient: env === process.env });
114
128
  if (!apiKey) return null;
115
129
  const data = await fetchJson('https://api.openai.com/v1/models', {
116
130
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -141,7 +155,7 @@ export async function pollOpenAI({ env = process.env } = {}) {
141
155
  }
142
156
 
143
157
  export async function pollAnthropic({ env = process.env } = {}) {
144
- const apiKey = resolveFirstSecret(['ANTHROPIC_API_KEY'], { env });
158
+ const apiKey = resolveSecretOrNull(['ANTHROPIC_API_KEY'], { env, allowAmbient: env === process.env });
145
159
  if (!apiKey) return null;
146
160
  const data = await fetchJson('https://api.anthropic.com/v1/models?limit=100', {
147
161
  headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },