@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
@@ -0,0 +1,207 @@
1
+ /**
2
+ * lib/overrides/resolver.mjs — single override + backup helper for every
3
+ * user-customizable Construct primitive (personas, skills, rules,
4
+ * contracts, role manifests).
5
+ *
6
+ * Convention:
7
+ * - Original lives in the install (`personas/<n>.md`, `skills/.../<n>.md`, …)
8
+ * - User override lives at `.cx/<category>/<n>.<ext>` — gitignored
9
+ * - Each edit snapshots the prior content to
10
+ * `.cx/backups/<category>/<n>.<iso>.<ext>` — gitignored, 60-day
11
+ * auto-prune
12
+ *
13
+ * Override semantics: full file replacement (decided 2026-05-14). No
14
+ * section-merge. Means sync() reads the override file verbatim if
15
+ * present; otherwise reads the original.
16
+ *
17
+ * Backups: each call to `applyEdit` writes the prior content (if any)
18
+ * to `.cx/backups/<category>/<n>.<iso>.<ext>` before the new content
19
+ * lands. `restoreFromBackup` is the inverse.
20
+ *
21
+ * `pruneBackups(maxDays=60)` deletes backup files older than the cap.
22
+ * Idempotent — safe to call from a cron, post-merge hook, or doctor
23
+ * --fix.
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+
28
+ const DEFAULT_BACKUP_MAX_DAYS = 60;
29
+
30
+ export const SUPPORTED_CATEGORIES = Object.freeze([
31
+ 'personas',
32
+ 'agents',
33
+ 'skills',
34
+ 'rules',
35
+ 'contracts',
36
+ 'role-manifests',
37
+ ]);
38
+
39
+ const CATEGORY_RULES = {
40
+ personas: { originalDir: 'personas', defaultExt: '.md' },
41
+ agents: { originalDir: 'agents/prompts', defaultExt: '.md' },
42
+ skills: { originalDir: 'skills', defaultExt: '.md' },
43
+ rules: { originalDir: 'rules', defaultExt: '.md' },
44
+ contracts: { originalDir: 'agents', defaultExt: '.json', singleFile: 'contracts.json' },
45
+ 'role-manifests': { originalDir: 'agents', defaultExt: '.json', singleFile: 'role-manifests.json' },
46
+ };
47
+
48
+ function categoryRule(category) {
49
+ const rule = CATEGORY_RULES[category];
50
+ if (!rule) throw new Error(`unknown override category: ${category}`);
51
+ return rule;
52
+ }
53
+
54
+ function isoStamp() {
55
+ return new Date().toISOString().replace(/[:.]/g, '-');
56
+ }
57
+
58
+ function ensureCxIgnore(projectRoot) {
59
+ const cxDir = path.join(projectRoot, '.cx');
60
+ fs.mkdirSync(cxDir, { recursive: true });
61
+ const gitignorePath = path.join(cxDir, '.gitignore');
62
+ const want = 'backups/\n';
63
+ let existing = '';
64
+ try { existing = fs.readFileSync(gitignorePath, 'utf8'); } catch { /* fresh */ }
65
+ if (!existing.split(/\r?\n/).map((l) => l.trim()).includes('backups/')) {
66
+ fs.writeFileSync(gitignorePath, existing + (existing.endsWith('\n') || !existing ? '' : '\n') + want);
67
+ }
68
+ }
69
+
70
+ function originalPath(projectRoot, category, name) {
71
+ const rule = categoryRule(category);
72
+ if (rule.singleFile) {
73
+ return path.join(projectRoot, rule.originalDir, rule.singleFile);
74
+ }
75
+ const ext = name.includes('.') ? '' : rule.defaultExt;
76
+ return path.join(projectRoot, rule.originalDir, `${name}${ext}`);
77
+ }
78
+
79
+ function overridePath(projectRoot, category, name) {
80
+ const rule = categoryRule(category);
81
+ if (rule.singleFile) {
82
+ return path.join(projectRoot, '.cx', category, rule.singleFile);
83
+ }
84
+ const ext = name.includes('.') ? '' : rule.defaultExt;
85
+ return path.join(projectRoot, '.cx', category, `${name}${ext}`);
86
+ }
87
+
88
+ function backupDir(projectRoot, category) {
89
+ return path.join(projectRoot, '.cx', 'backups', category);
90
+ }
91
+
92
+ function backupName(name, ext) {
93
+ return `${name}.${isoStamp()}${ext}`;
94
+ }
95
+
96
+ function readIfExists(filePath) {
97
+ try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
98
+ }
99
+
100
+ export function resolveOverride(projectRoot, category, name) {
101
+ const orig = originalPath(projectRoot, category, name);
102
+ const over = overridePath(projectRoot, category, name);
103
+ if (fs.existsSync(over)) {
104
+ return { path: over, source: 'override', overrideExists: true, originalPath: orig };
105
+ }
106
+ if (fs.existsSync(orig)) {
107
+ return { path: orig, source: 'original', overrideExists: false, originalPath: orig };
108
+ }
109
+ return { path: null, source: 'missing', overrideExists: false, originalPath: orig };
110
+ }
111
+
112
+ export function readResolved(projectRoot, category, name) {
113
+ const r = resolveOverride(projectRoot, category, name);
114
+ if (!r.path) return { content: null, source: 'missing' };
115
+ return { content: readIfExists(r.path), source: r.source };
116
+ }
117
+
118
+ export function applyEdit(projectRoot, category, name, newContent) {
119
+ if (typeof newContent !== 'string') throw new Error('newContent must be a string');
120
+ const over = overridePath(projectRoot, category, name);
121
+ const rule = categoryRule(category);
122
+ const ext = name.includes('.') ? path.extname(name) : rule.defaultExt;
123
+ ensureCxIgnore(projectRoot);
124
+ fs.mkdirSync(path.dirname(over), { recursive: true });
125
+
126
+ const priorContent = fs.existsSync(over)
127
+ ? readIfExists(over)
128
+ : readIfExists(originalPath(projectRoot, category, name));
129
+
130
+ let backupPath = null;
131
+ if (priorContent !== null && priorContent !== newContent) {
132
+ const dir = backupDir(projectRoot, category);
133
+ fs.mkdirSync(dir, { recursive: true });
134
+ backupPath = path.join(dir, backupName(name, ext));
135
+ fs.writeFileSync(backupPath, priorContent);
136
+ }
137
+
138
+ fs.writeFileSync(over, newContent);
139
+ return { overridePath: over, backupPath, wrote: newContent.length };
140
+ }
141
+
142
+ export function listBackups(projectRoot, category, name) {
143
+ const dir = backupDir(projectRoot, category);
144
+ if (!fs.existsSync(dir)) return [];
145
+ const prefix = `${name}.`;
146
+ const entries = fs.readdirSync(dir)
147
+ .filter((f) => f.startsWith(prefix))
148
+ .map((f) => {
149
+ const full = path.join(dir, f);
150
+ const stat = fs.statSync(full);
151
+ return { filename: f, path: full, mtimeMs: stat.mtimeMs, size: stat.size };
152
+ })
153
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
154
+ return entries;
155
+ }
156
+
157
+ export function restoreFromBackup(projectRoot, category, name, backupFilename) {
158
+ const dir = backupDir(projectRoot, category);
159
+ const src = path.join(dir, backupFilename);
160
+ if (!fs.existsSync(src)) throw new Error(`backup not found: ${src}`);
161
+ const content = fs.readFileSync(src, 'utf8');
162
+ return applyEdit(projectRoot, category, name, content);
163
+ }
164
+
165
+ export function pruneBackups(projectRoot, { maxDays = DEFAULT_BACKUP_MAX_DAYS, now = Date.now() } = {}) {
166
+ const root = path.join(projectRoot, '.cx', 'backups');
167
+ if (!fs.existsSync(root)) return { pruned: [], kept: 0 };
168
+ const cutoff = now - maxDays * 24 * 60 * 60 * 1000;
169
+ const pruned = [];
170
+ let kept = 0;
171
+ for (const category of fs.readdirSync(root)) {
172
+ const dir = path.join(root, category);
173
+ if (!fs.statSync(dir).isDirectory()) continue;
174
+ for (const entry of fs.readdirSync(dir)) {
175
+ const full = path.join(dir, entry);
176
+ const stat = fs.statSync(full);
177
+ if (stat.mtimeMs < cutoff) {
178
+ fs.unlinkSync(full);
179
+ pruned.push(full);
180
+ } else {
181
+ kept += 1;
182
+ }
183
+ }
184
+ }
185
+ return { pruned, kept };
186
+ }
187
+
188
+ export function describeOverrides(projectRoot) {
189
+ const summary = {};
190
+ for (const category of SUPPORTED_CATEGORIES) {
191
+ const dir = path.join(projectRoot, '.cx', category);
192
+ if (!fs.existsSync(dir)) { summary[category] = []; continue; }
193
+ const entries = [];
194
+ const walk = (rel = '') => {
195
+ const cur = path.join(dir, rel);
196
+ for (const name of fs.readdirSync(cur)) {
197
+ const full = path.join(cur, name);
198
+ const relPath = path.join(rel, name);
199
+ if (fs.statSync(full).isDirectory()) { walk(relPath); continue; }
200
+ entries.push(relPath);
201
+ }
202
+ };
203
+ walk();
204
+ summary[category] = entries;
205
+ }
206
+ return summary;
207
+ }
package/lib/parity.mjs ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * lib/parity.mjs — Cross-surface parity verifier.
3
+ *
4
+ * After `construct sync` writes adapters to multiple surfaces (Claude Code,
5
+ * OpenCode, Codex), the module diffs each surface's actual state against the
6
+ * canonical `agents/registry.json`. Backs `construct doctor` for surfacing
7
+ * silent divergence — for instance, an agent added to the registry that
8
+ * never made it to OpenCode because of a sync regression.
9
+ *
10
+ * Each surface check is independent. A surface that is not installed (no
11
+ * config dir, no agents dir) reports `status: 'absent'` rather than
12
+ * generating a false-negative parity error. Surfaces explicitly opt out per
13
+ * entry via `entry.platforms` (an allowlist, when present); entries without
14
+ * the field are mirrored everywhere.
15
+ *
16
+ * Surfaces checked:
17
+ * claude ~/.claude/agents/*.md
18
+ * opencode ~/.config/opencode/opencode.json (agent table)
19
+ * codex ~/.codex/agents/*.toml
20
+ */
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import os from 'node:os';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
28
+ const ROOT_DIR = path.resolve(MODULE_DIR, '..');
29
+
30
+ function loadRegistry(rootDir = ROOT_DIR) {
31
+ const file = path.join(rootDir, 'agents', 'registry.json');
32
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
33
+ }
34
+
35
+ function adapterName(entry, prefix) {
36
+ return entry.isPersona ? entry.name : `${prefix}-${entry.name}`;
37
+ }
38
+
39
+ function entriesForSurface(registry, surface) {
40
+ const prefix = registry.prefix || 'cx';
41
+ const entries = [
42
+ ...(registry.personas || []).map((p) => ({ ...p, isPersona: true })),
43
+ ...(registry.agents || []).map((a) => ({ ...a, isPersona: false })),
44
+ ];
45
+
46
+ return entries
47
+ .filter((e) => {
48
+ if (!Array.isArray(e.platforms)) return true;
49
+ return e.platforms.includes(surface);
50
+ })
51
+ .map((e) => adapterName(e, prefix));
52
+ }
53
+
54
+ function diffSets(expected, actual) {
55
+ const expectedSet = new Set(expected);
56
+ const actualSet = new Set(actual);
57
+ const missing = expected.filter((n) => !actualSet.has(n));
58
+ const extra = actual.filter((n) => !expectedSet.has(n));
59
+ return { missing, extra };
60
+ }
61
+
62
+ function checkClaude(registry, { homeDir = os.homedir() } = {}) {
63
+ const dir = path.join(homeDir, '.claude', 'agents');
64
+ if (!fs.existsSync(dir)) return { surface: 'claude', status: 'absent', dir };
65
+ const expected = entriesForSurface(registry, 'claude');
66
+ const actual = fs.readdirSync(dir)
67
+ .filter((name) => name.endsWith('.md'))
68
+ .map((name) => name.replace(/\.md$/, ''));
69
+ const { missing, extra } = diffSets(expected, actual);
70
+ return {
71
+ surface: 'claude',
72
+ status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
73
+ dir,
74
+ expectedCount: expected.length,
75
+ actualCount: actual.length,
76
+ missing,
77
+ extra,
78
+ };
79
+ }
80
+
81
+ function checkOpenCode(registry, { homeDir = os.homedir() } = {}) {
82
+ const file = path.join(homeDir, '.config', 'opencode', 'opencode.json');
83
+ if (!fs.existsSync(file)) return { surface: 'opencode', status: 'absent', file };
84
+ let config;
85
+ try {
86
+ config = JSON.parse(fs.readFileSync(file, 'utf8'));
87
+ } catch (err) {
88
+ return { surface: 'opencode', status: 'unreadable', file, error: err.message };
89
+ }
90
+ const expected = entriesForSurface(registry, 'opencode');
91
+ const actual = Object.keys(config.agent || config.agents || {});
92
+ const { missing, extra } = diffSets(expected, actual);
93
+ return {
94
+ surface: 'opencode',
95
+ status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
96
+ file,
97
+ expectedCount: expected.length,
98
+ actualCount: actual.length,
99
+ missing,
100
+ extra,
101
+ };
102
+ }
103
+
104
+ function checkCodex(registry, { homeDir = os.homedir() } = {}) {
105
+ const dir = path.join(homeDir, '.codex', 'agents');
106
+ if (!fs.existsSync(dir)) return { surface: 'codex', status: 'absent', dir };
107
+ const expected = entriesForSurface(registry, 'codex');
108
+ const actual = fs.readdirSync(dir)
109
+ .filter((name) => name.endsWith('.toml'))
110
+ .map((name) => name.replace(/\.toml$/, ''));
111
+ const { missing, extra } = diffSets(expected, actual);
112
+ return {
113
+ surface: 'codex',
114
+ status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
115
+ dir,
116
+ expectedCount: expected.length,
117
+ actualCount: actual.length,
118
+ missing,
119
+ extra,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Run parity checks across every supported surface. Never throws — returns a
125
+ * structured report so callers can render it however they like.
126
+ */
127
+ export function checkParity({ rootDir = ROOT_DIR, homeDir = os.homedir() } = {}) {
128
+ const registry = loadRegistry(rootDir);
129
+ const surfaces = [
130
+ checkClaude(registry, { homeDir }),
131
+ checkOpenCode(registry, { homeDir }),
132
+ checkCodex(registry, { homeDir }),
133
+ ];
134
+
135
+ const ok = surfaces.every((s) => s.status === 'ok' || s.status === 'absent');
136
+ const summary = surfaces.map((s) => {
137
+ if (s.status === 'absent') return `${s.surface}: not installed`;
138
+ if (s.status === 'unreadable') return `${s.surface}: unreadable (${s.error})`;
139
+ if (s.status === 'ok') return `${s.surface}: ok (${s.actualCount}/${s.expectedCount})`;
140
+ const parts = [];
141
+ if (s.missing.length) parts.push(`missing: ${s.missing.join(', ')}`);
142
+ if (s.extra.length) parts.push(`extra: ${s.extra.join(', ')}`);
143
+ return `${s.surface}: drift — ${parts.join(' · ')}`;
144
+ });
145
+
146
+ return { ok, surfaces, summary };
147
+ }
package/lib/paths.mjs ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * lib/paths.mjs — cross-platform resolution for Construct's home-relative directories.
3
+ *
4
+ * The codebase historically scattered `process.env.HOME + '/.cx'` and similar
5
+ * patterns. Those break on Windows where `HOME` is unset and `USERPROFILE`
6
+ * holds the home path. Use `homeDir()`, `cxDir()`, and `constructDir()`
7
+ * instead — they consult `os.homedir()` (which respects every OS's user
8
+ * directory convention) and join with `path.join` so separators work on any
9
+ * platform.
10
+ *
11
+ * The `--toolkit-dir` style env override (`CX_TOOLKIT_DIR`) takes precedence
12
+ * over `~/.construct/` so operators with non-default install layouts can
13
+ * point Construct at the right tree.
14
+ */
15
+
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+
19
+ export function homeDir() {
20
+ return process.env.CX_HOME_OVERRIDE || os.homedir() || process.env.HOME || process.env.USERPROFILE || '';
21
+ }
22
+
23
+ export function cxDir() {
24
+ return path.join(homeDir(), '.cx');
25
+ }
26
+
27
+ export function constructDir() {
28
+ return process.env.CX_TOOLKIT_DIR || path.join(homeDir(), '.construct');
29
+ }
30
+
31
+ export function hookPath(name) {
32
+ return path.join(constructDir(), 'lib', 'hooks', `${name}.mjs`);
33
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * lib/performance/generate.mjs — end-of-session performance review writer.
3
+ *
4
+ * Reads two sources for the trailing 7-day window:
5
+ *
6
+ * 1. `~/.cx/session-cost.jsonl` — appended by the Stop hook.
7
+ * Per-call entries with model, tokens, cost, agent.
8
+ * 2. Telemetry `/api/public/observations?type=GENERATION` — generation
9
+ * observations with latency, completionStartTime, errors,
10
+ * metadata.agent. Optional — review degrades when telemetry
11
+ * isn't reachable.
12
+ *
13
+ * Aggregates per agent: invocations, avgScore (placeholder = 1 -
14
+ * failureRate until telemetry scores are wired), failureRate, p50/p95
15
+ * latency, totalCostUsd, trend (compares this window to the prior
16
+ * 7-day window — 'improving' / 'stable' / 'declining' / 'unknown'),
17
+ * status ('healthy' if failureRate < 5% and avgScore >= 0.6;
18
+ * 'needs_attention' if failureRate 5–20% or avgScore 0.4–0.6;
19
+ * 'failing' if worse).
20
+ *
21
+ * Writes the result to
22
+ * `~/.cx/performance-reviews/review-<iso>.json` (no test- prefix,
23
+ * no -mock suffix, so the dashboard surfaces it as a real review).
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+
29
+ const WINDOW_DAYS = 7;
30
+ const REVIEW_DIR = path.join(os.homedir(), '.cx', 'performance-reviews');
31
+ const COST_LEDGER = path.join(os.homedir(), '.cx', 'session-cost.jsonl');
32
+
33
+ function quantile(values, q) {
34
+ if (!values.length) return null;
35
+ const sorted = [...values].sort((a, b) => a - b);
36
+ const pos = (sorted.length - 1) * q;
37
+ const base = Math.floor(pos);
38
+ const rest = pos - base;
39
+ return sorted[base + 1] !== undefined ? sorted[base] + rest * (sorted[base + 1] - sorted[base]) : sorted[base];
40
+ }
41
+
42
+ function readLedger(cutoffMs) {
43
+ if (!fs.existsSync(COST_LEDGER)) return [];
44
+ const raw = fs.readFileSync(COST_LEDGER, 'utf8');
45
+ const entries = [];
46
+ for (const line of raw.split('\n')) {
47
+ if (!line.trim()) continue;
48
+ try {
49
+ const entry = JSON.parse(line);
50
+ const ts = Date.parse(entry.ts);
51
+ if (Number.isFinite(ts) && ts >= cutoffMs) entries.push({ ...entry, _ts: ts });
52
+ } catch { /* skip malformed */ }
53
+ }
54
+ return entries;
55
+ }
56
+
57
+ async function fetchTelemetryGenerations(env, { sinceMs, limit = 500 } = {}) {
58
+ const key = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
59
+ const secret = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
60
+ if (!key || !secret) return null;
61
+ const baseUrl = (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
62
+ const auth = `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`;
63
+ const controller = new AbortController();
64
+ const timer = setTimeout(() => controller.abort(), 8000);
65
+ try {
66
+ let res = await fetch(
67
+ `${baseUrl}/api/public/observations?type=GENERATION&fromStartTime=${encodeURIComponent(new Date(sinceMs).toISOString())}&limit=${limit}`,
68
+ { headers: { Authorization: auth }, signal: controller.signal },
69
+ );
70
+ if (res.status === 400 || res.status === 422) {
71
+ res = await fetch(
72
+ `${baseUrl}/api/public/observations?type=GENERATION&limit=${limit}`,
73
+ { headers: { Authorization: auth }, signal: controller.signal },
74
+ );
75
+ }
76
+ clearTimeout(timer);
77
+ if (!res.ok) return null;
78
+ const json = await res.json();
79
+ return Array.isArray(json.data) ? json.data : [];
80
+ } catch {
81
+ clearTimeout(timer);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function classifyStatus(avgScore, failureRate) {
87
+ if (failureRate >= 0.2 || avgScore < 0.4) return 'failing';
88
+ if (failureRate >= 0.05 || avgScore < 0.6) return 'needs_attention';
89
+ return 'healthy';
90
+ }
91
+
92
+ function classifyTrend(currentScore, priorScore) {
93
+ if (priorScore == null) return 'unknown';
94
+ if (currentScore > priorScore + 0.05) return 'improving';
95
+ if (currentScore < priorScore - 0.05) return 'declining';
96
+ return 'stable';
97
+ }
98
+
99
+ function aggregateAgentStats(ledger, generations) {
100
+ const byAgent = new Map();
101
+ for (const entry of ledger) {
102
+ const agent = entry.agent || 'orchestrator';
103
+ const stat = byAgent.get(agent) || {
104
+ name: agent,
105
+ invocations: 0,
106
+ totalCostUsd: 0,
107
+ latencies: [],
108
+ errors: 0,
109
+ models: new Set(),
110
+ };
111
+ stat.invocations += 1;
112
+ stat.totalCostUsd += Number(entry.cost_usd) || 0;
113
+ if (entry.model) stat.models.add(entry.model);
114
+ byAgent.set(agent, stat);
115
+ }
116
+ if (Array.isArray(generations)) {
117
+ for (const g of generations) {
118
+ const agent =
119
+ g.metadata?.agent ||
120
+ (typeof g.name === 'string' && g.name.startsWith('llm.') ? g.name.slice(4) : null);
121
+ if (!agent || agent === 'chat') continue;
122
+ const stat = byAgent.get(agent) || {
123
+ name: agent,
124
+ invocations: 0,
125
+ totalCostUsd: 0,
126
+ latencies: [],
127
+ errors: 0,
128
+ models: new Set(),
129
+ };
130
+ const start = g.startTime ? Date.parse(g.startTime) : null;
131
+ const end = g.endTime ? Date.parse(g.endTime) : null;
132
+ if (start && end && end > start) stat.latencies.push(end - start);
133
+ if ((g.level || '').toLowerCase() === 'error') stat.errors += 1;
134
+ if (g.model) stat.models.add(g.model);
135
+ byAgent.set(agent, stat);
136
+ }
137
+ }
138
+ return byAgent;
139
+ }
140
+
141
+ export async function generatePerformanceReview({
142
+ windowDays = WINDOW_DAYS,
143
+ now = Date.now(),
144
+ env = process.env,
145
+ } = {}) {
146
+ const cutoff = now - windowDays * 24 * 60 * 60 * 1000;
147
+ const priorCutoff = now - windowDays * 2 * 24 * 60 * 60 * 1000;
148
+ const ledger = readLedger(cutoff);
149
+ const priorLedger = readLedger(priorCutoff).filter((e) => e._ts < cutoff);
150
+ const generations = await fetchTelemetryGenerations(env, { sinceMs: cutoff });
151
+ const currentByAgent = aggregateAgentStats(ledger, generations || []);
152
+ const priorByAgent = aggregateAgentStats(priorLedger, []);
153
+ const agentStats = [];
154
+ for (const [name, s] of currentByAgent) {
155
+ const failureRate = s.invocations > 0 ? s.errors / s.invocations : 0;
156
+ const avgScore = Math.max(0, Math.min(1, 1 - failureRate));
157
+ const priorStats = priorByAgent.get(name);
158
+ const priorScore = priorStats && priorStats.invocations > 0
159
+ ? Math.max(0, Math.min(1, 1 - priorStats.errors / priorStats.invocations))
160
+ : null;
161
+ const status = classifyStatus(avgScore, failureRate);
162
+ const trend = classifyTrend(avgScore, priorScore);
163
+ agentStats.push({
164
+ name,
165
+ invocations: s.invocations,
166
+ scoredInvocations: s.invocations,
167
+ avgScore: Number(avgScore.toFixed(2)),
168
+ failureRate: Number(failureRate.toFixed(3)),
169
+ status,
170
+ trend,
171
+ totalCostUsd: Number(s.totalCostUsd.toFixed(4)),
172
+ latencyMs: s.latencies.length
173
+ ? { p50: Math.round(quantile(s.latencies, 0.5)), p95: Math.round(quantile(s.latencies, 0.95)) }
174
+ : null,
175
+ models: [...s.models],
176
+ reasoning: failureRate > 0
177
+ ? `${s.errors}/${s.invocations} calls errored over the window`
178
+ : `${s.invocations} calls, no errors detected`,
179
+ });
180
+ }
181
+ agentStats.sort((a, b) => b.invocations - a.invocations);
182
+ const generatedAt = new Date(now).toISOString();
183
+ return {
184
+ generated: generatedAt,
185
+ period: {
186
+ from: new Date(cutoff).toISOString(),
187
+ to: generatedAt,
188
+ days: windowDays,
189
+ },
190
+ sources: {
191
+ costLedgerEntries: ledger.length,
192
+ telemetryGenerations: Array.isArray(generations) ? generations.length : null,
193
+ telemetryReachable: generations !== null,
194
+ },
195
+ agentStats,
196
+ };
197
+ }
198
+
199
+ export function writeReview(report, { dir = REVIEW_DIR, now = Date.now() } = {}) {
200
+ fs.mkdirSync(dir, { recursive: true });
201
+ const stamp = new Date(now).toISOString().replace(/[:.]/g, '-');
202
+ const filename = `review-${stamp}.json`;
203
+ const full = path.join(dir, filename);
204
+ fs.writeFileSync(full, JSON.stringify(report, null, 2));
205
+ return { filename, path: full };
206
+ }
207
+
208
+ export async function runGenerator({ env = process.env, now = Date.now() } = {}) {
209
+ const report = await generatePerformanceReview({ env, now });
210
+ const written = writeReview(report, { now });
211
+ return { ...written, agentCount: report.agentStats.length };
212
+ }