@geraldmaron/construct 1.2.3 → 1.3.0

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 (504) 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/source-checkout.mjs +3 -4
  44. package/lib/doctor/watchers/consistency.mjs +90 -22
  45. package/lib/doctor/watchers/credential-parity.mjs +97 -0
  46. package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
  47. package/lib/doctor/watchers/service-health.mjs +4 -32
  48. package/lib/document-export.mjs +73 -9
  49. package/lib/document-extract.mjs +10 -198
  50. package/lib/embed/artifact.mjs +2 -3
  51. package/lib/embed/cli.mjs +1 -1
  52. package/lib/embed/config.mjs +1 -2
  53. package/lib/embed/customer-profiles.mjs +1 -1
  54. package/lib/embed/daemon.mjs +5 -6
  55. package/lib/embed/demand-fetch.mjs +114 -2
  56. package/lib/embed/docs-lifecycle.mjs +9 -5
  57. package/lib/embed/intake-metrics.mjs +2 -2
  58. package/lib/embed/notifications.mjs +2 -3
  59. package/lib/embed/recommendation-store.mjs +25 -22
  60. package/lib/embed/role-framing.mjs +4 -9
  61. package/lib/embedded-contract/capability.mjs +4 -2
  62. package/lib/embedded-contract/contract-version.mjs +1 -1
  63. package/lib/env-config.mjs +2 -2
  64. package/lib/flavors/loader.mjs +21 -19
  65. package/lib/graph/build-from-registry.mjs +31 -7
  66. package/lib/graph/staleness.mjs +1 -1
  67. package/lib/headhunt.mjs +82 -35
  68. package/lib/hooks/agent-tracker.mjs +2 -2
  69. package/lib/hooks/config-protection.mjs +1 -1
  70. package/lib/hooks/registry-sync.mjs +3 -3
  71. package/lib/host-capabilities.mjs +5 -5
  72. package/lib/improvement/controller.mjs +2 -2
  73. package/lib/init-unified.mjs +19 -21
  74. package/lib/intake/classify.mjs +26 -0
  75. package/lib/intake/daemon.mjs +2 -3
  76. package/lib/intake/prepare.mjs +10 -3
  77. package/lib/intake/session-prelude.mjs +1 -1
  78. package/lib/intake/tables/creative.mjs +9 -9
  79. package/lib/intake/tables/operations.mjs +4 -4
  80. package/lib/intake/tables/research.mjs +2 -2
  81. package/lib/integrations/intake-integrations.mjs +9 -10
  82. package/lib/knowledge/research-store.mjs +2 -2
  83. package/lib/mcp/server.mjs +104 -61
  84. package/lib/mcp/tool-budget.mjs +1 -46
  85. package/lib/mcp/tool-recovery.mjs +44 -0
  86. package/lib/mcp/tools/artifact-author.mjs +36 -0
  87. package/lib/mcp/tools/find-tool.mjs +86 -0
  88. package/lib/mcp/tools/orchestration-run.mjs +107 -69
  89. package/lib/mcp/tools/project.mjs +4 -3
  90. package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
  91. package/lib/mcp/tools/skills.mjs +39 -22
  92. package/lib/mcp/tools/telemetry.mjs +2 -1
  93. package/lib/mcp/tools/workflow.mjs +1 -1
  94. package/lib/mcp-platform-config.mjs +7 -5
  95. package/lib/migrations/index.mjs +4 -2
  96. package/lib/migrations/v2-unified-registry.mjs +35 -0
  97. package/lib/model-router.mjs +203 -36
  98. package/lib/models/catalog.mjs +25 -9
  99. package/lib/models/execution-capability-profile.mjs +3 -3
  100. package/lib/models/execution-policy.mjs +7 -6
  101. package/lib/models/provider-poll.mjs +18 -4
  102. package/lib/opencode-config.mjs +56 -1
  103. package/lib/opencode-runtime-plugin.mjs +13 -10
  104. package/lib/oracle/artifact-gate.mjs +14 -4
  105. package/lib/oracle/cli.mjs +2 -0
  106. package/lib/oracle/dispatch.mjs +18 -1
  107. package/lib/oracle/execute.mjs +39 -7
  108. package/lib/oracle/index.mjs +1 -1
  109. package/lib/oracle/org-graph.mjs +10 -5
  110. package/lib/oracle/policy.mjs +6 -0
  111. package/lib/oracle/read-model.mjs +103 -6
  112. package/lib/oracle/reconcile.mjs +31 -4
  113. package/lib/oracle/remediation-dispatch.mjs +53 -0
  114. package/lib/oracle/routing.mjs +5 -0
  115. package/lib/oracle/synthesize.mjs +80 -3
  116. package/lib/orchestration/routing-tables.mjs +44 -10
  117. package/lib/orchestration/runtime.mjs +2 -0
  118. package/lib/orchestration/worker.mjs +1 -1
  119. package/lib/orchestration-policy.mjs +351 -62
  120. package/lib/overrides/resolver.mjs +1 -12
  121. package/lib/parity.mjs +37 -11
  122. package/lib/policy/engine.mjs +80 -15
  123. package/lib/prompt-composer.js +51 -15
  124. package/lib/prompt-metadata.mjs +3 -2
  125. package/lib/providers/connection-probe.mjs +58 -0
  126. package/lib/providers/copilot-auth.mjs +1 -1
  127. package/lib/providers/credential-bootstrap.mjs +1 -1
  128. package/lib/providers/op-run.mjs +3 -4
  129. package/lib/providers/secret-resolver.mjs +31 -0
  130. package/lib/publish-tooling.mjs +3 -11
  131. package/lib/publish.mjs +12 -29
  132. package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
  133. package/lib/reflect.mjs +2 -2
  134. package/lib/registry/agent-manifest.mjs +190 -0
  135. package/lib/registry/assemble.mjs +133 -0
  136. package/lib/registry/catalog.mjs +171 -0
  137. package/lib/registry/cli.mjs +590 -6
  138. package/lib/registry/consolidation.mjs +5 -4
  139. package/lib/registry/docs-sync.mjs +67 -0
  140. package/lib/registry/loader.mjs +147 -0
  141. package/lib/registry/org-io.mjs +76 -0
  142. package/lib/registry/retired-paths.mjs +71 -0
  143. package/lib/registry/surface-map.mjs +5 -7
  144. package/lib/registry/validator.mjs +438 -0
  145. package/lib/research-execution-policy.mjs +173 -0
  146. package/lib/roles/catalog.mjs +4 -9
  147. package/lib/roles/fence.mjs +107 -1
  148. package/lib/roles/flavor-bindings.mjs +68 -0
  149. package/lib/roles/gateway.mjs +140 -29
  150. package/lib/roles/manifest.mjs +22 -13
  151. package/lib/roles/router.mjs +1 -1
  152. package/lib/scopes/enrich.mjs +67 -0
  153. package/lib/scopes/hooks.mjs +12 -0
  154. package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
  155. package/lib/scopes/loader.mjs +104 -0
  156. package/lib/scopes/rebrand.mjs +32 -0
  157. package/lib/scopes/research-profile.mjs +16 -0
  158. package/lib/scopes/teams.mjs +98 -0
  159. package/lib/service-manager.mjs +1 -117
  160. package/lib/setup-prompts.mjs +1 -1
  161. package/lib/setup.mjs +6 -26
  162. package/lib/skills/composition-graph.mjs +98 -0
  163. package/lib/skills/router.mjs +80 -0
  164. package/lib/specialist-contracts.mjs +17 -18
  165. package/lib/specialists/postconditions.mjs +2 -2
  166. package/lib/specialists/prompt-schema.mjs +6 -5
  167. package/lib/specialists/roster.mjs +7 -12
  168. package/lib/specialists/schema.mjs +9 -6
  169. package/lib/status.mjs +25 -90
  170. package/lib/storage/file-lock.mjs +6 -3
  171. package/lib/telemetry/skill-calls.mjs +1 -1
  172. package/lib/templates/doc-presentation.mjs +63 -0
  173. package/lib/templates/visual-requirements.mjs +35 -2
  174. package/lib/term-format.mjs +107 -2
  175. package/lib/test-env-setup.mjs +21 -0
  176. package/lib/ui/components.mjs +42 -0
  177. package/lib/ui/glyphs.mjs +58 -0
  178. package/lib/ui/links.mjs +115 -0
  179. package/lib/ui/theme.mjs +108 -0
  180. package/lib/uninstall/uninstall.mjs +2 -1
  181. package/lib/validator.mjs +45 -8
  182. package/lib/validators/skill-effectiveness.mjs +174 -0
  183. package/lib/validators/skills.mjs +1 -1
  184. package/package.json +12 -8
  185. package/personas/construct.md +12 -3
  186. package/platforms/claude/CLAUDE.md +1 -1
  187. package/rules/common/beads-hygiene.md +52 -0
  188. package/rules/common/neurodivergent-output.md +4 -7
  189. package/rules/common/research.md +2 -0
  190. package/scripts/sync-specialists.mjs +260 -49
  191. package/skills/ai/prompt-optimizer.md +3 -3
  192. package/skills/brand/output-vibe.md +48 -0
  193. package/skills/docs/adr-workflow.md +3 -0
  194. package/skills/docs/backlog-proposal-workflow.md +3 -0
  195. package/skills/docs/codebase-research-workflow.md +5 -2
  196. package/skills/docs/customer-profile-workflow.md +3 -0
  197. package/skills/docs/document-ingest-workflow.md +3 -0
  198. package/skills/docs/evidence-ingest-workflow.md +4 -1
  199. package/skills/docs/init-project.md +1 -1
  200. package/skills/docs/prd-workflow.md +1 -1
  201. package/skills/docs/prfaq-workflow.md +3 -0
  202. package/skills/docs/product-intelligence-workflow.md +4 -1
  203. package/skills/docs/product-signal-workflow.md +3 -0
  204. package/skills/docs/research-workflow.md +13 -6
  205. package/skills/docs/runbook-workflow.md +3 -0
  206. package/skills/docs/strategy-workflow.md +3 -0
  207. package/skills/docs/user-research-workflow.md +6 -3
  208. package/skills/operating/orchestration-reference.md +1 -1
  209. package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
  210. package/skills/roles/architect.ai-systems.md +1 -1
  211. package/skills/roles/architect.data.md +1 -1
  212. package/skills/roles/architect.enterprise.md +1 -1
  213. package/skills/roles/architect.integration.md +1 -1
  214. package/skills/roles/architect.md +1 -1
  215. package/skills/roles/architect.platform.md +1 -1
  216. package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
  217. package/skills/roles/data-analyst.experiment.md +1 -1
  218. package/skills/roles/data-analyst.md +1 -1
  219. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  220. package/skills/roles/data-analyst.product.md +1 -1
  221. package/skills/roles/data-analyst.telemetry.md +1 -1
  222. package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
  223. package/skills/roles/data-engineer.pipeline.md +2 -2
  224. package/skills/roles/data-engineer.vector-retrieval.md +2 -2
  225. package/skills/roles/data-engineer.warehouse.md +2 -2
  226. package/skills/roles/debugger.md +1 -1
  227. package/skills/roles/designer.accessibility.md +1 -1
  228. package/skills/roles/designer.md +1 -1
  229. package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
  230. package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
  231. package/skills/roles/engineer.md +3 -7
  232. package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
  233. package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
  234. package/skills/roles/{operator.md → operations.md} +2 -5
  235. package/skills/roles/orchestrator.md +1 -1
  236. package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
  237. package/skills/roles/product-manager.ai-product.md +1 -1
  238. package/skills/roles/product-manager.enterprise.md +1 -1
  239. package/skills/roles/product-manager.growth.md +1 -2
  240. package/skills/roles/product-manager.md +1 -2
  241. package/skills/roles/product-manager.platform.md +1 -1
  242. package/skills/roles/product-manager.product.md +1 -1
  243. package/skills/roles/qa.ai-eval.md +1 -1
  244. package/skills/roles/qa.api-contract.md +1 -1
  245. package/skills/roles/qa.data-pipeline.md +1 -1
  246. package/skills/roles/qa.md +1 -1
  247. package/skills/roles/qa.web-ui.md +1 -1
  248. package/skills/roles/{operator.release.md → release-manager.md} +3 -3
  249. package/skills/roles/researcher.md +1 -1
  250. package/skills/roles/reviewer.md +1 -1
  251. package/skills/roles/security.ai.md +1 -1
  252. package/skills/roles/security.appsec.md +1 -1
  253. package/skills/roles/security.cloud.md +1 -1
  254. package/skills/roles/security.legal-compliance.md +1 -1
  255. package/skills/roles/security.md +1 -1
  256. package/skills/roles/security.privacy.md +1 -1
  257. package/skills/roles/security.supply-chain.md +1 -1
  258. package/skills/roles/{operator.sre.md → sre.md} +3 -3
  259. package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
  260. package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
  261. package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
  262. package/skills/routing.json +18 -0
  263. package/skills/routing.md +1 -1
  264. package/specialists/artifact-manifest.json +2 -1
  265. package/specialists/audit-enrichments.json +14 -14
  266. package/specialists/org/contracts/accessibility-to-qa.json +37 -0
  267. package/specialists/org/contracts/any-to-business-strategist.json +57 -0
  268. package/specialists/org/contracts/any-to-debugger.json +38 -0
  269. package/specialists/org/contracts/any-to-designer.json +33 -0
  270. package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
  271. package/specialists/org/contracts/any-to-explorer.json +39 -0
  272. package/specialists/org/contracts/any-to-sre-incident.json +33 -0
  273. package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
  274. package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
  275. package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
  276. package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
  277. package/specialists/org/contracts/architect-to-engineer.json +34 -0
  278. package/specialists/org/contracts/architect-to-evaluator.json +59 -0
  279. package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
  280. package/specialists/org/contracts/architect-to-operations.json +45 -0
  281. package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
  282. package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
  283. package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
  284. package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
  285. package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
  286. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
  287. package/specialists/org/contracts/designer-to-accessibility.json +36 -0
  288. package/specialists/org/contracts/engineer-to-qa.json +34 -0
  289. package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
  290. package/specialists/org/contracts/explorer-to-engineer.json +38 -0
  291. package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
  292. package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
  293. package/specialists/org/contracts/product-manager-to-architect.json +71 -0
  294. package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
  295. package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
  296. package/specialists/org/contracts/qa-to-release-manager.json +42 -0
  297. package/specialists/org/contracts/qa-to-test-automation.json +42 -0
  298. package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
  299. package/specialists/org/contracts/researcher-to-architect.json +40 -0
  300. package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
  301. package/specialists/org/contracts/reviewer-to-security.json +41 -0
  302. package/specialists/org/contracts/sre-to-release-manager.json +36 -0
  303. package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
  304. package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
  305. package/specialists/org/contracts/user-to-construct.json +30 -0
  306. package/specialists/org/groups/engineering-group.json +43 -0
  307. package/specialists/org/groups/governance-group.json +38 -0
  308. package/specialists/org/groups/operations-group.json +41 -0
  309. package/specialists/org/groups/product-group.json +46 -0
  310. package/specialists/org/groups/quality-group.json +42 -0
  311. package/specialists/org/groups/strategy-group.json +41 -0
  312. package/specialists/org/policies/action-approval.json +13 -0
  313. package/specialists/org/policies/agents-routing.json +13 -0
  314. package/specialists/org/policies/architecture.json +20 -0
  315. package/specialists/org/policies/bash-safety.json +13 -0
  316. package/specialists/org/policies/beads-hygiene.json +13 -0
  317. package/specialists/org/policies/bootstrap-state.json +13 -0
  318. package/specialists/org/policies/code-review.json +13 -0
  319. package/specialists/org/policies/coding-style.json +13 -0
  320. package/specialists/org/policies/comments.json +13 -0
  321. package/specialists/org/policies/commit-approval.json +13 -0
  322. package/specialists/org/policies/contract-preconditions.json +13 -0
  323. package/specialists/org/policies/deployment.json +20 -0
  324. package/specialists/org/policies/description.json +14 -0
  325. package/specialists/org/policies/design-approval.json +19 -0
  326. package/specialists/org/policies/doc-ownership.json +13 -0
  327. package/specialists/org/policies/file-path-fence.json +13 -0
  328. package/specialists/org/policies/framing.json +13 -0
  329. package/specialists/org/policies/git-workflow.json +13 -0
  330. package/specialists/org/policies/incident-response.json +15 -0
  331. package/specialists/org/policies/intake-triage.json +15 -0
  332. package/specialists/org/policies/neurodivergent-output.json +13 -0
  333. package/specialists/org/policies/no-fabrication.json +13 -0
  334. package/specialists/org/policies/patterns.json +13 -0
  335. package/specialists/org/policies/quality-gate-approval.json +17 -0
  336. package/specialists/org/policies/release-gates.json +13 -0
  337. package/specialists/org/policies/research-evidence.json +13 -0
  338. package/specialists/org/policies/review-before-change.json +13 -0
  339. package/specialists/org/policies/rollback.json +15 -0
  340. package/specialists/org/policies/scope-change.json +20 -0
  341. package/specialists/org/policies/secret-scan.json +13 -0
  342. package/specialists/org/policies/security-approval.json +17 -0
  343. package/specialists/org/policies/security.json +13 -0
  344. package/specialists/org/policies/session-efficiency.json +13 -0
  345. package/specialists/org/policies/skill-routing.json +13 -0
  346. package/specialists/org/policies/strategic-prioritization.json +17 -0
  347. package/specialists/org/policies/testing.json +13 -0
  348. package/specialists/org/policies/tool-invisibility.json +14 -0
  349. package/specialists/org/scopes/creative.json +54 -0
  350. package/specialists/org/scopes/operations.json +51 -0
  351. package/specialists/org/scopes/research.json +60 -0
  352. package/specialists/org/scopes/rnd.json +81 -0
  353. package/specialists/org/specialists/cx-accessibility.json +69 -0
  354. package/specialists/org/specialists/cx-ai-engineer.json +75 -0
  355. package/specialists/org/specialists/cx-architect.json +89 -0
  356. package/specialists/org/specialists/cx-business-strategist.json +72 -0
  357. package/specialists/org/specialists/cx-data-analyst.json +68 -0
  358. package/specialists/org/specialists/cx-data-engineer.json +71 -0
  359. package/specialists/org/specialists/cx-debugger.json +75 -0
  360. package/specialists/org/specialists/cx-designer.json +85 -0
  361. package/specialists/org/specialists/cx-devil-advocate.json +71 -0
  362. package/specialists/org/specialists/cx-docs-keeper.json +82 -0
  363. package/specialists/org/specialists/cx-engineer.json +106 -0
  364. package/specialists/org/specialists/cx-evaluator.json +69 -0
  365. package/specialists/org/specialists/cx-explorer.json +69 -0
  366. package/specialists/org/specialists/cx-legal-compliance.json +74 -0
  367. package/specialists/org/specialists/cx-operations.json +72 -0
  368. package/specialists/org/specialists/cx-oracle.json +46 -0
  369. package/specialists/org/specialists/cx-orchestrator.json +81 -0
  370. package/specialists/org/specialists/cx-platform-engineer.json +80 -0
  371. package/specialists/org/specialists/cx-product-manager.json +102 -0
  372. package/specialists/org/specialists/cx-qa.json +79 -0
  373. package/specialists/org/specialists/cx-rd-lead.json +73 -0
  374. package/specialists/org/specialists/cx-release-manager.json +77 -0
  375. package/specialists/org/specialists/cx-researcher.json +82 -0
  376. package/specialists/org/specialists/cx-reviewer.json +71 -0
  377. package/specialists/org/specialists/cx-security.json +91 -0
  378. package/specialists/org/specialists/cx-sre.json +94 -0
  379. package/specialists/org/specialists/cx-test-automation.json +69 -0
  380. package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
  381. package/specialists/org/specialists/cx-ux-researcher.json +68 -0
  382. package/specialists/org/teams/accessibility-team.json +39 -0
  383. package/specialists/org/teams/design-team.json +40 -0
  384. package/specialists/org/teams/engineering-team.json +50 -0
  385. package/specialists/org/teams/governance-team.json +41 -0
  386. package/specialists/org/teams/operations-team.json +46 -0
  387. package/specialists/org/teams/product-management-team.json +45 -0
  388. package/specialists/org/teams/quality-team.json +49 -0
  389. package/specialists/org/teams/research-team.json +39 -0
  390. package/specialists/org/teams/strategy-team.json +48 -0
  391. package/specialists/org/teams/ux-research-team.json +39 -0
  392. package/specialists/prompts/_shared/validation-contract.md +1 -1
  393. package/specialists/prompts/_team-template.md +10 -0
  394. package/specialists/prompts/cx-accessibility.md +1 -0
  395. package/specialists/prompts/cx-ai-engineer.md +1 -1
  396. package/specialists/prompts/cx-business-strategist.md +1 -1
  397. package/specialists/prompts/cx-data-engineer.md +1 -1
  398. package/specialists/prompts/cx-designer.md +1 -0
  399. package/specialists/prompts/cx-devil-advocate.md +1 -1
  400. package/specialists/prompts/cx-docs-keeper.md +1 -1
  401. package/specialists/prompts/cx-evaluator.md +1 -1
  402. package/specialists/prompts/cx-explorer.md +1 -1
  403. package/specialists/prompts/cx-operations.md +1 -1
  404. package/specialists/prompts/cx-oracle.md +7 -3
  405. package/specialists/prompts/cx-orchestrator.md +17 -1
  406. package/specialists/prompts/cx-platform-engineer.md +1 -1
  407. package/specialists/prompts/cx-product-manager.md +2 -0
  408. package/specialists/prompts/cx-release-manager.md +1 -1
  409. package/specialists/prompts/cx-researcher.md +7 -4
  410. package/specialists/prompts/cx-sre.md +1 -1
  411. package/specialists/prompts/cx-test-automation.md +2 -2
  412. package/specialists/prompts/cx-trace-reviewer.md +3 -3
  413. package/specialists/prompts/cx-ux-researcher.md +2 -1
  414. package/templates/demos/scripts/architecture-review-adr.json +30 -0
  415. package/templates/demos/scripts/capability-contract.json +25 -0
  416. package/templates/demos/scripts/intake-triage.json +25 -0
  417. package/templates/demos/scripts/profile-doctor-health.json +25 -0
  418. package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
  419. package/templates/demos/tapes/architecture-review-adr.tape +44 -0
  420. package/templates/demos/tapes/capability-contract.tape +39 -0
  421. package/templates/demos/tapes/intake-triage.tape +39 -0
  422. package/templates/demos/tapes/profile-doctor-health.tape +39 -0
  423. package/templates/distribution/construct-brand.typ +23 -18
  424. package/templates/distribution/construct-decision.typ +1 -1
  425. package/templates/distribution/construct-pdf.typ +1 -1
  426. package/templates/distribution/construct-prd.typ +1 -1
  427. package/templates/distribution/construct-research.typ +1 -1
  428. package/templates/distribution/mermaid-puppeteer.json +8 -0
  429. package/templates/docs/persona-artifact.md +1 -1
  430. package/templates/docs/prd.md +6 -0
  431. package/apps/chat/engine/ai-sdk-agent.mjs +0 -183
  432. package/apps/chat/engine/loop-driver.mjs +0 -211
  433. package/apps/chat/engine/models.mjs +0 -122
  434. package/apps/chat/engine/provider-adapters.mjs +0 -171
  435. package/apps/chat/engine/tools/permission.mjs +0 -54
  436. package/apps/chat/engine/tools/primitives.mjs +0 -180
  437. package/apps/chat/engine/tools/registry.mjs +0 -122
  438. package/apps/chat/engine/turn-controls.mjs +0 -70
  439. package/lib/boundary.mjs +0 -127
  440. package/lib/certification/dashboard-api.mjs +0 -71
  441. package/lib/chat/cli.mjs +0 -333
  442. package/lib/chat/command-suggest.mjs +0 -161
  443. package/lib/chat/commands.mjs +0 -215
  444. package/lib/chat/config.mjs +0 -142
  445. package/lib/chat/context-compactor.mjs +0 -250
  446. package/lib/chat/context-continuation.mjs +0 -253
  447. package/lib/chat/continuation-source.mjs +0 -58
  448. package/lib/chat/demo-guide.mjs +0 -61
  449. package/lib/chat/design-tokens.mjs +0 -91
  450. package/lib/chat/desktop-binary.mjs +0 -81
  451. package/lib/chat/desktop-build.mjs +0 -130
  452. package/lib/chat/desktop-launcher.mjs +0 -133
  453. package/lib/chat/evidence.mjs +0 -145
  454. package/lib/chat/export.mjs +0 -74
  455. package/lib/chat/harness/driver.mjs +0 -91
  456. package/lib/chat/list-picker.mjs +0 -112
  457. package/lib/chat/model-picker.mjs +0 -356
  458. package/lib/chat/openrouter-fallback.mjs +0 -151
  459. package/lib/chat/permission-prompt.mjs +0 -33
  460. package/lib/chat/picker-catalog.mjs +0 -45
  461. package/lib/chat/policy-telemetry.mjs +0 -34
  462. package/lib/chat/present.mjs +0 -246
  463. package/lib/chat/session-context.mjs +0 -39
  464. package/lib/chat/session-persist.mjs +0 -73
  465. package/lib/chat/session-restore.mjs +0 -71
  466. package/lib/chat/session-settings.mjs +0 -53
  467. package/lib/chat/system-prompt.mjs +0 -52
  468. package/lib/chat/transparency.mjs +0 -93
  469. package/lib/chat/tui/color-scheme.mjs +0 -42
  470. package/lib/chat/tui/markdown.mjs +0 -123
  471. package/lib/chat/tui/presentation.mjs +0 -100
  472. package/lib/chat/tui/render.mjs +0 -500
  473. package/lib/chat/tui/turn-block.mjs +0 -284
  474. package/lib/chat/tui/turn-present.mjs +0 -18
  475. package/lib/chat/tui/turn-state.mjs +0 -88
  476. package/lib/chat/tui/usage.mjs +0 -122
  477. package/lib/chat/web-commands.mjs +0 -146
  478. package/lib/chat/web-launcher.mjs +0 -63
  479. package/lib/chat/web-picker-keys.mjs +0 -46
  480. package/lib/chat/web-session.mjs +0 -159
  481. package/lib/config/alias.mjs +0 -57
  482. package/lib/dashboard-demo.mjs +0 -71
  483. package/lib/dashboard-static.mjs +0 -175
  484. package/lib/install/desktop-binary-download.mjs +0 -88
  485. package/lib/profiles/loader.mjs +0 -123
  486. package/lib/profiles/rebrand.mjs +0 -46
  487. package/lib/server/auth.mjs +0 -169
  488. package/lib/server/chat-loop.mjs +0 -622
  489. package/lib/server/chat.mjs +0 -336
  490. package/lib/server/cors.mjs +0 -77
  491. package/lib/server/csrf.mjs +0 -103
  492. package/lib/server/demo-preview.mjs +0 -63
  493. package/lib/server/index.mjs +0 -2641
  494. package/lib/server/insights.mjs +0 -780
  495. package/lib/server/langfuse-login.mjs +0 -58
  496. package/lib/server/rate-limit.mjs +0 -93
  497. package/lib/server/telemetry-login.mjs +0 -100
  498. package/lib/server/webhook.mjs +0 -512
  499. package/specialists/contracts.json +0 -1032
  500. package/specialists/contracts.schema.json +0 -83
  501. package/specialists/policy-inventory.json +0 -188
  502. package/specialists/registry.json +0 -1412
  503. package/specialists/role-manifests.json +0 -217
  504. package/specialists/teams.json +0 -94
@@ -19,6 +19,9 @@ import { buildTaskPacketFromIntent } from '../../workflow-state.mjs';
19
19
  import { logSkillCall } from '../../telemetry/skill-calls.mjs';
20
20
  import { templateMetadata } from '../../artifact-manifest.mjs';
21
21
  import { buildSpecialistCatalog } from '../../specialists/roster.mjs';
22
+ import { loadRegistry } from '../../registry/loader.mjs';
23
+ import { listGroupsHierarchical } from '../../registry/assemble.mjs';
24
+ import { suggestSkills } from '../../skills/router.mjs';
22
25
 
23
26
  export function listSkills({ ROOT_DIR }) {
24
27
  const skillsDir = join(ROOT_DIR, 'skills');
@@ -48,9 +51,22 @@ export function listSkills({ ROOT_DIR }) {
48
51
  }
49
52
 
50
53
  export function getSkill(args, { ROOT_DIR }) {
51
- const { path: skillPath } = args;
54
+ const { path: skillPath, specialistId, agentId } = args;
52
55
  if (!skillPath) return { error: 'Missing path argument' };
53
56
 
57
+ const strict = process.env.CONSTRUCT_STRICT_SKILLS === '1';
58
+ const specId = specialistId || agentId;
59
+ let entitlementWarning = null;
60
+ if (specId) {
61
+ const registry = loadRegistry({ rootDir: ROOT_DIR, skipValidation: true });
62
+ const id = String(specId).startsWith('cx-') ? specId : `cx-${specId}`;
63
+ const entitled = new Set(registry.specialists?.[id]?.skills || []);
64
+ if (entitled.size > 0 && !entitled.has(skillPath)) {
65
+ entitlementWarning = `Skill ${skillPath} is not in ${id} entitlement list`;
66
+ if (strict) return { error: entitlementWarning };
67
+ }
68
+ }
69
+
54
70
  // Primary: flat file form skills/<path>.md
55
71
  // Fallback: directory form skills/<path>/SKILL.md (one minor-release grace period)
56
72
  const candidates = [
@@ -73,7 +89,7 @@ export function getSkill(args, { ROOT_DIR }) {
73
89
  sessionId: args.sessionId,
74
90
  tokensReturned: Math.ceil(content.length / 4),
75
91
  });
76
- return { content };
92
+ return { content, ...(entitlementWarning ? { warning: entitlementWarning } : {}) };
77
93
  }
78
94
 
79
95
  export function searchSkills(args, { ROOT_DIR }) {
@@ -434,29 +450,30 @@ export async function workerRun(args = {}) {
434
450
  return { ...result, ...(evidence ? { evidence } : {}) };
435
451
  }
436
452
 
437
- export function listTeams({ ROOT_DIR }) {
438
- const teamsPath = join(ROOT_DIR, 'specialists', 'teams.json');
439
- if (!existsSync(teamsPath)) return { error: 'specialists/teams.json not found' };
440
- const { templates } = JSON.parse(readFileSync(teamsPath, 'utf8'));
453
+ export function suggestSkillsTool(args, { ROOT_DIR, cwd } = {}) {
454
+ const intent = args?.intent || args?.query || '';
455
+ return suggestSkills({
456
+ intent,
457
+ specialistId: args?.specialistId || args?.agentId,
458
+ limit: args?.limit || 5,
459
+ rootDir: ROOT_DIR,
460
+ cwd: cwd || ROOT_DIR,
461
+ });
462
+ }
463
+
464
+ export function listTeams({ ROOT_DIR } = {}) {
465
+ const registry = loadRegistry({ rootDir: ROOT_DIR, skipValidation: true });
441
466
  return {
442
- teams: Object.entries(templates).map(([name, t]) => ({
443
- name,
444
- description: t.description,
445
- members: t.members,
446
- focus: t.focus,
447
- skills: t.skills,
448
- promotionGates: t.promotionGates,
449
- })),
467
+ groups: listGroupsHierarchical(registry),
468
+ teams: Object.entries(registry.teams || {}).map(([id, t]) => ({ id, ...t })),
450
469
  };
451
470
  }
452
471
 
453
- export function getTeam(args, { ROOT_DIR }) {
454
- const { name } = args;
472
+ export function getTeam(args, { ROOT_DIR } = {}) {
473
+ const name = args?.name || args?.id;
455
474
  if (!name) return { error: 'Missing name argument' };
456
- const teamsPath = join(ROOT_DIR, 'specialists', 'teams.json');
457
- if (!existsSync(teamsPath)) return { error: 'specialists/teams.json not found' };
458
- const { templates } = JSON.parse(readFileSync(teamsPath, 'utf8'));
459
- const team = templates[name];
460
- if (!team) return { error: `Team not found: ${name}`, available: Object.keys(templates) };
461
- return { name, ...team };
475
+ const registry = loadRegistry({ rootDir: ROOT_DIR, skipValidation: true });
476
+ const team = registry.teams?.[name];
477
+ if (!team) return { error: `Team not found: ${name}`, available: Object.keys(registry.teams || {}) };
478
+ return { id: name, ...team };
462
479
  }
@@ -6,6 +6,7 @@
6
6
  * adapter when configured; local JSONL remains available by default.
7
7
  */
8
8
  import { join, resolve } from 'node:path';
9
+ import { loadRegistry } from '../../registry/loader.mjs';
9
10
  import { homedir } from 'node:os';
10
11
  import { readFileSync, existsSync } from 'node:fs';
11
12
  import { createTelemetryClient } from '../../telemetry/client.mjs';
@@ -71,7 +72,7 @@ function resolveSessionContext() {
71
72
 
72
73
  export async function cxTrace(args, { ROOT_DIR }) {
73
74
  const ctx = resolveSessionContext();
74
- const registry = readJSON(join(ROOT_DIR, 'specialists', 'registry.json')) ?? {};
75
+ const registry = loadRegistry({ rootDir: ROOT_DIR });
75
76
  const registryModels = registry.models ?? {};
76
77
  const currentModels = readCurrentModels(join(ROOT_DIR, '.env'), registryModels, process.env);
77
78
  const route = typeof args.input === 'string' ? routeRequest({ request: args.input }) : null;
@@ -97,7 +97,7 @@ export function workflowValidate(args) {
97
97
  }
98
98
 
99
99
  /**
100
- * Validate a producer→consumer handoff against specialists/contracts.json.
100
+ * Validate a producer→consumer handoff against specialists/org.
101
101
  *
102
102
  * Args:
103
103
  * producer — agent or persona name producing the artifact
@@ -14,16 +14,16 @@ export function getOpenCodeMcpId(id) {
14
14
  return id;
15
15
  }
16
16
 
17
- function resolveTemplateString(value, resolvedValues) {
18
- return value.replace(/__([A-Z0-9_]+)__/g, (_, name) => resolvedValues[name] ?? `__${name}__`);
17
+ function resolveTemplateString(value, resolvedValues, fallback = (name) => `__${name}__`) {
18
+ return value.replace(/__([A-Z0-9_]+)__/g, (_, name) => resolvedValues[name] ?? fallback(name));
19
19
  }
20
20
 
21
- function resolveTemplateObject(input, resolvedValues) {
21
+ function resolveTemplateObject(input, resolvedValues, fallback) {
22
22
  if (!input || typeof input !== "object" || Array.isArray(input)) return {};
23
23
  return Object.fromEntries(
24
24
  Object.entries(input).map(([key, value]) => [
25
25
  key,
26
- typeof value === "string" ? resolveTemplateString(value, resolvedValues) : value,
26
+ typeof value === "string" ? resolveTemplateString(value, resolvedValues, fallback) : value,
27
27
  ]),
28
28
  );
29
29
  }
@@ -45,7 +45,9 @@ function buildLocalEnvironment(mcpDef, resolvedValues) {
45
45
  }
46
46
 
47
47
  function buildRemoteHeaders(mcpDef, resolvedValues) {
48
- return stripUnresolvedValues(resolveTemplateObject(mcpDef.headers ?? {}, resolvedValues));
48
+ return stripUnresolvedValues(
49
+ resolveTemplateObject(mcpDef.headers ?? {}, resolvedValues, (name) => `{env:${name}}`),
50
+ );
49
51
  }
50
52
 
51
53
  function withMemoryDefaults(id, values) {
@@ -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 }) ?? [])