@geraldmaron/construct 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +9 -6
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1000 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +523 -36
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
package/lib/auto-docs.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  * <!-- /AUTO:region-name -->
8
8
  *
9
9
  * Running regenerateDocs() is idempotent. With check:true it returns whether
10
- * anything would change without writing files, used by CI to detect drift.
10
+ * anything would change without writing files the mode CI uses to detect drift.
11
11
  *
12
12
  * buildSite() writes site/docs/ from the same sources feeding the AUTO regions
13
13
  * so the GitHub Pages site never drifts from the in-repo markdown.
@@ -65,24 +65,30 @@ function buildCoreDocsContract() {
65
65
  '',
66
66
  '| File | Purpose | Update when |',
67
67
  '|---|---|---|',
68
+ '| `AGENTS.md` | Canonical agent operating contract | Workflow rules, tracker hierarchy, or repo-wide guardrails change |',
68
69
  '| `.cx/context.md` | Human-readable resumable project context | Active work, decisions, architecture assumptions, or open questions change |',
69
70
  '| `.cx/context.json` | Machine-readable resumable context | Context state needs to stay in sync with `.cx/context.md` |',
70
- '| `.cx/workflow.json` | Canonical workflow/task state | Non-trivial work starts, changes phase, or completes |',
71
71
  '| `docs/README.md` | Docs index and maintenance contract | Core docs set or maintenance expectations change |',
72
- '| `docs/architecture.md` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
72
+ '| `docs/concepts/architecture.md` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
73
73
  '',
74
- 'All LLMs working in the repo, including Construct, must read these as project state and keep them current when work changes project reality.',
74
+ '`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed durable work belongs in the tracker (Beads or external).',
75
+ '',
76
+ 'Tracker hierarchy: external tracker (prefer Beads) for durable work, `plan.md` for the local working plan, and cass-memory via MCP `memory` for cross-session recall.',
77
+ '',
78
+ '`AGENTS.md` is the canonical agent instruction file. On case-sensitive filesystems you may also add a lowercase `agents.md` shim for tools that require it.',
79
+ 'All LLMs working in the repo, including Construct, must read these as project state, keep them current when work changes project reality, and prune stale sections instead of letting managed docs drift.',
75
80
  ].join('\n');
76
81
  }
77
82
 
78
83
  const DIR_DESCRIPTIONS = {
79
84
  agents: 'Registry and generated platform adapter chains',
85
+ apps: 'User-facing apps shipped from this repo (e.g., apps/docs/ — Fumadocs docs site)',
80
86
  bin: 'CLI entrypoint (`construct`)',
81
87
  claude: 'Claude Code integration (agents, settings template)',
82
88
  commands: 'Command prompt assets',
83
89
  codex: 'GitHub Copilot / Codex integration',
84
90
  docs: 'Architecture notes, runbooks, and documentation contract',
85
- langfuse: 'Langfuse trace backend for agent observability',
91
+ telemetry: 'Telemetry backend for agent observability',
86
92
  lib: 'Core runtime: CLI, hooks, MCP, status, sync, workflow',
87
93
  opencode: 'OpenCode integration config',
88
94
  personas: 'Persona prompt definitions',
@@ -166,7 +172,7 @@ function buildAgentsTable(rootDir) {
166
172
  // --- public API ---
167
173
 
168
174
  /**
169
- * Regenerate all AUTO regions in README.md and docs/README.md/docs/architecture.md.
175
+ * Regenerate all AUTO regions in README.md, docs/README.md, and docs/concepts/architecture.md.
170
176
  * Returns { changed: string[], checked: boolean }.
171
177
  * With check:true writes nothing and sets changed to files that would differ.
172
178
  */
@@ -184,7 +190,7 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
184
190
  },
185
191
  },
186
192
  {
187
- file: path.join(rootDir, 'docs', 'architecture.md'),
193
+ file: path.join(rootDir, 'docs', 'concepts', 'architecture.md'),
188
194
  regions: {
189
195
  agents: buildAgentsTable(rootDir),
190
196
  },
@@ -223,71 +229,245 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
223
229
  }
224
230
 
225
231
  /**
226
- * Write site/docs/ from the same generators feeding the AUTO regions.
227
- * Invoked by `construct docs:site` before `mkdocs build`.
232
+ * Check CLI command coverage against how-to links in docs/README.md.
233
+ *
234
+ * Returns an object with:
235
+ * - covered: string[] commands that have a linked how-to
236
+ * - uncovered: string[] commands with no how-to link in docs/README.md
237
+ * - total: number
238
+ *
239
+ * A command is considered covered if its name appears in any markdown link
240
+ * inside the how-to guides section of docs/README.md.
228
241
  */
229
- export async function buildSite({ rootDir } = {}) {
242
+ export function checkDocsCoverage({ rootDir } = {}) {
230
243
  rootDir = rootDir ?? process.cwd();
231
- const siteDocsDir = path.join(rootDir, 'site', 'docs');
232
- fs.mkdirSync(siteDocsDir, { recursive: true });
233
- fs.mkdirSync(path.join(siteDocsDir, 'templates'), { recursive: true });
234
-
235
- const readOrEmpty = (p) => readFile(p) ?? '';
236
- const writeTransformed = (src, dest, transform = (content) => content) => {
237
- const content = readFile(src);
238
- if (content) fs.writeFileSync(path.join(siteDocsDir, dest), transform(content));
239
- };
240
-
241
- // index.md strip the "For contributors" section onwards for a user-facing landing
242
- const readme = readOrEmpty(path.join(rootDir, 'README.md'));
243
- const cutAt = readme.indexOf('## For contributors');
244
- const index = cutAt > 0 ? readme.slice(0, cutAt).trimEnd() + '\n' : readme;
245
- fs.writeFileSync(path.join(siteDocsDir, 'index.md'), index);
246
-
247
- // commands.md full CLI reference
248
- const cmdLines = [
249
- '<!--\nsite/docs/commands.md — full CLI reference, generated by construct docs:site.\nSource: lib/cli-commands.mjs\n-->\n',
250
- '# Command reference\n',
251
- buildCommandsTable(),
252
- ];
253
- fs.writeFileSync(path.join(siteDocsDir, 'commands.md'), cmdLines.join('\n'));
244
+ const docsReadme = readFile(path.join(rootDir, 'docs', 'README.md')) ?? '';
245
+
246
+ // Extract all href targets from markdown links in docs/README.md
247
+ const linkTargets = [...docsReadme.matchAll(/\[.*?\]\((.*?)\)/g)].map(m => m[1]);
248
+
249
+ // Build a combined corpus: docs/README.md + every linked how-to file
250
+ const howToDir = path.join(rootDir, 'docs', 'how-to');
251
+ let corpus = docsReadme;
252
+ for (const target of linkTargets) {
253
+ if (!target.startsWith('./how-to/')) continue;
254
+ const filePath = path.join(rootDir, 'docs', target.replace(/^\.\//, ''));
255
+ const content = readFile(filePath);
256
+ if (content) corpus += '\n' + content;
257
+ }
258
+
259
+ // Capture command mentions from both inline code (`construct cmd`) and code blocks (bare lines)
260
+ const mentionedCmds = new Set([
261
+ ...corpus.matchAll(/`construct\s+([\w:_-]+)`/g),
262
+ ...corpus.matchAll(/^construct\s+([\w:_-]+)/gm),
263
+ ].map(m => m[1]));
264
+
265
+ // Collect all command names from the registry
266
+ const allCommands = [];
267
+ for (const cmds of Object.values(CLI_COMMANDS_BY_CATEGORY)) {
268
+ for (const cmd of cmds) allCommands.push(cmd.name);
269
+ }
270
+
271
+ // Skip internal / plumbing commands and simple commands covered by getting-started.md
272
+ const skipList = new Set([
273
+ 'version', 'diff', 'validate', 'docs:update', 'docs:site', 'docs:check', 'sync',
274
+ 'list', 'setup', 'show', 'hosts', 'plugin', 'mcp', 'evals',
275
+ 'telemetry-backfill', 'lint:comments', 'lint:research',
276
+ // short commands documented in getting-started.md
277
+ 'up', 'down', 'status', 'serve', 'init', 'update', 'doctor',
278
+ // team sub-commands — covered as part of review how-to
279
+ 'team',
280
+ // covered in getting-started.md under Memory Layer section
281
+ 'bootstrap', 'memory',
282
+ // niche overlay command — documented in registry description
283
+ 'headhunt',
284
+ ]);
285
+
286
+ const covered = [];
287
+ const uncovered = [];
288
+
289
+ for (const name of allCommands) {
290
+ if (skipList.has(name)) continue;
291
+ const slug = name.replace(':', '-');
292
+ const isCovered =
293
+ linkTargets.some(t => t.includes(slug) || t.includes(name)) ||
294
+ mentionedCmds.has(name);
295
+ if (isCovered) covered.push(name);
296
+ else uncovered.push(name);
297
+ }
298
+
299
+ return { covered, uncovered, total: covered.length + uncovered.length };
300
+ }
301
+
302
+ // Categories surfaced in the Fumadocs reference. Internal commands (e.g.,
303
+ // `construct hook <name>`) are not user-facing reference material.
304
+ const REFERENCE_CATEGORIES = [
305
+ 'Services',
306
+ 'Agents & Sync',
307
+ 'Work',
308
+ 'Embed',
309
+ 'Models & Integrations',
310
+ 'Providers',
311
+ 'Observability',
312
+ 'Docs',
313
+ 'Diagnostics',
314
+ ];
315
+
316
+ function slugify(category) {
317
+ return category.toLowerCase()
318
+ .replace(/&/g, 'and')
319
+ .replace(/[^a-z0-9]+/g, '-')
320
+ .replace(/^-|-$/g, '');
321
+ }
254
322
 
255
- // hooks.md
256
- const hooksLines = [
257
- '<!--\nsite/docs/hooks.md — hook reference, generated by construct docs:site.\nSource: lib/hooks/\n-->\n',
258
- '# Hooks reference\n',
259
- 'Hooks run in every Claude Code session. They are registered in `claude/settings.template.json`.\n',
323
+ function renderCommandPage(category, commands) {
324
+ const lines = [];
325
+ lines.push('---');
326
+ lines.push(`title: ${category}`);
327
+ lines.push(`description: ${category} commands for Construct.`);
328
+ lines.push('---');
329
+ lines.push('');
330
+ lines.push(`# ${category}`);
331
+ lines.push('');
332
+ lines.push('| Command | What it does |');
333
+ lines.push('|---|---|');
334
+ for (const cmd of commands) {
335
+ lines.push(`| \`construct ${cmd.name}\` | ${cmd.description} |`);
336
+ }
337
+ lines.push('');
338
+ for (const cmd of commands) {
339
+ lines.push(`## construct ${cmd.name}`);
340
+ lines.push('');
341
+ lines.push(cmd.description);
342
+ lines.push('');
343
+ if (cmd.usage) {
344
+ lines.push('**Usage**');
345
+ lines.push('');
346
+ lines.push('```bash');
347
+ lines.push(cmd.usage);
348
+ lines.push('```');
349
+ lines.push('');
350
+ }
351
+ if (cmd.subcommands && cmd.subcommands.length) {
352
+ lines.push('**Subcommands**');
353
+ lines.push('');
354
+ for (const sub of cmd.subcommands) {
355
+ lines.push(`- \`${sub}\``);
356
+ }
357
+ lines.push('');
358
+ }
359
+ if (cmd.options && cmd.options.length) {
360
+ lines.push('**Options**');
361
+ lines.push('');
362
+ lines.push('| Flag | Description |');
363
+ lines.push('|---|---|');
364
+ for (const opt of cmd.options) {
365
+ lines.push(`| \`${opt.flag}\` | ${opt.desc} |`);
366
+ }
367
+ lines.push('');
368
+ }
369
+ }
370
+ return lines.join('\n');
371
+ }
372
+
373
+ function renderHooksPage(rootDir) {
374
+ return [
375
+ '---',
376
+ 'title: Hooks',
377
+ 'description: Hooks fire during Claude Code sessions, on file edits, commits, pushes, and prompts. Generated from lib/hooks/.',
378
+ '---',
379
+ '',
380
+ '> Generated from `lib/hooks/`. Re-run `construct docs:site` to refresh.',
381
+ '',
382
+ 'Hooks are wired in `platforms/claude/settings.template.json` and execute as child processes during Claude Code sessions. Each hook reads JSON on stdin, performs its check or transform, and exits with a status that signals whether the surrounding tool call should proceed.',
383
+ '',
260
384
  buildHooksTable(rootDir),
261
- ];
262
- fs.writeFileSync(path.join(siteDocsDir, 'hooks.md'), hooksLines.join('\n'));
263
-
264
- // agents.md
265
- const agentsTable = buildAgentsTable(rootDir);
266
- if (agentsTable) {
267
- const agentLines = [
268
- '<!--\nsite/docs/agents.md — agent reference, generated by construct docs:site.\nSource: agents/registry.json\n-->\n',
269
- '# Agent reference\n',
270
- agentsTable,
271
- ];
272
- fs.writeFileSync(path.join(siteDocsDir, 'agents.md'), agentLines.join('\n'));
385
+ '',
386
+ ].join('\n');
387
+ }
388
+
389
+ function renderAgentsPage(rootDir) {
390
+ const table = buildAgentsTable(rootDir);
391
+ if (!table) return null;
392
+ return [
393
+ '---',
394
+ 'title: Agents',
395
+ 'description: The 28 cx-* specialists plus the construct persona. Generated from agents/registry.json.',
396
+ '---',
397
+ '',
398
+ '> Generated from `agents/registry.json`. Re-run `construct docs:site` to refresh.',
399
+ '',
400
+ 'Construct ships one persona (`construct`) plus 28 specialists. Each specialist has a role, model tier, and prompt that defines its decision authority. Specialists are addressable directly when you want a focused conversation; for everyday work, address `@construct` and let it route.',
401
+ '',
402
+ table,
403
+ '',
404
+ ].join('\n');
405
+ }
406
+
407
+ /**
408
+ * Emit MDX reference pages into docs/reference/ for the Fumadocs site.
409
+ * Mirrors the data sources behind AUTO regions and buildSite() but writes
410
+ * Markdown files Fumadocs can render directly (no MkDocs intermediate).
411
+ */
412
+ export function buildFumadocsReference({ rootDir } = {}) {
413
+ rootDir = rootDir ?? process.cwd();
414
+ const refDir = path.join(rootDir, 'docs', 'reference');
415
+ const cliDir = path.join(refDir, 'cli');
416
+ fs.mkdirSync(cliDir, { recursive: true });
417
+
418
+ const written = [];
419
+
420
+ const cliPages = [];
421
+ for (const category of REFERENCE_CATEGORIES) {
422
+ const commands = CLI_COMMANDS_BY_CATEGORY[category] ?? [];
423
+ if (!commands.length) continue;
424
+ const slug = slugify(category);
425
+ const out = path.join(cliDir, `${slug}.md`);
426
+ if (writeIfChanged(out, renderCommandPage(category, commands))) {
427
+ written.push(path.relative(rootDir, out));
428
+ }
429
+ cliPages.push(slug);
273
430
  }
274
431
 
275
- // copy static docs
276
- for (const [src, dest] of [
277
- [path.join(rootDir, 'docs', 'architecture.md'), 'architecture.md'],
278
- [path.join(rootDir, 'CONTRIBUTING.md'), 'contributing.md'],
279
- [path.join(rootDir, 'CLAUDE.md'), 'claude.md'],
280
- ]) {
281
- const content = readFile(src);
282
- if (!content) continue;
283
- const transformed = dest === 'contributing.md'
284
- ? content.replaceAll('[CLAUDE.md](CLAUDE.md)', '[CLAUDE.md](claude.md)')
285
- : dest === 'claude.md'
286
- ? content.replaceAll('[docs/templates/README.md](docs/templates/README.md)', '[docs/templates/README.md](templates/README.md)')
287
- : content;
288
- fs.writeFileSync(path.join(siteDocsDir, dest), transformed);
432
+ const cliMeta = path.join(cliDir, 'meta.json');
433
+ const cliMetaContent = JSON.stringify({ title: 'CLI', pages: ['index', ...cliPages] }, null, 2) + '\n';
434
+ if (writeIfChanged(cliMeta, cliMetaContent)) written.push(path.relative(rootDir, cliMeta));
435
+
436
+ const cliIndexContent = [
437
+ '---',
438
+ 'title: CLI',
439
+ 'description: Every construct command, grouped by category. Generated from lib/cli-commands.mjs.',
440
+ '---',
441
+ '',
442
+ '> Generated from `lib/cli-commands.mjs`. Re-run `construct docs:site` to refresh.',
443
+ '',
444
+ 'Commands are grouped by what they do. Pick a category below.',
445
+ '',
446
+ ...REFERENCE_CATEGORIES.map((cat) => {
447
+ const slug = slugify(cat);
448
+ const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
449
+ if (!commands.length) return null;
450
+ return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
451
+ }).filter(Boolean),
452
+ '',
453
+ ].join('\n');
454
+ if (writeIfChanged(path.join(cliDir, 'index.md'), cliIndexContent)) {
455
+ written.push(path.relative(rootDir, path.join(cliDir, 'index.md')));
289
456
  }
290
457
 
291
- writeTransformed(path.join(rootDir, 'docs', 'templates', 'README.md'), path.join('templates', 'README.md'));
458
+ const hooksPage = path.join(refDir, 'hooks.md');
459
+ if (writeIfChanged(hooksPage, renderHooksPage(rootDir))) {
460
+ written.push(path.relative(rootDir, hooksPage));
461
+ }
292
462
 
463
+ const agentsContent = renderAgentsPage(rootDir);
464
+ if (agentsContent) {
465
+ const agentsPage = path.join(refDir, 'agents.md');
466
+ if (writeIfChanged(agentsPage, agentsContent)) {
467
+ written.push(path.relative(rootDir, agentsPage));
468
+ }
469
+ }
470
+
471
+ return { written };
293
472
  }
473
+
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/beads-auto-close.mjs — auto-close beads on merge.
4
+ *
5
+ * Scans the commit messages on the just-merged range for any
6
+ * `closes construct-xxx` / `fixes construct-xxx` / `resolves construct-xxx`
7
+ * references (case-insensitive). For each unique bead id found, runs
8
+ * `bd close <id> --reason="Auto-closed on merge <sha>: <subject>"` so
9
+ * the tracker matches reality without manual intervention.
10
+ *
11
+ * Invocation: the `.beads/hooks/post-merge` git hook shim runs this,
12
+ * or `--range=<git-range>` triggers a standalone backfill.
13
+ *
14
+ * Exit codes:
15
+ * 0 no closes referenced, or all closes succeeded
16
+ * 1 one or more bd close calls failed
17
+ * 2 could not parse git range / no bd CLI
18
+ *
19
+ * Idempotent: closing an already-closed bead is a no-op so re-running
20
+ * the hook after a merge is safe.
21
+ */
22
+
23
+ import { spawnSync } from 'node:child_process';
24
+
25
+ const CLOSES_RE = /\b(?:closes?|close|fix(?:es)?|fixed|resolves?|resolved)\s+(construct-[a-z0-9]+)/gi;
26
+
27
+ export function parseClosesFromMessage(message) {
28
+ if (!message) return [];
29
+ const found = new Set();
30
+ let match;
31
+ CLOSES_RE.lastIndex = 0;
32
+ while ((match = CLOSES_RE.exec(message)) !== null) {
33
+ found.add(match[1].toLowerCase());
34
+ }
35
+ return [...found];
36
+ }
37
+
38
+ export function listCommitsInRange(range, { runner = spawnSync } = {}) {
39
+ const result = runner('git', ['log', range, '--pretty=format:%H%x09%s%x09%B%x00'], {
40
+ encoding: 'utf8',
41
+ stdio: ['ignore', 'pipe', 'pipe'],
42
+ });
43
+ if (result.status !== 0) return [];
44
+ return result.stdout
45
+ .split('\x00')
46
+ .map((entry) => entry.trim())
47
+ .filter(Boolean)
48
+ .map((entry) => {
49
+ const [sha, subject, ...rest] = entry.split('\t');
50
+ return { sha, subject, body: rest.join('\t') };
51
+ });
52
+ }
53
+
54
+ export function collectAutoCloses(commits) {
55
+ const closesById = new Map();
56
+ for (const commit of commits) {
57
+ const ids = parseClosesFromMessage(`${commit.subject}\n${commit.body}`);
58
+ for (const id of ids) {
59
+ if (!closesById.has(id)) closesById.set(id, []);
60
+ closesById.get(id).push({ sha: commit.sha, subject: commit.subject });
61
+ }
62
+ }
63
+ return closesById;
64
+ }
65
+
66
+ export function buildAutoCloseReason(commits) {
67
+ const first = commits[0];
68
+ if (!first) return 'Auto-closed on merge';
69
+ const shortSha = first.sha.slice(0, 12);
70
+ return `Auto-closed on merge ${shortSha}: ${first.subject}`;
71
+ }
72
+
73
+ /**
74
+ * Drive the auto-close: list commits in the range, parse closes, invoke
75
+ * `bd close` for each. Returns a summary the caller can log or test.
76
+ */
77
+ export async function runAutoClose({ range, runner = spawnSync } = {}) {
78
+ if (!range) return { ok: false, reason: 'no range', closed: [], failed: [] };
79
+
80
+ const commits = listCommitsInRange(range, { runner });
81
+ if (commits.length === 0) return { ok: true, closed: [], failed: [], commits: 0 };
82
+
83
+ const closesById = collectAutoCloses(commits);
84
+ if (closesById.size === 0) return { ok: true, closed: [], failed: [], commits: commits.length };
85
+
86
+ const closed = [];
87
+ const failed = [];
88
+ for (const [id, refs] of closesById) {
89
+ const reason = buildAutoCloseReason(refs);
90
+ const result = runner('bd', ['close', id, `--reason=${reason}`], {
91
+ encoding: 'utf8',
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ });
94
+ if (result.status === 0) {
95
+ closed.push({ id, sha: refs[0]?.sha, subject: refs[0]?.subject });
96
+ } else {
97
+ failed.push({
98
+ id,
99
+ exitCode: result.status,
100
+ stderr: (result.stderr || '').slice(0, 500),
101
+ });
102
+ }
103
+ }
104
+ return { ok: failed.length === 0, closed, failed, commits: commits.length };
105
+ }
106
+
107
+ // CLI entry: read --range, run, print summary.
108
+ const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
109
+ if (invokedDirectly) {
110
+ const args = process.argv.slice(2);
111
+ const rangeArg = args.find((a) => a.startsWith('--range='));
112
+ // Default: every commit unique to the current branch that landed via the
113
+ // current HEAD's range (post-merge sees ORIG_HEAD..HEAD typically).
114
+ const range = rangeArg ? rangeArg.split('=')[1] : (process.env.BEADS_AUTO_CLOSE_RANGE || 'ORIG_HEAD..HEAD');
115
+ const summary = await runAutoClose({ range });
116
+ if (summary.closed.length > 0) {
117
+ process.stdout.write(`[beads-auto-close] closed ${summary.closed.length} bead(s): ${summary.closed.map((c) => c.id).join(', ')}\n`);
118
+ }
119
+ if (summary.failed.length > 0) {
120
+ for (const f of summary.failed) {
121
+ process.stderr.write(`[beads-auto-close] failed ${f.id} (exit ${f.exitCode}): ${f.stderr}\n`);
122
+ }
123
+ process.exit(1);
124
+ }
125
+ process.exit(0);
126
+ }
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/beads-drift.mjs — beads hygiene drift detection.
4
+ *
5
+ * Three checks that surface stale tracker state before it accumulates:
6
+ *
7
+ * 1. Stale-open: open beads whose updatedAt is older than the staleness
8
+ * window (default 14 days). Stale-open isn't a bug by itself but
9
+ * means nobody has reviewed the bead in two weeks — likely irrelevant.
10
+ *
11
+ * 2. Stuck-in-progress: in_progress beads whose updatedAt is older than
12
+ * the stuck window (default 3 days). A bead in_progress is supposed
13
+ * to be claimed by an active session; 3+ days untouched means the
14
+ * session that claimed it didn't follow through.
15
+ *
16
+ * 3. Merge-drift: open beads whose title or description appears verbatim
17
+ * in a commit subject from the last N commits (default 50). Cross-
18
+ * checks tracker state against recently merged work to catch beads
19
+ * that should have been closed in the merge.
20
+ *
21
+ * The module exposes `detectBeadsDrift({...})` as a pure function so
22
+ * `construct doctor` and the pre-push gate can both consume it. CLI
23
+ * entry writes a human-readable report (and machine-readable JSON via
24
+ * `--json`) and exits non-zero when any check exceeds its threshold.
25
+ */
26
+
27
+ import { spawnSync } from 'node:child_process';
28
+
29
+ const DEFAULT_STALE_OPEN_DAYS = 14;
30
+ const DEFAULT_STUCK_IN_PROGRESS_DAYS = 3;
31
+ const DEFAULT_MERGE_LOOKBACK = 50;
32
+
33
+ function daysAgo(value) {
34
+ if (!value) return Infinity;
35
+ const ts = value instanceof Date ? value.getTime() : Date.parse(value);
36
+ if (!Number.isFinite(ts)) return Infinity;
37
+ return (Date.now() - ts) / (24 * 60 * 60 * 1000);
38
+ }
39
+
40
+ function listBeadsJson({ status, runner = spawnSync } = {}) {
41
+ const args = ['list', '--json'];
42
+ if (status) args.push(`--status=${status}`);
43
+ const result = runner('bd', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
44
+ if (result.status !== 0) return [];
45
+ try {
46
+ const parsed = JSON.parse(result.stdout);
47
+ if (Array.isArray(parsed)) return parsed;
48
+ if (Array.isArray(parsed?.issues)) return parsed.issues;
49
+ return [];
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+
55
+ function recentCommitSubjects(limit, { runner = spawnSync } = {}) {
56
+ const result = runner('git', ['log', '-n', String(limit), '--pretty=format:%s'], {
57
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
58
+ });
59
+ if (result.status !== 0) return [];
60
+ return result.stdout.split('\n').filter(Boolean);
61
+ }
62
+
63
+ function tokenize(text) {
64
+ return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 4);
65
+ }
66
+
67
+ function titleAppearsInSubject(beadTitle, commitSubject) {
68
+ // Normalize both sides, then check that the bead's first significant
69
+ // tokens are present in the commit subject. Strict enough to avoid
70
+ // false positives, lax enough to catch reasonable matches.
71
+ const beadTokens = tokenize(beadTitle).slice(0, 5);
72
+ if (beadTokens.length < 3) return false;
73
+ const lowerSubject = String(commitSubject || '').toLowerCase();
74
+ let hits = 0;
75
+ for (const token of beadTokens) {
76
+ if (lowerSubject.includes(token)) hits += 1;
77
+ }
78
+ return hits >= 3;
79
+ }
80
+
81
+ /**
82
+ * Run the three drift checks against the live tracker + git state.
83
+ * Pure-ish — accepts a runner injection for tests.
84
+ */
85
+ export function detectBeadsDrift({
86
+ staleOpenDays = DEFAULT_STALE_OPEN_DAYS,
87
+ stuckInProgressDays = DEFAULT_STUCK_IN_PROGRESS_DAYS,
88
+ mergeLookback = DEFAULT_MERGE_LOOKBACK,
89
+ runner = spawnSync,
90
+ } = {}) {
91
+ const open = listBeadsJson({ status: 'open', runner });
92
+ const inProgress = listBeadsJson({ status: 'in_progress', runner });
93
+ const commits = recentCommitSubjects(mergeLookback, { runner });
94
+
95
+ const staleOpen = open.filter((b) => daysAgo(b.updated || b.updatedAt) > staleOpenDays);
96
+ const stuckInProgress = inProgress.filter((b) => daysAgo(b.updated || b.updatedAt) > stuckInProgressDays);
97
+
98
+ const mergeDrift = [];
99
+ for (const bead of open) {
100
+ for (const subject of commits) {
101
+ if (titleAppearsInSubject(bead.title, subject)) {
102
+ mergeDrift.push({ id: bead.id, title: bead.title, matchedSubject: subject });
103
+ break;
104
+ }
105
+ }
106
+ }
107
+
108
+ return {
109
+ staleOpen: staleOpen.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
110
+ stuckInProgress: stuckInProgress.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
111
+ mergeDrift,
112
+ thresholds: { staleOpenDays, stuckInProgressDays, mergeLookback },
113
+ counts: {
114
+ staleOpen: staleOpen.length,
115
+ stuckInProgress: stuckInProgress.length,
116
+ mergeDrift: mergeDrift.length,
117
+ },
118
+ };
119
+ }
120
+
121
+ export function formatDriftReport(report) {
122
+ const lines = [];
123
+ lines.push(`Beads hygiene drift report — checked against last ${report.thresholds.mergeLookback} commits`);
124
+ lines.push('');
125
+ if (report.counts.staleOpen === 0 && report.counts.stuckInProgress === 0 && report.counts.mergeDrift === 0) {
126
+ lines.push('✓ no drift detected');
127
+ return lines.join('\n') + '\n';
128
+ }
129
+ if (report.counts.stuckInProgress > 0) {
130
+ lines.push(`Stuck in_progress (> ${report.thresholds.stuckInProgressDays}d untouched):`);
131
+ for (const b of report.stuckInProgress) lines.push(` ${b.id} · ${b.ageDays}d · ${b.title}`);
132
+ lines.push('');
133
+ }
134
+ if (report.counts.staleOpen > 0) {
135
+ lines.push(`Stale open (> ${report.thresholds.staleOpenDays}d untouched):`);
136
+ for (const b of report.staleOpen) lines.push(` ${b.id} · ${b.ageDays}d · ${b.title}`);
137
+ lines.push('');
138
+ }
139
+ if (report.counts.mergeDrift > 0) {
140
+ lines.push(`Possible merge drift (open bead title overlaps a recent commit subject):`);
141
+ for (const b of report.mergeDrift) lines.push(` ${b.id} · ${b.title}\n matched: ${b.matchedSubject}`);
142
+ lines.push('');
143
+ }
144
+ return lines.join('\n');
145
+ }
146
+
147
+ // CLI: print report, exit non-zero when thresholds breached unless --report-only.
148
+ const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
149
+ if (invokedDirectly) {
150
+ const args = process.argv.slice(2);
151
+ const json = args.includes('--json');
152
+ const reportOnly = args.includes('--report-only');
153
+ const opts = {};
154
+ for (const arg of args) {
155
+ const m = arg.match(/^--([a-z-]+)=(\d+)$/);
156
+ if (!m) continue;
157
+ if (m[1] === 'stale-open-days') opts.staleOpenDays = Number(m[2]);
158
+ if (m[1] === 'stuck-in-progress-days') opts.stuckInProgressDays = Number(m[2]);
159
+ if (m[1] === 'merge-lookback') opts.mergeLookback = Number(m[2]);
160
+ }
161
+ const report = detectBeadsDrift(opts);
162
+ if (json) {
163
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
164
+ } else {
165
+ process.stdout.write(formatDriftReport(report));
166
+ }
167
+ if (!reportOnly && (report.counts.stuckInProgress > 0 || report.counts.mergeDrift > 0)) {
168
+ process.exit(2);
169
+ }
170
+ process.exit(0);
171
+ }