@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,183 @@
1
+ /**
2
+ * lib/dispatch-batch.js — Batch multiplexer for parallel agents.
3
+ *
4
+ * When orchestration policy returns a dispatch plan with parallel: true:
5
+ * 1. Check all agents use the same model tier AND same provider family.
6
+ * 2. Compose one shared system message (deduplicated static fragments).
7
+ * 3. Append N per-agent task suffixes as separate user messages.
8
+ * 4. Request structured JSON output with agent names as keys.
9
+ * 5. Fall back to sequential dispatch if tiers differ.
10
+ *
11
+ * Only activates when dispatchPlan.someParallel === true AND agents share the same model.
12
+ */
13
+ import { composePrompt } from './prompt-composer.js';
14
+ import { resolveProviderCapabilities } from './provider-capabilities.js';
15
+ import { estimateTokensSync } from './token-engine.js';
16
+
17
+ const MAX_BATCH_AGENTS = 5; // limit batch size for token budget
18
+
19
+ /**
20
+ * Check if a dispatch plan can be batched.
21
+ *
22
+ * @param {Array} agents - [{ name, ... }]
23
+ * @param {object} dispatchPlan
24
+ * @returns {{ canBatch: boolean, reason: string, sharedModel: string }}
25
+ */
26
+ export function canBatch(agents, dispatchPlan) {
27
+ if (!dispatchPlan?.someParallel) {
28
+ return { canBatch: false, reason: 'Dispatch plan does not request parallel execution' };
29
+ }
30
+
31
+ if (!Array.isArray(agents) || agents.length < 2) {
32
+ return { canBatch: false, reason: 'Need at least 2 agents for batch' };
33
+ }
34
+
35
+ if (agents.length > MAX_BATCH_AGENTS) {
36
+ return { canBatch: false, reason: `Batch size ${agents.length} exceeds max ${MAX_BATCH_AGENTS}` };
37
+ }
38
+
39
+ // Check all agents share the same model tier
40
+ const tiers = new Set();
41
+ const providers = new Set();
42
+
43
+ for (const agent of agents) {
44
+ tiers.add(agent.tier || 'standard');
45
+ providers.add(agent.provider || 'unknown');
46
+ }
47
+
48
+ if (tiers.size > 1) {
49
+ return { canBatch: false, reason: `Multiple tiers: ${[...tiers].join(', ')}` };
50
+ }
51
+
52
+ if (providers.size > 1) {
53
+ return { canBatch: false, reason: `Multiple providers: ${[...providers].join(', ')}` };
54
+ }
55
+
56
+ const sharedModel = agents[0].modelId || '';
57
+ return { canBatch: true, reason: null, sharedModel, sharedTier: [...tiers][0] };
58
+ }
59
+
60
+ /**
61
+ * Build a batch prompt for parallel agents.
62
+ *
63
+ * @param {Array} agents - [{ name, task, ... }]
64
+ * @param {object} options - { sharedModel, rootDir, ... }]
65
+ * @returns {Promise<{ system, messages, expectedTokens }>}
66
+ */
67
+ export async function buildBatchPrompt(agents, {
68
+ rootDir = process.cwd(),
69
+ ...opts
70
+ } = {}) {
71
+ if (!agents?.length) return null;
72
+
73
+ // Compose prompt for first agent (the template)
74
+ const first = await composePrompt(agents[0].name, {
75
+ rootDir,
76
+ task: agents[0].task,
77
+ intent: agents[0].intent,
78
+ workCategory: agents[0].workCategory,
79
+ ...opts,
80
+ });
81
+
82
+ // Extract shared system from first agent
83
+ const sharedSystem = first.system || '';
84
+ const staticEndIndex = first.staticEndIndex;
85
+
86
+ // Build per-agent suffixes
87
+ const agentSuffixes = [];
88
+ let totalSuffixTokens = 0;
89
+
90
+ for (let i = 0; i < agents.length; i++) {
91
+ const agent = agents[i];
92
+ const agentPrompt = await composePrompt(agent.name, {
93
+ rootDir,
94
+ task: agent.task,
95
+ intent: agent.intent,
96
+ workCategory: agent.workCategory,
97
+ injectLearnedPatterns: i === 0, // only inject once
98
+ ...opts,
99
+ });
100
+
101
+ // The dynamic part (after system)
102
+ const suffix = agentPrompt.messages?.[1]?.content || agentPrompt.system || '';
103
+ const suffixTokens = estimateTokensSync(suffix, { modelId: opts.sharedModel });
104
+
105
+ agentSuffixes.push({
106
+ agent: agent.name,
107
+ content: suffix,
108
+ tokens: suffixTokens,
109
+ });
110
+ totalSuffixTokens += suffixTokens;
111
+ }
112
+
113
+ const systemTokens = estimateTokensSync(sharedSystem, { modelId: opts.sharedModel });
114
+ const totalTokens = systemTokens + totalSuffixTokens;
115
+
116
+ // Build messages: one system + N user messages
117
+ const messages = [
118
+ { role: 'system', content: sharedSystem },
119
+ ...agentSuffixes.map(a => ({
120
+ role: 'user',
121
+ content: `Agent: ${a.agent}\n\n${a.content}`,
122
+ })),
123
+ ];
124
+
125
+ return {
126
+ system: sharedSystem,
127
+ messages,
128
+ expectedTokens: totalTokens,
129
+ agentSuffixes,
130
+ canSend: totalTokens < 30000, // conservative limit for batch
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Parse a structured batch response.
136
+ *
137
+ * @param {string} responseText - LLM response with JSON
138
+ * @param {Array} agentNames
139
+ * @returns {object} - { agentName: responseText }
140
+ */
141
+ export function parseBatchResponse(responseText, agentNames) {
142
+ if (!responseText || !Array.isArray(agentNames)) return {};
143
+
144
+ try {
145
+ // Try to parse as JSON
146
+ const parsed = JSON.parse(responseText);
147
+ if (typeof parsed === 'object' && parsed !== null) {
148
+ // Check if response has agent keys
149
+ const result = {};
150
+ for (const name of agentNames) {
151
+ result[name] = parsed[name] || parsed[name.replace(/^cx-/, '')] || '';
152
+ }
153
+ return result;
154
+ }
155
+ } catch {
156
+ // Not JSON — return full text as fallback
157
+ }
158
+
159
+ // Fallback: split by agent headers
160
+ const result = {};
161
+ const lines = responseText.split('\n');
162
+ let currentAgent = null;
163
+ let currentText = [];
164
+
165
+ for (const line of lines) {
166
+ const agentMatch = line.match(/^Agent:\s*(cx-[^\s]+)/);
167
+ if (agentMatch) {
168
+ if (currentAgent) {
169
+ result[currentAgent] = currentText.join('\n').trim();
170
+ }
171
+ currentAgent = agentMatch[1];
172
+ currentText = [];
173
+ } else {
174
+ currentText.push(line);
175
+ }
176
+ }
177
+
178
+ if (currentAgent) {
179
+ result[currentAgent] = currentText.join('\n').trim();
180
+ }
181
+
182
+ return result;
183
+ }
package/lib/distill.mjs CHANGED
@@ -1,20 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/distill.mjs — <one-line purpose>
3
+ * lib/distill.mjs — Query-focused document distillation for large context windows.
4
4
  *
5
- * <2–6 line summary.>
5
+ * Chunks local files, scores relevance against a query, and returns the top-k
6
+ * evidence blocks with citations. Backs the MCP distill tool and the RAG
7
+ * knowledge layer for shrinking context before LLM calls.
6
8
  */
7
9
  import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, openSync, readSync, closeSync } from 'fs';
8
10
  import { join, extname, relative, dirname, basename } from 'path';
9
11
  import { execSync, spawnSync } from 'child_process';
10
12
  import { homedir } from 'os';
11
13
  import { getActiveOverlays } from './headhunt.mjs';
14
+ import {
15
+ EXTRACTABLE_DOCUMENT_EXTS,
16
+ UTF8_TEXT_EXTS,
17
+ extractDocumentText as extractSharedDocumentText,
18
+ } from './document-extract.mjs';
12
19
 
13
- const TEXT_EXTS = new Set([
14
- '.md', '.txt', '.rst', '.adoc', '.json', '.yaml', '.yml', '.toml',
15
- '.js', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.sh', '.bash',
16
- '.html', '.css', '.csv', '.xml', '.env', '.env.example', '.conf', '.ini', '.tf', '.hcl',
17
- ]);
20
+ const TEXT_EXTS = EXTRACTABLE_DOCUMENT_EXTS;
18
21
 
19
22
  const SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '__pycache__', '.cache']);
20
23
  const MAX_FILE_BYTES = 80_000;
@@ -180,7 +183,17 @@ function readFiles(files, dir, options = {}) {
180
183
  if (chunks.length >= MAX_FILES) break;
181
184
  if (totalBytes >= MAX_TOTAL_BYTES) break;
182
185
  try {
183
- const bounded = readBoundedText(file.path, file.size);
186
+ const bounded = UTF8_TEXT_EXTS.has(file.ext)
187
+ ? readBoundedText(file.path, file.size)
188
+ : (() => {
189
+ const extracted = extractSharedDocumentText(file.path, { maxChars: MAX_FILE_BYTES });
190
+ return {
191
+ content: extracted.text,
192
+ truncated: extracted.truncated,
193
+ sampledBytes: Math.min(file.size, MAX_FILE_BYTES),
194
+ originalBytes: file.size,
195
+ };
196
+ })();
184
197
  totalBytes += bounded.sampledBytes;
185
198
  if (bounded.truncated) truncatedFileCount += 1;
186
199
  chunks.push(...chunkContent(relative(dir, file.path), bounded.content, {
@@ -0,0 +1,164 @@
1
+ /**
2
+ * lib/doc-stamp.mjs — Auditability stamps for Construct-generated documents.
3
+ *
4
+ * Provides:
5
+ * uuidv7() — time-ordered UUID (RFC 9562) for document identity
6
+ * bodyHash(text) — SHA-256 of document body for tamper detection
7
+ * stampFrontmatter — inject/update audit block at top of a markdown string
8
+ * verifyStamp — verify body_hash against current content
9
+ * parseStamp — extract stamp fields from a document string
10
+ *
11
+ * Design:
12
+ * - Zero npm dependencies (Node built-ins only)
13
+ * - Stamp is a YAML-fenced block at the very top of the file, before any
14
+ * other content. Markdown renderers show it as a metadata block.
15
+ * - body_hash covers everything after the closing --- of the stamp block,
16
+ * trimmed, so whitespace-only edits do not break verification.
17
+ * - generator field is intentionally short (no version pinning) so the
18
+ * stamp survives construct upgrades without false-positive hash mismatches.
19
+ */
20
+
21
+ import { createHash, randomBytes } from 'node:crypto';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // UUIDv7 — RFC 9562 §5.7
25
+ // Top 48 bits: Unix ms timestamp. Next 4 bits: version=7. Next 12 bits:
26
+ // random seq. Next 2 bits: variant=10. Remaining 62 bits: random.
27
+ // ---------------------------------------------------------------------------
28
+ export function uuidv7() {
29
+ const ms = BigInt(Date.now());
30
+ const rand = randomBytes(10);
31
+ // Build 128-bit value as two 64-bit halves
32
+ const hi =
33
+ ((ms & 0xffffffffffffn) << 16n) |
34
+ (7n << 12n) |
35
+ (BigInt(rand[0] & 0x0f) << 8n) |
36
+ BigInt(rand[1]);
37
+ const lo =
38
+ (0x8000000000000000n) |
39
+ (BigInt(rand[2] & 0x3f) << 56n) |
40
+ (BigInt(rand[3]) << 48n) |
41
+ (BigInt(rand[4]) << 40n) |
42
+ (BigInt(rand[5]) << 32n) |
43
+ (BigInt(rand[6]) << 24n) |
44
+ (BigInt(rand[7]) << 16n) |
45
+ (BigInt(rand[8]) << 8n) |
46
+ BigInt(rand[9]);
47
+
48
+ const hex = hi.toString(16).padStart(16, '0') + lo.toString(16).padStart(16, '0');
49
+ return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Content hash — SHA-256 of trimmed body text
54
+ // ---------------------------------------------------------------------------
55
+ export function bodyHash(bodyText) {
56
+ return 'sha256:' + createHash('sha256').update(bodyText.trim(), 'utf8').digest('hex');
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Stamp block helpers
61
+ // ---------------------------------------------------------------------------
62
+ const STAMP_OPEN = '---';
63
+ const STAMP_CLOSE = '---';
64
+ const STAMP_MARKER = 'cx_doc_id:';
65
+
66
+ /**
67
+ * Returns true if the document already has a Construct stamp block.
68
+ */
69
+ export function hasStamp(content) {
70
+ if (!content.startsWith(STAMP_OPEN)) return false;
71
+ const rest = content.slice(3);
72
+ return rest.includes(STAMP_MARKER);
73
+ }
74
+
75
+ /**
76
+ * Split document into { stampBlock, body }.
77
+ * stampBlock includes the opening and closing --- lines.
78
+ * body is everything after the closing ---.
79
+ * Returns null if no stamp found.
80
+ */
81
+ function splitStamp(content) {
82
+ if (!content.startsWith(STAMP_OPEN + '\n')) return null;
83
+ const afterOpen = content.slice(4);
84
+ const closeIdx = afterOpen.indexOf('\n' + STAMP_CLOSE + '\n');
85
+ if (closeIdx === -1) return null;
86
+ const stampBlock = STAMP_OPEN + '\n' + afterOpen.slice(0, closeIdx) + '\n' + STAMP_CLOSE;
87
+ const body = afterOpen.slice(closeIdx + STAMP_CLOSE.length + 2);
88
+ return { stampBlock, body };
89
+ }
90
+
91
+ /**
92
+ * Inject a new audit stamp at the top of a markdown string.
93
+ * If a stamp already exists it is replaced (id preserved, hash updated).
94
+ *
95
+ * @param {string} content - Raw markdown content (may already have a stamp)
96
+ * @param {object} options
97
+ * generator {string} - Short generator label (default: 'construct')
98
+ * sessionId {string} - Construct session ID if available
99
+ * model {string} - Model name/version if available
100
+ * preserve_id {boolean} - Keep existing cx_doc_id if present (default: true)
101
+ */
102
+ export function stampFrontmatter(content, {
103
+ generator = 'construct',
104
+ sessionId = null,
105
+ model = null,
106
+ preserve_id = true,
107
+ } = {}) {
108
+ const existing = hasStamp(content) ? splitStamp(content) : null;
109
+ const existingFields = existing ? parseStamp(content) : {};
110
+
111
+ const id = (preserve_id && existingFields.cx_doc_id) ? existingFields.cx_doc_id : uuidv7();
112
+ const createdAt = existingFields.created_at || new Date().toISOString();
113
+ const updatedAt = new Date().toISOString();
114
+
115
+ // Body is everything after stamp (or all content if no prior stamp)
116
+ const body = existing ? existing.body : content;
117
+ const hash = bodyHash(body);
118
+
119
+ const lines = [
120
+ STAMP_OPEN,
121
+ `cx_doc_id: ${id}`,
122
+ `created_at: ${createdAt}`,
123
+ `updated_at: ${updatedAt}`,
124
+ `generator: ${generator}`,
125
+ ];
126
+ if (model) lines.push(`model: ${model}`);
127
+ if (sessionId) lines.push(`session_id: ${sessionId}`);
128
+ lines.push(`body_hash: ${hash}`);
129
+ lines.push(STAMP_CLOSE);
130
+ lines.push('');
131
+
132
+ return lines.join('\n') + body;
133
+ }
134
+
135
+ /**
136
+ * Parse stamp fields from a stamped document.
137
+ * Returns {} if no stamp found.
138
+ */
139
+ export function parseStamp(content) {
140
+ const parts = splitStamp(content);
141
+ if (!parts) return {};
142
+ const fields = {};
143
+ for (const line of parts.stampBlock.split('\n')) {
144
+ const m = line.match(/^(\w+):\s*(.+)$/);
145
+ if (m) fields[m[1]] = m[2].trim();
146
+ }
147
+ return fields;
148
+ }
149
+
150
+ /**
151
+ * Verify that the stored body_hash matches the current body.
152
+ * Returns { valid: true } or { valid: false, reason, stored, computed }.
153
+ */
154
+ export function verifyStamp(content) {
155
+ const parts = splitStamp(content);
156
+ if (!parts) return { valid: false, reason: 'no stamp found' };
157
+ const fields = parseStamp(content);
158
+ if (!fields.body_hash) return { valid: false, reason: 'no body_hash in stamp' };
159
+ const computed = bodyHash(parts.body);
160
+ if (computed !== fields.body_hash) {
161
+ return { valid: false, reason: 'body_hash mismatch', stored: fields.body_hash, computed };
162
+ }
163
+ return { valid: true, id: fields.cx_doc_id, created_at: fields.created_at };
164
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * lib/doc-verify.mjs — CLI handler for `construct doc verify`
3
+ *
4
+ * Walks one file or a directory tree, checks every stamped .md file for
5
+ * body_hash integrity, and reports pass/fail/unstamped counts.
6
+ *
7
+ * Usage:
8
+ * construct doc verify [path] [--json]
9
+ *
10
+ * Exit codes:
11
+ * 0 all stamped files pass (or no stamped files found)
12
+ * 1 one or more stamped files fail verification
13
+ */
14
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { verifyStamp, hasStamp } from './doc-stamp.mjs';
19
+
20
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
21
+
22
+ function walkMd(target) {
23
+ const stat = fs.statSync(target);
24
+ if (stat.isFile()) return target.endsWith('.md') ? [target] : [];
25
+ const files = [];
26
+ for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
27
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
28
+ const full = path.join(target, entry.name);
29
+ if (entry.isDirectory()) files.push(...walkMd(full));
30
+ else if (entry.isFile() && entry.name.endsWith('.md')) files.push(full);
31
+ }
32
+ return files;
33
+ }
34
+
35
+ export function runDocVerifyCli(args) {
36
+ const jsonMode = args.includes('--json');
37
+ const targetArg = args.find(a => !a.startsWith('--'));
38
+ const target = path.resolve(targetArg ?? process.cwd());
39
+
40
+ if (!fs.existsSync(target)) {
41
+ console.error(`construct doc verify: path not found: ${target}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ const files = walkMd(target);
46
+ const results = { pass: [], fail: [], unstamped: [] };
47
+
48
+ for (const file of files) {
49
+ const content = fs.readFileSync(file, 'utf8');
50
+ if (!hasStamp(content)) {
51
+ results.unstamped.push(file);
52
+ continue;
53
+ }
54
+ const v = verifyStamp(content);
55
+ if (v.valid) results.pass.push({ file, id: v.id, created_at: v.created_at });
56
+ else results.fail.push({ file, reason: v.reason, stored: v.stored, computed: v.computed });
57
+ }
58
+
59
+ if (jsonMode) {
60
+ console.log(JSON.stringify(results, null, 2));
61
+ } else {
62
+ const rel = f => path.relative(process.cwd(), f);
63
+ for (const r of results.fail) {
64
+ console.log(` FAIL ${rel(r.file)}`);
65
+ console.log(` ${r.reason}`);
66
+ }
67
+ for (const r of results.pass) {
68
+ console.log(` ok ${rel(r.file)} [${r.id}]`);
69
+ }
70
+ if (results.unstamped.length) {
71
+ console.log(`\n ${results.unstamped.length} file(s) have no stamp (not generated by construct).`);
72
+ }
73
+ const summary = `\n ${results.pass.length} passed, ${results.fail.length} failed, ${results.unstamped.length} unstamped`;
74
+ console.log(summary);
75
+ }
76
+
77
+ if (results.fail.length > 0) process.exit(1);
78
+ }
79
+
80
+ /**
81
+ * Install the prepare-commit-msg git hook into the nearest .git/hooks/ directory.
82
+ */
83
+ export function runDocInstallHooksCli(_args) {
84
+ const src = path.join(__dirname, 'git-hooks', 'prepare-commit-msg');
85
+ if (!fs.existsSync(src)) {
86
+ console.error(`construct doc install-hooks: hook source not found: ${src}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ // Walk up from cwd to find .git
91
+ let dir = process.cwd();
92
+ let gitDir = null;
93
+ while (true) {
94
+ const candidate = path.join(dir, '.git');
95
+ if (fs.existsSync(candidate)) { gitDir = candidate; break; }
96
+ const parent = path.dirname(dir);
97
+ if (parent === dir) break;
98
+ dir = parent;
99
+ }
100
+
101
+ if (!gitDir) {
102
+ console.error('construct doc install-hooks: not inside a git repository');
103
+ process.exit(1);
104
+ }
105
+
106
+ const hooksDir = path.join(gitDir, 'hooks');
107
+ fs.mkdirSync(hooksDir, { recursive: true });
108
+ const dest = path.join(hooksDir, 'prepare-commit-msg');
109
+
110
+ if (fs.existsSync(dest)) {
111
+ console.log(` already installed: ${path.relative(process.cwd(), dest)}`);
112
+ return;
113
+ }
114
+
115
+ fs.copyFileSync(src, dest);
116
+ fs.chmodSync(dest, 0o755);
117
+ console.log(` installed: ${path.relative(process.cwd(), dest)}`);
118
+ console.log('\n Set CONSTRUCT_MODEL and CONSTRUCT_SESSION_ID in your shell or Construct config to populate trailers.');
119
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * lib/docs-routing.mjs — suggests documentation lanes for files based on content and naming.
3
+ *
4
+ * Maps file paths and content patterns to documentation sections (prds, adrs, rfcs, etc.)
5
+ * for automatic documentation organization.
6
+ */
7
+
8
+ import path from 'node:path';
9
+
10
+ export const DOC_LANE_DIRS = {
11
+ adrs: 'adr',
12
+ briefs: 'briefs',
13
+ changelogs: 'changelogs',
14
+ intake: 'intake',
15
+ memos: 'memos',
16
+ meetings: 'meetings',
17
+ notes: 'notes',
18
+ onboarding: 'onboarding',
19
+ postmortems: 'postmortems',
20
+ prds: 'prds',
21
+ rfcs: 'rfcs',
22
+ runbooks: 'runbooks',
23
+ };
24
+
25
+ export function docLaneDir(laneKey) {
26
+ return DOC_LANE_DIRS[laneKey] ?? laneKey;
27
+ }
28
+
29
+ export function suggestDocsLaneForFile(filePath, content = '') {
30
+ const fileName = path.basename(filePath).toLowerCase();
31
+
32
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
33
+ if (frontmatterMatch) {
34
+ const frontmatter = frontmatterMatch[1];
35
+ const typeMatch = frontmatter.match(/type:\s*(prd|adr|rfc|memo|runbook|brief|meeting|note)/i);
36
+ if (typeMatch) {
37
+ const type = typeMatch[1].toLowerCase();
38
+ if (type === 'adr') return 'adrs';
39
+ if (type === 'prd') return 'prds';
40
+ if (type === 'rfc') return 'rfcs';
41
+ if (type === 'memo') return 'memos';
42
+ if (type === 'runbook') return 'runbooks';
43
+ if (type === 'brief') return 'briefs';
44
+ if (type === 'meeting') return 'meetings';
45
+ if (type === 'note') return 'notes';
46
+ }
47
+ }
48
+
49
+ const prdPatterns = ['prd', 'product', 'requirement', 'spec', 'feature'];
50
+ const adrPatterns = ['adr', 'architecture', 'design', 'decision'];
51
+ const rfcPatterns = ['rfc', 'request', 'comment', 'proposal'];
52
+ const memoPatterns = ['memo', 'memorandum'];
53
+ const runbookPatterns = ['runbook', 'run book', 'operational', 'procedure'];
54
+ const briefPatterns = ['brief', 'one-pager', 'signal', 'evidence', 'research'];
55
+ const changelogPatterns = ['changelog', 'release-notes', 'releases'];
56
+ const onboardingPatterns = ['onboarding', 'setup', 'getting-started', 'local-setup'];
57
+ const postmortemPatterns = ['postmortem', 'incident-report', 'incident', 'sev-'];
58
+ const meetingPatterns = ['meeting', 'minutes', 'retro', 'standup', 'agenda', 'sync', '1-1', '1:1'];
59
+ const notePatterns = ['note', 'notes', 'scratchpad', 'journal'];
60
+
61
+ const checkPatterns = (patterns) => patterns.some((p) => fileName.includes(p));
62
+
63
+ if (checkPatterns(prdPatterns)) return 'prds';
64
+ if (checkPatterns(adrPatterns)) return 'adrs';
65
+ if (checkPatterns(rfcPatterns)) return 'rfcs';
66
+ if (checkPatterns(memoPatterns)) return 'memos';
67
+ if (checkPatterns(runbookPatterns)) return 'runbooks';
68
+ if (checkPatterns(briefPatterns)) return 'briefs';
69
+ if (checkPatterns(changelogPatterns)) return 'changelogs';
70
+ if (checkPatterns(onboardingPatterns)) return 'onboarding';
71
+ if (checkPatterns(postmortemPatterns)) return 'postmortems';
72
+ if (checkPatterns(meetingPatterns)) return 'meetings';
73
+ if (checkPatterns(notePatterns)) return 'notes';
74
+
75
+ const preview = content.slice(0, 4000).toLowerCase();
76
+ if (preview.includes('product requirement') || preview.includes('user story') || preview.includes('acceptance criteria')) return 'prds';
77
+ if (preview.includes('architecture decision') || preview.includes('design decision') || preview.includes('we decided')) return 'adrs';
78
+ if (preview.includes('request for comments') || preview.includes('please comment') || preview.includes('drawbacks')) return 'rfcs';
79
+ if (preview.includes('decision memo') || preview.includes('memorandum')) return 'memos';
80
+ if (preview.includes('runbook') || preview.includes('operational procedure') || preview.includes('troubleshooting')) return 'runbooks';
81
+ if (preview.includes('signal') || preview.includes('evidence') || preview.includes('competitive analysis') || preview.includes('research brief')) return 'briefs';
82
+ if (preview.includes('## added') || preview.includes('## changed') || preview.includes('## fixed') || preview.includes('release notes')) return 'changelogs';
83
+ if (preview.includes('local setup') || preview.includes('getting started') || preview.includes('prerequisites')) return 'onboarding';
84
+ if (preview.includes('postmortem') || preview.includes('incident report') || (preview.includes('root cause') && preview.includes('timeline') && preview.includes('impact'))) return 'postmortems';
85
+ if (preview.includes('meeting notes') || preview.includes('meeting minutes') || preview.includes('attendees') && preview.includes('action items')) return 'meetings';
86
+ if (preview.includes('notes') || preview.includes('working notes')) return 'notes';
87
+
88
+ return null;
89
+ }