@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
@@ -1,16 +1,35 @@
1
+ #!/usr/bin/env node
1
2
  /**
2
- * lib/hooks/comment-lint.mjs — PostToolUse hook: check edited files against comment policy.
3
+ * lib/hooks/comment-lint.mjs — PostToolUse hook: enforce the comment policy at write time.
3
4
  *
4
- * Runs after Write/Edit on scoped paths. Emits warnings for missing headers and
5
- * banned patterns so violations are caught at authoring time, not at CI.
6
- * Non-blocking: exits 0 even on warnings so it never halts a write.
5
+ * Runs after Write/Edit/MultiEdit on scoped paths. Blocks the edit (exit 2)
6
+ * if the file violates the policy missing required header, banned patterns,
7
+ * narrative voice, point-in-time notes, noise sentinels. The agent sees the
8
+ * block reason and is expected to fix the comment before continuing.
9
+ *
10
+ * Bypass: set CONSTRUCT_SKIP_COMMENT_LINT=1 in the environment.
11
+ *
12
+ * Input: reads tool_input.file_path from either the legacy TOOL_INPUT_FILE_PATH
13
+ * env var or modern stdin JSON ({ tool_input: { file_path } }).
14
+ *
15
+ * @p95ms 60
16
+ * @maxBlockingScope PostToolUse
7
17
  */
8
18
 
19
+ import { readFileSync } from 'node:fs';
9
20
  import path from 'node:path';
10
21
  import { fileURLToPath } from 'node:url';
11
22
  import { lintFile, formatResults } from '../comment-lint.mjs';
12
23
 
13
- const filePath = process.env.TOOL_INPUT_FILE_PATH;
24
+ if (process.env.CONSTRUCT_SKIP_COMMENT_LINT === '1') process.exit(0);
25
+
26
+ let filePath = process.env.TOOL_INPUT_FILE_PATH;
27
+ if (!filePath) {
28
+ try {
29
+ const input = JSON.parse(readFileSync(0, 'utf8'));
30
+ filePath = input?.tool_input?.file_path || input?.tool_input?.path;
31
+ } catch {}
32
+ }
14
33
  if (!filePath) process.exit(0);
15
34
 
16
35
  const ext = path.extname(filePath).toLowerCase();
@@ -22,5 +41,8 @@ const result = lintFile(filePath, { rootDir });
22
41
  if (!result.errors.length && !result.warnings.length) process.exit(0);
23
42
 
24
43
  const { output } = formatResults([result]);
25
- process.stdout.write(output);
26
- process.exit(0);
44
+ process.stderr.write(output);
45
+ process.stderr.write(
46
+ '\nComment policy blocked this edit. Remove the violations or set CONSTRUCT_SKIP_COMMENT_LINT=1.\n',
47
+ );
48
+ process.exit(2);
@@ -1,14 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/hooks/config-protection.mjs — Config protection hook prevents edits to protected runtime config files.
3
+ * lib/hooks/config-protection.mjs — protects code-quality config from being weakened.
4
4
  *
5
- * Runs as PreToolUse on Edit/Write. Blocks modifications to ~/.construct/config.env, settings.template.json, and other protected config paths. Exits 2 to block.
5
+ * Runs as PreToolUse on Edit|Write|MultiEdit. Blocks edits to eslint /
6
+ * prettier / tsconfig / biome / stylelint configs. Those rules are a contract
7
+ * with the codebase; weakening them is almost always a bug fix in disguise.
8
+ *
9
+ * Meta-system files (hooks, registry, settings template, install.sh) are
10
+ * audited but NOT blocked. Editing them is normal development work on
11
+ * Construct itself. The previous block-with-CX_ALLOW_META_EDIT pattern was a
12
+ * sticky env-var toggle: friction the first time, zero protection thereafter
13
+ * because it stayed on. Layers that actually work for self-modification
14
+ * accidents: CLAUDE.md guidance, Claude Code's auto-classifier (intent-aware),
15
+ * and the role-event audit log this hook still writes.
16
+ *
17
+ * @p95ms 5
18
+ * @maxBlockingScope PreToolUse
6
19
  */
7
20
  import { readFileSync } from 'fs';
21
+ import { logHookFailure } from './_lib/log.mjs';
8
22
 
9
23
  const filePath = process.env.TOOL_INPUT_FILE_PATH || (() => {
10
24
  try { return JSON.parse(readFileSync(0, 'utf8'))?.tool_input?.file_path || ''; }
11
- catch { return ''; }
25
+ catch (err) { logHookFailure({ hook: 'config-protection', err, phase: 'parse' }); return ''; }
12
26
  })();
13
27
 
14
28
  if (!filePath) process.exit(0);
@@ -24,29 +38,36 @@ const PROTECTED = [
24
38
  /stylelint\.config(\.[a-z]+)?$/i,
25
39
  ];
26
40
 
41
+ const META_FILES = [
42
+ /(?:^|\/)agents\/registry\.json$/,
43
+ /(?:^|\/)install\.sh$/,
44
+ /(?:^|\/)claude\/settings\.template\.json$/,
45
+ /(?:^|\/)lib\/hooks\/[^/]+\.mjs$/,
46
+ ];
47
+
48
+ async function emitEvent(type, category, summary) {
49
+ try {
50
+ const { emitRoleEvent } = await import('../roles/hook-emit.mjs');
51
+ emitRoleEvent({
52
+ type,
53
+ summary,
54
+ hookInput: {},
55
+ context: { filePath, category },
56
+ });
57
+ } catch { /* best effort */ }
58
+ }
59
+
27
60
  const base = filePath.split('/').pop();
28
61
  if (PROTECTED.some(r => r.test(base))) {
29
62
  process.stderr.write(
30
63
  `[config-protection] The code quality rules are protected. Fix the code to meet the existing standards — don't weaken the rules.\nFile: ${filePath}\n`
31
64
  );
65
+ await emitEvent('config.protection.violation', 'code-quality-config', `Blocked edit to code-quality-config: ${filePath}`);
32
66
  process.exit(2);
33
67
  }
34
68
 
35
- // Meta-system protection: block edits to Construct's own critical files unless CX_ALLOW_META_EDIT=1
36
- if (process.env.CX_ALLOW_META_EDIT !== '1') {
37
- const META_FILES = [
38
- /(?:^|\/)agents\/registry\.json$/,
39
- /(?:^|\/)install\.sh$/,
40
- /(?:^|\/)claude\/settings\.template\.json$/,
41
- /(?:^|\/)lib\/hooks\/[^/]+\.mjs$/,
42
- ];
43
-
44
- if (META_FILES.some(r => r.test(filePath))) {
45
- process.stderr.write(
46
- `[config-protection] This file is part of the Construct meta-system. Editing it affects all agent sessions and platforms. Set CX_ALLOW_META_EDIT=1 to proceed if you know what you're doing.\nFile: ${filePath}\n`
47
- );
48
- process.exit(2);
49
- }
69
+ if (META_FILES.some(r => r.test(filePath))) {
70
+ await emitEvent('meta.edit', 'meta-system', `Meta-system edit: ${filePath}`);
50
71
  }
51
72
 
52
73
  process.exit(0);
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/context-watch.mjs — monitors cumulative token usage and injects
4
+ * a compaction recommendation when thresholds are crossed.
5
+ *
6
+ * Runs as UserPromptSubmit. Reads ~/.cx/session-cost.jsonl, takes the most
7
+ * recent turn's cache-read + cache-creation + processed input tokens,
8
+ * resolves the active model's context window, and compares against
9
+ * percentage-of-window thresholds.
10
+ *
11
+ * Window resolution (resolveContextWindow):
12
+ * 1. observedMaxTokens — largest input ever seen this session (authoritative:
13
+ * if the API accepted N tokens, the window is >= N)
14
+ * 2. catalog maxInputTokens — from LiteLLM live data or static fallback
15
+ * 3. 200k Anthropic baseline
16
+ * The ladder lets a 1M-tier session self-promote once a turn exceeds 200k
17
+ * tokens, without requiring the user to declare their tier anywhere.
18
+ *
19
+ * Thresholds are policy ratios, not absolute numbers:
20
+ * - 60% of window: gentle nudge to distill + summarize
21
+ * - 80% of window: stronger push to compact now
22
+ *
23
+ * Only fires once per threshold per session to avoid spam. State is in
24
+ * ~/.cx/context-watch-state.json, reset when the session (by start timestamp
25
+ * in session-efficiency.json) rolls.
26
+ *
27
+ * @p95ms 20
28
+ * @maxBlockingScope UserPromptSubmit
29
+ */
30
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+ import { homedir } from 'node:os';
33
+ import { resolveContextWindow } from '../telemetry/model-pricing-catalog.mjs';
34
+
35
+ const CX_DIR = join(homedir(), '.cx');
36
+ const COST_LOG = join(CX_DIR, 'session-cost.jsonl');
37
+ const EFFICIENCY = join(CX_DIR, 'session-efficiency.json');
38
+ const STATE = join(CX_DIR, 'context-watch-state.json');
39
+
40
+ const WARN_RATIO = 0.60;
41
+ const URGENT_RATIO = 0.80;
42
+
43
+ function readSessionStart() {
44
+ try {
45
+ const e = JSON.parse(readFileSync(EFFICIENCY, 'utf8'));
46
+ return e?.sessionStartedAt || null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function readCurrentContextSize(sessionStart) {
53
+ // Use the MOST RECENT turn's tokens — that approximates current context
54
+ // size (what the model saw on its last request). Summing across turns
55
+ // double-counts cached content since each turn re-reads the same cache.
56
+ try {
57
+ if (!existsSync(COST_LOG)) return { total: 0, model: null };
58
+ const lines = readFileSync(COST_LOG, 'utf8').split('\n').filter(Boolean);
59
+ const cutoff = sessionStart ? new Date(sessionStart).getTime() : 0;
60
+ let latest = null;
61
+ for (const line of lines) {
62
+ try {
63
+ const row = JSON.parse(line);
64
+ const rowTs = new Date(row.ts).getTime();
65
+ if (rowTs < cutoff) continue;
66
+ if (!latest || rowTs > new Date(latest.ts).getTime()) latest = row;
67
+ } catch { /* skip malformed line */ }
68
+ }
69
+ if (!latest) return { total: 0, model: null };
70
+ const total =
71
+ Number(latest.cache_read_input_tokens || 0) +
72
+ Number(latest.cache_creation_input_tokens || 0) +
73
+ Number(latest.input_tokens || 0);
74
+ return { total, model: latest.model || null };
75
+ } catch {
76
+ return { total: 0, model: null };
77
+ }
78
+ }
79
+
80
+ function loadState() {
81
+ try {
82
+ return JSON.parse(readFileSync(STATE, 'utf8'));
83
+ } catch {
84
+ return { sessionStart: null, warnedAt: null, urgentAt: null, observedMaxTokens: 0 };
85
+ }
86
+ }
87
+
88
+ function saveState(state) {
89
+ try {
90
+ mkdirSync(CX_DIR, { recursive: true });
91
+ writeFileSync(STATE, JSON.stringify(state, null, 2));
92
+ } catch { /* best effort */ }
93
+ }
94
+
95
+ const sessionStart = readSessionStart();
96
+ const { total, model } = readCurrentContextSize(sessionStart);
97
+
98
+ let state = loadState();
99
+ if (state.sessionStart !== sessionStart) {
100
+ state = { sessionStart, warnedAt: null, urgentAt: null, observedMaxTokens: 0 };
101
+ }
102
+
103
+ state.observedMaxTokens = Math.max(Number(state.observedMaxTokens) || 0, total);
104
+
105
+ const contextWindow = resolveContextWindow(model, state.observedMaxTokens);
106
+ const WARN_AT = Math.floor(contextWindow * WARN_RATIO);
107
+ const URGENT_AT = Math.floor(contextWindow * URGENT_RATIO);
108
+
109
+ let message = null;
110
+
111
+ if (total >= URGENT_AT && !state.urgentAt) {
112
+ message =
113
+ `Context at ~${Math.round(total / 1000)}k of ~${Math.round(contextWindow / 1000)}k tokens — above the 80% urgent threshold. ` +
114
+ `Stop broad exploration. Save the salient state to .cx/context.md (current task, ` +
115
+ `decisions, open questions, files changed), then run /compact. Continuing without ` +
116
+ `compaction risks degraded responses and higher cost per turn.`;
117
+ state.urgentAt = new Date().toISOString();
118
+ } else if (total >= WARN_AT && !state.warnedAt) {
119
+ message =
120
+ `Context at ~${Math.round(total / 1000)}k of ~${Math.round(contextWindow / 1000)}k tokens — past the 60% nudge threshold. ` +
121
+ `Consider running \`construct distill --query "<focus>"\` before more broad reads, ` +
122
+ `and summarize the active task to .cx/context.md so /compact has something to preserve.`;
123
+ state.warnedAt = new Date().toISOString();
124
+ }
125
+
126
+ saveState(state);
127
+
128
+ if (message) {
129
+ process.stdout.write(JSON.stringify({
130
+ hookSpecificOutput: {
131
+ hookEventName: 'UserPromptSubmit',
132
+ additionalContext: `[context-watch] ${message}`,
133
+ },
134
+ }));
135
+ }
136
+
137
+ process.exit(0);
@@ -3,11 +3,13 @@
3
3
  * lib/hooks/context-window-recovery.mjs — Context window recovery hook — detects near-limit context and suggests compaction.
4
4
  *
5
5
  * Runs as PostToolUse. Reads context usage from the hook input and emits a compaction suggestion when the context window is above 80%. Non-blocking.
6
+ *
7
+ * @p95ms 10
8
+ * @maxBlockingScope none (PostToolUse, non-blocking)
6
9
  */
7
10
  import { readFileSync, writeFileSync, mkdirSync } from 'fs';
8
11
  import { homedir } from 'os';
9
12
  import { join } from 'path';
10
- import { loadWorkflow, summarizeWorkflow } from '../workflow-state.mjs';
11
13
  import { readContextState, writeContextState } from '../context-state.mjs';
12
14
 
13
15
  const CONTEXT_LIMIT_PATTERNS = [
@@ -42,23 +44,6 @@ try { writeFileSync(STATE_PATH, JSON.stringify({ lastTriggeredAt: now })); } cat
42
44
 
43
45
  const cwd = input?.cwd || process.cwd();
44
46
 
45
- const workflowLines = [];
46
- try {
47
- const workflow = loadWorkflow(cwd);
48
- if (workflow) {
49
- const summary = summarizeWorkflow(workflow);
50
- workflowLines.push('## Active Workflow', summary, '');
51
- const pending = (workflow.tasks || []).filter(t => t.status === 'in-progress' || t.status === 'todo');
52
- if (pending.length) {
53
- workflowLines.push('## Pending Tasks');
54
- for (const t of pending.slice(0, 10)) {
55
- workflowLines.push(`- [${t.key}] ${t.title} (${t.status}, owner: ${t.owner})`);
56
- }
57
- workflowLines.push('');
58
- }
59
- }
60
- } catch { /* best effort */ }
61
-
62
47
  let existingContext = '';
63
48
  try {
64
49
  const state = readContextState(cwd) || readContextState(homedir());
@@ -71,7 +56,6 @@ const recoveryContext = [
71
56
  `# Session Context`,
72
57
  `Last saved: ${date} (context-window-recovery)`,
73
58
  '',
74
- ...workflowLines,
75
59
  existingContext ? `## Prior Context\n${existingContext}` : '',
76
60
  ].join('\n') + '\n';
77
61
 
@@ -83,7 +67,7 @@ try { writeContextState(homedir(), { source: 'context-window-recovery', recovery
83
67
  process.stdout.write([
84
68
  ``,
85
69
  `⚠ Context window limit hit. Session state saved to .cx/context.md.`,
86
- `Resume by reading .cx/context.md and .cx/workflow.json, then continue from where you left off.`,
70
+ `Resume by reading .cx/context.md, AGENTS.md, and plan.md, then continue from where you left off.`,
87
71
  ``,
88
72
  ].join('\n'));
89
73
 
@@ -5,6 +5,9 @@
5
5
  * After editing a dependency manifest, runs the appropriate audit tool and
6
6
  * appends critical/high vulnerabilities to ~/.cx/warn-flags.txt.
7
7
  * Silent when nothing is wrong. Runs async so it never blocks editing.
8
+ *
9
+ * @p95ms 5000
10
+ * @maxBlockingScope none (PostToolUse, async, non-blocking)
8
11
  */
9
12
  import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
10
13
  import { execSync } from 'child_process';
@@ -27,6 +30,19 @@ const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
27
30
  function appendWarn(msg) {
28
31
  try { appendFileSync(warnFlagsPath, msg + '\n'); } catch { /* best effort */ }
29
32
  process.stderr.write(`[dep-audit] ${msg}\n`);
33
+ emitVulnerability(msg);
34
+ }
35
+
36
+ async function emitVulnerability(msg) {
37
+ try {
38
+ const { emitRoleEvent } = await import('../roles/hook-emit.mjs');
39
+ emitRoleEvent({
40
+ type: 'dep.cve',
41
+ summary: msg,
42
+ hookInput: { cwd: projectDir },
43
+ context: { manifest: filePath },
44
+ });
45
+ } catch { /* best effort */ }
30
46
  }
31
47
 
32
48
  // ── npm / yarn / pnpm ─────────────────────────────────────────────────────────
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/doc-coupling-check.mjs — PostToolUse hook: nudge when code edits aren't paired with doc updates.
4
+ *
5
+ * Maintains a session-scoped counter of code-file edits (lib/, bin/, src/, app/)
6
+ * since the last documentation touch (CHANGELOG.md, docs/**, .cx/context.*).
7
+ * Emits an advisory to stderr at thresholds 3, 5, 10 so the agent observation
8
+ * surfaces the coupling reminder during the run rather than at commit time
9
+ * (where the hard gate in .beads/hooks/pre-commit will refuse to commit).
10
+ *
11
+ * Non-blocking by design — this is the soft predecessor to the commit gate.
12
+ *
13
+ * @p95ms 15
14
+ * @maxBlockingScope none (PostToolUse, non-blocking)
15
+ */
16
+
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join, dirname } from 'node:path';
20
+
21
+ const STATE_PATH = join(homedir(), '.cx', 'doc-coupling.json');
22
+ const CODE_RE = /^(lib|bin|src|app)\/.*\.(?:m?js|jsx?|tsx?|cjs)$/;
23
+ const DOC_RE = /^(?:CHANGELOG\.md|docs\/.+\.md|\.cx\/context\.(?:md|json))$/;
24
+ const THRESHOLDS = [3, 5, 10];
25
+
26
+ let filePath = process.env.TOOL_INPUT_FILE_PATH;
27
+ let cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
28
+ if (!filePath) {
29
+ try {
30
+ const input = JSON.parse(readFileSync(0, 'utf8'));
31
+ filePath = input?.tool_input?.file_path || input?.tool_input?.path;
32
+ if (input?.cwd) cwd = input.cwd;
33
+ } catch {}
34
+ }
35
+ if (!filePath) process.exit(0);
36
+
37
+ const rel = filePath.startsWith(cwd) ? filePath.slice(cwd.length).replace(/^\/+/, '') : filePath;
38
+
39
+ function readState() {
40
+ try { return JSON.parse(readFileSync(STATE_PATH, 'utf8')); }
41
+ catch { return { codeEdits: 0, notified: [] }; }
42
+ }
43
+
44
+ function writeState(state) {
45
+ try {
46
+ mkdirSync(dirname(STATE_PATH), { recursive: true });
47
+ writeFileSync(STATE_PATH, JSON.stringify(state));
48
+ } catch {}
49
+ }
50
+
51
+ const state = readState();
52
+
53
+ if (DOC_RE.test(rel)) {
54
+ writeState({ codeEdits: 0, notified: [] });
55
+ process.exit(0);
56
+ }
57
+
58
+ if (!CODE_RE.test(rel)) process.exit(0);
59
+
60
+ state.codeEdits = (state.codeEdits || 0) + 1;
61
+ const notified = new Set(state.notified || []);
62
+
63
+ let advisory = null;
64
+ for (const t of THRESHOLDS) {
65
+ if (state.codeEdits >= t && !notified.has(t)) {
66
+ notified.add(t);
67
+ advisory = t;
68
+ }
69
+ }
70
+
71
+ state.notified = [...notified];
72
+ writeState(state);
73
+
74
+ if (advisory != null) {
75
+ process.stderr.write(
76
+ `[doc-coupling] ${state.codeEdits} code file edit(s) this session under lib/|bin/|src/|app/; CHANGELOG.md / docs / .cx/context still not touched. Update docs before the next commit (the pre-commit gate will block otherwise; bypass: CONSTRUCT_SKIP_DOCS=1).\n`,
77
+ );
78
+ }
79
+
80
+ process.exit(0);
@@ -3,6 +3,9 @@
3
3
  * lib/hooks/edit-accumulator.mjs — Edit accumulator hook — batches and summarizes recent file edits for context.
4
4
  *
5
5
  * Runs as PostToolUse after Edit/Write. Maintains a rolling log of recent edits in ~/.cx/edit-log.json for use by other hooks and efficiency reporting.
6
+ *
7
+ * @p95ms 10
8
+ * @maxBlockingScope none (PostToolUse, non-blocking)
6
9
  */
7
10
  import { appendFileSync, readFileSync, writeFileSync, existsSync } from 'fs';
8
11
  import { homedir } from 'os';
@@ -3,6 +3,9 @@
3
3
  * lib/hooks/edit-error-recovery.mjs — Edit error recovery hook — recovers from failed edit attempts and suggests fixes.
4
4
  *
5
5
  * Runs as PostToolUse when Edit tool returns an error. Reads the error and provides a targeted fix suggestion based on the error type (e.g., old_string not found).
6
+ *
7
+ * @p95ms 10
8
+ * @maxBlockingScope none (PostToolUse, non-blocking)
6
9
  */
7
10
  import { readFileSync } from 'fs';
8
11
 
@@ -3,14 +3,15 @@
3
3
  * lib/hooks/edit-guard.mjs — Edit guard hook — validates old_string exists in target file before allowing edits.
4
4
  *
5
5
  * Runs as PreToolUse on Edit. Re-reads the target file and checks that old_string is present to prevent no-op or mismatched edits. Exits 2 (block) when not found.
6
+ *
7
+ * @p95ms 20
8
+ * @maxBlockingScope PreToolUse
6
9
  */
7
- // PreToolUse(Edit|MultiEdit|Write) — hash-anchored edit verification
8
- // Blocks Edit/MultiEdit when old_string is not found verbatim in the file.
9
- // Warns (non-blocking) on hash staleness when file changed since last Read.
10
10
  import { readFileSync, existsSync } from 'fs';
11
11
  import { createHash } from 'crypto';
12
12
  import { join, resolve } from 'path';
13
13
  import { homedir } from 'os';
14
+ import { logHookFailure } from './_lib/log.mjs';
14
15
 
15
16
  const HASH_STORE = join(homedir(), '.cx', 'file-hashes.json');
16
17
 
@@ -26,7 +27,8 @@ function fileHash(content) {
26
27
  }
27
28
 
28
29
  let input = {};
29
- try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
30
+ try { input = JSON.parse(readFileSync(0, 'utf8')); }
31
+ catch (err) { logHookFailure({ hook: 'edit-guard', err, phase: 'parse' }); process.exit(0); }
30
32
 
31
33
  const toolName = input?.tool_name || '';
32
34
  const ti = input?.tool_input || {};
@@ -34,6 +36,45 @@ const cwd = input?.cwd || process.cwd();
34
36
 
35
37
  function absOf(p) { return p.startsWith('/') ? p : resolve(cwd, p); }
36
38
 
39
+ // Role-fence check: if the most recently dispatched persona is one we manage
40
+ // (cx-sre, cx-qa, cx-security, cx-docs-keeper) and the dispatch is fresh,
41
+ // emit an advisory stderr warning when the edit target is outside the fence.
42
+ // Hard-block only when the fence returns "outside-fence" (default-deny case);
43
+ // "needs-approval" routes to existing commit-approval flow.
44
+
45
+ if ((toolName === 'Edit' || toolName === 'Write' || toolName === 'MultiEdit') && process.env.CONSTRUCT_ROLES !== 'off') {
46
+ try {
47
+ const lastAgentPath = join(homedir(), '.cx', 'last-agent.json');
48
+ if (existsSync(lastAgentPath)) {
49
+ const last = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
50
+ const lastTs = last?.ts ? Date.parse(last.ts) : 0;
51
+ const fresh = lastTs && (Date.now() - lastTs) < 10 * 60 * 1000;
52
+ const id = String(last?.agent || '').replace(/^cx-/, '');
53
+ const ONBOARDED = new Set(['sre', 'qa', 'security', 'docs-keeper']);
54
+ const targetPath = ti.file_path || ti.path || '';
55
+ if (fresh && ONBOARDED.has(id) && targetPath) {
56
+ const { checkAction } = await import('../roles/fence.mjs');
57
+ const rel = targetPath.startsWith(cwd + '/') ? targetPath.slice(cwd.length + 1) : targetPath;
58
+ const verdict = checkAction({ personaId: id, action: 'edit', target: rel });
59
+ if (!verdict.allowed && verdict.reason === 'outside-fence') {
60
+ process.stderr.write(
61
+ `[fence] cx-${id} cannot edit ${rel} — outside declared fence.\n` +
62
+ `Allowed paths: see agents/role-manifests.json → ${id} → fence.allowedPaths.\n`
63
+ );
64
+ process.exit(2);
65
+ }
66
+ if (!verdict.allowed && verdict.approval) {
67
+ process.stderr.write(
68
+ `[fence] cx-${id} editing ${rel} requires user approval (outside fence, approval-required class).\n`
69
+ );
70
+ const { recordApprovalRequest } = await import('../roles/approval-surface.mjs');
71
+ await recordApprovalRequest({ personaId: id, action: 'edit', target: rel, reason: verdict.reason || 'needs-approval' });
72
+ }
73
+ }
74
+ }
75
+ } catch { /* best effort — fence check never blocks for internal errors */ }
76
+ }
77
+
37
78
  if (toolName === 'Edit') {
38
79
  const { file_path, old_string } = ti;
39
80
  if (!file_path || !old_string) process.exit(0);
@@ -5,6 +5,9 @@
5
5
  * Reads .env.example from the project root, compares against .env and
6
6
  * process.env, and writes missing required vars to stdout so Claude sees
7
7
  * them at session open. Silent when everything is in order.
8
+ *
9
+ * @p95ms 20
10
+ * @maxBlockingScope SessionStart
8
11
  */
9
12
  import { readFileSync, existsSync, realpathSync } from 'fs';
10
13
  import { fileURLToPath } from 'url';
@@ -3,8 +3,15 @@
3
3
  * lib/hooks/guard-bash.mjs — Guard bash hook — blocks dangerous shell commands from running unreviewed.
4
4
  *
5
5
  * Runs as PreToolUse on Bash. Scans the command against a blocklist of destructive patterns (rm -rf, force push to main, etc.) and exits 2 to block matches.
6
+ *
7
+ * @p95ms 5
8
+ * @maxBlockingScope PreToolUse
6
9
  */
7
10
  import { createInterface } from 'readline';
11
+ import { readFileSync, existsSync } from 'fs';
12
+ import { homedir } from 'os';
13
+ import { join } from 'path';
14
+ import { logHookFailure } from './_lib/log.mjs';
8
15
 
9
16
  const BLOCK_PATTERNS = [
10
17
  {
@@ -56,7 +63,8 @@ async function getCommand() {
56
63
  try {
57
64
  const parsed = JSON.parse(data);
58
65
  resolve(parsed?.tool_input?.command ?? parsed?.command ?? '');
59
- } catch {
66
+ } catch (err) {
67
+ logHookFailure({ hook: 'guard-bash', err, phase: 'parse' });
60
68
  resolve('');
61
69
  }
62
70
  });
@@ -80,4 +88,53 @@ for (const { pattern, reason } of WARN_PATTERNS) {
80
88
  }
81
89
  }
82
90
 
91
+ // Role-fence check: if the most recently dispatched persona is one we manage
92
+ // and the dispatch is fresh, evaluate the command against the persona's
93
+ // fence. git commit/push are always commit/push actions; bd ... maps to
94
+ // allowedCommands prefix-match; everything else is the bash action.
95
+
96
+ if (process.env.CONSTRUCT_ROLES !== 'off') {
97
+ try {
98
+ const lastAgentPath = join(homedir(), '.cx', 'last-agent.json');
99
+ if (existsSync(lastAgentPath)) {
100
+ const last = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
101
+ const lastTs = last?.ts ? Date.parse(last.ts) : 0;
102
+ const fresh = lastTs && (Date.now() - lastTs) < 10 * 60 * 1000;
103
+ const id = String(last?.agent || '').replace(/^cx-/, '');
104
+ if (fresh && id) {
105
+ const { isOnboarded } = await import('../roles/manifest.mjs');
106
+ if (isOnboarded(id)) {
107
+ const { checkAction } = await import('../roles/fence.mjs');
108
+ const trimmed = command.trim();
109
+ let verdict;
110
+ if (/^git\s+commit\b/.test(trimmed)) {
111
+ verdict = checkAction({ personaId: id, action: 'commit', target: '' });
112
+ } else if (/^git\s+push\b/.test(trimmed)) {
113
+ verdict = checkAction({ personaId: id, action: 'push', target: '' });
114
+ } else {
115
+ verdict = checkAction({ personaId: id, action: 'bash', target: trimmed });
116
+ }
117
+ if (!verdict.allowed && verdict.reason === 'outside-fence') {
118
+ process.stderr.write(
119
+ `[fence] cx-${id} cannot run this command — outside declared fence.\n` +
120
+ `Command: ${trimmed.slice(0, 200)}\n` +
121
+ `Allowed commands prefix list: see agents/role-manifests.json → ${id} → fence.allowedCommands.\n`
122
+ );
123
+ process.exit(2);
124
+ }
125
+ if (!verdict.allowed && verdict.approval) {
126
+ process.stderr.write(
127
+ `[fence] cx-${id} running this requires user approval (${verdict.reason}).\n` +
128
+ `Command: ${trimmed.slice(0, 200)}\n`
129
+ );
130
+ const { recordApprovalRequest } = await import('../roles/approval-surface.mjs');
131
+ const actionLabel = /^git\s+commit\b/.test(trimmed) ? 'commit' : /^git\s+push\b/.test(trimmed) ? 'push' : 'bash';
132
+ await recordApprovalRequest({ personaId: id, action: actionLabel, target: trimmed, reason: verdict.reason || 'needs-approval' });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ } catch { /* best effort */ }
138
+ }
139
+
83
140
  process.exit(0);