@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
@@ -3,17 +3,17 @@
3
3
  * lib/hooks/mcp-audit.mjs — MCP audit hook — logs all MCP tool calls for observability and review.
4
4
  *
5
5
  * Runs as PostToolUse on MCP tool calls. Records tool name, input, and output summary to ~/.cx/mcp-audit.json for telemetry and security review.
6
+ *
7
+ * @p95ms 10
8
+ * @maxBlockingScope none (PostToolUse, non-blocking)
6
9
  */
7
- // PostToolUse(mcp__*) — audits every MCP tool call against the active workflow task.
8
- // Writes .cx/mcp-audit.json: per-server call log with task attribution.
9
- // Gives per-task MCP usage visibility that per-task scoped MCPs (OmO) cannot provide.
10
10
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
11
11
  import { join } from 'path';
12
+ import { shouldCreateCx } from '../project-detection.mjs';
12
13
 
13
14
  let input = {};
14
15
  try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
15
16
 
16
- const cwd = input?.cwd || process.cwd();
17
17
  const toolName = input?.tool_name || '';
18
18
 
19
19
  const match = toolName.match(/^mcp__([^_]+(?:__[^_]+)*)__(.+)$/);
@@ -22,36 +22,34 @@ if (!match) process.exit(0);
22
22
  const mcpServer = match[1].replace(/__/g, '-');
23
23
  const mcpTool = match[2];
24
24
 
25
- // Get active task key from workflow
26
- let activeTaskKey = null;
27
- let activeTaskTitle = null;
28
- try {
29
- const wf = JSON.parse(readFileSync(join(cwd, '.cx', 'workflow.json'), 'utf8'));
30
- activeTaskKey = wf.currentTaskKey || null;
31
- if (activeTaskKey) {
32
- const t = (wf.tasks || []).find(t => t.key === activeTaskKey);
33
- activeTaskTitle = t?.title || null;
34
- }
35
- } catch { /* no workflow */ }
25
+ const cwd = input?.cwd || process.cwd();
26
+
27
+ // Only create .cx if this is an initialized project
28
+ const shouldCreateCxDir = shouldCreateCx(cwd);
36
29
 
37
30
  const auditPath = join(cwd, '.cx', 'mcp-audit.json');
38
31
  let audit = {};
39
- try { audit = JSON.parse(readFileSync(auditPath, 'utf8')); } catch { /* fresh */ }
32
+
33
+ // Try to read existing audit file if it exists
34
+ if (existsSync(auditPath)) {
35
+ try { audit = JSON.parse(readFileSync(auditPath, 'utf8')); } catch { /* fresh */ }
36
+ }
40
37
 
41
38
  if (!audit[mcpServer]) audit[mcpServer] = [];
42
39
  audit[mcpServer].push({
43
40
  tool: mcpTool,
44
41
  ts: new Date().toISOString(),
45
- taskKey: activeTaskKey,
46
- taskTitle: activeTaskTitle,
42
+ coordination: 'tracker-plus-plan',
47
43
  });
48
44
 
49
45
  // Keep last 200 per server
50
46
  if (audit[mcpServer].length > 200) audit[mcpServer] = audit[mcpServer].slice(-200);
51
47
 
52
48
  try {
53
- mkdirSync(join(cwd, '.cx'), { recursive: true });
54
- writeFileSync(auditPath, JSON.stringify(audit, null, 2));
49
+ if (shouldCreateCxDir) {
50
+ mkdirSync(join(cwd, '.cx'), { recursive: true });
51
+ writeFileSync(auditPath, JSON.stringify(audit, null, 2));
52
+ }
55
53
  } catch { /* best effort */ }
56
54
 
57
55
  process.exit(0);
@@ -3,6 +3,9 @@
3
3
  * lib/hooks/mcp-health-check.mjs — MCP health check hook — verifies MCP servers are reachable before tool use.
4
4
  *
5
5
  * Runs as PreToolUse on MCP tool calls. Checks that the target MCP server is running and reachable. Emits a warning (does not block) if the server is unavailable.
6
+ *
7
+ * @p95ms 51
8
+ * @maxBlockingScope none (PreToolUse, warn-only)
6
9
  */
7
10
  import { readFileSync, writeFileSync, existsSync } from 'fs';
8
11
  import { homedir } from 'os';
@@ -47,5 +50,38 @@ if (entry?.status === 'unhealthy' && (Date.now() - entry.since) < CACHE_TTL_MS)
47
50
  process.exit(2);
48
51
  }
49
52
 
53
+ // Role-fence check for MCP tool calls. Most personas have no MCP fence
54
+ // declared (allows everything) — when fence.allowedMcpTools is present, the
55
+ // tool name must match an entry. Out-of-fence MCP calls block with exit 2.
56
+
57
+ if (process.env.CONSTRUCT_ROLES !== 'off') {
58
+ try {
59
+ const lastAgentPath = `${homedir()}/.cx/last-agent.json`;
60
+ if (existsSync(lastAgentPath)) {
61
+ const last = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
62
+ const lastTs = last?.ts ? Date.parse(last.ts) : 0;
63
+ const fresh = lastTs && (Date.now() - lastTs) < 10 * 60 * 1000;
64
+ const id = String(last?.agent || '').replace(/^cx-/, '');
65
+ if (fresh && id) {
66
+ const { isOnboarded, loadManifest } = await import('../roles/manifest.mjs');
67
+ if (isOnboarded(id)) {
68
+ const manifest = loadManifest(id);
69
+ const allowedMcpTools = manifest?.fence?.allowedMcpTools;
70
+ if (Array.isArray(allowedMcpTools) && allowedMcpTools.length > 0) {
71
+ const matches = allowedMcpTools.some((p) => toolName === p || toolName.startsWith(p));
72
+ if (!matches) {
73
+ process.stderr.write(
74
+ `[fence] cx-${id} cannot call MCP tool ${toolName} — outside declared fence.\n` +
75
+ `Allowed MCP tools: ${allowedMcpTools.join(', ')}\n`
76
+ );
77
+ process.exit(2);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ } catch { /* best effort */ }
84
+ }
85
+
50
86
  process.stdout.write(JSON.stringify(input) + '\n');
51
87
  process.exit(0);
@@ -1,36 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/hooks/model-fallback.mjs — Model fallback hook — reroutes requests when the primary model is unavailable.
3
+ * lib/hooks/model-fallback.mjs — Provider-aware model fallback hook.
4
4
  *
5
- * Runs as PostToolUse when model calls fail. Reads the error, selects a fallback provider from the model router, and retries the request with the alternate model.
5
+ * Runs as PostToolUse. On a retryable provider failure it:
6
+ * 1. Classifies the error via classifyProviderFailure.
7
+ * 2. Selects a fallback candidate via selectFallbackModel, skipping any
8
+ * provider currently in its 5-minute cooldown window.
9
+ * 3. Writes the new model directly to the project .env via applyToEnv.
10
+ * 4. Records a cooldown entry for the failing provider.
11
+ *
12
+ * Falls back to `construct models --apply` only when no candidate is resolved
13
+ * (e.g. no registry fallback list, all candidates on cooldown, or no OpenRouter
14
+ * key for the free-tier path).
15
+ *
16
+ * @p95ms 150
17
+ * @maxBlockingScope none (PostToolUse, non-blocking)
6
18
  */
7
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
20
  import { execFileSync } from 'node:child_process';
9
21
  import { homedir } from 'node:os';
10
- import { dirname, join } from 'node:path';
11
- import { readOpenRouterApiKeyFromOpenCodeConfig } from '../model-router.mjs';
22
+ import { join } from 'node:path';
23
+ import {
24
+ applyToEnv,
25
+ classifyProviderFailure,
26
+ readOpenRouterApiKeyFromOpenCodeConfig,
27
+ selectFallbackModel,
28
+ writeProviderCooldown,
29
+ } from '../model-router.mjs';
12
30
 
13
- const RATE_LIMIT_PATTERNS = [
14
- /\b429\b/i,
15
- /rate limit/i,
16
- /usage limits?/i,
17
- /specified API usage limits?/i,
18
- /regain access/i,
19
- /weekly limit/i,
20
- /monthly limit/i,
21
- /daily limit/i,
22
- /too many requests/i,
23
- /quota exceeded/i,
24
- /model unavailable/i,
25
- /model.*overloaded/i,
26
- /timeout/i,
27
- /timed out/i,
28
- /ETIMEDOUT/i,
29
- /ECONNRESET/i,
30
- ];
31
-
32
- const COOLDOWN_MS = 10 * 60 * 1000;
33
- const statePath = join(homedir(), '.cx', 'construct-model-fallback.json');
31
+ const cooldownPath = join(homedir(), '.cx', 'provider-cooldowns.json');
32
+ const envPath = join(process.cwd(), '.env');
34
33
 
35
34
  function readInput() {
36
35
  try {
@@ -41,65 +40,52 @@ function readInput() {
41
40
  }
42
41
  }
43
42
 
44
- function flatten(value) {
45
- if (value === null || value === undefined) return '';
46
- if (typeof value === 'string') return value;
47
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
48
- if (Array.isArray(value)) return value.map(flatten).join('\n');
49
- if (typeof value === 'object') return Object.values(value).map(flatten).join('\n');
50
- return '';
51
- }
52
-
53
- function recentlyApplied(now) {
54
- try {
55
- const state = JSON.parse(readFileSync(statePath, 'utf8'));
56
- return typeof state.lastAppliedAt === 'number' && now - state.lastAppliedAt < COOLDOWN_MS;
57
- } catch {
58
- return false;
59
- }
60
- }
43
+ const input = readInput();
61
44
 
62
- function writeState(payload) {
63
- mkdirSync(dirname(statePath), { recursive: true });
64
- writeFileSync(statePath, JSON.stringify(payload, null, 2));
65
- }
45
+ const classified = classifyProviderFailure(input);
46
+ if (!classified || !classified.retryable) process.exit(0);
66
47
 
67
- const input = readInput();
68
- const haystack = flatten(input);
69
- if (!RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(haystack))) process.exit(0);
48
+ process.stderr.write(`[model-fallback] ${classified.kind} detected (provider: ${classified.provider ?? 'unknown'}).\n`);
70
49
 
71
50
  const now = Date.now();
72
- if (recentlyApplied(now)) {
73
- process.stderr.write('[model-fallback] Rate limit/timeout detected; fallback recently applied, skipping cooldown window.\n');
51
+ const result = selectFallbackModel({ hookInput: input, envPath, cooldownPath, now });
52
+
53
+ if (result) {
54
+ process.stderr.write(`[model-fallback] Switching ${result.tier} → ${result.targetModel} (reason: ${result.reason}).\n`);
55
+ applyToEnv(envPath, { [result.tier]: result.targetModel });
56
+ if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
74
57
  process.exit(0);
75
58
  }
76
59
 
60
+ // No candidate resolved — fall back to full `construct models --apply` if possible.
77
61
  const toolkitDir = process.env.CX_TOOLKIT_DIR || join(homedir(), '.construct');
78
62
  const constructBin = join(toolkitDir, 'bin', 'construct');
79
63
 
80
64
  if (!existsSync(constructBin)) {
81
- process.stderr.write(`[model-fallback] construct binary not found at ${constructBin}\n`);
65
+ process.stderr.write(`[model-fallback] No fallback candidate and construct binary not found at ${constructBin}.\n`);
66
+ if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
82
67
  process.exit(0);
83
68
  }
84
69
 
85
70
  const openRouterApiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
86
71
 
87
72
  if (!openRouterApiKey) {
88
- process.stderr.write('[model-fallback] Rate limit/timeout detected; no OpenRouter API key found in OPENROUTER_API_KEY or OpenCode config, so automatic fallback cannot poll models.\n');
73
+ process.stderr.write('[model-fallback] No fallback candidate and no OpenRouter API key cannot poll models.\n');
74
+ if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
89
75
  process.exit(0);
90
76
  }
91
77
 
92
78
  try {
93
- process.stderr.write('[model-fallback] Rate limit/timeout detected. Running construct models --apply.\n');
79
+ process.stderr.write('[model-fallback] No direct candidate running construct models --apply.\n');
94
80
  execFileSync(constructBin, ['models', '--apply'], {
95
81
  cwd: toolkitDir,
96
82
  stdio: 'inherit',
97
83
  env: { ...process.env, CX_TOOLKIT_DIR: toolkitDir, OPENROUTER_API_KEY: openRouterApiKey },
98
84
  timeout: 120_000,
99
85
  });
100
- writeState({ lastAppliedAt: now, reason: 'rate-limit-or-timeout' });
86
+ if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
101
87
  } catch (error) {
102
- process.stderr.write(`[model-fallback] Fallback failed: ${error.message}\n`);
88
+ process.stderr.write(`[model-fallback] construct models --apply failed: ${error.message}\n`);
103
89
  }
104
90
 
105
91
  process.exit(0);
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/policy-engine.mjs — consolidated session policy enforcement hook.
4
+ *
5
+ * Handles three hook events:
6
+ * PreToolUse bootstrap gating (Write/Edit blocked until session is grounded)
7
+ * Stop drive-mode criteria blocks; red-CI and open-bd are advisory warns
8
+ * UserPromptSubmit reserved for workflow rule (no-op today)
9
+ *
10
+ * Stop is intentionally non-blocking for red CI and in-progress beads — those
11
+ * are normal states across sessions, not session-ending failures. Blocking
12
+ * with a sticky env-var bypass is the antipattern we removed elsewhere; it
13
+ * trades real friction for theoretical safety. Drive mode still blocks because
14
+ * drive mode represents an explicit user opt-in.
15
+ *
16
+ * @p95ms 80
17
+ * @maxBlockingScope PreToolUse, Stop, UserPromptSubmit
18
+ */
19
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
20
+ import { execSync } from 'node:child_process';
21
+ import { join } from 'node:path';
22
+ import { homedir } from 'node:os';
23
+
24
+ let input = {};
25
+ try { input = JSON.parse(readFileSync(0, 'utf8')) || {}; } catch { process.exit(0); }
26
+
27
+ const hookEvent = process.env.CONSTRUCT_HOOK_EVENT || input?.hook_event || process.argv[2] || '';
28
+ const toolName = input?.tool_name || input?.tool || '';
29
+ const toolInput = input?.tool_input || {};
30
+ const cwd = input?.cwd || process.cwd();
31
+ const home = homedir();
32
+
33
+ function safeExec(cmd, opts = {}) {
34
+ try { return execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, ...opts }).toString().trim(); }
35
+ catch { return null; }
36
+ }
37
+
38
+ if (hookEvent === 'PreToolUse') {
39
+ // Bootstrap gate scope: only mutation tools (Write/Edit/MultiEdit/
40
+ // NotebookEdit/TodoWrite) require grounding. Bash is not gated here —
41
+ // dangerous Bash is owned by guard-bash.mjs; gating read-only shell
42
+ // here would block grep/find/git log and create chicken-and-egg
43
+ // friction when the same hook also needs Read calls to mark the
44
+ // session bootstrapped.
45
+
46
+ const BOOTSTRAP_TOOLS = new Set([
47
+ 'mcp__construct-mcp__project_context',
48
+ 'mcp__construct-mcp__memory_search',
49
+ 'project_context', 'memory_search',
50
+ ]);
51
+ const READ_TOOLS = new Set(['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']);
52
+ const BLOCKED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'TodoWrite']);
53
+
54
+ const stateDir = join(home, '.cx');
55
+ const statePath = join(stateDir, 'bootstrap-state.json');
56
+ const sessionId = input.session_id || input.sessionId || 'default';
57
+
58
+ let state = {};
59
+ if (existsSync(statePath)) {
60
+ try { state = JSON.parse(readFileSync(statePath, 'utf8')) || {}; } catch { state = {}; }
61
+ }
62
+ const now = Date.now();
63
+ for (const [k, v] of Object.entries(state)) {
64
+ if (!v?.ts || now - v.ts > 24 * 60 * 60 * 1000) delete state[k];
65
+ }
66
+
67
+ const s = state[sessionId] || { ts: now, reads: 0, bootstrap: [], done: false };
68
+ const bootstrapSet = new Set(Array.isArray(s.bootstrap) ? s.bootstrap : []);
69
+ s.ts = now;
70
+
71
+ if (BOOTSTRAP_TOOLS.has(toolName)) bootstrapSet.add(toolName.replace(/^.*__/, ''));
72
+ if (READ_TOOLS.has(toolName)) s.reads = (s.reads || 0) + 1;
73
+
74
+ if (!s.done) {
75
+ const hasPair = bootstrapSet.has('project_context') && bootstrapSet.has('memory_search');
76
+ if (hasPair || (s.reads || 0) >= 3) s.done = true;
77
+ }
78
+ if (!s.done && READ_TOOLS.has(toolName) && (s.reads || 0) >= 2) s.done = true;
79
+
80
+ const block = !s.done && BLOCKED_TOOLS.has(toolName);
81
+
82
+ try {
83
+ mkdirSync(stateDir, { recursive: true });
84
+ state[sessionId] = { ...s, bootstrap: Array.from(bootstrapSet) };
85
+ writeFileSync(statePath, JSON.stringify(state));
86
+ } catch {}
87
+
88
+ if (block) {
89
+ process.stderr.write(
90
+ `[policy-engine/bootstrap] ${toolName} requires session bootstrap.\n` +
91
+ `Run in parallel first: project_context, memory_search.\n` +
92
+ `(Or do 3+ exploratory reads — Read/Grep/Glob/LS.)\n`
93
+ );
94
+ process.exit(2);
95
+ }
96
+
97
+ process.exit(0);
98
+ }
99
+
100
+ if (hookEvent === 'Stop') {
101
+ const driveStatePath = join(cwd, '.cx', 'drive-state.json');
102
+
103
+ if (existsSync(driveStatePath)) {
104
+ let driveState = {};
105
+ try { driveState = JSON.parse(readFileSync(driveStatePath, 'utf8')); } catch { driveState = {}; }
106
+
107
+ if (driveState.active && !driveState.canStop) {
108
+ const allCriteria = Array.isArray(driveState.criteria) ? driveState.criteria : [];
109
+ const criteriaStatus = driveState.criteriaStatus || {};
110
+ const metCriteria = allCriteria.filter(c => criteriaStatus[c]?.met);
111
+ const unmetCriteria = allCriteria.filter(c => !criteriaStatus[c]?.met);
112
+ const pendingTodos = driveState.pendingTodos || 0;
113
+ const iteration = driveState.iteration || 1;
114
+ const momentum = driveState.momentumScore != null ? Math.round(driveState.momentumScore * 100) : null;
115
+
116
+ const hasPendingWork = unmetCriteria.length > 0 || (allCriteria.length === 0 && pendingTodos > 0);
117
+ if (hasPendingWork) {
118
+ const lines = [`[policy-engine/drive] Drive mode active — iteration ${iteration}. Cannot stop yet.`, ''];
119
+ if (unmetCriteria.length > 0) {
120
+ lines.push(`Unmet criteria (${unmetCriteria.length}/${allCriteria.length}):`);
121
+ for (const c of unmetCriteria) lines.push(` ✗ ${c}`);
122
+ lines.push('');
123
+ }
124
+ if (metCriteria.length > 0) {
125
+ lines.push(`Verified criteria (${metCriteria.length}/${allCriteria.length}):`);
126
+ for (const c of metCriteria) {
127
+ const ev = criteriaStatus[c]?.evidence || 'recorded';
128
+ lines.push(` ✓ ${c} — ${ev.slice(0, 80)}`);
129
+ }
130
+ lines.push('');
131
+ }
132
+ if (allCriteria.length === 0) {
133
+ lines.push(`Remaining: ${pendingTodos} pending todos`, '');
134
+ }
135
+ if (momentum != null) lines.push(`Momentum: ${momentum}% (iteration ${iteration})`);
136
+ lines.push('', `[policy-engine/drive] Set criteriaStatus["<criterion>"] = { met: true, evidence: "..." } to unblock.`);
137
+
138
+ process.stderr.write(lines.join('\n') + '\n');
139
+ process.exit(2);
140
+ }
141
+ }
142
+ }
143
+
144
+ const branch = safeExec('git branch --show-current');
145
+ const isFeatureBranch = branch && branch !== 'main' && branch !== 'dev' && branch !== 'master';
146
+ const filesChangedPath = join(home, '.cx', 'files-changed-count.txt');
147
+ let filesChanged = 0;
148
+ try { filesChanged = parseInt(readFileSync(filesChangedPath, 'utf8').trim() || '0', 10); } catch {}
149
+
150
+ if (isFeatureBranch && filesChanged > 0) {
151
+ const json = safeExec(
152
+ `gh run list --branch=${JSON.stringify(branch)} --limit=1 --json conclusion,databaseId,url`,
153
+ { timeout: 4000 },
154
+ );
155
+ if (json) {
156
+ let run = null;
157
+ try { [run] = JSON.parse(json); } catch {}
158
+ if (run?.conclusion === 'failure') {
159
+ process.stderr.write(
160
+ `[policy-engine/red-ci] CI is red on '${branch}' (advisory).\n` +
161
+ ` Run: ${run.url || `gh run view ${run.databaseId}`}\n` +
162
+ ` Logs: gh run view ${run.databaseId} --log-failed\n`,
163
+ );
164
+ }
165
+ }
166
+ }
167
+
168
+ if (safeExec('command -v bd', { timeout: 1000 })) {
169
+ const bdJson = safeExec('bd list --status in_progress --json', { timeout: 3000 });
170
+ if (bdJson) {
171
+ let issues = [];
172
+ try { issues = JSON.parse(bdJson) || []; } catch {}
173
+ if (Array.isArray(issues) && issues.length > 0) {
174
+ const lines = issues.slice(0, 10).map((iss) => {
175
+ const id = iss.id || iss.ID || '?';
176
+ const title = iss.title || iss.summary || '';
177
+ return ` - ${id}: ${title}`;
178
+ });
179
+ process.stderr.write(
180
+ `[policy-engine/open-bd] In-progress beads issues (${issues.length} total, advisory):\n` +
181
+ lines.join('\n') + '\n',
182
+ );
183
+ }
184
+ }
185
+ }
186
+
187
+ const driveSessionPath = join(home, '.cx', 'drive-session.json');
188
+ if (existsSync(driveSessionPath)) {
189
+ let ds = {};
190
+ try { ds = JSON.parse(readFileSync(driveSessionPath, 'utf8')); } catch {}
191
+ if (ds?.open === true) {
192
+ const id = ds.sessionId || ds.id || '<unknown>';
193
+ const goal = ds.goal || ds.title || '';
194
+ process.stderr.write(
195
+ `[policy-engine/drive-session] Open drive session detected (id ${id}).\n` +
196
+ (goal ? ` Goal: ${goal}\n` : '') +
197
+ ` Close it before assuming session is complete (advisory).\n`,
198
+ );
199
+ }
200
+ }
201
+
202
+ process.exit(0);
203
+ }
204
+
205
+ if (hookEvent === 'UserPromptSubmit') {
206
+ process.exit(0);
207
+ }
208
+
209
+ process.exit(0);
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * post-merge-docs-check.mjs — PostToolUse / Bash (async)
4
+ *
5
+ * When a merge or pull lands on the working tree (detected via `git log -1
6
+ * --format=%P` returning two parents), diff the merge and emit
7
+ * `pr.merged.no-docs` if `src/`, `lib/`, or `bin/` changed without a touching
8
+ * file under `docs/**` or `CHANGELOG.md`. Emits `changelog.missing`
9
+ * specifically when no CHANGELOG entry was added.
10
+ *
11
+ * @p95ms 1500
12
+ * @maxBlockingScope none (async, non-blocking)
13
+ */
14
+
15
+ import { readFileSync } from 'node:fs';
16
+ import { execSync } from 'node:child_process';
17
+ import { logHookFailure } from './_lib/log.mjs';
18
+ import { emitRoleEvent } from '../roles/hook-emit.mjs';
19
+
20
+ let input = {};
21
+ try { input = JSON.parse(readFileSync(0, 'utf8')); }
22
+ catch (err) { logHookFailure({ hook: 'post-merge-docs-check', err, phase: 'parse' }); process.exit(0); }
23
+
24
+ const command = input?.tool_input?.command || '';
25
+ const exitCode = input?.tool_response?.exit_code ?? input?.tool_response?.exitCode;
26
+ const isMergePull = /\bgit\s+(merge|pull|rebase|cherry-pick)\b/.test(command);
27
+ if (!isMergePull) process.exit(0);
28
+ if (exitCode !== 0 && exitCode !== undefined) process.exit(0);
29
+
30
+ const cwd = input?.cwd || process.cwd();
31
+
32
+ try {
33
+ const parents = execSync('git log -1 --format=%P', { cwd, timeout: 2000 }).toString().trim();
34
+ if (parents.split(/\s+/).filter(Boolean).length < 2) process.exit(0);
35
+
36
+ const files = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { cwd, timeout: 2000 })
37
+ .toString().split('\n').filter(Boolean);
38
+
39
+ const codeChanged = files.some((f) => /^(src|lib|bin|app)\//.test(f));
40
+ if (!codeChanged) process.exit(0);
41
+
42
+ const docChanged = files.some((f) => /^docs\//.test(f) || /(^|\/)README\.md$/.test(f));
43
+ const changelogChanged = files.some((f) => /(^|\/)CHANGELOG\.md$/.test(f));
44
+
45
+ if (!changelogChanged) {
46
+ emitRoleEvent({
47
+ type: 'changelog.missing',
48
+ summary: `Merge landed without CHANGELOG update — ${files.length} file(s) changed`,
49
+ hookInput: input,
50
+ context: { files: files.slice(0, 50) },
51
+ });
52
+ }
53
+ if (!docChanged && !changelogChanged) {
54
+ emitRoleEvent({
55
+ type: 'pr.merged.no-docs',
56
+ summary: `Merge landed touching code without docs — ${files.length} file(s) changed`,
57
+ hookInput: input,
58
+ context: { files: files.slice(0, 50) },
59
+ });
60
+ }
61
+ } catch {}
62
+
63
+ process.exit(0);
@@ -2,7 +2,10 @@
2
2
  /**
3
3
  * lib/hooks/pre-compact.mjs — Pre-compact hook — prepares context summary before compaction runs.
4
4
  *
5
- * Runs before context compaction. Writes a structured summary of active workflow, context state, and pending tasks to preserve continuity across the compaction boundary.
5
+ * Runs before context compaction. Writes a structured summary of context state and pending tasks to preserve continuity across the compaction boundary.
6
+ *
7
+ * @p95ms 100
8
+ * @maxBlockingScope PreCompact
6
9
  */
7
10
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
8
11
  import { homedir } from 'os';
@@ -18,7 +21,6 @@ const transcriptPath = input?.transcript_path || '';
18
21
  const filesChanged = [];
19
22
  const decisions = [];
20
23
  const pendingTodos = [];
21
- const workflowTasks = [];
22
24
  const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
23
25
  const efficiencyPath = join(homedir(), '.cx', 'session-efficiency.json');
24
26
  let lastSummary = '';
@@ -85,19 +87,6 @@ if (transcriptPath && existsSync(transcriptPath)) {
85
87
  }
86
88
  }
87
89
 
88
- // Extract active workflow tasks from .cx/workflow.json
89
- const workflowPath = join(cwd, '.cx', 'workflow.json');
90
- if (existsSync(workflowPath)) {
91
- try {
92
- const wf = JSON.parse(readFileSync(workflowPath, 'utf8'));
93
- const tasks = wf?.tasks || wf?.state?.tasks || [];
94
- for (const task of tasks) {
95
- if (task.status !== 'done' && task.status !== 'skipped') {
96
- workflowTasks.push({ key: task.key || task.id, title: task.title || task.description || '' });
97
- }
98
- }
99
- } catch { /* best effort */ }
100
- }
101
90
  } catch { /* best effort */ }
102
91
  }
103
92
 
@@ -154,14 +143,6 @@ if (pendingTodos.length) {
154
143
  contextLines.push('');
155
144
  }
156
145
 
157
- if (workflowTasks.length) {
158
- contextLines.push('## Active workflow tasks');
159
- for (const task of workflowTasks.slice(0, 10)) {
160
- contextLines.push(`- ${task.key}: ${task.title}`);
161
- }
162
- contextLines.push('');
163
- }
164
-
165
146
  if (efficiencySummary) {
166
147
  contextLines.push('## Session efficiency snapshot');
167
148
  contextLines.push(efficiencySummary);
@@ -179,7 +160,6 @@ const contextJson = {
179
160
  filesChanged: filesChanged.slice(0, 20),
180
161
  decisions: decisions.slice(-10),
181
162
  pendingTodos: pendingTodos.slice(0, 20),
182
- workflowTasks: workflowTasks.slice(0, 10),
183
163
  efficiencySummary,
184
164
  warnFlags: warnFlags || 'None',
185
165
  };