@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/features.mjs CHANGED
@@ -1,15 +1,14 @@
1
1
  /**
2
- * lib/features.mjs — <one-line purpose>
2
+ * lib/features.mjs — Feature flag resolution for Construct capabilities.
3
3
  *
4
- * <2–6 line summary: what it does, who calls it, key side effects.>
4
+ * Reads ~/.cx/features.json and project-level .cx/features.json to determine
5
+ * which optional features are active. Provides isFlagEnabled() used throughout
6
+ * the codebase to gate experimental paths without modifying core logic.
5
7
  */
6
- import { readFileSync, existsSync } from 'fs';
8
+ import { readFileSync, existsSync, readdirSync } from 'fs';
7
9
  import { homedir } from 'os';
8
- import { join, dirname } from 'path';
9
- import { fileURLToPath } from 'url';
10
-
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const CATALOG_PATH = join(__dirname, 'mcp-catalog.json');
10
+ import { join } from 'path';
11
+ import { loadPluginRegistry } from './plugin-registry.mjs';
13
12
 
14
13
  function getHomeDir(overrides = {}) {
15
14
  return overrides.homeDir ?? homedir();
@@ -94,58 +93,189 @@ function readJSON(p) {
94
93
  try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
95
94
  }
96
95
 
97
- // OpenCode uses 'cass' for what Claude Code calls 'memory'
98
- const OC_ID_MAP = { memory: 'cass' };
96
+ function getHostSupport(mcp, host) {
97
+ const support = mcp?.hostSupport?.[host];
98
+ if (!support) return { mode: 'managed' };
99
+ if (typeof support === 'string') return { mode: support };
100
+ if (typeof support === 'object') return { mode: support.mode ?? 'managed', ...support };
101
+ return { mode: 'managed' };
102
+ }
103
+
104
+ function escapeRegExp(value) {
105
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
106
+ }
107
+
108
+ /**
109
+ * Build the set of IDs to match against host configs for a given MCP entry.
110
+ * Includes the canonical id, any declared aliases, and the OpenCode mapping.
111
+ */
112
+ function getMcpMatchIds(mcp) {
113
+ const ids = new Set([mcp.id]);
114
+ for (const alias of mcp.aliases ?? []) {
115
+ ids.add(alias);
116
+ }
117
+ return ids;
118
+ }
119
+
120
+ /**
121
+ * Check whether any of the given IDs appear as keys in a config object.
122
+ */
123
+ function configHasAny(config, ids) {
124
+ if (!config || typeof config !== 'object') return false;
125
+ for (const id of ids) {
126
+ if (id in config) return true;
127
+ }
128
+ return false;
129
+ }
130
+
131
+ /**
132
+ * Resolve all project-level .mcp.json paths to check.
133
+ * Claude Code supports .mcp.json at project root and in .claude/ directories.
134
+ */
135
+ function getProjectMcpPaths(overrides = {}) {
136
+ if (overrides.projectMcpPaths) return overrides.projectMcpPaths;
137
+ const cwd = overrides.cwd ?? process.cwd();
138
+ return [
139
+ join(cwd, '.mcp.json'),
140
+ join(cwd, '.claude', 'mcp.json'),
141
+ ];
142
+ }
143
+
144
+ /**
145
+ * Load Claude.ai server-side managed MCP IDs.
146
+ * These are integrations connected through Claude.ai's web interface
147
+ * (e.g., Notion, Google Drive) that aren't discoverable from local config.
148
+ * Users declare them via ~/.construct/claude-ai-mcps.json: { "mcps": ["notion", "slack"] }
149
+ */
150
+ function getClaudeAiManagedMcps(overrides = {}) {
151
+ const p = overrides.claudeAiMcpsPath ?? join(getHomeDir(overrides), '.construct', 'claude-ai-mcps.json');
152
+ if (!existsSync(p)) return [];
153
+ try {
154
+ const data = JSON.parse(readFileSync(p, 'utf8'));
155
+ return Array.isArray(data?.mcps) ? data.mcps : [];
156
+ } catch {
157
+ return [];
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Resolve Claude marketplace plugin directories to scan for .mcp.json files.
163
+ */
164
+ function getMarketplaceMcpPaths(overrides = {}) {
165
+ if (overrides.marketplaceMcpPaths) return overrides.marketplaceMcpPaths;
166
+ const home = getHomeDir(overrides);
167
+ const marketplaceRoot = join(home, '.claude', 'plugins', 'marketplaces');
168
+ if (!existsSync(marketplaceRoot)) return [];
99
169
 
100
- function getMcpPlatforms(serverId, overrides = {}) {
170
+ const paths = [];
171
+ try {
172
+ for (const marketplace of readdirSync(marketplaceRoot)) {
173
+ const extDir = join(marketplaceRoot, marketplace, 'external_plugins');
174
+ if (!existsSync(extDir)) continue;
175
+ try {
176
+ for (const plugin of readdirSync(extDir)) {
177
+ const mcpFile = join(extDir, plugin, '.mcp.json');
178
+ if (existsSync(mcpFile)) paths.push(mcpFile);
179
+ }
180
+ } catch { /* skip unreadable dirs */ }
181
+ }
182
+ } catch { /* skip unreadable marketplace root */ }
183
+ return paths;
184
+ }
185
+
186
+ function getMcpPlatforms(mcp, overrides = {}) {
101
187
  const platforms = [];
188
+ const matchIds = getMcpMatchIds(mcp);
189
+
190
+ // --- Claude.ai server-side managed MCPs ---
191
+ const cloudMcps = getClaudeAiManagedMcps(overrides);
192
+ if (cloudMcps.some((id) => matchIds.has(id))) {
193
+ platforms.push('Claude.ai');
194
+ }
102
195
 
196
+ // --- Claude Code: user settings (~/.claude/settings.json) ---
103
197
  const claudeSettings = readClaudeSettings(overrides);
104
- if (claudeSettings?.mcpServers && serverId in claudeSettings.mcpServers) {
198
+ if (getHostSupport(mcp, 'claude').mode === 'managed' && configHasAny(claudeSettings?.mcpServers, matchIds)) {
105
199
  platforms.push('Claude Code');
106
200
  }
107
201
 
202
+ // --- Claude Code: project-level .mcp.json ---
203
+ if (!platforms.includes('Claude Code')) {
204
+ for (const p of getProjectMcpPaths(overrides)) {
205
+ if (!existsSync(p)) continue;
206
+ const projectMcp = readJSON(p);
207
+ if (configHasAny(projectMcp?.mcpServers, matchIds) || configHasAny(projectMcp, matchIds)) {
208
+ platforms.push('Claude Code');
209
+ break;
210
+ }
211
+ }
212
+ }
213
+
214
+ // --- Claude marketplace plugins (~/.claude/plugins/marketplaces/*/external_plugins/*/.mcp.json) ---
215
+ if (!platforms.includes('Claude Code')) {
216
+ for (const p of getMarketplaceMcpPaths(overrides)) {
217
+ const pluginMcp = readJSON(p);
218
+ if (configHasAny(pluginMcp, matchIds)) {
219
+ platforms.push('Claude Code');
220
+ break;
221
+ }
222
+ }
223
+ }
224
+
225
+ // --- OpenCode ---
108
226
  const ocConfig = readOpenCodeConfig(overrides);
109
- const ocId = OC_ID_MAP[serverId] ?? serverId;
110
- if (ocConfig?.mcp && ocId in ocConfig.mcp) {
227
+ const ocIds = new Set([...matchIds]);
228
+ if (getHostSupport(mcp, 'opencode').mode === 'managed' && configHasAny(ocConfig?.mcp, ocIds)) {
111
229
  platforms.push('OpenCode');
112
230
  }
113
231
 
232
+ // --- VS Code / Copilot ---
114
233
  for (const p of getVSCodeSettingsPaths(overrides)) {
115
234
  const settings = readJSON(p);
116
- if (settings?.['github.copilot.mcpServers'] && serverId in settings['github.copilot.mcpServers']) {
235
+ if (configHasAny(settings?.['github.copilot.mcpServers'], matchIds)) {
117
236
  const label = p.includes('Insiders') ? 'VS Code Insiders' : 'VS Code';
118
237
  if (!platforms.includes(label)) platforms.push(label);
119
238
  }
120
239
  }
121
240
 
241
+ // --- Cursor ---
122
242
  const cursorMcpPath = overrides.cursorMcpPath ?? join(getHomeDir(overrides), '.cursor', 'mcp.json');
123
243
  if (existsSync(cursorMcpPath)) {
124
244
  const cursorConfig = readJSON(cursorMcpPath);
125
- if (cursorConfig?.mcpServers && serverId in cursorConfig.mcpServers) {
245
+ if (configHasAny(cursorConfig?.mcpServers, matchIds)) {
126
246
  platforms.push('Cursor');
127
247
  }
128
248
  }
129
249
 
130
- return platforms;
131
- }
132
-
133
- function readCatalog() {
134
- if (!existsSync(CATALOG_PATH)) return { mcps: [] };
135
- try {
136
- return JSON.parse(readFileSync(CATALOG_PATH, 'utf8'));
137
- } catch {
138
- return { mcps: [] };
250
+ // --- Codex ---
251
+ const codexConfigPath = overrides.codexConfigPath ?? join(getHomeDir(overrides), '.codex', 'config.toml');
252
+ if (existsSync(codexConfigPath)) {
253
+ const codexConfig = readFileSync(codexConfigPath, 'utf8');
254
+ const codexSupport = getHostSupport(mcp, 'codex');
255
+ if (codexSupport.mode === 'plugin' && codexSupport.plugin) {
256
+ const pattern = new RegExp(`\\[plugins\\.${JSON.stringify(codexSupport.plugin)}\\]\\s*\\nenabled = true`, 'm');
257
+ if (pattern.test(codexConfig)) platforms.push(codexSupport.label ?? 'Codex Plugin');
258
+ } else if (codexSupport.mode === 'managed') {
259
+ const idAlts = [...matchIds].map(escapeRegExp).join('|');
260
+ const pattern = new RegExp(`^\\[mcp_servers\\.("?)(?:${idAlts})\\1\\]`, 'm');
261
+ if (pattern.test(codexConfig)) platforms.push('Codex');
262
+ }
139
263
  }
264
+
265
+ return platforms;
140
266
  }
141
267
 
142
268
  /**
143
- * Build features dynamically from mcp-catalog.json.
269
+ * Build features dynamically from the plugin registry.
144
270
  * Each MCP reports configured state when present in host configs and unavailable when missing.
145
271
  */
146
272
  function buildFeatures(overrides = {}) {
147
- const catalog = readCatalog();
148
- return catalog.mcps.map((mcp) => ({
273
+ const registry = loadPluginRegistry({
274
+ cwd: overrides.cwd ?? process.cwd(),
275
+ homeDir: getHomeDir(overrides),
276
+ env: overrides.env ?? process.env,
277
+ });
278
+ return registry.mcps.map((mcp) => ({
149
279
  id: mcp.id,
150
280
  name: mcp.name,
151
281
  category: mcp.category,
@@ -153,7 +283,7 @@ function buildFeatures(overrides = {}) {
153
283
  degradedMessage: mcp.degradedMessage,
154
284
  usedBy: mcp.usedBy ?? [],
155
285
  async check() {
156
- const platforms = getMcpPlatforms(mcp.id, overrides);
286
+ const platforms = getMcpPlatforms(mcp, overrides);
157
287
  if (platforms.length > 0) {
158
288
  return { status: 'configured', message: `Configured in ${platforms.join(', ')}` };
159
289
  }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * lib/gates-audit.mjs — Cross-surface enforcement audit.
3
+ *
4
+ * Verifies that every policy gate is consistently expressed across the four
5
+ * places enforcement can live: CI workflow jobs, the local pre-push hook,
6
+ * the local pre-commit hook (when `core.hooksPath` is wired), and GitHub
7
+ * branch protection (required status checks). Identifies gaps — checks that
8
+ * exist in one place but not another, or that are required-to-merge but
9
+ * absent from CI, or that are CI-only with no local mirror.
10
+ *
11
+ * The mapping between CI job names and local commands lives in the
12
+ * GATE_DEFINITIONS table below. When a new CI job is added, the mapping
13
+ * must be updated to declare what its local equivalent should be (or that
14
+ * none is expected). This is the same single-source-of-truth pattern the
15
+ * project already uses for CLI commands in lib/cli-commands.mjs.
16
+ *
17
+ * Exit code (in `construct gates:audit`): 0 if no critical gaps; 1 if any
18
+ * critical gap is found. Non-critical advisories never fail the command.
19
+ */
20
+
21
+ import { readFileSync, existsSync } from 'node:fs';
22
+ import { join, dirname } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { execSync } from 'node:child_process';
25
+
26
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
27
+ const DEFAULT_ROOT_DIR = join(MODULE_DIR, '..');
28
+
29
+ // Authoritative gate mapping. Each entry declares a CI job name and its
30
+ // expected local mirror. `critical: true` means this gate MUST be in CI and
31
+ // SHOULD be in branch-protection required checks; absence is a hard gap.
32
+ // `localMirror: 'ci-only'` flags gates that legitimately have no local
33
+ // equivalent (e.g., dockerized integration tests).
34
+
35
+ const GATE_DEFINITIONS = [
36
+ { ciJob: 'test (ubuntu-latest / node 22)', prePushLabel: 'tests', critical: true },
37
+ { ciJob: 'test (macos-latest / node 22)', prePushLabel: 'tests', critical: true },
38
+ { ciJob: 'test (ubuntu-latest / node 20)', prePushLabel: 'tests', critical: true },
39
+ { ciJob: 'test (macos-latest / node 20)', prePushLabel: 'tests', critical: true },
40
+ { ciJob: 'retrieval evals', prePushLabel: 'evals', critical: true },
41
+ { ciJob: 'dependency CVE audit', prePushLabel: 'audit', critical: true },
42
+ { ciJob: 'secret scanning', prePushLabel: null, preCommitCheck: 'ECC secret scan', critical: true, note: 'pre-commit ECC scan covers a subset of gitleaks rules' },
43
+ { ciJob: 'postgres + pgvector integration', prePushLabel: null, preCommitCheck: null, critical: false, note: 'CI-only: requires Docker Postgres + pgvector container; not practical locally', localMirror: 'ci-only' },
44
+ { ciJob: 'docs drift check', prePushLabel: 'docs', critical: true },
45
+ { ciJob: 'comment policy', prePushLabel: null, preCommitCheck: 'Construct comment-lint (staged)', critical: true, note: 'pre-commit Construct policy section calls `lint:comments --staged`' },
46
+ { ciJob: 'template policy', prePushLabel: null, preCommitGhPrIntercept: true, critical: true, note: 'pre-push-gate intercepts `gh pr create` / `gh pr edit` and lints the body' },
47
+ ];
48
+
49
+ function parseCIJobs(rootDir) {
50
+ const path = join(rootDir, '.github', 'workflows', 'ci.yml');
51
+ if (!existsSync(path)) return [];
52
+ const yml = readFileSync(path, 'utf8');
53
+ return [...yml.matchAll(/^ name:\s*(.+?)\s*$/gm)]
54
+ .map((m) => m[1].trim())
55
+ .map((n) => n.replace(/\$\{\{\s*matrix\.(\w+)\s*\}\}/g, (_, k) => `<${k}>`));
56
+ }
57
+
58
+ function parsePrePushJobs(rootDir) {
59
+ const path = join(rootDir, 'lib', 'hooks', 'pre-push-gate.mjs');
60
+ if (!existsSync(path)) return [];
61
+ const src = readFileSync(path, 'utf8');
62
+ return [...src.matchAll(/label:\s*['"]([^'"]+)['"]/g)].map((m) => m[1]);
63
+ }
64
+
65
+ function parsePreCommitChecks(rootDir) {
66
+ const path = join(rootDir, '.beads', 'hooks', 'pre-commit');
67
+ if (!existsSync(path)) return [];
68
+ const src = readFileSync(path, 'utf8');
69
+ const checks = [];
70
+ if (src.includes('scan_added_lines')) checks.push('ECC secret scan');
71
+ if (src.includes('construct lint:comments --staged')) checks.push('Construct comment-lint (staged)');
72
+ if (src.includes('construct docs:verify --staged')) checks.push('Construct docs:verify (staged)');
73
+ if (src.includes('BEADS INTEGRATION')) checks.push('BEADS dispatcher');
74
+ return checks;
75
+ }
76
+
77
+ function getCoreHooksPath(rootDir) {
78
+ try {
79
+ return execSync('git config --get core.hooksPath', {
80
+ cwd: rootDir, stdio: 'pipe', encoding: 'utf8', timeout: 2000,
81
+ }).trim();
82
+ } catch {
83
+ return '';
84
+ }
85
+ }
86
+
87
+ function fetchBranchProtection(branch, { repo = 'geraldmaron/construct' } = {}) {
88
+ try {
89
+ const out = execSync(
90
+ `gh api repos/${repo}/branches/${branch}/protection/required_status_checks`,
91
+ { stdio: 'pipe', encoding: 'utf8', timeout: 5000 },
92
+ );
93
+ const data = JSON.parse(out);
94
+ return { protected: true, requiredContexts: data.contexts || [], status: 'fetched' };
95
+ } catch (err) {
96
+ const msg = err?.stderr?.toString() || err?.message || '';
97
+ if (/Branch not protected/i.test(msg)) {
98
+ return { protected: false, requiredContexts: [], status: 'unprotected' };
99
+ }
100
+ return { protected: false, requiredContexts: [], status: 'unfetchable' };
101
+ }
102
+ }
103
+
104
+ function ciHasJob(ciJobs, jobName) {
105
+ if (ciJobs.includes(jobName)) return true;
106
+ const stripped = jobName.replace(/\s*\([^)]*\)\s*/g, '').trim();
107
+ return ciJobs.some((j) => j.replace(/\s*\(.*?\)\s*/g, '').trim() === stripped);
108
+ }
109
+
110
+ function identifyGaps(state) {
111
+ const { ciJobs, prePushJobs, preCommitChecks, branchProtection, hooksPath, rootDir, inCI } = state;
112
+ const gaps = [];
113
+ const protectionVisible = branchProtection.status === 'fetched' || branchProtection.status === 'unprotected';
114
+
115
+ for (const def of GATE_DEFINITIONS) {
116
+ const inCi = ciHasJob(ciJobs, def.ciJob);
117
+ if (!inCi) {
118
+ gaps.push({ kind: 'missing-ci', gate: def.ciJob, critical: def.critical });
119
+ continue;
120
+ }
121
+
122
+ if (def.critical && protectionVisible) {
123
+ const required = branchProtection.requiredContexts.includes(def.ciJob);
124
+ if (!required) {
125
+ gaps.push({ kind: 'not-required-to-merge', gate: def.ciJob, critical: true });
126
+ }
127
+ }
128
+
129
+ const hasPrePush = def.prePushLabel ? prePushJobs.includes(def.prePushLabel) : false;
130
+ const hasPreCommit = def.preCommitCheck ? preCommitChecks.includes(def.preCommitCheck) : false;
131
+ const hasIntercept = !!def.preCommitGhPrIntercept;
132
+ const hasLocal = hasPrePush || hasPreCommit || hasIntercept || def.localMirror === 'ci-only';
133
+
134
+ if (def.critical && !hasLocal) {
135
+ gaps.push({ kind: 'no-local-mirror', gate: def.ciJob, note: def.note, critical: true });
136
+ }
137
+ if (!def.critical && !hasLocal && !def.note) {
138
+ gaps.push({ kind: 'no-local-mirror-advisory', gate: def.ciJob, critical: false });
139
+ }
140
+ }
141
+
142
+ if (protectionVisible) {
143
+ for (const ctx of branchProtection.requiredContexts) {
144
+ if (!ciHasJob(ciJobs, ctx)) {
145
+ gaps.push({ kind: 'required-but-not-in-ci', gate: ctx, critical: true });
146
+ }
147
+ }
148
+ }
149
+
150
+ if (!inCI && existsSync(join(rootDir, '.beads', 'hooks', 'pre-commit')) && hooksPath !== '.beads/hooks') {
151
+ gaps.push({
152
+ kind: 'hooks-unwired',
153
+ expected: '.beads/hooks',
154
+ actual: hooksPath || '(unset)',
155
+ critical: true,
156
+ note: 'pre-commit policy gates are inactive locally; fix with: git config core.hooksPath .beads/hooks',
157
+ });
158
+ }
159
+
160
+ return gaps;
161
+ }
162
+
163
+ export function auditGates({ rootDir = DEFAULT_ROOT_DIR, repo, branch = 'main' } = {}) {
164
+ const inCI = process.env.CI === 'true' || process.env.CI === '1';
165
+ const ciJobs = parseCIJobs(rootDir);
166
+ const prePushJobs = parsePrePushJobs(rootDir);
167
+ const preCommitChecks = parsePreCommitChecks(rootDir);
168
+ const hooksPath = getCoreHooksPath(rootDir);
169
+ const branchProtection = fetchBranchProtection(branch, repo ? { repo } : {});
170
+
171
+ const state = { ciJobs, prePushJobs, preCommitChecks, branchProtection, hooksPath, rootDir, inCI };
172
+ const gaps = identifyGaps(state);
173
+ const criticalGaps = gaps.filter((g) => g.critical);
174
+
175
+ return {
176
+ rootDir,
177
+ ciJobs,
178
+ prePushJobs,
179
+ preCommitChecks,
180
+ hooksPath,
181
+ branchProtection,
182
+ gaps,
183
+ criticalGaps,
184
+ inCI,
185
+ ok: criticalGaps.length === 0,
186
+ };
187
+ }
188
+
189
+ export function formatReport(report) {
190
+ const lines = [];
191
+ lines.push('Construct Gates Audit');
192
+ lines.push('═════════════════════');
193
+ lines.push('');
194
+
195
+ lines.push(`CI jobs (${report.ciJobs.length}):`);
196
+ for (const j of report.ciJobs) lines.push(` • ${j}`);
197
+ lines.push('');
198
+
199
+ lines.push(`Local pre-push gate jobs (${report.prePushJobs.length}):`);
200
+ for (const j of report.prePushJobs) lines.push(` • ${j}`);
201
+ lines.push('');
202
+
203
+ lines.push(`Local pre-commit checks (${report.preCommitChecks.length}):`);
204
+ for (const j of report.preCommitChecks) lines.push(` • ${j}`);
205
+ lines.push('');
206
+
207
+ lines.push(`core.hooksPath: ${report.hooksPath || '(unset)'}`);
208
+ lines.push('');
209
+
210
+ if (report.branchProtection.status === 'fetched') {
211
+ lines.push(`Branch protection (main): ${report.branchProtection.requiredContexts.length} required contexts`);
212
+ for (const c of report.branchProtection.requiredContexts) lines.push(` • ${c}`);
213
+ } else if (report.branchProtection.status === 'unprotected') {
214
+ lines.push('Branch protection (main): NOT PROTECTED — no gate enforces CI before merge');
215
+ } else {
216
+ lines.push('Branch protection (main): unfetchable (insufficient gh permissions) — protection-dependent gaps skipped');
217
+ }
218
+ lines.push('');
219
+
220
+ if (report.gaps.length === 0) {
221
+ lines.push('Gaps: none');
222
+ } else {
223
+ lines.push(`Gaps (${report.gaps.length}; ${report.criticalGaps.length} critical):`);
224
+ for (const g of report.gaps) {
225
+ const marker = g.critical ? '✗' : '⚠';
226
+ const detail = g.note ? ` — ${g.note}` : '';
227
+ const extra = g.kind === 'hooks-unwired' ? ` (expected ${g.expected}, actual ${g.actual})` : '';
228
+ lines.push(` ${marker} ${g.kind}: ${g.gate || ''}${extra}${detail}`);
229
+ }
230
+ }
231
+ lines.push('');
232
+
233
+ lines.push(`Summary: ${report.ok ? 'OK ✓' : `${report.criticalGaps.length} critical gap(s) ✗`}`);
234
+
235
+ return lines.join('\n') + '\n';
236
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/git-hooks/prepare-commit-msg
4
+ *
5
+ * Git hook that appends AI provenance trailers to every commit message.
6
+ * Follows the RFC 9562 / git-interpret-trailers convention.
7
+ *
8
+ * Trailers added (when env vars are present):
9
+ * AI-Generator: construct/sync-agents
10
+ * AI-Model: claude-sonnet-4-6
11
+ * AI-Session: 019dbb90-5885-...
12
+ *
13
+ * Install for a repo:
14
+ * construct doc install-hooks
15
+ *
16
+ * Or manually:
17
+ * cp node_modules/.bin/construct-prepare-commit-msg .git/hooks/prepare-commit-msg
18
+ * chmod +x .git/hooks/prepare-commit-msg
19
+ *
20
+ * The hook reads env vars set by Construct sessions:
21
+ * CONSTRUCT_MODEL — active model name
22
+ * CONSTRUCT_SESSION_ID — UUIDv7 session id
23
+ * CONSTRUCT_GENERATOR — generator label (optional)
24
+ */
25
+
26
+ import fs from 'node:fs';
27
+
28
+ const msgFile = process.argv[2];
29
+ const source = process.argv[3]; // 'message', 'template', 'merge', 'squash', 'commit'
30
+
31
+ // Only annotate regular commits and explicit message commits, not merges/squashes
32
+ if (source === 'merge' || source === 'squash') process.exit(0);
33
+ if (!msgFile) process.exit(0);
34
+
35
+ const model = process.env.CONSTRUCT_MODEL;
36
+ const sessionId = process.env.CONSTRUCT_SESSION_ID;
37
+ const generator = process.env.CONSTRUCT_GENERATOR;
38
+
39
+ // Nothing to add if no Construct env vars are set
40
+ if (!model && !sessionId && !generator) process.exit(0);
41
+
42
+ const original = fs.readFileSync(msgFile, 'utf8');
43
+
44
+ // Build trailers — only include fields that are populated
45
+ const trailers = [];
46
+ if (generator) trailers.push(`AI-Generator: ${generator}`);
47
+ if (model) trailers.push(`AI-Model: ${model}`);
48
+ if (sessionId) trailers.push(`AI-Session: ${sessionId}`);
49
+
50
+ if (!trailers.length) process.exit(0);
51
+
52
+ // git-interpret-trailers convention: trailers follow a blank line after the body.
53
+ // We append only if the trailers aren't already present (idempotent).
54
+ const alreadyPresent = trailers.every(t => original.includes(t.split(':')[0] + ':'));
55
+ if (alreadyPresent) process.exit(0);
56
+
57
+ const separator = original.trimEnd().endsWith('\n\n') ? '' : '\n\n';
58
+ fs.writeFileSync(msgFile, original.trimEnd() + separator + trailers.join('\n') + '\n');