@geraldmaron/construct 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +10 -7
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1116 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +525 -38
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -0,0 +1,765 @@
1
+ /**
2
+ * lib/server/insights.mjs — dashboard insights aggregator.
3
+ *
4
+ * Pulls real data from the running stack (telemetry traces, cost ledger,
5
+ * intake queue, beads) into a single payload the Mission Control panel
6
+ * renders. Every section degrades to a typed state when its backing
7
+ * source is unreachable — the dashboard never shows a bare error or
8
+ * a hung "loading…" surface.
9
+ *
10
+ * State shape per section: `{ state: 'ok' | 'unreachable' | 'misconfigured' | 'empty', ... }`.
11
+ */
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { spawnSync } from 'node:child_process';
16
+ import { readCostLog, summarizeCostData } from '../cost.mjs';
17
+
18
+ const TELEMETRY_TIMEOUT_MS = 3500;
19
+
20
+ function telemetryAuthHeader(env) {
21
+ const key = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
22
+ const secret = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
23
+ if (!key || !secret) return null;
24
+ return `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`;
25
+ }
26
+
27
+ function telemetryBaseUrl(env) {
28
+ return (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
29
+ }
30
+
31
+ async function fetchTelemetrySummary(env, { limit = 100 } = {}) {
32
+ const auth = telemetryAuthHeader(env);
33
+ const baseUrl = telemetryBaseUrl(env);
34
+
35
+ // Try Telemetry API first
36
+ if (auth) {
37
+ const controller = new AbortController();
38
+ const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
39
+ try {
40
+ const res = await fetch(`${baseUrl}/api/public/traces?limit=${limit}`, {
41
+ headers: { Authorization: auth }, signal: controller.signal,
42
+ });
43
+ clearTimeout(timer);
44
+ if (res.status === 401 || res.status === 403) {
45
+ return { state: 'misconfigured', message: `Telemetry credentials rejected (HTTP ${res.status}). Run construct init.` };
46
+ }
47
+ if (!res.ok) return { state: 'unreachable', message: `Telemetry HTTP ${res.status}` };
48
+ const json = await res.json();
49
+ const traces = Array.isArray(json.data) ? json.data : [];
50
+ let totalCost = 0; let observations = 0; const byModel = new Map(); let withError = 0; let withInput = 0; let withOutput = 0;
51
+ for (const trace of traces) {
52
+ totalCost += Number(trace?.totalCost) || 0;
53
+ observations += Number(trace?.observationCount) || 0;
54
+ if (trace?.input != null) withInput += 1;
55
+ if (trace?.output != null) withOutput += 1;
56
+ const tags = Array.isArray(trace?.tags) ? trace.tags : [];
57
+ if (tags.some((t) => /error|failed|fail/i.test(t))) withError += 1;
58
+ const model = trace?.metadata?.model || trace?.metadata?.lastModel || trace?.metadata?.costModel ||
59
+ trace?.output?.model || trace?.output?.costModel || trace?.input?.metadata?.model ||
60
+ (Array.isArray(trace?.observations) ? trace.observations.find((o) => o?.model)?.model : null) || null;
61
+ if (!model) continue;
62
+ const prior = byModel.get(model) || { traces: 0, cost: 0 };
63
+ prior.traces += 1; prior.cost += Number(trace?.totalCost) || 0;
64
+ byModel.set(model, prior);
65
+ }
66
+ return {
67
+ state: traces.length === 0 ? 'empty' : 'ok',
68
+ baseUrl, sampleSize: traces.length,
69
+ totalCostUsd: Number(totalCost.toFixed(4)), observations, withInput, withOutput, withError,
70
+ topModels: [...byModel.entries()].map(([model, v]) => ({ model, traces: v.traces, cost: Number(v.cost.toFixed(6)) }))
71
+ .sort((a, b) => b.cost - a.cost || b.traces - a.traces).slice(0, 5),
72
+ };
73
+ } catch (err) {
74
+ clearTimeout(timer);
75
+ // Fall through to local traces
76
+ }
77
+ }
78
+
79
+ // Fallback: read from local .cx/traces/ when Telemetry is unreachable or not configured.
80
+ // The daemon always writes heartbeat, lifecycle, and probe events to local JSONL files.
81
+ try {
82
+ const traceDir = path.join(os.homedir(), '.cx', 'traces');
83
+ if (!fs.existsSync(traceDir)) return { state: 'empty', message: 'No local traces. Start the daemon with `construct dev`.' };
84
+ const files = fs.readdirSync(traceDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
85
+ if (!files.length) return { state: 'empty', message: 'No local trace files.' };
86
+
87
+ const events = { total: 0, daemon: 0, probe: 0, lifecycle: 0, other: 0 };
88
+ const byEventType = {};
89
+ const byDay = {};
90
+
91
+ for (const file of files.slice(0, 7)) {
92
+ const content = fs.readFileSync(path.join(traceDir, file), 'utf8');
93
+ for (const line of content.trim().split('\n').filter(Boolean)) {
94
+ try {
95
+ const e = JSON.parse(line);
96
+ events.total++;
97
+ byEventType[e.eventType] = (byEventType[e.eventType] || 0) + 1;
98
+ if (e.eventType?.startsWith('daemon.')) events.daemon++;
99
+ else if (e.eventType?.startsWith('telemetry.') || e.eventType === 'lifecycle.completed') events.lifecycle++;
100
+ else events.other++;
101
+ const day = file.replace('.jsonl', '');
102
+ byDay[day] = (byDay[day] || 0) + 1;
103
+ } catch { /* skip malformed */ }
104
+ }
105
+ }
106
+
107
+ const lfState = auth ? 'unreachable' : 'misconfigured';
108
+ return {
109
+ state: events.total > 0 ? 'ok' : 'empty',
110
+ source: 'local-traces',
111
+ baseUrl: null,
112
+ sampleSize: events.total,
113
+ eventBreakdown: byEventType,
114
+ daemonEvents: events.daemon,
115
+ lifecyleEvents: events.lifecycle,
116
+ days: Object.keys(byDay).length,
117
+ message: `Telemetry ${lfState}. Showing ${events.total} events from local trace files.`,
118
+ };
119
+ } catch (err) {
120
+ return { state: 'unreachable', message: `Local traces unavailable: ${err.message}` };
121
+ }
122
+ }
123
+
124
+ function resolveBillingMode(env) {
125
+ // Precedence: CONSTRUCT_BILLING_MODE env > construct.config.json.costs.billingMode
126
+ // > inference (ANTHROPIC_API_KEY set → metered, else subscription).
127
+ // Returns {mode, source} so callers can distinguish declared from inferred.
128
+ const envOverride = env?.CONSTRUCT_BILLING_MODE;
129
+ if (envOverride === 'metered' || envOverride === 'subscription' || envOverride === 'mixed') {
130
+ return { mode: envOverride, source: 'env' };
131
+ }
132
+ try {
133
+ const cfgPath = path.join(process.cwd(), 'construct.config.json');
134
+ if (fs.existsSync(cfgPath)) {
135
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
136
+ const m = cfg?.costs?.billingMode;
137
+ if (m === 'metered' || m === 'subscription' || m === 'mixed') {
138
+ return { mode: m, source: 'config' };
139
+ }
140
+ }
141
+ } catch { /* config is best-effort */ }
142
+ const hasMeteredKey = typeof env?.ANTHROPIC_API_KEY === 'string'
143
+ && env.ANTHROPIC_API_KEY.trim().length > 0
144
+ && !env.ANTHROPIC_API_KEY.includes('__');
145
+ return { mode: hasMeteredKey ? 'metered' : 'subscription', source: 'inferred' };
146
+ }
147
+
148
+ async function summarizeCostLedger(env, homeDir = os.homedir()) {
149
+ try {
150
+ const entries = readCostLog(homeDir);
151
+ const { getTotalDailySpend, totalBudget, enforcementActive } = await import('../cost-ledger.mjs');
152
+ const todayTotal = getTotalDailySpend();
153
+ const cap = totalBudget();
154
+ const billing = resolveBillingMode(env);
155
+ const billingMode = billing.mode;
156
+ // Per-provider billing-mode overrides live in construct.config.json.costs.providers.
157
+ // summarizeCostData reads them to produce meteredActualUsd (excludes subscription
158
+ // providers) alongside the legacy totalCostUsd (now also exposed as meteredEquivalentUsd).
159
+ let costsConfig = {};
160
+ try {
161
+ const { loadProjectConfig } = await import('../config/project-config.mjs');
162
+ costsConfig = loadProjectConfig(process.cwd(), env)?.config?.costs ?? {};
163
+ } catch { /* config is best-effort */ }
164
+ if (!entries.length) {
165
+ return {
166
+ state: 'empty',
167
+ message: 'No cost entries yet. Run a chat session to populate the ledger.',
168
+ billingMode,
169
+ billingSource: billing.source,
170
+ providerBilling: costsConfig.providers || {},
171
+ budget: { todayUsd: todayTotal.costUsd, capUsd: cap, enforce: enforcementActive() },
172
+ };
173
+ }
174
+ const last7 = summarizeCostData(entries, { days: 7, costsConfig });
175
+ const last24h = summarizeCostData(entries, { days: 1, costsConfig });
176
+
177
+ // Cache savings — order-of-magnitude estimate. Ledger has per-call cost
178
+ // but no per-call input price, so the math uses a weighted-average input
179
+ // price across the models seen in the window:
180
+ // gross = cacheReadTokens * avgInputPrice * 0.9
181
+ // premium = cacheCreationTokens * avgInputPrice * 0.25
182
+ // The card surfaces the caveat explicitly to callers.
183
+ let cacheSavings = null;
184
+ try {
185
+ const { resolveModelPricing } = await import('../telemetry/model-pricing-catalog.mjs');
186
+ // Token-weighted avg input price across models in this window.
187
+ // byAgent gives us per-agent input-token totals; we can resolve
188
+ // each agent's model and weight its inputPrice by its share of
189
+ // total billable input. If only one model is present, this
190
+ // collapses to that model's price.
191
+ let weightedInputPrice = 0;
192
+ let weightedDenominator = 0;
193
+ const modelsSeen = new Set();
194
+ for (const a of last7.byAgent || []) {
195
+ const model = a.model || 'claude-sonnet-4-6';
196
+ modelsSeen.add(model);
197
+ const p = resolveModelPricing(model);
198
+ if (!p) continue;
199
+ const tokens = (a.inputTokens || 0) + (a.cacheReadInputTokens || 0) + (a.cacheCreationInputTokens || 0);
200
+ if (tokens === 0) continue;
201
+ weightedInputPrice += p.inputPrice * tokens;
202
+ weightedDenominator += tokens;
203
+ }
204
+ const avgInputPrice = weightedDenominator > 0
205
+ ? weightedInputPrice / weightedDenominator
206
+ : (resolveModelPricing('claude-sonnet-4-6')?.inputPrice ?? 3 / 1_000_000);
207
+ const grossSaved = last7.cacheReadInputTokens * avgInputPrice * 0.9;
208
+ const writePremium = last7.cacheCreationInputTokens * avgInputPrice * 0.25;
209
+ const netSaved = grossSaved - writePremium;
210
+ const wouldHaveCost = last7.totalCostUsd + netSaved;
211
+ cacheSavings = {
212
+ netSavedUsd: Number(netSaved.toFixed(4)),
213
+ grossSavedUsd: Number(grossSaved.toFixed(4)),
214
+ writePremiumUsd: Number(writePremium.toFixed(4)),
215
+ wouldHaveCostUsd: Number(wouldHaveCost.toFixed(4)),
216
+ savingsRatio: wouldHaveCost > 0 ? netSaved / wouldHaveCost : 0,
217
+ modelsInWindow: [...modelsSeen],
218
+ avgInputPriceUsdPerToken: Number(avgInputPrice.toFixed(10)),
219
+ approximation: 'token-weighted avg input price; precise per-call price is not recoverable from the ledger',
220
+ };
221
+ } catch { /* savings is best-effort */ }
222
+
223
+ // Token mix — total counts to render a stacked bar on the dashboard.
224
+ const tokenMix = {
225
+ input: last7.totalInputTokens,
226
+ cacheRead: last7.cacheReadInputTokens,
227
+ cacheCreation: last7.cacheCreationInputTokens,
228
+ output: last7.totalOutputTokens,
229
+ reasoning: last7.totalReasoningTokens || 0,
230
+ };
231
+ const tokenMixTotal = Object.values(tokenMix).reduce((s, v) => s + v, 0);
232
+
233
+ // Today's actual metered spend — recomputed against per-provider modes so
234
+ // a subscription provider (Claude Pro, e.g.) doesn't dump its
235
+ // metered-equivalent into the headline number. Falls back to the legacy
236
+ // todayTotal.costUsd when no day-bucketing of today's slice is available.
237
+ const today = summarizeCostData(entries, { days: 1, costsConfig });
238
+ const todayActualUsd = today.meteredActualUsd;
239
+ const todayEquivUsd = todayTotal.costUsd;
240
+
241
+ return {
242
+ state: 'ok',
243
+ billingMode,
244
+ billingSource: billing.source,
245
+ providerBilling: costsConfig.providers || {},
246
+ budget: {
247
+ todayUsd: todayEquivUsd,
248
+ todayActualUsd,
249
+ todayMeteredEquivalentUsd: todayEquivUsd,
250
+ capUsd: cap,
251
+ usageRatio: cap > 0 ? todayActualUsd / cap : 0,
252
+ enforce: enforcementActive(),
253
+ byPersona: todayTotal.byPersona,
254
+ },
255
+ cacheSavings,
256
+ tokenMix: { ...tokenMix, total: tokenMixTotal },
257
+ byProvider: last7.byProvider,
258
+ windows: {
259
+ last24h: {
260
+ interactions: last24h.interactions,
261
+ totalCostUsd: last24h.totalCostUsd,
262
+ meteredActualUsd: last24h.meteredActualUsd,
263
+ inputTokens: last24h.totalInputTokens,
264
+ outputTokens: last24h.totalOutputTokens,
265
+ cacheHitRate: last24h.cacheHitRate,
266
+ },
267
+ last7d: {
268
+ interactions: last7.interactions,
269
+ totalCostUsd: last7.totalCostUsd,
270
+ meteredActualUsd: last7.meteredActualUsd,
271
+ inputTokens: last7.totalInputTokens,
272
+ outputTokens: last7.totalOutputTokens,
273
+ cacheHitRate: last7.cacheHitRate,
274
+ byAgent: last7.byAgent,
275
+ byProvider: last7.byProvider,
276
+ },
277
+ },
278
+ };
279
+ } catch (err) {
280
+ return { state: 'unreachable', message: err.message };
281
+ }
282
+ }
283
+
284
+ function summarizeIntakeQueue(env, cwd = process.cwd()) {
285
+ try {
286
+ const pending = path.join(cwd, '.cx', 'intake', 'pending');
287
+ const processed = path.join(cwd, '.cx', 'intake', 'processed');
288
+ const skipped = path.join(cwd, '.cx', 'intake', 'skipped');
289
+ const count = (dir) => {
290
+ try {
291
+ if (!fs.existsSync(dir)) return 0;
292
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.json')).length;
293
+ } catch { return 0; }
294
+ };
295
+ const pendingCount = count(pending);
296
+ const processedCount = count(processed);
297
+ const skippedCount = count(skipped);
298
+ // Count customer-linked items in pending queue
299
+ let customerLinked = 0;
300
+ try {
301
+ const pendingFiles = fs.readdirSync(pending).filter((f) => f.endsWith('.json'));
302
+ for (const f of pendingFiles) {
303
+ try {
304
+ const content = JSON.parse(fs.readFileSync(path.join(pending, f), 'utf8'));
305
+ if (content.customers?.length > 0) customerLinked++;
306
+ } catch { /* skip malformed */ }
307
+ }
308
+ } catch { /* skip */ }
309
+
310
+ return {
311
+ state: pendingCount === 0 && processedCount === 0 && skippedCount === 0 ? 'empty' : 'ok',
312
+ pending: pendingCount,
313
+ processed: processedCount,
314
+ skipped: skippedCount,
315
+ customerLinked,
316
+ };
317
+ } catch (err) {
318
+ return { state: 'unreachable', message: err.message };
319
+ }
320
+ }
321
+
322
+ function summarizeBeads(rootDir) {
323
+ try {
324
+ let bdCwd = rootDir;
325
+ try {
326
+ const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: rootDir, encoding: 'utf8' });
327
+ if (r.status === 0) {
328
+ const t = r.stdout.trim();
329
+ const resolved = t.startsWith('/') ? t : path.join(rootDir, t);
330
+ const cand = path.dirname(resolved);
331
+ if (fs.existsSync(path.join(cand, '.beads'))) bdCwd = cand;
332
+ }
333
+ } catch { /* fall back */ }
334
+ const r = spawnSync('bd', ['list', '--status', 'all', '--json'], {
335
+ cwd: bdCwd, encoding: 'utf8', env: { ...process.env, NO_COLOR: '1' }, timeout: 5000,
336
+ });
337
+ if (r.status !== 0) return { state: 'unreachable', message: r.stderr?.slice(0, 200) || 'bd list failed' };
338
+ const issues = JSON.parse(r.stdout);
339
+ const byStatus = {};
340
+ const byPriority = {};
341
+ for (const it of issues) {
342
+ byStatus[it.status] = (byStatus[it.status] || 0) + 1;
343
+ byPriority[`P${it.priority}`] = (byPriority[`P${it.priority}`] || 0) + 1;
344
+ }
345
+ return {
346
+ state: issues.length === 0 ? 'empty' : 'ok',
347
+ total: issues.length,
348
+ byStatus,
349
+ byPriority,
350
+ };
351
+ } catch (err) {
352
+ return { state: 'unreachable', message: err.message };
353
+ }
354
+ }
355
+
356
+ function summarizeModelProviders(env) {
357
+ const entries = [
358
+ { id: 'anthropic', displayName: 'Anthropic', envKey: 'ANTHROPIC_API_KEY', altKey: null, docsUrl: 'https://docs.anthropic.com/api' },
359
+ { id: 'openai', displayName: 'OpenAI', envKey: 'OPENAI_API_KEY', altKey: null, docsUrl: 'https://platform.openai.com/api-keys' },
360
+ { id: 'openrouter', displayName: 'OpenRouter', envKey: 'OPENROUTER_API_KEY', altKey: 'OPEN_ROUTER_API_KEY', docsUrl: 'https://openrouter.ai/keys' },
361
+ { id: 'ollama', displayName: 'Ollama (local)', envKey: 'OLLAMA_BASE_URL', altKey: null, docsUrl: 'https://ollama.com' },
362
+ { id: 'groq', displayName: 'Groq', envKey: 'GROQ_API_KEY', altKey: null, docsUrl: 'https://console.groq.com/keys' },
363
+ { id: 'mistral', displayName: 'Mistral', envKey: 'MISTRAL_API_KEY', altKey: null, docsUrl: 'https://console.mistral.ai/api-keys' },
364
+ { id: 'gemini', displayName: 'Google Gemini', envKey: 'GEMINI_API_KEY', altKey: null, docsUrl: 'https://aistudio.google.com/app/apikey' },
365
+ ];
366
+ return entries.map((e) => {
367
+ // Check primary key, alt key, then multi-source resolution
368
+ const raw = env[e.envKey] || (e.altKey ? env[e.altKey] : null);
369
+ const configured = typeof raw === 'string' && raw.trim().length > 0 && !raw.includes('__');
370
+ // If not found in env, try multi-source resolution (config.env, .env, shell rc, 1Password)
371
+ const resolved = !configured ? resolveModelCredential(e.envKey, e.altKey) : null;
372
+ const isConfigured = configured || Boolean(resolved);
373
+ return {
374
+ ...e,
375
+ configured: isConfigured,
376
+ state: isConfigured ? 'configured' : 'not-configured',
377
+ };
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Resolve a model provider credential from config.env, ~/.env, and shell rc files.
383
+ */
384
+ function resolveModelCredential(primaryKey, altKey) {
385
+ const homeDir = os.homedir();
386
+ const keys = [primaryKey, altKey].filter(Boolean);
387
+
388
+ // Special case: Ollama on default port (no OLLAMA_BASE_URL needed)
389
+ if (primaryKey === 'OLLAMA_BASE_URL') {
390
+ try {
391
+ const r = spawnSync('curl', ['-s', '--connect-timeout', '1', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:11434/api/tags'], { encoding: 'utf8', timeout: 3000 });
392
+ if (r.status === 0 && r.stdout?.trim() === '200') return 'http://localhost:11434';
393
+ } catch { /* not available */ }
394
+ // Also try the ollama binary
395
+ try {
396
+ const r = spawnSync('ollama', ['--version'], { encoding: 'utf8', timeout: 2000 });
397
+ if (r.status === 0) return 'found-ollama-binary';
398
+ } catch { /* not available */ }
399
+ }
400
+
401
+ const paths = [path.join(homeDir, '.construct', 'config.env'), path.join(homeDir, '.env')];
402
+ for (const p of paths) {
403
+ try {
404
+ if (!fs.existsSync(p)) continue;
405
+ const content = fs.readFileSync(p, 'utf8');
406
+ for (const key of keys) {
407
+ const m = content.match(new RegExp(`^${key}=["']?(.+?)["']?$`, 'm'));
408
+ if (m && m[1]) return m[1].trim();
409
+ }
410
+ } catch { /* skip */ }
411
+ }
412
+ const shellFiles = [path.join(homeDir, '.zshrc'), path.join(homeDir, '.bashrc'), path.join(homeDir, '.bash_profile'), path.join(homeDir, '.profile')];
413
+ for (const rc of shellFiles) {
414
+ if (!fs.existsSync(rc)) continue;
415
+ try {
416
+ const content = fs.readFileSync(rc, 'utf8');
417
+ for (const key of keys) {
418
+ const directRe = new RegExp(`^\\s*export\\s+${key}=`, 'm');
419
+ if (directRe.test(content)) return 'found-in-rc';
420
+ }
421
+ } catch { /* skip */ }
422
+ }
423
+ return null;
424
+ }
425
+
426
+ async function summarizeTelemetryCostTrend(env, { days = 7 } = {}) {
427
+ // Fetches each trace + its totalCost and buckets by yyyy-mm-dd. The
428
+ // public /traces endpoint includes totalCost when generations have
429
+ // been priced; sparse trace cost is fine — the chart shows shape, not
430
+ // an audit-grade ledger value.
431
+ const auth = telemetryAuthHeader(env);
432
+ if (!auth) return { state: 'misconfigured', message: 'CONSTRUCT_TELEMETRY_PUBLIC_KEY / SECRET_KEY not set.' };
433
+ const baseUrl = telemetryBaseUrl(env);
434
+ const controller = new AbortController();
435
+ const timer = setTimeout(() => controller.abort(), 6000);
436
+ try {
437
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
438
+ // Telemetry caps `limit` at 100 — 500 returns 400. Use the cap and accept that
439
+ // weeks with >100 traces will undercount the trailing days; the chart is for
440
+ // shape, not audit. Re-try without `fromTimestamp` if the server version
441
+ // rejects that param (same fallback shape as the latency probe).
442
+ let res = await fetch(`${baseUrl}/api/public/traces?limit=100&fromTimestamp=${encodeURIComponent(since)}`, {
443
+ headers: { Authorization: auth },
444
+ signal: controller.signal,
445
+ });
446
+ if (res.status === 400 || res.status === 422) {
447
+ res = await fetch(`${baseUrl}/api/public/traces?limit=100`, {
448
+ headers: { Authorization: auth },
449
+ signal: controller.signal,
450
+ });
451
+ }
452
+ clearTimeout(timer);
453
+ if (!res.ok) return { state: 'unreachable', message: `Telemetry HTTP ${res.status}` };
454
+ const json = await res.json();
455
+ const traces = Array.isArray(json.data) ? json.data : [];
456
+ const buckets = new Map();
457
+ for (let d = 0; d < days; d++) {
458
+ const day = new Date(Date.now() - d * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
459
+ buckets.set(day, { day, costUsd: 0, traces: 0, errors: 0 });
460
+ }
461
+ let withCost = 0;
462
+ for (const trace of traces) {
463
+ const ts = trace?.timestamp || trace?.createdAt;
464
+ if (!ts) continue;
465
+ const day = String(ts).slice(0, 10);
466
+ const bucket = buckets.get(day);
467
+ if (!bucket) continue;
468
+ bucket.traces += 1;
469
+ const cost = Number(trace?.totalCost) || 0;
470
+ if (cost > 0) { bucket.costUsd += cost; withCost += 1; }
471
+ const tags = Array.isArray(trace?.tags) ? trace.tags : [];
472
+ if (tags.some((t) => /error|fail/i.test(t))) bucket.errors += 1;
473
+ }
474
+ const series = [...buckets.values()].sort((a, b) => a.day.localeCompare(b.day));
475
+ return { state: traces.length === 0 ? 'empty' : 'ok', days, sampleSize: traces.length, withCost, series };
476
+ } catch (err) {
477
+ clearTimeout(timer);
478
+ return { state: 'unreachable', message: err?.message || 'fetch failed' };
479
+ }
480
+ }
481
+
482
+ function quantile(values, q) {
483
+ if (!values.length) return null;
484
+ const sorted = [...values].sort((a, b) => a - b);
485
+ const pos = (sorted.length - 1) * q;
486
+ const base = Math.floor(pos);
487
+ const rest = pos - base;
488
+ if (sorted[base + 1] !== undefined) {
489
+ return sorted[base] + rest * (sorted[base + 1] - sorted[base]);
490
+ }
491
+ return sorted[base];
492
+ }
493
+
494
+ async function summarizeTelemetryLatency(env, { days = 7, limit = 50 } = {}) {
495
+ const auth = telemetryAuthHeader(env);
496
+ if (!auth) return { state: 'misconfigured', message: 'CONSTRUCT_TELEMETRY_PUBLIC_KEY / SECRET_KEY not set.' };
497
+ const baseUrl = telemetryBaseUrl(env);
498
+ const controller = new AbortController();
499
+ const timer = setTimeout(() => controller.abort(), 6000);
500
+ try {
501
+ // Telemetry public API observations: `type=GENERATION` plus an
502
+ // optional `fromStartTime` (some versions reject the param, so we
503
+ // try with it first and fall back to a plain page if the server
504
+ // returns 4xx with that param).
505
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
506
+ let res = await fetch(
507
+ `${baseUrl}/api/public/observations?type=GENERATION&fromStartTime=${encodeURIComponent(since)}&limit=${limit}`,
508
+ { headers: { Authorization: auth }, signal: controller.signal },
509
+ );
510
+ if (res.status === 400 || res.status === 422) {
511
+ res = await fetch(
512
+ `${baseUrl}/api/public/observations?type=GENERATION&limit=${limit}`,
513
+ { headers: { Authorization: auth }, signal: controller.signal },
514
+ );
515
+ }
516
+ clearTimeout(timer);
517
+ if (!res.ok) return { state: 'unreachable', message: `Telemetry HTTP ${res.status}` };
518
+ const json = await res.json();
519
+ const observations = Array.isArray(json.data) ? json.data : [];
520
+ if (observations.length === 0) {
521
+ return { state: 'empty', message: 'No generation observations in the window.' };
522
+ }
523
+ const latencies = [];
524
+ const ttfts = [];
525
+ const tps = [];
526
+ let withTtft = 0;
527
+ let errorCount = 0;
528
+ const byModel = new Map();
529
+ for (const g of observations) {
530
+ const start = g.startTime ? Date.parse(g.startTime) : null;
531
+ const end = g.endTime ? Date.parse(g.endTime) : null;
532
+ const ttft = g.completionStartTime ? Date.parse(g.completionStartTime) : null;
533
+ const model = g.model || 'unknown';
534
+ const level = (g.level || '').toLowerCase();
535
+ const isError = level === 'error';
536
+ if (isError) errorCount += 1;
537
+ if (start && end && end > start) {
538
+ const totalMs = end - start;
539
+ latencies.push(totalMs);
540
+ const outTokens = Number(g.completionTokens ?? g.usage?.output ?? 0);
541
+ if (outTokens > 0 && ttft && end > ttft) {
542
+ tps.push((outTokens / (end - ttft)) * 1000);
543
+ }
544
+ if (ttft && start && ttft > start) {
545
+ ttfts.push(ttft - start);
546
+ withTtft += 1;
547
+ }
548
+ const entry = byModel.get(model) || { traces: 0, totalMs: 0, errors: 0 };
549
+ entry.traces += 1;
550
+ entry.totalMs += totalMs;
551
+ if (isError) entry.errors += 1;
552
+ byModel.set(model, entry);
553
+ }
554
+ }
555
+ const byAgent = new Map();
556
+ for (const g of observations) {
557
+ const agent = g.metadata?.agent || g.parentObservationId ? (g.metadata?.agent || null) : null;
558
+ // Observation `name` often follows `llm.<agent>` from the opencode
559
+ // plugin — extract the agent from there as a fallback when the
560
+ // metadata field is empty.
561
+ let resolvedAgent = agent;
562
+ if (!resolvedAgent && typeof g.name === 'string' && g.name.startsWith('llm.')) {
563
+ const tail = g.name.slice(4);
564
+ if (tail && tail !== 'chat') resolvedAgent = tail;
565
+ }
566
+ if (!resolvedAgent) continue;
567
+ const start = g.startTime ? Date.parse(g.startTime) : null;
568
+ const end = g.endTime ? Date.parse(g.endTime) : null;
569
+ if (!start || !end || end <= start) continue;
570
+ const totalMs = end - start;
571
+ const entry = byAgent.get(resolvedAgent) || { traces: 0, totalMs: 0, errors: 0 };
572
+ entry.traces += 1;
573
+ entry.totalMs += totalMs;
574
+ if ((g.level || '').toLowerCase() === 'error') entry.errors += 1;
575
+ byAgent.set(resolvedAgent, entry);
576
+ }
577
+
578
+ return {
579
+ state: 'ok',
580
+ sampleSize: observations.length,
581
+ windowDays: days,
582
+ latencyMs: {
583
+ p50: quantile(latencies, 0.5),
584
+ p95: quantile(latencies, 0.95),
585
+ p99: quantile(latencies, 0.99),
586
+ },
587
+ ttftMs: ttfts.length ? { p50: quantile(ttfts, 0.5), p95: quantile(ttfts, 0.95), sampleSize: withTtft } : null,
588
+ tokensPerSecond: tps.length ? { p50: quantile(tps, 0.5), p95: quantile(tps, 0.95) } : null,
589
+ errorRate: observations.length > 0 ? errorCount / observations.length : 0,
590
+ errorCount,
591
+ perModel: [...byModel.entries()]
592
+ .map(([model, v]) => ({ model, traces: v.traces, avgMs: Math.round(v.totalMs / v.traces), errors: v.errors }))
593
+ .sort((a, b) => b.traces - a.traces)
594
+ .slice(0, 8),
595
+ perAgent: [...byAgent.entries()]
596
+ .map(([agent, v]) => ({ agent, traces: v.traces, avgMs: Math.round(v.totalMs / v.traces), errors: v.errors }))
597
+ .sort((a, b) => b.traces - a.traces)
598
+ .slice(0, 12),
599
+ };
600
+ } catch (err) {
601
+ clearTimeout(timer);
602
+ return { state: 'unreachable', message: err?.message || 'fetch failed' };
603
+ }
604
+ }
605
+
606
+ function summarizeRetrievalEval(homeDir = os.homedir()) {
607
+ try {
608
+ const cachePath = path.join(homeDir, '.cx', 'evals', 'retrieval-latest.json');
609
+ if (!fs.existsSync(cachePath)) {
610
+ return {
611
+ state: 'empty',
612
+ message: 'No retrieval eval yet. Run `construct evals retrieval` to populate.',
613
+ };
614
+ }
615
+ const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
616
+ const r = cached.report?.summary || cached.report || {};
617
+ let history = [];
618
+ try {
619
+ const historyPath = path.join(homeDir, '.cx', 'evals', 'retrieval-history.jsonl');
620
+ if (fs.existsSync(historyPath)) {
621
+ const lines = fs.readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
622
+ history = lines.slice(-30).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
623
+ }
624
+ } catch { /* history is best-effort */ }
625
+ return {
626
+ state: 'ok',
627
+ ranAt: cached.ts,
628
+ fixture: cached.fixture,
629
+ recallAt5: r.recallAt5 ?? r.recall_at_5 ?? null,
630
+ precisionAt5: r.precisionAt5 ?? r.precision_at_5 ?? null,
631
+ mrr: r.mrr ?? null,
632
+ p50LatencyMs: r.p50LatencyMs ?? r.p50_latency_ms ?? null,
633
+ p95LatencyMs: r.p95LatencyMs ?? r.p95_latency_ms ?? null,
634
+ emptyRate: r.emptyRate ?? r.empty_rate ?? null,
635
+ fallbackRate: r.fallbackRate ?? r.fallback_rate ?? null,
636
+ queryCount: cached.report?.perQuery?.length ?? null,
637
+ history: history.map((h) => ({ ts: h.ts, recallAt5: h.recallAt5 ?? null, mrr: h.mrr ?? null })),
638
+ };
639
+ } catch (err) {
640
+ return { state: 'unreachable', message: err.message };
641
+ }
642
+ }
643
+
644
+ async function summarizeRecommendations() {
645
+ try {
646
+ const { recommendationStats } = await import('../embed/recommendation-store.mjs');
647
+ const stats = recommendationStats();
648
+ return {
649
+ state: stats.total > 0 ? 'ok' : 'empty',
650
+ total: stats.total,
651
+ active: stats.active,
652
+ dismissed: stats.dismissed,
653
+ byPriority: stats.byPriority,
654
+ byType: stats.byType,
655
+ };
656
+ } catch (err) {
657
+ return { state: 'unreachable', message: err.message };
658
+ }
659
+ }
660
+
661
+ async function summarizeIntegrations() {
662
+ try {
663
+ const { detectIntegrationConfig } = await import('../integrations/intake-integrations.mjs');
664
+ const config = detectIntegrationConfig();
665
+ return {
666
+ state: 'ok',
667
+ configured: Object.entries(config)
668
+ .filter(([, v]) => v === true || typeof v === 'string')
669
+ .map(([k]) => k)
670
+ .filter(k => k !== 'githubRepo' && k !== 'jiraHost' && k !== 'confluenceSpace'),
671
+ details: {
672
+ github: config.githubMethod ? `${config.githubMethod}${config.githubRepo ? ` (${config.githubRepo})` : ''}` : 'not configured',
673
+ jira: config.jira ? 'configured' : 'not configured',
674
+ confluence: config.confluence ? 'configured' : 'not configured',
675
+ },
676
+ };
677
+ } catch (err) {
678
+ return { state: 'unreachable', message: err.message };
679
+ }
680
+ }
681
+
682
+ function summarizeVectorStore(env) {
683
+ try {
684
+ const mode = env.CONSTRUCT_VECTOR_BACKEND || 'file';
685
+ const indexPath = env.CONSTRUCT_VECTOR_INDEX_PATH || '.cx/vector-index.json';
686
+ const resolved = path.isAbsolute(indexPath) ? indexPath : path.join(process.cwd(), indexPath);
687
+ if (!fs.existsSync(resolved)) {
688
+ return { state: 'empty', mode, message: 'No vector index yet. Run `construct ingest` to populate.' };
689
+ }
690
+ const stat = fs.statSync(resolved);
691
+ let records = 0;
692
+ let model = null;
693
+ let updatedAt = null;
694
+ try {
695
+ const json = JSON.parse(fs.readFileSync(resolved, 'utf8'));
696
+ records = Array.isArray(json.records) ? json.records.length : 0;
697
+ model = json.model || null;
698
+ updatedAt = json.updatedAt || null;
699
+ } catch { /* malformed — fall through to size-only stats */ }
700
+ return {
701
+ state: 'ok',
702
+ mode,
703
+ bytes: stat.size,
704
+ records,
705
+ model,
706
+ updatedAt,
707
+ path: resolved,
708
+ };
709
+ } catch (err) {
710
+ return { state: 'unreachable', message: err.message };
711
+ }
712
+ }
713
+
714
+ export async function buildInsights({ env = process.env, cwd = process.cwd(), rootDir = process.cwd() } = {}) {
715
+ const { measureUsage } = await import('../resources/budget.mjs');
716
+ const { summarizeHandoffs } = await import('../handoffs/inventory.mjs');
717
+ let resources;
718
+ try {
719
+ const u = measureUsage(cwd, env);
720
+ resources = { state: 'ok', ...u };
721
+ } catch (err) {
722
+ resources = { state: 'unreachable', message: err.message };
723
+ }
724
+ let handoffs;
725
+ try {
726
+ handoffs = summarizeHandoffs(cwd, env);
727
+ } catch (err) {
728
+ handoffs = { state: 'unreachable', message: err.message };
729
+ }
730
+ const [telemetry, telemetryTrend, telemetryLatency, cost, intake, beads, recommendations, integrations] = await Promise.all([
731
+ fetchTelemetrySummary(env),
732
+ summarizeTelemetryCostTrend(env),
733
+ summarizeTelemetryLatency(env),
734
+ summarizeCostLedger(env),
735
+ Promise.resolve(summarizeIntakeQueue(env, cwd)),
736
+ Promise.resolve(summarizeBeads(rootDir)),
737
+ summarizeRecommendations(),
738
+ summarizeIntegrations(),
739
+ ]);
740
+ const providers = summarizeModelProviders(env);
741
+ const vector = summarizeVectorStore(env);
742
+ const retrievalEval = summarizeRetrievalEval();
743
+ let sessionUsage;
744
+ try {
745
+ const { readSessionUsage } = await import('../status.mjs');
746
+ sessionUsage = readSessionUsage(os.homedir());
747
+ } catch { sessionUsage = null; }
748
+ return {
749
+ telemetry,
750
+ telemetryTrend,
751
+ telemetryLatency,
752
+ cost,
753
+ intake,
754
+ recommendations,
755
+ integrations,
756
+ beads,
757
+ resources,
758
+ handoffs,
759
+ providers,
760
+ vector,
761
+ retrievalEval,
762
+ sessionUsage,
763
+ generatedAt: new Date().toISOString(),
764
+ };
765
+ }