@geraldmaron/construct 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +10 -7
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1116 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +525 -38
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -0,0 +1,108 @@
1
+ /**
2
+ * lib/server/telemetry-login.mjs — magic-link bridge for a local telemetry backend.
3
+ *
4
+ * When a self-hosted telemetry backend (e.g. a self-hosted or cloud telemetry backend)
5
+ * is running locally, this bridge turns the "Open Telemetry" link in the
6
+ * dashboard into a one-click sign-in. The server fetches a CSRF token from
7
+ * the backend, then returns an HTML auto-submit form that POSTs the seeded
8
+ * credentials to NextAuth's callback.
9
+ *
10
+ * The seeded creds are NOT secret — they are deterministic local defaults
11
+ * for zero-touch local development. Not used when CONSTRUCT_TELEMETRY_URL
12
+ * points at a remote backend.
13
+ */
14
+
15
+ const HTML_HEADERS = { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' };
16
+
17
+ /**
18
+ * Resolve the telemetry backend URL from env with backward compat.
19
+ */
20
+ function resolveTelemetryUrl(env = process.env) {
21
+ return (
22
+ env.CONSTRUCT_TELEMETRY_URL ?? ''
23
+ ).replace(/\/$/, '');
24
+ }
25
+
26
+ export const TELEMETRY_LOGIN_DEFAULTS = Object.freeze({
27
+ baseUrl: '',
28
+ email: '',
29
+ password: '',
30
+ });
31
+
32
+ function escapeHtml(value) {
33
+ return String(value).replace(/[&<>"']/g, (c) => ({
34
+ '&': '&amp;',
35
+ '<': '&lt;',
36
+ '>': '&gt;',
37
+ '"': '&quot;',
38
+ "'": '&#39;',
39
+ })[c]);
40
+ }
41
+
42
+ /**
43
+ * Render the auto-submit form HTML. Pure for testing — pass everything
44
+ * the form needs and get back the string.
45
+ */
46
+ export function renderAutoSubmitForm({ csrfToken, callbackUrl, redirectAfter, email, password }) {
47
+ return `<!doctype html>
48
+ <html><head><meta charset="utf-8"><title>Signing in to telemetry backend…</title>
49
+ <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;color:#666;background:#fff}</style>
50
+ </head><body>
51
+ <p>Signing you in to telemetry backend…</p>
52
+ <form id="tf" method="POST" action="${escapeHtml(callbackUrl)}">
53
+ <input type="hidden" name="csrfToken" value="${escapeHtml(csrfToken)}">
54
+ <input type="hidden" name="email" value="${escapeHtml(email)}">
55
+ <input type="hidden" name="password" value="${escapeHtml(password)}">
56
+ <input type="hidden" name="callbackUrl" value="${escapeHtml(redirectAfter)}">
57
+ <input type="hidden" name="json" value="false">
58
+ </form>
59
+ <script>document.getElementById('tf').submit();</script>
60
+ </body></html>`;
61
+ }
62
+
63
+ /**
64
+ * HTTP handler: GET /api/services/telemetry/login
65
+ *
66
+ * Returns an HTML page that immediately POSTs credentials to the backend's
67
+ * NextAuth callback. Returns 502 when the backend is unreachable.
68
+ */
69
+ export async function handleTelemetryLogin(req, res, {
70
+ baseUrl,
71
+ email = '',
72
+ password = '',
73
+ fetchFn = globalThis.fetch,
74
+ } = {}) {
75
+ const resolvedBaseUrl = baseUrl ?? resolveTelemetryUrl();
76
+
77
+ if (!resolvedBaseUrl) {
78
+ res.writeHead(503, HTML_HEADERS);
79
+ res.end(`<!doctype html><meta charset="utf-8"><title>Telemetry backend not configured</title>` +
80
+ `<pre>CONSTRUCT_TELEMETRY_URL is not set. Configure a telemetry backend to use this feature.</pre>`);
81
+ return;
82
+ }
83
+
84
+ try {
85
+ const csrfResponse = await fetchFn(`${resolvedBaseUrl}/api/auth/csrf`, {
86
+ headers: { Accept: 'application/json' },
87
+ });
88
+ if (!csrfResponse.ok) {
89
+ res.writeHead(502, HTML_HEADERS);
90
+ res.end(`<!doctype html><meta charset="utf-8"><title>Telemetry backend unreachable</title>` +
91
+ `<pre>Could not reach telemetry backend at ${escapeHtml(resolvedBaseUrl)}. Status: ${csrfResponse.status}. ` +
92
+ `Try \`construct up\` first.</pre>`);
93
+ return;
94
+ }
95
+ const { csrfToken } = await csrfResponse.json();
96
+ const callbackUrl = `${resolvedBaseUrl}/api/auth/callback/credentials`;
97
+ const redirectAfter = `${resolvedBaseUrl}/`;
98
+ res.writeHead(200, HTML_HEADERS);
99
+ res.end(renderAutoSubmitForm({ csrfToken, callbackUrl, redirectAfter, email, password }));
100
+ } catch (err) {
101
+ res.writeHead(502, HTML_HEADERS);
102
+ res.end(`<!doctype html><meta charset="utf-8"><title>Telemetry backend unreachable</title>` +
103
+ `<pre>Could not reach telemetry backend: ${escapeHtml(err?.message || 'unknown error')}. ` +
104
+ `Try \`construct up\` first.</pre>`);
105
+ }
106
+ }
107
+
108
+
@@ -0,0 +1,510 @@
1
+ /**
2
+ * lib/server/webhook.mjs — Webhook ingestion + Slack slash command handler.
3
+ *
4
+ * Receives inbound webhook payloads from external providers (GitHub, Jira,
5
+ * Slack, Confluence, etc.), normalises them through the provider's
6
+ * `normalizeWebhook` capability, and routes the resulting event to:
7
+ *
8
+ * 1. The approval queue — if the event matches a configured approval rule.
9
+ * 2. A snapshot trigger — if the event is tagged as a significant change.
10
+ * 3. The SSE notification stream — so the dashboard updates in real-time.
11
+ *
12
+ * Route: POST /api/webhooks/:provider
13
+ *
14
+ * Security:
15
+ * Each provider validates its own signature. The handler calls
16
+ * provider.verifyWebhookSignature(payload, headers) before processing.
17
+ * Requests that fail signature verification are rejected with 401.
18
+ *
19
+ * Provider registration:
20
+ * Providers are loaded lazily from providers/<name>/index.mjs. A provider
21
+ * is eligible for webhook ingestion if it declares 'webhook' in its
22
+ * capabilities array and exports normalizeWebhook.
23
+ *
24
+ * Slack slash commands:
25
+ * Route: POST /api/slack/commands
26
+ * Requires SLACK_SIGNING_SECRET in config.env.
27
+ * Set the Slack app's slash command Request URL to:
28
+ * https://<your-domain>/api/slack/commands
29
+ *
30
+ * Supported commands:
31
+ * /construct snapshot — run a live embed snapshot and post summary
32
+ * /construct risks — surface escalating risks from the knowledge base
33
+ * /construct plan — post the current plan.md workflow summary
34
+ */
35
+
36
+ import { createHmac, timingSafeEqual } from 'crypto';
37
+ import { existsSync, readFileSync } from 'fs';
38
+ import { join } from 'path';
39
+ import { fileURLToPath } from 'url';
40
+ import { homedir as osHomedir } from 'os';
41
+
42
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
43
+ const ROOT_DIR = join(__dirname, '..', '..');
44
+ const HOME = osHomedir();
45
+
46
+ // ── Provider loader ────────────────────────────────────────────────────────
47
+
48
+ const _providerCache = new Map();
49
+
50
+ async function loadProvider(name) {
51
+ if (_providerCache.has(name)) return _providerCache.get(name);
52
+ const providerPath = join(ROOT_DIR, 'providers', name, 'index.mjs');
53
+ if (!existsSync(providerPath)) return null;
54
+ try {
55
+ const mod = await import(providerPath);
56
+ _providerCache.set(name, mod);
57
+ return mod;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ // ── Signature verification helpers ────────────────────────────────────────
64
+
65
+ /**
66
+ * GitHub: X-Hub-Signature-256: sha256=<hex>
67
+ */
68
+ function verifyGitHubSignature(body, headers, secret) {
69
+ const sig = headers['x-hub-signature-256'];
70
+ if (!sig || !secret) return !secret; // open if no secret configured
71
+ const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
72
+ try {
73
+ return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Jira: no standard HMAC — verify via shared token in X-Atlassian-Token header.
81
+ */
82
+ function verifyJiraSignature(body, headers, secret) {
83
+ if (!secret) return true;
84
+ const token = headers['x-atlassian-token'] || headers['x-webhook-token'];
85
+ if (!token) return false;
86
+ try {
87
+ return timingSafeEqual(Buffer.from(token), Buffer.from(secret));
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Slack: X-Slack-Signature: v0=<hex> with X-Slack-Request-Timestamp
95
+ */
96
+ function verifySlackSignature(body, headers, secret) {
97
+ const sig = headers['x-slack-signature'];
98
+ const ts = headers['x-slack-request-timestamp'];
99
+ if (!sig || !ts || !secret) return !secret;
100
+ // Reject requests older than 5 minutes
101
+ if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false;
102
+ const baseString = `v0:${ts}:${body}`;
103
+ const expected = 'v0=' + createHmac('sha256', secret).update(baseString).digest('hex');
104
+ try {
105
+ return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ const SIGNATURE_VERIFIERS = {
112
+ github: verifyGitHubSignature,
113
+ jira: verifyJiraSignature,
114
+ slack: verifySlackSignature,
115
+ confluence: verifyJiraSignature, // same Atlassian token scheme
116
+ };
117
+
118
+ // ── Event classification ────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Returns { significant: boolean, reason: string } for a normalised event.
122
+ * Significant events trigger an immediate snapshot; others are logged only.
123
+ */
124
+ function classifyEvent(providerName, event) {
125
+ const type = (event?.type || '').toLowerCase();
126
+ const SIGNIFICANT = [
127
+ 'pr.merged', 'pr.closed',
128
+ 'issue.created', 'issue.closed', 'issue.transitioned',
129
+ 'push',
130
+ 'message.posted',
131
+ 'page.created', 'page.updated',
132
+ ];
133
+ const significant = SIGNIFICANT.some(t => type === t || type.startsWith(t));
134
+ return { significant, reason: significant ? `${providerName}:${type} is a tracked event` : 'informational' };
135
+ }
136
+
137
+ // ── Main handler ────────────────────────────────────────────────────────────
138
+
139
+ /**
140
+ * @param {object} opts
141
+ * @param {ApprovalQueue} opts.approvalQueue
142
+ * @param {function} opts.notifyClients — triggers SSE broadcast
143
+ * @param {object} opts.webhookSecrets — { github: '...', slack: '...', ... }
144
+ */
145
+ export function createWebhookHandler({ approvalQueue, notifyClients, webhookSecrets = {} }) {
146
+ return async function handleWebhook(req, res) {
147
+ // Extract provider name from path: /api/webhooks/:provider
148
+ const match = req.url.match(/\/api\/webhooks\/([a-z0-9_-]+)/i);
149
+ if (!match) {
150
+ res.writeHead(400, { 'Content-Type': 'application/json' });
151
+ res.end(JSON.stringify({ error: 'Missing provider name in path' }));
152
+ return;
153
+ }
154
+
155
+ const providerName = match[1].toLowerCase();
156
+
157
+ // Collect body
158
+ const chunks = [];
159
+ await new Promise((resolve, reject) => {
160
+ req.on('data', c => chunks.push(c));
161
+ req.on('end', resolve);
162
+ req.on('error', reject);
163
+ });
164
+ const rawBody = Buffer.concat(chunks).toString('utf8');
165
+
166
+ // Signature verification
167
+ const verifier = SIGNATURE_VERIFIERS[providerName];
168
+ const secret = webhookSecrets[providerName] || process.env[`WEBHOOK_SECRET_${providerName.toUpperCase()}`];
169
+ if (verifier && !verifier(rawBody, req.headers, secret)) {
170
+ res.writeHead(401, { 'Content-Type': 'application/json' });
171
+ res.end(JSON.stringify({ error: 'Webhook signature verification failed' }));
172
+ return;
173
+ }
174
+
175
+ // Parse payload
176
+ let payload;
177
+ try {
178
+ payload = JSON.parse(rawBody || '{}');
179
+ } catch {
180
+ res.writeHead(400, { 'Content-Type': 'application/json' });
181
+ res.end(JSON.stringify({ error: 'Invalid JSON payload' }));
182
+ return;
183
+ }
184
+
185
+ // Normalise via provider if available
186
+ let event = { type: 'unknown', raw: payload, provider: providerName, receivedAt: new Date().toISOString() };
187
+ const provider = await loadProvider(providerName);
188
+ if (provider?.normalizeWebhook) {
189
+ try {
190
+ event = { ...event, ...provider.normalizeWebhook(payload, req.headers) };
191
+ } catch {
192
+ // Normalisation failure is non-fatal — log and continue with raw event
193
+ }
194
+ }
195
+
196
+ // Classify and route
197
+ const { significant } = classifyEvent(providerName, event);
198
+
199
+ if (significant && approvalQueue) {
200
+ // Queue for embed awareness (not necessarily requiring human approval)
201
+ approvalQueue.enqueue({
202
+ action: `webhook.${providerName}.${event.type || 'event'}`,
203
+ payload: event,
204
+ source: `webhook:${providerName}`,
205
+ autoApprove: true, // informational enqueue; hybrid model decides actual approval need
206
+ });
207
+ }
208
+
209
+ // Notify dashboard clients
210
+ if (notifyClients) notifyClients();
211
+
212
+ res.writeHead(200, { 'Content-Type': 'application/json' });
213
+ res.end(JSON.stringify({ ok: true, provider: providerName, type: event.type, significant }));
214
+ };
215
+ }
216
+
217
+ // ── Slack slash command handler ────────────────────────────────────────────
218
+
219
+ /**
220
+ * Parse application/x-www-form-urlencoded body into an object.
221
+ */
222
+ function parseFormBody(raw) {
223
+ const out = {};
224
+ for (const pair of raw.split('&')) {
225
+ const eq = pair.indexOf('=');
226
+ if (eq === -1) continue;
227
+ const k = decodeURIComponent(pair.slice(0, eq).replace(/\+/g, ' '));
228
+ const v = decodeURIComponent(pair.slice(eq + 1).replace(/\+/g, ' '));
229
+ out[k] = v;
230
+ }
231
+ return out;
232
+ }
233
+
234
+ /**
235
+ * Post a message back to Slack using response_url (no bot token required for
236
+ * slash command responses). Falls back to bot token + chat.postMessage if the
237
+ * response_url is absent (e.g. in tests).
238
+ */
239
+ async function postSlackMessage(responseUrl, text, botToken) {
240
+ const body = JSON.stringify({ response_type: 'in_channel', text });
241
+
242
+ if (responseUrl) {
243
+ const { fetch: nodeFetch } = await import('node:http');
244
+ // Use built-in fetch (Node 18+) or a simple https post
245
+ const url = new URL(responseUrl);
246
+ const isHttps = url.protocol === 'https:';
247
+ const mod = isHttps ? await import('node:https') : await import('node:http');
248
+ return new Promise((resolve, reject) => {
249
+ const req = mod.request(responseUrl, {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
252
+ }, (res) => {
253
+ res.resume();
254
+ res.on('end', resolve);
255
+ });
256
+ req.on('error', reject);
257
+ req.write(body);
258
+ req.end();
259
+ });
260
+ }
261
+
262
+ if (botToken) {
263
+ const https = await import('node:https');
264
+ const postBody = JSON.stringify({ text });
265
+ return new Promise((resolve, reject) => {
266
+ const req = https.request('https://slack.com/api/chat.postMessage', {
267
+ method: 'POST',
268
+ headers: {
269
+ 'Content-Type': 'application/json',
270
+ 'Authorization': `Bearer ${botToken}`,
271
+ 'Content-Length': Buffer.byteLength(postBody),
272
+ },
273
+ }, (res) => { res.resume(); res.on('end', resolve); });
274
+ req.on('error', reject);
275
+ req.write(postBody);
276
+ req.end();
277
+ });
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Load config.env values as an object.
283
+ */
284
+ function loadConfigEnv() {
285
+ const envFile = join(HOME, '.construct', 'config.env');
286
+ if (!existsSync(envFile)) return {};
287
+ const lines = readFileSync(envFile, 'utf8').split('\n');
288
+ const out = {};
289
+ for (const line of lines) {
290
+ const trimmed = line.trim();
291
+ if (!trimmed || trimmed.startsWith('#')) continue;
292
+ const eq = trimmed.indexOf('=');
293
+ if (eq === -1) continue;
294
+ out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim().replace(/^['"]|['"]$/g, '');
295
+ }
296
+ return out;
297
+ }
298
+
299
+ /**
300
+ * Run an embed snapshot and return a formatted Slack message string.
301
+ */
302
+ async function runSnapshotForSlack() {
303
+ try {
304
+ const env = { ...process.env, ...loadConfigEnv() };
305
+ const { ProviderRegistry } = await import('../embed/providers/registry.mjs');
306
+ const { SnapshotEngine, renderMarkdown } = await import('../embed/snapshot.mjs');
307
+ const { EMPTY_CONFIG } = await import('../embed/config.mjs');
308
+
309
+ const registry = ProviderRegistry.fromEnv(env);
310
+ const sources = registry.autoSources(env);
311
+ if (!sources.length) return '⚠️ No embed sources configured. Add credentials to config.env.';
312
+
313
+ const config = { ...EMPTY_CONFIG, sources, snapshot: { maxItems: 10 } };
314
+ const engine = new SnapshotEngine(registry, config);
315
+ const snapshot = await engine.generate();
316
+
317
+ const totalItems = snapshot.sections.reduce((n, s) => n + s.items.length, 0);
318
+ const errorCount = snapshot.errors.length;
319
+
320
+ const lines = [`*Construct Snapshot* — ${new Date(snapshot.generatedAt).toLocaleString()}`];
321
+ lines.push(`${totalItems} items across ${snapshot.sections.length} sources${errorCount ? ` (${errorCount} errors)` : ''}`);
322
+ lines.push('');
323
+
324
+ for (const section of snapshot.sections) {
325
+ if (!section.items.length) continue;
326
+ lines.push(`*${section.provider}* (${section.items.length})`);
327
+ for (const item of section.items.slice(0, 5)) {
328
+ const title = item.title || item.summary || item.text || '(untitled)';
329
+ const url = item.url || item.html_url || '';
330
+ lines.push(url ? `• <${url}|${title}>` : `• ${title}`);
331
+ }
332
+ if (section.items.length > 5) lines.push(` _…and ${section.items.length - 5} more_`);
333
+ }
334
+
335
+ if (errorCount) {
336
+ lines.push('');
337
+ lines.push(`⚠️ Errors: ${snapshot.errors.map(e => `${e.source}: ${e.error}`).join(', ')}`);
338
+ }
339
+
340
+ return lines.join('\n');
341
+ } catch (err) {
342
+ return `❌ Snapshot failed: ${err.message}`;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Surface escalating risks from the knowledge base.
348
+ */
349
+ async function runRisksForSlack() {
350
+ try {
351
+ const { buildTrendReport } = await import('../knowledge/trends.mjs');
352
+ const report = buildTrendReport(ROOT_DIR);
353
+
354
+ if (!report.escalatingRisks?.length && !report.decisionDrift?.length) {
355
+ return '✅ No escalating risks detected in the current knowledge base.';
356
+ }
357
+
358
+ const lines = ['*Construct Risks*', ''];
359
+
360
+ if (report.escalatingRisks?.length) {
361
+ lines.push('*Escalating Risks*');
362
+ for (const r of report.escalatingRisks) {
363
+ lines.push(`• ${r.summary} _(↑${r.escalationScore}× — ${r.recentCount} recent)_`);
364
+ }
365
+ }
366
+
367
+ if (report.decisionDrift?.length) {
368
+ lines.push('');
369
+ lines.push('*Decision Drift*');
370
+ for (const d of report.decisionDrift) {
371
+ lines.push(`• ${d.decision.summary} _(drift score ${d.driftScore})_`);
372
+ }
373
+ }
374
+
375
+ return lines.join('\n');
376
+ } catch (err) {
377
+ return `❌ Risk analysis failed: ${err.message}`;
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Summarise plan.md as a Slack message.
383
+ */
384
+ function runPlanForSlack() {
385
+ try {
386
+ const planPath = join(ROOT_DIR, 'plan.md');
387
+ if (!existsSync(planPath)) return '⚠️ No plan.md found in the project root.';
388
+
389
+ const content = readFileSync(planPath, 'utf8');
390
+ const lines = content.split('\n');
391
+
392
+ // Extract phase headers and their done/in-progress items
393
+ const output = ['*Construct Plan*', ''];
394
+ let currentPhase = null;
395
+ let phaseItems = [];
396
+ let inTable = false;
397
+
398
+ for (const line of lines) {
399
+ if (line.startsWith('## Phase') || line.startsWith('## Goal') || line.startsWith('## Next')) {
400
+ if (currentPhase && phaseItems.length) {
401
+ output.push(`*${currentPhase}*`);
402
+ output.push(...phaseItems.slice(0, 6));
403
+ if (phaseItems.length > 6) output.push(` _…and ${phaseItems.length - 6} more_`);
404
+ output.push('');
405
+ }
406
+ currentPhase = line.replace(/^#+\s*/, '').trim();
407
+ phaseItems = [];
408
+ inTable = false;
409
+ continue;
410
+ }
411
+ // Table rows with status
412
+ if (line.includes('| done |') || line.includes('| in-progress |') || line.includes('| blocked |')) {
413
+ const match = line.match(/\|\s*[\d.]+\s*\|\s*(.+?)\s*\|\s*(done|in-progress|blocked)\s*\|/);
414
+ if (match) {
415
+ const emoji = match[2] === 'done' ? '✅' : match[2] === 'in-progress' ? '🔄' : '🚫';
416
+ phaseItems.push(`${emoji} ${match[1].trim()}`);
417
+ }
418
+ }
419
+ }
420
+
421
+ // Flush last phase
422
+ if (currentPhase && phaseItems.length) {
423
+ output.push(`*${currentPhase}*`);
424
+ output.push(...phaseItems.slice(0, 6));
425
+ }
426
+
427
+ // Extract next steps if present
428
+ const nextIdx = lines.findIndex(l => l.startsWith('## Next Steps') || l.startsWith('## Next'));
429
+ if (nextIdx !== -1) {
430
+ const nextLines = [];
431
+ for (let i = nextIdx + 1; i < Math.min(nextIdx + 10, lines.length); i++) {
432
+ if (lines[i].startsWith('#')) break;
433
+ const item = lines[i].trim();
434
+ if (item.startsWith('1.') || item.startsWith('2.') || item.startsWith('3.') || item.startsWith('-')) {
435
+ nextLines.push(item);
436
+ }
437
+ }
438
+ if (nextLines.length) {
439
+ output.push('');
440
+ output.push('*Next Steps*');
441
+ output.push(...nextLines);
442
+ }
443
+ }
444
+
445
+ return output.join('\n') || '_(Plan is empty)_';
446
+ } catch (err) {
447
+ return `❌ Could not read plan: ${err.message}`;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * @param {object} opts
453
+ * @param {object} opts.webhookSecrets
454
+ */
455
+ export function createSlackCommandHandler({ webhookSecrets = {} } = {}) {
456
+ return async function handleSlackCommand(req, res) {
457
+ // Collect body
458
+ const chunks = [];
459
+ await new Promise((resolve, reject) => {
460
+ req.on('data', c => chunks.push(c));
461
+ req.on('end', resolve);
462
+ req.on('error', reject);
463
+ });
464
+ const rawBody = Buffer.concat(chunks).toString('utf8');
465
+
466
+ // Verify Slack signature
467
+ const env = loadConfigEnv();
468
+ const signingSecret = webhookSecrets.slack
469
+ || env.SLACK_SIGNING_SECRET
470
+ || process.env.SLACK_SIGNING_SECRET;
471
+
472
+ if (signingSecret && !verifySlackSignature(rawBody, req.headers, signingSecret)) {
473
+ res.writeHead(401, { 'Content-Type': 'application/json' });
474
+ res.end(JSON.stringify({ error: 'Invalid Slack signature' }));
475
+ return;
476
+ }
477
+
478
+ const params = parseFormBody(rawBody);
479
+ const text = (params.text || '').trim().toLowerCase();
480
+ const responseUrl = params.response_url || '';
481
+ const botToken = env.SLACK_BOT_TOKEN || process.env.SLACK_BOT_TOKEN;
482
+
483
+ // Acknowledge immediately — Slack requires a 200 within 3 seconds
484
+ res.writeHead(200, { 'Content-Type': 'application/json' });
485
+
486
+ let ackText;
487
+ if (text === 'snapshot') ackText = '⏳ Running snapshot…';
488
+ else if (text === 'risks') ackText = '⏳ Analysing risks…';
489
+ else if (text === 'plan') ackText = '⏳ Loading plan…';
490
+ else {
491
+ res.end(JSON.stringify({
492
+ response_type: 'ephemeral',
493
+ text: 'Unknown command. Try: `/construct snapshot`, `/construct risks`, `/construct plan`',
494
+ }));
495
+ return;
496
+ }
497
+
498
+ res.end(JSON.stringify({ response_type: 'in_channel', text: ackText }));
499
+
500
+ // Do the work after acknowledging
501
+ setImmediate(async () => {
502
+ let message;
503
+ if (text === 'snapshot') message = await runSnapshotForSlack();
504
+ else if (text === 'risks') message = await runRisksForSlack();
505
+ else message = runPlanForSlack();
506
+
507
+ await postSlackMessage(responseUrl, message, botToken).catch(() => {});
508
+ });
509
+ };
510
+ }