@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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config/schema.mjs — construct.config.json v1 schema + tiny validator.
|
|
3
|
+
*
|
|
4
|
+
* Defines the project-level config shape. Hand-rolled validator instead of
|
|
5
|
+
* AJV/zod to keep Construct dependency-free at startup — the loader runs
|
|
6
|
+
* before npm install completes in some bootstrap paths.
|
|
7
|
+
*
|
|
8
|
+
* Schema v1 covers: alias, deployment, providers, autoEmbed, telemetry.
|
|
9
|
+
* Tier model selection lives in `agents/registry.json` only — this config
|
|
10
|
+
* file is intentionally not a second edit surface for model assignments.
|
|
11
|
+
* Later phases (6, 7b) extend it with `resources` and `costs` blocks
|
|
12
|
+
* without bumping the version — additive keys are allowed.
|
|
13
|
+
*
|
|
14
|
+
* Secret interpolation rule: any string starting with `$` is treated as
|
|
15
|
+
* an env-var pointer (`"$ANTHROPIC_API_KEY"` resolves at load time from
|
|
16
|
+
* process.env). Pointers keep secrets in .env where they belong.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const CONFIG_SCHEMA_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
export const DEPLOYMENT_MODES = ['solo', 'team', 'enterprise'];
|
|
22
|
+
export const MCP_BROKER_VALUES = ['auto', 'on', 'off'];
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
25
|
+
version: CONFIG_SCHEMA_VERSION,
|
|
26
|
+
alias: 'Construct',
|
|
27
|
+
deployment: Object.freeze({
|
|
28
|
+
mode: 'solo',
|
|
29
|
+
mcpBroker: 'auto',
|
|
30
|
+
projectName: null,
|
|
31
|
+
tenantId: null,
|
|
32
|
+
}),
|
|
33
|
+
providers: Object.freeze({}),
|
|
34
|
+
autoEmbed: false,
|
|
35
|
+
telemetry: Object.freeze({
|
|
36
|
+
enabled: true,
|
|
37
|
+
}),
|
|
38
|
+
roleSelection: Object.freeze({
|
|
39
|
+
primary: null,
|
|
40
|
+
secondary: null,
|
|
41
|
+
perConversationOverride: true,
|
|
42
|
+
}),
|
|
43
|
+
resources: Object.freeze({
|
|
44
|
+
disk: Object.freeze({
|
|
45
|
+
tracesMaxDays: 30,
|
|
46
|
+
intakeArchiveMaxItems: 500,
|
|
47
|
+
intakeArchiveMaxDays: 90,
|
|
48
|
+
taskGraphsMaxItems: 200,
|
|
49
|
+
taskGraphsMaxDays: 90,
|
|
50
|
+
workerLogsMaxMb: 100,
|
|
51
|
+
workerLogsMaxDays: 14,
|
|
52
|
+
sessionsMaxItems: 100,
|
|
53
|
+
backupsMaxDays: 60,
|
|
54
|
+
handoffsMaxDays: 30,
|
|
55
|
+
handoffsMaxItems: 50,
|
|
56
|
+
totalCxMaxMb: 2000,
|
|
57
|
+
}),
|
|
58
|
+
process: Object.freeze({
|
|
59
|
+
embedDaemonMaxRssMb: 800,
|
|
60
|
+
mcpServerMaxRssMb: 250,
|
|
61
|
+
workerReplicaMaxRssMb: 256,
|
|
62
|
+
}),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const FIELD_RULES = {
|
|
67
|
+
version: { type: 'number', required: true },
|
|
68
|
+
alias: { type: 'string', required: false, maxLength: 120 },
|
|
69
|
+
deployment: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
required: false,
|
|
72
|
+
fields: {
|
|
73
|
+
mode: { type: 'string', enum: DEPLOYMENT_MODES },
|
|
74
|
+
mcpBroker: { type: 'string', enum: MCP_BROKER_VALUES },
|
|
75
|
+
projectName: { type: ['string', 'null'] },
|
|
76
|
+
tenantId: { type: ['string', 'null'] },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
providers: { type: 'object', required: false },
|
|
80
|
+
autoEmbed: { type: 'boolean', required: false },
|
|
81
|
+
telemetry: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
required: false,
|
|
84
|
+
fields: {
|
|
85
|
+
enabled: { type: 'boolean' },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
roleSelection: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
required: false,
|
|
91
|
+
fields: {
|
|
92
|
+
primary: { type: ['string', 'null'], maxLength: 50 },
|
|
93
|
+
secondary: { type: ['string', 'null'], maxLength: 50 },
|
|
94
|
+
perConversationOverride: { type: 'boolean' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
resources: { type: 'object', required: false },
|
|
98
|
+
costs: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
required: false,
|
|
101
|
+
fields: {
|
|
102
|
+
billingMode: { type: 'string', enum: ['metered', 'subscription', 'mixed'] },
|
|
103
|
+
enforce: { type: 'boolean' },
|
|
104
|
+
budgets: { type: 'object' },
|
|
105
|
+
// Per-provider billing-mode overrides. The global billingMode is the
|
|
106
|
+
// fallback; entries here win for the provider id (anthropic, openai,
|
|
107
|
+
// openrouter, gemini, ollama, local, github-copilot, …). Subscription
|
|
108
|
+
// entries contribute $0 to "actual spend" headline numbers; the
|
|
109
|
+
// metered-equivalent stays in the footnote.
|
|
110
|
+
providers: { type: 'object', required: false },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function checkType(value, expected) {
|
|
116
|
+
if (Array.isArray(expected)) return expected.some((t) => checkType(value, t));
|
|
117
|
+
if (expected === 'null') return value === null;
|
|
118
|
+
if (expected === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
119
|
+
if (expected === 'array') return Array.isArray(value);
|
|
120
|
+
return typeof value === expected;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateField(value, rule, path) {
|
|
124
|
+
const errors = [];
|
|
125
|
+
if (value === undefined) {
|
|
126
|
+
if (rule.required) errors.push(`${path}: required field missing`);
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
129
|
+
if (!checkType(value, rule.type)) {
|
|
130
|
+
errors.push(`${path}: expected type ${JSON.stringify(rule.type)}, got ${value === null ? 'null' : typeof value}`);
|
|
131
|
+
return errors;
|
|
132
|
+
}
|
|
133
|
+
if (rule.enum && !rule.enum.includes(value)) {
|
|
134
|
+
errors.push(`${path}: must be one of ${JSON.stringify(rule.enum)}, got ${JSON.stringify(value)}`);
|
|
135
|
+
}
|
|
136
|
+
if (rule.maxLength && typeof value === 'string' && value.length > rule.maxLength) {
|
|
137
|
+
errors.push(`${path}: exceeds maxLength ${rule.maxLength}`);
|
|
138
|
+
}
|
|
139
|
+
if (rule.fields && checkType(value, 'object')) {
|
|
140
|
+
for (const [key, subRule] of Object.entries(rule.fields)) {
|
|
141
|
+
errors.push(...validateField(value[key], subRule, `${path}.${key}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return errors;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function validateProjectConfig(raw) {
|
|
148
|
+
const errors = [];
|
|
149
|
+
if (!checkType(raw, 'object')) {
|
|
150
|
+
return { valid: false, errors: ['root: must be an object'] };
|
|
151
|
+
}
|
|
152
|
+
for (const [key, rule] of Object.entries(FIELD_RULES)) {
|
|
153
|
+
errors.push(...validateField(raw[key], rule, key));
|
|
154
|
+
}
|
|
155
|
+
if (raw.version !== undefined && raw.version !== CONFIG_SCHEMA_VERSION) {
|
|
156
|
+
errors.push(`version: expected ${CONFIG_SCHEMA_VERSION}, got ${raw.version}`);
|
|
157
|
+
}
|
|
158
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
159
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/context-router.mjs — role-aware context routing for persona dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Reduces token waste and context leakage by giving each persona only the
|
|
5
|
+
* artifact types its role policy prefers, within a caller-supplied token
|
|
6
|
+
* budget. Routing is deterministic: same input → same output. Each kept
|
|
7
|
+
* artifact carries a `reason` string; each omitted candidate carries the
|
|
8
|
+
* reason it was dropped so debug output can explain the packet end-to-end.
|
|
9
|
+
*
|
|
10
|
+
* Per-role policies map a persona name to a prioritized list of artifact
|
|
11
|
+
* kinds (e.g. product-manager prefers user-signal → prd → research-brief).
|
|
12
|
+
* Unknown roles fall through to a DEFAULT_POLICY so the router never
|
|
13
|
+
* silently produces an empty packet.
|
|
14
|
+
*
|
|
15
|
+
* The router does not call retrieval. The caller does hybrid retrieval
|
|
16
|
+
* up front, then hands the candidate list in. Keeps this module pure and
|
|
17
|
+
* fast to test.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const APPROX_CHARS_PER_TOKEN = 4;
|
|
21
|
+
|
|
22
|
+
function approximateTokens(text) {
|
|
23
|
+
if (!text) return 0;
|
|
24
|
+
return Math.ceil(String(text).length / APPROX_CHARS_PER_TOKEN);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function artifactTokenCost(artifact) {
|
|
28
|
+
return approximateTokens(artifact.summary || '') + approximateTokens(artifact.title || '') + approximateTokens(artifact.path || '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-role context policy. `prefers` is an ordered list of artifact kinds
|
|
33
|
+
* for that role. `avoids` is an explicit reject list. `maxArtifacts` caps
|
|
34
|
+
* the count regardless of budget. Roles not listed here fall back to
|
|
35
|
+
* DEFAULT_POLICY.
|
|
36
|
+
*
|
|
37
|
+
* Artifact `kind` strings align with the existing storage taxonomy
|
|
38
|
+
* (lib/docs-routing.mjs lanes and construct_documents.kind values):
|
|
39
|
+
* user-signal, prd, prfaq, research-brief, adr, rfc, system-design,
|
|
40
|
+
* interface-contract, dep-map, target-file, test, build-command,
|
|
41
|
+
* eval-spec, success-metric, failure-case, trace, quality-score,
|
|
42
|
+
* incident, runbook, slo, log, service-dep.
|
|
43
|
+
*/
|
|
44
|
+
export const ROLE_POLICIES = {
|
|
45
|
+
'product-manager': {
|
|
46
|
+
prefers: ['user-signal', 'prd', 'prfaq', 'research-brief', 'acceptance-criteria', 'roadmap'],
|
|
47
|
+
avoids: ['runbook', 'service-dep'],
|
|
48
|
+
maxArtifacts: 8,
|
|
49
|
+
},
|
|
50
|
+
'ux-researcher': {
|
|
51
|
+
prefers: ['user-signal', 'research-brief', 'prfaq', 'prd'],
|
|
52
|
+
avoids: ['runbook', 'service-dep', 'interface-contract'],
|
|
53
|
+
maxArtifacts: 8,
|
|
54
|
+
},
|
|
55
|
+
'business-strategist': {
|
|
56
|
+
prefers: ['research-brief', 'prfaq', 'prd', 'roadmap'],
|
|
57
|
+
avoids: ['runbook', 'target-file'],
|
|
58
|
+
maxArtifacts: 8,
|
|
59
|
+
},
|
|
60
|
+
'researcher': {
|
|
61
|
+
prefers: ['research-brief', 'user-signal', 'prfaq', 'eval-spec'],
|
|
62
|
+
avoids: ['runbook'],
|
|
63
|
+
maxArtifacts: 10,
|
|
64
|
+
},
|
|
65
|
+
'rd-lead': {
|
|
66
|
+
prefers: ['research-brief', 'eval-spec', 'prfaq', 'prd', 'failure-case'],
|
|
67
|
+
avoids: ['runbook'],
|
|
68
|
+
maxArtifacts: 10,
|
|
69
|
+
},
|
|
70
|
+
'architect': {
|
|
71
|
+
prefers: ['adr', 'rfc', 'system-design', 'interface-contract', 'dep-map'],
|
|
72
|
+
avoids: ['runbook', 'user-signal'],
|
|
73
|
+
maxArtifacts: 8,
|
|
74
|
+
},
|
|
75
|
+
'ai-engineer': {
|
|
76
|
+
prefers: ['eval-spec', 'failure-case', 'trace', 'system-design', 'target-file'],
|
|
77
|
+
avoids: ['runbook'],
|
|
78
|
+
maxArtifacts: 10,
|
|
79
|
+
},
|
|
80
|
+
'engineer': {
|
|
81
|
+
prefers: ['target-file', 'test', 'interface-contract', 'build-command', 'system-design'],
|
|
82
|
+
avoids: ['prfaq', 'roadmap', 'user-signal'],
|
|
83
|
+
maxArtifacts: 12,
|
|
84
|
+
},
|
|
85
|
+
'debugger': {
|
|
86
|
+
prefers: ['target-file', 'test', 'trace', 'failure-case', 'log'],
|
|
87
|
+
avoids: ['prfaq', 'roadmap'],
|
|
88
|
+
maxArtifacts: 10,
|
|
89
|
+
},
|
|
90
|
+
'qa': {
|
|
91
|
+
prefers: ['test', 'acceptance-criteria', 'failure-case', 'target-file'],
|
|
92
|
+
avoids: ['research-brief', 'prfaq'],
|
|
93
|
+
maxArtifacts: 10,
|
|
94
|
+
},
|
|
95
|
+
'reviewer': {
|
|
96
|
+
prefers: ['target-file', 'test', 'interface-contract', 'adr'],
|
|
97
|
+
avoids: ['prfaq', 'roadmap'],
|
|
98
|
+
maxArtifacts: 10,
|
|
99
|
+
},
|
|
100
|
+
'evaluator': {
|
|
101
|
+
prefers: ['eval-spec', 'success-metric', 'failure-case', 'trace', 'quality-score'],
|
|
102
|
+
avoids: ['runbook', 'user-signal'],
|
|
103
|
+
maxArtifacts: 10,
|
|
104
|
+
},
|
|
105
|
+
'trace-reviewer': {
|
|
106
|
+
prefers: ['trace', 'failure-case', 'quality-score', 'eval-spec'],
|
|
107
|
+
avoids: ['runbook', 'roadmap'],
|
|
108
|
+
maxArtifacts: 10,
|
|
109
|
+
},
|
|
110
|
+
'security': {
|
|
111
|
+
prefers: ['adr', 'interface-contract', 'target-file', 'trace', 'failure-case'],
|
|
112
|
+
avoids: ['roadmap', 'prfaq'],
|
|
113
|
+
maxArtifacts: 10,
|
|
114
|
+
},
|
|
115
|
+
'sre': {
|
|
116
|
+
prefers: ['incident', 'runbook', 'slo', 'log', 'service-dep'],
|
|
117
|
+
avoids: ['prfaq', 'roadmap', 'research-brief'],
|
|
118
|
+
maxArtifacts: 12,
|
|
119
|
+
},
|
|
120
|
+
'platform-engineer': {
|
|
121
|
+
prefers: ['service-dep', 'runbook', 'slo', 'interface-contract', 'target-file'],
|
|
122
|
+
avoids: ['prfaq', 'roadmap'],
|
|
123
|
+
maxArtifacts: 12,
|
|
124
|
+
},
|
|
125
|
+
'operations': {
|
|
126
|
+
prefers: ['runbook', 'service-dep', 'slo', 'dep-map'],
|
|
127
|
+
avoids: ['prfaq', 'research-brief'],
|
|
128
|
+
maxArtifacts: 10,
|
|
129
|
+
},
|
|
130
|
+
'release-manager': {
|
|
131
|
+
prefers: ['acceptance-criteria', 'test', 'changelog', 'roadmap'],
|
|
132
|
+
avoids: ['research-brief'],
|
|
133
|
+
maxArtifacts: 8,
|
|
134
|
+
},
|
|
135
|
+
'docs-keeper': {
|
|
136
|
+
prefers: ['adr', 'rfc', 'prd', 'changelog'],
|
|
137
|
+
avoids: ['log', 'trace'],
|
|
138
|
+
maxArtifacts: 10,
|
|
139
|
+
},
|
|
140
|
+
'legal-compliance': {
|
|
141
|
+
prefers: ['adr', 'prfaq', 'research-brief', 'interface-contract'],
|
|
142
|
+
avoids: ['runbook', 'log'],
|
|
143
|
+
maxArtifacts: 8,
|
|
144
|
+
},
|
|
145
|
+
'designer': {
|
|
146
|
+
prefers: ['prd', 'prfaq', 'user-signal', 'research-brief'],
|
|
147
|
+
avoids: ['runbook', 'target-file'],
|
|
148
|
+
maxArtifacts: 8,
|
|
149
|
+
},
|
|
150
|
+
'accessibility': {
|
|
151
|
+
prefers: ['prd', 'failure-case', 'test', 'target-file'],
|
|
152
|
+
avoids: ['runbook', 'research-brief'],
|
|
153
|
+
maxArtifacts: 8,
|
|
154
|
+
},
|
|
155
|
+
'data-analyst': {
|
|
156
|
+
prefers: ['research-brief', 'success-metric', 'trace', 'eval-spec'],
|
|
157
|
+
avoids: ['runbook', 'roadmap'],
|
|
158
|
+
maxArtifacts: 10,
|
|
159
|
+
},
|
|
160
|
+
'data-engineer': {
|
|
161
|
+
prefers: ['interface-contract', 'system-design', 'target-file', 'dep-map'],
|
|
162
|
+
avoids: ['prfaq', 'roadmap'],
|
|
163
|
+
maxArtifacts: 10,
|
|
164
|
+
},
|
|
165
|
+
'explorer': {
|
|
166
|
+
prefers: ['research-brief', 'user-signal', 'prd', 'rfc'],
|
|
167
|
+
avoids: [],
|
|
168
|
+
maxArtifacts: 12,
|
|
169
|
+
},
|
|
170
|
+
'orchestrator': {
|
|
171
|
+
prefers: ['rfc', 'adr', 'prd', 'roadmap'],
|
|
172
|
+
avoids: [],
|
|
173
|
+
maxArtifacts: 10,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const DEFAULT_POLICY = {
|
|
178
|
+
prefers: ['rfc', 'adr', 'prd', 'target-file', 'test'],
|
|
179
|
+
avoids: [],
|
|
180
|
+
maxArtifacts: 8,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const DEFAULT_BUDGET_TOKENS = 6000;
|
|
184
|
+
|
|
185
|
+
function policyFor(role) {
|
|
186
|
+
return ROLE_POLICIES[role] || DEFAULT_POLICY;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function scoreCandidate(candidate, policy) {
|
|
190
|
+
const kind = candidate.kind || 'unknown';
|
|
191
|
+
if (policy.avoids.includes(kind)) return -1;
|
|
192
|
+
const preferIdx = policy.prefers.indexOf(kind);
|
|
193
|
+
const preferenceScore = preferIdx === -1 ? 0 : (policy.prefers.length - preferIdx) / policy.prefers.length;
|
|
194
|
+
const retrievalScore = typeof candidate.score === 'number' ? candidate.score : 0;
|
|
195
|
+
return preferenceScore * 0.7 + retrievalScore * 0.3;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function reasonForKeep(candidate, policy) {
|
|
199
|
+
const kind = candidate.kind || 'unknown';
|
|
200
|
+
const idx = policy.prefers.indexOf(kind);
|
|
201
|
+
if (idx !== -1) return `role prioritizes "${kind}" (rank ${idx + 1}/${policy.prefers.length})`;
|
|
202
|
+
return `acceptable kind "${kind}" for role`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function reasonForOmit(candidate, policy, cause) {
|
|
206
|
+
const kind = candidate.kind || 'unknown';
|
|
207
|
+
if (cause === 'avoid') return `kind "${kind}" is in this role's avoid list`;
|
|
208
|
+
if (cause === 'budget') return `exceeded token budget`;
|
|
209
|
+
if (cause === 'maxArtifacts') return `role's maxArtifacts cap (${policy.maxArtifacts}) reached`;
|
|
210
|
+
return 'no reason recorded';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildTaskSummary(request, triage) {
|
|
214
|
+
const lines = [];
|
|
215
|
+
if (request) lines.push(String(request).slice(0, 240));
|
|
216
|
+
if (triage?.intakeType && triage.intakeType !== 'unknown') {
|
|
217
|
+
lines.push(`Triage: ${triage.intakeType} / ${triage.rdStage} · owner ${triage.primaryOwner} · next ${triage.recommendedAction}`);
|
|
218
|
+
}
|
|
219
|
+
return lines.join(' — ');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Build a role-aware context packet.
|
|
224
|
+
*
|
|
225
|
+
* @param {object} opts
|
|
226
|
+
* @param {string} opts.request
|
|
227
|
+
* @param {object} [opts.triage]
|
|
228
|
+
* @param {string} opts.role
|
|
229
|
+
* @param {string} [opts.project]
|
|
230
|
+
* @param {object} [opts.budget]
|
|
231
|
+
* @param {number} [opts.budget.maxTokens]
|
|
232
|
+
* @param {Array} [opts.candidates] — pre-retrieved artifacts (path, title, kind, summary, score)
|
|
233
|
+
* @param {Array} [opts.constraints]
|
|
234
|
+
* @param {Array} [opts.priorObservations]
|
|
235
|
+
* @param {Array} [opts.verificationRequirements]
|
|
236
|
+
* @returns {{ role: string, contextPacket: object, omitted: Array, tokensUsed: number }}
|
|
237
|
+
*/
|
|
238
|
+
export function buildContextPacket({
|
|
239
|
+
request = '',
|
|
240
|
+
triage = null,
|
|
241
|
+
role = 'orchestrator',
|
|
242
|
+
project = null,
|
|
243
|
+
budget = {},
|
|
244
|
+
candidates = [],
|
|
245
|
+
constraints = [],
|
|
246
|
+
priorObservations = [],
|
|
247
|
+
verificationRequirements = [],
|
|
248
|
+
} = {}) {
|
|
249
|
+
const policy = policyFor(role);
|
|
250
|
+
const maxTokens = budget?.maxTokens ?? DEFAULT_BUDGET_TOKENS;
|
|
251
|
+
|
|
252
|
+
const partitioned = [];
|
|
253
|
+
const omitted = [];
|
|
254
|
+
|
|
255
|
+
for (const c of candidates) {
|
|
256
|
+
const score = scoreCandidate(c, policy);
|
|
257
|
+
if (score < 0) {
|
|
258
|
+
omitted.push({ artifact: c, reason: reasonForOmit(c, policy, 'avoid') });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
partitioned.push({ candidate: c, score });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
partitioned.sort((a, b) => b.score - a.score);
|
|
265
|
+
|
|
266
|
+
const taskSummary = buildTaskSummary(request, triage);
|
|
267
|
+
let tokensUsed = approximateTokens(taskSummary);
|
|
268
|
+
const relatedArtifacts = [];
|
|
269
|
+
|
|
270
|
+
for (const { candidate } of partitioned) {
|
|
271
|
+
if (relatedArtifacts.length >= policy.maxArtifacts) {
|
|
272
|
+
omitted.push({ artifact: candidate, reason: reasonForOmit(candidate, policy, 'maxArtifacts') });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const cost = artifactTokenCost(candidate);
|
|
276
|
+
if (tokensUsed + cost > maxTokens) {
|
|
277
|
+
omitted.push({ artifact: candidate, reason: reasonForOmit(candidate, policy, 'budget') });
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
tokensUsed += cost;
|
|
281
|
+
relatedArtifacts.push({
|
|
282
|
+
path: candidate.path,
|
|
283
|
+
title: candidate.title,
|
|
284
|
+
kind: candidate.kind,
|
|
285
|
+
summary: candidate.summary,
|
|
286
|
+
reason: reasonForKeep(candidate, policy),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const relevantFiles = relatedArtifacts
|
|
291
|
+
.filter((a) => a.kind === 'target-file' || a.kind === 'test')
|
|
292
|
+
.map(({ path, reason, kind }) => ({ path, kind, reason }));
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
role,
|
|
296
|
+
project,
|
|
297
|
+
contextPacket: {
|
|
298
|
+
taskSummary,
|
|
299
|
+
relevantFiles,
|
|
300
|
+
relatedArtifacts,
|
|
301
|
+
priorObservations: [...priorObservations],
|
|
302
|
+
constraints: [...constraints],
|
|
303
|
+
verificationRequirements: [...verificationRequirements],
|
|
304
|
+
},
|
|
305
|
+
omitted,
|
|
306
|
+
tokensUsed,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/cost-ledger.mjs — per-persona / per-day token-spend ledger.
|
|
3
|
+
*
|
|
4
|
+
* Tracks Claude API spend so the gateway can hard-stop persona invocations
|
|
5
|
+
* before they blow the daily budget. Separate from lib/roles/ because both
|
|
6
|
+
* the gateway and the cost watcher consume it.
|
|
7
|
+
*
|
|
8
|
+
* Storage: ~/.cx/cost-ledger.json — small JSON object keyed by `<dayKey>:<personaId>`.
|
|
9
|
+
* Rotates: entries older than 30 days are pruned on each write.
|
|
10
|
+
*
|
|
11
|
+
* Budgets are env-driven defaults; explicit `CONSTRUCT_BUDGET_<PERSONA>` wins.
|
|
12
|
+
* CONSTRUCT_BUDGET_DEFAULT — per persona, USD/day (default 1.00)
|
|
13
|
+
* CONSTRUCT_BUDGET_TOTAL — across all personas, USD/day (default 10.00)
|
|
14
|
+
* CONSTRUCT_BUDGET_<PERSONA> — specific persona override (uppercase, dashes → underscores)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { cwd as procCwd } from 'node:process';
|
|
21
|
+
|
|
22
|
+
const PRUNE_DAYS = 30;
|
|
23
|
+
const DEFAULT_PER_PERSONA = 10.0;
|
|
24
|
+
const DEFAULT_TOTAL = 50.0;
|
|
25
|
+
|
|
26
|
+
function rootDir() {
|
|
27
|
+
return process.env.CONSTRUCT_DOCTOR_ROOT || join(homedir(), '.cx');
|
|
28
|
+
}
|
|
29
|
+
function ledgerPath() {
|
|
30
|
+
return join(rootDir(), 'cost-ledger.json');
|
|
31
|
+
}
|
|
32
|
+
function ensureDir() {
|
|
33
|
+
const r = rootDir();
|
|
34
|
+
if (!existsSync(r)) mkdirSync(r, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function dayKey(ts = Date.now()) {
|
|
38
|
+
const d = new Date(ts);
|
|
39
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function load() {
|
|
43
|
+
const p = ledgerPath();
|
|
44
|
+
if (!existsSync(p)) return {};
|
|
45
|
+
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return {}; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function save(state) {
|
|
49
|
+
ensureDir();
|
|
50
|
+
const cutoff = dayKey(Date.now() - PRUNE_DAYS * 24 * 60 * 60 * 1000);
|
|
51
|
+
const trimmed = {};
|
|
52
|
+
for (const [key, value] of Object.entries(state)) {
|
|
53
|
+
const k = key.split(':')[0];
|
|
54
|
+
if (k >= cutoff) trimmed[key] = value;
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(ledgerPath(), JSON.stringify(trimmed));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function recordSpend({ personaId, tokens = 0, costUsd = 0, sessionId = '', ts = Date.now() }) {
|
|
60
|
+
if (process.env.CONSTRUCT_ROLES === 'off') return null;
|
|
61
|
+
const state = load();
|
|
62
|
+
const tsMs = typeof ts === 'string' ? Date.parse(ts) : Number(ts);
|
|
63
|
+
const bucketTs = Number.isFinite(tsMs) && tsMs > 0 ? tsMs : Date.now();
|
|
64
|
+
const key = `${dayKey(bucketTs)}:${personaId || 'unknown'}`;
|
|
65
|
+
const entry = state[key] || { tokens: 0, costUsd: 0, invocations: 0, sessions: [] };
|
|
66
|
+
entry.tokens += tokens;
|
|
67
|
+
entry.costUsd += costUsd;
|
|
68
|
+
entry.invocations += 1;
|
|
69
|
+
if (sessionId && !entry.sessions.includes(sessionId)) entry.sessions.push(sessionId);
|
|
70
|
+
state[key] = entry;
|
|
71
|
+
save(state);
|
|
72
|
+
return entry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getDailySpend({ personaId, ts = Date.now() } = {}) {
|
|
76
|
+
const state = load();
|
|
77
|
+
const key = `${dayKey(ts)}:${personaId || 'unknown'}`;
|
|
78
|
+
return state[key] || { tokens: 0, costUsd: 0, invocations: 0, sessions: [] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getTotalDailySpend({ ts = Date.now() } = {}) {
|
|
82
|
+
const state = load();
|
|
83
|
+
const dk = dayKey(ts);
|
|
84
|
+
let total = { tokens: 0, costUsd: 0, invocations: 0 };
|
|
85
|
+
for (const [key, value] of Object.entries(state)) {
|
|
86
|
+
if (key.startsWith(dk + ':')) {
|
|
87
|
+
total.tokens += value.tokens;
|
|
88
|
+
total.costUsd += value.costUsd;
|
|
89
|
+
total.invocations += value.invocations;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return total;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function envBudget(name, fallback) {
|
|
96
|
+
const v = parseFloat(process.env[name]);
|
|
97
|
+
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Reads construct.config.json.costs.budgets at lookup time. JSON wins
|
|
101
|
+
// when set; env vars are back-compat fallback. Loader is best-effort
|
|
102
|
+
// — config errors fall through to env / default so budget checks never
|
|
103
|
+
// throw on a malformed file.
|
|
104
|
+
function configBudgetValue(keyPath) {
|
|
105
|
+
try {
|
|
106
|
+
const cfgPath = join(procCwd(), 'construct.config.json');
|
|
107
|
+
if (!existsSync(cfgPath)) return null;
|
|
108
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
109
|
+
let cur = cfg;
|
|
110
|
+
for (const part of keyPath.split('.')) {
|
|
111
|
+
if (cur === null || typeof cur !== 'object') return null;
|
|
112
|
+
cur = cur[part];
|
|
113
|
+
}
|
|
114
|
+
const n = Number(cur);
|
|
115
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
116
|
+
} catch { return null; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function personaBudget(personaId) {
|
|
120
|
+
const envName = `CONSTRUCT_BUDGET_${String(personaId || '').toUpperCase().replace(/-/g, '_')}`;
|
|
121
|
+
const specific = parseFloat(process.env[envName]);
|
|
122
|
+
if (Number.isFinite(specific) && specific > 0) return specific;
|
|
123
|
+
const fromConfig = configBudgetValue(`costs.budgets.${personaId}.dailyUsd`);
|
|
124
|
+
if (fromConfig) return fromConfig;
|
|
125
|
+
const fromConfigDefault = configBudgetValue('costs.budgets.default.dailyUsd');
|
|
126
|
+
if (fromConfigDefault) return fromConfigDefault;
|
|
127
|
+
return envBudget('CONSTRUCT_BUDGET_DEFAULT', DEFAULT_PER_PERSONA);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function totalBudget() {
|
|
131
|
+
const envVal = parseFloat(process.env.CONSTRUCT_BUDGET_TOTAL);
|
|
132
|
+
if (Number.isFinite(envVal) && envVal > 0) return envVal;
|
|
133
|
+
const fromConfig = configBudgetValue('costs.budgets.total.dailyUsd');
|
|
134
|
+
if (fromConfig) return fromConfig;
|
|
135
|
+
return DEFAULT_TOTAL;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function enforcementActive() {
|
|
139
|
+
if (process.env.CONSTRUCT_BUDGET_ENFORCE === 'on') return true;
|
|
140
|
+
if (process.env.CONSTRUCT_BUDGET_ENFORCE === 'off') return false;
|
|
141
|
+
try {
|
|
142
|
+
const cfgPath = join(procCwd(), 'construct.config.json');
|
|
143
|
+
if (!existsSync(cfgPath)) return false;
|
|
144
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
145
|
+
return Boolean(cfg?.costs?.enforce);
|
|
146
|
+
} catch { return false; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function checkBudget({ personaId }) {
|
|
150
|
+
// Default to advisory-only. Hard-stop when CONSTRUCT_BUDGET_ENFORCE=on or
|
|
151
|
+
// construct.config.json.costs.enforce=true. Ledger still records and
|
|
152
|
+
// reports spend in both modes so over-budget shows up in the doctor
|
|
153
|
+
// report regardless.
|
|
154
|
+
if (!enforcementActive()) {
|
|
155
|
+
return { allowed: true, reason: 'enforcement-advisory' };
|
|
156
|
+
}
|
|
157
|
+
const personaSpend = getDailySpend({ personaId });
|
|
158
|
+
const personaCap = personaBudget(personaId);
|
|
159
|
+
if (personaSpend.costUsd >= personaCap) {
|
|
160
|
+
return { allowed: false, reason: 'persona-budget-exhausted', spent: personaSpend.costUsd, cap: personaCap };
|
|
161
|
+
}
|
|
162
|
+
const total = getTotalDailySpend();
|
|
163
|
+
const totalCap = totalBudget();
|
|
164
|
+
if (total.costUsd >= totalCap) {
|
|
165
|
+
return { allowed: false, reason: 'total-budget-exhausted', spent: total.costUsd, cap: totalCap };
|
|
166
|
+
}
|
|
167
|
+
const warnAt = 0.8;
|
|
168
|
+
let warning = null;
|
|
169
|
+
if (personaSpend.costUsd / personaCap >= warnAt) {
|
|
170
|
+
warning = `persona ${personaId} at ${Math.round((personaSpend.costUsd / personaCap) * 100)}% of $${personaCap.toFixed(2)} daily cap`;
|
|
171
|
+
} else if (total.costUsd / totalCap >= warnAt) {
|
|
172
|
+
warning = `total spend at ${Math.round((total.costUsd / totalCap) * 100)}% of $${totalCap.toFixed(2)} daily cap`;
|
|
173
|
+
}
|
|
174
|
+
return { allowed: true, reason: 'within-budget', warning };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const _paths = { rootDir, ledgerPath };
|