@geraldmaron/construct 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/telemetry/backends/local.mjs — Local JSONL trace backend.
|
|
3
|
+
*
|
|
4
|
+
* Reads trace events from ~/.cx/traces/*.jsonl files.
|
|
5
|
+
* No remote calls. Always available. Used when CONSTRUCT_TRACE_BACKEND=local.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
|
|
12
|
+
export const name = 'local';
|
|
13
|
+
|
|
14
|
+
const TRACES_DIR = join(homedir(), '.cx', 'traces');
|
|
15
|
+
|
|
16
|
+
export async function isAvailable() {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fetch traces from local JSONL files within a time window.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} teamId - The overlay/team ID to filter by.
|
|
24
|
+
* @param {number} windowMs - Lookback window in milliseconds.
|
|
25
|
+
* @returns {Promise<object[]>}
|
|
26
|
+
*/
|
|
27
|
+
export async function listTraces(teamId, windowMs) {
|
|
28
|
+
if (!existsSync(TRACES_DIR)) return [];
|
|
29
|
+
|
|
30
|
+
const since = Date.now() - windowMs;
|
|
31
|
+
const files = readdirSync(TRACES_DIR)
|
|
32
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
33
|
+
.sort()
|
|
34
|
+
.reverse()
|
|
35
|
+
.slice(0, 7); // last 7 days of files
|
|
36
|
+
|
|
37
|
+
const traces = [];
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const lines = readFileSync(join(TRACES_DIR, file), 'utf8')
|
|
40
|
+
.split('\n')
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
try {
|
|
44
|
+
const event = JSON.parse(line);
|
|
45
|
+
if (new Date(event.createdAt).getTime() < since) continue;
|
|
46
|
+
if (teamId && event.project && event.project !== teamId) continue;
|
|
47
|
+
traces.push({
|
|
48
|
+
id: event.traceId ?? event.spanId,
|
|
49
|
+
teamId: event.project ?? teamId,
|
|
50
|
+
agentName: event.role ?? event.eventType ?? 'unknown',
|
|
51
|
+
status: 'local',
|
|
52
|
+
latencyMs: null,
|
|
53
|
+
qualityScore: null,
|
|
54
|
+
createdAt: event.createdAt,
|
|
55
|
+
blockers: [],
|
|
56
|
+
handoffs: 0,
|
|
57
|
+
});
|
|
58
|
+
} catch { /* skip malformed lines */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return traces;
|
|
62
|
+
}
|
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/telemetry/backends/
|
|
2
|
+
* lib/telemetry/backends/remote.mjs — Remote telemetry backend adapter.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
4
|
+
* Reads CONSTRUCT_TELEMETRY_URL, CONSTRUCT_TELEMETRY_PUBLIC_KEY,
|
|
5
|
+
* CONSTRUCT_TELEMETRY_SECRET_KEY from env.
|
|
5
6
|
* Implements the TraceBackend interface: listTraces(teamId, windowMs) → Trace[].
|
|
7
|
+
*
|
|
8
|
+
* The HTTP protocol is compatible with open trace ingestion APIs.
|
|
9
|
+
* Point CONSTRUCT_TELEMETRY_URL at any compatible backend.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
export const name = '
|
|
12
|
+
export const name = 'remote';
|
|
9
13
|
|
|
10
|
-
function
|
|
11
|
-
const key =
|
|
12
|
-
const secret =
|
|
13
|
-
if (!key || !secret)
|
|
14
|
+
export function telemetryHeaders(env = process.env) {
|
|
15
|
+
const key = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
|
|
16
|
+
const secret = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
|
|
17
|
+
if (!key || !secret)
|
|
18
|
+
throw new Error(
|
|
19
|
+
'CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY must be set.',
|
|
20
|
+
);
|
|
14
21
|
return {
|
|
15
22
|
Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
|
|
16
23
|
'Content-Type': 'application/json',
|
|
17
24
|
};
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
function
|
|
21
|
-
return (
|
|
27
|
+
export function telemetryBaseUrl(env = process.env) {
|
|
28
|
+
return (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export async function isAvailable() {
|
|
25
|
-
return Boolean(
|
|
32
|
+
return Boolean(
|
|
33
|
+
(process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY) &&
|
|
34
|
+
(process.env.CONSTRUCT_TELEMETRY_SECRET_KEY),
|
|
35
|
+
);
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
/**
|
|
@@ -35,12 +45,13 @@ export async function isAvailable() {
|
|
|
35
45
|
*/
|
|
36
46
|
export async function listTraces(teamId, windowMs) {
|
|
37
47
|
const since = new Date(Date.now() - windowMs).toISOString();
|
|
38
|
-
const
|
|
48
|
+
const base = telemetryBaseUrl();
|
|
49
|
+
const url = `${base}/api/public/traces?tags=${encodeURIComponent(teamId)}&fromTimestamp=${since}&limit=100`;
|
|
39
50
|
|
|
40
|
-
const resp = await fetch(url, { headers:
|
|
51
|
+
const resp = await fetch(url, { headers: telemetryHeaders() });
|
|
41
52
|
if (!resp.ok) {
|
|
42
53
|
const body = await resp.text();
|
|
43
|
-
throw new Error(`
|
|
54
|
+
throw new Error(`Telemetry backend API error ${resp.status}: ${body}`);
|
|
44
55
|
}
|
|
45
56
|
const json = await resp.json();
|
|
46
57
|
|
|
@@ -49,10 +60,12 @@ export async function listTraces(teamId, windowMs) {
|
|
|
49
60
|
teamId,
|
|
50
61
|
agentName: t.metadata?.agentName ?? t.name ?? 'unknown',
|
|
51
62
|
status: t.metadata?.status ?? 'unknown',
|
|
52
|
-
latencyMs: t.latency != null ? Math.round(t.latency * 1000) : null,
|
|
63
|
+
latencyMs: t.latency != null ? Math.round(t.latency * 1000) : null,
|
|
53
64
|
qualityScore: t.scores?.find((s) => s.name === 'quality')?.value ?? null,
|
|
54
65
|
createdAt: t.timestamp,
|
|
55
66
|
blockers: t.metadata?.blockers ?? [],
|
|
56
67
|
handoffs: t.metadata?.handoffs ?? 0,
|
|
57
68
|
}));
|
|
58
69
|
}
|
|
70
|
+
|
|
71
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/telemetry/backfill.mjs — Sparse telemetry trace backfill.
|
|
3
|
+
*
|
|
4
|
+
* Fetches the most recent sparse traces from the telemetry backend and posts a synthetic
|
|
5
|
+
* EVENT observation against each one so it graduates from sparse → partial.
|
|
6
|
+
* Safe to call repeatedly — observations are idempotent (same deterministic ID).
|
|
7
|
+
*
|
|
8
|
+
* Consumed by:
|
|
9
|
+
* • lib/status.mjs — auto-triggered when sparse coverage is detected.
|
|
10
|
+
* • bin/construct — `construct telemetry-backfill` CLI command.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { createIngestClient } from "./ingest.mjs";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_LIMIT = 10;
|
|
17
|
+
const SPARSE_THRESHOLD = 0.35;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Derive a stable observation ID from a trace ID so repeated backfills are
|
|
21
|
+
* idempotent from the caller's perspective (de-duped by ID on upsert).
|
|
22
|
+
*/
|
|
23
|
+
function stableObservationId(traceId) {
|
|
24
|
+
return createHash("sha256").update(`backfill:${traceId}`).digest("hex").slice(0, 32);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function classifySparseTrace(trace) {
|
|
28
|
+
const observationCount = Number(
|
|
29
|
+
trace?.observationCount ?? trace?.observations?.length ?? trace?.spanCount ?? trace?.generationCount ?? 0
|
|
30
|
+
) || 0;
|
|
31
|
+
const hasInput = trace?.input != null;
|
|
32
|
+
const hasOutput = trace?.output != null;
|
|
33
|
+
const metaKeys = trace?.metadata && typeof trace.metadata === "object"
|
|
34
|
+
? Object.keys(trace.metadata).length
|
|
35
|
+
: 0;
|
|
36
|
+
const hasRichMetadata = metaKeys >= 5;
|
|
37
|
+
const hasPayload = hasInput || hasOutput || metaKeys > 0;
|
|
38
|
+
|
|
39
|
+
if ((hasInput || hasOutput) && (observationCount >= 1 || hasRichMetadata)) return "rich";
|
|
40
|
+
if (hasPayload || observationCount >= 1) return "partial";
|
|
41
|
+
return "sparse";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch recent traces from the telemetry backend and return only the sparse ones.
|
|
46
|
+
*/
|
|
47
|
+
async function fetchSparseTraces({
|
|
48
|
+
baseUrl,
|
|
49
|
+
headers,
|
|
50
|
+
limit = DEFAULT_LIMIT,
|
|
51
|
+
signal,
|
|
52
|
+
fetchImpl = globalThis.fetch,
|
|
53
|
+
} = {}) {
|
|
54
|
+
const url = `${baseUrl}/api/public/traces?limit=${limit * 3}`;
|
|
55
|
+
const res = await fetchImpl(url, { headers, signal });
|
|
56
|
+
if (!res.ok) throw new Error(`telemetry traces ${res.status}`);
|
|
57
|
+
const json = await res.json().catch(() => ({}));
|
|
58
|
+
const traces = Array.isArray(json.data) ? json.data : [];
|
|
59
|
+
return traces
|
|
60
|
+
.filter((t) => classifySparseTrace(t) === "sparse")
|
|
61
|
+
.slice(0, limit);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Post a backfill EVENT observation against a trace.
|
|
66
|
+
*/
|
|
67
|
+
function buildBackfillObservation(traceId, { healReason = "auto", source = "construct-backfill" } = {}) {
|
|
68
|
+
return {
|
|
69
|
+
id: stableObservationId(traceId),
|
|
70
|
+
traceId,
|
|
71
|
+
type: "EVENT",
|
|
72
|
+
name: "construct.backfill",
|
|
73
|
+
startTime: new Date().toISOString(),
|
|
74
|
+
metadata: {
|
|
75
|
+
heal_reason: healReason,
|
|
76
|
+
source,
|
|
77
|
+
backfilledAt: new Date().toISOString(),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Backfill sparse traces. Returns a result summary.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} opts
|
|
86
|
+
* @param {string} [opts.baseUrl]
|
|
87
|
+
* @param {string} [opts.publicKey]
|
|
88
|
+
* @param {string} [opts.secretKey]
|
|
89
|
+
* @param {number} [opts.limit]
|
|
90
|
+
* @param {boolean} [opts.bestEffort] - skip individual failures instead of throwing
|
|
91
|
+
* @param {Function} [opts.fetchImpl]
|
|
92
|
+
* @returns {{ backfilled: number, skipped: number, errors: string[] }}
|
|
93
|
+
*/
|
|
94
|
+
export async function backfillSparseTraces({
|
|
95
|
+
baseUrl = (process.env.CONSTRUCT_TELEMETRY_URL ?? "").replace(/\/$/, ""),
|
|
96
|
+
publicKey = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
|
|
97
|
+
secretKey = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY,
|
|
98
|
+
limit = DEFAULT_LIMIT,
|
|
99
|
+
bestEffort = false,
|
|
100
|
+
fetchImpl = globalThis.fetch,
|
|
101
|
+
} = {}) {
|
|
102
|
+
if (!publicKey || !secretKey || !fetchImpl) {
|
|
103
|
+
return { backfilled: 0, skipped: 0, errors: ["Telemetry credentials not configured"] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const headers = {
|
|
107
|
+
Authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString("base64")}`,
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const errors = [];
|
|
112
|
+
let sparseTraces;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
sparseTraces = await fetchSparseTraces({ baseUrl, headers, limit, fetchImpl });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return { backfilled: 0, skipped: 0, errors: [err.message] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!sparseTraces.length) {
|
|
121
|
+
return { backfilled: 0, skipped: 0, errors: [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const client = createIngestClient({ baseUrl, publicKey, secretKey, fetchImpl });
|
|
125
|
+
let backfilled = 0;
|
|
126
|
+
let skipped = 0;
|
|
127
|
+
|
|
128
|
+
for (const trace of sparseTraces) {
|
|
129
|
+
const traceId = trace.id;
|
|
130
|
+
if (!traceId) { skipped += 1; continue; }
|
|
131
|
+
try {
|
|
132
|
+
client.event(buildBackfillObservation(traceId));
|
|
133
|
+
backfilled += 1;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (bestEffort) {
|
|
136
|
+
errors.push(`${traceId}: ${err.message}`);
|
|
137
|
+
skipped += 1;
|
|
138
|
+
} else {
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await client.flush();
|
|
145
|
+
return { backfilled, skipped, errors };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Auto-heal trigger: run backfill only when telemetry coverage is below threshold.
|
|
150
|
+
* Fire-and-forget — errors are swallowed. Intended for background use from buildStatus.
|
|
151
|
+
*/
|
|
152
|
+
export function triggerAutoBackfillIfSparse(telemetryRichness, opts = {}) {
|
|
153
|
+
const coverage = telemetryRichness?.coverage ?? 1;
|
|
154
|
+
const total = telemetryRichness?.total ?? 0;
|
|
155
|
+
if (total === 0 || coverage >= SPARSE_THRESHOLD) return;
|
|
156
|
+
|
|
157
|
+
backfillSparseTraces({ ...opts, bestEffort: true }).catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* CLI entrypoint for `construct telemetry-backfill`.
|
|
162
|
+
*/
|
|
163
|
+
export async function runTelemetryBackfillCli(args = []) {
|
|
164
|
+
const limitArg = args.find((a) => a.startsWith("--limit="));
|
|
165
|
+
const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) || DEFAULT_LIMIT : DEFAULT_LIMIT;
|
|
166
|
+
const bestEffort = args.includes("--best-effort");
|
|
167
|
+
|
|
168
|
+
process.stdout.write(`Backfilling sparse telemetry traces (limit: ${limit})…\n`);
|
|
169
|
+
|
|
170
|
+
const result = await backfillSparseTraces({ limit, bestEffort });
|
|
171
|
+
|
|
172
|
+
if (result.errors.length && !bestEffort) {
|
|
173
|
+
process.stderr.write(`Backfill error: ${result.errors[0]}\n`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
process.stdout.write(
|
|
178
|
+
`Done. backfilled=${result.backfilled} skipped=${result.skipped}${result.errors.length ? ` errors=${result.errors.length}` : ""}\n`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/telemetry/eval-datasets.mjs — Telemetry eval dataset writer.
|
|
3
|
+
*
|
|
4
|
+
* Reads recent scored traces from the telemetry backend, groups them by agent + workCategory,
|
|
5
|
+
* and upserts structured dataset items so prompt changes can be evaluated in
|
|
6
|
+
* the telemetry backend Datasets feature.
|
|
7
|
+
*
|
|
8
|
+
* One dataset per agent (e.g. "construct-cx-engineer"). Each dataset item is one
|
|
9
|
+
* trace: input = agent goal, expected_output = score+comment, metadata = promptHash.
|
|
10
|
+
*
|
|
11
|
+
* Closes the loop:
|
|
12
|
+
* agent runs → cx_score called → score written to telemetry backend
|
|
13
|
+
* → eval-datasets job reads scores → upserts dataset items
|
|
14
|
+
* → Telemetry Datasets UI shows score distribution per prompt version
|
|
15
|
+
* → prompt author can see whether a change improved or regressed quality
|
|
16
|
+
*
|
|
17
|
+
* Consumed by:
|
|
18
|
+
* • lib/embed/daemon.mjs — scheduled every 6h as 'eval-dataset-sync' job
|
|
19
|
+
* • bin/construct — `construct eval-datasets` CLI command
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const MAX_TRACES = 100;
|
|
23
|
+
const MIN_SCORE_TO_INCLUDE = 0; // include all scores — 0.0 failures are valuable
|
|
24
|
+
|
|
25
|
+
function baseHeaders(publicKey, secretKey) {
|
|
26
|
+
return {
|
|
27
|
+
Authorization: `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function baseUrl(env = process.env) {
|
|
33
|
+
return (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch recent traces that have at least one quality score.
|
|
38
|
+
* Returns array of { traceId, name, input, metadata, scores[] }.
|
|
39
|
+
*/
|
|
40
|
+
async function fetchScoredTraces({ url, headers, limit = MAX_TRACES, fetchImpl = globalThis.fetch }) {
|
|
41
|
+
const res = await fetchImpl(`${url}/api/public/scores?name=quality&limit=${limit}`, { headers });
|
|
42
|
+
if (!res.ok) throw new Error(`Telemetry scores fetch ${res.status}`);
|
|
43
|
+
const json = await res.json().catch(() => ({}));
|
|
44
|
+
const scores = Array.isArray(json.data) ? json.data : [];
|
|
45
|
+
if (!scores.length) return [];
|
|
46
|
+
|
|
47
|
+
// De-duplicate by traceId — take the latest score per trace
|
|
48
|
+
const byTrace = new Map();
|
|
49
|
+
for (const score of scores) {
|
|
50
|
+
if (!score.traceId) continue;
|
|
51
|
+
const existing = byTrace.get(score.traceId);
|
|
52
|
+
if (!existing || new Date(score.timestamp) > new Date(existing.timestamp)) {
|
|
53
|
+
byTrace.set(score.traceId, score);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fetch trace details in parallel (up to 10 at a time)
|
|
58
|
+
const traceIds = [...byTrace.keys()].slice(0, limit);
|
|
59
|
+
const BATCH = 10;
|
|
60
|
+
const enriched = [];
|
|
61
|
+
for (let i = 0; i < traceIds.length; i += BATCH) {
|
|
62
|
+
const batch = traceIds.slice(i, i + BATCH);
|
|
63
|
+
const results = await Promise.allSettled(
|
|
64
|
+
batch.map((id) =>
|
|
65
|
+
fetchImpl(`${url}/api/public/traces/${id}`, { headers })
|
|
66
|
+
.then((r) => r.ok ? r.json() : null)
|
|
67
|
+
.catch(() => null)
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
for (let j = 0; j < batch.length; j++) {
|
|
71
|
+
const trace = results[j].status === 'fulfilled' ? results[j].value : null;
|
|
72
|
+
if (!trace) continue;
|
|
73
|
+
const score = byTrace.get(batch[j]);
|
|
74
|
+
enriched.push({ trace, score });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return enriched;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Upsert a dataset item in the telemetry backend.
|
|
82
|
+
* Creates the dataset if it does not exist (returns 200 on existing datasets).
|
|
83
|
+
*/
|
|
84
|
+
async function upsertDatasetItem({ url, headers, datasetName, item, fetchImpl = globalThis.fetch }) {
|
|
85
|
+
// Create dataset (idempotent)
|
|
86
|
+
await fetchImpl(`${url}/api/public/datasets`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers,
|
|
89
|
+
body: JSON.stringify({ name: datasetName, description: `Auto-generated eval dataset for ${datasetName}` }),
|
|
90
|
+
}).catch(() => {});
|
|
91
|
+
|
|
92
|
+
// Upsert item — use traceId as the stable ID so reruns are idempotent
|
|
93
|
+
const res = await fetchImpl(`${url}/api/public/dataset-items`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers,
|
|
96
|
+
body: JSON.stringify(item),
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const text = await res.text().catch(() => '');
|
|
100
|
+
throw new Error(`dataset-items ${res.status}: ${text.slice(0, 200)}`);
|
|
101
|
+
}
|
|
102
|
+
return res.json().catch(() => ({}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Derive a dataset name from a trace.
|
|
107
|
+
* Uses agentName from trace metadata, falling back to trace.name.
|
|
108
|
+
*/
|
|
109
|
+
function datasetNameForTrace(trace) {
|
|
110
|
+
const agent = trace?.metadata?.agentName || trace?.name || 'construct';
|
|
111
|
+
const workCategory = trace?.metadata?.routeWorkCategory || null;
|
|
112
|
+
const base = `construct-${agent}`;
|
|
113
|
+
return workCategory ? `${base}-${workCategory}` : base;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Main entrypoint. Reads scored traces from the telemetry backend and upserts dataset items.
|
|
118
|
+
* Returns { synced, skipped, errors }.
|
|
119
|
+
*/
|
|
120
|
+
export async function syncEvalDatasets({
|
|
121
|
+
publicKey = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
|
|
122
|
+
secretKey = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY,
|
|
123
|
+
env = process.env,
|
|
124
|
+
limit = MAX_TRACES,
|
|
125
|
+
bestEffort = true,
|
|
126
|
+
fetchImpl = globalThis.fetch,
|
|
127
|
+
} = {}) {
|
|
128
|
+
if (!publicKey || !secretKey) {
|
|
129
|
+
return { synced: 0, skipped: 0, errors: ['Telemetry credentials not configured'] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const url = baseUrl(env);
|
|
133
|
+
const headers = baseHeaders(publicKey, secretKey);
|
|
134
|
+
const errors = [];
|
|
135
|
+
let synced = 0;
|
|
136
|
+
let skipped = 0;
|
|
137
|
+
|
|
138
|
+
let scoredTraces;
|
|
139
|
+
try {
|
|
140
|
+
scoredTraces = await fetchScoredTraces({ url, headers, limit, fetchImpl });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return { synced: 0, skipped: 0, errors: [err.message] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const { trace, score } of scoredTraces) {
|
|
146
|
+
try {
|
|
147
|
+
const datasetName = datasetNameForTrace(trace);
|
|
148
|
+
const promptHash = trace?.metadata?.composedPromptHash ?? null;
|
|
149
|
+
const promptVersion = trace?.metadata?.composedPromptVersion ?? null;
|
|
150
|
+
|
|
151
|
+
const item = {
|
|
152
|
+
// Use traceId as external ID so upserts are idempotent
|
|
153
|
+
id: `trace-${trace.id}`,
|
|
154
|
+
datasetName,
|
|
155
|
+
input: trace.input ?? trace?.metadata?.goal ?? '',
|
|
156
|
+
expectedOutput: {
|
|
157
|
+
qualityScore: score.value,
|
|
158
|
+
comment: score.comment ?? null,
|
|
159
|
+
agentName: trace?.metadata?.agentName ?? trace?.name ?? null,
|
|
160
|
+
workCategory: trace?.metadata?.routeWorkCategory ?? null,
|
|
161
|
+
},
|
|
162
|
+
metadata: {
|
|
163
|
+
traceId: trace.id,
|
|
164
|
+
traceName: trace.name,
|
|
165
|
+
promptHash,
|
|
166
|
+
promptVersion,
|
|
167
|
+
routeIntent: trace?.metadata?.routeIntent ?? null,
|
|
168
|
+
routeTrack: trace?.metadata?.routeTrack ?? null,
|
|
169
|
+
fragmentTypes: trace?.metadata?.promptFragmentTypes ?? null,
|
|
170
|
+
hasLearnedPatterns: trace?.metadata?.promptHasLearnedPatterns ?? false,
|
|
171
|
+
scoredAt: score.timestamp ?? new Date().toISOString(),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await upsertDatasetItem({ url, headers, datasetName, item, fetchImpl });
|
|
176
|
+
synced += 1;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (bestEffort) {
|
|
179
|
+
errors.push(`${trace?.id}: ${err.message}`);
|
|
180
|
+
skipped += 1;
|
|
181
|
+
} else {
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { synced, skipped, errors };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* CLI entrypoint for `construct eval-datasets`.
|
|
192
|
+
*/
|
|
193
|
+
export async function runEvalDatasetsCli(args = []) {
|
|
194
|
+
const limitArg = args.find((a) => a.startsWith('--limit='));
|
|
195
|
+
const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) || MAX_TRACES : MAX_TRACES;
|
|
196
|
+
|
|
197
|
+
process.stdout.write(`Syncing eval datasets from telemetry backend (limit: ${limit})…\n`);
|
|
198
|
+
const result = await syncEvalDatasets({ limit, bestEffort: false });
|
|
199
|
+
process.stdout.write(`Done. synced=${result.synced} skipped=${result.skipped}${result.errors.length ? ` errors=${result.errors.length}` : ''}\n`);
|
|
200
|
+
if (result.errors.length) {
|
|
201
|
+
for (const e of result.errors) process.stderr.write(` ${e}\n`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/telemetry/
|
|
2
|
+
* lib/telemetry/ingest.mjs — Telemetry backend batch ingestion client.
|
|
3
3
|
*
|
|
4
4
|
* POSTs observations to /api/public/ingestion in batches. Silent no-op when
|
|
5
|
-
*
|
|
5
|
+
* credentials are missing. Never throws from the hot path.
|
|
6
|
+
*
|
|
7
|
+
* Uses an open ingestion API protocol. Point CONSTRUCT_TELEMETRY_URL
|
|
8
|
+
* at any compatible backend.
|
|
6
9
|
*/
|
|
7
|
-
import { randomUUID } from
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
8
11
|
|
|
9
12
|
const DEFAULT_FLUSH_INTERVAL_MS = 500;
|
|
10
13
|
const DEFAULT_MAX_BATCH = 50;
|
|
@@ -12,7 +15,7 @@ const MAX_PAYLOAD_CHARS = 16_000;
|
|
|
12
15
|
|
|
13
16
|
function truncate(value) {
|
|
14
17
|
if (value == null) return value;
|
|
15
|
-
if (typeof value ===
|
|
18
|
+
if (typeof value === 'string') {
|
|
16
19
|
return value.length > MAX_PAYLOAD_CHARS
|
|
17
20
|
? `${value.slice(0, MAX_PAYLOAD_CHARS)}…[truncated ${value.length - MAX_PAYLOAD_CHARS} chars]`
|
|
18
21
|
: value;
|
|
@@ -27,9 +30,11 @@ function truncate(value) {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export function createIngestClient({
|
|
30
|
-
baseUrl = (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
baseUrl = (
|
|
34
|
+
process.env.CONSTRUCT_TELEMETRY_URL ?? ''
|
|
35
|
+
).replace(/\/$/, ''),
|
|
36
|
+
publicKey = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
|
|
37
|
+
secretKey = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY,
|
|
33
38
|
flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS,
|
|
34
39
|
maxBatch = DEFAULT_MAX_BATCH,
|
|
35
40
|
onError = () => {},
|
|
@@ -41,8 +46,8 @@ export function createIngestClient({
|
|
|
41
46
|
let inflight = Promise.resolve();
|
|
42
47
|
|
|
43
48
|
const authHeader = available
|
|
44
|
-
? `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString(
|
|
45
|
-
:
|
|
49
|
+
? `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`
|
|
50
|
+
: '';
|
|
46
51
|
|
|
47
52
|
function scheduleFlush() {
|
|
48
53
|
if (!available || flushTimer) return;
|
|
@@ -50,7 +55,7 @@ export function createIngestClient({
|
|
|
50
55
|
flushTimer = null;
|
|
51
56
|
void flushBatch();
|
|
52
57
|
}, flushIntervalMs);
|
|
53
|
-
if (typeof flushTimer.unref ===
|
|
58
|
+
if (typeof flushTimer.unref === 'function') flushTimer.unref();
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
async function flushBatch() {
|
|
@@ -62,14 +67,14 @@ export function createIngestClient({
|
|
|
62
67
|
try { await prev; } catch { /* ignore prior */ }
|
|
63
68
|
try {
|
|
64
69
|
const res = await fetchImpl(`${baseUrl}/api/public/ingestion`, {
|
|
65
|
-
method:
|
|
66
|
-
headers: { Authorization: authHeader,
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { Authorization: authHeader, 'Content-Type': 'application/json' },
|
|
67
72
|
body: payload,
|
|
68
73
|
});
|
|
69
74
|
if (!res.ok && res.status !== 207) {
|
|
70
|
-
let detail =
|
|
75
|
+
let detail = '';
|
|
71
76
|
try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
|
|
72
|
-
onError(new Error(`
|
|
77
|
+
onError(new Error(`telemetry ingest ${res.status}: ${detail}`));
|
|
73
78
|
}
|
|
74
79
|
} catch (err) {
|
|
75
80
|
onError(err);
|
|
@@ -92,12 +97,13 @@ export function createIngestClient({
|
|
|
92
97
|
|
|
93
98
|
return {
|
|
94
99
|
available,
|
|
95
|
-
trace: (body) => push(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
trace: (body) => push('trace-create', body),
|
|
101
|
+
traceUpdate: (body) => push('trace-update', body),
|
|
102
|
+
generation: (body) => push('generation-create', body),
|
|
103
|
+
generationUpdate: (body) => push('generation-update', body),
|
|
104
|
+
span: (body) => push('span-create', body),
|
|
105
|
+
spanUpdate: (body) => push('span-update', body),
|
|
106
|
+
event: (body) => push('event-create', body),
|
|
101
107
|
async flush() {
|
|
102
108
|
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
103
109
|
while (queue.length > 0) await flushBatch();
|