@geraldmaron/construct 1.0.0 → 1.0.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 (471) hide show
  1. package/.env.example +10 -7
  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 +1116 -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 +525 -38
  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
@@ -0,0 +1,184 @@
1
+ /**
2
+ * lib/model-free-selector.mjs — Free model polling, scoring, and selection.
3
+ *
4
+ * Extracted from model-router.mjs to keep each module under 800 lines.
5
+ * Polls OpenRouter for free models, scores them by tier, and provides
6
+ * helpers for free-preference modes (global, same-family).
7
+ *
8
+ * Consumed by:
9
+ * • bin/construct – model configuration commands (--auto, --apply).
10
+ * • lib/model-router.mjs – re-exports for backward compatibility.
11
+ */
12
+
13
+ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
14
+ const FETCH_TIMEOUT_MS = 10_000;
15
+
16
+ /**
17
+ * Fetch the list of currently available free models from OpenRouter.
18
+ * Returns an array sorted by descending capability score.
19
+ * Returns `[]` (with a logged error) if the API key is missing or the request fails.
20
+ * @param {string} apiKey – OpenRouter bearer token.
21
+ * @returns {Promise<Array<{id:string,name:string,contextLength:number,isFree:boolean,score:number}>>}
22
+ */
23
+ export async function pollFreeModels(apiKey) {
24
+ if (!apiKey) {
25
+ console.error(
26
+ "Error: OPENROUTER_API_KEY is not set. Cannot poll free models."
27
+ );
28
+ return [];
29
+ }
30
+
31
+ const controller = new AbortController();
32
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
33
+
34
+ let data;
35
+ try {
36
+ const res = await fetch(OPENROUTER_MODELS_URL, {
37
+ headers: { Authorization: `Bearer ${apiKey}` },
38
+ signal: controller.signal,
39
+ });
40
+ if (!res.ok) {
41
+ console.error(
42
+ `Error: OpenRouter models endpoint returned ${res.status} ${res.statusText}`
43
+ );
44
+ return [];
45
+ }
46
+ data = await res.json();
47
+ } catch (err) {
48
+ if (err.name === "AbortError") {
49
+ console.error(
50
+ "Error: OpenRouter request timed out after 10 seconds."
51
+ );
52
+ } else {
53
+ console.error(`Error: Failed to fetch OpenRouter models — ${err.message}`);
54
+ }
55
+ return [];
56
+ } finally {
57
+ clearTimeout(timeout);
58
+ }
59
+
60
+ const models = (data?.data ?? [])
61
+ .filter(
62
+ (m) =>
63
+ m?.pricing?.prompt === "0" && m?.pricing?.completion === "0"
64
+ )
65
+ .map((m) => ({
66
+ id: `openrouter/${m.id}`,
67
+ name: m.name ?? m.id,
68
+ contextLength: m.context_length ?? 0,
69
+ isFree: true,
70
+ }));
71
+
72
+ return models.sort((a, b) => score(b, "standard") - score(a, "standard"));
73
+ }
74
+
75
+ /**
76
+ * Score a model for a given tier (higher is better).
77
+ * @param {object} model – Model object as returned by `pollFreeModels`.
78
+ * @param {"reasoning"|"standard"|"fast"} tier – The tier to score for.
79
+ * @returns {number} – Numerical score used for ranking.
80
+ */
81
+ export function score(model, tier) {
82
+ const { id, contextLength } = model;
83
+ let s = 0;
84
+
85
+ // Context-length weighting
86
+ if (contextLength >= 128_000) s += 40;
87
+ else if (contextLength >= 32_000) s += 20;
88
+ else if (contextLength >= 16_000) s += 10;
89
+
90
+ // Provider-agnostic capability weighting
91
+ if (id.includes("gemma-4")) s += 30;
92
+ if (id.includes("gemma-3")) s += 25;
93
+ if (id.includes("gpt-4o")) s += 25;
94
+ if (id.includes("nemotron-3-super")) s += 20;
95
+ if (id.includes("llama-3.3-70b")) s += 20;
96
+ if (id.includes("qwen3") && contextLength >= 32_000) s += 20;
97
+ if (id.includes("deepseek-r1")) s += 20;
98
+ if (id.includes("deepseek-v3")) s += 15;
99
+ // Extra boost for reasoning-heavy models when the tier is reasoning
100
+ if (
101
+ tier === "reasoning" &&
102
+ (id.includes("deepseek-r1") ||
103
+ id.includes("nemotron-3-super") ||
104
+ id.includes("qwen3"))
105
+ ) {
106
+ s += 15;
107
+ }
108
+
109
+ return s;
110
+ }
111
+
112
+ /**
113
+ * Select the best free model for a given tier from an array of free models.
114
+ * Falls back to a registry-provided fallback chain if no suitable free model exists.
115
+ * @param {Array} freeModels – Output of `pollFreeModels`.
116
+ * @param {"reasoning"|"standard"|"fast"} tier – Target tier.
117
+ * @param {string[]} [registryFallbacks=[]] – Ordered list of fallback IDs.
118
+ * @returns {string|null} – Best model ID, or `null` if none found.
119
+ */
120
+ export function selectForTier(freeModels, tier, registryFallbacks = []) {
121
+ const minContext = tier === "fast" ? 8_000 : tier === "standard" ? 16_000 : 32_000;
122
+
123
+ if (freeModels && freeModels.length > 0) {
124
+ const candidates = freeModels
125
+ .filter((m) => m.contextLength >= minContext)
126
+ .map((m) => ({ ...m, tierScore: score(m, tier) }))
127
+ .sort((a, b) => b.tierScore - a.tierScore);
128
+
129
+ if (candidates[0]?.id) return candidates[0].id;
130
+ }
131
+
132
+ return registryFallbacks[0] ?? null;
133
+ }
134
+
135
+ /**
136
+ * Return the top N candidates for a given tier (used for UI lists).
137
+ * @param {Array} freeModels – Array of model objects.
138
+ * @param {"reasoning"|"standard"|"fast"} tier – Target tier.
139
+ * @param {number} n – Number of candidates to return (default = 3).
140
+ * @returns {Array} – Sorted candidate objects.
141
+ */
142
+ export function topForTier(freeModels, tier, n = 3) {
143
+ if (!freeModels || freeModels.length === 0) return [];
144
+
145
+ const minContext = tier === "fast" ? 8_000 : tier === "standard" ? 16_000 : 32_000;
146
+ return freeModels
147
+ .filter((m) => m.contextLength >= minContext)
148
+ .map((m) => ({ ...m, tierScore: score(m, tier) }))
149
+ .sort((a, b) => b.tierScore - a.tierScore)
150
+ .slice(0, n);
151
+ }
152
+
153
+ /**
154
+ * Check if a model ID ends with `:free`.
155
+ * @param {string} modelId – Model identifier.
156
+ * @returns {boolean} – True if the model is marked as free.
157
+ */
158
+ export function isFreeModel(modelId = "") {
159
+ return /:free$/i.test(modelId);
160
+ }
161
+
162
+ /**
163
+ * Choose a free model when multiple candidates exist.
164
+ * Preference order: explicit primary > fallback > registry default > builtin default.
165
+ * @param {string} primary – Explicitly selected model.
166
+ * @param {string} fallback – Second-choice candidate.
167
+ * @param {string} registryDefault – Default from registry.
168
+ * @param {string} builtinDefault – Built-in fallback.
169
+ * @returns {string|null} – The chosen model ID, or `null` if none selected.
170
+ */
171
+ export function preferFreeValue(
172
+ primary,
173
+ fallback,
174
+ registryDefault,
175
+ builtinDefault
176
+ ) {
177
+ if (primary && isFreeModel(primary)) return primary;
178
+ if (fallback && isFreeModel(fallback)) return fallback;
179
+ if (registryDefault && isFreeModel(registryDefault))
180
+ return registryDefault;
181
+ if (builtinDefault && isFreeModel(builtinDefault))
182
+ return builtinDefault;
183
+ return primary ?? fallback ?? registryDefault ?? builtinDefault ?? null;
184
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * lib/model-pricing.mjs — best-effort live pricing for the model selector.
3
+ *
4
+ * OpenRouter exposes a free, unauthenticated `GET /api/v1/models` endpoint
5
+ * with per-1M-token pricing for every model in its catalog. We hit it once
6
+ * per process (with a 5-minute cache to ~/.cx/model-pricing.json) and fall
7
+ * back to a built-in pricing table for first-party endpoints (Anthropic,
8
+ * OpenAI) when their providers aren't proxied through OpenRouter.
9
+ *
10
+ * For local providers (ollama, local) we return zero pricing with a
11
+ * `runs locally` label so the UI can render `free · runs locally` without
12
+ * any network call.
13
+ *
14
+ * Pricing shape: { input: number, output: number, unit: '1M tokens', currency: 'USD', source }.
15
+ * Numbers are USD per 1M tokens for direct comparability across providers.
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+
22
+ const DEFAULT_CACHE_FILE = path.join(os.homedir(), '.cx', 'model-pricing.json');
23
+ const CACHE_TTL_MS = 5 * 60 * 1000;
24
+ const OPENROUTER_ENDPOINT = 'https://openrouter.ai/api/v1/models';
25
+
26
+ const STATIC_PRICING = {
27
+ 'anthropic/claude-opus-4-6': { input: 15.0, output: 75.0 },
28
+ 'anthropic/claude-sonnet-4-6': { input: 3.0, output: 15.0 },
29
+ 'anthropic/claude-haiku-4-5-20251001': { input: 0.8, output: 4.0 },
30
+ 'openai/gpt-5.4': { input: 12.0, output: 60.0 },
31
+ 'openai/gpt-5.1': { input: 2.5, output: 12.5 },
32
+ 'openai/gpt-5.1-mini': { input: 0.6, output: 3.0 },
33
+ };
34
+
35
+ const LOCAL_PRICING = { input: 0, output: 0, source: 'local', label: 'free · runs locally' };
36
+
37
+ function readCache(cacheFile) {
38
+ try {
39
+ const raw = fs.readFileSync(cacheFile, 'utf8');
40
+ const parsed = JSON.parse(raw);
41
+ if (parsed && typeof parsed === 'object' && parsed.fetchedAt) return parsed;
42
+ } catch { /* cache miss */ }
43
+ return null;
44
+ }
45
+
46
+ function writeCache(cacheFile, payload) {
47
+ try {
48
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
49
+ fs.writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
50
+ } catch { /* non-fatal */ }
51
+ }
52
+
53
+ function isCacheFresh(cache, now = Date.now()) {
54
+ if (!cache || typeof cache.fetchedAt !== 'number') return false;
55
+ const age = now - cache.fetchedAt;
56
+ if (age < 0) return false;
57
+ return age < CACHE_TTL_MS;
58
+ }
59
+
60
+ /**
61
+ * Fetch the OpenRouter catalog. Returns a Map of model id -> { input, output }
62
+ * in USD per 1M tokens. The endpoint returns pricing per token; we multiply
63
+ * by 1e6 so callers compare in the same unit as Anthropic / OpenAI lists.
64
+ */
65
+ async function fetchOpenRouterPricing({ fetchImpl = globalThis.fetch, signal } = {}) {
66
+ if (typeof fetchImpl !== 'function') return null;
67
+ let res;
68
+ try {
69
+ res = await fetchImpl(OPENROUTER_ENDPOINT, { signal });
70
+ } catch {
71
+ return null;
72
+ }
73
+ if (!res?.ok) return null;
74
+ let data;
75
+ try { data = await res.json(); } catch { return null; }
76
+ const list = Array.isArray(data?.data) ? data.data : [];
77
+ const map = {};
78
+ for (const entry of list) {
79
+ if (!entry?.id) continue;
80
+ const p = entry.pricing || {};
81
+ const input = Number(p.prompt);
82
+ const output = Number(p.completion);
83
+ if (!Number.isFinite(input) || !Number.isFinite(output)) continue;
84
+ map[`openrouter/${entry.id}`] = {
85
+ input: input * 1_000_000,
86
+ output: output * 1_000_000,
87
+ context: entry.context_length || null,
88
+ };
89
+ }
90
+ return map;
91
+ }
92
+
93
+ /**
94
+ * Resolve pricing for a list of model ids. Always returns an object keyed by
95
+ * model id; missing entries map to null so the UI can render an em-dash.
96
+ */
97
+ export async function getPricingForModels(modelIds, { fetchImpl, signal, now = Date.now(), cacheFile = DEFAULT_CACHE_FILE } = {}) {
98
+ const result = {};
99
+ const needsRemote = [];
100
+ for (const id of modelIds || []) {
101
+ if (typeof id !== 'string' || !id) continue;
102
+ if (/^ollama\//.test(id) || /^local\//.test(id)) {
103
+ result[id] = { ...LOCAL_PRICING, unit: '1M tokens', currency: 'USD' };
104
+ continue;
105
+ }
106
+ if (STATIC_PRICING[id]) {
107
+ result[id] = { ...STATIC_PRICING[id], unit: '1M tokens', currency: 'USD', source: 'static' };
108
+ continue;
109
+ }
110
+ if (/^openrouter\//.test(id)) {
111
+ needsRemote.push(id);
112
+ continue;
113
+ }
114
+ result[id] = null;
115
+ }
116
+
117
+ if (needsRemote.length === 0) return result;
118
+
119
+ let cache = readCache(cacheFile);
120
+ if (!isCacheFresh(cache, now)) {
121
+ const fresh = await fetchOpenRouterPricing({ fetchImpl, signal });
122
+ if (fresh) {
123
+ cache = { fetchedAt: now, models: fresh };
124
+ writeCache(cacheFile, cache);
125
+ }
126
+ }
127
+
128
+ const remoteMap = cache?.models ?? {};
129
+ for (const id of needsRemote) {
130
+ const hit = remoteMap[id];
131
+ result[id] = hit
132
+ ? { input: hit.input, output: hit.output, context: hit.context, unit: '1M tokens', currency: 'USD', source: 'openrouter' }
133
+ : null;
134
+ }
135
+ return result;
136
+ }
137
+
138
+ /**
139
+ * Format a pricing entry into a compact selector label like
140
+ * `$3.00 / $15.00 per 1M`. Returns null for missing or all-zero entries so
141
+ * callers can render `free` or an em-dash without branching on each field.
142
+ */
143
+ export function formatPricingLabel(entry) {
144
+ if (!entry) return null;
145
+ if (entry.source === 'local' || (entry.input === 0 && entry.output === 0)) {
146
+ return entry.label || 'free · runs locally';
147
+ }
148
+ const input = Number(entry.input);
149
+ const output = Number(entry.output);
150
+ if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
151
+ return `$${input.toFixed(2)} in · $${output.toFixed(2)} out / 1M`;
152
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * lib/model-registry.mjs — Single source of truth for model tier resolution.
3
+ *
4
+ * Addresses model tier complexity by:
5
+ * 1. ONE resolution path with clear precedence
6
+ * 2. Validated at config time, not runtime
7
+ * 3. Simple env-only overrides for emergencies
8
+ * 4. Clear error messages when config is missing
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Configuration precedence (single source of truth)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * RESOLUTION ORDER (highest to lowest):
20
+ * 1. Environment variables (CX_MODEL_*): Emergency overrides
21
+ * 2. User config (~/.construct/config.env): Personal defaults
22
+ * 3. Registry (agents/registry.json): Project recommendations
23
+ * 4. Built-in defaults: Last resort fallbacks
24
+ *
25
+ * Primary resolution path. All code must use this function.
26
+ */
27
+
28
+ const BUILTIN_DEFAULTS = {
29
+ reasoning: 'anthropic/claude-3-5-sonnet',
30
+ standard: 'anthropic/claude-3-haiku',
31
+ fast: 'anthropic/claude-3-haiku',
32
+ };
33
+
34
+ const ENV_PREFIX = 'CX_MODEL';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Core resolution function
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export function resolveModelTiers(options = {}) {
41
+ const {
42
+ env = process.env,
43
+ registryPath = null,
44
+ strict = false, // If true, require all tiers to be configured
45
+ } = options;
46
+
47
+ const result = {
48
+ reasoning: null,
49
+ standard: null,
50
+ fast: null,
51
+ };
52
+
53
+ const sources = {};
54
+ const errors = [];
55
+
56
+ for (const tier of ['reasoning', 'standard', 'fast']) {
57
+ const envKey = `${ENV_PREFIX}_${tier.toUpperCase()}`;
58
+
59
+ // Environment variable (highest priority)
60
+ if (env[envKey]) {
61
+ result[tier] = env[envKey];
62
+ sources[tier] = 'env';
63
+ continue;
64
+ }
65
+
66
+ // User config (read from env which was loaded by env-config)
67
+ const configKey = `CONSTRUCT_MODEL_${tier.toUpperCase()}`;
68
+ if (env[configKey]) {
69
+ result[tier] = env[configKey];
70
+ sources[tier] = 'config';
71
+ continue;
72
+ }
73
+
74
+ // Registry
75
+ if (registryPath && fs.existsSync(registryPath)) {
76
+ try {
77
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
78
+ const registryModel = registry.models?.[tier]?.primary;
79
+ if (registryModel) {
80
+ result[tier] = registryModel;
81
+ sources[tier] = 'registry';
82
+ continue;
83
+ }
84
+ } catch (error) {
85
+ errors.push(`Failed to read registry: ${error.message}`);
86
+ }
87
+ }
88
+
89
+ // Built-in default
90
+ result[tier] = BUILTIN_DEFAULTS[tier];
91
+ sources[tier] = 'default';
92
+ }
93
+
94
+ // Validation
95
+ const configured = Object.values(result).filter(Boolean).length;
96
+
97
+ if (strict && configured < 3) {
98
+ const missing = Object.entries(result)
99
+ .filter(([, v]) => !v)
100
+ .map(([k]) => k);
101
+ errors.push(`Missing configuration for tiers: ${missing.join(', ')}`);
102
+ }
103
+
104
+ return {
105
+ models: result,
106
+ sources,
107
+ configured,
108
+ complete: configured === 3,
109
+ errors: errors.length > 0 ? errors : null,
110
+ defaultsUsed: Object.values(sources).filter(s => s === 'default').length,
111
+ };
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Quick access helpers
116
+ // ---------------------------------------------------------------------------
117
+
118
+ export function getModelForTier(tier, options = {}) {
119
+ const resolved = resolveModelTiers(options);
120
+ return resolved.models[tier] || BUILTIN_DEFAULTS[tier];
121
+ }
122
+
123
+ export function getModelSource(tier, options = {}) {
124
+ const resolved = resolveModelTiers(options);
125
+ return resolved.sources[tier] || 'unknown';
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Configuration helpers
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export function validateModelTiers(options = {}) {
133
+ const resolved = resolveModelTiers({ ...options, strict: true });
134
+
135
+ return {
136
+ valid: resolved.complete && !resolved.errors,
137
+ errors: resolved.errors,
138
+ warnings: resolved.defaultsUsed > 0
139
+ ? [`Using ${resolved.defaultsUsed} built-in default(s). Consider configuring custom models.`]
140
+ : [],
141
+ resolution: resolved,
142
+ };
143
+ }
144
+
145
+ export function formatModelStatus(options = {}) {
146
+ const resolved = resolveModelTiers(options);
147
+
148
+ let output = 'Model Configuration:\n\n';
149
+
150
+ for (const tier of ['reasoning', 'standard', 'fast']) {
151
+ const model = resolved.models[tier];
152
+ const source = resolved.sources[tier];
153
+ const icon = source === 'default' ? '⚠' : '✓';
154
+
155
+ output += `${icon} ${tier.padEnd(10)} ${model}\n`;
156
+ output += ` Source: ${source}\n\n`;
157
+ }
158
+
159
+ if (resolved.defaultsUsed > 0) {
160
+ output += '\nNote: Using built-in defaults. To customize:\n';
161
+ output += ' export CX_MODEL_REASONING=your-model\n';
162
+ output += ' export CX_MODEL_STANDARD=your-model\n';
163
+ output += ' export CX_MODEL_FAST=your-model\n';
164
+ }
165
+
166
+ return output;
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Backwards compatibility
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Legacy wrapper for code that expects the old interface.
175
+ * New code should use resolveModelTiers() directly.
176
+ */
177
+ export function readCurrentModels(envPath, registryModels = {}) {
178
+ // Maintains backwards compatibility with existing code
179
+ // while using the new resolution logic internally
180
+
181
+ const env = {};
182
+
183
+ // Load from env file if provided
184
+ if (envPath && fs.existsSync(envPath)) {
185
+ const content = fs.readFileSync(envPath, 'utf8');
186
+ for (const line of content.split('\n')) {
187
+ const match = line.match(/^([^=]+)=(.*)$/);
188
+ if (match) {
189
+ env[match[1].trim()] = match[2].trim();
190
+ }
191
+ }
192
+ }
193
+
194
+ // Merge with process.env
195
+ Object.assign(env, process.env);
196
+
197
+ const resolved = resolveModelTiers({ env });
198
+
199
+ // Return in legacy format
200
+ return {
201
+ reasoning: resolved.models.reasoning,
202
+ standard: resolved.models.standard,
203
+ fast: resolved.models.fast,
204
+ // Legacy code expects these fields
205
+ _resolved: resolved,
206
+ };
207
+ }
208
+
209
+ export function setTierModel(tier, model, envPath) {
210
+ // Simplified interface for setting a model
211
+ // In practice, this should update the user config file
212
+
213
+ if (!['reasoning', 'standard', 'fast'].includes(tier)) {
214
+ throw new Error(`Invalid tier: ${tier}. Must be reasoning, standard, or fast.`);
215
+ }
216
+
217
+ const envKey = `${ENV_PREFIX}_${tier.toUpperCase()}`;
218
+
219
+ return {
220
+ success: true,
221
+ tier,
222
+ model,
223
+ envKey,
224
+ instruction: `Add to your shell profile: export ${envKey}="${model}"`,
225
+ };
226
+ }