@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,62 @@
1
+ /**
2
+ * lib/telemetry/backends/local.mjs — Local JSONL trace backend.
3
+ *
4
+ * Reads trace events from ~/.cx/traces/*.jsonl files.
5
+ * No remote calls. Always available. Used when CONSTRUCT_TRACE_BACKEND=local.
6
+ */
7
+
8
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+
12
+ export const name = 'local';
13
+
14
+ const TRACES_DIR = join(homedir(), '.cx', 'traces');
15
+
16
+ export async function isAvailable() {
17
+ return true;
18
+ }
19
+
20
+ /**
21
+ * Fetch traces from local JSONL files within a time window.
22
+ *
23
+ * @param {string} teamId - The overlay/team ID to filter by.
24
+ * @param {number} windowMs - Lookback window in milliseconds.
25
+ * @returns {Promise<object[]>}
26
+ */
27
+ export async function listTraces(teamId, windowMs) {
28
+ if (!existsSync(TRACES_DIR)) return [];
29
+
30
+ const since = Date.now() - windowMs;
31
+ const files = readdirSync(TRACES_DIR)
32
+ .filter((f) => f.endsWith('.jsonl'))
33
+ .sort()
34
+ .reverse()
35
+ .slice(0, 7); // last 7 days of files
36
+
37
+ const traces = [];
38
+ for (const file of files) {
39
+ const lines = readFileSync(join(TRACES_DIR, file), 'utf8')
40
+ .split('\n')
41
+ .filter(Boolean);
42
+ for (const line of lines) {
43
+ try {
44
+ const event = JSON.parse(line);
45
+ if (new Date(event.createdAt).getTime() < since) continue;
46
+ if (teamId && event.project && event.project !== teamId) continue;
47
+ traces.push({
48
+ id: event.traceId ?? event.spanId,
49
+ teamId: event.project ?? teamId,
50
+ agentName: event.role ?? event.eventType ?? 'unknown',
51
+ status: 'local',
52
+ latencyMs: null,
53
+ qualityScore: null,
54
+ createdAt: event.createdAt,
55
+ blockers: [],
56
+ handoffs: 0,
57
+ });
58
+ } catch { /* skip malformed lines */ }
59
+ }
60
+ }
61
+ return traces;
62
+ }
@@ -1,28 +1,38 @@
1
1
  /**
2
- * lib/telemetry/backends/langfuse.mjs — Langfuse trace backend adapter.
2
+ * lib/telemetry/backends/remote.mjs — Remote telemetry backend adapter.
3
3
  *
4
- * Reads LANGFUSE_BASEURL, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY from env.
4
+ * Reads CONSTRUCT_TELEMETRY_URL, CONSTRUCT_TELEMETRY_PUBLIC_KEY,
5
+ * CONSTRUCT_TELEMETRY_SECRET_KEY from env.
5
6
  * Implements the TraceBackend interface: listTraces(teamId, windowMs) → Trace[].
7
+ *
8
+ * The HTTP protocol is compatible with open trace ingestion APIs.
9
+ * Point CONSTRUCT_TELEMETRY_URL at any compatible backend.
6
10
  */
7
11
 
8
- export const name = 'langfuse';
12
+ export const name = 'remote';
9
13
 
10
- function baseHeaders() {
11
- const key = process.env.LANGFUSE_PUBLIC_KEY;
12
- const secret = process.env.LANGFUSE_SECRET_KEY;
13
- if (!key || !secret) throw new Error('LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set.');
14
+ export function telemetryHeaders(env = process.env) {
15
+ const key = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
16
+ const secret = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
17
+ if (!key || !secret)
18
+ throw new Error(
19
+ 'CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY must be set.',
20
+ );
14
21
  return {
15
22
  Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
16
23
  'Content-Type': 'application/json',
17
24
  };
18
25
  }
19
26
 
20
- function baseUrl() {
21
- return (process.env.LANGFUSE_BASEURL ?? 'https://cloud.langfuse.com').replace(/\/$/, '');
27
+ export function telemetryBaseUrl(env = process.env) {
28
+ return (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
22
29
  }
23
30
 
24
31
  export async function isAvailable() {
25
- return Boolean(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY);
32
+ return Boolean(
33
+ (process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY) &&
34
+ (process.env.CONSTRUCT_TELEMETRY_SECRET_KEY),
35
+ );
26
36
  }
27
37
 
28
38
  /**
@@ -35,12 +45,13 @@ export async function isAvailable() {
35
45
  */
36
46
  export async function listTraces(teamId, windowMs) {
37
47
  const since = new Date(Date.now() - windowMs).toISOString();
38
- const url = `${baseUrl()}/api/public/traces?tags=${encodeURIComponent(teamId)}&fromTimestamp=${since}&limit=100`;
48
+ const base = telemetryBaseUrl();
49
+ const url = `${base}/api/public/traces?tags=${encodeURIComponent(teamId)}&fromTimestamp=${since}&limit=100`;
39
50
 
40
- const resp = await fetch(url, { headers: baseHeaders() });
51
+ const resp = await fetch(url, { headers: telemetryHeaders() });
41
52
  if (!resp.ok) {
42
53
  const body = await resp.text();
43
- throw new Error(`Langfuse API error ${resp.status}: ${body}`);
54
+ throw new Error(`Telemetry backend API error ${resp.status}: ${body}`);
44
55
  }
45
56
  const json = await resp.json();
46
57
 
@@ -49,10 +60,12 @@ export async function listTraces(teamId, windowMs) {
49
60
  teamId,
50
61
  agentName: t.metadata?.agentName ?? t.name ?? 'unknown',
51
62
  status: t.metadata?.status ?? 'unknown',
52
- latencyMs: t.latency != null ? Math.round(t.latency * 1000) : null, // Langfuse returns seconds
63
+ latencyMs: t.latency != null ? Math.round(t.latency * 1000) : null,
53
64
  qualityScore: t.scores?.find((s) => s.name === 'quality')?.value ?? null,
54
65
  createdAt: t.timestamp,
55
66
  blockers: t.metadata?.blockers ?? [],
56
67
  handoffs: t.metadata?.handoffs ?? 0,
57
68
  }));
58
69
  }
70
+
71
+
@@ -0,0 +1,180 @@
1
+ /**
2
+ * lib/telemetry/backfill.mjs — Sparse telemetry trace backfill.
3
+ *
4
+ * Fetches the most recent sparse traces from the telemetry backend and posts a synthetic
5
+ * EVENT observation against each one so it graduates from sparse → partial.
6
+ * Safe to call repeatedly — observations are idempotent (same deterministic ID).
7
+ *
8
+ * Consumed by:
9
+ * • lib/status.mjs — auto-triggered when sparse coverage is detected.
10
+ * • bin/construct — `construct telemetry-backfill` CLI command.
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ import { createHash } from "node:crypto";
14
+ import { createIngestClient } from "./ingest.mjs";
15
+
16
+ const DEFAULT_LIMIT = 10;
17
+ const SPARSE_THRESHOLD = 0.35;
18
+
19
+ /**
20
+ * Derive a stable observation ID from a trace ID so repeated backfills are
21
+ * idempotent from the caller's perspective (de-duped by ID on upsert).
22
+ */
23
+ function stableObservationId(traceId) {
24
+ return createHash("sha256").update(`backfill:${traceId}`).digest("hex").slice(0, 32);
25
+ }
26
+
27
+ function classifySparseTrace(trace) {
28
+ const observationCount = Number(
29
+ trace?.observationCount ?? trace?.observations?.length ?? trace?.spanCount ?? trace?.generationCount ?? 0
30
+ ) || 0;
31
+ const hasInput = trace?.input != null;
32
+ const hasOutput = trace?.output != null;
33
+ const metaKeys = trace?.metadata && typeof trace.metadata === "object"
34
+ ? Object.keys(trace.metadata).length
35
+ : 0;
36
+ const hasRichMetadata = metaKeys >= 5;
37
+ const hasPayload = hasInput || hasOutput || metaKeys > 0;
38
+
39
+ if ((hasInput || hasOutput) && (observationCount >= 1 || hasRichMetadata)) return "rich";
40
+ if (hasPayload || observationCount >= 1) return "partial";
41
+ return "sparse";
42
+ }
43
+
44
+ /**
45
+ * Fetch recent traces from the telemetry backend and return only the sparse ones.
46
+ */
47
+ async function fetchSparseTraces({
48
+ baseUrl,
49
+ headers,
50
+ limit = DEFAULT_LIMIT,
51
+ signal,
52
+ fetchImpl = globalThis.fetch,
53
+ } = {}) {
54
+ const url = `${baseUrl}/api/public/traces?limit=${limit * 3}`;
55
+ const res = await fetchImpl(url, { headers, signal });
56
+ if (!res.ok) throw new Error(`telemetry traces ${res.status}`);
57
+ const json = await res.json().catch(() => ({}));
58
+ const traces = Array.isArray(json.data) ? json.data : [];
59
+ return traces
60
+ .filter((t) => classifySparseTrace(t) === "sparse")
61
+ .slice(0, limit);
62
+ }
63
+
64
+ /**
65
+ * Post a backfill EVENT observation against a trace.
66
+ */
67
+ function buildBackfillObservation(traceId, { healReason = "auto", source = "construct-backfill" } = {}) {
68
+ return {
69
+ id: stableObservationId(traceId),
70
+ traceId,
71
+ type: "EVENT",
72
+ name: "construct.backfill",
73
+ startTime: new Date().toISOString(),
74
+ metadata: {
75
+ heal_reason: healReason,
76
+ source,
77
+ backfilledAt: new Date().toISOString(),
78
+ },
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Backfill sparse traces. Returns a result summary.
84
+ *
85
+ * @param {object} opts
86
+ * @param {string} [opts.baseUrl]
87
+ * @param {string} [opts.publicKey]
88
+ * @param {string} [opts.secretKey]
89
+ * @param {number} [opts.limit]
90
+ * @param {boolean} [opts.bestEffort] - skip individual failures instead of throwing
91
+ * @param {Function} [opts.fetchImpl]
92
+ * @returns {{ backfilled: number, skipped: number, errors: string[] }}
93
+ */
94
+ export async function backfillSparseTraces({
95
+ baseUrl = (process.env.CONSTRUCT_TELEMETRY_URL ?? "").replace(/\/$/, ""),
96
+ publicKey = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
97
+ secretKey = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY,
98
+ limit = DEFAULT_LIMIT,
99
+ bestEffort = false,
100
+ fetchImpl = globalThis.fetch,
101
+ } = {}) {
102
+ if (!publicKey || !secretKey || !fetchImpl) {
103
+ return { backfilled: 0, skipped: 0, errors: ["Telemetry credentials not configured"] };
104
+ }
105
+
106
+ const headers = {
107
+ Authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`,
108
+ "Content-Type": "application/json",
109
+ };
110
+
111
+ const errors = [];
112
+ let sparseTraces;
113
+
114
+ try {
115
+ sparseTraces = await fetchSparseTraces({ baseUrl, headers, limit, fetchImpl });
116
+ } catch (err) {
117
+ return { backfilled: 0, skipped: 0, errors: [err.message] };
118
+ }
119
+
120
+ if (!sparseTraces.length) {
121
+ return { backfilled: 0, skipped: 0, errors: [] };
122
+ }
123
+
124
+ const client = createIngestClient({ baseUrl, publicKey, secretKey, fetchImpl });
125
+ let backfilled = 0;
126
+ let skipped = 0;
127
+
128
+ for (const trace of sparseTraces) {
129
+ const traceId = trace.id;
130
+ if (!traceId) { skipped += 1; continue; }
131
+ try {
132
+ client.event(buildBackfillObservation(traceId));
133
+ backfilled += 1;
134
+ } catch (err) {
135
+ if (bestEffort) {
136
+ errors.push(`${traceId}: ${err.message}`);
137
+ skipped += 1;
138
+ } else {
139
+ throw err;
140
+ }
141
+ }
142
+ }
143
+
144
+ await client.flush();
145
+ return { backfilled, skipped, errors };
146
+ }
147
+
148
+ /**
149
+ * Auto-heal trigger: run backfill only when telemetry coverage is below threshold.
150
+ * Fire-and-forget — errors are swallowed. Intended for background use from buildStatus.
151
+ */
152
+ export function triggerAutoBackfillIfSparse(telemetryRichness, opts = {}) {
153
+ const coverage = telemetryRichness?.coverage ?? 1;
154
+ const total = telemetryRichness?.total ?? 0;
155
+ if (total === 0 || coverage >= SPARSE_THRESHOLD) return;
156
+
157
+ backfillSparseTraces({ ...opts, bestEffort: true }).catch(() => {});
158
+ }
159
+
160
+ /**
161
+ * CLI entrypoint for `construct telemetry-backfill`.
162
+ */
163
+ export async function runTelemetryBackfillCli(args = []) {
164
+ const limitArg = args.find((a) => a.startsWith("--limit="));
165
+ const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) || DEFAULT_LIMIT : DEFAULT_LIMIT;
166
+ const bestEffort = args.includes("--best-effort");
167
+
168
+ process.stdout.write(`Backfilling sparse telemetry traces (limit: ${limit})…\n`);
169
+
170
+ const result = await backfillSparseTraces({ limit, bestEffort });
171
+
172
+ if (result.errors.length && !bestEffort) {
173
+ process.stderr.write(`Backfill error: ${result.errors[0]}\n`);
174
+ process.exit(1);
175
+ }
176
+
177
+ process.stdout.write(
178
+ `Done. backfilled=${result.backfilled} skipped=${result.skipped}${result.errors.length ? ` errors=${result.errors.length}` : ""}\n`
179
+ );
180
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * lib/telemetry/eval-datasets.mjs — Telemetry eval dataset writer.
3
+ *
4
+ * Reads recent scored traces from the telemetry backend, groups them by agent + workCategory,
5
+ * and upserts structured dataset items so prompt changes can be evaluated in
6
+ * the telemetry backend Datasets feature.
7
+ *
8
+ * One dataset per agent (e.g. "construct-cx-engineer"). Each dataset item is one
9
+ * trace: input = agent goal, expected_output = score+comment, metadata = promptHash.
10
+ *
11
+ * Closes the loop:
12
+ * agent runs → cx_score called → score written to telemetry backend
13
+ * → eval-datasets job reads scores → upserts dataset items
14
+ * → Telemetry Datasets UI shows score distribution per prompt version
15
+ * → prompt author can see whether a change improved or regressed quality
16
+ *
17
+ * Consumed by:
18
+ * • lib/embed/daemon.mjs — scheduled every 6h as 'eval-dataset-sync' job
19
+ * • bin/construct — `construct eval-datasets` CLI command
20
+ */
21
+
22
+ const MAX_TRACES = 100;
23
+ const MIN_SCORE_TO_INCLUDE = 0; // include all scores — 0.0 failures are valuable
24
+
25
+ function baseHeaders(publicKey, secretKey) {
26
+ return {
27
+ Authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`,
28
+ 'Content-Type': 'application/json',
29
+ };
30
+ }
31
+
32
+ function baseUrl(env = process.env) {
33
+ return (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
34
+ }
35
+
36
+ /**
37
+ * Fetch recent traces that have at least one quality score.
38
+ * Returns array of { traceId, name, input, metadata, scores[] }.
39
+ */
40
+ async function fetchScoredTraces({ url, headers, limit = MAX_TRACES, fetchImpl = globalThis.fetch }) {
41
+ const res = await fetchImpl(`${url}/api/public/scores?name=quality&limit=${limit}`, { headers });
42
+ if (!res.ok) throw new Error(`Telemetry scores fetch ${res.status}`);
43
+ const json = await res.json().catch(() => ({}));
44
+ const scores = Array.isArray(json.data) ? json.data : [];
45
+ if (!scores.length) return [];
46
+
47
+ // De-duplicate by traceId — take the latest score per trace
48
+ const byTrace = new Map();
49
+ for (const score of scores) {
50
+ if (!score.traceId) continue;
51
+ const existing = byTrace.get(score.traceId);
52
+ if (!existing || new Date(score.timestamp) > new Date(existing.timestamp)) {
53
+ byTrace.set(score.traceId, score);
54
+ }
55
+ }
56
+
57
+ // Fetch trace details in parallel (up to 10 at a time)
58
+ const traceIds = [...byTrace.keys()].slice(0, limit);
59
+ const BATCH = 10;
60
+ const enriched = [];
61
+ for (let i = 0; i < traceIds.length; i += BATCH) {
62
+ const batch = traceIds.slice(i, i + BATCH);
63
+ const results = await Promise.allSettled(
64
+ batch.map((id) =>
65
+ fetchImpl(`${url}/api/public/traces/${id}`, { headers })
66
+ .then((r) => r.ok ? r.json() : null)
67
+ .catch(() => null)
68
+ )
69
+ );
70
+ for (let j = 0; j < batch.length; j++) {
71
+ const trace = results[j].status === 'fulfilled' ? results[j].value : null;
72
+ if (!trace) continue;
73
+ const score = byTrace.get(batch[j]);
74
+ enriched.push({ trace, score });
75
+ }
76
+ }
77
+ return enriched;
78
+ }
79
+
80
+ /**
81
+ * Upsert a dataset item in the telemetry backend.
82
+ * Creates the dataset if it does not exist (returns 200 on existing datasets).
83
+ */
84
+ async function upsertDatasetItem({ url, headers, datasetName, item, fetchImpl = globalThis.fetch }) {
85
+ // Create dataset (idempotent)
86
+ await fetchImpl(`${url}/api/public/datasets`, {
87
+ method: 'POST',
88
+ headers,
89
+ body: JSON.stringify({ name: datasetName, description: `Auto-generated eval dataset for ${datasetName}` }),
90
+ }).catch(() => {});
91
+
92
+ // Upsert item — use traceId as the stable ID so reruns are idempotent
93
+ const res = await fetchImpl(`${url}/api/public/dataset-items`, {
94
+ method: 'POST',
95
+ headers,
96
+ body: JSON.stringify(item),
97
+ });
98
+ if (!res.ok) {
99
+ const text = await res.text().catch(() => '');
100
+ throw new Error(`dataset-items ${res.status}: ${text.slice(0, 200)}`);
101
+ }
102
+ return res.json().catch(() => ({}));
103
+ }
104
+
105
+ /**
106
+ * Derive a dataset name from a trace.
107
+ * Uses agentName from trace metadata, falling back to trace.name.
108
+ */
109
+ function datasetNameForTrace(trace) {
110
+ const agent = trace?.metadata?.agentName || trace?.name || 'construct';
111
+ const workCategory = trace?.metadata?.routeWorkCategory || null;
112
+ const base = `construct-${agent}`;
113
+ return workCategory ? `${base}-${workCategory}` : base;
114
+ }
115
+
116
+ /**
117
+ * Main entrypoint. Reads scored traces from the telemetry backend and upserts dataset items.
118
+ * Returns { synced, skipped, errors }.
119
+ */
120
+ export async function syncEvalDatasets({
121
+ publicKey = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
122
+ secretKey = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY,
123
+ env = process.env,
124
+ limit = MAX_TRACES,
125
+ bestEffort = true,
126
+ fetchImpl = globalThis.fetch,
127
+ } = {}) {
128
+ if (!publicKey || !secretKey) {
129
+ return { synced: 0, skipped: 0, errors: ['Telemetry credentials not configured'] };
130
+ }
131
+
132
+ const url = baseUrl(env);
133
+ const headers = baseHeaders(publicKey, secretKey);
134
+ const errors = [];
135
+ let synced = 0;
136
+ let skipped = 0;
137
+
138
+ let scoredTraces;
139
+ try {
140
+ scoredTraces = await fetchScoredTraces({ url, headers, limit, fetchImpl });
141
+ } catch (err) {
142
+ return { synced: 0, skipped: 0, errors: [err.message] };
143
+ }
144
+
145
+ for (const { trace, score } of scoredTraces) {
146
+ try {
147
+ const datasetName = datasetNameForTrace(trace);
148
+ const promptHash = trace?.metadata?.composedPromptHash ?? null;
149
+ const promptVersion = trace?.metadata?.composedPromptVersion ?? null;
150
+
151
+ const item = {
152
+ // Use traceId as external ID so upserts are idempotent
153
+ id: `trace-${trace.id}`,
154
+ datasetName,
155
+ input: trace.input ?? trace?.metadata?.goal ?? '',
156
+ expectedOutput: {
157
+ qualityScore: score.value,
158
+ comment: score.comment ?? null,
159
+ agentName: trace?.metadata?.agentName ?? trace?.name ?? null,
160
+ workCategory: trace?.metadata?.routeWorkCategory ?? null,
161
+ },
162
+ metadata: {
163
+ traceId: trace.id,
164
+ traceName: trace.name,
165
+ promptHash,
166
+ promptVersion,
167
+ routeIntent: trace?.metadata?.routeIntent ?? null,
168
+ routeTrack: trace?.metadata?.routeTrack ?? null,
169
+ fragmentTypes: trace?.metadata?.promptFragmentTypes ?? null,
170
+ hasLearnedPatterns: trace?.metadata?.promptHasLearnedPatterns ?? false,
171
+ scoredAt: score.timestamp ?? new Date().toISOString(),
172
+ },
173
+ };
174
+
175
+ await upsertDatasetItem({ url, headers, datasetName, item, fetchImpl });
176
+ synced += 1;
177
+ } catch (err) {
178
+ if (bestEffort) {
179
+ errors.push(`${trace?.id}: ${err.message}`);
180
+ skipped += 1;
181
+ } else {
182
+ throw err;
183
+ }
184
+ }
185
+ }
186
+
187
+ return { synced, skipped, errors };
188
+ }
189
+
190
+ /**
191
+ * CLI entrypoint for `construct eval-datasets`.
192
+ */
193
+ export async function runEvalDatasetsCli(args = []) {
194
+ const limitArg = args.find((a) => a.startsWith('--limit='));
195
+ const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) || MAX_TRACES : MAX_TRACES;
196
+
197
+ process.stdout.write(`Syncing eval datasets from telemetry backend (limit: ${limit})…\n`);
198
+ const result = await syncEvalDatasets({ limit, bestEffort: false });
199
+ process.stdout.write(`Done. synced=${result.synced} skipped=${result.skipped}${result.errors.length ? ` errors=${result.errors.length}` : ''}\n`);
200
+ if (result.errors.length) {
201
+ for (const e of result.errors) process.stderr.write(` ${e}\n`);
202
+ }
203
+ }
@@ -1,10 +1,13 @@
1
1
  /**
2
- * lib/telemetry/langfuse-ingest.mjs — Langfuse batch ingestion client.
2
+ * lib/telemetry/ingest.mjs — Telemetry backend batch ingestion client.
3
3
  *
4
4
  * POSTs observations to /api/public/ingestion in batches. Silent no-op when
5
- * LANGFUSE_PUBLIC_KEY/SECRET_KEY are missing. Never throws from the hot path.
5
+ * credentials are missing. Never throws from the hot path.
6
+ *
7
+ * Uses an open ingestion API protocol. Point CONSTRUCT_TELEMETRY_URL
8
+ * at any compatible backend.
6
9
  */
7
- import { randomUUID } from "node:crypto";
10
+ import { randomUUID } from 'node:crypto';
8
11
 
9
12
  const DEFAULT_FLUSH_INTERVAL_MS = 500;
10
13
  const DEFAULT_MAX_BATCH = 50;
@@ -12,7 +15,7 @@ const MAX_PAYLOAD_CHARS = 16_000;
12
15
 
13
16
  function truncate(value) {
14
17
  if (value == null) return value;
15
- if (typeof value === "string") {
18
+ if (typeof value === 'string') {
16
19
  return value.length > MAX_PAYLOAD_CHARS
17
20
  ? `${value.slice(0, MAX_PAYLOAD_CHARS)}…[truncated ${value.length - MAX_PAYLOAD_CHARS} chars]`
18
21
  : value;
@@ -27,9 +30,11 @@ function truncate(value) {
27
30
  }
28
31
 
29
32
  export function createIngestClient({
30
- baseUrl = (process.env.LANGFUSE_BASEURL ?? "https://cloud.langfuse.com").replace(/\/$/, ""),
31
- publicKey = process.env.LANGFUSE_PUBLIC_KEY,
32
- secretKey = process.env.LANGFUSE_SECRET_KEY,
33
+ baseUrl = (
34
+ process.env.CONSTRUCT_TELEMETRY_URL ?? ''
35
+ ).replace(/\/$/, ''),
36
+ publicKey = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
37
+ secretKey = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY,
33
38
  flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS,
34
39
  maxBatch = DEFAULT_MAX_BATCH,
35
40
  onError = () => {},
@@ -41,8 +46,8 @@ export function createIngestClient({
41
46
  let inflight = Promise.resolve();
42
47
 
43
48
  const authHeader = available
44
- ? `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`
45
- : "";
49
+ ? `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`
50
+ : '';
46
51
 
47
52
  function scheduleFlush() {
48
53
  if (!available || flushTimer) return;
@@ -50,7 +55,7 @@ export function createIngestClient({
50
55
  flushTimer = null;
51
56
  void flushBatch();
52
57
  }, flushIntervalMs);
53
- if (typeof flushTimer.unref === "function") flushTimer.unref();
58
+ if (typeof flushTimer.unref === 'function') flushTimer.unref();
54
59
  }
55
60
 
56
61
  async function flushBatch() {
@@ -62,14 +67,14 @@ export function createIngestClient({
62
67
  try { await prev; } catch { /* ignore prior */ }
63
68
  try {
64
69
  const res = await fetchImpl(`${baseUrl}/api/public/ingestion`, {
65
- method: "POST",
66
- headers: { Authorization: authHeader, "Content-Type": "application/json" },
70
+ method: 'POST',
71
+ headers: { Authorization: authHeader, 'Content-Type': 'application/json' },
67
72
  body: payload,
68
73
  });
69
74
  if (!res.ok && res.status !== 207) {
70
- let detail = "";
75
+ let detail = '';
71
76
  try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
72
- onError(new Error(`langfuse ingest ${res.status}: ${detail}`));
77
+ onError(new Error(`telemetry ingest ${res.status}: ${detail}`));
73
78
  }
74
79
  } catch (err) {
75
80
  onError(err);
@@ -92,12 +97,13 @@ export function createIngestClient({
92
97
 
93
98
  return {
94
99
  available,
95
- trace: (body) => push("trace-create", body),
96
- generation: (body) => push("generation-create", body),
97
- generationUpdate: (body) => push("generation-update", body),
98
- span: (body) => push("span-create", body),
99
- spanUpdate: (body) => push("span-update", body),
100
- event: (body) => push("event-create", body),
100
+ trace: (body) => push('trace-create', body),
101
+ traceUpdate: (body) => push('trace-update', body),
102
+ generation: (body) => push('generation-create', body),
103
+ generationUpdate: (body) => push('generation-update', body),
104
+ span: (body) => push('span-create', body),
105
+ spanUpdate: (body) => push('span-update', body),
106
+ event: (body) => push('event-create', body),
101
107
  async flush() {
102
108
  if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
103
109
  while (queue.length > 0) await flushBatch();