@geraldmaron/construct 1.0.0 → 1.0.2

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 (471) hide show
  1. package/.env.example +9 -6
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1000 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +523 -36
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -3,38 +3,39 @@
3
3
  *
4
4
  * What it does:
5
5
  * • Defines provider-family tier mappings and resolves model IDs.
6
- * • Polls OpenRouter for free models, scores them, and selects appropriate
7
- * alternatives for each tier (reasoning, standard, fast).
8
- * • Provides helpers to apply free-preference modes (global, same‑family,
6
+ * • Provides helpers to apply free-preference modes (global, same-family,
9
7
  * explicit selection) and to persist selections to .env.
8
+ * • Provider-aware failover: classifies failures, resolves fallback candidates,
9
+ * and tracks per-provider cooldowns in ~/.cx/provider-cooldowns.json.
10
10
  *
11
11
  * Consumed by:
12
- * • lib/cli-commands.mjs – model configuration commands.
13
- * • lib/model-router.mjs internal model inference utilities.
14
- * • lib/setup.mjs environment variable generation.
12
+ * • bin/construct – model configuration commands.
13
+ * • lib/prompt-composer.js execution contract metadata.
14
+ * • lib/mcp/server.mjs runtime model resolution.
15
+ * • lib/setup.mjs – environment variable generation.
16
+ * • lib/hooks/model-fallback.mjs – automatic failover on rate-limit / outage.
15
17
  *
16
18
  * Maintenance:
17
- * • Adding new provider families → extend `PROVIDER_FAMILY_TIERS` with a
18
- * `test`/`resolve` entry.
19
- * • Changing scoring logic modify `_score()` function.
20
- * • Adjusting token‑budget behavior edit `applyFreeSameFamilyPreferenceToTierSet()`.
21
- * • Persisting selections → update `applyToEnv()` comment block.
19
+ * • Adding new provider families → extend `PROVIDER_FAMILY_TIERS`.
20
+ * • Changing scoring logic → modify `score()` in model-free-selector.mjs.
21
+ * • Persisting selectionsupdate `applyToEnv()`.
22
+ * • Cooldown windowchange `PROVIDER_COOLDOWN_MS`.
22
23
  */
23
24
  import fs from "node:fs";
24
25
  import path from "node:path";
26
+ import { spawnSync } from "child_process";
25
27
  import { findOpenCodeConfigPath } from "./opencode-config.mjs";
26
-
27
- const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
28
- const FETCH_TIMEOUT_MS = 10_000;
29
-
30
- /**
31
- * Built‑in default model IDs for each tier when no overrides are present.
32
- */
33
- const BUILTIN_DEFAULTS = {
34
- reasoning: "openrouter/deepseek/deepseek-r1",
35
- standard: "openrouter/qwen/qwen3-coder:free",
36
- fast: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
37
- };
28
+ import {
29
+ isFreeModel,
30
+ pollFreeModels,
31
+ preferFreeValue,
32
+ selectForTier,
33
+ topForTier,
34
+ } from "./model-free-selector.mjs";
35
+
36
+ function uniqueStrings(values = []) {
37
+ return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
38
+ }
38
39
 
39
40
  export const MODEL_TIER_BY_WORK_CATEGORY = {
40
41
  visual: "standard",
@@ -45,7 +46,7 @@ export const MODEL_TIER_BY_WORK_CATEGORY = {
45
46
  };
46
47
 
47
48
  /**
48
- * Providerfamily definitions. Each entry contains:
49
+ * Provider-family definitions. Each entry contains:
49
50
  * - `test`: RegExp that matches provider URLs.
50
51
  * - `resolve`: Function that maps selected tier values to concrete model IDs.
51
52
  *
@@ -53,54 +54,98 @@ export const MODEL_TIER_BY_WORK_CATEGORY = {
53
54
  */
54
55
  const PROVIDER_FAMILY_TIERS = [
55
56
  {
57
+ id: 'anthropic',
58
+ label: 'Anthropic (direct)',
56
59
  test: (modelId) => /^anthropic\//.test(modelId),
57
60
  resolve: ({ reasoning, standard, fast }) => ({
58
61
  reasoning: reasoning ?? "anthropic/claude-opus-4-6",
59
62
  standard: standard ?? "anthropic/claude-sonnet-4-6",
60
63
  fast: fast ?? "anthropic/claude-haiku-4-5-20251001",
61
64
  }),
65
+ options: {
66
+ reasoning: ["anthropic/claude-opus-4-6", "anthropic/claude-sonnet-4-6"],
67
+ standard: ["anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6"],
68
+ fast: ["anthropic/claude-haiku-4-5-20251001", "anthropic/claude-sonnet-4-6"],
69
+ },
62
70
  },
63
71
  {
72
+ id: 'openrouter-anthropic',
73
+ label: 'Anthropic via OpenRouter',
64
74
  test: (modelId) => /^openrouter\/anthropic\//.test(modelId),
65
75
  resolve: ({ reasoning, standard, fast }) => ({
66
76
  reasoning: reasoning ?? "openrouter/anthropic/claude-opus-4-6",
67
77
  standard: standard ?? "openrouter/anthropic/claude-sonnet-4-6",
68
78
  fast: fast ?? "openrouter/anthropic/claude-haiku-4-5-20251001",
69
79
  }),
80
+ options: {
81
+ reasoning: ["openrouter/anthropic/claude-opus-4-6", "openrouter/anthropic/claude-sonnet-4-6"],
82
+ standard: ["openrouter/anthropic/claude-sonnet-4-6", "openrouter/anthropic/claude-opus-4-6"],
83
+ fast: ["openrouter/anthropic/claude-haiku-4-5-20251001", "openrouter/anthropic/claude-sonnet-4-6"],
84
+ },
70
85
  },
71
86
  {
87
+ id: 'openrouter-google',
88
+ label: 'Google via OpenRouter',
72
89
  test: (modelId) => /^openrouter\/google\//.test(modelId),
73
90
  resolve: ({ reasoning, standard, fast }) => ({
74
91
  reasoning: reasoning ?? "openrouter/google/gemini-2.5-pro",
75
92
  standard: standard ?? "openrouter/google/gemini-2.0-flash-001",
76
93
  fast: fast ?? "openrouter/google/gemma-3-27b-it:free",
77
94
  }),
95
+ options: {
96
+ reasoning: ["openrouter/google/gemini-2.5-pro", "openrouter/google/gemini-2.5-flash"],
97
+ standard: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemini-2.5-flash"],
98
+ fast: ["openrouter/google/gemma-3-27b-it:free", "openrouter/google/gemini-2.0-flash-001"],
99
+ },
78
100
  },
79
101
  {
102
+ id: 'openrouter-deepseek',
103
+ label: 'DeepSeek via OpenRouter',
80
104
  test: (modelId) => /^openrouter\/deepseek\//.test(modelId),
81
105
  resolve: ({ reasoning, standard, fast }) => ({
82
106
  reasoning: reasoning ?? "openrouter/deepseek/deepseek-r1",
83
107
  standard: standard ?? "openrouter/deepseek/deepseek-v3",
84
108
  fast: fast ?? standard ?? "openrouter/qwen/qwen3-coder:free",
85
109
  }),
110
+ options: {
111
+ reasoning: ["openrouter/deepseek/deepseek-r1", "openrouter/deepseek/deepseek-v3"],
112
+ standard: ["openrouter/deepseek/deepseek-v3", "openrouter/deepseek/deepseek-r1"],
113
+ fast: ["openrouter/qwen/qwen3-coder:free", "openrouter/deepseek/deepseek-v3"],
114
+ },
86
115
  },
87
116
  {
117
+ id: 'openrouter-qwen',
118
+ label: 'Qwen via OpenRouter',
88
119
  test: (modelId) => /^openrouter\/qwen\//.test(modelId),
89
120
  resolve: ({ reasoning, standard, fast }) => ({
90
121
  reasoning: reasoning ?? "openrouter/qwen/qwen3-coder",
91
122
  standard: standard ?? "openrouter/qwen/qwen3-coder:free",
92
123
  fast: fast ?? "openrouter/qwen/qwen2.5-coder-32b-instruct",
93
124
  }),
125
+ options: {
126
+ reasoning: ["openrouter/qwen/qwen3-coder", "openrouter/qwen/qwen3-coder:free"],
127
+ standard: ["openrouter/qwen/qwen3-coder:free", "openrouter/qwen/qwen3-coder"],
128
+ fast: ["openrouter/qwen/qwen2.5-coder-32b-instruct", "openrouter/qwen/qwen3-coder:free"],
129
+ },
94
130
  },
95
131
  {
132
+ id: 'github-copilot',
133
+ label: 'GitHub Copilot',
96
134
  test: (modelId) => /^github-copilot\//.test(modelId),
97
135
  resolve: ({ reasoning, standard, fast }) => ({
98
136
  reasoning: reasoning ?? "github-copilot/gpt-5.4",
99
137
  standard: standard ?? "github-copilot/gpt-5.1",
100
138
  fast: fast ?? "github-copilot/gpt-5.1-mini",
101
139
  }),
140
+ options: {
141
+ reasoning: ["github-copilot/gpt-5.4", "github-copilot/gpt-5.1"],
142
+ standard: ["github-copilot/gpt-5.1", "github-copilot/gpt-5.1-mini"],
143
+ fast: ["github-copilot/gpt-5.1-mini", "github-copilot/gpt-5.1"],
144
+ },
102
145
  },
103
146
  {
147
+ id: 'openai',
148
+ label: 'OpenAI',
104
149
  test: (modelId) => /^openai\//.test(modelId) || /^openrouter\/openai\//.test(modelId),
105
150
  resolve: ({ reasoning, standard, fast }) => {
106
151
  const prefix = /^openrouter\//.test(reasoning || standard || fast || "") ? "openrouter/openai" : "openai";
@@ -110,30 +155,207 @@ const PROVIDER_FAMILY_TIERS = [
110
155
  fast: fast ?? `${prefix}/gpt-5.1-mini`,
111
156
  };
112
157
  },
158
+ options: {
159
+ reasoning: ["openai/gpt-5.4", "openrouter/openai/gpt-5.4", "openai/gpt-5.1"],
160
+ standard: ["openai/gpt-5.1", "openrouter/openai/gpt-5.1", "openai/gpt-5.1-mini"],
161
+ fast: ["openai/gpt-5.1-mini", "openrouter/openai/gpt-5.1-mini", "openai/gpt-5.1"],
162
+ },
113
163
  },
114
164
  {
165
+ id: 'openrouter-llama',
166
+ label: 'Meta Llama via OpenRouter',
115
167
  test: (modelId) => /^openrouter\/meta-llama\//.test(modelId),
116
168
  resolve: ({ reasoning, standard, fast }) => ({
117
169
  reasoning: reasoning ?? "openrouter/meta-llama/llama-3.1-405b-instruct",
118
170
  standard: standard ?? "openrouter/meta-llama/llama-3.3-70b-instruct",
119
171
  fast: fast ?? "openrouter/meta-llama/llama-3.3-70b-instruct:free",
120
172
  }),
173
+ options: {
174
+ reasoning: ["openrouter/meta-llama/llama-3.1-405b-instruct", "openrouter/meta-llama/llama-3.3-70b-instruct"],
175
+ standard: ["openrouter/meta-llama/llama-3.3-70b-instruct", "openrouter/meta-llama/llama-3.3-70b-instruct:free"],
176
+ fast: ["openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/meta-llama/llama-3.3-70b-instruct"],
177
+ },
178
+ },
179
+ {
180
+ id: 'ollama',
181
+ label: 'Ollama (local)',
182
+ test: (modelId) => /^ollama\//.test(modelId),
183
+ resolve: ({ reasoning, standard, fast }) => ({
184
+ reasoning: reasoning ?? "ollama/llama3.1:70b",
185
+ standard: standard ?? "ollama/llama3.1:8b",
186
+ fast: fast ?? "ollama/llama3.2:3b",
187
+ }),
188
+ options: {
189
+ reasoning: ["ollama/llama3.1:70b", "ollama/qwen2.5:32b", "ollama/deepseek-r1:32b"],
190
+ standard: ["ollama/llama3.1:8b", "ollama/qwen2.5:7b", "ollama/mistral:7b"],
191
+ fast: ["ollama/llama3.2:3b", "ollama/phi3:mini", "ollama/qwen2.5:3b"],
192
+ },
193
+ local: true,
194
+ requiresEnv: ['OLLAMA_BASE_URL'],
195
+ pricingHint: 'free · runs locally',
196
+ },
197
+ {
198
+ id: 'local',
199
+ label: 'Local OpenAI-compatible server',
200
+ test: (modelId) => /^local\//.test(modelId),
201
+ resolve: ({ reasoning, standard, fast }) => ({
202
+ reasoning: reasoning ?? "local/custom-large",
203
+ standard: standard ?? "local/custom-medium",
204
+ fast: fast ?? "local/custom-small",
205
+ }),
206
+ options: {
207
+ reasoning: ["local/custom-large"],
208
+ standard: ["local/custom-medium"],
209
+ fast: ["local/custom-small"],
210
+ },
211
+ local: true,
212
+ requiresEnv: ['LOCAL_LLM_BASE_URL'],
213
+ pricingHint: 'free · runs locally',
121
214
  },
122
215
  ];
123
216
 
217
+ export { PROVIDER_FAMILY_TIERS };
218
+
219
+ /**
220
+ * Map provider family IDs to the env var(s) that indicate the provider
221
+ * is configured (has credentials). Used by getProviderModelCatalog to
222
+ * mark which providers are available.
223
+ */
224
+ const PROVIDER_ENV_MAP = {
225
+ 'anthropic': ['ANTHROPIC_API_KEY'],
226
+ 'openrouter-anthropic': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
227
+ 'openrouter-google': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
228
+ 'openrouter-deepseek': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
229
+ 'openrouter-qwen': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
230
+ 'github-copilot': ['GITHUB_TOKEN', 'GH_TOKEN'],
231
+ 'openai': ['OPENAI_API_KEY'],
232
+ 'openrouter-llama': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
233
+ 'ollama': ['OLLAMA_BASE_URL'],
234
+ 'local': ['LOCAL_LLM_BASE_URL'],
235
+ };
236
+
124
237
  /**
125
- * Find the provider‑family entry that matches a given model identifier.
126
- * @param {string} modelId The model ID string.
127
- * @returns {object|null} – The matching tier‑resolution object, or `null`.
238
+ * Check if a provider is configured by checking env vars and CLI tools.
239
+ * Uses the passed env, then checks config.env, then checks gh CLI for Copilot.
240
+ */
241
+ function isProviderConfigured(familyId, env) {
242
+ const varNames = PROVIDER_ENV_MAP[familyId];
243
+ if (!varNames?.length) return false;
244
+
245
+ // Special case: Ollama on default port (no OLLAMA_BASE_URL needed)
246
+ if (familyId === 'ollama') {
247
+ try {
248
+ const r = spawnSync('curl', ['-s', '--connect-timeout', '1', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:11434/api/tags'], { encoding: 'utf8', timeout: 3000 });
249
+ if (r.status === 0 && r.stdout?.trim() === '200') return true;
250
+ } catch { /* not available */ }
251
+ try {
252
+ const r = spawnSync('ollama', ['--version'], { encoding: 'utf8', timeout: 2000 });
253
+ if (r.status === 0) return true;
254
+ } catch { /* not available */ }
255
+ }
256
+
257
+ // 1. Check passed env (usually process.env)
258
+ const anyInEnv = varNames.some(name => {
259
+ const val = env?.[name];
260
+ return val && typeof val === 'string' && val.length > 0;
261
+ });
262
+ if (anyInEnv) return true;
263
+
264
+ // 2. Check construct config.env
265
+ try {
266
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
267
+ const cfgPath = path.join(homeDir, '.construct', 'config.env');
268
+ if (fs.existsSync(cfgPath)) {
269
+ const content = fs.readFileSync(cfgPath, 'utf8');
270
+ const anyInConfig = varNames.some(name => {
271
+ const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
272
+ const m = content.match(re);
273
+ return m && m[1] && m[1].length > 0;
274
+ });
275
+ if (anyInConfig) return true;
276
+ }
277
+ } catch { /* skip */ }
278
+
279
+ // 3. Special case: GitHub Copilot via gh CLI
280
+ if (familyId === 'github-copilot') {
281
+ try {
282
+ const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 3000 });
283
+ return r.status === 0;
284
+ } catch { return false; }
285
+ }
286
+
287
+ // 4. Check ~/.env
288
+ try {
289
+ const homeEnv = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.env');
290
+ if (fs.existsSync(homeEnv)) {
291
+ const content = fs.readFileSync(homeEnv, 'utf8');
292
+ const anyInHomeEnv = varNames.some(name => {
293
+ const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
294
+ const m = content.match(re);
295
+ return m && m[1] && m[1].length > 0;
296
+ });
297
+ if (anyInHomeEnv) return true;
298
+ }
299
+ } catch { /* skip */ }
300
+
301
+ // 5. Check shell rc files for op:// refs (1Password CLI integration)
302
+ try {
303
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
304
+ const shellFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'].map(f => path.join(homeDir, f));
305
+ for (const rcPath of shellFiles) {
306
+ if (!fs.existsSync(rcPath)) continue;
307
+ const content = fs.readFileSync(rcPath, 'utf8');
308
+ const anyInRc = varNames.some(name => {
309
+ // Direct export (with or without command substitution)
310
+ const re = new RegExp(`^\\s*export\\s+${name}=`, 'm');
311
+ return re.test(content);
312
+ });
313
+ if (anyInRc) return true;
314
+ }
315
+ } catch { /* skip */ }
316
+
317
+ return false;
318
+ }
319
+
320
+ export function getProviderModelCatalog({ env = process.env } = {}) {
321
+ const providers = PROVIDER_FAMILY_TIERS.map((family) => {
322
+ const tiers = family.resolve({});
323
+ const options = {
324
+ reasoning: uniqueStrings([...(family.options?.reasoning ?? []), tiers.reasoning]),
325
+ standard: uniqueStrings([...(family.options?.standard ?? []), tiers.standard]),
326
+ fast: uniqueStrings([...(family.options?.fast ?? []), tiers.fast]),
327
+ };
328
+ return {
329
+ id: family.id,
330
+ label: family.label,
331
+ tiers,
332
+ options,
333
+ local: family.local === true,
334
+ requiresEnv: Array.isArray(family.requiresEnv) ? family.requiresEnv : [],
335
+ pricingHint: family.pricingHint ?? null,
336
+ configured: isProviderConfigured(family.id, env),
337
+ };
338
+ });
339
+
340
+ const tierOptions = {
341
+ reasoning: uniqueStrings(providers.flatMap((provider) => provider.options.reasoning)),
342
+ standard: uniqueStrings(providers.flatMap((provider) => provider.options.standard)),
343
+ fast: uniqueStrings(providers.flatMap((provider) => provider.options.fast)),
344
+ };
345
+
346
+ return { providers, tierOptions };
347
+ }
348
+
349
+ /**
350
+ * Find the provider-family entry that matches a given model identifier.
128
351
  */
129
352
  function matchProviderFamily(modelId) {
130
353
  return PROVIDER_FAMILY_TIERS.find((entry) => entry.test(modelId));
131
354
  }
132
355
 
133
356
  /**
134
- * Given a primary model the user selected, return tier modelId mapping in
135
- * the same provider family. Returns null if no family matches (caller should
136
- * fall back to configured CX_MODEL_* values).
357
+ * Given a primary model the user selected, return tier -> modelId mapping in
358
+ * the same provider family. Returns null if no family matches.
137
359
  */
138
360
  export function resolveTiersForPrimary(primaryModelId) {
139
361
  if (!primaryModelId || typeof primaryModelId !== "string") return null;
@@ -143,11 +365,7 @@ export function resolveTiersForPrimary(primaryModelId) {
143
365
  }
144
366
 
145
367
  /**
146
- * Read the OpenRouter API key from the OpenCode config file instead of
147
- * requiring it to be exported as `OPENROUTER_API_KEY`.
148
- * @param {string} configPath – Path to the OpenCode config (defaults to the
149
- * standard location).
150
- * @returns {string} – The API key, or an empty string if not found.
368
+ * Read the OpenRouter API key from the OpenCode config file.
151
369
  */
152
370
  export function readOpenRouterApiKeyFromOpenCodeConfig(
153
371
  configPath = findOpenCodeConfigPath(),
@@ -165,108 +383,8 @@ export function readOpenRouterApiKeyFromOpenCodeConfig(
165
383
  }
166
384
  }
167
385
 
168
- /**
169
- * Fetch the list of currently available free models from OpenRouter.
170
- * Returns an array sorted by descending capability score.
171
- * Returns `[]` (with a logged error) if the API key is missing or the request fails.
172
- * @param {string} apiKey – OpenRouter bearer token.
173
- * @returns {Promise<Array<{id:string,name:string,contextLength:number,isFree:boolean,score:number}>>}
174
- */
175
- export async function pollFreeModels(apiKey) {
176
- if (!apiKey) {
177
- console.error(
178
- "Error: OPENROUTER_API_KEY is not set. Cannot poll free models."
179
- );
180
- return [];
181
- }
182
-
183
- const controller = new AbortController();
184
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
185
-
186
- let data;
187
- try {
188
- const res = await fetch(OPENROUTER_MODELS_URL, {
189
- headers: { Authorization: `Bearer ${apiKey}` },
190
- signal: controller.signal,
191
- });
192
- if (!res.ok) {
193
- console.error(
194
- `Error: OpenRouter models endpoint returned ${res.status} ${res.statusText}`
195
- );
196
- return [];
197
- }
198
- data = await res.json();
199
- } catch (err) {
200
- if (err.name === "AbortError") {
201
- console.error(
202
- "Error: OpenRouter request timed out after 10 seconds."
203
- );
204
- } else {
205
- console.error(`Error: Failed to fetch OpenRouter models — ${err.message}`);
206
- }
207
- return [];
208
- } finally {
209
- clearTimeout(timeout);
210
- }
211
-
212
- const models = (data?.data ?? [])
213
- .filter(
214
- (m) =>
215
- m?.pricing?.prompt === "0" && m?.pricing?.completion === "0"
216
- )
217
- .map((m) => ({
218
- id: `openrouter/${m.id}`,
219
- name: m.name ?? m.id,
220
- contextLength: m.context_length ?? 0,
221
- isFree: true,
222
- }));
223
-
224
- return models.sort((a, b) => _score(b, "standard") - _score(a, "standard"));
225
- }
226
-
227
- /**
228
- * Score a model for a given tier (higher is better).
229
- * @private
230
- * @param {object} model – Model object as returned by `pollFreeModels`.
231
- * @param {"reasoning"|"standard"|"fast"} tier – The tier to score for.
232
- * @returns {number} – Numerical score used for ranking.
233
- */
234
- function _score(model, tier) {
235
- const { id, contextLength } = model;
236
- let score = 0;
237
-
238
- // Context‑length weighting
239
- if (contextLength >= 128_000) score += 40;
240
- else if (contextLength >= 32_000) score += 20;
241
- else if (contextLength >= 16_000) score += 10;
242
-
243
- // Provider‑agnostic capability weighting – no vendor bias
244
- if (id.includes("gemma-4")) score += 30;
245
- if (id.includes("gemma-3")) score += 25;
246
- if (id.includes("gpt-4o")) score += 25;
247
- if (id.includes("nemotron-3-super")) score += 20;
248
- if (id.includes("llama-3.3-70b")) score += 20;
249
- if (id.includes("qwen3") && contextLength >= 32_000) score += 20;
250
- if (id.includes("deepseek-r1")) score += 20;
251
- if (id.includes("deepseek-v3")) score += 15;
252
- // Extra boost for reasoning‑heavy models when the tier is reasoning
253
- if (
254
- tier === "reasoning" &&
255
- (id.includes("deepseek-r1") ||
256
- id.includes("nemotron-3-super") ||
257
- id.includes("qwen3"))
258
- ) {
259
- score += 15;
260
- }
386
+ // --- Internal helpers ---
261
387
 
262
- return score;
263
- }
264
-
265
- /**
266
- * Read CX_MODEL_* assignments from a .env file.
267
- * @param {string} envPath – Path to the .env file.
268
- * @returns {{reasoning:any,standard:any,fast:any}} – Current values.
269
- */
270
388
  function readEnvAssignments(envPath) {
271
389
  const tierKeys = {
272
390
  reasoning: "CX_MODEL_REASONING",
@@ -291,11 +409,6 @@ function readEnvAssignments(envPath) {
291
409
  );
292
410
  }
293
411
 
294
- /**
295
- * Extract the primary model ID from a definition that may be a string or an object.
296
- * @param {string|object} def – Definition to inspect.
297
- * @returns {string|null} – Primary model ID if found.
298
- */
299
412
  function extractPrimary(def) {
300
413
  if (typeof def === "string") return def;
301
414
  if (def && typeof def === "object")
@@ -303,21 +416,11 @@ function extractPrimary(def) {
303
416
  return null;
304
417
  }
305
418
 
306
- /**
307
- * Build a defaults object from registry‑provided model mappings.
308
- * Falls back to `BUILTIN_DEFAULTS` when a registry entry is missing.
309
- * @param {{reasoning:any,standard:any,fast:any}} registryModels – Optional overrides.
310
- * @returns {{reasoning:string,standard:string,fast:string}} – Resolved defaults.
311
- */
312
419
  function getRegistryDefaults(registryModels = {}) {
313
420
  return {
314
- reasoning:
315
- extractPrimary(registryModels.reasoning) ??
316
- BUILTIN_DEFAULTS.reasoning,
317
- standard:
318
- extractPrimary(registryModels.standard) ??
319
- BUILTIN_DEFAULTS.standard,
320
- fast: extractPrimary(registryModels.fast) ?? BUILTIN_DEFAULTS.fast,
421
+ reasoning: extractPrimary(registryModels.reasoning) ?? null,
422
+ standard: extractPrimary(registryModels.standard) ?? null,
423
+ fast: extractPrimary(registryModels.fast) ?? null,
321
424
  };
322
425
  }
323
426
 
@@ -351,24 +454,95 @@ function resolveTierDefinition(definition) {
351
454
  };
352
455
  }
353
456
 
457
+ // --- Provider cooldown helpers ---
458
+
459
+ const PROVIDER_COOLDOWN_MS = 5 * 60 * 1000;
460
+
461
+ /**
462
+ * Read the per-provider cooldown state file.
463
+ * Returns a map of providerKey → expiresAt timestamp.
464
+ */
465
+ export function readProviderCooldowns(cooldownPath) {
466
+ try {
467
+ const raw = fs.readFileSync(cooldownPath, "utf8");
468
+ const parsed = JSON.parse(raw);
469
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
470
+ } catch {
471
+ // missing or corrupt — treat as empty
472
+ }
473
+ return {};
474
+ }
475
+
476
+ /**
477
+ * Record a cooldown for a provider key, expiring after PROVIDER_COOLDOWN_MS.
478
+ */
479
+ export function writeProviderCooldown(cooldownPath, provider, now = Date.now()) {
480
+ if (!provider) return;
481
+ const existing = readProviderCooldowns(cooldownPath);
482
+ existing[provider] = now + PROVIDER_COOLDOWN_MS;
483
+ fs.mkdirSync(path.dirname(cooldownPath), { recursive: true });
484
+ fs.writeFileSync(cooldownPath, JSON.stringify(existing, null, 2));
485
+ }
486
+
487
+ /**
488
+ * Return true if the given provider is still within its cooldown window.
489
+ */
490
+ export function isProviderOnCooldown(cooldownPath, provider, now = Date.now()) {
491
+ if (!provider) return false;
492
+ const state = readProviderCooldowns(cooldownPath);
493
+ const expiresAt = state[provider];
494
+ return typeof expiresAt === "number" && now < expiresAt;
495
+ }
496
+
497
+ /**
498
+ * High-level entry point for the fallback hook.
499
+ *
500
+ * Given raw hook input, current env path, and a cooldown file path:
501
+ * 1. Classifies the failure.
502
+ * 2. Resolves a candidate model from the tier's fallback list, skipping any
503
+ * provider currently on cooldown.
504
+ * 3. Returns { targetModel, tier, reason } or null if nothing actionable.
505
+ */
506
+ export function selectFallbackModel({
507
+ hookInput,
508
+ envPath,
509
+ cooldownPath,
510
+ registryModels = {},
511
+ now = Date.now(),
512
+ } = {}) {
513
+ const classified = classifyProviderFailure(hookInput);
514
+ if (!classified || !classified.retryable) return null;
515
+
516
+ const failingProvider = providerKey(classified.provider || "");
517
+ if (failingProvider && isProviderOnCooldown(cooldownPath, failingProvider, now)) return null;
518
+
519
+ const currentModels = readCurrentModels(envPath, registryModels);
520
+ const action = resolveFallbackAction({
521
+ failure: classified,
522
+ currentModels,
523
+ registryModels,
524
+ });
525
+ if (!action) return null;
526
+
527
+ const candidateProvider = providerKey(action.targetModel);
528
+ if (candidateProvider && isProviderOnCooldown(cooldownPath, candidateProvider, now)) return null;
529
+
530
+ return { targetModel: action.targetModel, tier: action.tier, reason: action.reason };
531
+ }
532
+
533
+ // --- Public API ---
534
+
354
535
  export function classifyProviderFailure(input) {
355
536
  const error = input?.error && typeof input.error === "object" ? input.error : input;
356
537
  const text = flattenText([
357
- error?.message,
358
- error?.name,
359
- error?.code,
360
- error?.status,
361
- error?.statusCode,
362
- input?.message,
363
- input?.error,
538
+ error?.message, error?.name, error?.code,
539
+ error?.status, error?.statusCode,
540
+ input?.message, input?.error,
364
541
  ]);
365
542
  if (!text) return null;
366
543
 
367
544
  const provider = [
368
- error?.provider,
369
- input?.provider,
370
- input?.model?.provider,
371
- input?.session?.provider,
545
+ error?.provider, input?.provider, input?.model?.provider, input?.session?.provider,
372
546
  ].find((value) => typeof value === "string" && value) || null;
373
547
 
374
548
  const patterns = [
@@ -381,11 +555,7 @@ export function classifyProviderFailure(input) {
381
555
 
382
556
  for (const pattern of patterns) {
383
557
  if (pattern.test.test(text)) {
384
- return {
385
- kind: pattern.kind,
386
- provider,
387
- retryable: pattern.retryable,
388
- };
558
+ return { kind: pattern.kind, provider, retryable: pattern.retryable };
389
559
  }
390
560
  }
391
561
 
@@ -410,10 +580,7 @@ export function resolveFallbackAction({
410
580
  const currentProvider = providerKey(currentModel || "");
411
581
  const failingProvider = providerKey(classified.provider || "");
412
582
 
413
- const candidates = [
414
- tierDef.primary,
415
- ...tierDef.fallback,
416
- ]
583
+ const candidates = [tierDef.primary, ...tierDef.fallback]
417
584
  .filter((modelId) => typeof modelId === "string" && modelId)
418
585
  .filter((modelId) => modelId !== currentModel)
419
586
  .filter((modelId) => {
@@ -427,43 +594,25 @@ export function resolveFallbackAction({
427
594
  const targetModel = candidates[0] ?? null;
428
595
  if (!targetModel) return null;
429
596
 
430
- return {
431
- action: "apply-models",
432
- reason: classified.kind,
433
- targetModel,
434
- tier,
435
- };
597
+ return { action: "apply-models", reason: classified.kind, targetModel, tier };
436
598
  }
437
599
 
438
600
  function resolveTierAssignments(envValues = {}, registryModels = {}) {
439
601
  const normalizedEnv = normalizeEnvAssignments(envValues);
440
- const explicitSources = envValues?.sources && typeof envValues.sources === "object"
441
- ? envValues.sources
442
- : {};
602
+ const explicitSources = envValues?.sources && typeof envValues.sources === "object" ? envValues.sources : {};
443
603
  const defaults = getRegistryDefaults(registryModels);
444
604
  const tiers = {};
445
605
 
446
606
  for (const tier of ["reasoning", "standard", "fast"]) {
447
607
  if (explicitSources[tier]) {
448
- tiers[tier] = {
449
- model: normalizedEnv[tier] ?? defaults[tier],
450
- source: explicitSources[tier],
451
- };
452
- continue;
453
- }
454
-
455
- if (normalizedEnv[tier]) {
456
- tiers[tier] = {
457
- model: normalizedEnv[tier],
458
- source: "env override",
459
- };
460
- continue;
608
+ tiers[tier] = { model: normalizedEnv[tier] ?? defaults[tier], source: explicitSources[tier] };
609
+ } else if (normalizedEnv[tier]) {
610
+ tiers[tier] = { model: normalizedEnv[tier], source: "env override" };
611
+ } else if (defaults[tier]) {
612
+ tiers[tier] = { model: defaults[tier], source: "registry" };
613
+ } else {
614
+ tiers[tier] = { model: null, source: "not configured" };
461
615
  }
462
-
463
- tiers[tier] = {
464
- model: defaults[tier],
465
- source: registryModels[tier] ? "registry default" : "built-in default",
466
- };
467
616
  }
468
617
 
469
618
  return tiers;
@@ -494,45 +643,6 @@ export function resolveExecutionContractModelMetadata({
494
643
  };
495
644
  }
496
645
 
497
- /**
498
- * Check if a model ID ends with `:free`.
499
- * @param {string} modelId – Model identifier.
500
- * @returns {boolean} – True if the model is marked as free.
501
- */
502
- function isFreeModel(modelId = "") {
503
- return /:free$/i.test(modelId);
504
- }
505
-
506
- /**
507
- * Choose a free model when multiple candidates exist.
508
- * Preference order: explicit primary > fallback > registry default > builtin default.
509
- * @param {string} primary – Explicitly selected model.
510
- * @param {string} fallback – Second‑choice candidate.
511
- * @param {string} registryDefault – Default from registry.
512
- * @param {string} builtinDefault – Built‑in fallback.
513
- * @returns {string|null} – The chosen model ID, or `null` if none selected.
514
- */
515
- function preferFreeValue(
516
- primary,
517
- fallback,
518
- registryDefault,
519
- builtinDefault
520
- ) {
521
- if (primary && isFreeModel(primary)) return primary;
522
- if (fallback && isFreeModel(fallback)) return fallback;
523
- if (registryDefault && isFreeModel(registryDefault))
524
- return registryDefault;
525
- if (builtinDefault && isFreeModel(builtinDefault))
526
- return builtinDefault;
527
- return primary ?? fallback ?? registryDefault ?? builtinDefault ?? null;
528
- }
529
-
530
- /**
531
- * Infer tier‑specific models from a user‑selected model.
532
- * @param {string} selectedModel – Model explicitly chosen by the user.
533
- * @param {{registryModels:object,existing:object}} [options] – Context objects.
534
- * @returns {{reasoning:string,standard:string,fast:string}|null} – Inferred tier assignments.
535
- */
536
646
  export function inferTierModelsFromSelection(
537
647
  selectedModel,
538
648
  { registryModels = {}, existing = {} } = {}
@@ -562,53 +672,20 @@ export function inferTierModelsFromSelection(
562
672
  };
563
673
  }
564
674
 
565
- /**
566
- * Apply a global “prefer‑free” preference to an existing tier set.
567
- * @param {{reasoning:string,standard:string,fast:string}} tierSet – Current tier assignments.
568
- * @param {{registryModels:object}} [options] – Registry context.
569
- * @returns {{reasoning:string,standard:string,fast:string}} – Updated tier set.
570
- */
571
675
  export function applyFreePreferenceToTierSet(tierSet, { registryModels = {} } = {}) {
572
676
  const defaults = getRegistryDefaults(registryModels);
573
677
  return {
574
- reasoning: preferFreeValue(
575
- tierSet.reasoning,
576
- tierSet.standard,
577
- defaults.reasoning,
578
- BUILTIN_DEFAULTS.reasoning
579
- ),
580
- standard: preferFreeValue(
581
- tierSet.standard,
582
- tierSet.fast,
583
- defaults.standard,
584
- BUILTIN_DEFAULTS.standard
585
- ),
586
- fast: preferFreeValue(
587
- tierSet.fast,
588
- tierSet.standard,
589
- defaults.fast,
590
- BUILTIN_DEFAULTS.fast
591
- ),
678
+ reasoning: preferFreeValue(tierSet.reasoning, tierSet.standard, defaults.reasoning, null),
679
+ standard: preferFreeValue(tierSet.standard, tierSet.fast, defaults.standard, null),
680
+ fast: preferFreeValue(tierSet.fast, tierSet.standard, defaults.fast, null),
592
681
  };
593
682
  }
594
683
 
595
- /**
596
- * Apply “prefer‑free‑same‑family” logic to a tier set based on an explicit
597
- * model selection. Only swaps a tier to a free sibling if that sibling stays
598
- * within the same provider family.
599
- * @param {{reasoning:string,standard:string,fast:string}} tierSet – Current assignments.
600
- * @param {string} selectedModel – Explicitly selected model ID.
601
- * @returns {{reasoning:string,standard:string,fast:string}} – Updated tier set.
602
- */
603
- export function applyFreeSameFamilyPreferenceToTierSet(
604
- tierSet,
605
- selectedModel
606
- ) {
684
+ export function applyFreeSameFamilyPreferenceToTierSet(tierSet, selectedModel) {
607
685
  const family = matchProviderFamily(selectedModel);
608
686
  if (!family) return tierSet;
609
687
 
610
688
  const sameFamily = family.resolve({ reasoning: null, standard: null, fast: null });
611
-
612
689
  const next = { ...tierSet };
613
690
  for (const tier of ["reasoning", "standard", "fast"]) {
614
691
  if (tierSet[tier] === selectedModel) continue;
@@ -618,57 +695,6 @@ export function applyFreeSameFamilyPreferenceToTierSet(
618
695
  return next;
619
696
  }
620
697
 
621
- /**
622
- * Select the best free model for a given tier from an array of free models.
623
- * Falls back to a registry‑provided fallback chain if no suitable free model exists.
624
- * @param {Array} freeModels – Output of `pollFreeModels`.
625
- * @param {"reasoning"|"standard"|"fast"} tier – Target tier.
626
- * @param {string[]} [registryFallbacks=[]] – Ordered list of fallback IDs.
627
- * @returns {string|null} – Best model ID, or `null` if none found.
628
- */
629
- export function selectForTier(freeModels, tier, registryFallbacks = []) {
630
- const minContext = tier === "fast" ? 8_000 : tier === "standard" ? 16_000 : 32_000;
631
-
632
- if (freeModels && freeModels.length > 0) {
633
- const candidates = freeModels
634
- .filter((m) => m.contextLength >= minContext)
635
- .map((m) => ({ ...m, tierScore: _score(m, tier) }))
636
- .sort((a, b) => b.tierScore - a.tierScore);
637
-
638
- if (candidates[0]?.id) return candidates[0].id;
639
- }
640
-
641
- // No suitable free model found – walk the registry fallback chain
642
- return registryFallbacks[0] ?? null;
643
- }
644
-
645
- /**
646
- * Return the top N candidates for a given tier (used for UI lists).
647
- * @param {Array} freeModels – Array of model objects.
648
- * @param {"reasoning"|"standard"|"fast"} tier – Target tier.
649
- * @param {number} n – Number of candidates to return (default = 3).
650
- * @returns {Array} – Sorted candidate objects.
651
- */
652
- export function topForTier(freeModels, tier, n = 3) {
653
- if (!freeModels || freeModels.length === 0) return [];
654
-
655
- const minContext = tier === "fast" ? 8_000 : tier === "standard" ? 16_000 : 32_000;
656
- return freeModels
657
- .filter((m) => m.contextLength >= minContext)
658
- .map((m) => ({ ...m, tierScore: _score(m, tier) }))
659
- .sort((a, b) => b.tierScore - a.tierScore)
660
- .slice(0, n);
661
- }
662
-
663
- /**
664
- * Persist CX_MODEL_* assignments into a .env file.
665
- * • Removes any existing comment block that was generated by `--apply`.
666
- * • Inserts a fresh comment with the current date.
667
- * • Writes the new CX_MODEL_* lines at the top of the file (or appends if the file only contains comments).
668
- *
669
- * @param {string} envPath – Path to the .env file.
670
- * @param {{reasoning?:string,standard?:string,fast?:string}} selections – Model IDs to store.
671
- */
672
698
  export function applyToEnv(envPath, selections) {
673
699
  const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
674
700
  const date = new Date().toISOString().slice(0, 10);
@@ -680,7 +706,6 @@ export function applyToEnv(envPath, selections) {
680
706
  fast: "CX_MODEL_FAST",
681
707
  };
682
708
 
683
- // Strip out any old auto‑generated block
684
709
  let lines = existing.split("\n");
685
710
  lines = lines.filter((l) => {
686
711
  const trimmed = l.trim();
@@ -690,38 +715,21 @@ export function applyToEnv(envPath, selections) {
690
715
  return true;
691
716
  });
692
717
 
693
- // Build the new comment + model lines
694
718
  const modelLines = [`${commentMarker} ${date}`];
695
719
  for (const [tier, envKey] of Object.entries(tierMap)) {
696
- if (selections[tier]) {
697
- modelLines.push(`${envKey}=${selections[tier]}`);
698
- }
720
+ if (selections[tier]) modelLines.push(`${envKey}=${selections[tier]}`);
699
721
  }
700
722
 
701
- // Insert just before the first blank line (or at EOF)
702
723
  const insertIdx = lines.findLastIndex((l) => l.trim() !== "") + 1;
703
- lines.splice(
704
- insertIdx === 0 ? lines.length : insertIdx,
705
- 0,
706
- "",
707
- ...modelLines
708
- );
724
+ lines.splice(insertIdx === 0 ? lines.length : insertIdx, 0, "", ...modelLines);
709
725
 
710
726
  fs.writeFileSync(envPath, lines.join("\n"));
711
727
  }
712
728
 
713
- /**
714
- * Remove all CX_MODEL_* entries and their associated comment from a .env file.
715
- * @param {string} envPath – Path to the .env file.
716
- */
717
729
  export function resetEnv(envPath) {
718
730
  if (!fs.existsSync(envPath)) return;
719
731
  const commentMarker = "# Auto-set by construct models --apply on";
720
- const tierKeys = new Set([
721
- "CX_MODEL_REASONING",
722
- "CX_MODEL_STANDARD",
723
- "CX_MODEL_FAST",
724
- ]);
732
+ const tierKeys = new Set(["CX_MODEL_REASONING", "CX_MODEL_STANDARD", "CX_MODEL_FAST"]);
725
733
 
726
734
  const lines = fs.readFileSync(envPath, "utf8").split("\n");
727
735
  const filtered = lines.filter((l) => {
@@ -735,42 +743,14 @@ export function resetEnv(envPath) {
735
743
  fs.writeFileSync(envPath, filtered.join("\n"));
736
744
  }
737
745
 
738
- /**
739
- * Set a single tier’s model in the .env file.
740
- * @param {string} envPath – Path to the .env file.
741
- * @param {"reasoning"|"standard"|"fast"} tier – Target tier.
742
- * @param {string} modelId – Model identifier to store.
743
- */
744
746
  export function setTierModel(envPath, tier, modelId) {
745
747
  applyToEnv(envPath, { [tier]: modelId });
746
748
  }
747
749
 
748
- /**
749
- * Replace an existing model selection with a new one, applying any
750
- * preference‑override flags (`preferFreeSameFamily` or `preferFree`).
751
- *
752
- * applyToEnv() → writes the updated assignments.
753
- *
754
- * @param {string} envPath – Path to the .env file.
755
- * @param {"reasoning"|"standard"|"fast"} tier – Tier whose model should be set.
756
- * @param {string} modelId – New model ID.
757
- * @param {{registryModels:object,existing:object}} [context] – Additional context objects.
758
- * @param {object} [options] – Preference flags.
759
- * @returns {object} – The resolved tier assignments after overrides.
760
- */
761
- export function setModelWithTierInference(
762
- envPath,
763
- tier,
764
- modelId,
765
- registryModels = {},
766
- options = {}
767
- ) {
750
+ export function setModelWithTierInference(envPath, tier, modelId, registryModels = {}, options = {}) {
768
751
  const existing = readEnvAssignments(envPath);
769
752
  existing[tier] = modelId;
770
- const inferred = inferTierModelsFromSelection(modelId, {
771
- registryModels,
772
- existing,
773
- }) || existing;
753
+ const inferred = inferTierModelsFromSelection(modelId, { registryModels, existing }) || existing;
774
754
  inferred[tier] = modelId;
775
755
 
776
756
  let resolved = inferred;
@@ -784,13 +764,6 @@ export function setModelWithTierInference(
784
764
  return resolved;
785
765
  }
786
766
 
787
- /**
788
- * Read current model assignments from .env, falling back to registry defaults.
789
- * Provides a source‑tracking map for debugging.
790
- * @param {string} envPath – Path to the .env file.
791
- * @param {{reasoning:any,standard:any,fast:any}} registryModels – Reference data.
792
- * @returns {{reasoning:string,standard:string,fast:string,sources:Record<string,string>>}
793
- */
794
767
  export function readCurrentModels(envPath, registryModels = {}) {
795
768
  const envValues = arguments.length > 2 ? arguments[2] : {};
796
769
  const fileAssignments = readEnvAssignments(envPath);
@@ -808,3 +781,6 @@ export function readCurrentModels(envPath, registryModels = {}) {
808
781
  }
809
782
  return result;
810
783
  }
784
+
785
+ export { resolveProviderCapabilities } from './provider-capabilities.js';
786
+ export { resolveProviderCapabilitiesSync } from './provider-capabilities.js';