@geraldmaron/construct 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -7
- 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 +1116 -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 +525 -38
- 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
package/lib/cost.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/cost.mjs —
|
|
2
|
+
* lib/cost.mjs — LLM token usage cost estimation and session budget tracking.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Maps model IDs to per-token pricing from a static table (with OpenRouter
|
|
5
|
+
* and LiteLLM fallbacks), estimates call cost from usage objects, and
|
|
6
|
+
* accumulates session spend in ~/.cx/session-cost.json. Called by the
|
|
7
|
+
* telemetry pipeline and the MCP session_usage tool.
|
|
5
8
|
*/
|
|
6
9
|
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
7
10
|
import { join } from 'node:path';
|
|
@@ -47,17 +50,33 @@ export function normalizeCostEntry(entry = {}) {
|
|
|
47
50
|
entry.completion_tokens,
|
|
48
51
|
entry.completionTokens,
|
|
49
52
|
);
|
|
53
|
+
const reasoningTokens = numberFrom(
|
|
54
|
+
entry.reasoning_tokens,
|
|
55
|
+
entry.reasoningTokens,
|
|
56
|
+
entry.completion_tokens_details?.reasoning_tokens,
|
|
57
|
+
);
|
|
58
|
+
const providerTotalTokens = numberFrom(
|
|
59
|
+
entry.total_tokens,
|
|
60
|
+
entry.totalTokens,
|
|
61
|
+
inputTokens + outputTokens + reasoningTokens,
|
|
62
|
+
);
|
|
63
|
+
const billedOutputTokens = outputTokens + reasoningTokens;
|
|
64
|
+
const billedTotalTokens = inputTokens + cacheReadInputTokens + cacheCreationInputTokens + billedOutputTokens;
|
|
50
65
|
|
|
51
66
|
return {
|
|
52
67
|
...entry,
|
|
53
68
|
inputTokens,
|
|
54
69
|
outputTokens,
|
|
70
|
+
reasoningTokens,
|
|
55
71
|
cacheReadInputTokens,
|
|
56
72
|
cacheCreationInputTokens,
|
|
57
73
|
cacheCreation5mInputTokens,
|
|
58
74
|
cacheCreation1hInputTokens,
|
|
59
75
|
processedInputTokens: inputTokens + cacheReadInputTokens + cacheCreationInputTokens,
|
|
60
|
-
|
|
76
|
+
providerTotalTokens,
|
|
77
|
+
billedOutputTokens,
|
|
78
|
+
billedTotalTokens,
|
|
79
|
+
totalTokens: providerTotalTokens,
|
|
61
80
|
costUsd: Number(entry.cost_usd ?? entry.costUsd ?? 0),
|
|
62
81
|
};
|
|
63
82
|
}
|
|
@@ -92,17 +111,23 @@ export function aggregateCostByAgent(entries) {
|
|
|
92
111
|
interactions: 0,
|
|
93
112
|
inputTokens: 0,
|
|
94
113
|
outputTokens: 0,
|
|
114
|
+
reasoningTokens: 0,
|
|
95
115
|
cacheReadInputTokens: 0,
|
|
96
116
|
cacheCreationInputTokens: 0,
|
|
97
117
|
processedInputTokens: 0,
|
|
118
|
+
providerTotalTokens: 0,
|
|
119
|
+
billedTotalTokens: 0,
|
|
98
120
|
costUsd: 0,
|
|
99
121
|
};
|
|
100
122
|
acc.interactions += 1;
|
|
101
123
|
acc.inputTokens += entry.inputTokens;
|
|
102
124
|
acc.outputTokens += entry.outputTokens;
|
|
125
|
+
acc.reasoningTokens += entry.reasoningTokens;
|
|
103
126
|
acc.cacheReadInputTokens += entry.cacheReadInputTokens;
|
|
104
127
|
acc.cacheCreationInputTokens += entry.cacheCreationInputTokens;
|
|
105
128
|
acc.processedInputTokens += entry.processedInputTokens;
|
|
129
|
+
acc.providerTotalTokens += entry.providerTotalTokens;
|
|
130
|
+
acc.billedTotalTokens += entry.billedTotalTokens;
|
|
106
131
|
acc.costUsd += entry.costUsd;
|
|
107
132
|
acc.cacheReadRate = acc.processedInputTokens > 0
|
|
108
133
|
? Number((acc.cacheReadInputTokens / acc.processedInputTokens).toFixed(3))
|
|
@@ -132,7 +157,47 @@ export function computeCacheStats(entries) {
|
|
|
132
157
|
};
|
|
133
158
|
}
|
|
134
159
|
|
|
135
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Map a model id to its provider id. Reads the model string the same way the
|
|
162
|
+
* model router does — prefix matches win, otherwise infer from naming family.
|
|
163
|
+
* Callers use this to look up per-provider billing-mode overrides so that
|
|
164
|
+
* subscription providers (Claude Pro, e.g.) contribute $0 to the headline
|
|
165
|
+
* "actual spend" while still reporting metered-equivalent for context.
|
|
166
|
+
*/
|
|
167
|
+
export function providerForModel(modelId) {
|
|
168
|
+
const id = String(modelId ?? '').toLowerCase();
|
|
169
|
+
if (!id) return 'unknown';
|
|
170
|
+
if (id.startsWith('openrouter/')) return 'openrouter';
|
|
171
|
+
if (id.startsWith('github-copilot/')) return 'github-copilot';
|
|
172
|
+
if (id.startsWith('ollama/')) return 'ollama';
|
|
173
|
+
if (id.startsWith('local/')) return 'local';
|
|
174
|
+
if (id.startsWith('anthropic/') || id.startsWith('claude-') || id.includes('/claude-')) return 'anthropic';
|
|
175
|
+
if (id.startsWith('openai/') || id.startsWith('gpt-') || /^o[3-9](-|$)/.test(id)) return 'openai';
|
|
176
|
+
if (id.startsWith('google/') || id.startsWith('gemini-')) return 'gemini';
|
|
177
|
+
if (id.startsWith('mistral/') || id.startsWith('mistralai/')) return 'mistral';
|
|
178
|
+
if (id.startsWith('groq/')) return 'groq';
|
|
179
|
+
return 'unknown';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve billing mode for a given provider id. Per-provider override in
|
|
184
|
+
* `costs.providers.<id>.billingMode` wins; otherwise fall back to the global
|
|
185
|
+
* `costs.billingMode`. Default is 'metered' (treat $ figures as real spend)
|
|
186
|
+
* which is the safe assumption when nothing is declared.
|
|
187
|
+
*/
|
|
188
|
+
export function billingModeForProvider(providerId, costsConfig = {}) {
|
|
189
|
+
const perProvider = costsConfig?.providers?.[providerId]?.billingMode;
|
|
190
|
+
if (perProvider === 'subscription' || perProvider === 'metered' || perProvider === 'mixed') {
|
|
191
|
+
return perProvider;
|
|
192
|
+
}
|
|
193
|
+
const globalMode = costsConfig?.billingMode;
|
|
194
|
+
if (globalMode === 'subscription' || globalMode === 'metered' || globalMode === 'mixed') {
|
|
195
|
+
return globalMode;
|
|
196
|
+
}
|
|
197
|
+
return 'metered';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function summarizeCostData(entries, { days, agent, costsConfig } = {}) {
|
|
136
201
|
const cutoff = days ? new Date(Date.now() - days * 86_400_000).toISOString() : null;
|
|
137
202
|
let filtered = cutoff ? entries.filter((e) => e.ts && e.ts >= cutoff) : entries;
|
|
138
203
|
if (agent) filtered = filtered.filter((e) => (e.agent || 'orchestrator') === agent);
|
|
@@ -140,16 +205,53 @@ export function summarizeCostData(entries, { days, agent } = {}) {
|
|
|
140
205
|
const normalized = filtered.map((entry) => normalizeCostEntry(entry));
|
|
141
206
|
const totalInputTokens = normalized.reduce((s, e) => s + e.inputTokens, 0);
|
|
142
207
|
const totalOutputTokens = normalized.reduce((s, e) => s + e.outputTokens, 0);
|
|
143
|
-
const
|
|
208
|
+
const totalReasoningTokens = normalized.reduce((s, e) => s + e.reasoningTokens, 0);
|
|
209
|
+
const providerTotalTokens = normalized.reduce((s, e) => s + e.providerTotalTokens, 0);
|
|
210
|
+
const billedTotalTokens = normalized.reduce((s, e) => s + e.billedTotalTokens, 0);
|
|
144
211
|
const totalCostUsd = normalized.reduce((s, e) => s + e.costUsd, 0);
|
|
145
212
|
const cacheStats = computeCacheStats(normalized);
|
|
146
213
|
|
|
214
|
+
// Per-provider rollup. byProvider[id] = { providerId, billingMode, costUsd, ... }.
|
|
215
|
+
// meteredActualUsd sums only metered/mixed providers; meteredEquivalentUsd is
|
|
216
|
+
// the unconditional total (what the bill would be if everything were metered).
|
|
217
|
+
const byProvider = new Map();
|
|
218
|
+
for (const e of normalized) {
|
|
219
|
+
const providerId = providerForModel(e.model);
|
|
220
|
+
const bucket = byProvider.get(providerId) || {
|
|
221
|
+
providerId,
|
|
222
|
+
billingMode: billingModeForProvider(providerId, costsConfig),
|
|
223
|
+
interactions: 0,
|
|
224
|
+
costUsd: 0,
|
|
225
|
+
inputTokens: 0,
|
|
226
|
+
outputTokens: 0,
|
|
227
|
+
cacheReadInputTokens: 0,
|
|
228
|
+
cacheCreationInputTokens: 0,
|
|
229
|
+
};
|
|
230
|
+
bucket.interactions += 1;
|
|
231
|
+
bucket.costUsd += e.costUsd;
|
|
232
|
+
bucket.inputTokens += e.inputTokens;
|
|
233
|
+
bucket.outputTokens += e.outputTokens;
|
|
234
|
+
bucket.cacheReadInputTokens += e.cacheReadInputTokens;
|
|
235
|
+
bucket.cacheCreationInputTokens += e.cacheCreationInputTokens;
|
|
236
|
+
byProvider.set(providerId, bucket);
|
|
237
|
+
}
|
|
238
|
+
const providerRollup = [...byProvider.values()]
|
|
239
|
+
.map((b) => ({ ...b, costUsd: Number(b.costUsd.toFixed(4)) }))
|
|
240
|
+
.sort((a, b) => b.costUsd - a.costUsd);
|
|
241
|
+
const meteredActualUsd = providerRollup
|
|
242
|
+
.filter((p) => p.billingMode !== 'subscription')
|
|
243
|
+
.reduce((s, p) => s + p.costUsd, 0);
|
|
244
|
+
|
|
147
245
|
return {
|
|
148
246
|
interactions: normalized.length,
|
|
149
247
|
totalInputTokens,
|
|
150
248
|
totalOutputTokens,
|
|
151
|
-
|
|
249
|
+
totalReasoningTokens,
|
|
250
|
+
providerTotalTokens,
|
|
251
|
+
billedTotalTokens,
|
|
152
252
|
totalCostUsd: Number(totalCostUsd.toFixed(4)),
|
|
253
|
+
meteredActualUsd: Number(meteredActualUsd.toFixed(4)),
|
|
254
|
+
meteredEquivalentUsd: Number(totalCostUsd.toFixed(4)),
|
|
153
255
|
processedInputTokens: cacheStats.totalProcessedInputTokens,
|
|
154
256
|
cacheReadInputTokens: cacheStats.totalCacheReadInputTokens,
|
|
155
257
|
cacheCreationInputTokens: cacheStats.totalCacheCreationInputTokens,
|
|
@@ -159,11 +261,58 @@ export function summarizeCostData(entries, { days, agent } = {}) {
|
|
|
159
261
|
cacheReadRate: cacheStats.cacheReadRate,
|
|
160
262
|
cacheHitRate: cacheStats.cacheReadRate,
|
|
161
263
|
byAgent: aggregateCostByAgent(normalized),
|
|
264
|
+
byProvider: providerRollup,
|
|
162
265
|
days: days ?? null,
|
|
163
266
|
agentFilter: agent ?? null,
|
|
164
267
|
};
|
|
165
268
|
}
|
|
166
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Estimate cache efficiency: compare expected vs actual cache usage.
|
|
272
|
+
*
|
|
273
|
+
* @param {object} data - normalized cost entry
|
|
274
|
+
* @param {number} expectedCacheableTokens - tokens we expected to cache
|
|
275
|
+
* @returns {{ efficiency: number, waste: number, hitRate: number }}
|
|
276
|
+
*/
|
|
277
|
+
export function computeCacheEfficiency(data, expectedCacheableTokens = 0) {
|
|
278
|
+
if (!data) return { efficiency: 0, waste: 0, hitRate: 0 };
|
|
279
|
+
|
|
280
|
+
const actualCacheRead = data.cacheReadInputTokens || 0;
|
|
281
|
+
const actualCacheWrite = data.cacheCreationInputTokens || 0;
|
|
282
|
+
const processedInput = data.processedInputTokens || data.inputTokens || 0;
|
|
283
|
+
|
|
284
|
+
const hitRate = processedInput > 0 ? actualCacheRead / processedInput : 0;
|
|
285
|
+
const writeRatio = expectedCacheableTokens > 0 ? actualCacheWrite / expectedCacheableTokens : 0;
|
|
286
|
+
|
|
287
|
+
// Efficiency: how close are we to expected cache usage?
|
|
288
|
+
const efficiency = expectedCacheableTokens > 0
|
|
289
|
+
? Math.min(1, actualCacheRead / expectedCacheableTokens)
|
|
290
|
+
: 0;
|
|
291
|
+
|
|
292
|
+
// Waste: expected writes minus actual reads (over-cacheing)
|
|
293
|
+
const waste = Math.max(0, actualCacheWrite - actualCacheRead);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
efficiency: Math.round(efficiency * 100) / 100,
|
|
297
|
+
waste,
|
|
298
|
+
hitRate: Math.round(hitRate * 100) / 100,
|
|
299
|
+
expectedCacheableTokens,
|
|
300
|
+
actualCacheRead,
|
|
301
|
+
actualCacheWrite,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Batch compute cache efficiency for multiple entries.
|
|
307
|
+
*
|
|
308
|
+
* @param {Array} entries - normalized cost entries
|
|
309
|
+
* @param {number} expectedCacheableTokens
|
|
310
|
+
* @returns {Array}
|
|
311
|
+
*/
|
|
312
|
+
export function computeBatchCacheEfficiency(entries, expectedCacheableTokens = 0) {
|
|
313
|
+
return (entries || []).map(e => computeCacheEfficiency(e, expectedCacheableTokens));
|
|
314
|
+
}
|
|
315
|
+
|
|
167
316
|
export function formatCostReport(data, colors = {}) {
|
|
168
317
|
const C = { bold: '', dim: '', reset: '', green: '', yellow: '', cyan: '', red: '', ...colors };
|
|
169
318
|
const lines = [];
|
|
@@ -175,7 +324,8 @@ export function formatCostReport(data, colors = {}) {
|
|
|
175
324
|
if (data.agentFilter) lines.push(`${C.dim}Agent filter: ${data.agentFilter}${C.reset}`);
|
|
176
325
|
|
|
177
326
|
lines.push(`Interactions: ${data.interactions}`);
|
|
178
|
-
lines.push(`
|
|
327
|
+
lines.push(`Provider total: ${Number(data.providerTotalTokens || 0).toLocaleString()} (${data.totalInputTokens.toLocaleString()} uncached in / ${data.totalOutputTokens.toLocaleString()} out / ${Number(data.totalReasoningTokens || 0).toLocaleString()} reasoning)`);
|
|
328
|
+
lines.push(`Billed total: ${Number(data.billedTotalTokens || 0).toLocaleString()}`);
|
|
179
329
|
lines.push(`Processed input: ${Number(data.processedInputTokens || data.totalInputTokens || 0).toLocaleString()}`);
|
|
180
330
|
|
|
181
331
|
const readPct = ((data.cacheReadRate ?? data.cacheHitRate ?? 0) * 100).toFixed(1);
|
|
@@ -187,15 +337,16 @@ export function formatCostReport(data, colors = {}) {
|
|
|
187
337
|
if (data.byAgent.length > 0) {
|
|
188
338
|
lines.push('');
|
|
189
339
|
lines.push('By Agent:');
|
|
190
|
-
lines.push(` ${'Agent'.padEnd(28)} ${'Turns'.padStart(5)} ${'
|
|
191
|
-
lines.push(` ${'-'.repeat(28)} ${'-'.repeat(5)} ${'-'.repeat(10)} ${'-'.repeat(8)} ${'-'.repeat(5)} -----`);
|
|
340
|
+
lines.push(` ${'Agent'.padEnd(28)} ${'Turns'.padStart(5)} ${'Provider'.padStart(10)} ${'Billed'.padStart(10)} ${'Cost'.padStart(8)} ${'Share'.padStart(5)} Cache`);
|
|
341
|
+
lines.push(` ${'-'.repeat(28)} ${'-'.repeat(5)} ${'-'.repeat(10)} ${'-'.repeat(10)} ${'-'.repeat(8)} ${'-'.repeat(5)} -----`);
|
|
192
342
|
for (const a of data.byAgent) {
|
|
193
343
|
const sharePct = data.totalCostUsd > 0 ? Math.round((a.costUsd / data.totalCostUsd) * 100) : 0;
|
|
194
344
|
const hitRate = Math.round((a.cacheReadRate || 0) * 100);
|
|
195
345
|
const shareColor = sharePct >= 50 ? C.red : sharePct >= 25 ? C.yellow : '';
|
|
196
346
|
lines.push(
|
|
197
347
|
` ${a.agent.padEnd(28)} ${String(a.interactions).padStart(5)}` +
|
|
198
|
-
` ${a.
|
|
348
|
+
` ${a.providerTotalTokens.toLocaleString().padStart(10)}` +
|
|
349
|
+
` ${a.billedTotalTokens.toLocaleString().padStart(10)}` +
|
|
199
350
|
` ${'$' + a.costUsd.toFixed(4).padStart(7)}` +
|
|
200
351
|
` ${shareColor}${String(sharePct + '%').padStart(5)}${C.reset}` +
|
|
201
352
|
` ${hitRate}%`,
|
|
@@ -208,6 +359,16 @@ export function formatCostReport(data, colors = {}) {
|
|
|
208
359
|
lines.push(`${C.dim}No cost data recorded yet. Token usage is logged at session end.${C.reset}`);
|
|
209
360
|
}
|
|
210
361
|
|
|
362
|
+
// Cache efficiency metrics
|
|
363
|
+
if (data.expectedCacheableTokens && data.expectedCacheableTokens > 0) {
|
|
364
|
+
const efficiency = computeCacheEfficiency(data, data.expectedCacheableTokens);
|
|
365
|
+
lines.push('');
|
|
366
|
+
lines.push('Cache Efficiency:');
|
|
367
|
+
lines.push(` Expected cacheable: ${Number(data.expectedCacheableTokens || 0).toLocaleString()} tokens`);
|
|
368
|
+
lines.push(` Actual cache reads: ${Number(data.cacheReadInputTokens || 0).toLocaleString()}`);
|
|
369
|
+
lines.push(` Efficiency: ${efficiency.efficiency * 100}% (${efficiency.waste > 0 ? `${efficiency.waste} over-cached` : 'optimal'})`);
|
|
370
|
+
}
|
|
371
|
+
|
|
211
372
|
lines.push('');
|
|
212
373
|
return lines.join('\n');
|
|
213
374
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/dashboard-static.mjs — build and sync the dashboard static bundle.
|
|
3
|
+
*
|
|
4
|
+
* Keeps `dashboard/dist/` and `lib/server/static/` aligned so the HTTP server
|
|
5
|
+
* serves the current dashboard build from repo-owned assets.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
function walkFiles(rootDir, currentDir = rootDir) {
|
|
13
|
+
if (!fs.existsSync(currentDir)) return [];
|
|
14
|
+
const entries = [];
|
|
15
|
+
for (const name of fs.readdirSync(currentDir)) {
|
|
16
|
+
const fullPath = path.join(currentDir, name);
|
|
17
|
+
const stat = fs.statSync(fullPath);
|
|
18
|
+
if (stat.isDirectory()) {
|
|
19
|
+
entries.push(...walkFiles(rootDir, fullPath));
|
|
20
|
+
} else {
|
|
21
|
+
entries.push(path.relative(rootDir, fullPath));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return entries.sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readBufferIfExists(filePath) {
|
|
28
|
+
if (!fs.existsSync(filePath)) return null;
|
|
29
|
+
return fs.readFileSync(filePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compareTrees(sourceDir, targetDir) {
|
|
33
|
+
const sourceFiles = walkFiles(sourceDir);
|
|
34
|
+
const targetFiles = walkFiles(targetDir);
|
|
35
|
+
const sourceSet = new Set(sourceFiles);
|
|
36
|
+
const targetSet = new Set(targetFiles);
|
|
37
|
+
const copied = [];
|
|
38
|
+
const removed = [];
|
|
39
|
+
|
|
40
|
+
for (const relPath of sourceFiles) {
|
|
41
|
+
const source = readBufferIfExists(path.join(sourceDir, relPath));
|
|
42
|
+
const target = readBufferIfExists(path.join(targetDir, relPath));
|
|
43
|
+
if (!target || !source.equals(target)) copied.push(relPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const relPath of targetFiles) {
|
|
47
|
+
if (!sourceSet.has(relPath)) removed.push(relPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
copied,
|
|
52
|
+
removed,
|
|
53
|
+
sourceFiles,
|
|
54
|
+
targetFiles,
|
|
55
|
+
changed: copied.length > 0 || removed.length > 0,
|
|
56
|
+
inSync: copied.length === 0 && removed.length === 0,
|
|
57
|
+
sourceExists: fs.existsSync(sourceDir),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ensureParentDir(filePath) {
|
|
62
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function removeEmptyDirs(dirPath, stopAt) {
|
|
66
|
+
let current = dirPath;
|
|
67
|
+
while (current.startsWith(stopAt) && current !== stopAt) {
|
|
68
|
+
if (!fs.existsSync(current)) {
|
|
69
|
+
current = path.dirname(current);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (fs.readdirSync(current).length > 0) break;
|
|
73
|
+
fs.rmdirSync(current);
|
|
74
|
+
current = path.dirname(current);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getDashboardPaths({ rootDir = process.cwd() } = {}) {
|
|
79
|
+
return {
|
|
80
|
+
rootDir,
|
|
81
|
+
dashboardDir: path.join(rootDir, 'dashboard'),
|
|
82
|
+
sourceDir: path.join(rootDir, 'dashboard', 'dist'),
|
|
83
|
+
targetDir: path.join(rootDir, 'lib', 'server', 'static'),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getDashboardStaticStatus({ rootDir = process.cwd() } = {}) {
|
|
88
|
+
const { sourceDir, targetDir } = getDashboardPaths({ rootDir });
|
|
89
|
+
return compareTrees(sourceDir, targetDir);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function syncDashboardStatic({ rootDir = process.cwd(), check = false } = {}) {
|
|
93
|
+
const { sourceDir, targetDir } = getDashboardPaths({ rootDir });
|
|
94
|
+
if (!fs.existsSync(sourceDir)) {
|
|
95
|
+
throw new Error(`Dashboard build output missing: ${sourceDir}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const status = compareTrees(sourceDir, targetDir);
|
|
99
|
+
if (check || !status.changed) return status;
|
|
100
|
+
|
|
101
|
+
for (const relPath of status.copied) {
|
|
102
|
+
const sourcePath = path.join(sourceDir, relPath);
|
|
103
|
+
const targetPath = path.join(targetDir, relPath);
|
|
104
|
+
ensureParentDir(targetPath);
|
|
105
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const relPath of status.removed) {
|
|
109
|
+
const targetPath = path.join(targetDir, relPath);
|
|
110
|
+
fs.rmSync(targetPath, { force: true });
|
|
111
|
+
removeEmptyDirs(path.dirname(targetPath), targetDir);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...status,
|
|
116
|
+
inSync: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildDashboardStatic({
|
|
121
|
+
rootDir = process.cwd(),
|
|
122
|
+
spawnSyncFn = spawnSync,
|
|
123
|
+
} = {}) {
|
|
124
|
+
const { dashboardDir } = getDashboardPaths({ rootDir });
|
|
125
|
+
const result = spawnSyncFn('npm', ['run', 'build'], {
|
|
126
|
+
cwd: dashboardDir,
|
|
127
|
+
encoding: 'utf8',
|
|
128
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
129
|
+
});
|
|
130
|
+
if (result.status !== 0) {
|
|
131
|
+
throw new Error((result.stderr || result.stdout || 'dashboard build failed').trim());
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function runDashboardStaticCli(args = [], { rootDir = process.cwd() } = {}) {
|
|
137
|
+
const build = args.includes('--build');
|
|
138
|
+
const check = args.includes('--check');
|
|
139
|
+
|
|
140
|
+
if (build) buildDashboardStatic({ rootDir });
|
|
141
|
+
const status = syncDashboardStatic({ rootDir, check });
|
|
142
|
+
|
|
143
|
+
if (check) {
|
|
144
|
+
if (status.changed) {
|
|
145
|
+
console.error('Dashboard static assets are out of date. Run `construct dashboard:sync --build`.');
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
console.log('Dashboard static assets are up to date.');
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (status.changed) {
|
|
153
|
+
console.log(`Dashboard static synced: ${status.copied.length} updated, ${status.removed.length} removed.`);
|
|
154
|
+
} else {
|
|
155
|
+
console.log('Dashboard static already up to date.');
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/deployment-mode.mjs — Construct deployment mode (solo | team | enterprise).
|
|
3
|
+
*
|
|
4
|
+
* Encodes the deployment posture the rest of the system reads to choose
|
|
5
|
+
* backends for the intake queue, memory, telemetry, and workers. The
|
|
6
|
+
* mode resolves with env > construct.config.json > default precedence
|
|
7
|
+
* (`getDeploymentMode(env, {cwd})`); the resource topology for each mode
|
|
8
|
+
* is derived here, not stored.
|
|
9
|
+
*/
|
|
10
|
+
import { loadProjectConfig } from './config/project-config.mjs';
|
|
11
|
+
|
|
12
|
+
export const DEPLOYMENT_MODES = ['solo', 'team', 'enterprise'];
|
|
13
|
+
export const DEFAULT_DEPLOYMENT_MODE = 'solo';
|
|
14
|
+
|
|
15
|
+
export const DEPLOYMENT_MODE_ENV_KEY = 'CONSTRUCT_DEPLOYMENT_MODE';
|
|
16
|
+
|
|
17
|
+
const RESOURCE_TOPOLOGY = {
|
|
18
|
+
solo: {
|
|
19
|
+
queue: 'filesystem',
|
|
20
|
+
memory: 'local',
|
|
21
|
+
database: 'optional',
|
|
22
|
+
telemetry: 'optional',
|
|
23
|
+
workers: 'local',
|
|
24
|
+
policy: 'lightweight',
|
|
25
|
+
mcp: 'direct',
|
|
26
|
+
},
|
|
27
|
+
team: {
|
|
28
|
+
queue: 'postgres',
|
|
29
|
+
memory: 'shared',
|
|
30
|
+
database: 'postgres',
|
|
31
|
+
telemetry: 'central',
|
|
32
|
+
workers: 'docker',
|
|
33
|
+
policy: 'server-side',
|
|
34
|
+
mcp: 'brokered',
|
|
35
|
+
},
|
|
36
|
+
enterprise: {
|
|
37
|
+
queue: 'postgres',
|
|
38
|
+
memory: 'shared',
|
|
39
|
+
database: 'postgres',
|
|
40
|
+
telemetry: 'central',
|
|
41
|
+
workers: 'isolated',
|
|
42
|
+
policy: 'enforceable',
|
|
43
|
+
mcp: 'brokered-signed',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const MODE_DESCRIPTIONS = {
|
|
48
|
+
solo: 'Individual usage. Filesystem queue, local repo state, optional Postgres/Docker/telemetry.',
|
|
49
|
+
team: 'Shared usage. Postgres queue, shared memory, Docker worker pool, central telemetry, brokered MCP.',
|
|
50
|
+
enterprise: 'Hardened usage. Adds tenant isolation, RBAC/ABAC scaffolding, isolated worker containers, signed MCP allowlists, mandatory audit on top of team.',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function isValidDeploymentMode(value) {
|
|
54
|
+
return typeof value === 'string' && DEPLOYMENT_MODES.includes(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function describeDeploymentMode(mode) {
|
|
58
|
+
return MODE_DESCRIPTIONS[mode] || '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getDeploymentMode(env = process.env, { cwd } = {}) {
|
|
62
|
+
const raw = env?.[DEPLOYMENT_MODE_ENV_KEY];
|
|
63
|
+
if (raw) {
|
|
64
|
+
const trimmed = String(raw).trim().toLowerCase();
|
|
65
|
+
return isValidDeploymentMode(trimmed) ? trimmed : DEFAULT_DEPLOYMENT_MODE;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
69
|
+
const fromConfig = config?.deployment?.mode;
|
|
70
|
+
if (typeof fromConfig === 'string' && isValidDeploymentMode(fromConfig)) return fromConfig;
|
|
71
|
+
} catch { /* loader is best-effort — fall through to default */ }
|
|
72
|
+
return DEFAULT_DEPLOYMENT_MODE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveResourceMode(mode = DEFAULT_DEPLOYMENT_MODE) {
|
|
76
|
+
const topology = RESOURCE_TOPOLOGY[mode];
|
|
77
|
+
if (!topology) {
|
|
78
|
+
throw new Error(`Unknown deployment mode: ${mode}. Valid modes: ${DEPLOYMENT_MODES.join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
return { ...topology };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function describeResourceLine(mode = DEFAULT_DEPLOYMENT_MODE) {
|
|
84
|
+
const r = resolveResourceMode(mode);
|
|
85
|
+
return `queue:${r.queue} · memory:${r.memory} · workers:${r.workers} · telemetry:${r.telemetry}`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/deprecate.mjs — single-warning-per-process deprecation infrastructure.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
*
|
|
6
|
+
* import { deprecate } from './deprecate.mjs';
|
|
7
|
+
*
|
|
8
|
+
* export function oldThing() {
|
|
9
|
+
* deprecate('oldThing()', { since: '1.2.0', removeIn: '2.0.0', use: 'newThing()' });
|
|
10
|
+
* return newThing();
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Each unique key (defaults to the symbol name) emits at most one stderr
|
|
14
|
+
* warning per process. Operators that want a strict signal can run with
|
|
15
|
+
* `CONSTRUCT_DEPRECATIONS=error` to make every deprecation fail-fast,
|
|
16
|
+
* which is useful in CI to flush out unintended deprecation usage before
|
|
17
|
+
* the next major release.
|
|
18
|
+
*
|
|
19
|
+
* The format is stable so it can be grepped from logs:
|
|
20
|
+
*
|
|
21
|
+
* [construct] deprecated: <symbol> will be removed in v<X.0.0> — use <replacement>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const SEEN = new Set();
|
|
25
|
+
|
|
26
|
+
function shouldThrow(env) {
|
|
27
|
+
return env.CONSTRUCT_DEPRECATIONS === 'error';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deprecate(symbol, opts = {}) {
|
|
31
|
+
const env = opts.env || process.env;
|
|
32
|
+
const key = opts.key || symbol;
|
|
33
|
+
if (SEEN.has(key)) return;
|
|
34
|
+
SEEN.add(key);
|
|
35
|
+
|
|
36
|
+
const since = opts.since ? ` since v${opts.since}` : '';
|
|
37
|
+
const removeIn = opts.removeIn ? ` will be removed in v${opts.removeIn}` : '';
|
|
38
|
+
const use = opts.use ? ` — use ${opts.use}` : '';
|
|
39
|
+
const message = `[construct] deprecated: ${symbol}${since}${removeIn}${use}`;
|
|
40
|
+
|
|
41
|
+
if (shouldThrow(env)) {
|
|
42
|
+
throw new Error(message);
|
|
43
|
+
}
|
|
44
|
+
process.stderr.write(message + '\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function _resetDeprecationsForTests() {
|
|
48
|
+
SEEN.clear();
|
|
49
|
+
}
|