@geraldmaron/construct 1.2.2 → 1.3.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.
- package/README.md +7 -10
- package/bin/construct +286 -220
- package/bin/construct-postinstall.mjs +0 -21
- package/commands/work/optimize-prompts.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/artifact-loop-core.mjs +412 -0
- package/lib/artifact-manifest.mjs +6 -0
- package/lib/artifact-release-gate.mjs +115 -49
- package/lib/audit-rules.mjs +2 -4
- package/lib/audit-skills.mjs +32 -20
- package/lib/audit-specialists.mjs +44 -26
- package/lib/auto-docs.mjs +6 -5
- package/lib/brand-prose.mjs +3 -3
- package/lib/brand-tokens.mjs +2 -2
- package/lib/certification/artifact-fixtures.mjs +4 -1
- package/lib/certification/demo-parity.mjs +6 -32
- package/lib/certification/real-llm-scenarios.mjs +58 -13
- package/lib/certification/role-cards.mjs +7 -6
- package/lib/certification/role-overlays.mjs +4 -4
- package/lib/certification/runner.mjs +9 -2
- package/lib/certification/skill-inventory.mjs +34 -22
- package/lib/certification/skill-scenarios.mjs +21 -8
- package/lib/certification/specialist-contracts.mjs +4 -3
- package/lib/certification/specialist-scenarios.mjs +7 -6
- package/lib/certification/status.mjs +5 -4
- package/lib/cli-commands.mjs +56 -57
- package/lib/comment-lint.mjs +1 -5
- package/lib/completions.mjs +20 -2
- package/lib/config/legacy-config-migration.mjs +125 -0
- package/lib/config/schema.mjs +5 -5
- package/lib/config/source-targets.mjs +80 -0
- package/lib/contracts/validate.mjs +119 -51
- package/lib/decisions/golden.mjs +3 -2
- package/lib/decisions/registry.mjs +11 -6
- package/lib/demo-project.mjs +188 -0
- package/lib/demo-recording.mjs +9 -34
- package/lib/demo-script.mjs +2 -2
- package/lib/demo-surface.mjs +13 -131
- package/lib/demo-tour-renderer.mjs +92 -0
- package/lib/demo.mjs +69 -54
- package/lib/diagram-export.mjs +63 -14
- package/lib/doctor/source-checkout.mjs +3 -4
- package/lib/doctor/watchers/consistency.mjs +90 -22
- package/lib/doctor/watchers/credential-parity.mjs +97 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
- package/lib/doctor/watchers/service-health.mjs +4 -32
- package/lib/document-export.mjs +73 -9
- package/lib/document-extract.mjs +10 -198
- package/lib/embed/artifact.mjs +2 -3
- package/lib/embed/cli.mjs +1 -1
- package/lib/embed/config.mjs +1 -2
- package/lib/embed/customer-profiles.mjs +1 -1
- package/lib/embed/daemon.mjs +5 -6
- package/lib/embed/demand-fetch.mjs +114 -2
- package/lib/embed/docs-lifecycle.mjs +9 -5
- package/lib/embed/intake-metrics.mjs +2 -2
- package/lib/embed/notifications.mjs +2 -3
- package/lib/embed/recommendation-store.mjs +25 -22
- package/lib/embed/role-framing.mjs +4 -9
- package/lib/embedded-contract/capability.mjs +4 -2
- package/lib/embedded-contract/contract-version.mjs +1 -1
- package/lib/env-config.mjs +2 -2
- package/lib/flavors/loader.mjs +21 -19
- package/lib/graph/build-from-registry.mjs +31 -7
- package/lib/graph/staleness.mjs +1 -1
- package/lib/headhunt.mjs +82 -35
- package/lib/hooks/agent-tracker.mjs +2 -2
- package/lib/hooks/config-protection.mjs +1 -1
- package/lib/hooks/registry-sync.mjs +3 -3
- package/lib/host-capabilities.mjs +5 -5
- package/lib/improvement/controller.mjs +2 -2
- package/lib/init-unified.mjs +19 -21
- package/lib/intake/classify.mjs +26 -0
- package/lib/intake/daemon.mjs +2 -3
- package/lib/intake/prepare.mjs +10 -3
- package/lib/intake/session-prelude.mjs +1 -1
- package/lib/intake/tables/creative.mjs +9 -9
- package/lib/intake/tables/operations.mjs +4 -4
- package/lib/intake/tables/research.mjs +2 -2
- package/lib/integrations/intake-integrations.mjs +9 -10
- package/lib/knowledge/research-store.mjs +2 -2
- package/lib/mcp/server.mjs +104 -61
- package/lib/mcp/tool-budget.mjs +1 -46
- package/lib/mcp/tool-recovery.mjs +44 -0
- package/lib/mcp/tools/artifact-author.mjs +36 -0
- package/lib/mcp/tools/find-tool.mjs +86 -0
- package/lib/mcp/tools/orchestration-run.mjs +107 -69
- package/lib/mcp/tools/project.mjs +4 -3
- package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
- package/lib/mcp/tools/skills.mjs +39 -22
- package/lib/mcp/tools/telemetry.mjs +2 -1
- package/lib/mcp/tools/workflow.mjs +1 -1
- package/lib/mcp-platform-config.mjs +7 -5
- package/lib/migrations/index.mjs +4 -2
- package/lib/migrations/v2-unified-registry.mjs +35 -0
- package/lib/model-router.mjs +203 -36
- package/lib/models/catalog.mjs +25 -9
- package/lib/models/execution-capability-profile.mjs +3 -3
- package/lib/models/execution-policy.mjs +7 -6
- package/lib/models/provider-poll.mjs +18 -4
- package/lib/opencode-config.mjs +56 -1
- package/lib/opencode-runtime-plugin.mjs +13 -10
- package/lib/oracle/artifact-gate.mjs +14 -4
- package/lib/oracle/cli.mjs +2 -0
- package/lib/oracle/dispatch.mjs +18 -1
- package/lib/oracle/execute.mjs +39 -7
- package/lib/oracle/index.mjs +1 -1
- package/lib/oracle/org-graph.mjs +10 -5
- package/lib/oracle/policy.mjs +6 -0
- package/lib/oracle/read-model.mjs +103 -6
- package/lib/oracle/reconcile.mjs +31 -4
- package/lib/oracle/remediation-dispatch.mjs +53 -0
- package/lib/oracle/routing.mjs +5 -0
- package/lib/oracle/synthesize.mjs +80 -3
- package/lib/orchestration/routing-tables.mjs +44 -10
- package/lib/orchestration/runtime.mjs +2 -0
- package/lib/orchestration/worker.mjs +1 -1
- package/lib/orchestration-policy.mjs +351 -62
- package/lib/overrides/resolver.mjs +1 -12
- package/lib/parity.mjs +37 -11
- package/lib/policy/engine.mjs +80 -15
- package/lib/prompt-composer.js +51 -15
- package/lib/prompt-metadata.mjs +3 -2
- package/lib/providers/connection-probe.mjs +58 -0
- package/lib/providers/copilot-auth.mjs +1 -1
- package/lib/providers/credential-bootstrap.mjs +1 -1
- package/lib/providers/op-run.mjs +3 -4
- package/lib/providers/secret-resolver.mjs +31 -0
- package/lib/publish-tooling.mjs +3 -11
- package/lib/publish.mjs +12 -29
- package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
- package/lib/reflect.mjs +2 -2
- package/lib/registry/agent-manifest.mjs +190 -0
- package/lib/registry/assemble.mjs +133 -0
- package/lib/registry/catalog.mjs +171 -0
- package/lib/registry/cli.mjs +590 -6
- package/lib/registry/consolidation.mjs +5 -4
- package/lib/registry/docs-sync.mjs +67 -0
- package/lib/registry/loader.mjs +147 -0
- package/lib/registry/org-io.mjs +76 -0
- package/lib/registry/retired-paths.mjs +71 -0
- package/lib/registry/surface-map.mjs +5 -7
- package/lib/registry/validator.mjs +438 -0
- package/lib/research-execution-policy.mjs +173 -0
- package/lib/roles/catalog.mjs +4 -9
- package/lib/roles/fence.mjs +107 -1
- package/lib/roles/flavor-bindings.mjs +68 -0
- package/lib/roles/gateway.mjs +140 -29
- package/lib/roles/manifest.mjs +22 -13
- package/lib/roles/router.mjs +1 -1
- package/lib/runtime-env.mjs +1 -1
- package/lib/scopes/enrich.mjs +67 -0
- package/lib/scopes/hooks.mjs +12 -0
- package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
- package/lib/scopes/loader.mjs +104 -0
- package/lib/scopes/rebrand.mjs +32 -0
- package/lib/scopes/research-profile.mjs +16 -0
- package/lib/scopes/teams.mjs +98 -0
- package/lib/service-manager.mjs +1 -117
- package/lib/setup-prompts.mjs +1 -1
- package/lib/setup.mjs +17 -28
- package/lib/skills/composition-graph.mjs +98 -0
- package/lib/skills/router.mjs +80 -0
- package/lib/specialist-contracts.mjs +17 -18
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/specialists/prompt-schema.mjs +6 -5
- package/lib/specialists/roster.mjs +7 -12
- package/lib/specialists/schema.mjs +9 -6
- package/lib/status.mjs +25 -90
- package/lib/storage/file-lock.mjs +6 -3
- package/lib/telemetry/skill-calls.mjs +1 -1
- package/lib/templates/doc-presentation.mjs +63 -0
- package/lib/templates/visual-requirements.mjs +35 -2
- package/lib/term-format.mjs +107 -2
- package/lib/test-env-setup.mjs +21 -0
- package/lib/ui/components.mjs +42 -0
- package/lib/ui/glyphs.mjs +58 -0
- package/lib/ui/links.mjs +115 -0
- package/lib/ui/theme.mjs +108 -0
- package/lib/uninstall/uninstall.mjs +2 -1
- package/lib/validator.mjs +45 -8
- package/lib/validators/skill-effectiveness.mjs +174 -0
- package/lib/validators/skills.mjs +1 -1
- package/package.json +12 -7
- package/personas/construct.md +12 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/beads-hygiene.md +52 -0
- package/rules/common/neurodivergent-output.md +4 -7
- package/rules/common/research.md +2 -0
- package/scripts/sync-specialists.mjs +260 -49
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/brand/output-vibe.md +48 -0
- package/skills/docs/adr-workflow.md +3 -0
- package/skills/docs/backlog-proposal-workflow.md +3 -0
- package/skills/docs/codebase-research-workflow.md +5 -2
- package/skills/docs/customer-profile-workflow.md +3 -0
- package/skills/docs/document-ingest-workflow.md +3 -0
- package/skills/docs/evidence-ingest-workflow.md +4 -1
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +1 -1
- package/skills/docs/prfaq-workflow.md +3 -0
- package/skills/docs/product-intelligence-workflow.md +4 -1
- package/skills/docs/product-signal-workflow.md +3 -0
- package/skills/docs/research-workflow.md +13 -6
- package/skills/docs/runbook-workflow.md +3 -0
- package/skills/docs/strategy-workflow.md +3 -0
- package/skills/docs/user-research-workflow.md +6 -3
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
- package/skills/roles/architect.ai-systems.md +1 -1
- package/skills/roles/architect.data.md +1 -1
- package/skills/roles/architect.enterprise.md +1 -1
- package/skills/roles/architect.integration.md +1 -1
- package/skills/roles/architect.md +1 -1
- package/skills/roles/architect.platform.md +1 -1
- package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
- package/skills/roles/data-analyst.experiment.md +1 -1
- package/skills/roles/data-analyst.md +1 -1
- package/skills/roles/data-analyst.product-intelligence.md +1 -1
- package/skills/roles/data-analyst.product.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
- package/skills/roles/data-engineer.pipeline.md +2 -2
- package/skills/roles/data-engineer.vector-retrieval.md +2 -2
- package/skills/roles/data-engineer.warehouse.md +2 -2
- package/skills/roles/debugger.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +1 -1
- package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
- package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
- package/skills/roles/engineer.md +3 -7
- package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
- package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
- package/skills/roles/{operator.md → operations.md} +2 -5
- package/skills/roles/orchestrator.md +1 -1
- package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
- package/skills/roles/product-manager.ai-product.md +1 -1
- package/skills/roles/product-manager.enterprise.md +1 -1
- package/skills/roles/product-manager.growth.md +1 -2
- package/skills/roles/product-manager.md +1 -2
- package/skills/roles/product-manager.platform.md +1 -1
- package/skills/roles/product-manager.product.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -1
- package/skills/roles/qa.api-contract.md +1 -1
- package/skills/roles/qa.data-pipeline.md +1 -1
- package/skills/roles/qa.md +1 -1
- package/skills/roles/qa.web-ui.md +1 -1
- package/skills/roles/{operator.release.md → release-manager.md} +3 -3
- package/skills/roles/researcher.md +1 -1
- package/skills/roles/reviewer.md +1 -1
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.appsec.md +1 -1
- package/skills/roles/security.cloud.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +1 -1
- package/skills/roles/security.privacy.md +1 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/{operator.sre.md → sre.md} +3 -3
- package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
- package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
- package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
- package/skills/routing.json +18 -0
- package/skills/routing.md +1 -1
- package/specialists/artifact-manifest.json +2 -1
- package/specialists/audit-enrichments.json +14 -14
- package/specialists/org/contracts/accessibility-to-qa.json +37 -0
- package/specialists/org/contracts/any-to-business-strategist.json +57 -0
- package/specialists/org/contracts/any-to-debugger.json +38 -0
- package/specialists/org/contracts/any-to-designer.json +33 -0
- package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
- package/specialists/org/contracts/any-to-explorer.json +39 -0
- package/specialists/org/contracts/any-to-sre-incident.json +33 -0
- package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
- package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
- package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
- package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
- package/specialists/org/contracts/architect-to-engineer.json +34 -0
- package/specialists/org/contracts/architect-to-evaluator.json +59 -0
- package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
- package/specialists/org/contracts/architect-to-operations.json +45 -0
- package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
- package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
- package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
- package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
- package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
- package/specialists/org/contracts/designer-to-accessibility.json +36 -0
- package/specialists/org/contracts/engineer-to-qa.json +34 -0
- package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
- package/specialists/org/contracts/explorer-to-engineer.json +38 -0
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
- package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
- package/specialists/org/contracts/product-manager-to-architect.json +71 -0
- package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
- package/specialists/org/contracts/qa-to-release-manager.json +42 -0
- package/specialists/org/contracts/qa-to-test-automation.json +42 -0
- package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
- package/specialists/org/contracts/researcher-to-architect.json +40 -0
- package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
- package/specialists/org/contracts/reviewer-to-security.json +41 -0
- package/specialists/org/contracts/sre-to-release-manager.json +36 -0
- package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
- package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
- package/specialists/org/contracts/user-to-construct.json +30 -0
- package/specialists/org/groups/engineering-group.json +43 -0
- package/specialists/org/groups/governance-group.json +38 -0
- package/specialists/org/groups/operations-group.json +41 -0
- package/specialists/org/groups/product-group.json +46 -0
- package/specialists/org/groups/quality-group.json +42 -0
- package/specialists/org/groups/strategy-group.json +41 -0
- package/specialists/org/policies/action-approval.json +13 -0
- package/specialists/org/policies/agents-routing.json +13 -0
- package/specialists/org/policies/architecture.json +20 -0
- package/specialists/org/policies/bash-safety.json +13 -0
- package/specialists/org/policies/beads-hygiene.json +13 -0
- package/specialists/org/policies/bootstrap-state.json +13 -0
- package/specialists/org/policies/code-review.json +13 -0
- package/specialists/org/policies/coding-style.json +13 -0
- package/specialists/org/policies/comments.json +13 -0
- package/specialists/org/policies/commit-approval.json +13 -0
- package/specialists/org/policies/contract-preconditions.json +13 -0
- package/specialists/org/policies/deployment.json +20 -0
- package/specialists/org/policies/description.json +14 -0
- package/specialists/org/policies/design-approval.json +19 -0
- package/specialists/org/policies/doc-ownership.json +13 -0
- package/specialists/org/policies/file-path-fence.json +13 -0
- package/specialists/org/policies/framing.json +13 -0
- package/specialists/org/policies/git-workflow.json +13 -0
- package/specialists/org/policies/incident-response.json +15 -0
- package/specialists/org/policies/intake-triage.json +15 -0
- package/specialists/org/policies/neurodivergent-output.json +13 -0
- package/specialists/org/policies/no-fabrication.json +13 -0
- package/specialists/org/policies/patterns.json +13 -0
- package/specialists/org/policies/quality-gate-approval.json +17 -0
- package/specialists/org/policies/release-gates.json +13 -0
- package/specialists/org/policies/research-evidence.json +13 -0
- package/specialists/org/policies/review-before-change.json +13 -0
- package/specialists/org/policies/rollback.json +15 -0
- package/specialists/org/policies/scope-change.json +20 -0
- package/specialists/org/policies/secret-scan.json +13 -0
- package/specialists/org/policies/security-approval.json +17 -0
- package/specialists/org/policies/security.json +13 -0
- package/specialists/org/policies/session-efficiency.json +13 -0
- package/specialists/org/policies/skill-routing.json +13 -0
- package/specialists/org/policies/strategic-prioritization.json +17 -0
- package/specialists/org/policies/testing.json +13 -0
- package/specialists/org/policies/tool-invisibility.json +14 -0
- package/specialists/org/scopes/creative.json +54 -0
- package/specialists/org/scopes/operations.json +51 -0
- package/specialists/org/scopes/research.json +60 -0
- package/specialists/org/scopes/rnd.json +81 -0
- package/specialists/org/specialists/cx-accessibility.json +69 -0
- package/specialists/org/specialists/cx-ai-engineer.json +75 -0
- package/specialists/org/specialists/cx-architect.json +89 -0
- package/specialists/org/specialists/cx-business-strategist.json +72 -0
- package/specialists/org/specialists/cx-data-analyst.json +68 -0
- package/specialists/org/specialists/cx-data-engineer.json +71 -0
- package/specialists/org/specialists/cx-debugger.json +75 -0
- package/specialists/org/specialists/cx-designer.json +85 -0
- package/specialists/org/specialists/cx-devil-advocate.json +71 -0
- package/specialists/org/specialists/cx-docs-keeper.json +82 -0
- package/specialists/org/specialists/cx-engineer.json +106 -0
- package/specialists/org/specialists/cx-evaluator.json +69 -0
- package/specialists/org/specialists/cx-explorer.json +69 -0
- package/specialists/org/specialists/cx-legal-compliance.json +74 -0
- package/specialists/org/specialists/cx-operations.json +72 -0
- package/specialists/org/specialists/cx-oracle.json +46 -0
- package/specialists/org/specialists/cx-orchestrator.json +81 -0
- package/specialists/org/specialists/cx-platform-engineer.json +80 -0
- package/specialists/org/specialists/cx-product-manager.json +102 -0
- package/specialists/org/specialists/cx-qa.json +79 -0
- package/specialists/org/specialists/cx-rd-lead.json +73 -0
- package/specialists/org/specialists/cx-release-manager.json +77 -0
- package/specialists/org/specialists/cx-researcher.json +82 -0
- package/specialists/org/specialists/cx-reviewer.json +71 -0
- package/specialists/org/specialists/cx-security.json +91 -0
- package/specialists/org/specialists/cx-sre.json +94 -0
- package/specialists/org/specialists/cx-test-automation.json +69 -0
- package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
- package/specialists/org/specialists/cx-ux-researcher.json +68 -0
- package/specialists/org/teams/accessibility-team.json +39 -0
- package/specialists/org/teams/design-team.json +40 -0
- package/specialists/org/teams/engineering-team.json +50 -0
- package/specialists/org/teams/governance-team.json +41 -0
- package/specialists/org/teams/operations-team.json +46 -0
- package/specialists/org/teams/product-management-team.json +45 -0
- package/specialists/org/teams/quality-team.json +49 -0
- package/specialists/org/teams/research-team.json +39 -0
- package/specialists/org/teams/strategy-team.json +48 -0
- package/specialists/org/teams/ux-research-team.json +39 -0
- package/specialists/prompts/_shared/validation-contract.md +1 -1
- package/specialists/prompts/_team-template.md +10 -0
- package/specialists/prompts/cx-accessibility.md +1 -0
- package/specialists/prompts/cx-ai-engineer.md +1 -1
- package/specialists/prompts/cx-business-strategist.md +1 -1
- package/specialists/prompts/cx-data-engineer.md +1 -1
- package/specialists/prompts/cx-designer.md +1 -0
- package/specialists/prompts/cx-devil-advocate.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-evaluator.md +1 -1
- package/specialists/prompts/cx-explorer.md +1 -1
- package/specialists/prompts/cx-operations.md +1 -1
- package/specialists/prompts/cx-oracle.md +7 -3
- package/specialists/prompts/cx-orchestrator.md +17 -1
- package/specialists/prompts/cx-platform-engineer.md +1 -1
- package/specialists/prompts/cx-product-manager.md +2 -0
- package/specialists/prompts/cx-release-manager.md +1 -1
- package/specialists/prompts/cx-researcher.md +7 -4
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/prompts/cx-test-automation.md +2 -2
- package/specialists/prompts/cx-trace-reviewer.md +3 -3
- package/specialists/prompts/cx-ux-researcher.md +2 -1
- package/templates/demos/scripts/architecture-review-adr.json +30 -0
- package/templates/demos/scripts/capability-contract.json +25 -0
- package/templates/demos/scripts/intake-triage.json +25 -0
- package/templates/demos/scripts/profile-doctor-health.json +25 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
- package/templates/demos/tapes/architecture-review-adr.tape +44 -0
- package/templates/demos/tapes/capability-contract.tape +39 -0
- package/templates/demos/tapes/intake-triage.tape +39 -0
- package/templates/demos/tapes/profile-doctor-health.tape +39 -0
- package/templates/distribution/construct-brand.typ +23 -18
- package/templates/distribution/construct-decision.typ +1 -1
- package/templates/distribution/construct-pdf.typ +1 -1
- package/templates/distribution/construct-prd.typ +1 -1
- package/templates/distribution/construct-research.typ +1 -1
- package/templates/distribution/mermaid-puppeteer.json +8 -0
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prd.md +6 -0
- package/lib/boundary.mjs +0 -127
- package/lib/certification/dashboard-api.mjs +0 -71
- package/lib/chat/cli.mjs +0 -333
- package/lib/chat/command-suggest.mjs +0 -161
- package/lib/chat/commands.mjs +0 -215
- package/lib/chat/config.mjs +0 -142
- package/lib/chat/context-compactor.mjs +0 -250
- package/lib/chat/context-continuation.mjs +0 -253
- package/lib/chat/continuation-source.mjs +0 -58
- package/lib/chat/demo-guide.mjs +0 -61
- package/lib/chat/design-tokens.mjs +0 -91
- package/lib/chat/desktop-binary.mjs +0 -81
- package/lib/chat/desktop-build.mjs +0 -130
- package/lib/chat/desktop-launcher.mjs +0 -133
- package/lib/chat/evidence.mjs +0 -145
- package/lib/chat/export.mjs +0 -74
- package/lib/chat/harness/driver.mjs +0 -91
- package/lib/chat/list-picker.mjs +0 -112
- package/lib/chat/model-picker.mjs +0 -356
- package/lib/chat/openrouter-fallback.mjs +0 -151
- package/lib/chat/permission-prompt.mjs +0 -33
- package/lib/chat/picker-catalog.mjs +0 -45
- package/lib/chat/policy-telemetry.mjs +0 -34
- package/lib/chat/present.mjs +0 -246
- package/lib/chat/session-context.mjs +0 -39
- package/lib/chat/session-persist.mjs +0 -73
- package/lib/chat/session-restore.mjs +0 -71
- package/lib/chat/session-settings.mjs +0 -53
- package/lib/chat/system-prompt.mjs +0 -52
- package/lib/chat/transparency.mjs +0 -93
- package/lib/chat/tui/color-scheme.mjs +0 -42
- package/lib/chat/tui/markdown.mjs +0 -123
- package/lib/chat/tui/presentation.mjs +0 -100
- package/lib/chat/tui/render.mjs +0 -500
- package/lib/chat/tui/turn-block.mjs +0 -284
- package/lib/chat/tui/turn-present.mjs +0 -18
- package/lib/chat/tui/turn-state.mjs +0 -88
- package/lib/chat/tui/usage.mjs +0 -122
- package/lib/chat/web-commands.mjs +0 -146
- package/lib/chat/web-launcher.mjs +0 -63
- package/lib/chat/web-picker-keys.mjs +0 -46
- package/lib/chat/web-session.mjs +0 -159
- package/lib/config/alias.mjs +0 -57
- package/lib/dashboard-demo.mjs +0 -71
- package/lib/dashboard-static.mjs +0 -175
- package/lib/install/desktop-binary-download.mjs +0 -88
- package/lib/profiles/loader.mjs +0 -123
- package/lib/profiles/rebrand.mjs +0 -46
- package/lib/server/auth.mjs +0 -169
- package/lib/server/chat-loop.mjs +0 -622
- package/lib/server/chat.mjs +0 -336
- package/lib/server/cors.mjs +0 -77
- package/lib/server/csrf.mjs +0 -103
- package/lib/server/demo-preview.mjs +0 -63
- package/lib/server/index.mjs +0 -2641
- package/lib/server/insights.mjs +0 -780
- package/lib/server/langfuse-login.mjs +0 -58
- package/lib/server/rate-limit.mjs +0 -93
- package/lib/server/telemetry-login.mjs +0 -100
- package/lib/server/webhook.mjs +0 -512
- package/specialists/contracts.json +0 -1032
- package/specialists/contracts.schema.json +0 -83
- package/specialists/policy-inventory.json +0 -188
- package/specialists/registry.json +0 -1412
- package/specialists/role-manifests.json +0 -217
- package/specialists/teams.json +0 -94
package/lib/server/insights.mjs
DELETED
|
@@ -1,780 +0,0 @@
|
|
|
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
|
-
import { getRebrand } from '../profiles/rebrand.mjs';
|
|
18
|
-
import { configDir, doctorRoot } from '../config/xdg.mjs';
|
|
19
|
-
|
|
20
|
-
const TELEMETRY_TIMEOUT_MS = 3500;
|
|
21
|
-
|
|
22
|
-
function telemetryAuthHeader(env) {
|
|
23
|
-
const key = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
|
|
24
|
-
const secret = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
|
|
25
|
-
if (!key || !secret) return null;
|
|
26
|
-
return `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function telemetryBaseUrl(env) {
|
|
30
|
-
return (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function fetchTelemetrySummary(env, { limit = 100 } = {}) {
|
|
34
|
-
const auth = telemetryAuthHeader(env);
|
|
35
|
-
const baseUrl = telemetryBaseUrl(env);
|
|
36
|
-
|
|
37
|
-
// Try Telemetry API first
|
|
38
|
-
if (auth) {
|
|
39
|
-
const controller = new AbortController();
|
|
40
|
-
const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
41
|
-
try {
|
|
42
|
-
const res = await fetch(`${baseUrl}/api/public/traces?limit=${limit}`, {
|
|
43
|
-
headers: { Authorization: auth }, signal: controller.signal,
|
|
44
|
-
});
|
|
45
|
-
clearTimeout(timer);
|
|
46
|
-
if (res.status === 401 || res.status === 403) {
|
|
47
|
-
return { state: 'misconfigured', message: `Telemetry credentials rejected (HTTP ${res.status}). Run construct init.` };
|
|
48
|
-
}
|
|
49
|
-
if (!res.ok) return { state: 'unreachable', message: `Telemetry HTTP ${res.status}` };
|
|
50
|
-
const json = await res.json();
|
|
51
|
-
const traces = Array.isArray(json.data) ? json.data : [];
|
|
52
|
-
// Cost figures are intentionally absent from the user-facing summary.
|
|
53
|
-
// The underlying telemetry trace data still carries totalCost; that
|
|
54
|
-
// surfaces via the OTel + dashboard wiring planned in Workstream J.
|
|
55
|
-
let observations = 0; const byModel = new Map(); let withError = 0; let withInput = 0; let withOutput = 0;
|
|
56
|
-
for (const trace of traces) {
|
|
57
|
-
observations += Number(trace?.observationCount) || 0;
|
|
58
|
-
if (trace?.input != null) withInput += 1;
|
|
59
|
-
if (trace?.output != null) withOutput += 1;
|
|
60
|
-
const tags = Array.isArray(trace?.tags) ? trace.tags : [];
|
|
61
|
-
if (tags.some((t) => /error|failed|fail/i.test(t))) withError += 1;
|
|
62
|
-
const model = trace?.metadata?.model || trace?.metadata?.lastModel || trace?.metadata?.costModel ||
|
|
63
|
-
trace?.output?.model || trace?.output?.costModel || trace?.input?.metadata?.model ||
|
|
64
|
-
(Array.isArray(trace?.observations) ? trace.observations.find((o) => o?.model)?.model : null) || null;
|
|
65
|
-
if (!model) continue;
|
|
66
|
-
const prior = byModel.get(model) || { traces: 0 };
|
|
67
|
-
prior.traces += 1;
|
|
68
|
-
byModel.set(model, prior);
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
state: traces.length === 0 ? 'empty' : 'ok',
|
|
72
|
-
baseUrl, sampleSize: traces.length,
|
|
73
|
-
observations, withInput, withOutput, withError,
|
|
74
|
-
topModels: [...byModel.entries()].map(([model, v]) => ({ model, traces: v.traces }))
|
|
75
|
-
.sort((a, b) => b.traces - a.traces).slice(0, 5),
|
|
76
|
-
};
|
|
77
|
-
} catch (err) {
|
|
78
|
-
clearTimeout(timer);
|
|
79
|
-
// Fall through to local traces
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Fallback: read from local .cx/traces/ when Telemetry is unreachable or not configured.
|
|
84
|
-
// The daemon always writes heartbeat, lifecycle, and probe events to local JSONL files.
|
|
85
|
-
try {
|
|
86
|
-
const traceDir = path.join(doctorRoot(), 'traces');
|
|
87
|
-
if (!fs.existsSync(traceDir)) return { state: 'empty', message: 'No local traces. Start the daemon with `construct dev`.' };
|
|
88
|
-
const files = fs.readdirSync(traceDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
89
|
-
if (!files.length) return { state: 'empty', message: 'No local trace files.' };
|
|
90
|
-
|
|
91
|
-
const events = { total: 0, daemon: 0, probe: 0, lifecycle: 0, other: 0 };
|
|
92
|
-
const byEventType = {};
|
|
93
|
-
const byDay = {};
|
|
94
|
-
|
|
95
|
-
for (const file of files.slice(0, 7)) {
|
|
96
|
-
const content = fs.readFileSync(path.join(traceDir, file), 'utf8');
|
|
97
|
-
for (const line of content.trim().split('\n').filter(Boolean)) {
|
|
98
|
-
try {
|
|
99
|
-
const e = JSON.parse(line);
|
|
100
|
-
events.total++;
|
|
101
|
-
byEventType[e.eventType] = (byEventType[e.eventType] || 0) + 1;
|
|
102
|
-
if (e.eventType?.startsWith('daemon.')) events.daemon++;
|
|
103
|
-
else if (e.eventType?.startsWith('telemetry.') || e.eventType === 'lifecycle.completed') events.lifecycle++;
|
|
104
|
-
else events.other++;
|
|
105
|
-
const day = file.replace('.jsonl', '');
|
|
106
|
-
byDay[day] = (byDay[day] || 0) + 1;
|
|
107
|
-
} catch { /* skip malformed */ }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const lfState = auth ? 'unreachable' : 'misconfigured';
|
|
112
|
-
return {
|
|
113
|
-
state: events.total > 0 ? 'ok' : 'empty',
|
|
114
|
-
source: 'local-traces',
|
|
115
|
-
baseUrl: null,
|
|
116
|
-
sampleSize: events.total,
|
|
117
|
-
eventBreakdown: byEventType,
|
|
118
|
-
daemonEvents: events.daemon,
|
|
119
|
-
lifecyleEvents: events.lifecycle,
|
|
120
|
-
days: Object.keys(byDay).length,
|
|
121
|
-
message: `Telemetry ${lfState}. Showing ${events.total} events from local trace files.`,
|
|
122
|
-
};
|
|
123
|
-
} catch (err) {
|
|
124
|
-
return { state: 'unreachable', message: `Local traces unavailable: ${err.message}` };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function resolveBillingMode(env) {
|
|
129
|
-
// Precedence: CONSTRUCT_BILLING_MODE env > construct.config.json.costs.billingMode
|
|
130
|
-
// > inference (ANTHROPIC_API_KEY set → metered, else subscription).
|
|
131
|
-
// Returns {mode, source} so callers can distinguish declared from inferred.
|
|
132
|
-
const envOverride = env?.CONSTRUCT_BILLING_MODE;
|
|
133
|
-
if (envOverride === 'metered' || envOverride === 'subscription' || envOverride === 'mixed') {
|
|
134
|
-
return { mode: envOverride, source: 'env' };
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
const cfgPath = path.join(process.cwd(), 'construct.config.json');
|
|
138
|
-
if (fs.existsSync(cfgPath)) {
|
|
139
|
-
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
140
|
-
const m = cfg?.costs?.billingMode;
|
|
141
|
-
if (m === 'metered' || m === 'subscription' || m === 'mixed') {
|
|
142
|
-
return { mode: m, source: 'config' };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} catch { /* config is best-effort */ }
|
|
146
|
-
const hasMeteredKey = typeof env?.ANTHROPIC_API_KEY === 'string'
|
|
147
|
-
&& env.ANTHROPIC_API_KEY.trim().length > 0
|
|
148
|
-
&& !env.ANTHROPIC_API_KEY.includes('__');
|
|
149
|
-
return { mode: hasMeteredKey ? 'metered' : 'subscription', source: 'inferred' };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function summarizeCostLedger(env, homeDir = os.homedir()) {
|
|
153
|
-
try {
|
|
154
|
-
const entries = readCostLog(homeDir);
|
|
155
|
-
const { getTotalDailySpend, totalBudget, enforcementActive } = await import('../cost-ledger.mjs');
|
|
156
|
-
const todayTotal = getTotalDailySpend();
|
|
157
|
-
const cap = totalBudget();
|
|
158
|
-
const billing = resolveBillingMode(env);
|
|
159
|
-
const billingMode = billing.mode;
|
|
160
|
-
// Per-provider billing-mode overrides live in construct.config.json.costs.providers.
|
|
161
|
-
// summarizeCostData reads them to produce meteredActualUsd (excludes subscription
|
|
162
|
-
// providers) alongside the legacy totalCostUsd (now also exposed as meteredEquivalentUsd).
|
|
163
|
-
let costsConfig = {};
|
|
164
|
-
try {
|
|
165
|
-
const { loadProjectConfig } = await import('../config/project-config.mjs');
|
|
166
|
-
costsConfig = loadProjectConfig(process.cwd(), env)?.config?.costs ?? {};
|
|
167
|
-
} catch { /* config is best-effort */ }
|
|
168
|
-
if (!entries.length) {
|
|
169
|
-
return {
|
|
170
|
-
state: 'empty',
|
|
171
|
-
message: 'No cost entries yet. Run a chat session to populate the ledger.',
|
|
172
|
-
billingMode,
|
|
173
|
-
billingSource: billing.source,
|
|
174
|
-
providerBilling: costsConfig.providers || {},
|
|
175
|
-
budget: { todayUsd: todayTotal.costUsd, capUsd: cap, enforce: enforcementActive() },
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
const last7 = summarizeCostData(entries, { days: 7, costsConfig });
|
|
179
|
-
const last24h = summarizeCostData(entries, { days: 1, costsConfig });
|
|
180
|
-
|
|
181
|
-
// Cache savings — order-of-magnitude estimate. Ledger has per-call cost
|
|
182
|
-
// but no per-call input price, so the math uses a weighted-average input
|
|
183
|
-
// price across the models seen in the window:
|
|
184
|
-
// gross = cacheReadTokens * avgInputPrice * 0.9
|
|
185
|
-
// premium = cacheCreationTokens * avgInputPrice * 0.25
|
|
186
|
-
// The card surfaces the caveat explicitly to callers.
|
|
187
|
-
let cacheSavings = null;
|
|
188
|
-
try {
|
|
189
|
-
const { resolveModelPricing } = await import('../telemetry/model-pricing-catalog.mjs');
|
|
190
|
-
// Token-weighted avg input price across models in this window.
|
|
191
|
-
// byAgent gives us per-agent input-token totals; we can resolve
|
|
192
|
-
// each agent's model and weight its inputPrice by its share of
|
|
193
|
-
// total billable input. If only one model is present, this
|
|
194
|
-
// collapses to that model's price.
|
|
195
|
-
let weightedInputPrice = 0;
|
|
196
|
-
let weightedDenominator = 0;
|
|
197
|
-
const modelsSeen = new Set();
|
|
198
|
-
for (const a of last7.byAgent || []) {
|
|
199
|
-
const model = a.model || 'claude-sonnet-4-6';
|
|
200
|
-
modelsSeen.add(model);
|
|
201
|
-
const p = resolveModelPricing(model);
|
|
202
|
-
if (!p) continue;
|
|
203
|
-
const tokens = (a.inputTokens || 0) + (a.cacheReadInputTokens || 0) + (a.cacheCreationInputTokens || 0);
|
|
204
|
-
if (tokens === 0) continue;
|
|
205
|
-
weightedInputPrice += p.inputPrice * tokens;
|
|
206
|
-
weightedDenominator += tokens;
|
|
207
|
-
}
|
|
208
|
-
const avgInputPrice = weightedDenominator > 0
|
|
209
|
-
? weightedInputPrice / weightedDenominator
|
|
210
|
-
: (resolveModelPricing('claude-sonnet-4-6')?.inputPrice ?? 3 / 1_000_000);
|
|
211
|
-
const grossSaved = last7.cacheReadInputTokens * avgInputPrice * 0.9;
|
|
212
|
-
const writePremium = last7.cacheCreationInputTokens * avgInputPrice * 0.25;
|
|
213
|
-
const netSaved = grossSaved - writePremium;
|
|
214
|
-
const wouldHaveCost = last7.totalCostUsd + netSaved;
|
|
215
|
-
cacheSavings = {
|
|
216
|
-
netSavedUsd: Number(netSaved.toFixed(4)),
|
|
217
|
-
grossSavedUsd: Number(grossSaved.toFixed(4)),
|
|
218
|
-
writePremiumUsd: Number(writePremium.toFixed(4)),
|
|
219
|
-
wouldHaveCostUsd: Number(wouldHaveCost.toFixed(4)),
|
|
220
|
-
savingsRatio: wouldHaveCost > 0 ? netSaved / wouldHaveCost : 0,
|
|
221
|
-
modelsInWindow: [...modelsSeen],
|
|
222
|
-
avgInputPriceUsdPerToken: Number(avgInputPrice.toFixed(10)),
|
|
223
|
-
approximation: 'token-weighted avg input price; precise per-call price is not recoverable from the ledger',
|
|
224
|
-
};
|
|
225
|
-
} catch { /* savings is best-effort */ }
|
|
226
|
-
|
|
227
|
-
// Token mix — total counts to render a stacked bar on the dashboard.
|
|
228
|
-
const tokenMix = {
|
|
229
|
-
input: last7.totalInputTokens,
|
|
230
|
-
cacheRead: last7.cacheReadInputTokens,
|
|
231
|
-
cacheCreation: last7.cacheCreationInputTokens,
|
|
232
|
-
output: last7.totalOutputTokens,
|
|
233
|
-
reasoning: last7.totalReasoningTokens || 0,
|
|
234
|
-
};
|
|
235
|
-
const tokenMixTotal = Object.values(tokenMix).reduce((s, v) => s + v, 0);
|
|
236
|
-
|
|
237
|
-
// Today's actual metered spend — recomputed against per-provider modes so
|
|
238
|
-
// a subscription provider (Claude Pro, e.g.) doesn't dump its
|
|
239
|
-
// metered-equivalent into the headline number. Falls back to the legacy
|
|
240
|
-
// todayTotal.costUsd when no day-bucketing of today's slice is available.
|
|
241
|
-
const today = summarizeCostData(entries, { days: 1, costsConfig });
|
|
242
|
-
const todayActualUsd = today.meteredActualUsd;
|
|
243
|
-
const todayEquivUsd = todayTotal.costUsd;
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
state: 'ok',
|
|
247
|
-
billingMode,
|
|
248
|
-
billingSource: billing.source,
|
|
249
|
-
providerBilling: costsConfig.providers || {},
|
|
250
|
-
budget: {
|
|
251
|
-
todayUsd: todayEquivUsd,
|
|
252
|
-
todayActualUsd,
|
|
253
|
-
todayMeteredEquivalentUsd: todayEquivUsd,
|
|
254
|
-
capUsd: cap,
|
|
255
|
-
usageRatio: cap > 0 ? todayActualUsd / cap : 0,
|
|
256
|
-
enforce: enforcementActive(),
|
|
257
|
-
byPersona: todayTotal.byPersona,
|
|
258
|
-
},
|
|
259
|
-
cacheSavings,
|
|
260
|
-
tokenMix: { ...tokenMix, total: tokenMixTotal },
|
|
261
|
-
byProvider: last7.byProvider,
|
|
262
|
-
windows: {
|
|
263
|
-
last24h: {
|
|
264
|
-
interactions: last24h.interactions,
|
|
265
|
-
totalCostUsd: last24h.totalCostUsd,
|
|
266
|
-
meteredActualUsd: last24h.meteredActualUsd,
|
|
267
|
-
inputTokens: last24h.totalInputTokens,
|
|
268
|
-
outputTokens: last24h.totalOutputTokens,
|
|
269
|
-
cacheHitRate: last24h.cacheHitRate,
|
|
270
|
-
},
|
|
271
|
-
last7d: {
|
|
272
|
-
interactions: last7.interactions,
|
|
273
|
-
totalCostUsd: last7.totalCostUsd,
|
|
274
|
-
meteredActualUsd: last7.meteredActualUsd,
|
|
275
|
-
inputTokens: last7.totalInputTokens,
|
|
276
|
-
outputTokens: last7.totalOutputTokens,
|
|
277
|
-
cacheHitRate: last7.cacheHitRate,
|
|
278
|
-
byAgent: last7.byAgent,
|
|
279
|
-
byProvider: last7.byProvider,
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
} catch (err) {
|
|
284
|
-
return { state: 'unreachable', message: err.message };
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function summarizeIntakeQueue(env, cwd = process.cwd()) {
|
|
289
|
-
try {
|
|
290
|
-
const pending = path.join(cwd, '.cx', 'intake', 'pending');
|
|
291
|
-
const processed = path.join(cwd, '.cx', 'intake', 'processed');
|
|
292
|
-
const skipped = path.join(cwd, '.cx', 'intake', 'skipped');
|
|
293
|
-
const count = (dir) => {
|
|
294
|
-
try {
|
|
295
|
-
if (!fs.existsSync(dir)) return 0;
|
|
296
|
-
return fs.readdirSync(dir).filter((f) => f.endsWith('.json')).length;
|
|
297
|
-
} catch { return 0; }
|
|
298
|
-
};
|
|
299
|
-
const pendingCount = count(pending);
|
|
300
|
-
const processedCount = count(processed);
|
|
301
|
-
const skippedCount = count(skipped);
|
|
302
|
-
// Count customer-linked items in pending queue
|
|
303
|
-
let customerLinked = 0;
|
|
304
|
-
try {
|
|
305
|
-
const pendingFiles = fs.readdirSync(pending).filter((f) => f.endsWith('.json'));
|
|
306
|
-
for (const f of pendingFiles) {
|
|
307
|
-
try {
|
|
308
|
-
const content = JSON.parse(fs.readFileSync(path.join(pending, f), 'utf8'));
|
|
309
|
-
if (content.customers?.length > 0) customerLinked++;
|
|
310
|
-
} catch { /* skip malformed */ }
|
|
311
|
-
}
|
|
312
|
-
} catch { /* skip */ }
|
|
313
|
-
|
|
314
|
-
// Profile-aware labels: the active profile's rebrand block (e.g.
|
|
315
|
-
// rnd → "Intake queue"/"signal"; sales-ops → "Lead inbox"/"lead")
|
|
316
|
-
// travels in the payload so the dashboard renders the right noun
|
|
317
|
-
// without hardcoding "intake" anywhere user-facing.
|
|
318
|
-
|
|
319
|
-
let rebrand;
|
|
320
|
-
try { rebrand = getRebrand(cwd); }
|
|
321
|
-
catch { rebrand = { intakeQueueLabel: 'Intake queue', signalNoun: 'signal' }; }
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
state: pendingCount === 0 && processedCount === 0 && skippedCount === 0 ? 'empty' : 'ok',
|
|
325
|
-
pending: pendingCount,
|
|
326
|
-
processed: processedCount,
|
|
327
|
-
skipped: skippedCount,
|
|
328
|
-
customerLinked,
|
|
329
|
-
label: rebrand.intakeQueueLabel,
|
|
330
|
-
itemNoun: rebrand.signalNoun,
|
|
331
|
-
};
|
|
332
|
-
} catch (err) {
|
|
333
|
-
return { state: 'unreachable', message: err.message };
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function summarizeBeads(rootDir) {
|
|
338
|
-
try {
|
|
339
|
-
let bdCwd = rootDir;
|
|
340
|
-
try {
|
|
341
|
-
const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: rootDir, encoding: 'utf8' });
|
|
342
|
-
if (r.status === 0) {
|
|
343
|
-
const t = r.stdout.trim();
|
|
344
|
-
const resolved = t.startsWith('/') ? t : path.join(rootDir, t);
|
|
345
|
-
const cand = path.dirname(resolved);
|
|
346
|
-
if (fs.existsSync(path.join(cand, '.beads'))) bdCwd = cand;
|
|
347
|
-
}
|
|
348
|
-
} catch { /* fall back */ }
|
|
349
|
-
const r = spawnSync('bd', ['list', '--status', 'all', '--json'], {
|
|
350
|
-
cwd: bdCwd, encoding: 'utf8', env: { ...process.env, NO_COLOR: '1' }, timeout: 5000,
|
|
351
|
-
});
|
|
352
|
-
if (r.status !== 0) return { state: 'unreachable', message: r.stderr?.slice(0, 200) || 'bd list failed' };
|
|
353
|
-
const issues = JSON.parse(r.stdout);
|
|
354
|
-
const byStatus = {};
|
|
355
|
-
const byPriority = {};
|
|
356
|
-
for (const it of issues) {
|
|
357
|
-
byStatus[it.status] = (byStatus[it.status] || 0) + 1;
|
|
358
|
-
byPriority[`P${it.priority}`] = (byPriority[`P${it.priority}`] || 0) + 1;
|
|
359
|
-
}
|
|
360
|
-
return {
|
|
361
|
-
state: issues.length === 0 ? 'empty' : 'ok',
|
|
362
|
-
total: issues.length,
|
|
363
|
-
byStatus,
|
|
364
|
-
byPriority,
|
|
365
|
-
};
|
|
366
|
-
} catch (err) {
|
|
367
|
-
return { state: 'unreachable', message: err.message };
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function summarizeModelProviders(env) {
|
|
372
|
-
const entries = [
|
|
373
|
-
{ id: 'anthropic', displayName: 'Anthropic', envKey: 'ANTHROPIC_API_KEY', altKey: null, docsUrl: 'https://docs.anthropic.com/api' },
|
|
374
|
-
{ id: 'openai', displayName: 'OpenAI', envKey: 'OPENAI_API_KEY', altKey: null, docsUrl: 'https://platform.openai.com/api-keys' },
|
|
375
|
-
{ id: 'openrouter', displayName: 'OpenRouter', envKey: 'OPENROUTER_API_KEY', altKey: 'OPEN_ROUTER_API_KEY', docsUrl: 'https://openrouter.ai/keys' },
|
|
376
|
-
{ id: 'ollama', displayName: 'Ollama (local)', envKey: 'OLLAMA_BASE_URL', altKey: null, docsUrl: 'https://ollama.com' },
|
|
377
|
-
{ id: 'groq', displayName: 'Groq', envKey: 'GROQ_API_KEY', altKey: null, docsUrl: 'https://console.groq.com/keys' },
|
|
378
|
-
{ id: 'mistral', displayName: 'Mistral', envKey: 'MISTRAL_API_KEY', altKey: null, docsUrl: 'https://console.mistral.ai/api-keys' },
|
|
379
|
-
{ id: 'gemini', displayName: 'Google Gemini', envKey: 'GEMINI_API_KEY', altKey: null, docsUrl: 'https://aistudio.google.com/app/apikey' },
|
|
380
|
-
];
|
|
381
|
-
return entries.map((e) => {
|
|
382
|
-
// Check primary key, alt key, then multi-source resolution
|
|
383
|
-
const raw = env[e.envKey] || (e.altKey ? env[e.altKey] : null);
|
|
384
|
-
const configured = typeof raw === 'string' && raw.trim().length > 0 && !raw.includes('__');
|
|
385
|
-
// If not found in env, try multi-source resolution (config.env, .env, shell rc, 1Password)
|
|
386
|
-
const resolved = !configured ? resolveModelCredential(e.envKey, e.altKey) : null;
|
|
387
|
-
const isConfigured = configured || Boolean(resolved);
|
|
388
|
-
return {
|
|
389
|
-
...e,
|
|
390
|
-
configured: isConfigured,
|
|
391
|
-
state: isConfigured ? 'configured' : 'not-configured',
|
|
392
|
-
};
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Resolve a model provider credential from config.env, ~/.env, and shell rc files.
|
|
398
|
-
*/
|
|
399
|
-
function resolveModelCredential(primaryKey, altKey) {
|
|
400
|
-
const homeDir = os.homedir();
|
|
401
|
-
const keys = [primaryKey, altKey].filter(Boolean);
|
|
402
|
-
|
|
403
|
-
// Special case: Ollama on default port (no OLLAMA_BASE_URL needed)
|
|
404
|
-
if (primaryKey === 'OLLAMA_BASE_URL') {
|
|
405
|
-
try {
|
|
406
|
-
const r = spawnSync('curl', ['-s', '--connect-timeout', '1', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:11434/api/tags'], { encoding: 'utf8', timeout: 3000 });
|
|
407
|
-
if (r.status === 0 && r.stdout?.trim() === '200') return 'http://localhost:11434';
|
|
408
|
-
} catch { /* not available */ }
|
|
409
|
-
// Also try the ollama binary
|
|
410
|
-
try {
|
|
411
|
-
const r = spawnSync('ollama', ['--version'], { encoding: 'utf8', timeout: 2000 });
|
|
412
|
-
if (r.status === 0) return 'found-ollama-binary';
|
|
413
|
-
} catch { /* not available */ }
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const paths = [path.join(configDir(homeDir), 'config.env'), path.join(homeDir, '.env')];
|
|
417
|
-
for (const p of paths) {
|
|
418
|
-
try {
|
|
419
|
-
if (!fs.existsSync(p)) continue;
|
|
420
|
-
const content = fs.readFileSync(p, 'utf8');
|
|
421
|
-
for (const key of keys) {
|
|
422
|
-
const m = content.match(new RegExp(`^${key}=["']?(.+?)["']?$`, 'm'));
|
|
423
|
-
if (m && m[1]) return m[1].trim();
|
|
424
|
-
}
|
|
425
|
-
} catch { /* skip */ }
|
|
426
|
-
}
|
|
427
|
-
const shellFiles = [path.join(homeDir, '.zshrc'), path.join(homeDir, '.bashrc'), path.join(homeDir, '.bash_profile'), path.join(homeDir, '.profile')];
|
|
428
|
-
for (const rc of shellFiles) {
|
|
429
|
-
if (!fs.existsSync(rc)) continue;
|
|
430
|
-
try {
|
|
431
|
-
const content = fs.readFileSync(rc, 'utf8');
|
|
432
|
-
for (const key of keys) {
|
|
433
|
-
const directRe = new RegExp(`^\\s*export\\s+${key}=`, 'm');
|
|
434
|
-
if (directRe.test(content)) return 'found-in-rc';
|
|
435
|
-
}
|
|
436
|
-
} catch { /* skip */ }
|
|
437
|
-
}
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function summarizeTelemetryCostTrend(env, { days = 7 } = {}) {
|
|
442
|
-
// Fetches each trace + its totalCost and buckets by yyyy-mm-dd. The
|
|
443
|
-
// public /traces endpoint includes totalCost when generations have
|
|
444
|
-
// been priced; sparse trace cost is fine — the chart shows shape, not
|
|
445
|
-
// an audit-grade ledger value.
|
|
446
|
-
const auth = telemetryAuthHeader(env);
|
|
447
|
-
if (!auth) return { state: 'misconfigured', message: 'CONSTRUCT_TELEMETRY_PUBLIC_KEY / SECRET_KEY not set.' };
|
|
448
|
-
const baseUrl = telemetryBaseUrl(env);
|
|
449
|
-
const controller = new AbortController();
|
|
450
|
-
const timer = setTimeout(() => controller.abort(), 6000);
|
|
451
|
-
try {
|
|
452
|
-
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
453
|
-
// Telemetry caps `limit` at 100 — 500 returns 400. Use the cap and accept that
|
|
454
|
-
// weeks with >100 traces will undercount the trailing days; the chart is for
|
|
455
|
-
// shape, not audit. Re-try without `fromTimestamp` if the server version
|
|
456
|
-
// rejects that param (same fallback shape as the latency probe).
|
|
457
|
-
let res = await fetch(`${baseUrl}/api/public/traces?limit=100&fromTimestamp=${encodeURIComponent(since)}`, {
|
|
458
|
-
headers: { Authorization: auth },
|
|
459
|
-
signal: controller.signal,
|
|
460
|
-
});
|
|
461
|
-
if (res.status === 400 || res.status === 422) {
|
|
462
|
-
res = await fetch(`${baseUrl}/api/public/traces?limit=100`, {
|
|
463
|
-
headers: { Authorization: auth },
|
|
464
|
-
signal: controller.signal,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
clearTimeout(timer);
|
|
468
|
-
if (!res.ok) return { state: 'unreachable', message: `Telemetry HTTP ${res.status}` };
|
|
469
|
-
const json = await res.json();
|
|
470
|
-
const traces = Array.isArray(json.data) ? json.data : [];
|
|
471
|
-
const buckets = new Map();
|
|
472
|
-
for (let d = 0; d < days; d++) {
|
|
473
|
-
const day = new Date(Date.now() - d * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
474
|
-
buckets.set(day, { day, costUsd: 0, traces: 0, errors: 0 });
|
|
475
|
-
}
|
|
476
|
-
let withCost = 0;
|
|
477
|
-
for (const trace of traces) {
|
|
478
|
-
const ts = trace?.timestamp || trace?.createdAt;
|
|
479
|
-
if (!ts) continue;
|
|
480
|
-
const day = String(ts).slice(0, 10);
|
|
481
|
-
const bucket = buckets.get(day);
|
|
482
|
-
if (!bucket) continue;
|
|
483
|
-
bucket.traces += 1;
|
|
484
|
-
const cost = Number(trace?.totalCost) || 0;
|
|
485
|
-
if (cost > 0) { bucket.costUsd += cost; withCost += 1; }
|
|
486
|
-
const tags = Array.isArray(trace?.tags) ? trace.tags : [];
|
|
487
|
-
if (tags.some((t) => /error|fail/i.test(t))) bucket.errors += 1;
|
|
488
|
-
}
|
|
489
|
-
const series = [...buckets.values()].sort((a, b) => a.day.localeCompare(b.day));
|
|
490
|
-
return { state: traces.length === 0 ? 'empty' : 'ok', days, sampleSize: traces.length, withCost, series };
|
|
491
|
-
} catch (err) {
|
|
492
|
-
clearTimeout(timer);
|
|
493
|
-
return { state: 'unreachable', message: err?.message || 'fetch failed' };
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function quantile(values, q) {
|
|
498
|
-
if (!values.length) return null;
|
|
499
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
500
|
-
const pos = (sorted.length - 1) * q;
|
|
501
|
-
const base = Math.floor(pos);
|
|
502
|
-
const rest = pos - base;
|
|
503
|
-
if (sorted[base + 1] !== undefined) {
|
|
504
|
-
return sorted[base] + rest * (sorted[base + 1] - sorted[base]);
|
|
505
|
-
}
|
|
506
|
-
return sorted[base];
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
async function summarizeTelemetryLatency(env, { days = 7, limit = 50 } = {}) {
|
|
510
|
-
const auth = telemetryAuthHeader(env);
|
|
511
|
-
if (!auth) return { state: 'misconfigured', message: 'CONSTRUCT_TELEMETRY_PUBLIC_KEY / SECRET_KEY not set.' };
|
|
512
|
-
const baseUrl = telemetryBaseUrl(env);
|
|
513
|
-
const controller = new AbortController();
|
|
514
|
-
const timer = setTimeout(() => controller.abort(), 6000);
|
|
515
|
-
try {
|
|
516
|
-
// Telemetry public API observations: `type=GENERATION` plus an
|
|
517
|
-
// optional `fromStartTime` (some versions reject the param, so we
|
|
518
|
-
// try with it first and fall back to a plain page if the server
|
|
519
|
-
// returns 4xx with that param).
|
|
520
|
-
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
521
|
-
let res = await fetch(
|
|
522
|
-
`${baseUrl}/api/public/observations?type=GENERATION&fromStartTime=${encodeURIComponent(since)}&limit=${limit}`,
|
|
523
|
-
{ headers: { Authorization: auth }, signal: controller.signal },
|
|
524
|
-
);
|
|
525
|
-
if (res.status === 400 || res.status === 422) {
|
|
526
|
-
res = await fetch(
|
|
527
|
-
`${baseUrl}/api/public/observations?type=GENERATION&limit=${limit}`,
|
|
528
|
-
{ headers: { Authorization: auth }, signal: controller.signal },
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
clearTimeout(timer);
|
|
532
|
-
if (!res.ok) return { state: 'unreachable', message: `Telemetry HTTP ${res.status}` };
|
|
533
|
-
const json = await res.json();
|
|
534
|
-
const observations = Array.isArray(json.data) ? json.data : [];
|
|
535
|
-
if (observations.length === 0) {
|
|
536
|
-
return { state: 'empty', message: 'No generation observations in the window.' };
|
|
537
|
-
}
|
|
538
|
-
const latencies = [];
|
|
539
|
-
const ttfts = [];
|
|
540
|
-
const tps = [];
|
|
541
|
-
let withTtft = 0;
|
|
542
|
-
let errorCount = 0;
|
|
543
|
-
const byModel = new Map();
|
|
544
|
-
for (const g of observations) {
|
|
545
|
-
const start = g.startTime ? Date.parse(g.startTime) : null;
|
|
546
|
-
const end = g.endTime ? Date.parse(g.endTime) : null;
|
|
547
|
-
const ttft = g.completionStartTime ? Date.parse(g.completionStartTime) : null;
|
|
548
|
-
const model = g.model || 'unknown';
|
|
549
|
-
const level = (g.level || '').toLowerCase();
|
|
550
|
-
const isError = level === 'error';
|
|
551
|
-
if (isError) errorCount += 1;
|
|
552
|
-
if (start && end && end > start) {
|
|
553
|
-
const totalMs = end - start;
|
|
554
|
-
latencies.push(totalMs);
|
|
555
|
-
const outTokens = Number(g.completionTokens ?? g.usage?.output ?? 0);
|
|
556
|
-
if (outTokens > 0 && ttft && end > ttft) {
|
|
557
|
-
tps.push((outTokens / (end - ttft)) * 1000);
|
|
558
|
-
}
|
|
559
|
-
if (ttft && start && ttft > start) {
|
|
560
|
-
ttfts.push(ttft - start);
|
|
561
|
-
withTtft += 1;
|
|
562
|
-
}
|
|
563
|
-
const entry = byModel.get(model) || { traces: 0, totalMs: 0, errors: 0 };
|
|
564
|
-
entry.traces += 1;
|
|
565
|
-
entry.totalMs += totalMs;
|
|
566
|
-
if (isError) entry.errors += 1;
|
|
567
|
-
byModel.set(model, entry);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
const byAgent = new Map();
|
|
571
|
-
for (const g of observations) {
|
|
572
|
-
const agent = g.metadata?.agent || g.parentObservationId ? (g.metadata?.agent || null) : null;
|
|
573
|
-
// Observation `name` often follows `llm.<agent>` from the opencode
|
|
574
|
-
// plugin — extract the agent from there as a fallback when the
|
|
575
|
-
// metadata field is empty.
|
|
576
|
-
let resolvedAgent = agent;
|
|
577
|
-
if (!resolvedAgent && typeof g.name === 'string' && g.name.startsWith('llm.')) {
|
|
578
|
-
const tail = g.name.slice(4);
|
|
579
|
-
if (tail && tail !== 'chat') resolvedAgent = tail;
|
|
580
|
-
}
|
|
581
|
-
if (!resolvedAgent) continue;
|
|
582
|
-
const start = g.startTime ? Date.parse(g.startTime) : null;
|
|
583
|
-
const end = g.endTime ? Date.parse(g.endTime) : null;
|
|
584
|
-
if (!start || !end || end <= start) continue;
|
|
585
|
-
const totalMs = end - start;
|
|
586
|
-
const entry = byAgent.get(resolvedAgent) || { traces: 0, totalMs: 0, errors: 0 };
|
|
587
|
-
entry.traces += 1;
|
|
588
|
-
entry.totalMs += totalMs;
|
|
589
|
-
if ((g.level || '').toLowerCase() === 'error') entry.errors += 1;
|
|
590
|
-
byAgent.set(resolvedAgent, entry);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
return {
|
|
594
|
-
state: 'ok',
|
|
595
|
-
sampleSize: observations.length,
|
|
596
|
-
windowDays: days,
|
|
597
|
-
latencyMs: {
|
|
598
|
-
p50: quantile(latencies, 0.5),
|
|
599
|
-
p95: quantile(latencies, 0.95),
|
|
600
|
-
p99: quantile(latencies, 0.99),
|
|
601
|
-
},
|
|
602
|
-
ttftMs: ttfts.length ? { p50: quantile(ttfts, 0.5), p95: quantile(ttfts, 0.95), sampleSize: withTtft } : null,
|
|
603
|
-
tokensPerSecond: tps.length ? { p50: quantile(tps, 0.5), p95: quantile(tps, 0.95) } : null,
|
|
604
|
-
errorRate: observations.length > 0 ? errorCount / observations.length : 0,
|
|
605
|
-
errorCount,
|
|
606
|
-
perModel: [...byModel.entries()]
|
|
607
|
-
.map(([model, v]) => ({ model, traces: v.traces, avgMs: Math.round(v.totalMs / v.traces), errors: v.errors }))
|
|
608
|
-
.sort((a, b) => b.traces - a.traces)
|
|
609
|
-
.slice(0, 8),
|
|
610
|
-
perAgent: [...byAgent.entries()]
|
|
611
|
-
.map(([agent, v]) => ({ agent, traces: v.traces, avgMs: Math.round(v.totalMs / v.traces), errors: v.errors }))
|
|
612
|
-
.sort((a, b) => b.traces - a.traces)
|
|
613
|
-
.slice(0, 12),
|
|
614
|
-
};
|
|
615
|
-
} catch (err) {
|
|
616
|
-
clearTimeout(timer);
|
|
617
|
-
return { state: 'unreachable', message: err?.message || 'fetch failed' };
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function summarizeRetrievalEval(homeDir = os.homedir()) {
|
|
622
|
-
try {
|
|
623
|
-
const cachePath = path.join(doctorRoot(homeDir), 'evals', 'retrieval-latest.json');
|
|
624
|
-
if (!fs.existsSync(cachePath)) {
|
|
625
|
-
return {
|
|
626
|
-
state: 'empty',
|
|
627
|
-
message: 'No retrieval eval yet. Run `construct evals retrieval` to populate.',
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
631
|
-
const r = cached.report?.summary || cached.report || {};
|
|
632
|
-
let history = [];
|
|
633
|
-
try {
|
|
634
|
-
const historyPath = path.join(doctorRoot(homeDir), 'evals', 'retrieval-history.jsonl');
|
|
635
|
-
if (fs.existsSync(historyPath)) {
|
|
636
|
-
const lines = fs.readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
|
|
637
|
-
history = lines.slice(-30).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
638
|
-
}
|
|
639
|
-
} catch { /* history is best-effort */ }
|
|
640
|
-
return {
|
|
641
|
-
state: 'ok',
|
|
642
|
-
ranAt: cached.ts,
|
|
643
|
-
fixture: cached.fixture,
|
|
644
|
-
recallAt5: r.recallAt5 ?? r.recall_at_5 ?? null,
|
|
645
|
-
precisionAt5: r.precisionAt5 ?? r.precision_at_5 ?? null,
|
|
646
|
-
mrr: r.mrr ?? null,
|
|
647
|
-
p50LatencyMs: r.p50LatencyMs ?? r.p50_latency_ms ?? null,
|
|
648
|
-
p95LatencyMs: r.p95LatencyMs ?? r.p95_latency_ms ?? null,
|
|
649
|
-
emptyRate: r.emptyRate ?? r.empty_rate ?? null,
|
|
650
|
-
fallbackRate: r.fallbackRate ?? r.fallback_rate ?? null,
|
|
651
|
-
queryCount: cached.report?.perQuery?.length ?? null,
|
|
652
|
-
history: history.map((h) => ({ ts: h.ts, recallAt5: h.recallAt5 ?? null, mrr: h.mrr ?? null })),
|
|
653
|
-
};
|
|
654
|
-
} catch (err) {
|
|
655
|
-
return { state: 'unreachable', message: err.message };
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
async function summarizeRecommendations() {
|
|
660
|
-
try {
|
|
661
|
-
const { recommendationStats } = await import('../embed/recommendation-store.mjs');
|
|
662
|
-
const stats = recommendationStats();
|
|
663
|
-
return {
|
|
664
|
-
state: stats.total > 0 ? 'ok' : 'empty',
|
|
665
|
-
total: stats.total,
|
|
666
|
-
active: stats.active,
|
|
667
|
-
dismissed: stats.dismissed,
|
|
668
|
-
byPriority: stats.byPriority,
|
|
669
|
-
byType: stats.byType,
|
|
670
|
-
};
|
|
671
|
-
} catch (err) {
|
|
672
|
-
return { state: 'unreachable', message: err.message };
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
async function summarizeIntegrations() {
|
|
677
|
-
try {
|
|
678
|
-
const { detectIntegrationConfig } = await import('../integrations/intake-integrations.mjs');
|
|
679
|
-
const config = detectIntegrationConfig();
|
|
680
|
-
return {
|
|
681
|
-
state: 'ok',
|
|
682
|
-
configured: Object.entries(config)
|
|
683
|
-
.filter(([, v]) => v === true || typeof v === 'string')
|
|
684
|
-
.map(([k]) => k)
|
|
685
|
-
.filter(k => k !== 'githubRepo' && k !== 'jiraHost' && k !== 'confluenceSpace'),
|
|
686
|
-
details: {
|
|
687
|
-
github: config.githubMethod ? `${config.githubMethod}${config.githubRepo ? ` (${config.githubRepo})` : ''}` : 'not configured',
|
|
688
|
-
jira: config.jira ? 'configured' : 'not configured',
|
|
689
|
-
confluence: config.confluence ? 'configured' : 'not configured',
|
|
690
|
-
},
|
|
691
|
-
};
|
|
692
|
-
} catch (err) {
|
|
693
|
-
return { state: 'unreachable', message: err.message };
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function summarizeVectorStore(env) {
|
|
698
|
-
try {
|
|
699
|
-
const mode = env.CONSTRUCT_VECTOR_BACKEND || 'file';
|
|
700
|
-
const indexPath = env.CONSTRUCT_VECTOR_INDEX_PATH || '.cx/vector-index.json';
|
|
701
|
-
const resolved = path.isAbsolute(indexPath) ? indexPath : path.join(process.cwd(), indexPath);
|
|
702
|
-
if (!fs.existsSync(resolved)) {
|
|
703
|
-
return { state: 'empty', mode, message: 'No vector index yet. Run `construct ingest` to populate.' };
|
|
704
|
-
}
|
|
705
|
-
const stat = fs.statSync(resolved);
|
|
706
|
-
let records = 0;
|
|
707
|
-
let model = null;
|
|
708
|
-
let updatedAt = null;
|
|
709
|
-
try {
|
|
710
|
-
const json = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
711
|
-
records = Array.isArray(json.records) ? json.records.length : 0;
|
|
712
|
-
model = json.model || null;
|
|
713
|
-
updatedAt = json.updatedAt || null;
|
|
714
|
-
} catch { /* malformed — fall through to size-only stats */ }
|
|
715
|
-
return {
|
|
716
|
-
state: 'ok',
|
|
717
|
-
mode,
|
|
718
|
-
bytes: stat.size,
|
|
719
|
-
records,
|
|
720
|
-
model,
|
|
721
|
-
updatedAt,
|
|
722
|
-
path: resolved,
|
|
723
|
-
};
|
|
724
|
-
} catch (err) {
|
|
725
|
-
return { state: 'unreachable', message: err.message };
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
export async function buildInsights({ env = process.env, cwd = process.cwd(), rootDir = process.cwd() } = {}) {
|
|
730
|
-
const { measureUsage } = await import('../resources/budget.mjs');
|
|
731
|
-
const { summarizeHandoffs } = await import('../handoffs/inventory.mjs');
|
|
732
|
-
let resources;
|
|
733
|
-
try {
|
|
734
|
-
const u = measureUsage(cwd, env);
|
|
735
|
-
resources = { state: 'ok', ...u };
|
|
736
|
-
} catch (err) {
|
|
737
|
-
resources = { state: 'unreachable', message: err.message };
|
|
738
|
-
}
|
|
739
|
-
let handoffs;
|
|
740
|
-
try {
|
|
741
|
-
handoffs = summarizeHandoffs(cwd, env);
|
|
742
|
-
} catch (err) {
|
|
743
|
-
handoffs = { state: 'unreachable', message: err.message };
|
|
744
|
-
}
|
|
745
|
-
// Cost-ledger and telemetry-cost-trend summaries are intentionally omitted
|
|
746
|
-
// from the dashboard payload. Underlying ledger writes + telemetry traces
|
|
747
|
-
// continue; the consumer surface lands with the OTel + dashboard wiring
|
|
748
|
-
// (plan Workstream J).
|
|
749
|
-
const [telemetry, telemetryLatency, intake, beads, recommendations, integrations] = await Promise.all([
|
|
750
|
-
fetchTelemetrySummary(env),
|
|
751
|
-
summarizeTelemetryLatency(env),
|
|
752
|
-
Promise.resolve(summarizeIntakeQueue(env, cwd)),
|
|
753
|
-
Promise.resolve(summarizeBeads(rootDir)),
|
|
754
|
-
summarizeRecommendations(),
|
|
755
|
-
summarizeIntegrations(),
|
|
756
|
-
]);
|
|
757
|
-
const providers = summarizeModelProviders(env);
|
|
758
|
-
const vector = summarizeVectorStore(env);
|
|
759
|
-
const retrievalEval = summarizeRetrievalEval();
|
|
760
|
-
let sessionUsage;
|
|
761
|
-
try {
|
|
762
|
-
const { readSessionUsage } = await import('../status.mjs');
|
|
763
|
-
sessionUsage = readSessionUsage(os.homedir());
|
|
764
|
-
} catch { sessionUsage = null; }
|
|
765
|
-
return {
|
|
766
|
-
telemetry,
|
|
767
|
-
telemetryLatency,
|
|
768
|
-
intake,
|
|
769
|
-
recommendations,
|
|
770
|
-
integrations,
|
|
771
|
-
beads,
|
|
772
|
-
resources,
|
|
773
|
-
handoffs,
|
|
774
|
-
providers,
|
|
775
|
-
vector,
|
|
776
|
-
retrievalEval,
|
|
777
|
-
sessionUsage,
|
|
778
|
-
generatedAt: new Date().toISOString(),
|
|
779
|
-
};
|
|
780
|
-
}
|