@geraldmaron/construct 1.0.0 → 1.0.1

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 +10 -7
  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 +1116 -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 +525 -38
  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
@@ -0,0 +1,140 @@
1
+ /**
2
+ * lib/doctor/index.mjs — `construct-doctor` daemon main loop.
3
+ *
4
+ * Long-running Node process spawned by `construct up`. Holds the L0 watchers
5
+ * for resource pressure, service health, disk + log rotation, and cost. Each
6
+ * watcher ticks on its own interval; the loop is bounded so a slow watcher
7
+ * never starves the others. Termination via SIGTERM/SIGINT triggers a clean
8
+ * shutdown that records a final audit entry.
9
+ *
10
+ * State file: ~/.construct/doctor.json (pid, started, lastTick).
11
+ * Audit log: ~/.cx/doctor-log.jsonl.
12
+ */
13
+
14
+ import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ import { join } from 'node:path';
17
+
18
+ import { record } from './audit.mjs';
19
+ import * as disk from './watchers/disk.mjs';
20
+ import * as cost from './watchers/cost.mjs';
21
+ import * as processPressure from './watchers/process-pressure.mjs';
22
+ import * as serviceHealth from './watchers/service-health.mjs';
23
+ import * as bdWatch from './watchers/bd-watch.mjs';
24
+ import * as handoffs from './watchers/handoffs.mjs';
25
+
26
+ const STATE_PATH = join(homedir(), '.construct', 'doctor.json');
27
+ const WATCHERS = [disk, cost, processPressure, serviceHealth, bdWatch, handoffs];
28
+
29
+ let running = false;
30
+ let timers = [];
31
+ let lastTickAt = {};
32
+
33
+ function writeState(state) {
34
+ const dir = join(homedir(), '.construct');
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+ writeFileSync(STATE_PATH, JSON.stringify({ ...state, updatedAt: Date.now() }));
37
+ }
38
+
39
+ function clearState() {
40
+ if (existsSync(STATE_PATH)) {
41
+ try { unlinkSync(STATE_PATH); } catch { /* best effort */ }
42
+ }
43
+ }
44
+
45
+ function processAlive(pid) {
46
+ if (!pid) return false;
47
+ try { process.kill(pid, 0); return true; } catch { return false; }
48
+ }
49
+
50
+ export function readState() {
51
+ if (!existsSync(STATE_PATH)) return null;
52
+ try {
53
+ const state = JSON.parse(readFileSync(STATE_PATH, 'utf8'));
54
+ if (!processAlive(state.pid)) {
55
+ clearState();
56
+ return null;
57
+ }
58
+ return state;
59
+ } catch { return null; }
60
+ }
61
+
62
+ async function runWatcher(watcher) {
63
+ if (!running) return;
64
+ const startedAt = Date.now();
65
+ try {
66
+ const result = await watcher.tick();
67
+ lastTickAt[watcher.name] = startedAt;
68
+ const tookMs = Date.now() - startedAt;
69
+ if (process.env.CONSTRUCT_DOCTOR_VERBOSE === '1') {
70
+ process.stderr.write(
71
+ `[doctor] ${watcher.name} ok ${tookMs}ms actions=${result.actions?.length || 0} escalations=${result.escalations?.length || 0}\n`
72
+ );
73
+ }
74
+ } catch (err) {
75
+ record({
76
+ kind: 'error',
77
+ watcher: watcher.name,
78
+ result: 'failed',
79
+ summary: `tick failed: ${err.message || err}`,
80
+ context: { stack: String(err.stack || '').slice(0, 1024) },
81
+ });
82
+ }
83
+ }
84
+
85
+ export async function start() {
86
+ if (running) return;
87
+ running = true;
88
+ lastTickAt = {};
89
+
90
+ writeState({ pid: process.pid, startedAt: Date.now(), watchers: WATCHERS.map((w) => w.name) });
91
+ record({
92
+ kind: 'lifecycle',
93
+ watcher: 'doctor',
94
+ action: 'start',
95
+ summary: `doctor started (pid ${process.pid}, ${WATCHERS.length} watchers)`,
96
+ context: { watchers: WATCHERS.map((w) => ({ name: w.name, intervalMs: w.intervalMs })) },
97
+ });
98
+
99
+ for (const watcher of WATCHERS) {
100
+ await runWatcher(watcher);
101
+ const t = setInterval(() => { runWatcher(watcher); }, watcher.intervalMs);
102
+ if (t.unref) t.unref();
103
+ timers.push(t);
104
+ }
105
+
106
+ const shutdown = (signal) => {
107
+ record({ kind: 'lifecycle', watcher: 'doctor', action: 'stop', summary: `doctor stopped (signal=${signal})` });
108
+ stop();
109
+ process.exit(0);
110
+ };
111
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
112
+ process.on('SIGINT', () => shutdown('SIGINT'));
113
+ }
114
+
115
+ export function stop() {
116
+ running = false;
117
+ for (const t of timers) clearInterval(t);
118
+ timers = [];
119
+ clearState();
120
+ }
121
+
122
+ export function statusSnapshot() {
123
+ const state = readState();
124
+ return {
125
+ running: !!state,
126
+ state,
127
+ watchers: WATCHERS.map((w) => ({
128
+ name: w.name,
129
+ intervalMs: w.intervalMs,
130
+ lastTickAt: lastTickAt[w.name] || null,
131
+ })),
132
+ };
133
+ }
134
+
135
+ const isMain = (() => {
136
+ try { return import.meta.url === `file://${process.argv[1]}`; } catch { return false; }
137
+ })();
138
+ if (isMain) {
139
+ await start();
140
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * lib/doctor/report.mjs — M1 self-host measurement reporter.
3
+ *
4
+ * Tallies what the L0 doctor and L1 role framework actually did over a window.
5
+ * Evaluates the "Construct runs on Construct" milestones (M1 = SRE-only,
6
+ * 7 days unattended; later milestones add more personas). Pure read-only —
7
+ * scans the JSONL audit logs and ledgers and produces a markdown summary.
8
+ */
9
+
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+
14
+ import { recent } from './audit.mjs';
15
+ import { getTotalDailySpend, getDailySpend, totalBudget, personaBudget, dayKey } from '../cost-ledger.mjs';
16
+
17
+ function parseWindow(arg) {
18
+ const m = /^(\d+)([dhm])$/.exec(arg || '7d');
19
+ if (!m) return 7 * 24 * 60 * 60 * 1000;
20
+ const n = parseInt(m[1], 10);
21
+ const unit = m[2];
22
+ return n * (unit === 'd' ? 86400_000 : unit === 'h' ? 3600_000 : 60_000);
23
+ }
24
+
25
+ function readJsonl(p) {
26
+ if (!existsSync(p)) return [];
27
+ return readFileSync(p, 'utf8').split('\n').filter(Boolean).map((l) => {
28
+ try { return JSON.parse(l); } catch { return null; }
29
+ }).filter(Boolean);
30
+ }
31
+
32
+ function groupBy(arr, fn) {
33
+ const out = {};
34
+ for (const x of arr) {
35
+ const k = fn(x) || 'unknown';
36
+ out[k] = (out[k] || 0) + 1;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function summarizeActions(auditEntries) {
42
+ const actions = auditEntries.filter((e) => e.kind === 'action');
43
+ const escalations = auditEntries.filter((e) => e.kind === 'escalate');
44
+ const samples = auditEntries.filter((e) => e.kind === 'sample');
45
+ const recoveries = auditEntries.filter((e) => e.kind === 'recovery');
46
+ const errors = auditEntries.filter((e) => e.kind === 'error');
47
+ return {
48
+ actions: { total: actions.length, byWatcher: groupBy(actions, (e) => e.watcher), byAction: groupBy(actions, (e) => e.action) },
49
+ escalations: { total: escalations.length, byWatcher: groupBy(escalations, (e) => e.watcher), byEvent: groupBy(escalations, (e) => e.target) },
50
+ samples: { total: samples.length, byWatcher: groupBy(samples, (e) => e.watcher) },
51
+ recoveries: { total: recoveries.length, byWatcher: groupBy(recoveries, (e) => e.watcher) },
52
+ errors: { total: errors.length, byWatcher: groupBy(errors, (e) => e.watcher) },
53
+ };
54
+ }
55
+
56
+ function fmtTable(title, obj) {
57
+ const entries = Object.entries(obj || {}).sort((a, b) => b[1] - a[1]);
58
+ if (entries.length === 0) return `${title}: (none)\n`;
59
+ let out = `${title}:\n`;
60
+ for (const [k, v] of entries) out += ` ${String(v).padStart(4, ' ')} ${k}\n`;
61
+ return out;
62
+ }
63
+
64
+ export async function runReport(args = []) {
65
+ const windowArg = args.find((a) => a.startsWith('--since='))?.split('=')[1] || '7d';
66
+ const windowMs = parseWindow(windowArg);
67
+ const since = Date.now() - windowMs;
68
+
69
+ const auditEntries = recent({ since, limit: 10000 });
70
+ const events = readJsonl(join(homedir(), '.cx', 'events.jsonl')).filter((e) => e.ts >= since);
71
+ const pending = readJsonl(join(homedir(), '.cx', 'role-pending.jsonl')).filter((e) => e.ts >= since);
72
+
73
+ const summary = summarizeActions(auditEntries);
74
+ const eventsByType = groupBy(events, (e) => e.type);
75
+ const pendingByPersona = groupBy(pending, (e) => e.cxId);
76
+ const pendingUnresolved = pending.filter((e) => !e.resolvedAt);
77
+
78
+ const personas = ['sre', 'qa', 'security', 'docs-keeper', 'engineer'];
79
+ const costByPersona = {};
80
+ for (const p of personas) {
81
+ const s = getDailySpend({ personaId: p });
82
+ costByPersona[p] = { spent: s.costUsd, cap: personaBudget(p), invocations: s.invocations };
83
+ }
84
+ const total = getTotalDailySpend();
85
+
86
+ const lines = [];
87
+ lines.push(`# Construct-on-Construct measurement — window: ${windowArg}`);
88
+ lines.push(``);
89
+ lines.push(`Window: ${new Date(since).toISOString()} → ${new Date().toISOString()}`);
90
+ lines.push(``);
91
+ lines.push(`## L0 — Deterministic doctor actions`);
92
+ lines.push(``);
93
+ lines.push(`Total actions: ${summary.actions.total}`);
94
+ lines.push('```');
95
+ lines.push(fmtTable('By watcher', summary.actions.byWatcher).trim());
96
+ lines.push(fmtTable('By action', summary.actions.byAction).trim());
97
+ lines.push(fmtTable('Recoveries (services that came back)', summary.recoveries.byWatcher).trim());
98
+ lines.push(fmtTable('Errors (watcher tick failures)', summary.errors.byWatcher).trim());
99
+ lines.push('```');
100
+ lines.push(``);
101
+ lines.push(`## L0 → L1 escalations`);
102
+ lines.push(``);
103
+ lines.push(`Total escalations: ${summary.escalations.total}`);
104
+ lines.push('```');
105
+ lines.push(fmtTable('By watcher', summary.escalations.byWatcher).trim());
106
+ lines.push(fmtTable('By event type', summary.escalations.byEvent).trim());
107
+ lines.push('```');
108
+ lines.push(``);
109
+ lines.push(`## L1 — Role framework`);
110
+ lines.push(``);
111
+ lines.push(`Events emitted: ${events.length}`);
112
+ lines.push('```');
113
+ lines.push(fmtTable('By event type', eventsByType).trim());
114
+ lines.push('```');
115
+ lines.push(``);
116
+ lines.push(`Pending invocations created: ${pending.length} · unresolved: ${pendingUnresolved.length}`);
117
+ lines.push('```');
118
+ lines.push(fmtTable('By persona', pendingByPersona).trim());
119
+ lines.push('```');
120
+ lines.push(``);
121
+ lines.push(`## Cost (today, ${dayKey()})`);
122
+ lines.push(``);
123
+ lines.push(`Total: $${total.costUsd.toFixed(4)} / $${totalBudget().toFixed(2)} cap · ${total.invocations} invocations`);
124
+ lines.push(``);
125
+ for (const p of personas) {
126
+ const c = costByPersona[p];
127
+ if (c.invocations === 0 && c.spent === 0) continue;
128
+ lines.push(`- cx-${p}: $${c.spent.toFixed(4)} / $${c.cap.toFixed(2)} · ${c.invocations} invocations`);
129
+ }
130
+ lines.push(``);
131
+
132
+ const pricingSources = {};
133
+ for (const e of auditEntries) {
134
+ if (e.kind === 'action' && e.action === 'ledger-sync' && e.context?.sources) {
135
+ for (const [src, n] of Object.entries(e.context.sources)) {
136
+ pricingSources[src] = (pricingSources[src] || 0) + n;
137
+ }
138
+ }
139
+ }
140
+ if (Object.keys(pricingSources).length > 0) {
141
+ const total = Object.values(pricingSources).reduce((a, b) => a + b, 0);
142
+ const staticPct = total > 0 ? Math.round(((pricingSources['estimated:static'] || 0) / total) * 100) : 0;
143
+ lines.push(`Pricing sources: ${Object.entries(pricingSources).map(([s, n]) => `${s}=${n}`).join(', ')}`);
144
+ if (staticPct >= 50) {
145
+ lines.push(`⚠ ${staticPct}% of entries priced from static fallback — telemetry model sync may be inactive; absolute spend is approximate. Run \`construct status\` to check telemetry integration.`);
146
+ }
147
+ lines.push(``);
148
+ }
149
+ lines.push(`## Health verdict`);
150
+ lines.push(``);
151
+ const verdicts = [];
152
+ if (summary.actions.total === 0 && summary.samples.total === 0) {
153
+ verdicts.push('⚠ No audit entries in window — doctor may not be running. Verify with \`construct doctor status\`.');
154
+ } else if (summary.samples.total === 0) {
155
+ verdicts.push('⚠ Actions logged but no samples — watchers may be partially failing. Check `construct doctor logs`.');
156
+ } else {
157
+ verdicts.push('✓ Doctor produced samples and actions in window.');
158
+ }
159
+ if (summary.errors.total > 0) {
160
+ verdicts.push(`⚠ ${summary.errors.total} watcher errors in window — see audit log.`);
161
+ }
162
+ if (pendingUnresolved.length > 0) {
163
+ verdicts.push(`→ ${pendingUnresolved.length} unresolved pending invocations — Construct should dispatch these on next session.`);
164
+ }
165
+ if (total.costUsd / totalBudget() > 0.5) {
166
+ verdicts.push(`⚠ Daily cost is ${Math.round((total.costUsd / totalBudget()) * 100)}% of cap — review burn rate.`);
167
+ }
168
+ for (const v of verdicts) lines.push(`- ${v}`);
169
+ return lines.join('\n');
170
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * lib/doctor/watchers/bd-watch.mjs — periodic bd state poller for handoffs.
3
+ *
4
+ * Every 5 min, polls bd for open issues labeled `next:cx-<role>` where the
5
+ * target persona is onboarded in the role framework. Newly-seen issues are
6
+ * enqueued as pending invocations so session-start surfaces them to
7
+ * Construct. Seen issues are remembered in ~/.cx/bd-watch-seen.json to
8
+ * prevent re-emission while the label remains.
9
+ *
10
+ * Closes a gap in agent-tracker: agent-tracker only catches handoffs that
11
+ * appear in Task result text. bd-watch catches every handoff that exists
12
+ * as a label, regardless of how it got there (manual, ai, automated).
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { homedir } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ import { record } from '../audit.mjs';
21
+ import { listOnboardedPersonas, loadManifest } from '../../roles/manifest.mjs';
22
+
23
+ export const name = 'bd-watch';
24
+ export const intervalMs = 5 * 60 * 1000;
25
+
26
+ const SEEN_PATH = join(homedir(), '.cx', 'bd-watch-seen.json');
27
+ const PENDING_PATH = join(homedir(), '.cx', 'role-pending.jsonl');
28
+
29
+ function ensureDir() {
30
+ const dir = join(homedir(), '.cx');
31
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
32
+ }
33
+
34
+ function loadSeen() {
35
+ if (!existsSync(SEEN_PATH)) return {};
36
+ try { return JSON.parse(readFileSync(SEEN_PATH, 'utf8')); } catch { return {}; }
37
+ }
38
+ function saveSeen(state) {
39
+ ensureDir();
40
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
41
+ const trimmed = {};
42
+ for (const [k, v] of Object.entries(state)) {
43
+ if ((v?.ts || 0) > cutoff) trimmed[k] = v;
44
+ }
45
+ writeFileSync(SEEN_PATH, JSON.stringify(trimmed));
46
+ }
47
+
48
+ function listByLabel(label) {
49
+ const r = spawnSync('bd', ['list', '-l', label, '--status', 'open', '--json'], {
50
+ encoding: 'utf8',
51
+ timeout: 8000,
52
+ });
53
+ if (r.status !== 0) return { ok: false, error: r.stderr || `bd exit ${r.status}` };
54
+ try {
55
+ const parsed = JSON.parse(r.stdout || '[]');
56
+ return { ok: true, issues: Array.isArray(parsed) ? parsed : [] };
57
+ } catch (err) {
58
+ return { ok: false, error: String(err) };
59
+ }
60
+ }
61
+
62
+ export async function tick() {
63
+ const actions = [];
64
+ const notes = [];
65
+ const seen = loadSeen();
66
+ const onboarded = listOnboardedPersonas();
67
+ let totalIssues = 0;
68
+ let newQueued = 0;
69
+
70
+ for (const personaId of onboarded) {
71
+ const label = `next:cx-${personaId}`;
72
+ const result = listByLabel(label);
73
+ if (!result.ok) {
74
+ record({
75
+ kind: 'error',
76
+ watcher: name,
77
+ target: label,
78
+ result: 'failed',
79
+ summary: `bd list -l ${label} failed: ${result.error}`,
80
+ });
81
+ continue;
82
+ }
83
+ totalIssues += result.issues.length;
84
+ for (const issue of result.issues) {
85
+ const key = `${issue.id}|${label}`;
86
+ if (seen[key]) continue;
87
+ seen[key] = { ts: Date.now(), id: issue.id };
88
+ const manifest = loadManifest(personaId);
89
+ const entry = {
90
+ ts: Date.now(),
91
+ personaId,
92
+ cxId: `cx-${personaId}`,
93
+ bdIssueId: issue.id,
94
+ fingerprint: `bd-handoff-${issue.id}-${personaId}`,
95
+ eventType: 'handoff.received',
96
+ summary: `bd ${issue.id} labeled next:cx-${personaId} — ${(issue.title || '').slice(0, 120)}`,
97
+ killSwitchEnv: manifest?.killSwitchEnv || '',
98
+ source: 'bd-watch',
99
+ };
100
+ appendFileSync(PENDING_PATH, JSON.stringify(entry) + '\n');
101
+ record({
102
+ kind: 'action',
103
+ watcher: name,
104
+ action: 'queue-handoff',
105
+ target: `cx-${personaId}`,
106
+ summary: `queued ${issue.id} for cx-${personaId}`,
107
+ context: { bdIssueId: issue.id, label, title: issue.title },
108
+ });
109
+ actions.push({ type: 'queue-handoff', target: `cx-${personaId}`, bdIssueId: issue.id });
110
+ newQueued++;
111
+ }
112
+ }
113
+
114
+ saveSeen(seen);
115
+ notes.push({ personasChecked: onboarded.length, totalIssues, newQueued });
116
+ return { actions, escalations: [], notes };
117
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * lib/doctor/watchers/cost.mjs — daily token-spend monitor.
3
+ *
4
+ * Reads the cost ledger and the existing ~/.cx/session-cost.jsonl. Audit-logs
5
+ * the daily totals so the user has a visible trend; warns when any persona
6
+ * hits 80% of its daily cap. Hard-stop enforcement lives in the gateway —
7
+ * the watcher's role is visibility, not enforcement.
8
+ *
9
+ * Tick: 10 min.
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import { record } from '../audit.mjs';
17
+ import {
18
+ getTotalDailySpend,
19
+ getDailySpend,
20
+ personaBudget,
21
+ totalBudget,
22
+ dayKey,
23
+ recordSpend,
24
+ } from '../../cost-ledger.mjs';
25
+
26
+ export const name = 'cost';
27
+ export const intervalMs = 10 * 60 * 1000;
28
+
29
+ const SESSION_COST_PATH = join(homedir(), '.cx', 'session-cost.jsonl');
30
+ const STATE_PATH = join(homedir(), '.cx', 'cost-watcher-state.json');
31
+
32
+ function loadState() {
33
+ if (!existsSync(STATE_PATH)) return { lastIngestedTsMs: 0 };
34
+ try { return JSON.parse(readFileSync(STATE_PATH, 'utf8')); } catch { return { lastIngestedTsMs: 0 }; }
35
+ }
36
+ function saveState(state) {
37
+ const dir = join(homedir(), '.cx');
38
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
39
+ writeFileSync(STATE_PATH, JSON.stringify(state));
40
+ }
41
+
42
+ function ingestSessionCosts() {
43
+ if (!existsSync(SESSION_COST_PATH)) return { ingested: 0, sources: {} };
44
+ const lines = readFileSync(SESSION_COST_PATH, 'utf8').split('\n').filter(Boolean);
45
+ if (lines.length === 0) return { ingested: 0, sources: {} };
46
+ const state = loadState();
47
+ const cutoffMs = state.lastIngestedTsMs || 0;
48
+ let ingested = 0;
49
+ let maxTsMs = cutoffMs;
50
+ const sources = {};
51
+ for (const line of lines) {
52
+ let entry;
53
+ try { entry = JSON.parse(line); } catch { continue; }
54
+ const tsMs = entry.ts ? Date.parse(entry.ts) : 0;
55
+ if (!Number.isFinite(tsMs) || tsMs <= cutoffMs) continue;
56
+ const tokens = (entry.total_tokens) || ((entry.input_tokens || 0) + (entry.output_tokens || 0)) || (entry.tokens?.input || 0) + (entry.tokens?.output || 0);
57
+ const costUsd = entry.cost_usd ?? entry.costUsd ?? entry.cost ?? 0;
58
+ const rawAgent = entry.agent || entry.persona || entry.personaId || 'unknown';
59
+ const personaId = String(rawAgent).replace(/^cx-/, '');
60
+ const source = entry.cost_source || 'unknown';
61
+ if (tokens > 0 || costUsd > 0) {
62
+ recordSpend({ personaId, tokens, costUsd, sessionId: entry.task_key || entry.sessionId || '', ts: tsMs });
63
+ ingested++;
64
+ sources[source] = (sources[source] || 0) + 1;
65
+ }
66
+ if (tsMs > maxTsMs) maxTsMs = tsMs;
67
+ }
68
+ if (maxTsMs > cutoffMs) saveState({ lastIngestedTsMs: maxTsMs });
69
+ return { ingested, sources };
70
+ }
71
+
72
+ export async function tick() {
73
+ const actions = [];
74
+ const notes = [];
75
+
76
+ const ingestResult = ingestSessionCosts();
77
+ if (ingestResult.ingested > 0) {
78
+ actions.push({ type: 'ledger-sync', count: ingestResult.ingested, sources: ingestResult.sources });
79
+ const sourcesStr = Object.entries(ingestResult.sources).map(([s, n]) => `${s}=${n}`).join(', ');
80
+ record({
81
+ kind: 'action',
82
+ watcher: name,
83
+ action: 'ledger-sync',
84
+ target: 'session-cost.jsonl',
85
+ summary: `ingested ${ingestResult.ingested} entries — sources: ${sourcesStr}`,
86
+ context: ingestResult,
87
+ });
88
+ }
89
+
90
+ const total = getTotalDailySpend();
91
+ const totalCap = totalBudget();
92
+ notes.push({ dayKey: dayKey(), totalSpentUsd: total.costUsd, totalCap, invocations: total.invocations, sources: ingestResult.sources });
93
+
94
+ record({
95
+ kind: 'sample',
96
+ watcher: name,
97
+ summary: `${dayKey()} total: $${total.costUsd.toFixed(4)} / $${totalCap.toFixed(2)} (${total.invocations} invocations)`,
98
+ context: { total, totalCap, ingestedFromSessionCost: ingestResult.ingested, sources: ingestResult.sources },
99
+ });
100
+
101
+ const warnAt = 0.8;
102
+ if (totalCap > 0 && total.costUsd / totalCap >= warnAt) {
103
+ record({
104
+ kind: 'warn',
105
+ watcher: name,
106
+ action: 'budget-warning',
107
+ target: 'total',
108
+ summary: `Daily total spend at ${Math.round((total.costUsd / totalCap) * 100)}% of $${totalCap.toFixed(2)} cap`,
109
+ context: { total, totalCap },
110
+ });
111
+ }
112
+
113
+ const onboarded = ['sre', 'qa', 'security', 'docs-keeper'];
114
+ for (const personaId of onboarded) {
115
+ const spend = getDailySpend({ personaId });
116
+ const cap = personaBudget(personaId);
117
+ if (cap > 0 && spend.costUsd / cap >= warnAt) {
118
+ record({
119
+ kind: 'warn',
120
+ watcher: name,
121
+ action: 'budget-warning',
122
+ target: `cx-${personaId}`,
123
+ summary: `cx-${personaId} at ${Math.round((spend.costUsd / cap) * 100)}% of $${cap.toFixed(2)} daily cap`,
124
+ context: { spend, cap },
125
+ });
126
+ }
127
+ }
128
+
129
+ return { actions, escalations: [], notes };
130
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * lib/doctor/watchers/disk.mjs — disk-space monitor + log rotation.
3
+ *
4
+ * Watches free space on the partition holding ~/.cx and rotates the doctor's
5
+ * own JSONL artifacts (events.jsonl, doctor-log.jsonl, role-pending.jsonl,
6
+ * agent-log.jsonl, session-cost.jsonl). When free space falls below the
7
+ * watermark, escalates `service.down` so cx-sre can triage.
8
+ *
9
+ * Tick: 5 min. Cheap — only reads file sizes + statfs.
10
+ */
11
+
12
+ import { existsSync, statSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
13
+ import { spawnSync } from 'node:child_process';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+
17
+ import { record } from '../audit.mjs';
18
+ import { escalate } from '../escalate.mjs';
19
+
20
+ export const name = 'disk';
21
+ export const intervalMs = 5 * 60 * 1000;
22
+
23
+ const TARGETS = [
24
+ { path: '.cx/events.jsonl', maxLines: 1000 },
25
+ { path: '.cx/doctor-log.jsonl', maxLines: 2000 },
26
+ { path: '.cx/role-pending.jsonl', maxLines: 500 },
27
+ { path: '.cx/agent-log.jsonl', maxLines: 1000 },
28
+ { path: '.cx/session-cost.jsonl', maxLines: 1000 },
29
+ { path: '.cx/hook-failures.jsonl', maxLines: 500 },
30
+ ];
31
+
32
+ function freeBytesAt(p) {
33
+ const r = spawnSync('df', ['-k', p], { encoding: 'utf8' });
34
+ if (r.status !== 0) return null;
35
+ const lines = r.stdout.split('\n').filter(Boolean);
36
+ if (lines.length < 2) return null;
37
+ const cols = lines[1].split(/\s+/);
38
+ const freeKb = parseInt(cols[3] || '0', 10);
39
+ return Number.isFinite(freeKb) ? freeKb * 1024 : null;
40
+ }
41
+
42
+ function rotateTarget({ path: relPath, maxLines }) {
43
+ const fullPath = join(homedir(), relPath);
44
+ if (!existsSync(fullPath)) return { rotated: false };
45
+ try {
46
+ const raw = readFileSync(fullPath, 'utf8').split('\n').filter(Boolean);
47
+ if (raw.length <= maxLines) return { rotated: false, lines: raw.length };
48
+ const trimmed = raw.slice(raw.length - maxLines).join('\n') + '\n';
49
+ writeFileSync(fullPath, trimmed);
50
+ return { rotated: true, droppedLines: raw.length - maxLines };
51
+ } catch (err) {
52
+ return { rotated: false, error: String(err) };
53
+ }
54
+ }
55
+
56
+ function pruneRuntimeLogs() {
57
+ const runtimeDir = join(homedir(), '.construct', '.runtime');
58
+ if (!existsSync(runtimeDir)) return { pruned: 0 };
59
+ const maxAgeMs = 7 * 24 * 60 * 60 * 1000;
60
+ const now = Date.now();
61
+ let pruned = 0;
62
+ try {
63
+ for (const name of readdirSync(runtimeDir)) {
64
+ const p = join(runtimeDir, name);
65
+ try {
66
+ const stat = statSync(p);
67
+ if (stat.isFile() && (now - stat.mtimeMs) > maxAgeMs) {
68
+ unlinkSync(p);
69
+ pruned++;
70
+ }
71
+ } catch { /* skip */ }
72
+ }
73
+ } catch { /* runtime dir disappeared */ }
74
+ return { pruned };
75
+ }
76
+
77
+ export async function tick() {
78
+ const actions = [];
79
+ const escalations = [];
80
+
81
+ for (const target of TARGETS) {
82
+ const r = rotateTarget(target);
83
+ if (r.rotated) {
84
+ record({
85
+ kind: 'action',
86
+ watcher: name,
87
+ action: 'rotate',
88
+ target: target.path,
89
+ summary: `rotated ${target.path}: dropped ${r.droppedLines} lines`,
90
+ context: { droppedLines: r.droppedLines, keptLines: target.maxLines },
91
+ });
92
+ actions.push({ type: 'rotate', target: target.path });
93
+ }
94
+ }
95
+
96
+ const runtimePrune = pruneRuntimeLogs();
97
+ if (runtimePrune.pruned > 0) {
98
+ record({
99
+ kind: 'action',
100
+ watcher: name,
101
+ action: 'prune',
102
+ target: '.construct/.runtime/',
103
+ summary: `pruned ${runtimePrune.pruned} stale runtime log(s) (>7 days)`,
104
+ context: runtimePrune,
105
+ });
106
+ actions.push({ type: 'prune', target: '.construct/.runtime/' });
107
+ }
108
+
109
+ const free = freeBytesAt(homedir());
110
+ const lowWaterBytes = 500 * 1024 * 1024;
111
+ if (free !== null && free < lowWaterBytes) {
112
+ const r = await escalate({
113
+ watcher: name,
114
+ eventType: 'service.down',
115
+ summary: `Free disk below 500MB on $HOME (${Math.round(free / 1024 / 1024)}MB remaining)`,
116
+ context: { freeBytes: free, lowWaterBytes },
117
+ });
118
+ escalations.push({ eventType: 'service.down', result: r });
119
+ }
120
+
121
+ return { actions, escalations, notes: [{ freeBytes: free, rotated: actions.filter((a) => a.type === 'rotate').length }] };
122
+ }