@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
@@ -2,28 +2,163 @@
2
2
  /**
3
3
  * pre-push-gate.mjs — PreToolUse / Bash
4
4
  *
5
- * Intercepts `git push` commands. Detects available test and build scripts
6
- * in the project, runs them in parallel, and blocks the push if either fails.
5
+ * Intercepts three command shapes:
6
+ * git push refuses claude/* branches, refuses on red remote CI,
7
+ * runs project test/build/evals/docs in parallel
8
+ * gh pr create lints the --body / --body-file against the PR
9
+ * template policy before the PR is opened
10
+ * gh pr edit same body lint when --body / --body-file is changed
11
+ *
7
12
  * Silent and instant when there's nothing to run.
13
+ *
14
+ * Bypasses (env vars):
15
+ * CONSTRUCT_SKIP_PREPUSH=1 skip the entire push gate
16
+ * CONSTRUCT_ALLOW_CLAUDE_PUSH=1 allow pushing a claude/* branch
17
+ * CONSTRUCT_SKIP_PR_LINT=1 skip the gh pr body lint
18
+ *
19
+ * @p95ms 30000
20
+ * @maxBlockingScope PreToolUse
8
21
  */
9
- import { readFileSync, existsSync } from 'fs';
10
- import { execSync, spawnSync } from 'child_process';
11
- import { dirname, join } from 'path';
22
+ import { readFileSync, existsSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
23
+ import { execSync, spawn, spawnSync } from 'child_process';
24
+ import { dirname, join, resolve } from 'path';
25
+ import { tmpdir } from 'os';
26
+ import { logHookFailure } from './_lib/log.mjs';
27
+ import { emitRoleEvent } from '../roles/hook-emit.mjs';
12
28
 
13
29
  let input = {};
14
- try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { /* no stdin */ }
30
+ try { input = JSON.parse(readFileSync(0, 'utf8')); }
31
+ catch (err) { logHookFailure({ hook: 'pre-push-gate', err, phase: 'parse' }); }
15
32
 
16
- // Must always echo stdin for PreToolUse chaining
17
33
  const echo = () => { process.stdout.write(JSON.stringify(input) + '\n'); process.exit(0); };
18
34
 
19
35
  const command = input?.tool_input?.command || input?.command || '';
36
+ const isGitPush = /\bgit\s+push\b/.test(command);
37
+ const isGhPrCreate = /\bgh\s+pr\s+create\b/.test(command);
38
+ const isGhPrEdit = /\bgh\s+pr\s+edit\b/.test(command);
20
39
 
21
- // Only act on git push
22
- if (!/\bgit\s+push\b/.test(command)) { echo(); }
40
+ if (!isGitPush && !isGhPrCreate && !isGhPrEdit) { echo(); }
23
41
 
24
- // Find nearest package.json / Cargo.toml / pyproject.toml from cwd
25
42
  const cwd = input?.cwd || process.cwd();
26
43
 
44
+ if ((isGhPrCreate || isGhPrEdit) && process.env.CONSTRUCT_SKIP_PR_LINT !== '1') {
45
+ const { body, bodyFile } = extractGhPrBody(command);
46
+ const hasArgs = body != null || bodyFile != null;
47
+
48
+ if (!hasArgs) {
49
+ process.stderr.write(
50
+ `[pre-push-gate] ${isGhPrCreate ? 'gh pr create' : 'gh pr edit'} without --body/--body-file/-F — cannot lint PR body locally.\n` +
51
+ ` CI will still enforce the template policy. To lint manually:\n` +
52
+ ` construct lint:templates --body-file=<path>\n`,
53
+ );
54
+ } else {
55
+ const scriptPath = findRepoFile(cwd, 'scripts/lint-commits-pr.mjs');
56
+ if (!scriptPath) {
57
+ process.stderr.write(`[pre-push-gate] could not locate scripts/lint-commits-pr.mjs from ${cwd}; skipping PR body lint\n`);
58
+ } else {
59
+ const env = { ...process.env };
60
+ let tmpFile = null;
61
+ try {
62
+ if (bodyFile) {
63
+ env.PR_BODY_FILE = resolve(cwd, bodyFile);
64
+ } else if (body != null) {
65
+ const dir = mkdtempSync(join(tmpdir(), 'construct-pr-lint-'));
66
+ tmpFile = join(dir, 'body.md');
67
+ writeFileSync(tmpFile, body);
68
+ env.PR_BODY_FILE = tmpFile;
69
+ }
70
+ const result = spawnSync(process.execPath, [scriptPath], {
71
+ cwd, env, stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000,
72
+ });
73
+ const out = (result.stdout?.toString() || '') + (result.stderr?.toString() || '');
74
+ if (result.status !== 0) {
75
+ process.stderr.write(`[pre-push-gate] PR body fails template policy:\n${out}\n`);
76
+ process.stderr.write(`Override: CONSTRUCT_SKIP_PR_LINT=1 <command>\n`);
77
+ emitRoleEvent({
78
+ type: 'push_gate.fail',
79
+ summary: 'PR body fails template policy',
80
+ hookInput: input,
81
+ context: { command: isGhPrCreate ? 'gh pr create' : 'gh pr edit', detail: out.slice(0, 800) },
82
+ });
83
+ if (tmpFile) try { rmSync(dirname(tmpFile), { recursive: true, force: true }); } catch {}
84
+ process.exit(2);
85
+ }
86
+ } finally {
87
+ if (tmpFile) try { rmSync(dirname(tmpFile), { recursive: true, force: true }); } catch {}
88
+ }
89
+ }
90
+ }
91
+ echo();
92
+ }
93
+
94
+ if (process.env.CONSTRUCT_SKIP_PREPUSH === '1') { echo(); }
95
+
96
+ function extractGhPrBody(cmd) {
97
+ let m = cmd.match(/--body-file[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
98
+ if (m) return { bodyFile: m[2] };
99
+ m = cmd.match(/(?:^|\s)-F[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
100
+ if (m) return { bodyFile: m[2] };
101
+ m = cmd.match(/--body[\s=]+"((?:\\.|[^"\\])*)"/s);
102
+ if (m) return { body: m[1].replace(/\\(["\\])/g, '$1') };
103
+ m = cmd.match(/--body[\s=]+'([^']*)'/s);
104
+ if (m) return { body: m[1] };
105
+ return {};
106
+ }
107
+
108
+ function findRepoFile(startDir, relPath) {
109
+ let dir = startDir;
110
+ for (let i = 0; i < 10; i++) {
111
+ const candidate = join(dir, relPath);
112
+ if (existsSync(candidate)) return candidate;
113
+ const parent = dirname(dir);
114
+ if (parent === dir) break;
115
+ dir = parent;
116
+ }
117
+ return null;
118
+ }
119
+
120
+ if (/\bclaude\//.test(command) && process.env.CONSTRUCT_ALLOW_CLAUDE_PUSH !== '1') {
121
+ process.stderr.write(
122
+ `[pre-push-gate] Refusing to push a claude/* branch to remote (user policy).\n` +
123
+ ` Use a non-agent branch name, or override with CONSTRUCT_ALLOW_CLAUDE_PUSH=1 git push ...\n`,
124
+ );
125
+ emitRoleEvent({
126
+ type: 'push_gate.fail',
127
+ summary: 'claude/* branch push refused',
128
+ hookInput: input,
129
+ context: { command },
130
+ });
131
+ process.exit(2);
132
+ }
133
+
134
+ try {
135
+ const branch = execSync('git branch --show-current', { cwd, timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] })
136
+ .toString().trim();
137
+ if (branch && branch !== 'main' && branch !== 'dev' && branch !== 'master') {
138
+ const json = execSync(
139
+ `gh run list --branch=${JSON.stringify(branch)} --limit=1 --json conclusion,databaseId,url`,
140
+ { cwd, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] },
141
+ ).toString().trim();
142
+ if (json) {
143
+ const [run] = JSON.parse(json);
144
+ if (run?.conclusion === 'failure') {
145
+ process.stderr.write(
146
+ `[pre-push-gate] Last CI run on '${branch}' FAILED — fix it before pushing more.\n` +
147
+ ` Run: ${run.url || `gh run view ${run.databaseId}`}\n` +
148
+ ` Override: CONSTRUCT_SKIP_PREPUSH=1 git push ...\n`,
149
+ );
150
+ emitRoleEvent({
151
+ type: 'push_gate.fail',
152
+ summary: 'remote CI red — push blocked',
153
+ hookInput: input,
154
+ context: { branch, runId: run.databaseId, runUrl: run.url },
155
+ });
156
+ process.exit(2);
157
+ }
158
+ }
159
+ }
160
+ } catch {}
161
+
27
162
  function findUp(filename, from) {
28
163
  let dir = from;
29
164
  for (let i = 0; i < 8; i++) {
@@ -52,8 +187,14 @@ function runNpmGate(projectDir) {
52
187
  : 'npm';
53
188
 
54
189
  const jobs = [];
55
- if (hasTest || hasCi) jobs.push({ label: 'tests', cmd: runner, args: ['run', testScript], timeout: 90_000 });
56
- if (hasBuild) jobs.push({ label: 'build', cmd: runner, args: ['run', 'build'], timeout: 120_000 });
190
+ if (hasTest || hasCi) jobs.push({ label: 'tests', cmd: runner, args: ['run', testScript], timeout: 90_000 });
191
+ if (hasBuild) jobs.push({ label: 'build', cmd: runner, args: ['run', 'build'], timeout: 120_000 });
192
+ jobs.push({ label: 'audit', cmd: 'npm', args: ['audit', '--omit=dev', '--audit-level=high'], timeout: 30_000 });
193
+
194
+ if (existsSync(join(projectDir, 'bin/construct'))) {
195
+ jobs.push({ label: 'evals', cmd: 'node', args: ['bin/construct', 'evals', 'retrieval'], timeout: 60_000 });
196
+ jobs.push({ label: 'docs', cmd: 'node', args: ['bin/construct', 'docs:verify'], timeout: 15_000 });
197
+ }
57
198
 
58
199
  return { projectDir, jobs };
59
200
  }
@@ -88,42 +229,64 @@ else if (pyproj) gate = runPythonGate(pyproj.dir);
88
229
  // Nothing to check
89
230
  if (!gate || gate.jobs.length === 0) { echo(); }
90
231
 
91
- // Run jobs in parallel via Promise.all simulation (spawnSync is synchronous run sequentially)
92
- // For true parallel, we use spawnSync with a short timeout and report errors
93
- const failures = [];
232
+ // Jobs run concurrently. Wall-clock = max(test, build) instead of sum, so the
233
+ // gate fits inside the harness's 180s timeout even when both jobs near their
234
+ // individual limits.
235
+ function runJob(job) {
236
+ return new Promise((resolve) => {
237
+ let stdoutBuf = '';
238
+ let stderrBuf = '';
239
+ const child = spawn(job.cmd, job.args, {
240
+ cwd: gate.projectDir,
241
+ stdio: ['ignore', 'pipe', 'pipe'],
242
+ env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
243
+ });
244
+
245
+ const timer = setTimeout(() => {
246
+ try { child.kill('SIGKILL'); } catch { /* already gone */ }
247
+ }, job.timeout);
94
248
 
95
- for (const job of gate.jobs) {
96
- const result = spawnSync(job.cmd, job.args, {
97
- cwd: gate.projectDir,
98
- encoding: 'utf8',
99
- timeout: job.timeout,
100
- stdio: 'pipe',
101
- env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
249
+ child.stdout?.on('data', (chunk) => { stdoutBuf += chunk.toString(); });
250
+ child.stderr?.on('data', (chunk) => { stderrBuf += chunk.toString(); });
251
+ child.on('error', (err) => {
252
+ clearTimeout(timer);
253
+ resolve({ label: job.label, status: -1, stdout: stdoutBuf, stderr: stderrBuf || err.message, error: err });
254
+ });
255
+ child.on('close', (code) => {
256
+ clearTimeout(timer);
257
+ resolve({ label: job.label, status: code ?? -1, stdout: stdoutBuf, stderr: stderrBuf });
258
+ });
102
259
  });
260
+ }
103
261
 
104
- if (result.status !== 0 || result.error) {
105
- const stderr = (result.stderr || '').trim();
106
- const stdout = (result.stdout || '').trim();
107
- const detail = stderr || stdout;
108
- // Extract first meaningful error line (skip blank, generic lines)
109
- const firstError = detail.split('\n')
110
- .map(l => l.trim())
111
- .filter(l => l && !/^>/.test(l) && !/^\s*at /.test(l))
112
- .slice(0, 3)
113
- .join(' · ');
114
-
115
- failures.push({ label: job.label, detail: firstError || `exited ${result.status}` });
116
- }
262
+ const results = await Promise.all(gate.jobs.map(runJob));
263
+ const failures = [];
264
+ for (const result of results) {
265
+ if (result.status === 0) continue;
266
+ const detail = (result.stderr || result.stdout || '').trim();
267
+ const firstError = detail.split('\n')
268
+ .map((l) => l.trim())
269
+ .filter((l) => l && !/^>/.test(l) && !/^\s*at /.test(l))
270
+ .slice(0, 3)
271
+ .join(' · ');
272
+ failures.push({ label: result.label, detail: firstError || `exited ${result.status}` });
117
273
  }
118
274
 
119
275
  if (failures.length === 0) { echo(); }
120
276
 
121
- // Block the push
122
277
  const summary = failures
123
- .map(f => `${f.label} failed — ${f.detail}`)
278
+ .map((f) => `${f.label} failed — ${f.detail}`)
124
279
  .join('\n');
125
280
 
126
281
  process.stderr.write(
127
282
  `[pre-push-gate] Push blocked — fix these before pushing:\n${summary}\n\nRun the failing checks locally, then push again.\n`
128
283
  );
284
+
285
+ emitRoleEvent({
286
+ type: 'push_gate.fail',
287
+ summary,
288
+ hookInput: input,
289
+ context: { failures },
290
+ });
291
+
129
292
  process.exit(2);
@@ -0,0 +1,284 @@
1
+ /**
2
+ * lib/hooks/proactive-activation.mjs — Event-driven specialist activation.
3
+ *
4
+ * Instead of waiting for user input, specialists can activate proactively
5
+ * based on:
6
+ * - File changes (security on auth code, docs-keeper on README changes)
7
+ * - CI failures (debugger on test fails, qa on coverage drops)
8
+ * - Schedule (docs-keeper on stale READMEs, sre on SLO reviews)
9
+ * - Intake signals (automatic classification → assignment)
10
+ *
11
+ * Each specialist defines triggers in agents/role-manifests.json:
12
+ * {
13
+ * "events": ["test.fail", "secrets.detected"],
14
+ * "severityImmediate": ["secrets.detected"],
15
+ * "fence": { ... },
16
+ * ...
17
+ * }
18
+ *
19
+ * The gateway routes events to owning specialists with rate limiting.
20
+ *
21
+ * @p95ms 5
22
+ * @maxBlockingScope none
23
+ */
24
+
25
+ import { EVENT_OWNERSHIP } from '../orchestration-policy.mjs';
26
+ import { loadProjectConfig } from '../config/project-config.mjs';
27
+
28
+ // Rate limiting: max activations per specialist per hour
29
+ const DEFAULT_RATE_LIMIT = 5;
30
+ const RATE_WINDOW_MS = 60 * 60 * 1000; // 1 hour
31
+
32
+ // Cooldown: minimum time between activations for same specialist
33
+ const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
34
+
35
+ // Activation state tracking
36
+ const activationState = new Map();
37
+
38
+ function getActivationState(specialist) {
39
+ if (!activationState.has(specialist)) {
40
+ activationState.set(specialist, {
41
+ activations: [],
42
+ lastActivation: null,
43
+ totalActivations: 0,
44
+ });
45
+ }
46
+ return activationState.get(specialist);
47
+ }
48
+
49
+ function pruneOldActivations(state, windowMs) {
50
+ const now = Date.now();
51
+ state.activations = state.activations.filter(ts => now - ts < windowMs);
52
+ }
53
+
54
+ /**
55
+ * Check if a specialist can be activated (rate limit + cooldown).
56
+ */
57
+ export function canActivate(specialist, options = {}) {
58
+ const {
59
+ rateLimit = DEFAULT_RATE_LIMIT,
60
+ cooldownMs = DEFAULT_COOLDOWN_MS,
61
+ windowMs = RATE_WINDOW_MS,
62
+ } = options;
63
+
64
+ const state = getActivationState(specialist);
65
+ const now = Date.now();
66
+
67
+ // Check cooldown
68
+ if (state.lastActivation && now - state.lastActivation < cooldownMs) {
69
+ return {
70
+ allowed: false,
71
+ reason: 'cooldown',
72
+ retryAfter: cooldownMs - (now - state.lastActivation),
73
+ };
74
+ }
75
+
76
+ // Check rate limit
77
+ pruneOldActivations(state, windowMs);
78
+ if (state.activations.length >= rateLimit) {
79
+ return {
80
+ allowed: false,
81
+ reason: 'rate-limit',
82
+ retryAfter: windowMs - (now - state.activations[0]),
83
+ };
84
+ }
85
+
86
+ return { allowed: true };
87
+ }
88
+
89
+ /**
90
+ * Record an activation for rate limiting.
91
+ */
92
+ export function recordActivation(specialist) {
93
+ const state = getActivationState(specialist);
94
+ state.activations.push(Date.now());
95
+ state.lastActivation = Date.now();
96
+ state.totalActivations++;
97
+ }
98
+
99
+ /**
100
+ * Get activation stats for a specialist.
101
+ */
102
+ export function getActivationStats(specialist) {
103
+ const state = getActivationState(specialist);
104
+ pruneOldActivations(state, RATE_WINDOW_MS);
105
+
106
+ return {
107
+ recentActivations: state.activations.length,
108
+ lastActivation: state.lastActivation,
109
+ totalActivations: state.totalActivations,
110
+ rateLimit: DEFAULT_RATE_LIMIT,
111
+ rateWindowMs: RATE_WINDOW_MS,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Route an event to the owning specialist.
117
+ */
118
+ export function routeEvent(eventType, eventData = {}) {
119
+ const owner = EVENT_OWNERSHIP[eventType];
120
+
121
+ if (!owner) {
122
+ return {
123
+ routed: false,
124
+ reason: 'no-owner',
125
+ eventType,
126
+ message: `No specialist owns event type: ${eventType}`,
127
+ };
128
+ }
129
+
130
+ // Check if specialist can activate
131
+ const canActivateResult = canActivate(owner);
132
+
133
+ if (!canActivateResult.allowed) {
134
+ return {
135
+ routed: false,
136
+ reason: canActivateResult.reason,
137
+ eventType,
138
+ owner,
139
+ retryAfter: canActivateResult.retryAfter,
140
+ queued: true,
141
+ };
142
+ }
143
+
144
+ // Record activation
145
+ recordActivation(owner);
146
+
147
+ return {
148
+ routed: true,
149
+ owner,
150
+ eventType,
151
+ eventData,
152
+ activationNumber: getActivationState(owner).totalActivations,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * File change watcher callback.
158
+ * Triggers specialists based on file paths and content patterns.
159
+ */
160
+ export function onFileChange(filePath, changeType, content = '') {
161
+ const events = [];
162
+
163
+ // Security: auth-related files
164
+ if (filePath.includes('auth') || filePath.includes('security') || filePath.includes('permission')) {
165
+ events.push({ type: 'file.change.auth', data: { filePath, changeType } });
166
+ }
167
+
168
+ // Docs-keeper: README, docs changes
169
+ if (filePath.includes('README') || filePath.startsWith('docs/')) {
170
+ events.push({ type: 'file.change.docs', data: { filePath, changeType } });
171
+ }
172
+
173
+ // SRE: infra, config, deployment files
174
+ if (filePath.includes('infra') || filePath.includes('deploy') || filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
175
+ events.push({ type: 'file.change.infra', data: { filePath, changeType } });
176
+ }
177
+
178
+ // Debugger: test files
179
+ if (filePath.includes('.test.') || filePath.includes('.spec.') || filePath.startsWith('tests/')) {
180
+ events.push({ type: 'file.change.tests', data: { filePath, changeType } });
181
+ }
182
+
183
+ return events;
184
+ }
185
+
186
+ /**
187
+ * CI failure watcher callback.
188
+ * Triggers specialists based on failure type.
189
+ */
190
+ export function onCIFailure(failureType, details = {}) {
191
+ const events = [];
192
+
193
+ if (failureType === 'test.fail') {
194
+ events.push({ type: 'test.fail', data: details });
195
+ }
196
+
197
+ if (failureType === 'test.flake') {
198
+ events.push({ type: 'test.flake', data: details });
199
+ }
200
+
201
+ if (failureType === 'coverage.drop') {
202
+ events.push({ type: 'coverage.drop', data: details });
203
+ }
204
+
205
+ if (failureType === 'build.fail') {
206
+ events.push({ type: 'build.fail', data: details });
207
+ }
208
+
209
+ if (failureType === 'lint.fail') {
210
+ events.push({ type: 'lint.fail', data: details });
211
+ }
212
+
213
+ return events;
214
+ }
215
+
216
+ /**
217
+ * Scheduled activation check.
218
+ * Runs periodically to trigger time-based activations.
219
+ */
220
+ export function onScheduleCheck() {
221
+ const events = [];
222
+ const now = new Date();
223
+
224
+ // Monday 9am: SRE SLO review
225
+ if (now.getDay() === 1 && now.getHours() === 9) {
226
+ events.push({ type: 'schedule.slo-review', data: { day: 'monday' } });
227
+ }
228
+
229
+ // Daily 6pm: Docs-keeper stale doc check
230
+ if (now.getHours() === 18) {
231
+ events.push({ type: 'schedule.stale-docs', data: { time: 'daily' } });
232
+ }
233
+
234
+ // First of month: Security CVE review
235
+ if (now.getDate() === 1) {
236
+ events.push({ type: 'schedule.cve-review', data: { frequency: 'monthly' } });
237
+ }
238
+
239
+ return events;
240
+ }
241
+
242
+ /**
243
+ * Get all pending activations for a specialist.
244
+ */
245
+ export function getPendingActivations(specialist) {
246
+ // In a full implementation, this would query a pending queue
247
+ // For now, return recent activation history
248
+ const state = getActivationState(specialist);
249
+ return {
250
+ pending: 0,
251
+ recent: state.activations.length,
252
+ total: state.totalActivations,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Clear activation state (for testing or manual reset).
258
+ */
259
+ export function clearActivationState(specialist) {
260
+ if (specialist) {
261
+ activationState.delete(specialist);
262
+ } else {
263
+ activationState.clear();
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Get activation summary for dashboard.
269
+ */
270
+ export function getActivationSummary() {
271
+ const summary = {};
272
+
273
+ for (const [specialist, state] of activationState.entries()) {
274
+ pruneOldActivations(state, RATE_WINDOW_MS);
275
+ summary[specialist] = {
276
+ recentActivations: state.activations.length,
277
+ totalActivations: state.totalActivations,
278
+ lastActivation: state.lastActivation,
279
+ rateLimitRemaining: DEFAULT_RATE_LIMIT - state.activations.length,
280
+ };
281
+ }
282
+
283
+ return summary;
284
+ }
@@ -3,9 +3,10 @@
3
3
  * lib/hooks/read-tracker.mjs — Read tracker hook — tracks file reads for efficiency analysis.
4
4
  *
5
5
  * Runs as PostToolUse after Read tool calls. Logs each read to ~/.cx/read-log.json including file path, line count, and session timestamp for efficiency reporting.
6
+ *
7
+ * @p95ms 10
8
+ * @maxBlockingScope none (PostToolUse, non-blocking)
6
9
  */
7
- // PostToolUse(Read) — records file content hash after each Read call
8
- // Powers hash-staleness check in edit-guard.mjs
9
10
  import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'fs';
10
11
  import { createHash } from 'crypto';
11
12
  import { join, resolve } from 'path';
@@ -17,9 +18,18 @@ const EFFICIENCY_STORE = join(CX_DIR, 'session-efficiency.json');
17
18
  const WARN_FLAGS = join(CX_DIR, 'warn-flags.txt');
18
19
  const SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
19
20
  const REPEATED_READ_WARNING_THRESHOLD = 5;
21
+ const REPEATED_READ_TIER2_THRESHOLD = 8;
20
22
  const LARGE_READ_WARNING_THRESHOLD = 3;
21
23
  const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
22
24
 
25
+ // Bound per-session `stats.files` so the read-tracking map cannot grow
26
+ // without limit across long sessions. The hook is a fresh node process per
27
+ // Read, so every invocation must read-mutate-write the full state — there
28
+ // is no in-memory accumulator that would survive between calls. The LRU cap
29
+ // drops the oldest entries by `lastReadAt`.
30
+
31
+ const FILES_LRU_CAP = 200;
32
+
23
33
  function loadEfficiencyStats(nowIso) {
24
34
  const fresh = {
25
35
  sessionStartedAt: nowIso,
@@ -85,8 +95,6 @@ try {
85
95
  if (new Date(v.ts).getTime() < cutoff) delete store[k];
86
96
  }
87
97
 
88
- writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
89
-
90
98
  const stats = loadEfficiencyStats(nowIso);
91
99
  const existingFile = stats.files[absPath];
92
100
  stats.readCount += 1;
@@ -104,11 +112,25 @@ try {
104
112
  lastRequestedLimit: effectiveLimit,
105
113
  };
106
114
 
115
+ // LRU cap: bound stats.files to FILES_LRU_CAP entries by lastReadAt so the
116
+ // structure does not grow without limit across long sessions.
117
+ const fileEntries = Object.entries(stats.files);
118
+ if (fileEntries.length > FILES_LRU_CAP) {
119
+ fileEntries.sort((a, b) => (a[1]?.lastReadAt || '').localeCompare(b[1]?.lastReadAt || ''));
120
+ const drop = fileEntries.length - FILES_LRU_CAP;
121
+ for (let i = 0; i < drop; i++) delete stats.files[fileEntries[i][0]];
122
+ }
123
+
107
124
  if (stats.repeatedReadCount >= REPEATED_READ_WARNING_THRESHOLD && !stats.warnings.repeatedReads) {
108
125
  const top = topRepeatedPath(stats.files);
109
126
  const topNote = top ? ` Top repeat: ${top.filePath} (${top.count}x).` : '';
110
127
  appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session.${topNote} Use rg or construct distill before re-reading more files.`);
111
128
  stats.warnings.repeatedReads = nowIso;
129
+
130
+ if (stats.repeatedReadCount >= REPEATED_READ_TIER2_THRESHOLD && !stats.warnings.repeatedReadsTier2) {
131
+ appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session — consider using construct distill or rg before re-reading entire files.`);
132
+ stats.warnings.repeatedReadsTier2 = nowIso;
133
+ }
112
134
  }
113
135
 
114
136
  if (stats.largeReadCount >= LARGE_READ_WARNING_THRESHOLD && !stats.warnings.largeReads) {
@@ -123,6 +145,7 @@ try {
123
145
 
124
146
  stats.lastUpdatedAt = nowIso;
125
147
 
148
+ writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
126
149
  writeFileSync(EFFICIENCY_STORE, JSON.stringify(stats, null, 2));
127
150
  } catch { /* best effort */ }
128
151