@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,365 @@
1
+ /**
2
+ * lib/beads-optimistic.mjs — Optimistic locking and concurrent read support for beads.
3
+ *
4
+ * Addresses the beads lock contention issue by:
5
+ * 1. Allowing concurrent reads (no lock required)
6
+ * 2. Using optimistic locking for writes (version-based conflict detection)
7
+ * 3. Automatic retry with exponential backoff on conflict
8
+ * 4. Read-replicas for team mode (when available)
9
+ *
10
+ * Replaces the exclusive lock model with a more scalable approach
11
+ * while maintaining consistency guarantees.
12
+ */
13
+
14
+ import { spawnSync } from 'node:child_process';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Configuration
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const DEFAULT_RETRY_OPTIONS = {
26
+ maxRetries: 5,
27
+ baseDelayMs: 50,
28
+ maxDelayMs: 2000,
29
+ backoffMultiplier: 2,
30
+ };
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Version tracking for optimistic locking
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Read the current version of a bead from the database.
38
+ * In Dolt, this uses the commit hash as a logical version.
39
+ */
40
+ export async function getBeadVersion(beadId, cwd = process.cwd()) {
41
+ try {
42
+ const result = spawnSync(
43
+ 'bd',
44
+ ['show', beadId, '--json'],
45
+ { cwd, encoding: 'utf8', timeout: 5000 }
46
+ );
47
+
48
+ if (result.status !== 0) return null;
49
+
50
+ const data = JSON.parse(result.stdout);
51
+ // Dolt tracks commit hashes; use as version
52
+ return data.commitHash || data.version || Date.now().toString();
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Execute a write operation with optimistic locking.
60
+ *
61
+ * @param {Object} options
62
+ * @param {string} options.beadId - The bead being modified
63
+ * @param {Function} options.execute - Async function that performs the write
64
+ * @param {string} options.expectedVersion - Version we expect the bead to have
65
+ * @param {Object} options.retry - Retry configuration
66
+ * @returns {Promise<{success: boolean, result: any, attempts: number}>}
67
+ */
68
+ export async function optimisticWrite({
69
+ beadId,
70
+ execute,
71
+ expectedVersion,
72
+ retry = DEFAULT_RETRY_OPTIONS,
73
+ cwd = process.cwd(),
74
+ } = {}) {
75
+ let attempts = 0;
76
+ let delay = retry.baseDelayMs;
77
+
78
+ while (attempts < retry.maxRetries) {
79
+ attempts++;
80
+
81
+ // Verify version hasn't changed (optimistic check)
82
+ const currentVersion = await getBeadVersion(beadId, cwd);
83
+
84
+ if (expectedVersion && currentVersion !== expectedVersion) {
85
+ // Conflict detected - another writer modified the bead
86
+ if (attempts >= retry.maxRetries) {
87
+ return {
88
+ success: false,
89
+ error: `Optimistic lock conflict after ${attempts} attempts. Bead ${beadId} was modified by another process.`,
90
+ attempts,
91
+ currentVersion,
92
+ expectedVersion,
93
+ };
94
+ }
95
+
96
+ // Wait with exponential backoff and jitter
97
+ const jitter = Math.random() * 50;
98
+ await new Promise(r => setTimeout(r, delay + jitter));
99
+ delay = Math.min(delay * retry.backoffMultiplier, retry.maxDelayMs);
100
+ continue;
101
+ }
102
+
103
+ try {
104
+ // Execute the write operation
105
+ const result = await execute();
106
+ return { success: true, result, attempts };
107
+ } catch (error) {
108
+ if (attempts >= retry.maxRetries) {
109
+ return {
110
+ success: false,
111
+ error: error.message,
112
+ attempts,
113
+ };
114
+ }
115
+
116
+ // Retry on transient errors
117
+ const jitter = Math.random() * 50;
118
+ await new Promise(r => setTimeout(r, delay + jitter));
119
+ delay = Math.min(delay * retry.backoffMultiplier, retry.maxDelayMs);
120
+ }
121
+ }
122
+
123
+ return {
124
+ success: false,
125
+ error: `Max retries (${retry.maxRetries}) exceeded`,
126
+ attempts,
127
+ };
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Concurrent read operations (lock-free)
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Execute a read operation without acquiring any lock.
136
+ * Reads are inherently safe to run concurrently.
137
+ */
138
+ export async function concurrentRead(execute, cwd = process.cwd()) {
139
+ try {
140
+ const result = await execute();
141
+ return { success: true, result };
142
+ } catch (error) {
143
+ return {
144
+ success: false,
145
+ error: error.message,
146
+ };
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Batch read multiple beads concurrently.
152
+ * Much faster than sequential reads when querying multiple items.
153
+ */
154
+ export async function batchReadBeads(beadIds, cwd = process.cwd()) {
155
+ const promises = beadIds.map(id =>
156
+ concurrentRead(async () => {
157
+ const result = spawnSync(
158
+ 'bd',
159
+ ['show', id, '--json'],
160
+ { cwd, encoding: 'utf8', timeout: 5000 }
161
+ );
162
+ if (result.status !== 0) throw new Error(`Failed to read ${id}`);
163
+ return JSON.parse(result.stdout);
164
+ }, cwd)
165
+ );
166
+
167
+ const results = await Promise.all(promises);
168
+
169
+ return {
170
+ success: results.every(r => r.success),
171
+ results: results.map(r => r.result),
172
+ errors: results.filter(r => !r.success).map(r => r.error),
173
+ };
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Enhanced beads client with optimistic locking
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * Update a bead with optimistic locking.
182
+ * Primary implementation for lock-free updates.
183
+ */
184
+ export async function updateBeadOptimistic(
185
+ beadId,
186
+ updates,
187
+ options = {}
188
+ ) {
189
+ const { actor = 'construct', cwd = process.cwd(), notes } = options;
190
+
191
+ // Get current version before attempting update
192
+ const currentVersion = await getBeadVersion(beadId, cwd);
193
+
194
+ if (!currentVersion) {
195
+ return {
196
+ success: false,
197
+ error: `Bead ${beadId} not found`,
198
+ };
199
+ }
200
+
201
+ return optimisticWrite({
202
+ beadId,
203
+ expectedVersion: currentVersion,
204
+ cwd,
205
+ execute: async () => {
206
+ const args = ['update', beadId];
207
+
208
+ if (updates.status) args.push('--status', updates.status);
209
+ if (updates.assignee) args.push('--assignee', updates.assignee);
210
+ if (notes) args.push('--notes', notes);
211
+ if (updates.labels) {
212
+ for (const label of updates.labels) {
213
+ args.push('--label', label);
214
+ }
215
+ }
216
+
217
+ const result = spawnSync('bd', args, {
218
+ cwd,
219
+ encoding: 'utf8',
220
+ timeout: 10000,
221
+ env: { ...process.env, BEADS_ACTOR: actor },
222
+ });
223
+
224
+ if (result.status !== 0) {
225
+ throw new Error(result.stderr || 'Update failed');
226
+ }
227
+
228
+ return { beadId, updated: true };
229
+ },
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Claim a bead with optimistic locking.
235
+ * Handles the common case of concurrent claim attempts.
236
+ */
237
+ export async function claimBeadOptimistic(beadId, options = {}) {
238
+ const { actor = 'construct', cwd = process.cwd() } = options;
239
+
240
+ return updateBeadOptimistic(
241
+ beadId,
242
+ { assignee: actor },
243
+ { actor, cwd, notes: `Claimed by ${actor}` }
244
+ );
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Statistics and monitoring
249
+ // ---------------------------------------------------------------------------
250
+
251
+ /**
252
+ * Get statistics about lock contention.
253
+ * Useful for monitoring and tuning.
254
+ */
255
+ export async function getContentionStats(cwd = process.cwd()) {
256
+ const statsPath = path.join(cwd, '.beads', 'contention-stats.json');
257
+
258
+ try {
259
+ if (fs.existsSync(statsPath)) {
260
+ return JSON.parse(fs.readFileSync(statsPath, 'utf8'));
261
+ }
262
+ } catch {
263
+ // Ignore read errors
264
+ }
265
+
266
+ return {
267
+ totalOperations: 0,
268
+ conflicts: 0,
269
+ retries: 0,
270
+ avgAttempts: 1,
271
+ lastReset: new Date().toISOString(),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Record operation statistics.
277
+ */
278
+ export function recordOperationStats(
279
+ operation,
280
+ { success, attempts, conflict = false },
281
+ cwd = process.cwd()
282
+ ) {
283
+ const statsPath = path.join(cwd, '.beads', 'contention-stats.json');
284
+
285
+ try {
286
+ const current = fs.existsSync(statsPath)
287
+ ? JSON.parse(fs.readFileSync(statsPath, 'utf8'))
288
+ : { totalOperations: 0, conflicts: 0, retries: 0, operations: {} };
289
+
290
+ current.totalOperations++;
291
+ if (conflict) current.conflicts++;
292
+ if (attempts > 1) current.retries += (attempts - 1);
293
+
294
+ // Per-operation stats
295
+ if (!current.operations[operation]) {
296
+ current.operations[operation] = { count: 0, conflicts: 0, avgAttempts: 1 };
297
+ }
298
+ const op = current.operations[operation];
299
+ op.count++;
300
+ if (conflict) op.conflicts++;
301
+ op.avgAttempts = (op.avgAttempts * (op.count - 1) + attempts) / op.count;
302
+
303
+ fs.writeFileSync(statsPath, JSON.stringify(current, null, 2));
304
+ } catch {
305
+ // Best effort - don't fail the operation
306
+ }
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Migration path: backwards-compatible wrapper
311
+ // ---------------------------------------------------------------------------
312
+
313
+ /**
314
+ * Determine if we should use optimistic locking or fall back to legacy locking.
315
+ * Uses optimistic locking by default, unless explicitly disabled.
316
+ */
317
+ export function shouldUseOptimisticLocking(env = process.env) {
318
+ if (env.CONSTRUCT_BEADS_LEGACY_LOCK === '1') return false;
319
+ if (env.CONSTRUCT_BEADS_OPTIMISTIC === 'off') return false;
320
+ return true;
321
+ }
322
+
323
+ /**
324
+ * Smart operation router that chooses the best strategy.
325
+ */
326
+ export async function runBeadsOperation(operation, options = {}) {
327
+ const {
328
+ type = 'read', // 'read' | 'write' | 'claim' | 'update'
329
+ beadId,
330
+ execute,
331
+ fallbackToLegacy = true,
332
+ cwd = process.cwd(),
333
+ } = options;
334
+
335
+ // Reads never need locking
336
+ if (type === 'read') {
337
+ return concurrentRead(execute, cwd);
338
+ }
339
+
340
+ // Try optimistic locking if enabled
341
+ if (shouldUseOptimisticLocking() && beadId) {
342
+ const result = await optimisticWrite({
343
+ beadId,
344
+ execute,
345
+ cwd,
346
+ });
347
+
348
+ recordOperationStats(operation, result, cwd);
349
+
350
+ if (result.success || !fallbackToLegacy) {
351
+ return result;
352
+ }
353
+
354
+ // Fall back to legacy locking on persistent failure
355
+ console.error(`[beads] Optimistic locking failed, falling back to legacy lock: ${result.error}`);
356
+ }
357
+
358
+ // Legacy path: use the file-based lock
359
+ if (fallbackToLegacy) {
360
+ const { runBd } = await import('./beads-client.mjs');
361
+ return runBd([operation], { cwd });
362
+ }
363
+
364
+ return { success: false, error: 'Operation failed and fallback disabled' };
365
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * lib/bootstrap/built-ins.mjs — first-party resource definitions.
3
+ *
4
+ * Registers the resources Construct ships with: Node ≥ 18, git, Docker, the
5
+ * local ONNX embedding model, and Postgres+pgvector. The probes are
6
+ * deliberately lightweight — they answer "is this thing present and
7
+ * approximately healthy?" — and never block. The install paths handle the
8
+ * messier details (Docker container creation, model download).
9
+ *
10
+ * Calling `registerBuiltInResources()` is idempotent; re-registration is a
11
+ * no-op so this file can be imported multiple times.
12
+ */
13
+
14
+ import { spawnSync } from 'node:child_process';
15
+ import { existsSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import os from 'node:os';
18
+ import { registerResource, getResource } from './resources.mjs';
19
+
20
+ function probeCommandVersion(cmd, args = ['--version']) {
21
+ const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 3000 });
22
+ if (r.status !== 0) return { present: false };
23
+ const text = (r.stdout || r.stderr || '').trim().split('\n')[0];
24
+ return { present: true, version: text || cmd };
25
+ }
26
+
27
+ function probeNodeMin18() {
28
+ const r = spawnSync(process.execPath, ['--version'], { encoding: 'utf8', timeout: 1000 });
29
+ if (r.status !== 0) return { present: false };
30
+ const v = (r.stdout || '').trim().replace(/^v/, '');
31
+ const major = Number(v.split('.')[0]);
32
+ return {
33
+ present: major >= 18,
34
+ version: v,
35
+ healthy: major >= 18,
36
+ detail: major < 18 ? `Node ${v} is too old; need ≥ 18` : null,
37
+ };
38
+ }
39
+
40
+ async function probePostgresContainer() {
41
+ const docker = spawnSync('docker', ['ps', '--format', '{{.Names}}\t{{.Image}}'], {
42
+ encoding: 'utf8', timeout: 5000,
43
+ });
44
+ if (docker.status !== 0) return { present: false, detail: 'docker not running' };
45
+ const lines = docker.stdout.trim().split('\n').filter(Boolean);
46
+ const constructPg = lines.find((l) => /construct.*postgres|construct-pg|pgvector/.test(l));
47
+ if (!constructPg) return { present: false, detail: 'construct-postgres container not running' };
48
+ const [name] = constructPg.split('\t');
49
+ return {
50
+ present: true,
51
+ location: `docker:${name}`,
52
+ healthy: true,
53
+ detail: 'managed Postgres+pgvector container',
54
+ };
55
+ }
56
+
57
+ async function probeEmbeddingModel() {
58
+ const cachePath = join(
59
+ os.homedir(),
60
+ '.construct', 'cache', 'embeddings',
61
+ 'Xenova', 'all-MiniLM-L6-v2', 'onnx', 'model_quantized.onnx'
62
+ );
63
+ if (existsSync(cachePath)) {
64
+ return {
65
+ present: true,
66
+ location: cachePath,
67
+ healthy: true,
68
+ version: 'Xenova/all-MiniLM-L6-v2',
69
+ };
70
+ }
71
+ return { present: false, detail: 'will download on first embedding' };
72
+ }
73
+
74
+ let registered = false;
75
+
76
+ export function registerBuiltInResources() {
77
+ if (registered) return;
78
+ registered = true;
79
+
80
+ if (!getResource('node-runtime')) {
81
+ registerResource({
82
+ id: 'node-runtime',
83
+ displayName: 'Node.js 18+',
84
+ required: true,
85
+ consentKey: 'BOOTSTRAP_NODE',
86
+ detect: probeNodeMin18,
87
+ fallback: () => '(none — Node is required to run Construct)',
88
+ });
89
+ }
90
+
91
+ if (!getResource('git')) {
92
+ registerResource({
93
+ id: 'git',
94
+ displayName: 'git',
95
+ required: false,
96
+ consentKey: 'BOOTSTRAP_GIT',
97
+ detect: () => probeCommandVersion('git'),
98
+ fallback: () => 'Construct features that read git history degrade to "no history"',
99
+ });
100
+ }
101
+
102
+ if (!getResource('docker')) {
103
+ registerResource({
104
+ id: 'docker',
105
+ displayName: 'Docker',
106
+ required: false,
107
+ consentKey: 'BOOTSTRAP_DOCKER',
108
+ detect: () => probeCommandVersion('docker'),
109
+ fallback: () => 'Construct cannot manage local Postgres or run the binary container',
110
+ });
111
+ }
112
+
113
+ if (!getResource('embedding-model-local')) {
114
+ registerResource({
115
+ id: 'embedding-model-local',
116
+ displayName: 'Local ONNX embedding model (Xenova/all-MiniLM-L6-v2)',
117
+ required: false,
118
+ consentKey: 'BOOTSTRAP_EMBEDDING_MODEL',
119
+ downloadSize: 50 * 1024 * 1024,
120
+ detect: probeEmbeddingModel,
121
+ fallback: () => 'Construct embeddings degrade to hashing-bow-v1 (lower retrieval quality)',
122
+ });
123
+ }
124
+
125
+ if (!getResource('postgres-pgvector')) {
126
+ registerResource({
127
+ id: 'postgres-pgvector',
128
+ displayName: 'Postgres with pgvector',
129
+ required: false,
130
+ consentKey: 'BOOTSTRAP_POSTGRES',
131
+ downloadSize: 250 * 1024 * 1024,
132
+ detect: probePostgresContainer,
133
+ fallback: () => 'Construct retrieval falls back to local JSON vector index (slower, no SQL search)',
134
+ });
135
+ }
136
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * lib/bootstrap/lazy-install.mjs — on-demand resource installer with consent gating.
3
+ *
4
+ * When a runtime code path needs a resource that is not present, call
5
+ * `lazyInstall(id)`. The function:
6
+ *
7
+ * 1. Reads cached operator consent from config.env (consent key set by the
8
+ * resource definition in `lib/bootstrap/resources.mjs`).
9
+ * 2. If consent is 'never', returns { installed: false, fallback: <text> }
10
+ * without prompting or printing anything.
11
+ * 3. If a TTY is available, prompts the user with the resource displayName,
12
+ * estimated download size, and [y/n/never].
13
+ * 4. On 'y' or an existing 'yes' consent: calls resource.install(), waits
14
+ * for it to complete, updates config.env, and returns { installed: true }.
15
+ * 5. On 'n': returns { installed: false, fallback: <text> } without saving.
16
+ * 6. On 'never': writes 'never' to config.env and returns { installed: false }.
17
+ *
18
+ * Hooks that run without a TTY (most hook invocations) skip to step 2 and
19
+ * use the fallback silently.
20
+ *
21
+ * Consent is cached per-resource in config.env so the prompt never repeats
22
+ * across process restarts once the user has answered.
23
+ */
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+ import readline from 'node:readline';
29
+
30
+ import { getResource, probeResource } from './resources.mjs';
31
+
32
+ const CONFIG_ENV = path.join(os.homedir(), '.construct', 'config.env');
33
+
34
+ function readConfigEnv() {
35
+ try {
36
+ return fs.readFileSync(CONFIG_ENV, 'utf8');
37
+ } catch {
38
+ return '';
39
+ }
40
+ }
41
+
42
+ function readConsentFromEnv(consentKey) {
43
+ const lines = readConfigEnv().split('\n');
44
+ for (const line of lines) {
45
+ const trimmed = line.trim();
46
+ if (!trimmed || trimmed.startsWith('#')) continue;
47
+ const eq = trimmed.indexOf('=');
48
+ if (eq === -1) continue;
49
+ const k = trimmed.slice(0, eq).trim();
50
+ const v = trimmed.slice(eq + 1).trim().replace(/^['"]|['"]$/g, '');
51
+ if (k === consentKey) return v;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function writeConsentToEnv(consentKey, value) {
57
+ let content = readConfigEnv();
58
+ const regex = new RegExp(`^${consentKey}=.*$`, 'm');
59
+ const line = `${consentKey}=${value}`;
60
+ if (regex.test(content)) {
61
+ content = content.replace(regex, line);
62
+ } else {
63
+ content = content.trimEnd() + (content ? '\n' : '') + line + '\n';
64
+ }
65
+ fs.mkdirSync(path.dirname(CONFIG_ENV), { recursive: true });
66
+ fs.writeFileSync(CONFIG_ENV, content, { mode: 0o600 });
67
+ }
68
+
69
+ function hasTty() {
70
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
71
+ }
72
+
73
+ function formatBytes(bytes) {
74
+ if (!bytes) return '';
75
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
76
+ return `${Math.round(bytes / (1024 * 1024))} MB`;
77
+ }
78
+
79
+ async function promptUser(question) {
80
+ return new Promise((resolve) => {
81
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
82
+ rl.question(question, (answer) => {
83
+ rl.close();
84
+ resolve(answer.trim().toLowerCase());
85
+ });
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Attempt to install a resource lazily, respecting cached consent.
91
+ *
92
+ * @param {string} id - resource id from the registry
93
+ * @param {object} [opts]
94
+ * @param {boolean} [opts.silent] - skip TTY prompt, use cached consent only
95
+ * @returns {{ installed: boolean, fallback?: string, error?: string }}
96
+ */
97
+ export async function lazyInstall(id, { silent = false } = {}) {
98
+ const resource = getResource(id);
99
+ if (!resource) {
100
+ return { installed: false, error: `unknown resource: ${id}` };
101
+ }
102
+
103
+ const probe = await probeResource(resource);
104
+ if (probe.present && probe.healthy) {
105
+ return { installed: true };
106
+ }
107
+
108
+ const { consentKey, displayName, downloadSize, fallback } = resource;
109
+ const fallbackMsg = typeof fallback === 'function' ? fallback() : (fallback || '');
110
+
111
+ const cached = readConsentFromEnv(consentKey);
112
+
113
+ if (cached === 'never') {
114
+ return { installed: false, fallback: fallbackMsg };
115
+ }
116
+
117
+ if (cached === 'yes') {
118
+ return await performInstall(resource, fallbackMsg);
119
+ }
120
+
121
+ if (silent || !hasTty()) {
122
+ return { installed: false, fallback: fallbackMsg };
123
+ }
124
+
125
+ const size = downloadSize ? ` (~${formatBytes(downloadSize)})` : '';
126
+ const answer = await promptUser(
127
+ `\n[construct] ${displayName} required${size}. Install now? [y/n/never] `
128
+ );
129
+
130
+ if (answer === 'y' || answer === 'yes') {
131
+ writeConsentToEnv(consentKey, 'yes');
132
+ return await performInstall(resource, fallbackMsg);
133
+ }
134
+
135
+ if (answer === 'never') {
136
+ writeConsentToEnv(consentKey, 'never');
137
+ process.stderr.write(
138
+ `[construct] "${displayName}" will not be prompted again. Using fallback: ${fallbackMsg}\n`
139
+ );
140
+ return { installed: false, fallback: fallbackMsg };
141
+ }
142
+
143
+ return { installed: false, fallback: fallbackMsg };
144
+ }
145
+
146
+ async function performInstall(resource, fallbackMsg) {
147
+ if (typeof resource.install !== 'function') {
148
+ return { installed: false, fallback: fallbackMsg, error: 'resource has no install()' };
149
+ }
150
+ try {
151
+ process.stderr.write(`[construct] Installing ${resource.displayName}...\n`);
152
+ const result = await resource.install();
153
+ if (result?.success) {
154
+ process.stderr.write(`[construct] ${resource.displayName} installed.\n`);
155
+ return { installed: true };
156
+ }
157
+ return { installed: false, fallback: fallbackMsg, error: result?.log };
158
+ } catch (err) {
159
+ return { installed: false, fallback: fallbackMsg, error: err?.message || String(err) };
160
+ }
161
+ }