@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,31 +2,121 @@
2
2
  /**
3
3
  * lib/hooks/stop-notify.mjs — Stop notify hook — emits a session summary notification when Claude stops.
4
4
  *
5
- * Runs as a Stop hook. Summarizes the session work, active workflow state, and efficiency signals into a final message written to stdout for the user.
5
+ * Runs as a Stop hook. Summarizes the session work and efficiency
6
+ * signals into a final message written to stdout for the user.
7
+ *
8
+ * Per-transcript checkpoints in ~/.cx/transcript-checkpoints.json track the last processed
9
+ * line so every assistant turn in a Stop cycle is priced, not just the final one.
10
+ * Cost is computed via estimateUsageCost using the model ID from each transcript entry.
11
+ *
12
+ * @p95ms 500
13
+ * @maxBlockingScope Stop
6
14
  */
7
- import { readFileSync, writeFileSync, existsSync, appendFileSync, statSync } from 'fs';
15
+ import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from 'fs';
8
16
  import { execSync } from 'child_process';
9
17
  import { homedir } from 'os';
10
- import { join } from 'path';
11
- import { loadWorkflow, summarizeWorkflow, alignmentFindings } from '../workflow-state.mjs';
18
+ import { join, dirname } from 'path';
19
+ import { listSessions, loadSession, updateSession } from '../session-store.mjs';
20
+ import { captureSessionArtifacts } from '../artifact-capture.mjs';
21
+ import { appendSessionStats } from '../memory-stats.mjs';
22
+ import { estimateUsageCost } from '../telemetry/model-pricing-catalog.mjs';
12
23
 
13
- function readLastTranscriptUsage(transcriptPath) {
24
+ function loadTranscriptCheckpoints(checkpointPath) {
14
25
  try {
15
- if (!transcriptPath || !existsSync(transcriptPath)) return null;
16
- const size = statSync(transcriptPath).size;
17
- const readSize = Math.min(size, 256 * 1024);
26
+ if (!existsSync(checkpointPath)) return {};
27
+ return JSON.parse(readFileSync(checkpointPath, 'utf8')) || {};
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+
33
+ function saveTranscriptCheckpoints(checkpointPath, data) {
34
+ try {
35
+ mkdirSync(dirname(checkpointPath), { recursive: true });
36
+ writeFileSync(checkpointPath, JSON.stringify(data));
37
+ } catch { /* non-critical */ }
38
+ }
39
+
40
+ function collectUnprocessedUsage(transcriptPath, checkpoints) {
41
+ const entries = [];
42
+ try {
43
+ if (!transcriptPath || !existsSync(transcriptPath)) return entries;
18
44
  const content = readFileSync(transcriptPath, 'utf8');
19
- const tail = content.slice(-readSize);
20
- const lines = tail.split(/\r?\n/).filter(Boolean);
21
- for (let i = lines.length - 1; i >= 0; i--) {
22
- try {
23
- const obj = JSON.parse(lines[i]);
24
- const usage = obj?.message?.usage || obj?.usage;
25
- if (usage && (usage.input_tokens || usage.output_tokens)) return usage;
26
- } catch { /* skip */ }
45
+ const lines = content.split(/\r?\n/);
46
+ const state = checkpoints[transcriptPath] || { lastLine: 0 };
47
+
48
+ // Reset on truncation/rotation rather than skipping lines or double-counting.
49
+ const startLine = state.lastLine > lines.length ? 0 : state.lastLine;
50
+
51
+ for (let i = startLine; i < lines.length; i++) {
52
+ const line = lines[i];
53
+ if (!line) continue;
54
+ let obj;
55
+ try { obj = JSON.parse(line); } catch { continue; }
56
+ const usage = obj?.message?.usage || obj?.usage;
57
+ if (!usage) continue;
58
+ if (!usage.input_tokens && !usage.output_tokens && !usage.cache_read_input_tokens && !usage.cache_creation_input_tokens) continue;
59
+ const model =
60
+ obj?.message?.model ||
61
+ obj?.model ||
62
+ obj?.message?.modelID ||
63
+ obj?.modelID ||
64
+ null;
65
+ const ts = obj?.timestamp || obj?.message?.timestamp || null;
66
+ entries.push({ usage, model, ts });
27
67
  }
28
- } catch { /* best effort */ }
29
- return null;
68
+ checkpoints[transcriptPath] = { lastLine: lines.length };
69
+ } catch { /* non-critical */ }
70
+ return entries;
71
+ }
72
+
73
+ function priceUsage(model, usage) {
74
+ const inputTokens = Number(usage?.input_tokens || 0);
75
+ const outputTokens = Number(usage?.output_tokens || 0);
76
+ const reasoningTokens = Number(
77
+ usage?.reasoning_tokens ||
78
+ usage?.output_token_details?.reasoning ||
79
+ usage?.completion_tokens_details?.reasoning_tokens ||
80
+ 0,
81
+ );
82
+ const cacheReadInputTokens = Number(usage?.cache_read_input_tokens || 0);
83
+ const cacheCreation5mInputTokens = Number(
84
+ usage?.cache_creation?.ephemeral_5m_input_tokens ||
85
+ usage?.cache_creation_5m_input_tokens ||
86
+ 0,
87
+ );
88
+ const cacheCreation1hInputTokens = Number(
89
+ usage?.cache_creation?.ephemeral_1h_input_tokens ||
90
+ usage?.cache_creation_1h_input_tokens ||
91
+ 0,
92
+ );
93
+ const explicitCacheCreationInputTokens = Number(usage?.cache_creation_input_tokens || 0);
94
+ const cacheCreationInputTokens =
95
+ explicitCacheCreationInputTokens ||
96
+ (cacheCreation5mInputTokens + cacheCreation1hInputTokens);
97
+
98
+ const pricing = estimateUsageCost(model, {
99
+ inputTokens,
100
+ outputTokens,
101
+ reasoningTokens,
102
+ cacheReadInputTokens,
103
+ cacheCreationInputTokens,
104
+ cacheCreation5mInputTokens,
105
+ cacheCreation1hInputTokens,
106
+ });
107
+
108
+ return {
109
+ inputTokens,
110
+ outputTokens,
111
+ reasoningTokens,
112
+ cacheReadInputTokens,
113
+ cacheCreationInputTokens,
114
+ cacheCreation5mInputTokens,
115
+ cacheCreation1hInputTokens,
116
+ costUsd: pricing.costUsd || 0,
117
+ costSource: pricing.costSource,
118
+ modelName: pricing.modelName || model || null,
119
+ };
30
120
  }
31
121
 
32
122
  function resolveAttribution(workflow) {
@@ -40,9 +130,8 @@ function resolveAttribution(workflow) {
40
130
  }
41
131
  }
42
132
 
43
- // Stop hooks must echo stdin to stdout
44
133
  let raw = '';
45
- try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
134
+ try { raw = readFileSync(0, 'utf8'); } catch { /* non-critical */ }
46
135
  if (raw) process.stdout.write(raw);
47
136
 
48
137
  const home = homedir();
@@ -51,108 +140,105 @@ const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
51
140
  const countPath = join(home, '.cx', 'files-changed-count.txt');
52
141
  const costLogPath = join(home, '.cx', 'session-cost.jsonl');
53
142
  const lastAgentPath = join(home, '.cx', 'last-agent.json');
143
+ const checkpointPath = join(home, '.cx', 'transcript-checkpoints.json');
54
144
 
55
- // Read TS result
56
145
  let tsResult = 'unchecked';
57
- try { tsResult = existsSync(tsResultPath) ? readFileSync(tsResultPath, 'utf8').trim() : 'unchecked'; } catch { /* best effort */ }
146
+ try { tsResult = existsSync(tsResultPath) ? readFileSync(tsResultPath, 'utf8').trim() : 'unchecked'; } catch { /* non-critical */ }
58
147
 
59
- // Read and clear warn flags
60
148
  let warnings = [];
61
149
  try {
62
150
  if (existsSync(warnFlagsPath)) {
63
151
  warnings = readFileSync(warnFlagsPath, 'utf8').split('\n').filter(Boolean);
64
152
  writeFileSync(warnFlagsPath, '');
65
153
  }
66
- } catch { /* best effort */ }
154
+ } catch { /* non-critical */ }
67
155
 
68
- // Read and clear files-changed count
69
156
  let fileCount = 0;
70
157
  try {
71
158
  if (existsSync(countPath)) {
72
159
  fileCount = parseInt(readFileSync(countPath, 'utf8').trim() || '0', 10) || 0;
73
160
  writeFileSync(countPath, '0');
74
161
  }
75
- } catch { /* best effort */ }
162
+ } catch { /* non-critical */ }
76
163
 
77
- // Estimate cost from stdin usage tokens using Sonnet 4.5/4.6 defaults.
78
164
  let costNote = '';
165
+ let totalCostUsd = 0;
166
+ let turnsLogged = 0;
79
167
  try {
80
168
  const payload = raw ? JSON.parse(raw) : {};
81
- let usage = payload?.usage || payload?.stop_hook_active?.usage || null;
82
- const transcriptPath = payload?.transcript_path || payload?.transcriptPath || process.env.CLAUDE_TRANSCRIPT_PATH;
83
- if (!usage || (!usage.input_tokens && !usage.output_tokens)) {
84
- const fromTranscript = readLastTranscriptUsage(transcriptPath);
85
- if (fromTranscript) usage = fromTranscript;
169
+ const transcriptPath =
170
+ payload?.transcript_path ||
171
+ payload?.transcriptPath ||
172
+ process.env.CLAUDE_TRANSCRIPT_PATH;
173
+
174
+ let agentName = null;
175
+ let taskKey = null;
176
+ try {
177
+ if (existsSync(lastAgentPath)) {
178
+ const lastAgent = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
179
+ agentName = lastAgent.agent || null;
180
+ taskKey = lastAgent.taskKey || null;
181
+ writeFileSync(lastAgentPath, '');
182
+ }
183
+ } catch { /* non-critical */ }
184
+
185
+ if (!agentName) {
186
+ agentName = 'construct';
86
187
  }
87
- usage = usage || {};
88
- const inputTokens = usage?.input_tokens || 0;
89
- const outputTokens = usage?.output_tokens || 0;
90
- const cacheReadInputTokens = usage?.cache_read_input_tokens || 0;
91
- const cacheCreation5mInputTokens = usage?.cache_creation?.ephemeral_5m_input_tokens || usage?.cache_creation_5m_input_tokens || 0;
92
- const cacheCreation1hInputTokens = usage?.cache_creation?.ephemeral_1h_input_tokens || usage?.cache_creation_1h_input_tokens || 0;
93
- const explicitCacheCreationInputTokens = usage?.cache_creation_input_tokens || 0;
94
- const cacheCreationInputTokens = explicitCacheCreationInputTokens || cacheCreation5mInputTokens + cacheCreation1hInputTokens;
95
- if (inputTokens || outputTokens) {
96
- const cost = (
97
- inputTokens * 3 +
98
- cacheCreation5mInputTokens * 3.75 +
99
- cacheCreation1hInputTokens * 6 +
100
- Math.max(0, cacheCreationInputTokens - cacheCreation5mInputTokens - cacheCreation1hInputTokens) * 3.75 +
101
- cacheReadInputTokens * 0.3 +
102
- outputTokens * 15
103
- ) / 1_000_000;
104
- costNote = `~${cost.toFixed(2)} this response`;
105
-
106
- // Read agent attribution recorded by agent-tracker hook (best effort, cleared after read)
107
- let agentName = null;
108
- let taskKey = null;
109
- try {
110
- if (existsSync(lastAgentPath)) {
111
- const lastAgent = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
112
- agentName = lastAgent.agent || null;
113
- taskKey = lastAgent.taskKey || null;
114
- writeFileSync(lastAgentPath, '');
115
- }
116
- } catch { /* best effort */ }
117
-
118
- // Fallback attribution: active workflow task owner, else 'construct'
119
- if (!agentName) {
120
- try {
121
- const wf = loadWorkflow(process.cwd());
122
- const resolved = resolveAttribution(wf);
123
- agentName = resolved.agent;
124
- if (!taskKey && resolved.taskKey) taskKey = resolved.taskKey;
125
- } catch { agentName = 'construct'; }
188
+
189
+ const checkpoints = loadTranscriptCheckpoints(checkpointPath);
190
+ let turns = collectUnprocessedUsage(transcriptPath, checkpoints);
191
+
192
+ if (turns.length === 0) {
193
+ const usage = payload?.usage || payload?.stop_hook_active?.usage || null;
194
+ const model = payload?.model || payload?.session?.model || null;
195
+ if (usage && (usage.input_tokens || usage.output_tokens)) {
196
+ turns = [{ usage, model, ts: null }];
126
197
  }
198
+ }
127
199
 
128
- try {
129
- const entry = { ts: new Date().toISOString(), input_tokens: inputTokens, output_tokens: outputTokens, cost_usd: cost };
130
- if (cacheReadInputTokens) entry.cache_read_input_tokens = cacheReadInputTokens;
131
- if (cacheCreationInputTokens) entry.cache_creation_input_tokens = cacheCreationInputTokens;
132
- if (cacheCreation5mInputTokens) entry.cache_creation_5m_input_tokens = cacheCreation5mInputTokens;
133
- if (cacheCreation1hInputTokens) entry.cache_creation_1h_input_tokens = cacheCreation1hInputTokens;
134
- if (agentName) entry.agent = agentName;
135
- if (taskKey) entry.task_key = taskKey;
136
- appendFileSync(costLogPath, JSON.stringify(entry) + '\n');
137
- } catch { /* best effort */ }
200
+ const nowIso = new Date().toISOString();
201
+ for (const turn of turns) {
202
+ const priced = priceUsage(turn.model, turn.usage);
203
+ if (
204
+ !priced.inputTokens &&
205
+ !priced.outputTokens &&
206
+ !priced.cacheReadInputTokens &&
207
+ !priced.cacheCreationInputTokens
208
+ ) continue;
209
+
210
+ const entry = {
211
+ ts: turn.ts || nowIso,
212
+ input_tokens: priced.inputTokens,
213
+ output_tokens: priced.outputTokens,
214
+ cost_usd: priced.costUsd,
215
+ };
216
+ if (priced.reasoningTokens) entry.reasoning_tokens = priced.reasoningTokens;
217
+ entry.total_tokens = priced.inputTokens + priced.outputTokens + priced.reasoningTokens;
218
+ if (priced.cacheReadInputTokens) entry.cache_read_input_tokens = priced.cacheReadInputTokens;
219
+ if (priced.cacheCreationInputTokens) entry.cache_creation_input_tokens = priced.cacheCreationInputTokens;
220
+ if (priced.cacheCreation5mInputTokens) entry.cache_creation_5m_input_tokens = priced.cacheCreation5mInputTokens;
221
+ if (priced.cacheCreation1hInputTokens) entry.cache_creation_1h_input_tokens = priced.cacheCreation1hInputTokens;
222
+ if (priced.modelName) entry.model = priced.modelName;
223
+ if (priced.costSource) entry.cost_source = priced.costSource;
224
+ if (agentName) entry.agent = agentName;
225
+ if (taskKey) entry.task_key = taskKey;
226
+
227
+ try { appendFileSync(costLogPath, JSON.stringify(entry) + '\n'); } catch { /* non-critical */ }
228
+
229
+ totalCostUsd += priced.costUsd;
230
+ turnsLogged += 1;
138
231
  }
139
- } catch { /* payload parse failure */ }
140
232
 
141
- // Executive Summary from Workflow
142
- let execSummary = '';
143
- try {
144
- const workflow = loadWorkflow(process.cwd());
145
- if (workflow) {
146
- const findings = alignmentFindings(workflow);
147
- const high = findings.filter(f => f.severity === 'HIGH').length;
148
- const phase = workflow.phase.toUpperCase();
149
- execSummary = `${phase}: ${workflow.status}${high > 0 ? ` (${high} blocks)` : ''}`;
233
+ saveTranscriptCheckpoints(checkpointPath, checkpoints);
234
+
235
+ if (totalCostUsd > 0) {
236
+ const label = turnsLogged > 1 ? `${turnsLogged} turns` : 'this response';
237
+ costNote = `~$${totalCostUsd.toFixed(2)} ${label}`;
150
238
  }
151
- } catch { /* best effort */ }
239
+ } catch { /* non-critical */ }
152
240
 
153
- // Compose message parts
154
241
  const parts = [];
155
- if (execSummary) parts.push(execSummary);
156
242
  if (fileCount > 0) parts.push(`${fileCount} file${fileCount !== 1 ? 's' : ''} updated`);
157
243
  if (tsResult === 'pass') parts.push('TS OK');
158
244
  else if (tsResult !== 'unchecked') parts.push(`TS: ${tsResult}`);
@@ -165,7 +251,62 @@ const body = [
165
251
  parts.join(' · '),
166
252
  ].filter(Boolean).join('\n');
167
253
 
168
- // Fire notification
254
+ try {
255
+ const cwd = process.cwd();
256
+ const activeSessions = listSessions(cwd, { status: 'active', limit: 1 });
257
+ if (activeSessions.length > 0) {
258
+ const sid = activeSessions[0].id;
259
+ const summaryParts = [];
260
+ try {
261
+ const recentCommits = execSync(`git -C "${cwd}" log --oneline -5 2>/dev/null`, { timeout: 4000 }).toString().trim();
262
+ if (recentCommits) summaryParts.push('commits: ' + recentCommits.split('\n').map((l) => l.trim()).slice(0, 3).join('; '));
263
+ } catch { /* non-critical */ }
264
+ if (fileCount > 0) summaryParts.push(fileCount + ' file' + (fileCount !== 1 ? 's' : '') + ' updated');
265
+ if (warnings.length > 0) summaryParts.push(warnings.length + ' warning' + (warnings.length !== 1 ? 's' : ''));
266
+ const summary = summaryParts.join(' — ') || 'Session completed';
267
+ updateSession(cwd, sid, { status: 'completed', summary });
268
+ }
269
+ } catch { /* non-critical */ }
270
+
271
+ try {
272
+ const cwd2 = process.cwd();
273
+ const closedSessions = listSessions(cwd2, { status: 'completed', limit: 1 });
274
+ if (closedSessions.length > 0) {
275
+ const session = loadSession(cwd2, closedSessions[0].id);
276
+ if (session) captureSessionArtifacts(cwd2, session);
277
+ }
278
+ } catch { /* non-critical */ }
279
+
280
+ const memStatsPath = join(home, '.cx', 'session-memory-stats.json');
281
+ try {
282
+ if (existsSync(memStatsPath)) {
283
+ const memStats = JSON.parse(readFileSync(memStatsPath, 'utf8'));
284
+ appendSessionStats(process.cwd(), {
285
+ project: memStats.project || null,
286
+ observationsInjected: memStats.observationsInjected || 0,
287
+ memoryEnabled: memStats.memoryEnabled !== false,
288
+ });
289
+ writeFileSync(memStatsPath, '');
290
+ }
291
+ } catch { /* non-critical */ }
292
+
293
+ // Auto-generate a performance review every 24h. Cheap — reads the cost
294
+ // ledger + (optionally) telemetry generations and writes a structured
295
+ // JSON file the dashboard's Performance page consumes. Skipped on
296
+ // repeat Stop events within the window to avoid review-spam.
297
+ try {
298
+ const homeDir = process.env.HOME || homedir();
299
+ const stampPath = join(homeDir, '.cx', 'performance-reviews', '.last-run');
300
+ let lastRun = 0;
301
+ try { lastRun = Number(readFileSync(stampPath, 'utf8')) || 0; } catch { /* fresh */ }
302
+ if (Date.now() - lastRun > 24 * 60 * 60 * 1000) {
303
+ const { runGenerator } = await import('../performance/generate.mjs');
304
+ await runGenerator({ env: process.env });
305
+ mkdirSync(dirname(stampPath), { recursive: true });
306
+ writeFileSync(stampPath, String(Date.now()));
307
+ }
308
+ } catch { /* review is best-effort */ }
309
+
169
310
  const platform = process.platform;
170
311
  try {
171
312
  if (platform === 'darwin') {
@@ -184,7 +325,6 @@ try {
184
325
  process.stderr.write(`[stop-notify] ${title}: ${body}\n`);
185
326
  }
186
327
  } catch {
187
- // Fallback to stderr if notification fails
188
328
  process.stderr.write(`[stop-notify] ${title}: ${body}\n`);
189
329
  }
190
330
 
@@ -3,6 +3,9 @@
3
3
  * lib/hooks/stop-typecheck.mjs — Stop typecheck hook — runs TypeScript type-check at session end and records result.
4
4
  *
5
5
  * Runs as a Stop hook. Invokes tsc --noEmit on the project and records pass/fail to ~/.cx/pending-typecheck.txt for the next session-start hook to surface.
6
+ *
7
+ * @p95ms 2000
8
+ * @maxBlockingScope Stop
6
9
  */
7
10
  import { readFileSync, writeFileSync, existsSync } from 'fs';
8
11
  import { execSync } from 'child_process';
@@ -64,7 +67,6 @@ try {
64
67
  try { writeFileSync(tsResultPath, 'pass'); } catch { /* best effort */ }
65
68
  } catch (e) {
66
69
  const output = (e.stdout?.toString() || '') + (e.stderr?.toString() || '');
67
- // Count error lines (lines ending with "error TS####:")
68
70
  const errorLines = output.split('\n').filter(l => /error TS\d+:/.test(l));
69
71
  const count = errorLines.length || 1;
70
72
 
@@ -78,6 +80,16 @@ try {
78
80
  process.stderr.write(
79
81
  `[stop-typecheck] TypeScript found ${count} error${count !== 1 ? 's' : ''} that need attention. The code runs but won't compile cleanly. Review before releasing.\n`
80
82
  );
83
+
84
+ try {
85
+ const { emitRoleEvent } = await import('../roles/hook-emit.mjs');
86
+ emitRoleEvent({
87
+ type: 'test.fail',
88
+ summary: `TypeScript: ${count} error${count !== 1 ? 's' : ''}\n${errorLines.slice(0, 3).join('\n')}`,
89
+ hookInput: { cwd: projectDir },
90
+ context: { tool: 'tsc', count },
91
+ });
92
+ } catch { /* best effort */ }
81
93
  }
82
94
 
83
95
  process.exit(0);
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * test-watch.mjs — PostToolUse / Bash (async)
4
+ *
5
+ * When a Bash invocation ran tests (vitest/jest/npm test/bun test/pytest) and
6
+ * exited non-zero, emit `test.fail` for cx-qa. Detects flake patterns when the
7
+ * same test name passes and then fails within an hour and emits `test.flake`.
8
+ *
9
+ * @p95ms 30
10
+ * @maxBlockingScope none (async, non-blocking)
11
+ */
12
+
13
+ import { readFileSync, existsSync, appendFileSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { logHookFailure } from './_lib/log.mjs';
17
+ import { emitRoleEvent } from '../roles/hook-emit.mjs';
18
+
19
+ let input = {};
20
+ try { input = JSON.parse(readFileSync(0, 'utf8')); }
21
+ catch (err) { logHookFailure({ hook: 'test-watch', err, phase: 'parse' }); process.exit(0); }
22
+
23
+ const command = input?.tool_input?.command || '';
24
+ const exitCode = input?.tool_response?.exit_code ?? input?.tool_response?.exitCode;
25
+ const stdout = String(input?.tool_response?.stdout || '');
26
+ const stderr = String(input?.tool_response?.stderr || '');
27
+
28
+ const isTestCmd = /\b(vitest|jest|npm\s+test|bun\s+test|pnpm\s+test|yarn\s+test|pytest|cargo\s+test|go\s+test)\b/.test(command);
29
+ if (!isTestCmd) process.exit(0);
30
+ if (exitCode === 0 || exitCode === undefined) process.exit(0);
31
+
32
+ const combined = (stderr + '\n' + stdout).slice(-4000);
33
+ const failedNames = [...combined.matchAll(/(?:FAIL|×|✗)\s+([^\n]{3,160})/g)].slice(0, 5).map((m) => m[1].trim());
34
+ const summary = `Tests failed: ${failedNames.length || 'unknown'}; first: ${failedNames[0] || combined.split('\n').find(Boolean)?.slice(0, 160) || command}`;
35
+
36
+ emitRoleEvent({
37
+ type: 'test.fail',
38
+ summary,
39
+ hookInput: input,
40
+ context: { command, exitCode, failedNames },
41
+ });
42
+
43
+ const flakeTrackerPath = join(homedir(), '.cx', 'test-watch-state.json');
44
+ try {
45
+ const state = existsSync(flakeTrackerPath) ? JSON.parse(readFileSync(flakeTrackerPath, 'utf8')) : {};
46
+ const now = Date.now();
47
+ for (const name of failedNames) {
48
+ const prev = state[name];
49
+ if (prev && prev.passedAt && now - prev.passedAt < 60 * 60 * 1000) {
50
+ emitRoleEvent({
51
+ type: 'test.flake',
52
+ summary: `Flake suspected: ${name} (passed ${Math.round((now - prev.passedAt) / 60000)} min ago, now failing)`,
53
+ hookInput: input,
54
+ context: { name, prevPassedAt: prev.passedAt },
55
+ });
56
+ }
57
+ state[name] = { failedAt: now };
58
+ }
59
+ appendFileSync(flakeTrackerPath, '');
60
+ const trimmed = {};
61
+ for (const [k, v] of Object.entries(state)) {
62
+ if ((v.failedAt || v.passedAt || 0) > now - 24 * 60 * 60 * 1000) trimmed[k] = v;
63
+ }
64
+ const writeFileSync = (await import('node:fs')).writeFileSync;
65
+ writeFileSync(flakeTrackerPath, JSON.stringify(trimmed));
66
+ } catch { /* best effort */ }
67
+
68
+ process.exit(0);
@@ -1,22 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/host-capabilities.mjs — <one-line purpose>
3
+ * lib/host-capabilities.mjs — Detect the active AI harness and its tool surface.
4
4
  *
5
- * <2–6 line summary.>
5
+ * Inspects the runtime environment (process title, env vars, OpenCode config)
6
+ * to classify whether Construct is running inside Claude Code, OpenCode,
7
+ * a plain terminal, or a multi-agent subagent context. The MCP server and
8
+ * CLI consult the classification to gate harness-specific features.
6
9
  */
7
10
  import { execFileSync, execSync } from "node:child_process";
8
11
  import { readOpenCodeConfig } from "./opencode-config.mjs";
9
12
  import net from "node:net";
10
13
 
11
- export async function findAvailablePort(startPort) {
12
- return new Promise((resolve) => {
14
+ const LOWEST_PORT = 0;
15
+ const HIGHEST_PORT = 65535;
16
+ const RETRYABLE_PORT_ERRORS = new Set(["EADDRINUSE", "EACCES"]);
17
+
18
+ export async function findAvailablePort(startPort, { maxPort = HIGHEST_PORT } = {}) {
19
+ if (!Number.isInteger(startPort) || startPort < LOWEST_PORT || startPort > HIGHEST_PORT) {
20
+ throw new RangeError(`startPort must be an integer between ${LOWEST_PORT} and ${HIGHEST_PORT}. Received ${startPort}.`);
21
+ }
22
+ if (!Number.isInteger(maxPort) || maxPort < LOWEST_PORT || maxPort > HIGHEST_PORT) {
23
+ throw new RangeError(`maxPort must be an integer between ${LOWEST_PORT} and ${HIGHEST_PORT}. Received ${maxPort}.`);
24
+ }
25
+ if (startPort > maxPort) {
26
+ throw new RangeError(`startPort ${startPort} must be less than or equal to maxPort ${maxPort}.`);
27
+ }
28
+
29
+ return new Promise((resolve, reject) => {
13
30
  const server = net.createServer();
14
31
  server.listen(startPort, () => {
15
32
  const { port } = server.address();
16
33
  server.close(() => resolve(port));
17
34
  });
18
- server.on("error", () => {
19
- resolve(findAvailablePort(startPort + 1));
35
+ server.on("error", (error) => {
36
+ if (!RETRYABLE_PORT_ERRORS.has(error?.code) || startPort >= maxPort) {
37
+ reject(error instanceof Error ? error : new Error(String(error)));
38
+ return;
39
+ }
40
+ resolve(findAvailablePort(startPort + 1, { maxPort }));
20
41
  });
21
42
  });
22
43
  }
@@ -81,11 +102,11 @@ export function detectHostCapabilities() {
81
102
  version: opencodeRaw,
82
103
  orchestration: "primary-plus-subagents",
83
104
  promptableWorkers: false,
84
- sharedTaskRuntime: "construct-workflow",
105
+ sharedTaskRuntime: "tracker-plus-plan",
85
106
  lifecycleHooks: ["session.error", "session.idle", "tool.execute.before", "tool.execute.after"],
86
107
  notes: [
87
108
  "Primary agents are promptable; subagents are bounded worker sessions.",
88
- "Construct uses .cx/workflow.json plus task permissions and plugins to coordinate parallel worker execution.",
109
+ "Construct uses tracker state, plan.md, task permissions, and plugins to coordinate parallel worker execution.",
89
110
  "Use NEEDS_MAIN_INPUT to route user questions back to the primary persona.",
90
111
  ],
91
112
  },
@@ -95,10 +116,10 @@ export function detectHostCapabilities() {
95
116
  version: codexRaw,
96
117
  orchestration: "profile-and-mcp",
97
118
  promptableWorkers: false,
98
- sharedTaskRuntime: "construct-workflow",
119
+ sharedTaskRuntime: "tracker-plus-plan",
99
120
  lifecycleHooks: [],
100
121
  notes: [
101
- "Use Construct profiles, MCP workflow tools, and the active session.",
122
+ "Use Construct profiles, MCP project/memory tools, and the active session.",
102
123
  "Native profile switching is not automatic; continue in-session when dispatch is unavailable.",
103
124
  ],
104
125
  },