@geraldmaron/construct 1.1.0 → 1.2.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 (425) hide show
  1. package/README.md +30 -18
  2. package/bin/construct +392 -91
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +112 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +77 -10
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +232 -11
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +703 -0
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +302 -0
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +4 -2
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/doctor/watchers/service-health.mjs +1 -1
  124. package/lib/document-export.mjs +360 -36
  125. package/lib/document-extract.mjs +88 -0
  126. package/lib/document-ingest.mjs +32 -18
  127. package/lib/embed/cli.mjs +2 -2
  128. package/lib/embed/daemon.mjs +18 -46
  129. package/lib/embed/semantic.mjs +5 -2
  130. package/lib/embed/worker.mjs +2 -2
  131. package/lib/embedded-contract/model-resolve.mjs +10 -9
  132. package/lib/env-config.mjs +26 -0
  133. package/lib/evals/dataset.mjs +137 -0
  134. package/lib/evals/gates.mjs +175 -0
  135. package/lib/graph/build-co-change.mjs +54 -0
  136. package/lib/graph/build-from-registry.mjs +136 -0
  137. package/lib/graph/build-import-graph.mjs +153 -0
  138. package/lib/graph/cli.mjs +110 -0
  139. package/lib/graph/impact-cli.mjs +89 -0
  140. package/lib/graph/impact.mjs +108 -0
  141. package/lib/graph/staleness.mjs +44 -0
  142. package/lib/graph/store.mjs +172 -0
  143. package/lib/handoffs/cleanup.mjs +1 -1
  144. package/lib/health-check.mjs +90 -76
  145. package/lib/hooks/artifact-release-gate.mjs +43 -0
  146. package/lib/hooks/audit-trail.mjs +14 -28
  147. package/lib/hooks/brand-prose-lint.mjs +38 -0
  148. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  149. package/lib/hooks/session-optimize.mjs +84 -207
  150. package/lib/hooks/session-start.mjs +4 -5
  151. package/lib/host-disposition.mjs +7 -0
  152. package/lib/improvement/cli.mjs +189 -0
  153. package/lib/improvement/controller.mjs +137 -0
  154. package/lib/improvement/proposal.mjs +120 -0
  155. package/lib/improvement/specialist-loop.mjs +192 -0
  156. package/lib/improvement/store.mjs +89 -0
  157. package/lib/improvement/surface.mjs +219 -0
  158. package/lib/ingest-tooling.mjs +97 -0
  159. package/lib/init/doc-lanes.mjs +165 -0
  160. package/lib/init-docs.mjs +31 -178
  161. package/lib/init-unified.mjs +27 -97
  162. package/lib/init-update-guide.mjs +102 -0
  163. package/lib/init-update.mjs +32 -1
  164. package/lib/init.mjs +8 -0
  165. package/lib/install/desktop-binary-download.mjs +85 -0
  166. package/lib/install/legacy-global-cleanup.mjs +189 -0
  167. package/lib/intake/daemon.mjs +2 -0
  168. package/lib/intake/git-queue.mjs +9 -8
  169. package/lib/intake/queue.mjs +2 -1
  170. package/lib/intake/session-prelude.mjs +90 -1
  171. package/lib/libreoffice-export.mjs +97 -0
  172. package/lib/logging/rotate.mjs +9 -0
  173. package/lib/maintenance/docker-reclaim.mjs +206 -0
  174. package/lib/mcp/external-schema-cost.mjs +74 -0
  175. package/lib/mcp/server.mjs +84 -9
  176. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  177. package/lib/mcp/tool-budget.mjs +55 -4
  178. package/lib/mcp/tools/document.mjs +18 -4
  179. package/lib/mcp/tools/skills.mjs +32 -3
  180. package/lib/mcp/tools/workflow.mjs +13 -1
  181. package/lib/model-free-selector.mjs +18 -0
  182. package/lib/model-registry.mjs +13 -229
  183. package/lib/model-router.mjs +425 -93
  184. package/lib/models/behavior-matrix.mjs +289 -0
  185. package/lib/models/catalog.mjs +209 -0
  186. package/lib/models/execution-capability-profile.mjs +196 -0
  187. package/lib/models/execution-policy.mjs +307 -0
  188. package/lib/models/provider-poll.mjs +383 -0
  189. package/lib/npm-spawn-env.mjs +17 -0
  190. package/lib/ollama/installed-models.mjs +129 -0
  191. package/lib/oracle/actions.mjs +187 -0
  192. package/lib/oracle/artifact-gate.mjs +99 -0
  193. package/lib/oracle/cli.mjs +204 -0
  194. package/lib/oracle/daemon-entry.mjs +14 -0
  195. package/lib/oracle/dispatch.mjs +81 -0
  196. package/lib/oracle/execute.mjs +143 -0
  197. package/lib/oracle/gaps.mjs +76 -0
  198. package/lib/oracle/index.mjs +84 -0
  199. package/lib/oracle/issues.mjs +164 -0
  200. package/lib/oracle/org-graph.mjs +170 -0
  201. package/lib/oracle/policy.mjs +80 -0
  202. package/lib/oracle/read-model.mjs +398 -0
  203. package/lib/oracle/reconcile.mjs +191 -0
  204. package/lib/oracle/routing.mjs +89 -0
  205. package/lib/oracle/synthesize.mjs +384 -0
  206. package/lib/oracle/verdicts.mjs +51 -0
  207. package/lib/orchestration/worker.mjs +88 -27
  208. package/lib/orchestration-policy.mjs +50 -0
  209. package/lib/parity.mjs +96 -2
  210. package/lib/persona-sections.mjs +66 -0
  211. package/lib/playwright-demo.mjs +252 -0
  212. package/lib/project-init-shared.mjs +4 -1
  213. package/lib/prompt-composer.js +9 -4
  214. package/lib/prompt-validation-contract.mjs +24 -0
  215. package/lib/provider-capabilities.js +57 -11
  216. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  217. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  218. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  219. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  220. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  221. package/lib/providers/contract/contract-tests.mjs +57 -0
  222. package/lib/providers/contract/errors.mjs +48 -0
  223. package/lib/providers/contract/interface.mjs +50 -0
  224. package/lib/providers/contract/registry.mjs +102 -0
  225. package/lib/providers/copilot-auth.mjs +297 -0
  226. package/lib/providers/credential-bootstrap.mjs +178 -0
  227. package/lib/providers/credential-catalog.mjs +46 -0
  228. package/lib/providers/credential-sources.mjs +63 -0
  229. package/lib/providers/creds.mjs +5 -2
  230. package/lib/providers/op-run.mjs +59 -0
  231. package/lib/providers/secret-resolver.mjs +159 -0
  232. package/lib/publish-template.mjs +163 -0
  233. package/lib/publish-tooling.mjs +119 -0
  234. package/lib/publish.mjs +305 -0
  235. package/lib/registry/cli.mjs +82 -0
  236. package/lib/registry/consolidation.mjs +147 -0
  237. package/lib/registry/generate-docs.mjs +75 -0
  238. package/lib/registry/skill-verification.mjs +57 -0
  239. package/lib/registry/surface-map.mjs +76 -0
  240. package/lib/registry/validate.mjs +135 -0
  241. package/lib/resources/budget.mjs +82 -0
  242. package/lib/resources/process-budget.mjs +45 -0
  243. package/lib/rules-delivery.mjs +9 -2
  244. package/lib/rules-read.mjs +26 -0
  245. package/lib/runtime-env.mjs +23 -0
  246. package/lib/runtime-pressure.mjs +51 -7
  247. package/lib/schema-infer.mjs +13 -2
  248. package/lib/server/chat-loop.mjs +622 -0
  249. package/lib/server/demo-preview.mjs +63 -0
  250. package/lib/server/index.mjs +229 -8
  251. package/lib/server/langfuse-login.mjs +3 -3
  252. package/lib/service-manager.mjs +61 -9
  253. package/lib/session-store.mjs +1 -1
  254. package/lib/setup.mjs +102 -2
  255. package/lib/specialists/prompt-schema.mjs +19 -10
  256. package/lib/specialists/roster.mjs +43 -0
  257. package/lib/specialists/scaffold.mjs +56 -0
  258. package/lib/storage/backend.mjs +6 -6
  259. package/lib/storage/embeddings-local.mjs +6 -3
  260. package/lib/storage/file-lock.mjs +18 -11
  261. package/lib/storage/hybrid-query.mjs +7 -4
  262. package/lib/storage/state-source.mjs +5 -5
  263. package/lib/storage/sync.mjs +2 -3
  264. package/lib/telemetry/rule-calls.mjs +52 -0
  265. package/lib/template-registry.mjs +2 -2
  266. package/lib/templates/visual-requirements.mjs +27 -51
  267. package/lib/test-corpus-inventory.mjs +313 -0
  268. package/lib/uninstall/uninstall.mjs +19 -7
  269. package/lib/update.mjs +12 -0
  270. package/lib/upgrade.mjs +14 -0
  271. package/lib/wireframe.mjs +20 -14
  272. package/lib/worker/run.mjs +17 -4
  273. package/lib/worker/trace.mjs +11 -3
  274. package/package.json +29 -13
  275. package/personas/construct.md +13 -13
  276. package/platforms/claude/settings.template.json +36 -0
  277. package/rules/common/patterns.md +1 -1
  278. package/rules/common/release-gates.md +4 -3
  279. package/scripts/sync-specialists.mjs +187 -60
  280. package/skills/devops/data-engineering.md +1 -1
  281. package/skills/docs/adr-workflow.md +1 -0
  282. package/skills/docs/backlog-proposal-workflow.md +1 -0
  283. package/skills/docs/codebase-research-workflow.md +40 -0
  284. package/skills/docs/customer-profile-workflow.md +1 -0
  285. package/skills/docs/document-ingest-workflow.md +1 -0
  286. package/skills/docs/evidence-ingest-workflow.md +1 -0
  287. package/skills/docs/init-docs.md +2 -2
  288. package/skills/docs/init-project.md +8 -3
  289. package/skills/docs/prd-workflow.md +24 -1
  290. package/skills/docs/prfaq-workflow.md +1 -0
  291. package/skills/docs/product-intelligence-workflow.md +1 -0
  292. package/skills/docs/product-signal-workflow.md +1 -0
  293. package/skills/docs/research-workflow.md +54 -37
  294. package/skills/docs/runbook-workflow.md +1 -0
  295. package/skills/docs/strategy-workflow.md +1 -0
  296. package/skills/docs/user-research-workflow.md +40 -0
  297. package/skills/operating/orchestration-reference.md +1 -1
  298. package/skills/roles/architect.md +5 -0
  299. package/skills/roles/operator.docs.md +4 -0
  300. package/skills/routing.md +4 -2
  301. package/specialists/artifact-manifest.json +480 -0
  302. package/specialists/artifact-manifest.schema.json +53 -0
  303. package/specialists/audit-enrichments.json +454 -0
  304. package/specialists/contracts.json +37 -25
  305. package/specialists/prompts/_shared/validation-contract.md +26 -0
  306. package/specialists/prompts/cx-accessibility.md +21 -2
  307. package/specialists/prompts/cx-ai-engineer.md +24 -1
  308. package/specialists/prompts/cx-architect.md +4 -1
  309. package/specialists/prompts/cx-business-strategist.md +23 -2
  310. package/specialists/prompts/cx-data-analyst.md +22 -1
  311. package/specialists/prompts/cx-data-engineer.md +23 -2
  312. package/specialists/prompts/cx-debugger.md +21 -2
  313. package/specialists/prompts/cx-designer.md +26 -3
  314. package/specialists/prompts/cx-devil-advocate.md +19 -2
  315. package/specialists/prompts/cx-docs-keeper.md +28 -1
  316. package/specialists/prompts/cx-engineer.md +22 -1
  317. package/specialists/prompts/cx-evaluator.md +18 -1
  318. package/specialists/prompts/cx-explorer.md +21 -2
  319. package/specialists/prompts/cx-legal-compliance.md +21 -2
  320. package/specialists/prompts/cx-operations.md +21 -2
  321. package/specialists/prompts/cx-oracle.md +94 -0
  322. package/specialists/prompts/cx-orchestrator.md +20 -1
  323. package/specialists/prompts/cx-platform-engineer.md +25 -2
  324. package/specialists/prompts/cx-product-manager.md +32 -1
  325. package/specialists/prompts/cx-qa.md +24 -2
  326. package/specialists/prompts/cx-rd-lead.md +23 -2
  327. package/specialists/prompts/cx-release-manager.md +23 -2
  328. package/specialists/prompts/cx-researcher.md +22 -2
  329. package/specialists/prompts/cx-reviewer.md +18 -1
  330. package/specialists/prompts/cx-security.md +22 -2
  331. package/specialists/prompts/cx-sre.md +30 -3
  332. package/specialists/prompts/cx-test-automation.md +7 -1
  333. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  334. package/specialists/prompts/cx-ux-researcher.md +21 -2
  335. package/specialists/registry.json +52 -173
  336. package/specialists/tone-profiles.json +42 -0
  337. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  338. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  339. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  340. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  341. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  342. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  343. package/templates/demos/vhs/construct-cockpit.json +24 -0
  344. package/templates/distribution/construct-brand.typ +446 -0
  345. package/templates/distribution/construct-decision.typ +38 -0
  346. package/templates/distribution/construct-deck.html +95 -0
  347. package/templates/distribution/construct-pdf.typ +38 -0
  348. package/templates/distribution/construct-prd.typ +38 -0
  349. package/templates/distribution/construct-research.typ +38 -0
  350. package/templates/distribution/construct-web.html +92 -0
  351. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  352. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  353. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  354. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  355. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  356. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  357. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  358. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  359. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  360. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  361. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  362. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  363. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  364. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  365. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  366. package/templates/distribution/fonts/README.md +36 -0
  367. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  368. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  369. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  370. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  371. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  372. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  373. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  374. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  375. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  376. package/templates/distribution/icons/activity.svg +15 -0
  377. package/templates/distribution/icons/alert-triangle.svg +17 -0
  378. package/templates/distribution/icons/book-open.svg +16 -0
  379. package/templates/distribution/icons/bot.svg +20 -0
  380. package/templates/distribution/icons/brain.svg +22 -0
  381. package/templates/distribution/icons/circle-check.svg +16 -0
  382. package/templates/distribution/icons/clipboard-check.svg +17 -0
  383. package/templates/distribution/icons/cpu.svg +28 -0
  384. package/templates/distribution/icons/database.svg +17 -0
  385. package/templates/distribution/icons/eye.svg +16 -0
  386. package/templates/distribution/icons/file-text.svg +19 -0
  387. package/templates/distribution/icons/gauge.svg +16 -0
  388. package/templates/distribution/icons/git-branch.svg +17 -0
  389. package/templates/distribution/icons/key.svg +17 -0
  390. package/templates/distribution/icons/layers.svg +17 -0
  391. package/templates/distribution/icons/list-checks.svg +19 -0
  392. package/templates/distribution/icons/lock.svg +16 -0
  393. package/templates/distribution/icons/message-square.svg +15 -0
  394. package/templates/distribution/icons/network.svg +19 -0
  395. package/templates/distribution/icons/route.svg +17 -0
  396. package/templates/distribution/icons/scale.svg +19 -0
  397. package/templates/distribution/icons/search.svg +16 -0
  398. package/templates/distribution/icons/send.svg +16 -0
  399. package/templates/distribution/icons/server.svg +18 -0
  400. package/templates/distribution/icons/shield-check.svg +16 -0
  401. package/templates/distribution/icons/users.svg +18 -0
  402. package/templates/distribution/icons/webhook.svg +17 -0
  403. package/templates/distribution/icons/workflow.svg +17 -0
  404. package/templates/distribution/icons/wrench.svg +15 -0
  405. package/templates/distribution/run.mjs +18 -1
  406. package/templates/docs/adr.md +7 -0
  407. package/templates/docs/construct_guide.md +10 -10
  408. package/templates/docs/customer-profile.md +4 -0
  409. package/templates/docs/one-pager.md +4 -0
  410. package/templates/docs/postmortem.md +31 -0
  411. package/templates/docs/prd-platform.md +19 -0
  412. package/templates/docs/prd.md +15 -0
  413. package/templates/docs/prfaq.md +7 -0
  414. package/templates/docs/qa-strategy.md +103 -0
  415. package/templates/docs/research-brief.md +8 -0
  416. package/templates/docs/rfc-platform.md +12 -0
  417. package/templates/docs/test-plan.md +7 -0
  418. package/lib/ingest/chunker.mjs +0 -94
  419. package/lib/ingest/pipeline.mjs +0 -53
  420. package/lib/ingest/store.mjs +0 -82
  421. package/lib/mode-commands.mjs +0 -122
  422. package/lib/policy/unified-gates.mjs +0 -96
  423. package/lib/profiles/validate-custom.mjs +0 -114
  424. package/lib/services/telemetry-backend.mjs +0 -177
  425. package/lib/storage/fusion.mjs +0 -95
@@ -24,7 +24,11 @@
24
24
  import fs from "node:fs";
25
25
  import path from "node:path";
26
26
  import { spawnSync } from "child_process";
27
+ import { hasAnySecret } from "./providers/secret-resolver.mjs";
28
+ import { isOllamaModelInstalled, toOllamaNativeModelId } from "./ollama/installed-models.mjs";
29
+ import { readRawFromOpenCodeProvider } from "./providers/credential-sources.mjs";
27
30
  import { findOpenCodeConfigPath } from "./opencode-config.mjs";
31
+ import { isLocalModel } from "./mcp/tool-budget.mjs";
28
32
  import {
29
33
  isFreeModel,
30
34
  pollFreeModels,
@@ -32,6 +36,11 @@ import {
32
36
  selectForTier,
33
37
  topForTier,
34
38
  } from "./model-free-selector.mjs";
39
+ import {
40
+ applyModelVisibilityFilter,
41
+ loadModelsCatalogContext,
42
+ mergeLiveModelsIntoProviders,
43
+ } from "./models/catalog.mjs";
35
44
 
36
45
  function uniqueStrings(values = []) {
37
46
  return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
@@ -79,15 +88,30 @@ function normalizeModelOperatingProfile(value) {
79
88
  return MODEL_OPERATING_PROFILES[normalized] ? normalized : null;
80
89
  }
81
90
 
91
+ function parseModelSizeB(model) {
92
+ const size = String(model || "").toLowerCase().match(/(?:[:/-])(\d+(?:\.\d+)?)b\b/);
93
+ return size ? parseFloat(size[1]) : null;
94
+ }
95
+
82
96
  function inferSmallModelProfile(selectedModel) {
83
97
  const model = String(selectedModel || '').toLowerCase();
84
98
  if (!model) return false;
85
- if (/^(ollama|local)\//.test(model) && /(?:[:/-])(3b|7b|8b|13b|14b|32b)\b/.test(model)) return true;
99
+ // Match any parameter-count marker (7b, 24b, 30b, 32b, …) rather than a fixed list —
100
+ // the old list silently missed 24b/30b, so Devstral-24B and qwen3-coder-30B resolved
101
+ // to the balanced profile. Treat <=34B local models as small.
102
+ if (/^(ollama|local)\//.test(model)) {
103
+ const size = parseModelSizeB(model);
104
+ if (size !== null && size <= 34) return true;
105
+ }
86
106
  if (/^(anthropic|openrouter\/anthropic)\/.*haiku/.test(model)) return true;
87
107
  if (/gpt-5\.1-mini|gemma-3|gemma-4|phi3:mini/.test(model)) return true;
88
108
  return false;
89
109
  }
90
110
 
111
+ // Temporary compatibility fallback (construct-6zga.1.8): the ExecutionCapabilityProfile
112
+ // records this profile id as source 'compatibility_fallback'; specialist composition
113
+ // consumes the profile, not this function directly.
114
+
91
115
  export function resolveModelOperatingProfile({
92
116
  envValues = {},
93
117
  selectedModel = null,
@@ -100,6 +124,49 @@ export function resolveModelOperatingProfile({
100
124
  return MODEL_OPERATING_PROFILES.balanced;
101
125
  }
102
126
 
127
+ // Small local models follow large multi-instruction prompts poorly; the binding
128
+ // constraint is instruction-following capacity, not the token window (which the cx32k
129
+ // Modelfile variants already widen). Map a model to the persona section tier it can
130
+ // comply with — floor (must-keep only), mid, or full. A COLLAPSED probe verdict forces
131
+ // floor; otherwise local size estimates the tier and any cloud model gets the full
132
+ // persona, so cloud configs are never slimmed.
133
+ //
134
+ // Temporary compatibility fallback (construct-6zga.1.8): this name/size heuristic is
135
+ // the fallback the ExecutionCapabilityProfile records as source 'compatibility_fallback'
136
+ // when no measured evidence exists. Consumers read the profile, not this directly.
137
+
138
+ export function resolveCapabilityTier({ model, verdict = null } = {}) {
139
+ if (!isLocalModel(model)) return 'full';
140
+ if (verdict === 'COLLAPSED') return 'floor';
141
+ const size = parseModelSizeB(model);
142
+ if (size === null) return 'floor';
143
+ if (size >= 24) return 'mid';
144
+ return 'floor';
145
+ }
146
+
147
+ const CODE_MODEL_RE = /coder|codellama|starcoder|deepseek-coder|devstral/i;
148
+
149
+ // Pick the model for the narrow local editor (construct-local) from the DECLARED local
150
+ // inventory rather than the generic fast-tier default (which, for an Ollama family,
151
+ // resolves to a non-code generalist like llama3.2:3b). The editor does bounded code
152
+ // edits, so prefer a code-specialized model in the reliable size band [7,34]B (smallest =
153
+ // cheapest competent editor), then the smallest code model, then the smallest local model
154
+ // of any kind. Candidates should already exclude probe-COLLAPSED models. Returns null only
155
+ // when there are no candidates, so the caller keeps its own fallback.
156
+
157
+ export function selectLocalEditorModel(candidates = []) {
158
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
159
+ const sized = candidates.map((m) => ({ m, size: parseModelSizeB(m) }));
160
+ const pick = (arr) => {
161
+ const band = arr.filter((x) => x.size !== null && x.size >= 7 && x.size <= 34).sort((a, b) => a.size - b.size);
162
+ if (band.length) return band[0].m;
163
+ const anySized = arr.filter((x) => x.size !== null).sort((a, b) => a.size - b.size);
164
+ return anySized.length ? anySized[0].m : arr[0].m;
165
+ };
166
+ const coders = sized.filter((x) => CODE_MODEL_RE.test(x.m));
167
+ return coders.length ? pick(coders) : pick(sized);
168
+ }
169
+
103
170
  /**
104
171
  * Provider-family definitions. Each entry contains:
105
172
  * - `test`: RegExp that matches provider URLs.
@@ -145,12 +212,12 @@ const PROVIDER_FAMILY_TIERS = [
145
212
  resolve: ({ reasoning, standard, fast }) => ({
146
213
  reasoning: reasoning ?? "openrouter/google/gemini-2.5-pro",
147
214
  standard: standard ?? "openrouter/google/gemini-2.0-flash-001",
148
- fast: fast ?? "openrouter/google/gemma-3-27b-it:free",
215
+ fast: fast ?? "openrouter/google/gemini-2.0-flash-001",
149
216
  }),
150
217
  options: {
151
218
  reasoning: ["openrouter/google/gemini-2.5-pro", "openrouter/google/gemini-2.5-flash"],
152
219
  standard: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemini-2.5-flash"],
153
- fast: ["openrouter/google/gemma-3-27b-it:free", "openrouter/google/gemini-2.0-flash-001"],
220
+ fast: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemma-4-26b-a4b-it:free"],
154
221
  },
155
222
  },
156
223
  {
@@ -188,14 +255,14 @@ const PROVIDER_FAMILY_TIERS = [
188
255
  label: 'GitHub Copilot',
189
256
  test: (modelId) => /^github-copilot\//.test(modelId),
190
257
  resolve: ({ reasoning, standard, fast }) => ({
191
- reasoning: reasoning ?? "github-copilot/gpt-5.4",
192
- standard: standard ?? "github-copilot/gpt-5.1",
193
- fast: fast ?? "github-copilot/gpt-5.1-mini",
258
+ reasoning: reasoning ?? "github-copilot/gpt-5.5",
259
+ standard: standard ?? "github-copilot/gpt-5.4",
260
+ fast: fast ?? "github-copilot/gpt-5.4-mini",
194
261
  }),
195
262
  options: {
196
- reasoning: ["github-copilot/gpt-5.4", "github-copilot/gpt-5.1"],
197
- standard: ["github-copilot/gpt-5.1", "github-copilot/gpt-5.1-mini"],
198
- fast: ["github-copilot/gpt-5.1-mini", "github-copilot/gpt-5.1"],
263
+ reasoning: ["github-copilot/gpt-5.5", "github-copilot/gpt-5.4", "github-copilot/claude-opus-4.8"],
264
+ standard: ["github-copilot/gpt-5.4", "github-copilot/gpt-4o", "github-copilot/claude-sonnet-4.6"],
265
+ fast: ["github-copilot/gpt-5.4-mini", "github-copilot/gpt-4o-mini"],
199
266
  },
200
267
  },
201
268
  {
@@ -231,6 +298,21 @@ const PROVIDER_FAMILY_TIERS = [
231
298
  fast: ["openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/meta-llama/llama-3.3-70b-instruct"],
232
299
  },
233
300
  },
301
+ {
302
+ id: 'openrouter',
303
+ label: 'OpenRouter (general)',
304
+ test: (modelId) => /^openrouter\//.test(modelId),
305
+ resolve: ({ reasoning, standard, fast }) => ({
306
+ reasoning: reasoning ?? "openrouter/qwen/qwen3-coder",
307
+ standard: standard ?? "openrouter/qwen/qwen3-coder:free",
308
+ fast: fast ?? "openrouter/qwen/qwen3-coder:free",
309
+ }),
310
+ options: {
311
+ reasoning: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder", "openrouter/deepseek/deepseek-r1"],
312
+ standard: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/google/gemini-2.0-flash-001"],
313
+ fast: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/meta-llama/llama-3.3-70b-instruct:free"],
314
+ },
315
+ },
234
316
  {
235
317
  id: 'ollama',
236
318
  label: 'Ollama (local)',
@@ -284,15 +366,46 @@ const PROVIDER_ENV_MAP = {
284
366
  'github-copilot': ['GITHUB_TOKEN', 'GH_TOKEN'],
285
367
  'openai': ['OPENAI_API_KEY'],
286
368
  'openrouter-llama': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
369
+ 'openrouter': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
287
370
  'ollama': ['OLLAMA_BASE_URL', 'OLLAMA_HOST'],
288
371
  'local': ['LOCAL_LLM_BASE_URL'],
289
372
  };
290
373
 
374
+ // A stored device-flow Copilot credential — Construct's own auth store or the
375
+ // standard ~/.config/github-copilot store other tools share — means Copilot is
376
+ // usable without a gh CLI session. Detection stays sync, file-only, and never
377
+ // performs the token exchange; copilot-auth.mjs owns read/write/refresh.
378
+
379
+ function hasCopilotCredential() {
380
+ const home = process.env.HOME || process.env.USERPROFILE || '';
381
+ if (!home) return false;
382
+ const candidates = [
383
+ path.join(home, '.construct', 'auth', 'github-copilot.json'),
384
+ path.join(home, '.config', 'github-copilot', 'apps.json'),
385
+ path.join(home, '.config', 'github-copilot', 'hosts.json'),
386
+ ];
387
+ for (const file of candidates) {
388
+ try {
389
+ if (!fs.existsSync(file)) continue;
390
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
391
+ if (file.endsWith('github-copilot.json')) {
392
+ if (data && (data.oauth_token || data.token || data.refresh_token || data.refresh)) return true;
393
+ } else {
394
+ for (const entry of Object.values(data || {})) {
395
+ if (entry && (entry.oauth_token || entry.token)) return true;
396
+ }
397
+ }
398
+ } catch { /* unreadable or non-JSON is not a usable credential */ }
399
+ }
400
+ return false;
401
+ }
402
+
291
403
  /**
292
404
  * Check if a provider is configured by checking env vars and CLI tools.
293
- * Uses the passed env, then checks config.env, then checks gh CLI for Copilot.
405
+ * Uses the passed env, dotenv files, shell rc, then the Copilot credential store
406
+ * or gh CLI for Copilot. A stored op:// reference counts as configured.
294
407
  */
295
- function isProviderConfigured(familyId, env) {
408
+ function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
296
409
  const varNames = PROVIDER_ENV_MAP[familyId];
297
410
  if (!varNames?.length) return false;
298
411
 
@@ -308,71 +421,31 @@ function isProviderConfigured(familyId, env) {
308
421
  } catch { /* not available */ }
309
422
  }
310
423
 
311
- // Check passed env (usually process.env)
312
- const anyInEnv = varNames.some(name => {
313
- const val = env?.[name];
314
- return val && typeof val === 'string' && val.length > 0;
315
- });
316
- if (anyInEnv) return true;
424
+ // Any credential source counts — env, ~/.construct/config.env, ~/.env, the
425
+ // project .env, and shell rc exports — and a stored 1Password op:// reference
426
+ // counts as configured because the resolver reads it lazily at call time.
427
+ if (hasAnySecret(varNames, { env, allowAmbient })) return true;
317
428
 
318
- // Check construct config.env
319
- try {
320
- const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
321
- const cfgPath = path.join(homeDir, '.construct', 'config.env');
322
- if (fs.existsSync(cfgPath)) {
323
- const content = fs.readFileSync(cfgPath, 'utf8');
324
- const anyInConfig = varNames.some(name => {
325
- const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
326
- const m = content.match(re);
327
- return m && m[1] && m[1].length > 0;
328
- });
329
- if (anyInConfig) return true;
330
- }
331
- } catch { /* skip */ }
332
-
333
- // GitHub Copilot: detect via gh CLI auth status
429
+ // GitHub Copilot authenticates via OAuth rather than an API key: a stored
430
+ // device-flow credential, or a gh CLI session as a fallback, both count.
334
431
  if (familyId === 'github-copilot') {
432
+ if (hasCopilotCredential()) return true;
335
433
  try {
336
434
  const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 3000 });
337
435
  return r.status === 0;
338
436
  } catch { return false; }
339
437
  }
340
438
 
341
- // Check ~/.env
342
- try {
343
- const homeEnv = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.env');
344
- if (fs.existsSync(homeEnv)) {
345
- const content = fs.readFileSync(homeEnv, 'utf8');
346
- const anyInHomeEnv = varNames.some(name => {
347
- const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
348
- const m = content.match(re);
349
- return m && m[1] && m[1].length > 0;
350
- });
351
- if (anyInHomeEnv) return true;
352
- }
353
- } catch { /* skip */ }
354
-
355
- // Check shell rc files for op:// refs (1Password CLI integration)
356
- try {
357
- const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
358
- const shellFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'].map(f => path.join(homeDir, f));
359
- for (const rcPath of shellFiles) {
360
- if (!fs.existsSync(rcPath)) continue;
361
- const content = fs.readFileSync(rcPath, 'utf8');
362
- const anyInRc = varNames.some(name => {
363
- // Direct export (with or without command substitution)
364
- const re = new RegExp(`^\\s*export\\s+${name}=`, 'm');
365
- return re.test(content);
366
- });
367
- if (anyInRc) return true;
368
- }
369
- } catch { /* skip */ }
370
-
371
439
  return false;
372
440
  }
373
441
 
374
- export function getProviderModelCatalog({ env = process.env } = {}) {
375
- const providers = PROVIDER_FAMILY_TIERS.map((family) => {
442
+ export function getProviderModelCatalog({
443
+ env = process.env,
444
+ cwd = process.cwd(),
445
+ activeModelId = null,
446
+ registryModels: registryModelsOverride = null,
447
+ } = {}) {
448
+ const baseProviders = PROVIDER_FAMILY_TIERS.map((family) => {
376
449
  const tiers = family.resolve({});
377
450
  const options = {
378
451
  reasoning: uniqueStrings([...(family.options?.reasoning ?? []), tiers.reasoning]),
@@ -391,13 +464,195 @@ export function getProviderModelCatalog({ env = process.env } = {}) {
391
464
  };
392
465
  });
393
466
 
394
- const tierOptions = {
395
- reasoning: uniqueStrings(providers.flatMap((provider) => provider.options.reasoning)),
396
- standard: uniqueStrings(providers.flatMap((provider) => provider.options.standard)),
397
- fast: uniqueStrings(providers.flatMap((provider) => provider.options.fast)),
467
+ const { modelsConfig, registryModels, liveModels } = loadModelsCatalogContext({ cwd, env });
468
+ const mergedProviders = mergeLiveModelsIntoProviders(
469
+ baseProviders,
470
+ liveModels,
471
+ { maxLiveFree: modelsConfig.catalog.maxLiveFree },
472
+ );
473
+
474
+ const tierOptionsRaw = {
475
+ reasoning: uniqueStrings(mergedProviders.flatMap((provider) => provider.options.reasoning)),
476
+ standard: uniqueStrings(mergedProviders.flatMap((provider) => provider.options.standard)),
477
+ fast: uniqueStrings(mergedProviders.flatMap((provider) => provider.options.fast)),
398
478
  };
399
479
 
400
- return { providers, tierOptions };
480
+ return applyModelVisibilityFilter(
481
+ { providers: mergedProviders, tierOptions: tierOptionsRaw },
482
+ {
483
+ visibility: modelsConfig.visibility,
484
+ registryModels: registryModelsOverride ?? registryModels,
485
+ activeModelId,
486
+ },
487
+ );
488
+ }
489
+
490
+ // Provider families with open-ended model catalogs: credential presence is the
491
+ // gate; static option lists are hints, not exhaustive allowlists.
492
+
493
+ const LENIENT_MODEL_FAMILIES = new Set([
494
+ 'openrouter-anthropic',
495
+ 'openrouter-google',
496
+ 'openrouter-deepseek',
497
+ 'openrouter-qwen',
498
+ 'openrouter-llama',
499
+ 'openrouter',
500
+ 'openai',
501
+ 'anthropic',
502
+ 'ollama',
503
+ 'local',
504
+ ]);
505
+
506
+ /**
507
+ * Check whether a model id can be used on this machine right now: the provider
508
+ * family must match, credentials must be present (including op:// and Copilot
509
+ * auth-store), and for fixed-catalog families (e.g. github-copilot) the id must
510
+ * appear in the shipped option list or tier defaults — not a stale CX_MODEL pin.
511
+ */
512
+ export function isChatModelAvailable(modelId, { env = process.env, excludeFamilies = [] } = {}) {
513
+ if (!modelId || typeof modelId !== 'string') {
514
+ return { ok: false, reason: 'missing', modelId: modelId || null };
515
+ }
516
+ const family = matchProviderFamily(modelId);
517
+ if (!family) {
518
+ return { ok: false, reason: 'unknown_family', modelId };
519
+ }
520
+ if (excludeFamilies.includes(family.id)) {
521
+ return { ok: false, reason: 'excluded', modelId, provider: family.id };
522
+ }
523
+ if (!isProviderConfigured(family.id, env)) {
524
+ return { ok: false, reason: 'provider_not_configured', modelId, provider: family.id };
525
+ }
526
+ if (family.id === 'ollama') {
527
+ const nativeModel = toOllamaNativeModelId(modelId);
528
+ const installed = isOllamaModelInstalled(modelId, { env });
529
+ if (installed === true) {
530
+ return { ok: true, modelId, provider: family.id };
531
+ }
532
+ if (installed === false) {
533
+ return {
534
+ ok: false,
535
+ reason: 'model_not_pulled',
536
+ modelId,
537
+ provider: family.id,
538
+ nativeModel,
539
+ pullCommand: `ollama pull ${nativeModel}`,
540
+ };
541
+ }
542
+ return { ok: true, modelId, provider: family.id };
543
+ }
544
+ if (LENIENT_MODEL_FAMILIES.has(family.id)) {
545
+ return { ok: true, modelId, provider: family.id };
546
+ }
547
+ const { providers } = getProviderModelCatalog({ env });
548
+ const provider = providers.find((p) => p.id === family.id);
549
+ if (!provider) {
550
+ return { ok: false, reason: 'unknown_family', modelId };
551
+ }
552
+ const known = uniqueStrings([
553
+ ...(provider.options?.reasoning ?? []),
554
+ ...(provider.options?.standard ?? []),
555
+ ...(provider.options?.fast ?? []),
556
+ provider.tiers?.reasoning,
557
+ provider.tiers?.standard,
558
+ provider.tiers?.fast,
559
+ ]);
560
+ if (known.includes(modelId)) {
561
+ return { ok: true, modelId, provider: family.id };
562
+ }
563
+ return { ok: false, reason: 'model_not_available', modelId, provider: family.id };
564
+ }
565
+
566
+ function availabilityNotice(rejected) {
567
+ if (!rejected?.modelId) return null;
568
+ const label = rejected.modelId;
569
+ if (rejected.reason === 'provider_not_configured') {
570
+ const family = rejected.provider || '';
571
+ if (family.startsWith('openrouter')) {
572
+ return `Saved ${label} — OpenRouter not configured. Set OPENROUTER_API_KEY in ~/.construct/config.env or run \`construct creds set openrouter\`.`;
573
+ }
574
+ return `Saved ${label} — provider not configured.`;
575
+ }
576
+ if (rejected.reason === 'model_not_available') {
577
+ return `Pinned ${label} — not available on your account.`;
578
+ }
579
+ if (rejected.reason === 'model_not_pulled') {
580
+ const native = rejected.nativeModel || toOllamaNativeModelId(label);
581
+ return `Pinned ${label} — not installed locally. Run \`ollama pull ${native}\` or \`construct ollama pull ${native}\`.`;
582
+ }
583
+ if (rejected.reason === 'unknown_family') {
584
+ return `Pinned ${label} — unrecognized model id.`;
585
+ }
586
+ return `Pinned ${label} — unavailable.`;
587
+ }
588
+
589
+ function recommendTierModel(tier, { env = process.env, excludeFamilies = [] } = {}) {
590
+ const { providers } = getProviderModelCatalog({ env });
591
+ for (const provider of providers) {
592
+ if (!provider.configured || excludeFamilies.includes(provider.id)) continue;
593
+ const candidates = uniqueStrings([
594
+ provider.tiers?.[tier],
595
+ ...(provider.options?.[tier] ?? []),
596
+ ]);
597
+ for (const id of candidates) {
598
+ const check = isChatModelAvailable(id, { env, excludeFamilies });
599
+ if (check.ok) return { id, provider: provider.label, tier };
600
+ }
601
+ }
602
+ return null;
603
+ }
604
+
605
+ /**
606
+ * Resolve the chat model with validation: explicit request and CX_MODEL pins
607
+ * are preferences, not blind truth. Stale or unconfigured pins fall through to
608
+ * the next configured provider for the tier, with a human-readable notice.
609
+ */
610
+ export function resolveValidatedChatModel({ env = process.env, requested = null, excludeFamilies = [] } = {}) {
611
+ const rejections = [];
612
+
613
+ if (requested) {
614
+ const check = isChatModelAvailable(requested, { env, excludeFamilies });
615
+ if (check.ok) {
616
+ return { id: requested, source: 'explicit', notice: null, rejected: [] };
617
+ }
618
+ rejections.push(check);
619
+ }
620
+
621
+ const pinOrder = [
622
+ ['standard', env.CX_MODEL_STANDARD],
623
+ ['reasoning', env.CX_MODEL_REASONING],
624
+ ['fast', env.CX_MODEL_FAST],
625
+ ];
626
+ for (const [tier, pin] of pinOrder) {
627
+ if (!pin) continue;
628
+ const check = isChatModelAvailable(pin, { env, excludeFamilies });
629
+ if (check.ok) {
630
+ const notice = rejections.length
631
+ ? `${availabilityNotice(rejections[0])} Using ${pin}.`
632
+ : null;
633
+ return { id: pin, source: 'pin', tier, notice, rejected: rejections };
634
+ }
635
+ rejections.push(check);
636
+ }
637
+
638
+ const standard = recommendTierModel('standard', { env, excludeFamilies });
639
+ if (standard) {
640
+ const notice = rejections.length
641
+ ? `${availabilityNotice(rejections[0])} Using ${standard.id} (${standard.provider}).`
642
+ : null;
643
+ return { id: standard.id, source: 'recommended', tier: 'standard', notice, rejected: rejections };
644
+ }
645
+
646
+ const anyTier = recommendTierModel('reasoning', { env, excludeFamilies }) || recommendTierModel('fast', { env, excludeFamilies });
647
+ if (anyTier) {
648
+ const notice = rejections.length
649
+ ? `${availabilityNotice(rejections[0])} Using ${anyTier.id} (${anyTier.provider}).`
650
+ : null;
651
+ return { id: anyTier.id, source: 'recommended', tier: anyTier.tier, notice, rejected: rejections };
652
+ }
653
+
654
+ const notice = rejections.length ? availabilityNotice(rejections[0]) : null;
655
+ return { id: null, source: null, notice, rejected: rejections };
401
656
  }
402
657
 
403
658
  /**
@@ -422,13 +677,10 @@ export function resolveTiersForPrimary(primaryModelId) {
422
677
  // callers (embedded model resolution, capability discovery) stay synchronous
423
678
  // and deterministic, and never expose a key value, only its presence.
424
679
 
425
- function familyDescriptor(family, env) {
680
+ function familyDescriptor(family, env, { allowAmbient = true } = {}) {
426
681
  const requiresEnv = PROVIDER_ENV_MAP[family.id] || (Array.isArray(family.requiresEnv) ? family.requiresEnv : []);
427
682
  const local = family.local === true;
428
- const configured = local || requiresEnv.some((key) => {
429
- const value = env?.[key];
430
- return typeof value === "string" && value.length > 0;
431
- });
683
+ const configured = isProviderConfigured(family.id, env, { allowAmbient });
432
684
  return {
433
685
  id: family.id,
434
686
  label: family.label,
@@ -444,18 +696,18 @@ function familyDescriptor(family, env) {
444
696
  * confirm credentials and whether they are present. Returns null when no family
445
697
  * matches.
446
698
  */
447
- export function describeModelFamily(modelId, { env = process.env } = {}) {
699
+ export function describeModelFamily(modelId, { env = process.env, allowAmbient = true } = {}) {
448
700
  const family = matchProviderFamily(modelId);
449
701
  if (!family) return null;
450
- return familyDescriptor(family, env);
702
+ return familyDescriptor(family, env, { allowAmbient });
451
703
  }
452
704
 
453
705
  /**
454
706
  * Describe every provider family. Resolves a host-supplied provider id and
455
707
  * enumerates model availability for the embedded contract layer.
456
708
  */
457
- export function listModelFamilies({ env = process.env } = {}) {
458
- return PROVIDER_FAMILY_TIERS.map((family) => familyDescriptor(family, env));
709
+ export function listModelFamilies({ env = process.env, allowAmbient = true } = {}) {
710
+ return PROVIDER_FAMILY_TIERS.map((family) => familyDescriptor(family, env, { allowAmbient }));
459
711
  }
460
712
 
461
713
  /**
@@ -464,17 +716,7 @@ export function listModelFamilies({ env = process.env } = {}) {
464
716
  export function readOpenRouterApiKeyFromOpenCodeConfig(
465
717
  configPath = findOpenCodeConfigPath(),
466
718
  ) {
467
- try {
468
- if (!fs.existsSync(configPath)) return "";
469
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
470
- const auth = config?.provider?.openrouter?.options?.headers?.Authorization;
471
- if (typeof auth !== "string") return "";
472
- const value = auth.replace(/^Bearer\s+/i, "").trim();
473
- if (!value || value.includes("__OPENROUTER_API_KEY__")) return "";
474
- return value;
475
- } catch {
476
- return "";
477
- }
719
+ return readRawFromOpenCodeProvider('openrouter', configPath) || "";
478
720
  }
479
721
 
480
722
  // --- Internal helpers ---
@@ -520,9 +762,18 @@ function getRegistryDefaults(registryModels = {}) {
520
762
 
521
763
  function normalizeEnvAssignments(envValues = {}) {
522
764
  return {
523
- reasoning: envValues.reasoning ?? envValues.CX_MODEL_REASONING ?? null,
524
- standard: envValues.standard ?? envValues.CX_MODEL_STANDARD ?? null,
525
- fast: envValues.fast ?? envValues.CX_MODEL_FAST ?? null,
765
+ reasoning: envValues.reasoning
766
+ ?? envValues.CX_MODEL_REASONING
767
+ ?? envValues.CONSTRUCT_MODEL_REASONING
768
+ ?? null,
769
+ standard: envValues.standard
770
+ ?? envValues.CX_MODEL_STANDARD
771
+ ?? envValues.CONSTRUCT_MODEL_STANDARD
772
+ ?? null,
773
+ fast: envValues.fast
774
+ ?? envValues.CX_MODEL_FAST
775
+ ?? envValues.CONSTRUCT_MODEL_FAST
776
+ ?? null,
526
777
  };
527
778
  }
528
779
 
@@ -863,6 +1114,87 @@ export function setModelWithTierInference(envPath, tier, modelId, registryModels
863
1114
  return resolved;
864
1115
  }
865
1116
 
1117
+ export function resolveModelTiers(options = {}) {
1118
+ const {
1119
+ env = process.env,
1120
+ registryPath = null,
1121
+ strict = false,
1122
+ } = options;
1123
+
1124
+ let registryModels = {};
1125
+ if (registryPath && fs.existsSync(registryPath)) {
1126
+ try {
1127
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
1128
+ registryModels = registry.models ?? {};
1129
+ } catch (err) {
1130
+ return {
1131
+ models: { reasoning: null, standard: null, fast: null },
1132
+ sources: { reasoning: 'not configured', standard: 'not configured', fast: 'not configured' },
1133
+ configured: 0,
1134
+ complete: false,
1135
+ errors: [`Failed to read registry: ${err.message}`],
1136
+ };
1137
+ }
1138
+ }
1139
+
1140
+ const tiers = resolveTierAssignments(normalizeEnvAssignments(env), registryModels);
1141
+ const models = {};
1142
+ const sources = {};
1143
+ for (const tier of ['reasoning', 'standard', 'fast']) {
1144
+ models[tier] = tiers[tier].model;
1145
+ sources[tier] = tiers[tier].source;
1146
+ }
1147
+ const configured = Object.values(models).filter(Boolean).length;
1148
+ const errors = strict && configured < 3
1149
+ ? [`Missing configuration for tiers: ${['reasoning', 'standard', 'fast'].filter((t) => !models[t]).join(', ')}`]
1150
+ : null;
1151
+
1152
+ return {
1153
+ models,
1154
+ sources,
1155
+ configured,
1156
+ complete: configured === 3,
1157
+ errors,
1158
+ };
1159
+ }
1160
+
1161
+ export function getModelForTier(tier, options = {}) {
1162
+ return resolveModelTiers(options).models[tier];
1163
+ }
1164
+
1165
+ export function getModelSource(tier, options = {}) {
1166
+ return resolveModelTiers(options).sources[tier] || 'unknown';
1167
+ }
1168
+
1169
+ export function validateModelTiers(options = {}) {
1170
+ const resolved = resolveModelTiers({ ...options, strict: true });
1171
+ const unconfigured = Object.entries(resolved.sources)
1172
+ .filter(([, source]) => source === 'not configured')
1173
+ .map(([tier]) => tier);
1174
+
1175
+ return {
1176
+ valid: resolved.complete && !resolved.errors,
1177
+ errors: resolved.errors,
1178
+ warnings: unconfigured.length > 0
1179
+ ? [`Unconfigured tier${unconfigured.length === 1 ? '' : 's'}: ${unconfigured.join(', ')}. Run 'construct models --apply' or set CX_MODEL_<TIER>.`]
1180
+ : [],
1181
+ resolution: resolved,
1182
+ };
1183
+ }
1184
+
1185
+ export function formatModelStatus(options = {}) {
1186
+ const resolved = resolveModelTiers(options);
1187
+ let output = 'Model Configuration:\n\n';
1188
+ for (const tier of ['reasoning', 'standard', 'fast']) {
1189
+ const model = resolved.models[tier];
1190
+ const source = resolved.sources[tier];
1191
+ const icon = source === 'not configured' ? '!' : 'ok';
1192
+ output += `${icon} ${tier.padEnd(10)} ${model ?? '(not configured)'}\n`;
1193
+ output += ` Source: ${source}\n\n`;
1194
+ }
1195
+ return output;
1196
+ }
1197
+
866
1198
  export function readCurrentModels(envPath, registryModels = {}) {
867
1199
  const envValues = arguments.length > 2 ? arguments[2] : {};
868
1200
  const fileAssignments = readEnvAssignments(envPath);