@geraldmaron/construct 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/README.md +30 -19
  2. package/bin/construct +487 -87
  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 +258 -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/artifact-workflow.mjs +252 -0
  20. package/lib/audit-skills.mjs +4 -0
  21. package/lib/audit-specialists.mjs +285 -0
  22. package/lib/audit-trail.mjs +1 -1
  23. package/lib/auto-docs.mjs +90 -19
  24. package/lib/beads/drift.mjs +27 -6
  25. package/lib/beads-client.mjs +49 -121
  26. package/lib/beads-optimistic.mjs +5 -12
  27. package/lib/brand-fonts.mjs +93 -0
  28. package/lib/brand-prose.mjs +214 -0
  29. package/lib/brand-tokens.mjs +92 -0
  30. package/lib/bridges/copilot-proxy.mjs +13 -26
  31. package/lib/capability-ledger.mjs +156 -0
  32. package/lib/certification/artifact-fixtures.mjs +132 -0
  33. package/lib/certification/artifact-gates.mjs +63 -0
  34. package/lib/certification/artifact-provenance.mjs +97 -0
  35. package/lib/certification/canonical-scenarios.mjs +78 -0
  36. package/lib/certification/cli.mjs +191 -0
  37. package/lib/certification/dashboard-api.mjs +71 -0
  38. package/lib/certification/demo-parity.mjs +116 -0
  39. package/lib/certification/document-io-fixtures.mjs +246 -0
  40. package/lib/certification/document-workflow.mjs +97 -0
  41. package/lib/certification/eval-bridge.mjs +77 -0
  42. package/lib/certification/model-routing.mjs +149 -0
  43. package/lib/certification/prompt-budget.mjs +119 -0
  44. package/lib/certification/rc-gate.mjs +206 -0
  45. package/lib/certification/real-llm-scenarios.mjs +303 -0
  46. package/lib/certification/role-cards.mjs +113 -0
  47. package/lib/certification/role-overlays.mjs +117 -0
  48. package/lib/certification/run.mjs +122 -0
  49. package/lib/certification/runner.mjs +323 -0
  50. package/lib/certification/scenarios.mjs +51 -0
  51. package/lib/certification/skill-inventory.mjs +289 -0
  52. package/lib/certification/skill-scenarios.mjs +147 -0
  53. package/lib/certification/specialist-contracts.mjs +85 -0
  54. package/lib/certification/specialist-scenarios.mjs +175 -0
  55. package/lib/certification/stale-impact.mjs +146 -0
  56. package/lib/certification/status.mjs +252 -0
  57. package/lib/certification/store.mjs +77 -0
  58. package/lib/chat/cli.mjs +333 -0
  59. package/lib/chat/command-suggest.mjs +161 -0
  60. package/lib/chat/commands.mjs +215 -0
  61. package/lib/chat/config.mjs +142 -0
  62. package/lib/chat/context-compactor.mjs +250 -0
  63. package/lib/chat/context-continuation.mjs +253 -0
  64. package/lib/chat/continuation-source.mjs +58 -0
  65. package/lib/chat/demo-guide.mjs +61 -0
  66. package/lib/chat/design-tokens.mjs +91 -0
  67. package/lib/chat/desktop-binary.mjs +79 -0
  68. package/lib/chat/desktop-build.mjs +130 -0
  69. package/lib/chat/desktop-launcher.mjs +133 -0
  70. package/lib/chat/evidence.mjs +145 -0
  71. package/lib/chat/export.mjs +74 -0
  72. package/lib/chat/harness/driver.mjs +91 -0
  73. package/lib/chat/list-picker.mjs +112 -0
  74. package/lib/chat/model-picker.mjs +356 -0
  75. package/lib/chat/openrouter-fallback.mjs +151 -0
  76. package/lib/chat/permission-prompt.mjs +33 -0
  77. package/lib/chat/picker-catalog.mjs +45 -0
  78. package/lib/chat/policy-telemetry.mjs +34 -0
  79. package/lib/chat/present.mjs +246 -0
  80. package/lib/chat/session-context.mjs +39 -0
  81. package/lib/chat/session-persist.mjs +73 -0
  82. package/lib/chat/session-restore.mjs +71 -0
  83. package/lib/chat/session-settings.mjs +53 -0
  84. package/lib/chat/system-prompt.mjs +52 -0
  85. package/lib/chat/transparency.mjs +93 -0
  86. package/lib/chat/tui/color-scheme.mjs +42 -0
  87. package/lib/chat/tui/markdown.mjs +123 -0
  88. package/lib/chat/tui/presentation.mjs +100 -0
  89. package/lib/chat/tui/render.mjs +500 -0
  90. package/lib/chat/tui/turn-block.mjs +284 -0
  91. package/lib/chat/tui/turn-present.mjs +18 -0
  92. package/lib/chat/tui/turn-state.mjs +88 -0
  93. package/lib/chat/tui/usage.mjs +122 -0
  94. package/lib/chat/web-commands.mjs +146 -0
  95. package/lib/chat/web-launcher.mjs +63 -0
  96. package/lib/chat/web-picker-keys.mjs +46 -0
  97. package/lib/chat/web-session.mjs +159 -0
  98. package/lib/cli-commands.mjs +146 -15
  99. package/lib/cli-service-inventory.mjs +42 -0
  100. package/lib/comment-lint.mjs +6 -4
  101. package/lib/config/intake-policy.mjs +209 -0
  102. package/lib/config/project-config.mjs +11 -2
  103. package/lib/config/schema.mjs +86 -6
  104. package/lib/config/source-targets.mjs +311 -0
  105. package/lib/contract-schemas/decision.json +50 -0
  106. package/lib/contract-schemas/implementation.json +51 -0
  107. package/lib/contract-schemas/review-report.json +32 -0
  108. package/lib/contract-schemas/test-report.json +43 -0
  109. package/lib/contracts/construct-handoff.mjs +60 -0
  110. package/lib/contracts/validate.mjs +32 -3
  111. package/lib/contracts/violation-log.mjs +58 -5
  112. package/lib/dashboard-demo.mjs +71 -0
  113. package/lib/dashboard-static.mjs +19 -5
  114. package/lib/decisions/registry.mjs +5 -3
  115. package/lib/deck-export-pptx.mjs +1152 -0
  116. package/lib/demo-recording.mjs +142 -0
  117. package/lib/demo-script.mjs +114 -0
  118. package/lib/demo-surface.mjs +249 -0
  119. package/lib/demo.mjs +598 -140
  120. package/lib/diagram-export.mjs +192 -0
  121. package/lib/diagram.mjs +13 -11
  122. package/lib/docs-verify.mjs +25 -7
  123. package/lib/doctor/index.mjs +3 -1
  124. package/lib/doctor/project-adapters.mjs +44 -0
  125. package/lib/doctor/source-checkout.mjs +16 -0
  126. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  127. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  128. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  129. package/lib/document-export.mjs +384 -38
  130. package/lib/document-extract.mjs +44 -4
  131. package/lib/document-ingest.mjs +29 -20
  132. package/lib/embed/auto-sources.mjs +44 -0
  133. package/lib/embed/cli.mjs +4 -3
  134. package/lib/embed/daemon.mjs +18 -46
  135. package/lib/embed/demand-fetch.mjs +50 -19
  136. package/lib/embed/inbox.mjs +6 -10
  137. package/lib/embed/worker.mjs +4 -3
  138. package/lib/embedded-contract/index.mjs +11 -0
  139. package/lib/embedded-contract/model-resolve.mjs +10 -9
  140. package/lib/env-config.mjs +26 -0
  141. package/lib/evals/dataset.mjs +137 -0
  142. package/lib/evals/gates.mjs +175 -0
  143. package/lib/export-branding.mjs +32 -0
  144. package/lib/graph/build-co-change.mjs +54 -0
  145. package/lib/graph/build-from-registry.mjs +136 -0
  146. package/lib/graph/build-import-graph.mjs +153 -0
  147. package/lib/graph/cli.mjs +110 -0
  148. package/lib/graph/impact-cli.mjs +89 -0
  149. package/lib/graph/impact.mjs +108 -0
  150. package/lib/graph/staleness.mjs +44 -0
  151. package/lib/graph/store.mjs +172 -0
  152. package/lib/health-check.mjs +90 -76
  153. package/lib/hooks/artifact-release-gate.mjs +43 -0
  154. package/lib/hooks/brand-prose-lint.mjs +38 -0
  155. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  156. package/lib/hooks/session-optimize.mjs +84 -207
  157. package/lib/hooks/session-start.mjs +18 -16
  158. package/lib/host-disposition.mjs +7 -0
  159. package/lib/improvement/cli.mjs +189 -0
  160. package/lib/improvement/controller.mjs +137 -0
  161. package/lib/improvement/proposal.mjs +120 -0
  162. package/lib/improvement/specialist-loop.mjs +192 -0
  163. package/lib/improvement/store.mjs +89 -0
  164. package/lib/improvement/surface.mjs +219 -0
  165. package/lib/ingest-tooling.mjs +97 -0
  166. package/lib/init/doc-lanes.mjs +165 -0
  167. package/lib/init-docs.mjs +31 -178
  168. package/lib/init-unified.mjs +50 -119
  169. package/lib/init-update-guide.mjs +102 -0
  170. package/lib/init-update.mjs +32 -1
  171. package/lib/init.mjs +8 -0
  172. package/lib/install/desktop-binary-download.mjs +85 -0
  173. package/lib/install/legacy-global-cleanup.mjs +189 -0
  174. package/lib/intake/constants.mjs +29 -0
  175. package/lib/intake/daemon.mjs +2 -0
  176. package/lib/intake/git-queue.mjs +9 -8
  177. package/lib/intake/intake-config.mjs +52 -105
  178. package/lib/intake/legacy-paths.mjs +5 -0
  179. package/lib/intake/queue.mjs +2 -1
  180. package/lib/intake/session-prelude.mjs +90 -1
  181. package/lib/libreoffice-export.mjs +97 -0
  182. package/lib/logging/rotate.mjs +9 -0
  183. package/lib/maintenance/docker-reclaim.mjs +206 -0
  184. package/lib/mcp/external-schema-cost.mjs +74 -0
  185. package/lib/mcp/server.mjs +103 -10
  186. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  187. package/lib/mcp/tool-budget.mjs +55 -4
  188. package/lib/mcp/tools/document.mjs +18 -4
  189. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  190. package/lib/mcp/tools/skills.mjs +32 -3
  191. package/lib/mcp/tools/workflow.mjs +13 -1
  192. package/lib/model-free-selector.mjs +18 -0
  193. package/lib/model-registry.mjs +13 -229
  194. package/lib/model-router.mjs +373 -92
  195. package/lib/models/behavior-matrix.mjs +289 -0
  196. package/lib/models/catalog.mjs +209 -0
  197. package/lib/models/execution-capability-profile.mjs +196 -0
  198. package/lib/models/execution-policy.mjs +307 -0
  199. package/lib/models/provider-poll.mjs +383 -0
  200. package/lib/npm-spawn-env.mjs +17 -0
  201. package/lib/ollama/installed-models.mjs +129 -0
  202. package/lib/oracle/actions.mjs +187 -0
  203. package/lib/oracle/artifact-gate.mjs +99 -0
  204. package/lib/oracle/cli.mjs +204 -0
  205. package/lib/oracle/daemon-entry.mjs +14 -0
  206. package/lib/oracle/dispatch.mjs +81 -0
  207. package/lib/oracle/execute.mjs +143 -0
  208. package/lib/oracle/gaps.mjs +76 -0
  209. package/lib/oracle/index.mjs +84 -0
  210. package/lib/oracle/issues.mjs +164 -0
  211. package/lib/oracle/org-graph.mjs +170 -0
  212. package/lib/oracle/policy.mjs +80 -0
  213. package/lib/oracle/read-model.mjs +398 -0
  214. package/lib/oracle/reconcile.mjs +191 -0
  215. package/lib/oracle/routing.mjs +89 -0
  216. package/lib/oracle/synthesize.mjs +384 -0
  217. package/lib/oracle/verdicts.mjs +51 -0
  218. package/lib/orchestration/worker.mjs +88 -27
  219. package/lib/orchestration-policy.mjs +50 -0
  220. package/lib/parity.mjs +96 -2
  221. package/lib/playwright-demo.mjs +254 -0
  222. package/lib/project-init-shared.mjs +4 -1
  223. package/lib/prompt-composer.js +7 -3
  224. package/lib/prompt-validation-contract.mjs +24 -0
  225. package/lib/provider-capabilities.js +57 -11
  226. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  227. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  228. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  229. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  230. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  231. package/lib/providers/contract/contract-tests.mjs +57 -0
  232. package/lib/providers/contract/errors.mjs +48 -0
  233. package/lib/providers/contract/interface.mjs +50 -0
  234. package/lib/providers/contract/registry.mjs +102 -0
  235. package/lib/providers/copilot-auth.mjs +297 -0
  236. package/lib/providers/credential-bootstrap.mjs +178 -0
  237. package/lib/providers/credential-catalog.mjs +46 -0
  238. package/lib/providers/credential-sources.mjs +63 -0
  239. package/lib/providers/creds.mjs +5 -2
  240. package/lib/providers/op-run.mjs +59 -0
  241. package/lib/providers/secret-resolver.mjs +159 -0
  242. package/lib/publish-template.mjs +163 -0
  243. package/lib/publish-tooling.mjs +119 -0
  244. package/lib/publish.mjs +305 -0
  245. package/lib/registry/cli.mjs +82 -0
  246. package/lib/registry/consolidation.mjs +147 -0
  247. package/lib/registry/generate-docs.mjs +75 -0
  248. package/lib/registry/skill-verification.mjs +57 -0
  249. package/lib/registry/surface-map.mjs +76 -0
  250. package/lib/registry/validate.mjs +135 -0
  251. package/lib/resources/budget.mjs +82 -0
  252. package/lib/resources/process-budget.mjs +45 -0
  253. package/lib/rules-delivery.mjs +9 -2
  254. package/lib/rules-read.mjs +26 -0
  255. package/lib/runtime-env.mjs +23 -0
  256. package/lib/runtime-pressure.mjs +51 -7
  257. package/lib/schema-infer.mjs +13 -2
  258. package/lib/server/chat-loop.mjs +622 -0
  259. package/lib/server/demo-preview.mjs +63 -0
  260. package/lib/server/index.mjs +259 -9
  261. package/lib/server/rate-limit.mjs +5 -3
  262. package/lib/server/webhook.mjs +3 -2
  263. package/lib/service-manager.mjs +60 -8
  264. package/lib/setup.mjs +70 -7
  265. package/lib/specialists/prompt-schema.mjs +19 -10
  266. package/lib/specialists/roster.mjs +43 -0
  267. package/lib/specialists/scaffold.mjs +56 -0
  268. package/lib/storage/backend.mjs +6 -6
  269. package/lib/storage/hybrid-query.mjs +7 -4
  270. package/lib/storage/state-source.mjs +5 -5
  271. package/lib/storage/sync.mjs +2 -3
  272. package/lib/telemetry/rule-calls.mjs +52 -0
  273. package/lib/template-registry.mjs +2 -2
  274. package/lib/templates/visual-requirements.mjs +27 -51
  275. package/lib/test-corpus-inventory.mjs +313 -0
  276. package/lib/uninstall/uninstall.mjs +19 -7
  277. package/lib/update.mjs +12 -0
  278. package/lib/upgrade.mjs +14 -0
  279. package/lib/wireframe.mjs +20 -14
  280. package/lib/worker/run.mjs +17 -4
  281. package/lib/worker/trace.mjs +11 -3
  282. package/package.json +28 -14
  283. package/personas/construct.md +2 -2
  284. package/platforms/claude/settings.template.json +36 -0
  285. package/rules/common/patterns.md +1 -1
  286. package/rules/common/release-gates.md +4 -3
  287. package/scripts/sync-specialists.mjs +41 -28
  288. package/skills/devops/data-engineering.md +1 -1
  289. package/skills/docs/adr-workflow.md +1 -0
  290. package/skills/docs/backlog-proposal-workflow.md +1 -0
  291. package/skills/docs/codebase-research-workflow.md +40 -0
  292. package/skills/docs/customer-profile-workflow.md +1 -0
  293. package/skills/docs/document-ingest-workflow.md +1 -0
  294. package/skills/docs/evidence-ingest-workflow.md +1 -0
  295. package/skills/docs/init-docs.md +2 -2
  296. package/skills/docs/init-project.md +7 -2
  297. package/skills/docs/prd-workflow.md +24 -1
  298. package/skills/docs/prfaq-workflow.md +1 -0
  299. package/skills/docs/product-intelligence-workflow.md +1 -0
  300. package/skills/docs/product-signal-workflow.md +1 -0
  301. package/skills/docs/research-workflow.md +54 -37
  302. package/skills/docs/runbook-workflow.md +1 -0
  303. package/skills/docs/strategy-workflow.md +1 -0
  304. package/skills/docs/user-research-workflow.md +40 -0
  305. package/skills/operating/orchestration-reference.md +1 -1
  306. package/skills/roles/architect.md +5 -0
  307. package/skills/roles/operator.docs.md +4 -0
  308. package/skills/routing.md +4 -2
  309. package/specialists/artifact-manifest.json +489 -0
  310. package/specialists/artifact-manifest.schema.json +83 -0
  311. package/specialists/audit-enrichments.json +454 -0
  312. package/specialists/contracts.json +37 -25
  313. package/specialists/prompts/_shared/validation-contract.md +26 -0
  314. package/specialists/prompts/cx-accessibility.md +21 -2
  315. package/specialists/prompts/cx-ai-engineer.md +24 -1
  316. package/specialists/prompts/cx-architect.md +4 -1
  317. package/specialists/prompts/cx-business-strategist.md +23 -2
  318. package/specialists/prompts/cx-data-analyst.md +22 -1
  319. package/specialists/prompts/cx-data-engineer.md +23 -2
  320. package/specialists/prompts/cx-debugger.md +21 -2
  321. package/specialists/prompts/cx-designer.md +26 -3
  322. package/specialists/prompts/cx-devil-advocate.md +19 -2
  323. package/specialists/prompts/cx-docs-keeper.md +28 -1
  324. package/specialists/prompts/cx-engineer.md +22 -1
  325. package/specialists/prompts/cx-evaluator.md +18 -1
  326. package/specialists/prompts/cx-explorer.md +21 -2
  327. package/specialists/prompts/cx-legal-compliance.md +21 -2
  328. package/specialists/prompts/cx-operations.md +21 -2
  329. package/specialists/prompts/cx-oracle.md +94 -0
  330. package/specialists/prompts/cx-orchestrator.md +20 -1
  331. package/specialists/prompts/cx-platform-engineer.md +25 -2
  332. package/specialists/prompts/cx-product-manager.md +32 -1
  333. package/specialists/prompts/cx-qa.md +24 -2
  334. package/specialists/prompts/cx-rd-lead.md +23 -2
  335. package/specialists/prompts/cx-release-manager.md +23 -2
  336. package/specialists/prompts/cx-researcher.md +22 -2
  337. package/specialists/prompts/cx-reviewer.md +18 -1
  338. package/specialists/prompts/cx-security.md +22 -2
  339. package/specialists/prompts/cx-sre.md +30 -3
  340. package/specialists/prompts/cx-test-automation.md +7 -1
  341. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  342. package/specialists/prompts/cx-ux-researcher.md +21 -2
  343. package/specialists/registry.json +52 -173
  344. package/specialists/tone-profiles.json +42 -0
  345. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  346. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  347. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  348. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  349. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  350. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  351. package/templates/demos/vhs/construct-cockpit.json +24 -0
  352. package/templates/distribution/construct-brand.typ +446 -0
  353. package/templates/distribution/construct-decision.typ +38 -0
  354. package/templates/distribution/construct-deck.html +95 -0
  355. package/templates/distribution/construct-pdf.typ +38 -0
  356. package/templates/distribution/construct-prd.typ +38 -0
  357. package/templates/distribution/construct-reference.docx +0 -0
  358. package/templates/distribution/construct-research.typ +38 -0
  359. package/templates/distribution/construct-web.html +92 -0
  360. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  361. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  362. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  363. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  364. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  365. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  366. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  367. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  368. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  369. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  370. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  371. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  372. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  373. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  374. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  375. package/templates/distribution/fonts/README.md +36 -0
  376. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  377. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  378. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  379. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  380. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  381. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  382. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  383. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  384. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  385. package/templates/distribution/icons/activity.svg +15 -0
  386. package/templates/distribution/icons/alert-triangle.svg +17 -0
  387. package/templates/distribution/icons/book-open.svg +16 -0
  388. package/templates/distribution/icons/bot.svg +20 -0
  389. package/templates/distribution/icons/brain.svg +22 -0
  390. package/templates/distribution/icons/circle-check.svg +16 -0
  391. package/templates/distribution/icons/clipboard-check.svg +17 -0
  392. package/templates/distribution/icons/cpu.svg +28 -0
  393. package/templates/distribution/icons/database.svg +17 -0
  394. package/templates/distribution/icons/eye.svg +16 -0
  395. package/templates/distribution/icons/file-text.svg +19 -0
  396. package/templates/distribution/icons/gauge.svg +16 -0
  397. package/templates/distribution/icons/git-branch.svg +17 -0
  398. package/templates/distribution/icons/key.svg +17 -0
  399. package/templates/distribution/icons/layers.svg +17 -0
  400. package/templates/distribution/icons/list-checks.svg +19 -0
  401. package/templates/distribution/icons/lock.svg +16 -0
  402. package/templates/distribution/icons/message-square.svg +15 -0
  403. package/templates/distribution/icons/network.svg +19 -0
  404. package/templates/distribution/icons/route.svg +17 -0
  405. package/templates/distribution/icons/scale.svg +19 -0
  406. package/templates/distribution/icons/search.svg +16 -0
  407. package/templates/distribution/icons/send.svg +16 -0
  408. package/templates/distribution/icons/server.svg +18 -0
  409. package/templates/distribution/icons/shield-check.svg +16 -0
  410. package/templates/distribution/icons/users.svg +18 -0
  411. package/templates/distribution/icons/webhook.svg +17 -0
  412. package/templates/distribution/icons/workflow.svg +17 -0
  413. package/templates/distribution/icons/wrench.svg +15 -0
  414. package/templates/distribution/run.mjs +18 -1
  415. package/templates/docs/adr.md +7 -0
  416. package/templates/docs/construct_guide.md +8 -8
  417. package/templates/docs/customer-profile.md +4 -0
  418. package/templates/docs/one-pager.md +4 -0
  419. package/templates/docs/postmortem.md +31 -0
  420. package/templates/docs/prd-platform.md +19 -0
  421. package/templates/docs/prd.md +15 -0
  422. package/templates/docs/prfaq.md +7 -0
  423. package/templates/docs/qa-strategy.md +103 -0
  424. package/templates/docs/research-brief.md +8 -0
  425. package/templates/docs/rfc-platform.md +12 -0
  426. package/templates/docs/test-plan.md +7 -0
@@ -24,6 +24,9 @@
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";
28
31
  import { isLocalModel } from "./mcp/tool-budget.mjs";
29
32
  import {
@@ -33,6 +36,11 @@ import {
33
36
  selectForTier,
34
37
  topForTier,
35
38
  } from "./model-free-selector.mjs";
39
+ import {
40
+ applyModelVisibilityFilter,
41
+ loadModelsCatalogContext,
42
+ mergeLiveModelsIntoProviders,
43
+ } from "./models/catalog.mjs";
36
44
 
37
45
  function uniqueStrings(values = []) {
38
46
  return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
@@ -100,6 +108,10 @@ function inferSmallModelProfile(selectedModel) {
100
108
  return false;
101
109
  }
102
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
+
103
115
  export function resolveModelOperatingProfile({
104
116
  envValues = {},
105
117
  selectedModel = null,
@@ -118,6 +130,10 @@ export function resolveModelOperatingProfile({
118
130
  // comply with — floor (must-keep only), mid, or full. A COLLAPSED probe verdict forces
119
131
  // floor; otherwise local size estimates the tier and any cloud model gets the full
120
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.
121
137
 
122
138
  export function resolveCapabilityTier({ model, verdict = null } = {}) {
123
139
  if (!isLocalModel(model)) return 'full';
@@ -196,12 +212,12 @@ const PROVIDER_FAMILY_TIERS = [
196
212
  resolve: ({ reasoning, standard, fast }) => ({
197
213
  reasoning: reasoning ?? "openrouter/google/gemini-2.5-pro",
198
214
  standard: standard ?? "openrouter/google/gemini-2.0-flash-001",
199
- fast: fast ?? "openrouter/google/gemma-3-27b-it:free",
215
+ fast: fast ?? "openrouter/google/gemini-2.0-flash-001",
200
216
  }),
201
217
  options: {
202
218
  reasoning: ["openrouter/google/gemini-2.5-pro", "openrouter/google/gemini-2.5-flash"],
203
219
  standard: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemini-2.5-flash"],
204
- 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"],
205
221
  },
206
222
  },
207
223
  {
@@ -239,14 +255,14 @@ const PROVIDER_FAMILY_TIERS = [
239
255
  label: 'GitHub Copilot',
240
256
  test: (modelId) => /^github-copilot\//.test(modelId),
241
257
  resolve: ({ reasoning, standard, fast }) => ({
242
- reasoning: reasoning ?? "github-copilot/gpt-5.4",
243
- standard: standard ?? "github-copilot/gpt-5.1",
244
- 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",
245
261
  }),
246
262
  options: {
247
- reasoning: ["github-copilot/gpt-5.4", "github-copilot/gpt-5.1"],
248
- standard: ["github-copilot/gpt-5.1", "github-copilot/gpt-5.1-mini"],
249
- 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"],
250
266
  },
251
267
  },
252
268
  {
@@ -282,6 +298,21 @@ const PROVIDER_FAMILY_TIERS = [
282
298
  fast: ["openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/meta-llama/llama-3.3-70b-instruct"],
283
299
  },
284
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
+ },
285
316
  {
286
317
  id: 'ollama',
287
318
  label: 'Ollama (local)',
@@ -335,15 +366,46 @@ const PROVIDER_ENV_MAP = {
335
366
  'github-copilot': ['GITHUB_TOKEN', 'GH_TOKEN'],
336
367
  'openai': ['OPENAI_API_KEY'],
337
368
  'openrouter-llama': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
369
+ 'openrouter': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
338
370
  'ollama': ['OLLAMA_BASE_URL', 'OLLAMA_HOST'],
339
371
  'local': ['LOCAL_LLM_BASE_URL'],
340
372
  };
341
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
+
342
403
  /**
343
404
  * Check if a provider is configured by checking env vars and CLI tools.
344
- * 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.
345
407
  */
346
- function isProviderConfigured(familyId, env) {
408
+ function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
347
409
  const varNames = PROVIDER_ENV_MAP[familyId];
348
410
  if (!varNames?.length) return false;
349
411
 
@@ -359,71 +421,31 @@ function isProviderConfigured(familyId, env) {
359
421
  } catch { /* not available */ }
360
422
  }
361
423
 
362
- // Check passed env (usually process.env)
363
- const anyInEnv = varNames.some(name => {
364
- const val = env?.[name];
365
- return val && typeof val === 'string' && val.length > 0;
366
- });
367
- 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;
368
428
 
369
- // Check construct config.env
370
- try {
371
- const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
372
- const cfgPath = path.join(homeDir, '.construct', 'config.env');
373
- if (fs.existsSync(cfgPath)) {
374
- const content = fs.readFileSync(cfgPath, 'utf8');
375
- const anyInConfig = varNames.some(name => {
376
- const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
377
- const m = content.match(re);
378
- return m && m[1] && m[1].length > 0;
379
- });
380
- if (anyInConfig) return true;
381
- }
382
- } catch { /* skip */ }
383
-
384
- // 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.
385
431
  if (familyId === 'github-copilot') {
432
+ if (hasCopilotCredential()) return true;
386
433
  try {
387
434
  const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 3000 });
388
435
  return r.status === 0;
389
436
  } catch { return false; }
390
437
  }
391
438
 
392
- // Check ~/.env
393
- try {
394
- const homeEnv = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.env');
395
- if (fs.existsSync(homeEnv)) {
396
- const content = fs.readFileSync(homeEnv, 'utf8');
397
- const anyInHomeEnv = varNames.some(name => {
398
- const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
399
- const m = content.match(re);
400
- return m && m[1] && m[1].length > 0;
401
- });
402
- if (anyInHomeEnv) return true;
403
- }
404
- } catch { /* skip */ }
405
-
406
- // Check shell rc files for op:// refs (1Password CLI integration)
407
- try {
408
- const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
409
- const shellFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'].map(f => path.join(homeDir, f));
410
- for (const rcPath of shellFiles) {
411
- if (!fs.existsSync(rcPath)) continue;
412
- const content = fs.readFileSync(rcPath, 'utf8');
413
- const anyInRc = varNames.some(name => {
414
- // Direct export (with or without command substitution)
415
- const re = new RegExp(`^\\s*export\\s+${name}=`, 'm');
416
- return re.test(content);
417
- });
418
- if (anyInRc) return true;
419
- }
420
- } catch { /* skip */ }
421
-
422
439
  return false;
423
440
  }
424
441
 
425
- export function getProviderModelCatalog({ env = process.env } = {}) {
426
- 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) => {
427
449
  const tiers = family.resolve({});
428
450
  const options = {
429
451
  reasoning: uniqueStrings([...(family.options?.reasoning ?? []), tiers.reasoning]),
@@ -442,13 +464,195 @@ export function getProviderModelCatalog({ env = process.env } = {}) {
442
464
  };
443
465
  });
444
466
 
445
- const tierOptions = {
446
- reasoning: uniqueStrings(providers.flatMap((provider) => provider.options.reasoning)),
447
- standard: uniqueStrings(providers.flatMap((provider) => provider.options.standard)),
448
- 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)),
449
478
  };
450
479
 
451
- 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 };
452
656
  }
453
657
 
454
658
  /**
@@ -473,13 +677,10 @@ export function resolveTiersForPrimary(primaryModelId) {
473
677
  // callers (embedded model resolution, capability discovery) stay synchronous
474
678
  // and deterministic, and never expose a key value, only its presence.
475
679
 
476
- function familyDescriptor(family, env) {
680
+ function familyDescriptor(family, env, { allowAmbient = true } = {}) {
477
681
  const requiresEnv = PROVIDER_ENV_MAP[family.id] || (Array.isArray(family.requiresEnv) ? family.requiresEnv : []);
478
682
  const local = family.local === true;
479
- const configured = local || requiresEnv.some((key) => {
480
- const value = env?.[key];
481
- return typeof value === "string" && value.length > 0;
482
- });
683
+ const configured = isProviderConfigured(family.id, env, { allowAmbient });
483
684
  return {
484
685
  id: family.id,
485
686
  label: family.label,
@@ -495,18 +696,18 @@ function familyDescriptor(family, env) {
495
696
  * confirm credentials and whether they are present. Returns null when no family
496
697
  * matches.
497
698
  */
498
- export function describeModelFamily(modelId, { env = process.env } = {}) {
699
+ export function describeModelFamily(modelId, { env = process.env, allowAmbient = true } = {}) {
499
700
  const family = matchProviderFamily(modelId);
500
701
  if (!family) return null;
501
- return familyDescriptor(family, env);
702
+ return familyDescriptor(family, env, { allowAmbient });
502
703
  }
503
704
 
504
705
  /**
505
706
  * Describe every provider family. Resolves a host-supplied provider id and
506
707
  * enumerates model availability for the embedded contract layer.
507
708
  */
508
- export function listModelFamilies({ env = process.env } = {}) {
509
- 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 }));
510
711
  }
511
712
 
512
713
  /**
@@ -515,17 +716,7 @@ export function listModelFamilies({ env = process.env } = {}) {
515
716
  export function readOpenRouterApiKeyFromOpenCodeConfig(
516
717
  configPath = findOpenCodeConfigPath(),
517
718
  ) {
518
- try {
519
- if (!fs.existsSync(configPath)) return "";
520
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
521
- const auth = config?.provider?.openrouter?.options?.headers?.Authorization;
522
- if (typeof auth !== "string") return "";
523
- const value = auth.replace(/^Bearer\s+/i, "").trim();
524
- if (!value || value.includes("__OPENROUTER_API_KEY__")) return "";
525
- return value;
526
- } catch {
527
- return "";
528
- }
719
+ return readRawFromOpenCodeProvider('openrouter', configPath) || "";
529
720
  }
530
721
 
531
722
  // --- Internal helpers ---
@@ -571,9 +762,18 @@ function getRegistryDefaults(registryModels = {}) {
571
762
 
572
763
  function normalizeEnvAssignments(envValues = {}) {
573
764
  return {
574
- reasoning: envValues.reasoning ?? envValues.CX_MODEL_REASONING ?? null,
575
- standard: envValues.standard ?? envValues.CX_MODEL_STANDARD ?? null,
576
- 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,
577
777
  };
578
778
  }
579
779
 
@@ -914,6 +1114,87 @@ export function setModelWithTierInference(envPath, tier, modelId, registryModels
914
1114
  return resolved;
915
1115
  }
916
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
+
917
1198
  export function readCurrentModels(envPath, registryModels = {}) {
918
1199
  const envValues = arguments.length > 2 ? arguments[2] : {};
919
1200
  const fileAssignments = readEnvAssignments(envPath);