@geraldmaron/construct 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +10 -7
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1116 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +525 -38
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -1,24 +1,39 @@
1
1
  /**
2
2
  * lib/service-manager.mjs — start, stop, and describe Construct runtime services.
3
3
  *
4
- * Manages the dashboard process, local Langfuse (Docker), memory server (cm),
5
- * and OpenCode. startServices() is the single entry point for `construct up`;
6
- * it spawns whatever is available and returns a result per service.
4
+ * Manages the dashboard process, local Postgres (Docker), memory server (cm),
5
+ * and OpenCode. startServices() is the single entry point for `construct up` —
6
+ * spawns whatever is available and returns a result per service.
7
7
  */
8
8
  import { spawn, spawnSync } from 'node:child_process';
9
9
  import fs from 'node:fs';
10
+ import net from 'node:net';
10
11
  import os from 'node:os';
11
12
  import path from 'node:path';
12
13
 
13
14
  import { findAvailablePort } from './host-capabilities.mjs';
14
- import { getUserEnvPath, loadConstructEnv, writeEnvValues } from './env-config.mjs';
15
+ import { getUserEnvPath, loadConstructEnv, parseEnvFile, writeEnvValues } from './env-config.mjs';
15
16
  import { detectDockerCompose } from './setup.mjs';
17
+ import { stashConstructDb, restoreConstructDb } from './storage/postgres-backup.mjs';
18
+ import { runPressureRelease } from './runtime-pressure.mjs';
19
+ import {
20
+ pruneStashDir,
21
+ verifyTelemetryKeys,
22
+ isRemoteTelemetry,
23
+ startManagedServices,
24
+ } from './services/telemetry-backend.mjs';
25
+
26
+ const CONSTRUCT_PG_COMPOSE_DIR = 'services/postgres';
27
+ const CONSTRUCT_PG_CONTAINER = 'construct-postgres';
28
+ const CONSTRUCT_PG_PORT = 54329;
29
+ const CONSTRUCT_PG_HEALTH_RETRIES = 12;
30
+ const CONSTRUCT_PG_HEALTH_INTERVAL_MS = 2000;
16
31
 
17
32
  const DASHBOARD_STATE_FILE = 'dashboard.json';
18
33
 
19
- const LANGFUSE_LOCAL_BASEURL = 'http://localhost:3000';
20
- const LANGFUSE_LOCAL_PUBLIC_KEY = 'pk-lf-construct-local';
21
- const LANGFUSE_LOCAL_SECRET_KEY = 'sk-lf-construct-local';
34
+ // pruneStashDir and verifyTelemetryKeys now live in lib/services/telemetry-backend.mjs.
35
+ // Re-exported below at module bottom for tests that already import from here.
36
+
22
37
 
23
38
  function runtimeStateDir(homeDir = os.homedir()) {
24
39
  return path.join(homeDir, '.construct', 'runtime');
@@ -42,6 +57,70 @@ function writeJson(filePath, value) {
42
57
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
43
58
  }
44
59
 
60
+ function latestHandoffPath(rootDir) {
61
+ const handoffsDir = path.join(rootDir, '.cx', 'handoffs');
62
+ try {
63
+ const entries = fs.readdirSync(handoffsDir, { withFileTypes: true })
64
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
65
+ .map((entry) => {
66
+ const fullPath = path.join(handoffsDir, entry.name);
67
+ const stat = fs.statSync(fullPath);
68
+ return { path: fullPath, mtimeMs: stat.mtimeMs };
69
+ })
70
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
71
+ return entries[0]?.path || null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function relativeOrNull(rootDir, filePath) {
78
+ return filePath ? path.relative(rootDir, filePath) || path.basename(filePath) : null;
79
+ }
80
+
81
+ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), results = [], env = process.env } = {}) {
82
+ const unavailable = results
83
+ .filter((service) => ['unavailable', 'degraded', 'error', 'failed'].includes(service.status))
84
+ .map((service) => ({
85
+ name: service.name,
86
+ status: service.status,
87
+ note: service.note || null,
88
+ }));
89
+
90
+ const durable = {
91
+ plan: fs.existsSync(path.join(rootDir, 'plan.md')) ? 'plan.md' : null,
92
+ context: fs.existsSync(path.join(rootDir, '.cx', 'context.md')) ? '.cx/context.md' : null,
93
+ contextJson: fs.existsSync(path.join(rootDir, '.cx', 'context.json')) ? '.cx/context.json' : null,
94
+ latestHandoff: relativeOrNull(rootDir, latestHandoffPath(rootDir)),
95
+ beads: fs.existsSync(path.join(rootDir, '.beads', 'metadata.json')) ? '.beads/metadata.json' : null,
96
+ userConfig: fs.existsSync(getUserEnvPath(homeDir)) ? getUserEnvPath(homeDir) : null,
97
+ userDataRoot: env.CX_DATA_DIR || path.join(homeDir, '.cx'),
98
+ };
99
+
100
+ return {
101
+ durable,
102
+ degraded: unavailable,
103
+ canResumeFromFiles: Boolean(durable.plan || durable.context || durable.latestHandoff || durable.beads),
104
+ message: unavailable.length
105
+ ? 'Runtime is partially degraded; resume from durable project files and restart services when dependencies return.'
106
+ : 'Runtime services are available; durable project files remain the source of truth.',
107
+ };
108
+ }
109
+
110
+ export function isManagedConstructPostgresUrl(databaseUrl = '') {
111
+ if (!databaseUrl) return false;
112
+ try {
113
+ const parsed = new URL(databaseUrl);
114
+ const host = parsed.hostname;
115
+ return (
116
+ parsed.port === String(CONSTRUCT_PG_PORT) &&
117
+ ['127.0.0.1', 'localhost', '::1'].includes(host)
118
+ ) || host === CONSTRUCT_PG_CONTAINER;
119
+ } catch {
120
+ return databaseUrl.includes(`:${CONSTRUCT_PG_PORT}/`);
121
+ }
122
+ }
123
+
45
124
  function processExists(pid) {
46
125
  if (!pid || Number(pid) <= 0) return false;
47
126
  try {
@@ -52,13 +131,72 @@ function processExists(pid) {
52
131
  }
53
132
  }
54
133
 
55
- function spawnDetached(command, args, homeDir, logFile) {
134
+ async function probeRuntimePort(port, { host = '127.0.0.1', timeoutMs = 750 } = {}) {
135
+ if (!Number.isInteger(port) || port <= 0) return false;
136
+ return new Promise((resolve) => {
137
+ const socket = net.createConnection({ host, port });
138
+ let settled = false;
139
+ const settle = (value) => {
140
+ if (settled) return;
141
+ settled = true;
142
+ socket.destroy();
143
+ resolve(value);
144
+ };
145
+ socket.setTimeout(timeoutMs);
146
+ socket.once('connect', () => settle(true));
147
+ socket.once('timeout', () => settle(false));
148
+ socket.once('error', () => settle(false));
149
+ });
150
+ }
151
+
152
+ async function probeRuntimeHttp(url, {
153
+ method = 'GET',
154
+ headers = undefined,
155
+ body = undefined,
156
+ timeoutMs = 1000,
157
+ } = {}) {
158
+ try {
159
+ const controller = new AbortController();
160
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
161
+ const res = await fetch(url, { method, headers, body, signal: controller.signal });
162
+ clearTimeout(timer);
163
+ return res.ok;
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ async function isDashboardRunning(port) {
170
+ return probeRuntimeHttp(`http://127.0.0.1:${port}/`, { timeoutMs: 1000 });
171
+ }
172
+
173
+ async function isMemoryRunning(port) {
174
+ const mcpReady = await probeRuntimeHttp(`http://127.0.0.1:${port}/`, {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json' },
177
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
178
+ timeoutMs: 1000,
179
+ });
180
+ if (mcpReady) return true;
181
+ return probeRuntimePort(port);
182
+ }
183
+
184
+ async function isOpenCodeRunning(port) {
185
+ return probeRuntimeHttp(`http://127.0.0.1:${port}/`, { timeoutMs: 1000 });
186
+ }
187
+
188
+ function spawnDetached(command, args, homeDir, logFile, options = {}) {
56
189
  const logPath = path.join(runtimeStateDir(homeDir), logFile);
57
190
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
58
191
  const fd = fs.openSync(logPath, 'a');
59
- const child = spawn(command, args, { detached: true, stdio: ['ignore', fd, fd] });
192
+ const child = spawn(command, args, {
193
+ detached: true,
194
+ stdio: ['ignore', fd, fd],
195
+ cwd: options.cwd,
196
+ env: options.env,
197
+ });
60
198
  child.unref();
61
- return child;
199
+ return { child, logPath };
62
200
  }
63
201
 
64
202
  export function readDashboardState(homeDir = os.homedir()) {
@@ -90,9 +228,51 @@ export function stopDashboard(homeDir = os.homedir()) {
90
228
  return { stopped: true, pid: Number(state.pid), port: state.port };
91
229
  }
92
230
 
231
+ function doctorStatePath(homeDir) {
232
+ return path.join(homeDir, '.construct', 'doctor.json');
233
+ }
234
+
235
+ export function readDoctorState(homeDir = os.homedir()) {
236
+ const state = readJson(doctorStatePath(homeDir));
237
+ if (!state) return null;
238
+ if (!processExists(state.pid)) return null;
239
+ return state;
240
+ }
241
+
242
+ export function stopDoctor(homeDir = os.homedir()) {
243
+ const state = readJson(doctorStatePath(homeDir));
244
+ const file = doctorStatePath(homeDir);
245
+ if (!state?.pid) {
246
+ if (fs.existsSync(file)) fs.rmSync(file, { force: true });
247
+ return { stopped: false, reason: 'not-running' };
248
+ }
249
+ if (!processExists(state.pid)) {
250
+ if (fs.existsSync(file)) fs.rmSync(file, { force: true });
251
+ return { stopped: false, reason: 'stale-state' };
252
+ }
253
+ try { process.kill(Number(state.pid), 'SIGTERM'); } catch { /* gone */ }
254
+ return { stopped: true, pid: Number(state.pid) };
255
+ }
256
+
257
+ export function startDoctor({ rootDir, homeDir = os.homedir() } = {}) {
258
+ if (process.env.CONSTRUCT_DOCTOR === 'off') {
259
+ return { started: false, reason: 'disabled' };
260
+ }
261
+ const existing = readDoctorState(homeDir);
262
+ if (existing) return { started: false, reused: true, pid: existing.pid };
263
+ const indexPath = path.join(rootDir, 'lib', 'doctor', 'index.mjs');
264
+ if (!fs.existsSync(indexPath)) return { started: false, reason: 'missing-binary' };
265
+ const { logPath } = spawnDetached('node', [indexPath], homeDir, 'doctor.log', { cwd: rootDir, env: { ...process.env } });
266
+ return { started: true, logPath };
267
+ }
268
+
93
269
  export async function startDashboard({ rootDir, homeDir = os.homedir(), preferredPort = 4242 } = {}) {
94
270
  const existing = readDashboardState(homeDir);
95
- if (existing) return { started: false, reused: true, pid: existing.pid, port: existing.port, url: existing.url };
271
+ if (existing && await isDashboardRunning(existing.port)) {
272
+ return { started: false, reused: true, pid: existing.pid, port: existing.port, url: existing.url };
273
+ }
274
+
275
+ if (existing) clearDashboardState(homeDir);
96
276
 
97
277
  const port = await findAvailablePort(preferredPort);
98
278
  const outPath = path.join(runtimeStateDir(homeDir), 'dashboard.log');
@@ -117,12 +297,31 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
117
297
  return { started: true, reused: false, ...state };
118
298
  }
119
299
 
120
- export async function getRuntimePorts(homeDir = os.homedir()) {
300
+ /**
301
+ * Resolve runtime ports. Re-uses ports from config.env when the service is
302
+ * already listening there (avoids port drift on repeated `construct up`).
303
+ */
304
+ export async function getRuntimePorts(homeDir = os.homedir(), {
305
+ dashboardProbeFn = isDashboardRunning,
306
+ memoryProbeFn = isMemoryRunning,
307
+ openCodeProbeFn = isOpenCodeRunning,
308
+ findAvailablePortFn = findAvailablePort,
309
+ } = {}) {
121
310
  const dashboard = readDashboardState(homeDir);
311
+ const existing = parseEnvFile(getUserEnvPath(homeDir));
312
+
313
+ async function resolvePort(key, fallback, probe = async (port) => probeRuntimePort(port)) {
314
+ const saved = Number(existing[key]);
315
+ if (Number.isFinite(saved) && saved > 0) {
316
+ if (await probe(saved)) return saved;
317
+ }
318
+ return findAvailablePortFn(saved || fallback);
319
+ }
320
+
122
321
  return {
123
- dashboard: dashboard?.port ?? await findAvailablePort(4242),
124
- memory: await findAvailablePort(8765),
125
- bridge: await findAvailablePort(5173),
322
+ dashboard: dashboard?.port ?? await resolvePort('DASHBOARD_PORT', 4242, dashboardProbeFn),
323
+ memory: await resolvePort('MEMORY_PORT', 8765, memoryProbeFn),
324
+ bridge: await resolvePort('BRIDGE_PORT', 5173, openCodeProbeFn),
126
325
  };
127
326
  }
128
327
 
@@ -143,83 +342,348 @@ export async function describeRuntimeSupport() {
143
342
  };
144
343
  }
145
344
 
146
- export async function startServices({ rootDir, homeDir = os.homedir() } = {}) {
147
- const support = await describeRuntimeSupport();
148
- const ports = await getRuntimePorts(homeDir);
345
+ // ── Construct Postgres management ──────────────────────────────────────────
346
+
347
+ function constructPgComposePath(rootDir) {
348
+ return path.join(rootDir, CONSTRUCT_PG_COMPOSE_DIR, 'docker-compose.yml');
349
+ }
350
+
351
+ function isConstructPostgresRunning(spawnSyncFn = spawnSync) {
352
+ const result = spawnSyncFn('docker', ['inspect', '--format', '{{.State.Running}}', CONSTRUCT_PG_CONTAINER], {
353
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
354
+ });
355
+ return result.stdout?.trim() === 'true';
356
+ }
357
+
358
+ function isConstructPostgresHealthy(spawnSyncFn = spawnSync) {
359
+ const result = spawnSyncFn('docker', ['exec', CONSTRUCT_PG_CONTAINER, 'pg_isready', '-U', 'construct'], {
360
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
361
+ });
362
+ return result.status === 0;
363
+ }
364
+
365
+ async function waitForConstructPostgresHealthy({
366
+ spawnSyncFn = spawnSync,
367
+ maxRetries = CONSTRUCT_PG_HEALTH_RETRIES,
368
+ intervalMs = CONSTRUCT_PG_HEALTH_INTERVAL_MS,
369
+ } = {}) {
370
+ for (let i = 0; i < maxRetries; i++) {
371
+ if (isConstructPostgresHealthy(spawnSyncFn)) return true;
372
+ await new Promise((r) => setTimeout(r, intervalMs));
373
+ }
374
+ return false;
375
+ }
376
+
377
+ function startConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
378
+ const composeRunner = detectDockerComposeFn();
379
+ if (!composeRunner) return { status: 'unavailable', note: 'Docker not available' };
380
+
381
+ const composeFile = constructPgComposePath(rootDir);
382
+ if (!fs.existsSync(composeFile)) return { status: 'unavailable', note: 'Postgres compose file not found — run construct init first' };
383
+
384
+ const args = [...composeRunner.argsPrefix, '-p', 'construct-postgres', '-f', composeFile, 'up', '-d'];
385
+ const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
386
+ if (r.status === 0) return { status: 'started' };
387
+ return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose up failed' };
388
+ }
389
+
390
+ function stopConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
391
+ const composeRunner = detectDockerComposeFn();
392
+ if (!composeRunner) return { status: 'skipped', note: 'Docker not available' };
393
+
394
+ const composeFile = constructPgComposePath(rootDir);
395
+ if (!fs.existsSync(composeFile)) return { status: 'skipped', note: 'no compose file' };
396
+
397
+ const args = [...composeRunner.argsPrefix, '-p', 'construct-postgres', '-f', composeFile, 'down'];
398
+ const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
399
+ if (r.status === 0) return { status: 'stopped' };
400
+ return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose down failed' };
401
+ }
402
+
403
+ function checkPgvectorEnabled(spawnSyncFn = spawnSync) {
404
+ const result = spawnSyncFn('docker', [
405
+ 'exec', CONSTRUCT_PG_CONTAINER, 'psql', '-U', 'construct', '-d', 'construct',
406
+ '-tAc', "SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector')",
407
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
408
+ return result.stdout?.trim() === 't';
409
+ }
410
+
411
+ /**
412
+ * On macOS, when the Docker CLI is installed but the daemon is down,
413
+ * launch Docker Desktop and poll `docker info` up to `timeoutMs` for
414
+ * the daemon to come up. Opt-out via `CONSTRUCT_DISABLE_DOCKER_AUTOSTART=1`
415
+ * so users who keep Docker off aren't forced into it. No-ops on Linux
416
+ * (systemd needs sudo) and Windows (different launcher).
417
+ */
418
+ async function tryStartDockerDaemon({
419
+ env = process.env,
420
+ spawnSyncFn = spawnSync,
421
+ timeoutMs = 60_000,
422
+ pollMs = 1500,
423
+ } = {}) {
424
+ if (env.CONSTRUCT_DISABLE_DOCKER_AUTOSTART === '1') return false;
425
+ if (process.platform !== 'darwin') return false;
426
+
427
+ // CLI must be present; without it, nothing to start.
428
+ if (spawnSyncFn('command -v docker', { stdio: 'ignore', shell: true }).status !== 0) return false;
429
+
430
+ // If the daemon is already up, no work needed.
431
+ if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
432
+
433
+ // Best-effort launch. `open -a Docker` returns immediately; the daemon
434
+ // takes 5-30s to come fully up so we poll.
435
+ spawnSyncFn('open', ['-a', 'Docker'], { stdio: 'ignore' });
436
+
437
+ const deadline = Date.now() + timeoutMs;
438
+ while (Date.now() < deadline) {
439
+ await new Promise((r) => setTimeout(r, pollMs));
440
+ if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
441
+ }
442
+ return false;
443
+ }
444
+
445
+ export async function startServices({
446
+ rootDir,
447
+ homeDir = os.homedir(),
448
+ describeRuntimeSupportFn = describeRuntimeSupport,
449
+ getRuntimePortsFn = getRuntimePorts,
450
+ startDashboardFn = startDashboard,
451
+ detectDockerComposeFn = detectDockerCompose,
452
+ loadConstructEnvFn = loadConstructEnv,
453
+ spawnDetachedFn = spawnDetached,
454
+ verifyTelemetryKeysFn = verifyTelemetryKeys,
455
+ memoryProbeFn = isMemoryRunning,
456
+ openCodeProbeFn = isOpenCodeRunning,
457
+ runPressureReleaseFn = runPressureRelease,
458
+ tryStartDockerDaemonFn = tryStartDockerDaemon,
459
+ } = {}) {
460
+ const support = await describeRuntimeSupportFn();
461
+ const ports = await getRuntimePortsFn(homeDir);
149
462
  const envPath = getUserEnvPath(homeDir);
150
463
 
464
+ // On macOS, auto-launch Docker Desktop when the CLI is present but the
465
+ // daemon is down. Polls up to 60s so downstream Postgres startup sees a working
466
+ // compose runner instead of "Docker not available".
467
+ await tryStartDockerDaemonFn({ env: process.env });
468
+
151
469
  writeEnvValues(envPath, {
152
470
  DASHBOARD_PORT: String(ports.dashboard),
153
471
  MEMORY_PORT: String(ports.memory),
154
472
  BRIDGE_PORT: String(ports.bridge),
155
473
  });
156
474
 
475
+ // Construct Postgres — start if DATABASE_URL points to managed container
476
+ const liveEnv = loadConstructEnvFn({ rootDir, homeDir });
157
477
  const results = [];
478
+ const pressureReport = runPressureReleaseFn({ env: liveEnv });
479
+ if (pressureReport?.killed?.length) {
480
+ results.push({
481
+ name: 'Pressure Guard',
482
+ url: 'local://cleanup',
483
+ status: pressureReport.pressureTriggered ? 'started' : 'reused',
484
+ note: `terminated ${pressureReport.killed.length} stale process(es)`,
485
+ });
486
+ }
487
+ const databaseUrl = liveEnv.DATABASE_URL || '';
488
+ const usesManagedPostgres = isManagedConstructPostgresUrl(databaseUrl);
489
+
490
+ if (usesManagedPostgres) {
491
+ if (!isConstructPostgresRunning()) {
492
+ const pgStart = startConstructPostgres({ rootDir, homeDir, spawnSyncFn: spawnSync, detectDockerComposeFn: detectDockerComposeFn });
493
+ if (pgStart.status === 'started') {
494
+ const healthy = await waitForConstructPostgresHealthy();
495
+ if (healthy) {
496
+ const pgvector = checkPgvectorEnabled();
497
+ results.push({
498
+ name: 'Postgres',
499
+ url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
500
+ status: pgvector ? 'started' : 'degraded',
501
+ note: pgvector ? 'pgvector enabled' : 'pgvector not installed — semantic search unavailable',
502
+ });
503
+ } else {
504
+ results.push({ name: 'Postgres', status: 'degraded', note: 'container started but health check timed out' });
505
+ }
506
+ } else {
507
+ results.push({ name: 'Postgres', status: 'error', note: pgStart.note });
508
+ }
509
+ } else {
510
+ const pgvector = checkPgvectorEnabled();
511
+ results.push({
512
+ name: 'Postgres',
513
+ url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
514
+ status: 'reused',
515
+ note: pgvector ? 'pgvector enabled' : 'pgvector not installed',
516
+ });
517
+ }
518
+ } else if (databaseUrl) {
519
+ results.push({ name: 'Postgres', url: databaseUrl, status: 'configured', note: 'external database' });
520
+ }
158
521
 
159
522
  // Dashboard
160
- const dashboard = await startDashboard({ rootDir, homeDir, preferredPort: ports.dashboard });
523
+ const dashboard = await startDashboardFn({ rootDir, homeDir, preferredPort: ports.dashboard });
161
524
  results.push({
162
525
  name: 'Dashboard',
163
526
  url: dashboard.url,
164
527
  status: dashboard.reused ? 'reused' : 'started',
165
528
  });
166
529
 
167
- // Langfusestart local Docker unless a remote URL is explicitly configured
168
- const liveEnv = loadConstructEnv({ rootDir, homeDir });
169
- const langfuseUrl = liveEnv.LANGFUSE_BASEURL ?? '';
170
- const isRemoteLangfuse = langfuseUrl && !langfuseUrl.includes('localhost') && !langfuseUrl.includes('127.0.0.1');
530
+ // Telemetry backend delegated to lib/services/telemetry-backend.mjs.
531
+ // Remote URLs short-circuit inside startManagedServices and return `configured`.
171
532
 
172
- if (isRemoteLangfuse) {
173
- results.push({ name: 'Langfuse', url: langfuseUrl, status: 'configured' });
533
+ const telemetryUrl = liveEnv.CONSTRUCT_TELEMETRY_URL ?? '';
534
+ if (telemetryUrl && isRemoteTelemetry(telemetryUrl)) {
535
+ results.push({
536
+ name: 'Telemetry',
537
+ url: telemetryUrl,
538
+ status: 'configured',
539
+ note: `remote — ${telemetryUrl}`,
540
+ });
174
541
  } else {
175
- const composeRunner = detectDockerCompose();
542
+ const composeRunner = detectDockerComposeFn();
176
543
  if (composeRunner) {
177
- const composeFile = path.join(rootDir, 'langfuse', 'docker-compose.yml');
178
- const r = spawnSync(
179
- composeRunner.command,
180
- [...composeRunner.argsPrefix, '-p', 'construct-langfuse', '-f', composeFile, 'up', '-d'],
181
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
182
- );
183
- if (r.status === 0) {
184
- writeEnvValues(envPath, {
185
- LANGFUSE_BASEURL: LANGFUSE_LOCAL_BASEURL,
186
- LANGFUSE_PUBLIC_KEY: LANGFUSE_LOCAL_PUBLIC_KEY,
187
- LANGFUSE_SECRET_KEY: LANGFUSE_LOCAL_SECRET_KEY,
188
- });
189
- results.push({ name: 'Langfuse', url: LANGFUSE_LOCAL_BASEURL, status: 'started', note: 'containers starting — allow 30-60s' });
190
- } else {
191
- const msg = (r.stderr || r.stdout || 'docker compose failed').trim().split('\n')[0];
192
- results.push({ name: 'Langfuse', status: 'failed', note: msg });
193
- }
544
+ const svc = await startManagedServices({
545
+ rootDir,
546
+ homeDir,
547
+ env: liveEnv,
548
+ composeRunner,
549
+ spawnDetached: spawnDetachedFn,
550
+ });
551
+ results.push({ name: 'Telemetry', url: svc.url, status: svc.status, note: svc.note });
194
552
  } else {
195
- results.push({ name: 'Langfuse', status: 'unavailable', note: 'Docker not available' });
553
+ results.push({ name: 'Telemetry', status: 'unavailable', note: 'Docker not available' });
196
554
  }
197
555
  }
198
556
 
199
557
  // Memory (cm)
200
558
  if (support.cm) {
201
- spawnDetached('cm', ['serve', '--port', String(ports.memory)], homeDir, 'cm.log');
202
- results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'started' });
559
+ if (await memoryProbeFn(ports.memory)) {
560
+ results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'reused' });
561
+ } else {
562
+ spawnDetachedFn('cm', ['serve', '--port', String(ports.memory)], homeDir, 'cm.log');
563
+ results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'started' });
564
+ }
203
565
  } else {
204
- results.push({ name: 'Memory (cm)', status: 'unavailable', note: 'cm not installed — run: npm install -g @cass-ai/memory' });
566
+ results.push({ name: 'Memory (cm)', status: 'unavailable', note: 'cm not installed — run: construct init or brew install dicklesworthstone/tap/cm' });
205
567
  }
206
568
 
207
569
  // OpenCode (optional)
208
570
  if (support.opencode) {
209
- spawnDetached('opencode', ['serve', '--port', String(ports.bridge)], homeDir, 'opencode.log');
210
- results.push({ name: 'OpenCode', url: `http://127.0.0.1:${ports.bridge}`, status: 'started' });
571
+ if (await openCodeProbeFn(ports.bridge)) {
572
+ results.push({ name: 'OpenCode', url: `http://127.0.0.1:${ports.bridge}`, status: 'reused' });
573
+ } else {
574
+ spawnDetachedFn('opencode', ['serve', '--port', String(ports.bridge)], homeDir, 'opencode.log');
575
+ results.push({ name: 'OpenCode', url: `http://127.0.0.1:${ports.bridge}`, status: 'started' });
576
+ }
211
577
  }
212
578
 
213
- return { support, ports, results };
214
- }
579
+ const doctor = startDoctor({ rootDir, homeDir });
580
+ if (doctor.started) {
581
+ results.push({ name: 'Doctor', status: 'started', note: `L0 daemon · logs: ${doctor.logPath}` });
582
+ } else if (doctor.reused) {
583
+ results.push({ name: 'Doctor', status: 'reused', note: `pid ${doctor.pid}` });
584
+ } else if (doctor.reason !== 'disabled') {
585
+ results.push({ name: 'Doctor', status: 'unavailable', note: doctor.reason });
586
+ }
587
+
588
+ emitServiceFailures(results);
215
589
 
216
- export async function stopServices({ homeDir = os.homedir() } = {}) {
217
- const dashboard = stopDashboard(homeDir);
218
590
  return {
219
- stopped: dashboard.stopped ? ['dashboard'] : [],
220
- guidance: dashboard.stopped
221
- ? []
222
- : ['Stop Construct-related services with their native commands or your process manager.'],
223
- dashboard,
591
+ support,
592
+ ports,
593
+ results,
594
+ recovery: buildRuntimeRecoverySummary({ rootDir, homeDir, results, env: liveEnv }),
224
595
  };
225
596
  }
597
+
598
+ // Surface service probe failures into the role-framework event bus so cx-sre
599
+ // can be invoked when a critical service is down. Non-blocking, best effort.
600
+
601
+ async function emitServiceFailures(results) {
602
+ if (process.env.CONSTRUCT_ROLES === 'off') return;
603
+ const failed = results.filter((r) => r.status === 'error');
604
+ if (failed.length === 0) return;
605
+ try {
606
+ const { emitRoleEvent } = await import('./roles/hook-emit.mjs');
607
+ for (const r of failed) {
608
+ emitRoleEvent({
609
+ type: 'service.down',
610
+ summary: `${r.name} ${r.status}: ${r.note || 'no detail'}`,
611
+ hookInput: {},
612
+ context: { service: r.name, note: r.note, url: r.url },
613
+ });
614
+ }
615
+ } catch { /* best effort */ }
616
+ }
617
+
618
+ // Exported for testing only
619
+ export { verifyTelemetryKeys as _verifyTelemetryKeys, pruneStashDir as _pruneStashDir };
620
+
621
+ /**
622
+ * Kill every process listening on a given TCP port (best-effort, non-fatal).
623
+ * Returns true if at least one PID was killed.
624
+ */
625
+ function killPortOwners(port, spawnSyncFn = spawnSync) {
626
+ if (!port || !Number.isInteger(port) || port <= 0) return false;
627
+ try {
628
+ // lsof works on macOS and Linux; -t returns just PIDs
629
+ const result = spawnSyncFn('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
630
+ const pids = (result.stdout || '').trim().split(/\s+/).filter(Boolean).map(Number).filter((n) => n > 0);
631
+ if (pids.length === 0) return false;
632
+ for (const pid of pids) {
633
+ try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ }
634
+ }
635
+ return true;
636
+ } catch {
637
+ return false;
638
+ }
639
+ }
640
+
641
+ export async function stopServices({
642
+ homeDir = os.homedir(),
643
+ rootDir,
644
+ spawnSyncFn = spawnSync,
645
+ detectDockerComposeFn = detectDockerCompose,
646
+ } = {}) {
647
+ const results = [];
648
+
649
+ // Stash construct Postgres data before any container is stopped so data
650
+ // survives Docker restarts and machine reboots.
651
+ const constructDbStash = stashConstructDb({ homeDir, spawnSyncFn });
652
+
653
+ // Construct Postgres
654
+ const pgStop = stopConstructPostgres({ rootDir, homeDir, spawnSyncFn, detectDockerComposeFn });
655
+ results.push({ name: 'Postgres', status: pgStop.status });
656
+
657
+ // Doctor (L0 daemon)
658
+ const doctor = stopDoctor(homeDir);
659
+ if (doctor.stopped) {
660
+ results.push({ name: 'Doctor', status: 'stopped', note: `pid ${doctor.pid}` });
661
+ } else if (doctor.reason === 'stale-state') {
662
+ results.push({ name: 'Doctor', status: 'cleaned', note: 'stale state file removed' });
663
+ }
664
+
665
+ // Dashboard
666
+ const dashboard = stopDashboard(homeDir);
667
+ if (dashboard.stopped) {
668
+ results.push({ name: 'Dashboard', status: 'stopped', note: `pid ${dashboard.pid}, port ${dashboard.port}` });
669
+ } else if (dashboard.reason === 'stale-state') {
670
+ results.push({ name: 'Dashboard', status: 'cleaned', note: 'stale state file removed (process already gone)' });
671
+ } else {
672
+ results.push({ name: 'Dashboard', status: 'not-running' });
673
+ }
674
+
675
+ // Memory (cm) — find port from config.env or fall back to default
676
+ const envPath = getUserEnvPath(homeDir);
677
+ const envValues = parseEnvFile(envPath);
678
+ const memoryPort = Number(envValues.MEMORY_PORT) || 8765;
679
+ const cmKilled = killPortOwners(memoryPort, spawnSyncFn);
680
+ results.push({ name: 'Memory (cm)', status: cmKilled ? 'stopped' : 'not-running' });
681
+
682
+ // OpenCode — find port from config.env or fall back to default
683
+ const bridgePort = Number(envValues.BRIDGE_PORT) || 5173;
684
+ const openCodeKilled = killPortOwners(bridgePort, spawnSyncFn);
685
+ results.push({ name: 'OpenCode', status: openCodeKilled ? 'stopped' : 'not-running' });
686
+
687
+ const stopped = results.filter((r) => r.status === 'stopped' || r.status === 'cleaned').map((r) => r.name);
688
+ return { stopped, results, constructDbStash };
689
+ }