@geraldmaron/construct 1.0.0

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 (352) hide show
  1. package/.env.example +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +344 -0
  4. package/agents/prompts/cx-accessibility.md +29 -0
  5. package/agents/prompts/cx-ai-engineer.md +31 -0
  6. package/agents/prompts/cx-architect.md +40 -0
  7. package/agents/prompts/cx-business-strategist.md +25 -0
  8. package/agents/prompts/cx-data-analyst.md +30 -0
  9. package/agents/prompts/cx-data-engineer.md +32 -0
  10. package/agents/prompts/cx-debugger.md +27 -0
  11. package/agents/prompts/cx-designer.md +33 -0
  12. package/agents/prompts/cx-devil-advocate.md +36 -0
  13. package/agents/prompts/cx-docs-keeper.md +50 -0
  14. package/agents/prompts/cx-engineer.md +36 -0
  15. package/agents/prompts/cx-evaluator.md +26 -0
  16. package/agents/prompts/cx-explorer.md +38 -0
  17. package/agents/prompts/cx-legal-compliance.md +25 -0
  18. package/agents/prompts/cx-operations.md +28 -0
  19. package/agents/prompts/cx-orchestrator.md +30 -0
  20. package/agents/prompts/cx-platform-engineer.md +25 -0
  21. package/agents/prompts/cx-product-manager.md +30 -0
  22. package/agents/prompts/cx-qa.md +42 -0
  23. package/agents/prompts/cx-rd-lead.md +25 -0
  24. package/agents/prompts/cx-release-manager.md +31 -0
  25. package/agents/prompts/cx-researcher.md +31 -0
  26. package/agents/prompts/cx-reviewer.md +36 -0
  27. package/agents/prompts/cx-security.md +37 -0
  28. package/agents/prompts/cx-sre.md +26 -0
  29. package/agents/prompts/cx-test-automation.md +35 -0
  30. package/agents/prompts/cx-trace-reviewer.md +76 -0
  31. package/agents/prompts/cx-ux-researcher.md +30 -0
  32. package/agents/registry.json +879 -0
  33. package/agents/teams.json +94 -0
  34. package/bin/construct +613 -0
  35. package/commands/build/feature.md +21 -0
  36. package/commands/build/fix.md +20 -0
  37. package/commands/design/access.md +19 -0
  38. package/commands/design/flow.md +16 -0
  39. package/commands/design/ui.md +18 -0
  40. package/commands/measure/experiment.md +18 -0
  41. package/commands/measure/metrics.md +18 -0
  42. package/commands/measure/results.md +18 -0
  43. package/commands/plan/api.md +19 -0
  44. package/commands/plan/challenge.md +18 -0
  45. package/commands/plan/decide.md +20 -0
  46. package/commands/plan/feature.md +49 -0
  47. package/commands/plan/requirements.md +24 -0
  48. package/commands/remember/context.md +26 -0
  49. package/commands/remember/handoff.md +19 -0
  50. package/commands/remember/runbook.md +20 -0
  51. package/commands/review/code.md +21 -0
  52. package/commands/review/quality.md +19 -0
  53. package/commands/review/security.md +16 -0
  54. package/commands/ship/ready.md +19 -0
  55. package/commands/ship/release.md +19 -0
  56. package/commands/ship/status.md +18 -0
  57. package/commands/understand/docs.md +22 -0
  58. package/commands/understand/research.md +22 -0
  59. package/commands/understand/this.md +23 -0
  60. package/commands/understand/why.md +18 -0
  61. package/commands/work/clean.md +32 -0
  62. package/commands/work/drive.md +69 -0
  63. package/commands/work/optimize-prompts.md +46 -0
  64. package/commands/work/parallel-review.md +40 -0
  65. package/db/migrations/001_init.sql +38 -0
  66. package/langfuse/docker-compose.yml +82 -0
  67. package/lib/audit-skills.mjs +118 -0
  68. package/lib/auto-docs.mjs +293 -0
  69. package/lib/cli-commands.mjs +409 -0
  70. package/lib/codex-config.mjs +114 -0
  71. package/lib/comment-lint.mjs +191 -0
  72. package/lib/completions.mjs +170 -0
  73. package/lib/context-state.mjs +99 -0
  74. package/lib/cost.mjs +213 -0
  75. package/lib/distill.mjs +403 -0
  76. package/lib/efficiency.mjs +139 -0
  77. package/lib/env-config.mjs +53 -0
  78. package/lib/eval-harness.mjs +59 -0
  79. package/lib/features.mjs +209 -0
  80. package/lib/headhunt.mjs +597 -0
  81. package/lib/hooks/adaptive-lint.mjs +112 -0
  82. package/lib/hooks/agent-tracker.mjs +50 -0
  83. package/lib/hooks/bootstrap-guard.mjs +90 -0
  84. package/lib/hooks/comment-lint.mjs +26 -0
  85. package/lib/hooks/config-protection.mjs +52 -0
  86. package/lib/hooks/console-warn.mjs +43 -0
  87. package/lib/hooks/context-window-recovery.mjs +90 -0
  88. package/lib/hooks/continuation-enforcer.mjs +72 -0
  89. package/lib/hooks/dep-audit.mjs +155 -0
  90. package/lib/hooks/drive-guard.mjs +89 -0
  91. package/lib/hooks/edit-accumulator.mjs +36 -0
  92. package/lib/hooks/edit-error-recovery.mjs +46 -0
  93. package/lib/hooks/edit-guard.mjs +109 -0
  94. package/lib/hooks/env-check.mjs +80 -0
  95. package/lib/hooks/guard-bash.mjs +83 -0
  96. package/lib/hooks/mcp-audit.mjs +57 -0
  97. package/lib/hooks/mcp-health-check.mjs +51 -0
  98. package/lib/hooks/mcp-task-scope.mjs +47 -0
  99. package/lib/hooks/model-fallback.mjs +105 -0
  100. package/lib/hooks/pre-compact.mjs +212 -0
  101. package/lib/hooks/pre-push-gate.mjs +129 -0
  102. package/lib/hooks/read-tracker.mjs +129 -0
  103. package/lib/hooks/registry-sync.mjs +23 -0
  104. package/lib/hooks/scan-secrets.mjs +76 -0
  105. package/lib/hooks/session-start.mjs +95 -0
  106. package/lib/hooks/stop-notify.mjs +191 -0
  107. package/lib/hooks/stop-typecheck.mjs +83 -0
  108. package/lib/hooks/task-completed-guard.mjs +43 -0
  109. package/lib/hooks/teammate-idle-guard.mjs +54 -0
  110. package/lib/hooks/workflow-guard.mjs +62 -0
  111. package/lib/host-capabilities.mjs +123 -0
  112. package/lib/init-docs.mjs +382 -0
  113. package/lib/mcp/server.mjs +1123 -0
  114. package/lib/mcp-catalog.json +243 -0
  115. package/lib/mcp-manager.mjs +433 -0
  116. package/lib/mcp-platform-config.mjs +104 -0
  117. package/lib/model-router.mjs +810 -0
  118. package/lib/opencode-config.mjs +44 -0
  119. package/lib/opencode-runtime-plugin.mjs +909 -0
  120. package/lib/opencode-telemetry.mjs +433 -0
  121. package/lib/orchestration-policy.mjs +258 -0
  122. package/lib/prompt-composer.mjs +196 -0
  123. package/lib/prompt-metadata.mjs +68 -0
  124. package/lib/review.mjs +429 -0
  125. package/lib/role-preload.mjs +52 -0
  126. package/lib/schemas/decision.json +50 -0
  127. package/lib/schemas/implementation.json +51 -0
  128. package/lib/schemas/review-report.json +32 -0
  129. package/lib/schemas/test-report.json +43 -0
  130. package/lib/server/index.mjs +334 -0
  131. package/lib/server/static/app.js +841 -0
  132. package/lib/server/static/index.html +820 -0
  133. package/lib/service-manager.mjs +225 -0
  134. package/lib/setup.mjs +319 -0
  135. package/lib/status.mjs +707 -0
  136. package/lib/storage/backend.mjs +44 -0
  137. package/lib/storage/embeddings.mjs +70 -0
  138. package/lib/storage/hybrid-query.mjs +184 -0
  139. package/lib/storage/sql-store.mjs +55 -0
  140. package/lib/storage/state-source.mjs +101 -0
  141. package/lib/storage/sync.mjs +164 -0
  142. package/lib/storage/vector-store.mjs +59 -0
  143. package/lib/telemetry/backends/langfuse.mjs +58 -0
  144. package/lib/telemetry/backends/noop.mjs +16 -0
  145. package/lib/telemetry/langfuse-ingest.mjs +108 -0
  146. package/lib/telemetry/langfuse-model-sync.mjs +270 -0
  147. package/lib/telemetry/team-rollup.mjs +143 -0
  148. package/lib/toolkit-env.mjs +30 -0
  149. package/lib/validator.mjs +181 -0
  150. package/lib/workflow-state.mjs +794 -0
  151. package/package.json +53 -0
  152. package/personas/construct.md +94 -0
  153. package/platforms/claude/CLAUDE.md +30 -0
  154. package/platforms/claude/settings.template.json +472 -0
  155. package/platforms/opencode/config.template.json +65 -0
  156. package/platforms/opencode/plugins/construct-fallback.js +5 -0
  157. package/platforms/opencode/sync-config.mjs +69 -0
  158. package/rules/common/agents.md +55 -0
  159. package/rules/common/code-review.md +129 -0
  160. package/rules/common/coding-style.md +95 -0
  161. package/rules/common/comments.md +139 -0
  162. package/rules/common/cx-agent-routing.md +61 -0
  163. package/rules/common/cx-skill-routing.md +11 -0
  164. package/rules/common/development-workflow.md +49 -0
  165. package/rules/common/git-workflow.md +29 -0
  166. package/rules/common/hooks.md +35 -0
  167. package/rules/common/patterns.md +36 -0
  168. package/rules/common/performance.md +71 -0
  169. package/rules/common/security.md +34 -0
  170. package/rules/common/testing.md +62 -0
  171. package/rules/golang/coding-style.md +37 -0
  172. package/rules/golang/hooks.md +22 -0
  173. package/rules/golang/patterns.md +50 -0
  174. package/rules/golang/security.md +39 -0
  175. package/rules/golang/testing.md +36 -0
  176. package/rules/python/coding-style.md +47 -0
  177. package/rules/python/hooks.md +24 -0
  178. package/rules/python/patterns.md +44 -0
  179. package/rules/python/security.md +35 -0
  180. package/rules/python/testing.md +43 -0
  181. package/rules/swift/coding-style.md +52 -0
  182. package/rules/swift/hooks.md +25 -0
  183. package/rules/swift/patterns.md +71 -0
  184. package/rules/swift/security.md +38 -0
  185. package/rules/swift/testing.md +50 -0
  186. package/rules/typescript/coding-style.md +204 -0
  187. package/rules/typescript/hooks.md +27 -0
  188. package/rules/typescript/patterns.md +57 -0
  189. package/rules/typescript/security.md +33 -0
  190. package/rules/typescript/testing.md +23 -0
  191. package/rules/web/coding-style.md +101 -0
  192. package/rules/web/design-quality.md +68 -0
  193. package/rules/web/hooks.md +125 -0
  194. package/rules/web/patterns.md +84 -0
  195. package/rules/web/performance.md +69 -0
  196. package/rules/web/security.md +62 -0
  197. package/rules/web/testing.md +60 -0
  198. package/rules/zh/README.md +113 -0
  199. package/rules/zh/agents.md +55 -0
  200. package/rules/zh/code-review.md +129 -0
  201. package/rules/zh/coding-style.md +53 -0
  202. package/rules/zh/development-workflow.md +49 -0
  203. package/rules/zh/git-workflow.md +29 -0
  204. package/rules/zh/hooks.md +35 -0
  205. package/rules/zh/patterns.md +36 -0
  206. package/rules/zh/performance.md +60 -0
  207. package/rules/zh/security.md +34 -0
  208. package/rules/zh/testing.md +34 -0
  209. package/skills/ai/agent-dev.md +102 -0
  210. package/skills/ai/llm-security.md +105 -0
  211. package/skills/ai/ml-ops.md +169 -0
  212. package/skills/ai/orchestration-workflow.md +87 -0
  213. package/skills/ai/prompt-and-eval.md +114 -0
  214. package/skills/ai/prompt-optimizer.md +114 -0
  215. package/skills/ai/rag-system.md +104 -0
  216. package/skills/architecture/api-design.md +100 -0
  217. package/skills/architecture/caching.md +101 -0
  218. package/skills/architecture/cloud-native.md +97 -0
  219. package/skills/architecture/message-queue.md +100 -0
  220. package/skills/architecture/security-arch.md +105 -0
  221. package/skills/development/cpp.md +127 -0
  222. package/skills/development/go.md +129 -0
  223. package/skills/development/java.md +134 -0
  224. package/skills/development/kotlin.md +140 -0
  225. package/skills/development/mobile-crossplatform.md +144 -0
  226. package/skills/development/python.md +109 -0
  227. package/skills/development/rust.md +127 -0
  228. package/skills/development/shell.md +127 -0
  229. package/skills/development/swift.md +129 -0
  230. package/skills/development/typescript.md +129 -0
  231. package/skills/devops/ci-cd.md +108 -0
  232. package/skills/devops/containerization.md +106 -0
  233. package/skills/devops/cost-optimization.md +105 -0
  234. package/skills/devops/data-engineering.md +181 -0
  235. package/skills/devops/database.md +95 -0
  236. package/skills/devops/dependency-management.md +91 -0
  237. package/skills/devops/devsecops.md +96 -0
  238. package/skills/devops/git-workflow.md +100 -0
  239. package/skills/devops/incident-response.md +167 -0
  240. package/skills/devops/monorepo.md +87 -0
  241. package/skills/devops/observability.md +103 -0
  242. package/skills/devops/performance.md +104 -0
  243. package/skills/devops/testing.md +104 -0
  244. package/skills/docs/adr-workflow.md +32 -0
  245. package/skills/docs/backlog-proposal-workflow.md +19 -0
  246. package/skills/docs/customer-profile-workflow.md +22 -0
  247. package/skills/docs/evidence-ingest-workflow.md +23 -0
  248. package/skills/docs/init-docs.md +160 -0
  249. package/skills/docs/init-project.md +50 -0
  250. package/skills/docs/prd-workflow.md +57 -0
  251. package/skills/docs/prfaq-workflow.md +25 -0
  252. package/skills/docs/product-intelligence-review.md +22 -0
  253. package/skills/docs/product-intelligence-workflow.md +62 -0
  254. package/skills/docs/product-signal-workflow.md +27 -0
  255. package/skills/docs/research-workflow.md +28 -0
  256. package/skills/docs/runbook-workflow.md +24 -0
  257. package/skills/exploration/repo-map.md +297 -0
  258. package/skills/frameworks/django.md +159 -0
  259. package/skills/frameworks/nextjs.md +148 -0
  260. package/skills/frameworks/react.md +132 -0
  261. package/skills/frameworks/spring-boot.md +165 -0
  262. package/skills/frontend-design/accessibility.md +153 -0
  263. package/skills/frontend-design/component-patterns.md +111 -0
  264. package/skills/frontend-design/engineering.md +122 -0
  265. package/skills/frontend-design/state-management.md +118 -0
  266. package/skills/frontend-design/ui-aesthetics.md +102 -0
  267. package/skills/frontend-design/ux-principles.md +126 -0
  268. package/skills/quality-gates/review-work.md +90 -0
  269. package/skills/quality-gates/verify-change.md +94 -0
  270. package/skills/quality-gates/verify-module.md +96 -0
  271. package/skills/quality-gates/verify-quality.md +93 -0
  272. package/skills/quality-gates/verify-security.md +95 -0
  273. package/skills/roles/architect.ai-systems.md +37 -0
  274. package/skills/roles/architect.data.md +37 -0
  275. package/skills/roles/architect.enterprise.md +37 -0
  276. package/skills/roles/architect.integration.md +37 -0
  277. package/skills/roles/architect.md +68 -0
  278. package/skills/roles/architect.platform.md +37 -0
  279. package/skills/roles/data-analyst.experiment.md +37 -0
  280. package/skills/roles/data-analyst.md +68 -0
  281. package/skills/roles/data-analyst.product-intelligence.md +37 -0
  282. package/skills/roles/data-analyst.product.md +37 -0
  283. package/skills/roles/data-analyst.telemetry.md +37 -0
  284. package/skills/roles/data-engineer.pipeline.md +37 -0
  285. package/skills/roles/data-engineer.vector-retrieval.md +37 -0
  286. package/skills/roles/data-engineer.warehouse.md +37 -0
  287. package/skills/roles/debugger.md +68 -0
  288. package/skills/roles/designer.accessibility.md +43 -0
  289. package/skills/roles/designer.md +68 -0
  290. package/skills/roles/engineer.ai.md +49 -0
  291. package/skills/roles/engineer.data.md +49 -0
  292. package/skills/roles/engineer.md +85 -0
  293. package/skills/roles/engineer.platform.md +49 -0
  294. package/skills/roles/operator.docs.md +43 -0
  295. package/skills/roles/operator.md +68 -0
  296. package/skills/roles/operator.release.md +43 -0
  297. package/skills/roles/operator.sre.md +43 -0
  298. package/skills/roles/orchestrator.md +66 -0
  299. package/skills/roles/product-manager.ai-product.md +37 -0
  300. package/skills/roles/product-manager.business-strategy.md +43 -0
  301. package/skills/roles/product-manager.enterprise.md +37 -0
  302. package/skills/roles/product-manager.growth.md +37 -0
  303. package/skills/roles/product-manager.md +67 -0
  304. package/skills/roles/product-manager.platform.md +37 -0
  305. package/skills/roles/product-manager.product.md +37 -0
  306. package/skills/roles/qa.ai-eval.md +37 -0
  307. package/skills/roles/qa.api-contract.md +37 -0
  308. package/skills/roles/qa.data-pipeline.md +37 -0
  309. package/skills/roles/qa.md +68 -0
  310. package/skills/roles/qa.test-automation.md +49 -0
  311. package/skills/roles/qa.web-ui.md +37 -0
  312. package/skills/roles/researcher.explorer.md +43 -0
  313. package/skills/roles/researcher.md +68 -0
  314. package/skills/roles/researcher.ux.md +43 -0
  315. package/skills/roles/reviewer.devil-advocate.md +43 -0
  316. package/skills/roles/reviewer.evaluator.md +43 -0
  317. package/skills/roles/reviewer.md +68 -0
  318. package/skills/roles/reviewer.trace.md +43 -0
  319. package/skills/roles/security.ai.md +37 -0
  320. package/skills/roles/security.appsec.md +37 -0
  321. package/skills/roles/security.cloud.md +37 -0
  322. package/skills/roles/security.legal-compliance.md +49 -0
  323. package/skills/roles/security.md +68 -0
  324. package/skills/roles/security.privacy.md +37 -0
  325. package/skills/roles/security.supply-chain.md +37 -0
  326. package/skills/routing.md +139 -0
  327. package/skills/security/blue-team.md +83 -0
  328. package/skills/security/code-audit.md +94 -0
  329. package/skills/security/pentest.md +84 -0
  330. package/skills/security/red-team.md +81 -0
  331. package/skills/security/threat-intel.md +89 -0
  332. package/skills/security/vuln-research.md +87 -0
  333. package/skills/utility/clean-code.md +165 -0
  334. package/sync-agents.mjs +899 -0
  335. package/templates/docs/adr.md +27 -0
  336. package/templates/docs/backlog-proposal.md +26 -0
  337. package/templates/docs/customer-profile.md +34 -0
  338. package/templates/docs/evidence-brief.md +37 -0
  339. package/templates/docs/incident-report.md +46 -0
  340. package/templates/docs/memo.md +27 -0
  341. package/templates/docs/meta-prd.md +56 -0
  342. package/templates/docs/one-pager.md +24 -0
  343. package/templates/docs/prd-business.md +61 -0
  344. package/templates/docs/prd-platform.md +62 -0
  345. package/templates/docs/prd.md +47 -0
  346. package/templates/docs/prfaq.md +31 -0
  347. package/templates/docs/product-intelligence-report.md +31 -0
  348. package/templates/docs/research-brief.md +29 -0
  349. package/templates/docs/rfc-platform.md +60 -0
  350. package/templates/docs/rfc.md +46 -0
  351. package/templates/docs/runbook.md +33 -0
  352. package/templates/docs/signal-brief.md +25 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * lib/telemetry/backends/langfuse.mjs — Langfuse trace backend adapter.
3
+ *
4
+ * Reads LANGFUSE_BASEURL, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY from env.
5
+ * Implements the TraceBackend interface: listTraces(teamId, windowMs) → Trace[].
6
+ */
7
+
8
+ export const name = 'langfuse';
9
+
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
+ return {
15
+ Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
16
+ 'Content-Type': 'application/json',
17
+ };
18
+ }
19
+
20
+ function baseUrl() {
21
+ return (process.env.LANGFUSE_BASEURL ?? 'https://cloud.langfuse.com').replace(/\/$/, '');
22
+ }
23
+
24
+ export async function isAvailable() {
25
+ return Boolean(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY);
26
+ }
27
+
28
+ /**
29
+ * Fetch traces associated with a teamId within a time window.
30
+ * Returns an array of normalized Trace objects.
31
+ *
32
+ * @param {string} teamId - The overlay/team ID to filter by.
33
+ * @param {number} windowMs - Lookback window in milliseconds.
34
+ * @returns {Promise<Trace[]>}
35
+ */
36
+ export async function listTraces(teamId, windowMs) {
37
+ const since = new Date(Date.now() - windowMs).toISOString();
38
+ const url = `${baseUrl()}/api/public/traces?tags=${encodeURIComponent(teamId)}&fromTimestamp=${since}&limit=100`;
39
+
40
+ const resp = await fetch(url, { headers: baseHeaders() });
41
+ if (!resp.ok) {
42
+ const body = await resp.text();
43
+ throw new Error(`Langfuse API error ${resp.status}: ${body}`);
44
+ }
45
+ const json = await resp.json();
46
+
47
+ return (json.data ?? []).map((t) => ({
48
+ id: t.id,
49
+ teamId,
50
+ agentName: t.metadata?.agentName ?? t.name ?? 'unknown',
51
+ status: t.metadata?.status ?? 'unknown',
52
+ latencyMs: t.latency != null ? Math.round(t.latency * 1000) : null, // Langfuse returns seconds
53
+ qualityScore: t.scores?.find((s) => s.name === 'quality')?.value ?? null,
54
+ createdAt: t.timestamp,
55
+ blockers: t.metadata?.blockers ?? [],
56
+ handoffs: t.metadata?.handoffs ?? 0,
57
+ }));
58
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * lib/telemetry/backends/noop.mjs — No-op trace backend for when no backend is configured.
3
+ *
4
+ * Returns empty results for all queries. Useful for local debug logging and
5
+ * environments where telemetry is optional.
6
+ */
7
+
8
+ export const name = 'noop';
9
+
10
+ export async function listTraces(_teamId, _windowMs) {
11
+ return [];
12
+ }
13
+
14
+ export async function isAvailable() {
15
+ return true;
16
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * lib/telemetry/langfuse-ingest.mjs — Langfuse batch ingestion client.
3
+ *
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.
6
+ */
7
+ import { randomUUID } from "node:crypto";
8
+
9
+ const DEFAULT_FLUSH_INTERVAL_MS = 500;
10
+ const DEFAULT_MAX_BATCH = 50;
11
+ const MAX_PAYLOAD_CHARS = 16_000;
12
+
13
+ function truncate(value) {
14
+ if (value == null) return value;
15
+ if (typeof value === "string") {
16
+ return value.length > MAX_PAYLOAD_CHARS
17
+ ? `${value.slice(0, MAX_PAYLOAD_CHARS)}…[truncated ${value.length - MAX_PAYLOAD_CHARS} chars]`
18
+ : value;
19
+ }
20
+ try {
21
+ const json = JSON.stringify(value);
22
+ if (json.length <= MAX_PAYLOAD_CHARS) return value;
23
+ return { _truncated: true, originalBytes: json.length, preview: json.slice(0, MAX_PAYLOAD_CHARS) };
24
+ } catch {
25
+ return value;
26
+ }
27
+ }
28
+
29
+ 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
+ flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS,
34
+ maxBatch = DEFAULT_MAX_BATCH,
35
+ onError = () => {},
36
+ fetchImpl = globalThis.fetch,
37
+ } = {}) {
38
+ const available = Boolean(publicKey && secretKey && fetchImpl);
39
+ const queue = [];
40
+ let flushTimer = null;
41
+ let inflight = Promise.resolve();
42
+
43
+ const authHeader = available
44
+ ? `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`
45
+ : "";
46
+
47
+ function scheduleFlush() {
48
+ if (!available || flushTimer) return;
49
+ flushTimer = setTimeout(() => {
50
+ flushTimer = null;
51
+ void flushBatch();
52
+ }, flushIntervalMs);
53
+ if (typeof flushTimer.unref === "function") flushTimer.unref();
54
+ }
55
+
56
+ async function flushBatch() {
57
+ if (!available || queue.length === 0) return;
58
+ const batch = queue.splice(0, maxBatch);
59
+ const payload = JSON.stringify({ batch });
60
+ const prev = inflight;
61
+ inflight = (async () => {
62
+ try { await prev; } catch { /* ignore prior */ }
63
+ try {
64
+ const res = await fetchImpl(`${baseUrl}/api/public/ingestion`, {
65
+ method: "POST",
66
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
67
+ body: payload,
68
+ });
69
+ if (!res.ok && res.status !== 207) {
70
+ let detail = "";
71
+ try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
72
+ onError(new Error(`langfuse ingest ${res.status}: ${detail}`));
73
+ }
74
+ } catch (err) {
75
+ onError(err);
76
+ }
77
+ })();
78
+ if (queue.length >= maxBatch) scheduleFlush();
79
+ }
80
+
81
+ function push(type, body) {
82
+ if (!available || !body) return;
83
+ const cleaned = {
84
+ ...body,
85
+ input: body.input !== undefined ? truncate(body.input) : undefined,
86
+ output: body.output !== undefined ? truncate(body.output) : undefined,
87
+ };
88
+ queue.push({ id: randomUUID(), type, timestamp: new Date().toISOString(), body: cleaned });
89
+ if (queue.length >= maxBatch) { void flushBatch(); return; }
90
+ scheduleFlush();
91
+ }
92
+
93
+ return {
94
+ 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),
101
+ async flush() {
102
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
103
+ while (queue.length > 0) await flushBatch();
104
+ await inflight;
105
+ },
106
+ queueSize: () => queue.length,
107
+ };
108
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * lib/telemetry/langfuse-model-sync.mjs — Sync live model pricing into Langfuse.
3
+ *
4
+ * Fetches OpenRouter's public model list for live pricing, merges with a static
5
+ * Anthropic table (no machine-readable public pricing API), and upserts all
6
+ * entries into Langfuse's custom model registry so that traces show real costs.
7
+ *
8
+ * Designed to be called once at plugin init; subsequent calls within the same
9
+ * process are skipped via a module-level guard to avoid hammering the APIs.
10
+ */
11
+
12
+ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
13
+
14
+ // Static Anthropic pricing (USD per token, as of 2026-04).
15
+ // No machine-readable public pricing endpoint — update on model price changes.
16
+ const ANTHROPIC_MODELS = [
17
+ {
18
+ modelName: "claude-opus-4-7",
19
+ matchPattern: "(?i)^(anthropic/)?claude-opus-4-7$",
20
+ inputPrice: 15 / 1_000_000,
21
+ outputPrice: 75 / 1_000_000,
22
+ },
23
+ {
24
+ modelName: "claude-opus-4-5",
25
+ matchPattern: "(?i)^(anthropic/)?claude-opus-4-5$",
26
+ inputPrice: 15 / 1_000_000,
27
+ outputPrice: 75 / 1_000_000,
28
+ },
29
+ {
30
+ modelName: "claude-sonnet-4-6",
31
+ matchPattern: "(?i)^(anthropic/)?claude-sonnet-4-6$",
32
+ inputPrice: 3 / 1_000_000,
33
+ outputPrice: 15 / 1_000_000,
34
+ },
35
+ {
36
+ modelName: "claude-sonnet-4-5",
37
+ matchPattern: "(?i)^(anthropic/)?claude-sonnet-4-5$",
38
+ inputPrice: 3 / 1_000_000,
39
+ outputPrice: 15 / 1_000_000,
40
+ },
41
+ {
42
+ modelName: "claude-haiku-4-5",
43
+ matchPattern: "(?i)^(anthropic/)?claude-haiku-4-5(-\\d+)?$",
44
+ inputPrice: 0.8 / 1_000_000,
45
+ outputPrice: 4 / 1_000_000,
46
+ },
47
+ ];
48
+
49
+ // GitHub Copilot uses a per-request billing model, not per-token.
50
+ // Register at $0 so Langfuse records usage without phantom costs.
51
+ const GITHUB_COPILOT_MODELS = [
52
+ { modelName: "github-copilot/gpt-5.4", matchPattern: "(?i)^(github-copilot/)?gpt-5\\.4$" },
53
+ { modelName: "github-copilot/gpt-4.1", matchPattern: "(?i)^(github-copilot/)?gpt-4\\.1$" },
54
+ { modelName: "github-copilot/claude-sonnet-4-5", matchPattern: "(?i)^github-copilot/claude-sonnet" },
55
+ { modelName: "github-copilot/gemini-2.5-pro", matchPattern: "(?i)^github-copilot/gemini" },
56
+ { modelName: "github-copilot/o4-mini", matchPattern: "(?i)^github-copilot/o4" },
57
+ ].map((m) => ({ ...m, inputPrice: 0, outputPrice: 0 }));
58
+
59
+ let syncDone = false;
60
+ let cachedPricingCatalog = buildPricingCatalog();
61
+
62
+ function normalizeModelKey(modelName = "") {
63
+ return String(modelName ?? "").trim().toLowerCase();
64
+ }
65
+
66
+ function indexPricingEntry(map, model) {
67
+ if (!model?.modelName) return;
68
+ const key = normalizeModelKey(model.modelName);
69
+ const entry = {
70
+ modelName: model.modelName,
71
+ inputPrice: Number(model.inputPrice) || 0,
72
+ outputPrice: Number(model.outputPrice) || 0,
73
+ source: model.source || "catalog",
74
+ };
75
+ map.set(key, entry);
76
+ if (key.startsWith("anthropic/")) map.set(key.replace(/^anthropic\//, ""), entry);
77
+ if (key.startsWith("github-copilot/")) map.set(key.replace(/^github-copilot\//, ""), entry);
78
+ if (key.startsWith("openrouter/")) map.set(key.replace(/^openrouter\//, ""), entry);
79
+ }
80
+
81
+ export function buildPricingCatalog(openRouterModels = []) {
82
+ const map = new Map();
83
+ for (const model of [...ANTHROPIC_MODELS, ...GITHUB_COPILOT_MODELS]) {
84
+ indexPricingEntry(map, { ...model, source: "static" });
85
+ }
86
+ for (const model of openRouterModels) {
87
+ indexPricingEntry(map, { ...model, source: "openrouter" });
88
+ }
89
+ return map;
90
+ }
91
+
92
+ export function resolveModelPricing(modelName, catalog = cachedPricingCatalog) {
93
+ const key = normalizeModelKey(modelName);
94
+ if (!key) return null;
95
+ return catalog.get(key) || null;
96
+ }
97
+
98
+ export function estimateUsageCost(modelName, usage = {}, catalog = cachedPricingCatalog) {
99
+ const pricing = resolveModelPricing(modelName, catalog);
100
+ if (!pricing) return { costUsd: 0, costSource: "unavailable", modelName: modelName || null };
101
+
102
+ const inputTokens = Number(usage.inputTokens ?? 0);
103
+ const outputTokens = Number(usage.outputTokens ?? 0);
104
+ const reasoningTokens = Number(usage.reasoningTokens ?? 0);
105
+ const billableOutputTokens = outputTokens + reasoningTokens;
106
+ const inputCost = inputTokens * pricing.inputPrice;
107
+ const outputCost = billableOutputTokens * pricing.outputPrice;
108
+ const costUsd = inputCost + outputCost;
109
+
110
+ return {
111
+ costUsd,
112
+ costSource: `estimated:${pricing.source}`,
113
+ modelName: pricing.modelName,
114
+ pricing,
115
+ breakdown: {
116
+ inputCostUsd: inputCost,
117
+ outputCostUsd: outputCost,
118
+ billableInputTokens: inputTokens,
119
+ billableOutputTokens,
120
+ },
121
+ };
122
+ }
123
+
124
+ function buildAuthHeader(publicKey, secretKey) {
125
+ return `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`;
126
+ }
127
+
128
+ async function fetchOpenRouterModels(fetchImpl = globalThis.fetch) {
129
+ try {
130
+ const res = await fetchImpl(OPENROUTER_MODELS_URL, {
131
+ headers: { "User-Agent": "construct-telemetry/1.0" },
132
+ signal: AbortSignal.timeout(10_000),
133
+ });
134
+ if (!res.ok) return [];
135
+ const { data } = await res.json();
136
+ if (!Array.isArray(data)) return [];
137
+ return data
138
+ .filter((m) => m?.id && m?.pricing)
139
+ .map((m) => {
140
+ const inputPrice = parseFloat(m.pricing.prompt) || 0;
141
+ const outputPrice = parseFloat(m.pricing.completion) || 0;
142
+ // Escape dots and slashes in match pattern
143
+ const escaped = m.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
144
+ return {
145
+ modelName: m.id,
146
+ matchPattern: `(?i)^${escaped}$`,
147
+ inputPrice,
148
+ outputPrice,
149
+ };
150
+ });
151
+ } catch {
152
+ return [];
153
+ }
154
+ }
155
+
156
+ async function listLangfuseModels(baseUrl, authHeader, fetchImpl) {
157
+ const all = [];
158
+ let page = 1;
159
+ try {
160
+ while (true) {
161
+ const res = await fetchImpl(`${baseUrl}/api/public/models?limit=100&page=${page}`, {
162
+ headers: { Authorization: authHeader },
163
+ signal: AbortSignal.timeout(8_000),
164
+ });
165
+ if (!res.ok) break;
166
+ const body = await res.json();
167
+ const items = Array.isArray(body?.data) ? body.data : [];
168
+ all.push(...items);
169
+ if (items.length < 100) break;
170
+ page++;
171
+ }
172
+ } catch { /* return what we have */ }
173
+ return all;
174
+ }
175
+
176
+ async function deleteLangfuseModel(baseUrl, authHeader, id, fetchImpl) {
177
+ try {
178
+ await fetchImpl(`${baseUrl}/api/public/models/${id}`, {
179
+ method: "DELETE",
180
+ headers: { Authorization: authHeader },
181
+ signal: AbortSignal.timeout(5_000),
182
+ });
183
+ } catch { /* best effort */ }
184
+ }
185
+
186
+ async function upsertLangfuseModel(baseUrl, authHeader, model, existing, fetchImpl) {
187
+ const match = existing.find((e) => e.modelName === model.modelName);
188
+ if (match) {
189
+ // Langfuse has no PATCH; delete then recreate if pricing changed.
190
+ const priceChanged =
191
+ Math.abs((match.inputPrice ?? 0) - model.inputPrice) > 1e-12 ||
192
+ Math.abs((match.outputPrice ?? 0) - model.outputPrice) > 1e-12;
193
+ if (!priceChanged) return "unchanged";
194
+ await deleteLangfuseModel(baseUrl, authHeader, match.id, fetchImpl);
195
+ }
196
+ const body = JSON.stringify({
197
+ modelName: model.modelName,
198
+ matchPattern: model.matchPattern,
199
+ unit: "TOKENS",
200
+ inputPrice: model.inputPrice,
201
+ outputPrice: model.outputPrice,
202
+ tokenizerId: model.modelName.includes("claude") ? "claude" : null,
203
+ });
204
+ try {
205
+ const res = await fetchImpl(`${baseUrl}/api/public/models`, {
206
+ method: "POST",
207
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
208
+ body,
209
+ signal: AbortSignal.timeout(5_000),
210
+ });
211
+ return res.ok ? "created" : "error";
212
+ } catch {
213
+ return "error";
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Syncs model pricing into Langfuse. Safe to call multiple times — only runs once per process.
219
+ *
220
+ * @param {object} opts
221
+ * @param {string} opts.baseUrl — Langfuse base URL
222
+ * @param {string} opts.publicKey
223
+ * @param {string} opts.secretKey
224
+ * @param {Function} [opts.fetchImpl] — injectable fetch for tests
225
+ * @param {boolean} [opts.force] — bypass the once-per-process guard
226
+ * @returns {Promise<{synced: number, unchanged: number, errors: number}>}
227
+ */
228
+ export async function syncModelPricing({
229
+ baseUrl = (process.env.LANGFUSE_BASEURL ?? "https://cloud.langfuse.com").replace(/\/$/, ""),
230
+ publicKey = process.env.LANGFUSE_PUBLIC_KEY,
231
+ secretKey = process.env.LANGFUSE_SECRET_KEY,
232
+ fetchImpl = globalThis.fetch,
233
+ force = false,
234
+ } = {}) {
235
+ if (!force && syncDone) return { synced: 0, unchanged: 0, errors: 0 };
236
+ if (!publicKey || !secretKey || !fetchImpl) return { synced: 0, unchanged: 0, errors: 0 };
237
+
238
+ syncDone = true;
239
+
240
+ const authHeader = buildAuthHeader(publicKey, secretKey);
241
+
242
+ const [openRouterModels, existing] = await Promise.all([
243
+ fetchOpenRouterModels(fetchImpl),
244
+ listLangfuseModels(baseUrl, authHeader, fetchImpl),
245
+ ]);
246
+
247
+ const allModels = [...ANTHROPIC_MODELS, ...GITHUB_COPILOT_MODELS, ...openRouterModels];
248
+ cachedPricingCatalog = buildPricingCatalog(openRouterModels);
249
+
250
+ let synced = 0;
251
+ let unchanged = 0;
252
+ let errors = 0;
253
+
254
+ for (const model of allModels) {
255
+ const result = await upsertLangfuseModel(baseUrl, authHeader, model, existing, fetchImpl);
256
+ if (result === "created") synced++;
257
+ else if (result === "unchanged") unchanged++;
258
+ else errors++;
259
+ }
260
+
261
+ return { synced, unchanged, errors };
262
+ }
263
+
264
+ export function resetSyncGuard() {
265
+ syncDone = false;
266
+ }
267
+
268
+ export function resetPricingCatalog() {
269
+ cachedPricingCatalog = buildPricingCatalog();
270
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * lib/telemetry/team-rollup.mjs — Backend-agnostic team telemetry rollup.
3
+ *
4
+ * Groups agent traces by teamId (stamped by construct headhunt at overlay creation),
5
+ * computes per-team success rates, latency, handoff counts, and common blockers.
6
+ * Backend is selected via CONSTRUCT_TRACE_BACKEND env var (default: langfuse).
7
+ * Called by 'construct team review'.
8
+ */
9
+
10
+ import * as langfuseBackend from './backends/langfuse.mjs';
11
+ import * as noopBackend from './backends/noop.mjs';
12
+
13
+ const BACKENDS = {
14
+ langfuse: langfuseBackend,
15
+ noop: noopBackend,
16
+ };
17
+
18
+ const DEFAULT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
19
+
20
+ function resolveBackend() {
21
+ const name = process.env.CONSTRUCT_TRACE_BACKEND ?? 'langfuse';
22
+ const backend = BACKENDS[name];
23
+ if (!backend) throw new Error(`Unknown trace backend: ${name}. Available: ${Object.keys(BACKENDS).join(', ')}`);
24
+ return backend;
25
+ }
26
+
27
+ function median(values) {
28
+ if (values.length === 0) return null;
29
+ const sorted = [...values].sort((a, b) => a - b);
30
+ const mid = Math.floor(sorted.length / 2);
31
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
32
+ }
33
+
34
+ function rollupByTeam(traces) {
35
+ const byTeam = {};
36
+ for (const trace of traces) {
37
+ const key = trace.teamId ?? 'unknown';
38
+ if (!byTeam[key]) byTeam[key] = { teamId: key, traces: [] };
39
+ byTeam[key].traces.push(trace);
40
+ }
41
+
42
+ return Object.values(byTeam).map(({ teamId, traces: tl }) => {
43
+ const total = tl.length;
44
+ const successes = tl.filter((t) => t.status === 'DONE').length;
45
+ const latencies = tl.map((t) => t.latencyMs).filter((v) => v != null);
46
+ const qualityScores = tl.map((t) => t.qualityScore).filter((v) => v != null);
47
+ const totalHandoffs = tl.reduce((acc, t) => acc + (t.handoffs ?? 0), 0);
48
+
49
+ const blockerCounts = {};
50
+ for (const t of tl) {
51
+ for (const b of (t.blockers ?? [])) {
52
+ blockerCounts[b] = (blockerCounts[b] ?? 0) + 1;
53
+ }
54
+ }
55
+ const topBlockers = Object.entries(blockerCounts)
56
+ .sort((a, b) => b[1] - a[1])
57
+ .slice(0, 3)
58
+ .map(([label, count]) => ({ label, count }));
59
+
60
+ return {
61
+ teamId,
62
+ total,
63
+ successRate: total > 0 ? (successes / total) : null,
64
+ medianLatencyMs: median(latencies),
65
+ medianQualityScore: median(qualityScores),
66
+ totalHandoffs,
67
+ topBlockers,
68
+ agents: [...new Set(tl.map((t) => t.agentName))],
69
+ };
70
+ });
71
+ }
72
+
73
+ function formatRollup(rollups) {
74
+ const lines = [];
75
+ lines.push('Team Telemetry Rollup');
76
+ lines.push('═════════════════════');
77
+ lines.push('');
78
+
79
+ if (rollups.length === 0) {
80
+ lines.push(' No team traces found in the selected window.');
81
+ lines.push(' Run construct headhunt with --temp or a template, then tasks will generate traces.');
82
+ return lines.join('\n');
83
+ }
84
+
85
+ for (const r of rollups) {
86
+ const successPct = r.successRate != null ? `${(r.successRate * 100).toFixed(0)}%` : 'n/a';
87
+ const latency = r.medianLatencyMs != null ? `${(r.medianLatencyMs / 1000).toFixed(1)}s` : 'n/a';
88
+ const quality = r.medianQualityScore != null ? r.medianQualityScore.toFixed(2) : 'n/a';
89
+ lines.push(` Team: ${r.teamId}`);
90
+ lines.push(` Runs: ${r.total} Success: ${successPct} Median latency: ${latency} Quality: ${quality}`);
91
+ lines.push(` Handoffs: ${r.totalHandoffs} Agents: ${r.agents.join(', ')}`);
92
+ if (r.topBlockers.length > 0) {
93
+ lines.push(` Top blockers: ${r.topBlockers.map((b) => `${b.label} (${b.count})`).join(', ')}`);
94
+ }
95
+ lines.push('');
96
+ }
97
+
98
+ return lines.join('\n');
99
+ }
100
+
101
+ export async function runTeamRollup({ windowMs = DEFAULT_WINDOW_MS, teamIds = [], silent = false } = {}) {
102
+ const backend = resolveBackend();
103
+
104
+ if (!(await backend.isAvailable())) {
105
+ if (!silent) {
106
+ process.stdout.write(`Trace backend '${backend.name}' is not configured — using noop.\n`);
107
+ process.stdout.write('Set CONSTRUCT_TRACE_BACKEND and required credentials to enable telemetry.\n');
108
+ }
109
+ return [];
110
+ }
111
+
112
+ const targets = teamIds.length > 0 ? teamIds : ['all'];
113
+ const allTraces = [];
114
+
115
+ for (const teamId of targets) {
116
+ const traces = await backend.listTraces(teamId === 'all' ? null : teamId, windowMs);
117
+ allTraces.push(...traces);
118
+ }
119
+
120
+ const rollups = rollupByTeam(allTraces);
121
+
122
+ if (!silent) {
123
+ process.stdout.write(formatRollup(rollups) + '\n');
124
+ }
125
+
126
+ return rollups;
127
+ }
128
+
129
+ export async function runTeamReviewCli(args = []) {
130
+ const windowArg = args.find((a) => a.startsWith('--window='))?.split('=')[1];
131
+ const windowMs = windowArg ? parseWindowArg(windowArg) : DEFAULT_WINDOW_MS;
132
+ const teamIds = args.filter((a) => !a.startsWith('--'));
133
+ await runTeamRollup({ windowMs, teamIds });
134
+ }
135
+
136
+ function parseWindowArg(value) {
137
+ const match = value.match(/^(\d+)(d|h|m)?$/);
138
+ if (!match) return DEFAULT_WINDOW_MS;
139
+ const n = parseInt(match[1], 10);
140
+ const unit = match[2] ?? 'd';
141
+ const multipliers = { d: 86_400_000, h: 3_600_000, m: 60_000 };
142
+ return n * (multipliers[unit] ?? multipliers.d);
143
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * lib/toolkit-env.mjs — Load `.env` from CX_TOOLKIT_DIR into process.env.
3
+ *
4
+ * Shared by OpenCode plugin and MCP server so Langfuse/OpenRouter creds
5
+ * reach every host that drives Construct.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ export function loadToolkitEnv(toolkitDir, env = process.env) {
11
+ if (!toolkitDir) return;
12
+ const envPath = join(toolkitDir, ".env");
13
+ if (!existsSync(envPath)) return;
14
+ try {
15
+ const text = readFileSync(envPath, "utf8");
16
+ for (const raw of text.split(/\r?\n/)) {
17
+ const line = raw.trim();
18
+ if (!line || line.startsWith("#")) continue;
19
+ const eq = line.indexOf("=");
20
+ if (eq <= 0) continue;
21
+ const key = line.slice(0, eq).trim();
22
+ if (!key || env[key] !== undefined) continue;
23
+ let val = line.slice(eq + 1).trim();
24
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
25
+ val = val.slice(1, -1);
26
+ }
27
+ env[key] = val;
28
+ }
29
+ } catch { /* best effort */ }
30
+ }