@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
@@ -0,0 +1,151 @@
1
+ /**
2
+ * lib/telemetry/setup.mjs — Initialize telemetry backend annotation queues and eval configs.
3
+ *
4
+ * Creates:
5
+ * 1. Annotation queues for human quality review
6
+ * 2. Eval configs for LLM-as-a-judge
7
+ * 3. Links datasets to queues/evals
8
+ *
9
+ * Idempotent — checks existence, updates if metadata changed.
10
+ * CLI: construct telemetry setup [--force]
11
+ * Daemon: runs on startup
12
+ *
13
+ * Uses an open trace ingestion API protocol.
14
+ * Point CONSTRUCT_TELEMETRY_URL at any compatible backend.
15
+ */
16
+
17
+ import { telemetryBaseUrl, telemetryHeaders } from './backends/remote.mjs';
18
+
19
+ const QUEUES = [
20
+ {
21
+ name: 'construct-quality-queue',
22
+ description: 'Human annotation queue for agent quality scoring (0.0-1.0)',
23
+ },
24
+ ];
25
+
26
+ const EVAL_CONFIGS = [
27
+ {
28
+ name: 'quality-llm-sonnet',
29
+ description: 'LLM-as-a-judge eval config using Claude Sonnet 3.5',
30
+ model: 'anthropic/claude-3-5-sonnet-20241022',
31
+ prompt: `You are evaluating agent work quality.
32
+
33
+ Rate the work on a scale of 0.0 (complete failure) to 1.0 (perfect):
34
+
35
+ CRITERIA:
36
+ 1. Task Completion (40%): Did it solve the stated problem?
37
+ 2. Requirements Adherence (30%): Followed all specs/constraints?
38
+ 3. Quality/Clarity (20%): Professional, well-structured?
39
+ 4. Thoroughness (10%): Complete coverage?
40
+
41
+ Input: {{input}}
42
+ Expected: {{expected_output}}
43
+
44
+ Respond with JSON: {"score": 0.85, "reason": "brief explanation"}`,
45
+ },
46
+ ];
47
+
48
+ async function ensureQueue(url, headers, queue, fetchImpl = globalThis.fetch) {
49
+ // Read-only check — avoids Zod validation errors on POST to annotation-queues.
50
+ // The queue is non-critical; skip creation to avoid crashing the backend process.
51
+ const listRes = await fetchImpl(`${url}/api/public/annotation-queues`, { headers });
52
+ if (!listRes.ok) return false;
53
+ const list = await listRes.json();
54
+ const existing = (list.data || []).find((q) => q.name === queue.name);
55
+ return !!existing;
56
+ }
57
+
58
+ async function ensureEvalConfig(url, headers, config, fetchImpl = globalThis.fetch) {
59
+ // Read-only check — same reason as ensureQueue.
60
+ const listRes = await fetchImpl(`${url}/api/public/evaluation-configs`, { headers });
61
+ if (!listRes.ok) return false;
62
+ const list = await listRes.json();
63
+ const existing = (list.data || []).find((e) => e.name === config.name);
64
+ return !!existing;
65
+ }
66
+
67
+ /**
68
+ * Main setup function. Idempotent.
69
+ */
70
+ export async function runTelemetrySetup({
71
+ publicKey,
72
+ secretKey,
73
+ baseUrl,
74
+ force = false,
75
+ fetchImpl = globalThis.fetch,
76
+ env = process.env,
77
+ } = {}) {
78
+ const resolvedPublicKey =
79
+ publicKey ??
80
+ env.CONSTRUCT_TELEMETRY_PUBLIC_KEY ??
81
+ process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
82
+ const resolvedSecretKey =
83
+ secretKey ??
84
+ env.CONSTRUCT_TELEMETRY_SECRET_KEY ??
85
+ process.env.CONSTRUCT_TELEMETRY_SECRET_KEY;
86
+ const resolvedBaseUrl =
87
+ baseUrl ??
88
+ env.CONSTRUCT_TELEMETRY_URL ??
89
+ process.env.CONSTRUCT_TELEMETRY_URL;
90
+
91
+ if (!resolvedPublicKey || !resolvedSecretKey) {
92
+ return {
93
+ ok: false,
94
+ error: 'CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY required',
95
+ };
96
+ }
97
+
98
+ const url = telemetryBaseUrl({ CONSTRUCT_TELEMETRY_URL: resolvedBaseUrl });
99
+ const headers = telemetryHeaders({
100
+ CONSTRUCT_TELEMETRY_PUBLIC_KEY: resolvedPublicKey,
101
+ CONSTRUCT_TELEMETRY_SECRET_KEY: resolvedSecretKey,
102
+ });
103
+
104
+ const results = [];
105
+
106
+ for (const queue of QUEUES) {
107
+ const success = await ensureQueue(url, headers, queue, fetchImpl);
108
+ results.push({ type: 'queue', name: queue.name, success });
109
+ }
110
+
111
+ for (const config of EVAL_CONFIGS) {
112
+ const success = await ensureEvalConfig(url, headers, config, fetchImpl);
113
+ results.push({ type: 'eval-config', name: config.name, success });
114
+ }
115
+
116
+ const successes = results.filter((r) => r.success).length;
117
+ const total = results.length;
118
+
119
+ return {
120
+ ok: true,
121
+ summary: `${successes}/${total} resources configured`,
122
+ results,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * CLI handler
128
+ */
129
+ export async function runTelemetrySetupCli(args = []) {
130
+ const force = args.includes('--force');
131
+
132
+ process.stdout.write('Configuring telemetry backend annotation queues and eval configs…\n');
133
+
134
+ const result = await runTelemetrySetup({ force });
135
+
136
+ if (result.ok) {
137
+ process.stdout.write(`✓ ${result.summary}\n`);
138
+ if (result.results.length) {
139
+ process.stdout.write('\nDetails:\n');
140
+ for (const r of result.results) {
141
+ const emoji = r.success ? '✓' : '✗';
142
+ process.stdout.write(` ${emoji} ${r.type}: ${r.name}\n`);
143
+ }
144
+ }
145
+ } else {
146
+ process.stderr.write(`✗ ${result.error}\n`);
147
+ process.exit(1);
148
+ }
149
+ }
150
+
151
+
@@ -0,0 +1,78 @@
1
+ /**
2
+ * lib/telemetry/skill-calls.mjs — measure which skill files actually get loaded.
3
+ *
4
+ * Appends one JSON line per skill load to ~/.cx/skill-calls.jsonl so a future
5
+ * `construct skills:audit` can answer two questions: which skills have no
6
+ * load events in a 30-day window (pruning candidates), and which skills
7
+ * receive load events from many sources (hot paths). Without this signal
8
+ * the 131-file skills tree grows without a pruning gate.
9
+ *
10
+ * Disable with `CONSTRUCT_SKILL_TELEMETRY=off` for users who don't want the
11
+ * append-only log. Errors here are non-fatal — telemetry must never break the
12
+ * skill load it is observing.
13
+ */
14
+
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+
19
+ export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'skill-calls.jsonl');
20
+
21
+ /**
22
+ * Fire-and-forget log of a skill load event.
23
+ *
24
+ * @param {object} event
25
+ * @param {string} event.skillId — path-relative-to-skills/ without the .md, e.g. "roles/engineer.platform"
26
+ * @param {'mcp'|'prompt-composer'|'role-preload'|'pattern-promotion'|'validation'|'other'} event.source
27
+ * @param {string} [event.callerContext] — optional free-form context (agent name, MCP client id, etc.)
28
+ * @param {object} [opts]
29
+ * @param {string} [opts.logPath] — override the default log path (tests pass a tmpdir)
30
+ * @param {NodeJS.ProcessEnv} [opts.env] — override env for the disable check (tests)
31
+ */
32
+ export function logSkillCall(event, opts = {}) {
33
+ const env = opts.env || process.env;
34
+ if (env.CONSTRUCT_SKILL_TELEMETRY === 'off') return;
35
+ if (!event || !event.skillId || !event.source) return;
36
+
37
+ const logPath = opts.logPath || DEFAULT_LOG_PATH;
38
+ const entry = {
39
+ ts: new Date().toISOString(),
40
+ skillId: event.skillId,
41
+ source: event.source,
42
+ ...(event.callerContext ? { callerContext: event.callerContext } : {}),
43
+ };
44
+
45
+ try {
46
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
47
+ fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf8');
48
+ } catch {
49
+ // Append-only telemetry; if the user's home dir is unwritable we silently
50
+ // skip rather than break the skill load that triggered us.
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Read every event from a skill-calls log and roll it up into per-skill stats:
56
+ * load count, distinct sources, most-recent timestamp.
57
+ */
58
+ export function summarizeSkillCalls({ logPath = DEFAULT_LOG_PATH } = {}) {
59
+ if (!fs.existsSync(logPath)) return { totalEvents: 0, skills: {} };
60
+ const lines = fs.readFileSync(logPath, 'utf8').split('\n').filter(Boolean);
61
+ const skills = {};
62
+ for (const line of lines) {
63
+ let entry;
64
+ try { entry = JSON.parse(line); } catch { continue; }
65
+ if (!entry?.skillId) continue;
66
+ const slot = skills[entry.skillId] ||= { calls: 0, sources: new Set(), lastCalledAt: null };
67
+ slot.calls += 1;
68
+ if (entry.source) slot.sources.add(entry.source);
69
+ if (entry.ts && (!slot.lastCalledAt || entry.ts > slot.lastCalledAt)) {
70
+ slot.lastCalledAt = entry.ts;
71
+ }
72
+ }
73
+ const result = {};
74
+ for (const [id, slot] of Object.entries(skills)) {
75
+ result[id] = { calls: slot.calls, sources: [...slot.sources].sort(), lastCalledAt: slot.lastCalledAt };
76
+ }
77
+ return { totalEvents: lines.length, skills: result };
78
+ }
@@ -3,22 +3,22 @@
3
3
  *
4
4
  * Groups agent traces by teamId (stamped by construct headhunt at overlay creation),
5
5
  * computes per-team success rates, latency, handoff counts, and common blockers.
6
- * Backend is selected via CONSTRUCT_TRACE_BACKEND env var (default: langfuse).
6
+ * Backend is selected via CONSTRUCT_TRACE_BACKEND env var (default: remote).
7
7
  * Called by 'construct team review'.
8
8
  */
9
9
 
10
- import * as langfuseBackend from './backends/langfuse.mjs';
10
+ import * as remoteBackend from './backends/remote.mjs';
11
11
  import * as noopBackend from './backends/noop.mjs';
12
12
 
13
13
  const BACKENDS = {
14
- langfuse: langfuseBackend,
14
+ remote: remoteBackend,
15
15
  noop: noopBackend,
16
16
  };
17
17
 
18
18
  const DEFAULT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
19
19
 
20
20
  function resolveBackend() {
21
- const name = process.env.CONSTRUCT_TRACE_BACKEND ?? 'langfuse';
21
+ const name = process.env.CONSTRUCT_TRACE_BACKEND ?? 'remote';
22
22
  const backend = BACKENDS[name];
23
23
  if (!backend) throw new Error(`Unknown trace backend: ${name}. Available: ${Object.keys(BACKENDS).join(', ')}`);
24
24
  return backend;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * lib/token-engine.js — Model-agnostic token estimation engine.
3
+ *
4
+ * Follows embeddings-engine.js pattern:
5
+ * - This file is the model-agnostic router.
6
+ * - Provider-specific logic lives in token-estimator-*.js adapters.
7
+ * - All adapters return: { estimate: function(text): number }
8
+ *
9
+ * Supported providers:
10
+ * - anthropic → ~3.5 chars/token (Claude)
11
+ * - openai → ~4 chars/token (GPT-4o)
12
+ * - google → ~4 chars/token (Gemini)
13
+ * - deepseek → ~3 chars/token
14
+ * - default → ~4 chars/token (conservative)
15
+ */
16
+ import { resolveProviderCapabilities } from './provider-capabilities.js';
17
+
18
+ const ADAPTERS = {
19
+ anthropic: () => import('./token-estimator-anthropic.js'),
20
+ 'anthropic-direct': () => import('./token-estimator-anthropic.js'),
21
+ openai: () => import('./token-estimator-openai.js'),
22
+ 'openai-direct': () => import('./token-estimator-openai.js'),
23
+ google: () => import('./token-estimator-google.js'),
24
+ 'google-direct': () => import('./token-estimator-google.js'),
25
+ deepseek: () => import('./token-estimator-deepseek.js'),
26
+ default: () => import('./token-estimator-default.js'),
27
+ };
28
+
29
+ function resolveAdapterKey(modelId) {
30
+ const id = String(modelId || '').toLowerCase();
31
+ if (/^anthropic\//.test(id) || /^openrouter\/anthropic\//.test(id)) return 'anthropic';
32
+ if (/^openai\//.test(id) || /^openrouter\/openai\//.test(id) || /^github-copilot\//.test(id)) return 'openai';
33
+ if (/^google\//.test(id) || /^openrouter\/google\//.test(id)) return 'google';
34
+ if (/^deepseek\//.test(id) || /^openrouter\/deepseek\//.test(id)) return 'deepseek';
35
+ return 'default';
36
+ }
37
+
38
+ /**
39
+ * Estimate tokens for a given text and model.
40
+ * Uses provider-specific ratios when available, falls back to default.
41
+ *
42
+ * @param {string} text
43
+ * @param {object} opts - { modelId: string }
44
+ * @returns {Promise<number>}
45
+ */
46
+ export async function estimateTokens(text, { modelId = 'default' } = {}) {
47
+ const adapterKey = resolveAdapterKey(modelId);
48
+ const loader = ADAPTERS[adapterKey] || ADAPTERS.default;
49
+
50
+ try {
51
+ const { estimate } = await loader();
52
+ return estimate(text);
53
+ } catch {
54
+ // Fallback to default
55
+ const { estimate } = await ADAPTERS.default();
56
+ return estimate(text);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Synchronous version — uses cached capabilities when possible.
62
+ * Returns 0 on error.
63
+ *
64
+ * @param {string} text
65
+ * @param {object} opts - { modelId: string }
66
+ * @returns {number}
67
+ */
68
+ export function estimateTokensSync(text, { modelId = 'default' } = {}) {
69
+ try {
70
+ // Try to get the ratio from provider capabilities (sync)
71
+ const caps = resolveProviderCapabilitiesSync(modelId);
72
+ const ratio = caps?.tokenRatio || 4;
73
+ const len = (text || '').length;
74
+ return Math.ceil(len / ratio);
75
+ } catch {
76
+ // Fallback: 4 chars per token
77
+ return Math.ceil((text || '').length / 4);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Estimate tokens for an array of texts.
83
+ *
84
+ * @param {string[]} texts
85
+ * @param {object} opts
86
+ * @returns {Promise<number[]>}
87
+ */
88
+ export async function estimateTokensBatch(texts, opts = {}) {
89
+ return Promise.all((texts || []).map(t => estimateTokens(t, opts)));
90
+ }
91
+
92
+ /**
93
+ * Estimate the total tokens for a prompt structure (from composePrompt).
94
+ *
95
+ * @param {object} promptStructure - { system, messages, fragments }
96
+ * @param {object} opts
97
+ * @returns {Promise<number>}
98
+ */
99
+ export async function estimatePromptTokens(promptStructure, opts = {}) {
100
+ const { system, messages, fragments } = promptStructure || {};
101
+ let total = 0;
102
+
103
+ if (system) total += await estimateTokens(system, opts);
104
+ if (Array.isArray(messages)) {
105
+ for (const msg of messages) {
106
+ const content = typeof msg?.content === 'string' ? msg.content : JSON.stringify(msg.content);
107
+ total += await estimateTokens(content, opts);
108
+ }
109
+ }
110
+ if (Array.isArray(fragments)) {
111
+ for (const f of fragments) {
112
+ total += await estimateTokens(f?.content || '', opts);
113
+ }
114
+ }
115
+
116
+ return total;
117
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * lib/token-estimator-anthropic.js — Anthropic Claude token estimator.
3
+ *
4
+ * Claude models use ~3.5 chars per token (more efficient than GPT).
5
+ * Uses approximation since we can't load the real tokenizer in Node.
6
+ */
7
+ export function estimate(text) {
8
+ // Claude: ~3.5 chars per token
9
+ const chars = (text || '').length;
10
+ return Math.ceil(chars / 3.5);
11
+ }
12
+
13
+ export async function estimateBatch(texts) {
14
+ return (texts || []).map(t => estimate(t));
15
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * lib/token-estimator-deepseek.js — DeepSeek token estimator.
3
+ *
4
+ * DeepSeek models use ~3 chars per token (similar to Claude).
5
+ */
6
+ export function estimate(text) {
7
+ // DeepSeek: ~3 chars per token
8
+ return Math.ceil((text || '').length / 3);
9
+ }
10
+
11
+ export async function estimateBatch(texts) {
12
+ return (texts || []).map(t => estimate(t));
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * lib/token-estimator-default.js — Conservative default token estimator.
3
+ *
4
+ * Uses ~4 chars per token (safe for most models).
5
+ */
6
+ export function estimate(text) {
7
+ // Conservative default: 4 chars per token
8
+ return Math.ceil((text || '').length / 4);
9
+ }
10
+
11
+ export async function estimateBatch(texts) {
12
+ return (texts || []).map(t => estimate(t));
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * lib/token-estimator-google.js — Google Gemini token estimator.
3
+ *
4
+ * Gemini models use ~4 chars per token.
5
+ */
6
+ export function estimate(text) {
7
+ // Gemini: ~4 chars per token
8
+ return Math.ceil((text || '').length / 4);
9
+ }
10
+
11
+ export async function estimateBatch(texts) {
12
+ return (texts || []).map(t => estimate(t));
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * lib/token-estimator-openai.js — OpenAI GPT token estimator.
3
+ *
4
+ * GPT models use ~4 chars per token.
5
+ */
6
+ export function estimate(text) {
7
+ // GPT: ~4 chars per token
8
+ return Math.ceil((text || '').length / 4);
9
+ }
10
+
11
+ export async function estimateBatch(texts) {
12
+ return (texts || []).map(t => estimate(t));
13
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/toolkit-env.mjs — Load `.env` from CX_TOOLKIT_DIR into process.env.
3
3
  *
4
- * Shared by OpenCode plugin and MCP server so Langfuse/OpenRouter creds
4
+ * Shared by OpenCode plugin and MCP server so telemetry/OpenRouter creds
5
5
  * reach every host that drives Construct.
6
6
  */
7
7
  import { existsSync, readFileSync } from "node:fs";
@@ -0,0 +1,211 @@
1
+ /**
2
+ * lib/tty-prompts.mjs — terminal UI components for construct CLI.
3
+ *
4
+ * Provides menu and multi-select interfaces with keyboard navigation,
5
+ * rendering, and input handling for interactive prompts.
6
+ */
7
+
8
+ import readline, { createInterface } from 'node:readline';
9
+
10
+ const ESC = '\x1b[';
11
+ const ALT_SCREEN_ON = `${ESC}?1049h`;
12
+ const ALT_SCREEN_OFF = `${ESC}?1049l`;
13
+ const CURSOR_HIDE = `${ESC}?25l`;
14
+ const CURSOR_SHOW = `${ESC}?25h`;
15
+ const HOME_CLEAR = `${ESC}H${ESC}2J`;
16
+ const RESET = '\x1b[0m';
17
+ const BOLD = '\x1b[1m';
18
+ const DIM = '\x1b[2m';
19
+ const CYAN = '\x1b[36m';
20
+ const YELLOW = '\x1b[33m';
21
+
22
+ function truncate(text, maxWidth) {
23
+ if (!text) return '';
24
+ if (text.length <= maxWidth) return text;
25
+ return `${text.slice(0, Math.max(0, maxWidth - 1))}...`;
26
+ }
27
+
28
+ function wrapText(text, width) {
29
+ if (!text) return [];
30
+ const words = String(text).split(/\s+/).filter(Boolean);
31
+ const lines = [];
32
+ let current = '';
33
+ for (const word of words) {
34
+ const next = current ? `${current} ${word}` : word;
35
+ if (next.length <= width) {
36
+ current = next;
37
+ continue;
38
+ }
39
+ if (current) lines.push(current);
40
+ current = word.length > width ? truncate(word, width) : word;
41
+ }
42
+ if (current) lines.push(current);
43
+ return lines;
44
+ }
45
+
46
+ function renderFrame(content) {
47
+ return `${HOME_CLEAR}${content}`;
48
+ }
49
+
50
+ function renderDetailBlock(label, text, width) {
51
+ const lines = wrapText(text, width);
52
+ if (!lines.length) return '';
53
+ return `${BOLD}${label}:${RESET}\n${lines.map((line) => ` ${line}`).join('\n')}\n`;
54
+ }
55
+
56
+ function createMenuSession(render, onKeypress) {
57
+ return new Promise((resolve, reject) => {
58
+ const rl = createInterface({
59
+ input: process.stdin,
60
+ output: process.stdout,
61
+ terminal: true,
62
+ escapeCodeTimeout: 50,
63
+ });
64
+
65
+ readline.emitKeypressEvents(process.stdin, rl);
66
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
67
+ process.stdin.resume();
68
+ process.stdout.write(`${ALT_SCREEN_ON}${CURSOR_HIDE}`);
69
+
70
+ function redraw() {
71
+ process.stdout.write(renderFrame(render()));
72
+ }
73
+
74
+ function cleanup() {
75
+ process.stdin.removeListener('keypress', handleKeypress);
76
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
77
+ rl.close();
78
+ process.stdin.pause();
79
+ process.stdout.write(`${CURSOR_SHOW}${ALT_SCREEN_OFF}`);
80
+ }
81
+
82
+ function finish(value) {
83
+ cleanup();
84
+ resolve(value);
85
+ }
86
+
87
+ function cancel(error) {
88
+ cleanup();
89
+ reject(error);
90
+ }
91
+
92
+ function handleKeypress(_str, key = {}) {
93
+ try {
94
+ if (key.ctrl && key.name === 'c') {
95
+ cancel(new Error('Canceled by user.'));
96
+ return;
97
+ }
98
+ const result = onKeypress(key, { redraw, finish, cancel });
99
+ if (result !== false) redraw();
100
+ } catch (error) {
101
+ cancel(error);
102
+ }
103
+ }
104
+
105
+ process.stdin.on('keypress', handleKeypress);
106
+ redraw();
107
+ });
108
+ }
109
+
110
+ export async function selectOption({ title, instructions, options, initialIndex = 0 }) {
111
+ let cursor = Math.min(Math.max(initialIndex, 0), Math.max(0, options.length - 1));
112
+
113
+ return createMenuSession(
114
+ () => {
115
+ const width = Math.max(60, (process.stdout.columns || 80) - 4);
116
+ const focused = options[cursor] ?? options[0];
117
+ const lines = [
118
+ `${BOLD}${title}${RESET}`,
119
+ `${DIM}${instructions}${RESET}`,
120
+ '',
121
+ ];
122
+
123
+ for (let index = 0; index < options.length; index += 1) {
124
+ const option = options[index];
125
+ const prefix = index === cursor ? `${CYAN}>${RESET}` : ' ';
126
+ const label = index === cursor ? `${BOLD}${option.label}${RESET}` : option.label;
127
+ const meta = option.meta ? ` ${DIM}${truncate(option.meta, 24)}${RESET}` : '';
128
+ lines.push(` ${prefix} ${label}${meta}`);
129
+ }
130
+
131
+ lines.push('');
132
+ if (focused) {
133
+ lines.push(renderDetailBlock('Details', focused.description, width).trimEnd());
134
+ }
135
+
136
+ return `${lines.filter(Boolean).join('\n')}\n`;
137
+ },
138
+ (key, controls) => {
139
+ if (key.name === 'up' || key.name === 'k' || (key.ctrl && key.name === 'p')) {
140
+ cursor = (cursor - 1 + options.length) % options.length;
141
+ } else if (key.name === 'down' || key.name === 'j' || (key.ctrl && key.name === 'n')) {
142
+ cursor = (cursor + 1) % options.length;
143
+ } else if (key.name === 'return' || key.name === 'enter') {
144
+ controls.finish(options[cursor]?.value ?? options[cursor]?.label);
145
+ } else if (key.name === 'escape' || key.name === 'q') {
146
+ controls.cancel(new Error('Canceled by user.'));
147
+ }
148
+ },
149
+ );
150
+ }
151
+
152
+ export async function multiSelect({ title, instructions, options }) {
153
+ const state = options.map((option) => ({ ...option }));
154
+ let cursor = 0;
155
+
156
+ return createMenuSession(
157
+ () => {
158
+ const width = Math.max(60, (process.stdout.columns || 80) - 4);
159
+ const focused = state[cursor] ?? state[0];
160
+ const selected = state.filter((option) => option.checked).map((option) => option.label);
161
+ const lines = [
162
+ `${BOLD}${title}${RESET}`,
163
+ `${DIM}${instructions}${RESET}`,
164
+ '',
165
+ ];
166
+
167
+ for (let index = 0; index < state.length; index += 1) {
168
+ const option = state[index];
169
+ const prefix = index === cursor ? `${CYAN}>${RESET}` : ' ';
170
+ const box = option.checked ? '[x]' : '[ ]';
171
+ const label = index === cursor ? `${BOLD}${option.label}${RESET}` : option.label;
172
+ const meta = option.meta ? ` ${DIM}${truncate(option.meta, 18)}${RESET}` : '';
173
+ const marker = option.suggestion ? ` ${YELLOW}*${RESET}` : '';
174
+ lines.push(` ${prefix} ${box} ${label}${meta}${marker}`);
175
+ }
176
+
177
+ lines.push('');
178
+ if (focused) {
179
+ lines.push(renderDetailBlock('Details', focused.description, width).trimEnd());
180
+ if (focused.suggestion) {
181
+ lines.push('');
182
+ lines.push(renderDetailBlock('Suggested because', focused.suggestion, width).trimEnd());
183
+ }
184
+ }
185
+ lines.push('');
186
+ lines.push(renderDetailBlock('Selected', selected.length ? selected.join(', ') : 'None yet.', width).trimEnd());
187
+ lines.push('');
188
+ lines.push(`${DIM}↑↓ Navigate • Space Toggle • Enter Confirm • A Select All • I Invert • Q Cancel${RESET}`);
189
+
190
+ return `${lines.filter(Boolean).join('\n')}\n`;
191
+ },
192
+ (key, controls) => {
193
+ if (key.name === 'up' || key.name === 'k' || (key.ctrl && key.name === 'p')) {
194
+ cursor = (cursor - 1 + state.length) % state.length;
195
+ } else if (key.name === 'down' || key.name === 'j' || (key.ctrl && key.name === 'n')) {
196
+ cursor = (cursor + 1) % state.length;
197
+ } else if (key.name === 'space') {
198
+ state[cursor].checked = !state[cursor].checked;
199
+ } else if (key.name === 'a') {
200
+ const allChecked = state.every((option) => option.checked);
201
+ state.forEach((option) => { option.checked = !allChecked; });
202
+ } else if (key.name === 'i') {
203
+ state.forEach((option) => { option.checked = !option.checked; });
204
+ } else if (key.name === 'return' || key.name === 'enter') {
205
+ controls.finish(state.filter((option) => option.checked).map((option) => option.value ?? option.label));
206
+ } else if (key.name === 'escape' || key.name === 'q') {
207
+ controls.cancel(new Error('Canceled by user.'));
208
+ }
209
+ },
210
+ );
211
+ }