@geraldmaron/construct 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
package/bin/construct
CHANGED
|
@@ -14,19 +14,55 @@ import { spawnSync } from 'node:child_process';
|
|
|
14
14
|
|
|
15
15
|
import { CLI_COMMANDS, CLI_COMMANDS_BY_CATEGORY, CATEGORY_ORDER } from '../lib/cli-commands.mjs';
|
|
16
16
|
import { buildStatus, formatStatusReport } from '../lib/status.mjs';
|
|
17
|
-
import { runWorkflowCli, loadWorkflow, validateWorkflowState } from '../lib/workflow-state.mjs';
|
|
18
17
|
import { validateRegistry } from '../lib/validator.mjs';
|
|
19
|
-
import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig,
|
|
18
|
+
import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig, applyToEnv, resetEnv, setTierModel, setModelWithTierInference } from '../lib/model-router.mjs';
|
|
19
|
+
import { pollFreeModels, topForTier, selectForTier } from '../lib/model-free-selector.mjs';
|
|
20
20
|
import { cmdMcpList, cmdMcpAdd, cmdMcpRemove, cmdMcpInfo } from '../lib/mcp-manager.mjs';
|
|
21
|
+
import { cmdOllama } from '../lib/ollama-manager.mjs';
|
|
21
22
|
import { printHostCapabilities } from '../lib/host-capabilities.mjs';
|
|
23
|
+
import { getPluginById, initPluginManifest, loadPluginRegistry } from '../lib/plugin-registry.mjs';
|
|
22
24
|
import { runSetup } from '../lib/setup.mjs';
|
|
23
|
-
import {
|
|
25
|
+
import { runUpdate } from '../lib/update.mjs';
|
|
26
|
+
import { runUpgrade } from '../lib/upgrade.mjs';
|
|
27
|
+
import { runUninstall } from '../lib/uninstall/uninstall.mjs';
|
|
28
|
+
import { maybeFirstInvocationProbe } from '../lib/install/first-invocation.mjs';
|
|
29
|
+
import { loadConstructEnv, getUserEnvPath, writeEnvValues, ensureUserConfigDir } from '../lib/env-config.mjs';
|
|
30
|
+
import {
|
|
31
|
+
loadProjectConfig,
|
|
32
|
+
writeProjectConfig,
|
|
33
|
+
initProjectConfig,
|
|
34
|
+
findProjectConfigPath,
|
|
35
|
+
getConfigValue,
|
|
36
|
+
setConfigValue,
|
|
37
|
+
PROJECT_CONFIG_FILENAME,
|
|
38
|
+
} from '../lib/config/project-config.mjs';
|
|
39
|
+
import { validateProjectConfig } from '../lib/config/schema.mjs';
|
|
40
|
+
import {
|
|
41
|
+
DEPLOYMENT_MODES,
|
|
42
|
+
DEFAULT_DEPLOYMENT_MODE,
|
|
43
|
+
DEPLOYMENT_MODE_ENV_KEY,
|
|
44
|
+
describeDeploymentMode,
|
|
45
|
+
describeResourceLine,
|
|
46
|
+
getDeploymentMode,
|
|
47
|
+
isValidDeploymentMode,
|
|
48
|
+
resolveResourceMode,
|
|
49
|
+
} from '../lib/deployment-mode.mjs';
|
|
24
50
|
import { runDistillCli } from '../lib/distill.mjs';
|
|
25
|
-
import {
|
|
51
|
+
import { runIngestCli } from '../lib/document-ingest.mjs';
|
|
52
|
+
import { runInferCli } from '../lib/schema-infer.mjs';
|
|
53
|
+
import { COMPLETIONS_DIR, generateCompletions, getCompletionScript } from '../lib/completions.mjs';
|
|
26
54
|
import { runHeadhunt, listTeamTemplates } from '../lib/headhunt.mjs';
|
|
55
|
+
import {
|
|
56
|
+
getLockStatus,
|
|
57
|
+
cleanupStaleLock,
|
|
58
|
+
cleanupStaleQueue,
|
|
59
|
+
readQueue,
|
|
60
|
+
formatStatus,
|
|
61
|
+
} from '../lib/beads-lock.mjs';
|
|
62
|
+
import { runBd, getHumanStatus, acquireMergeSlot, releaseMergeSlot } from '../lib/beads-client.mjs';
|
|
27
63
|
import { auditSkills, runAuditSkillsCli } from '../lib/audit-skills.mjs';
|
|
28
64
|
import { runTeamReviewCli } from '../lib/telemetry/team-rollup.mjs';
|
|
29
|
-
import { readDashboardState, startDashboard, startServices
|
|
65
|
+
import { readDashboardState, startDashboard, startServices } from '../lib/service-manager.mjs';
|
|
30
66
|
import { readCostLog, summarizeCostData, formatCostReport, clearCostLog } from '../lib/cost.mjs';
|
|
31
67
|
import { readEfficiencyLog, summarizeEfficiencyData, formatEfficiencyReport } from '../lib/efficiency.mjs';
|
|
32
68
|
import { buildHybridSearchResultsAsync } from '../lib/storage/hybrid-query.mjs';
|
|
@@ -34,6 +70,8 @@ import { createSqlClient, closeSqlClient, readVectorConfig } from '../lib/storag
|
|
|
34
70
|
import { describeVectorStore } from '../lib/storage/vector-store.mjs';
|
|
35
71
|
import { describeSqlStore } from '../lib/storage/sql-store.mjs';
|
|
36
72
|
import { syncFileStateToSql } from '../lib/storage/sync.mjs';
|
|
73
|
+
import { deleteIngestedArtifacts, getStorageStatus, inferProjectName, resetStorage } from '../lib/storage/admin.mjs';
|
|
74
|
+
import { runPressureRelease } from '../lib/runtime-pressure.mjs';
|
|
37
75
|
|
|
38
76
|
const ROOT_DIR = path.resolve(import.meta.dirname, '..');
|
|
39
77
|
const HOME = os.homedir();
|
|
@@ -55,16 +93,84 @@ function ok(message) { println(` ${COLORS.green}✓${COLORS.reset} ${message}`
|
|
|
55
93
|
function warn(message) { println(` ${COLORS.yellow}⚠${COLORS.reset} ${message}`); }
|
|
56
94
|
function info(message) { println(` ${COLORS.cyan}→${COLORS.reset} ${message}`); }
|
|
57
95
|
|
|
96
|
+
// VS16-suffixed emojis (e.g. ⌨️, 🛠️, ⬆️) come from BMP "text symbol" Unicode
|
|
97
|
+
// blocks and are commonly rendered as 1 visual column in macOS Terminal /
|
|
98
|
+
// iTerm2 despite the U+FE0F variation-selector intent. Pad them with an
|
|
99
|
+
// extra trailing space so the command-name column stays aligned regardless
|
|
100
|
+
// of which icon a CLI entry uses.
|
|
101
|
+
|
|
102
|
+
function iconColumn(emoji) {
|
|
103
|
+
const trailingSpaces = emoji.includes('️') ? ' ' : ' ';
|
|
104
|
+
return emoji + trailingSpaces;
|
|
105
|
+
}
|
|
106
|
+
|
|
58
107
|
function usage() {
|
|
59
|
-
|
|
108
|
+
const showAll = restArgsCache.includes('--all') || restArgsCache.includes('-a');
|
|
109
|
+
const commands = showAll ? CLI_COMMANDS : CLI_COMMANDS.filter(c => c.core);
|
|
110
|
+
|
|
111
|
+
if (showAll) {
|
|
112
|
+
println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness (all commands)`);
|
|
113
|
+
} else {
|
|
114
|
+
println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness`);
|
|
115
|
+
println(`${COLORS.dim}Tip: Run ${COLORS.reset}construct --all${COLORS.dim} to see all commands${COLORS.reset}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
60
118
|
println(`\n${COLORS.dim}Usage:${COLORS.reset} construct <command> [options]\n`);
|
|
119
|
+
|
|
61
120
|
for (const category of CATEGORY_ORDER) {
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
121
|
+
const categoryCommands = commands.filter(c => c.category === category);
|
|
122
|
+
if (!categoryCommands.length) continue;
|
|
64
123
|
println(`${COLORS.bold}${category}${COLORS.reset}`);
|
|
65
|
-
for (const command of
|
|
66
|
-
println(` ${command.emoji}
|
|
124
|
+
for (const command of categoryCommands) {
|
|
125
|
+
println(` ${iconColumn(command.emoji)}${command.name.padEnd(14)} ${command.description}`);
|
|
126
|
+
}
|
|
127
|
+
println('');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Show interactive context-aware menu when construct is run without arguments. */
|
|
132
|
+
async function showInteractiveMenu() {
|
|
133
|
+
const { loadProjectConfig } = await import('./lib/config/project-config.mjs');
|
|
134
|
+
const { existsSync } = await import('node:fs');
|
|
135
|
+
const { join } = await import('node:path');
|
|
136
|
+
|
|
137
|
+
const projectRoot = process.cwd();
|
|
138
|
+
const isConstructProject = existsSync(join(projectRoot, 'construct.config.json')) ||
|
|
139
|
+
existsSync(join(projectRoot, '.cx'));
|
|
140
|
+
|
|
141
|
+
println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness`);
|
|
142
|
+
println('');
|
|
143
|
+
|
|
144
|
+
if (isConstructProject) {
|
|
145
|
+
const projectName = process.env.CX_PROJECT_NAME || require('path').basename(projectRoot);
|
|
146
|
+
println(`${COLORS.dim}Project:${COLORS.reset} ${projectName}`);
|
|
147
|
+
println('');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
println(`${COLORS.bold}Core Commands:${COLORS.reset}`);
|
|
151
|
+
println(` ${COLORS.green}🚀 dev${COLORS.reset} Start services for development`);
|
|
152
|
+
println(` ${COLORS.green}⏹ stop${COLORS.reset} Stop all running services`);
|
|
153
|
+
println(` ${COLORS.green}📡 status${COLORS.reset} Show system health and credentials`);
|
|
154
|
+
println(` ${COLORS.green}🏗️ init${COLORS.reset} Initialize project and start services`);
|
|
155
|
+
println(` ${COLORS.green}🔄 sync${COLORS.reset} Sync agent adapters to AI tools`);
|
|
156
|
+
println('');
|
|
157
|
+
println(`${COLORS.dim}Run 'construct <command> --help' for command details${COLORS.reset}`);
|
|
158
|
+
println(`${COLORS.dim}Run 'construct --all' to see all commands${COLORS.reset}`);
|
|
159
|
+
println('');
|
|
160
|
+
|
|
161
|
+
// Show context-aware suggestions
|
|
162
|
+
if (isConstructProject) {
|
|
163
|
+
const { readDashboardState } = await import('./lib/service-manager.mjs');
|
|
164
|
+
const dashboard = readDashboardState(HOME);
|
|
165
|
+
|
|
166
|
+
if (!dashboard) {
|
|
167
|
+
println(`${COLORS.yellow}💡 Services not running. Start with:${COLORS.reset}`);
|
|
168
|
+
println(` ${COLORS.green}construct dev${COLORS.reset}`);
|
|
169
|
+
println('');
|
|
67
170
|
}
|
|
171
|
+
} else {
|
|
172
|
+
println(`${COLORS.yellow}💡 Not a Construct project. Initialize with:${COLORS.reset}`);
|
|
173
|
+
println(` ${COLORS.green}construct init${COLORS.reset}`);
|
|
68
174
|
println('');
|
|
69
175
|
}
|
|
70
176
|
}
|
|
@@ -80,11 +186,14 @@ function runNodeScript(scriptPath, args = [], extraEnv = {}) {
|
|
|
80
186
|
async function cmdStatus() {
|
|
81
187
|
const jsonOutput = restArgsCache.includes('--json');
|
|
82
188
|
const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
|
|
189
|
+
|
|
83
190
|
if (jsonOutput) {
|
|
84
191
|
println(JSON.stringify(status, null, 2));
|
|
85
192
|
return;
|
|
86
193
|
}
|
|
194
|
+
|
|
87
195
|
println(formatStatusReport(status));
|
|
196
|
+
|
|
88
197
|
}
|
|
89
198
|
|
|
90
199
|
async function cmdShow() {
|
|
@@ -105,7 +214,31 @@ async function cmdShow() {
|
|
|
105
214
|
|
|
106
215
|
async function cmdSync(args) {
|
|
107
216
|
generateCompletions();
|
|
108
|
-
runNodeScript(path.join(ROOT_DIR, 'sync-agents.mjs'), args);
|
|
217
|
+
runNodeScript(path.join(ROOT_DIR, 'scripts', 'sync-agents.mjs'), args);
|
|
218
|
+
if (args.includes('--no-docs')) return;
|
|
219
|
+
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
220
|
+
const { changed } = await regenerateDocs({ rootDir: ROOT_DIR });
|
|
221
|
+
for (const f of changed) ok(`Updated ${path.relative(ROOT_DIR, f)}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function cmdCompletions(args) {
|
|
225
|
+
const sub = args[0] ?? 'install';
|
|
226
|
+
if (sub === 'bash' || sub === 'zsh') {
|
|
227
|
+
println(getCompletionScript(sub));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (sub === 'install') {
|
|
231
|
+
const outDir = generateCompletions();
|
|
232
|
+
if (!outDir) process.exit(1);
|
|
233
|
+
println(`Completions written to ${outDir}`);
|
|
234
|
+
println('');
|
|
235
|
+
println('Enable them in your shell:');
|
|
236
|
+
println(` bash: source ${path.join(outDir, 'construct.bash')}`);
|
|
237
|
+
println(` zsh: fpath=(${outDir} $fpath) && autoload -Uz compinit && compinit`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
errorln('Usage: construct completions [bash|zsh|install]');
|
|
241
|
+
process.exit(1);
|
|
109
242
|
}
|
|
110
243
|
|
|
111
244
|
async function cmdList() {
|
|
@@ -122,10 +255,159 @@ async function cmdList() {
|
|
|
122
255
|
}
|
|
123
256
|
|
|
124
257
|
async function cmdDoctor() {
|
|
258
|
+
const args = process.argv.slice(3);
|
|
259
|
+
|
|
260
|
+
const DAEMON_SUBS = new Set(['status', 'watch', 'stop', 'logs', 'tick', 'report']);
|
|
261
|
+
if (args.length > 0 && DAEMON_SUBS.has(args[0])) {
|
|
262
|
+
const { runCli } = await import('../lib/doctor/cli.mjs');
|
|
263
|
+
const code = await runCli(args);
|
|
264
|
+
if (code) process.exit(code);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (args[0] === 'credentials' || (args.length && args.some(a => a.startsWith('cred')))) {
|
|
269
|
+
println(`${COLORS.bold}Credential diagnostics${COLORS.reset}`);
|
|
270
|
+
println('');
|
|
271
|
+
|
|
272
|
+
const { execSync } = await import('node:child_process');
|
|
273
|
+
const { homedir } = await import('node:os');
|
|
274
|
+
const { join } = await import('node:path');
|
|
275
|
+
const fs = await import('node:fs');
|
|
276
|
+
|
|
277
|
+
const vars = [
|
|
278
|
+
'OPENROUTER_API_KEY',
|
|
279
|
+
'ANTHROPIC_API_KEY',
|
|
280
|
+
'GITHUB_TOKEN',
|
|
281
|
+
'OPENAI_API_KEY',
|
|
282
|
+
'OPENCODE_API_KEY',
|
|
283
|
+
'JIRA_API_TOKEN',
|
|
284
|
+
'CONFLUENCE_API_TOKEN',
|
|
285
|
+
'SLACK_BOT_TOKEN',
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
for (const varName of vars) {
|
|
289
|
+
const sources = [];
|
|
290
|
+
|
|
291
|
+
// 1. process.env
|
|
292
|
+
if (process.env[varName]) {
|
|
293
|
+
sources.push(`process.env (${process.env[varName].slice(0, 8)}…)`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 2. .env files
|
|
297
|
+
for (const envPath of [join(process.cwd(), '.env'), join(homedir(), '.env'), join(homedir(), '.construct', 'config.env')]) {
|
|
298
|
+
if (fs.existsSync(envPath)) {
|
|
299
|
+
try {
|
|
300
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
301
|
+
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
302
|
+
if (m) sources.push(`${envPath.replace(homedir(), '~')} (found)`);
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 3. Shell rc files
|
|
308
|
+
const shellFiles = [join(homedir(), '.zshrc'), join(homedir(), '.bashrc'), join(homedir(), '.bash_profile'), join(homedir(), '.profile')];
|
|
309
|
+
for (const rcPath of shellFiles) {
|
|
310
|
+
if (!fs.existsSync(rcPath)) continue;
|
|
311
|
+
try {
|
|
312
|
+
const content = fs.readFileSync(rcPath, 'utf8');
|
|
313
|
+
const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
|
|
314
|
+
const opMatch = content.match(opRe);
|
|
315
|
+
if (opMatch) {
|
|
316
|
+
try {
|
|
317
|
+
const r = execSync(`op read '${opMatch[1]}'`, { encoding: 'utf8', timeout: 5000 });
|
|
318
|
+
sources.push(`${rcPath.replace(homedir(), '~')} → op read ✓ (${r.stdout?.trim()?.slice(0, 8)}…)`);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
const err = (e.stderr || e.message || '').slice(0, 120);
|
|
321
|
+
sources.push(`${rcPath.replace(homedir(), '~')} → op read ✗: ${err}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Check for direct export (allow leading whitespace for indented rc files)
|
|
325
|
+
const directRe = new RegExp(`^\\s*export\\s+${varName}=(.+)$`, 'm');
|
|
326
|
+
const directMatch = content.match(directRe);
|
|
327
|
+
if (directMatch) {
|
|
328
|
+
sources.push(`${rcPath.replace(homedir(), '~')} (export found)`);
|
|
329
|
+
}
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const pip = sources.length > 0 ? COLORS.green + '✓' : COLORS.yellow + '○';
|
|
334
|
+
println(` ${pip}${COLORS.reset} ${COLORS.bold}${varName}${COLORS.reset}`);
|
|
335
|
+
if (sources.length) {
|
|
336
|
+
for (const s of sources.slice(0, 3)) {
|
|
337
|
+
if (s.includes('op read ✗')) {
|
|
338
|
+
println(` ${COLORS.red}⚠${COLORS.reset} ${s}`);
|
|
339
|
+
} else {
|
|
340
|
+
println(` ${s}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (sources.length > 3) println(` … +${sources.length - 3} more`);
|
|
344
|
+
} else {
|
|
345
|
+
println(` not found in env, .env, config.env, or shell rc files`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
println('');
|
|
350
|
+
println('Fix tips:');
|
|
351
|
+
println(' - If op read fails with "does not have a field": the field name in .zshrc');
|
|
352
|
+
println(' doesn\'t match the 1Password item. Run `op item get <item> --vault <vault>`');
|
|
353
|
+
println(' to see the correct field name, then update your .zshrc.');
|
|
354
|
+
println(' - If op read fails with "not signed in": run `op signin` or enable the');
|
|
355
|
+
println(' 1Password desktop app CLI integration (Settings → Developer).');
|
|
356
|
+
println(' - Set env vars directly in ~/.construct/config.env to bypass shell rc.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (args.includes('--bootstrap')) {
|
|
361
|
+
const { registerBuiltInResources } = await import('../lib/bootstrap/built-ins.mjs');
|
|
362
|
+
const { probeAll, formatProbe } = await import('../lib/bootstrap/resources.mjs');
|
|
363
|
+
registerBuiltInResources();
|
|
364
|
+
const probes = await probeAll();
|
|
365
|
+
println('Bootstrap resource probe');
|
|
366
|
+
println('═══════════════════════');
|
|
367
|
+
for (const probe of probes) println(formatProbe(probe));
|
|
368
|
+
const missingRequired = probes.filter((p) => p.required && !p.present);
|
|
369
|
+
const missingOptional = probes.filter((p) => !p.required && !p.present);
|
|
370
|
+
println('');
|
|
371
|
+
if (missingRequired.length) {
|
|
372
|
+
errorln(`${missingRequired.length} required resource(s) missing — install them before continuing.`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
if (missingOptional.length) {
|
|
376
|
+
println(`${missingOptional.length} optional resource(s) absent — Construct runs in degraded mode.`);
|
|
377
|
+
println('Run `construct setup` to install on consent.');
|
|
378
|
+
} else {
|
|
379
|
+
ok('All resources present.');
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
125
384
|
const checks = [];
|
|
126
385
|
const add = (label, pass, optional = false) => checks.push({ label, pass, optional });
|
|
127
386
|
add('registry.json exists', fs.existsSync(path.join(ROOT_DIR, 'agents', 'registry.json')));
|
|
128
|
-
add('sync-agents.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'sync-agents.mjs')));
|
|
387
|
+
add('sync-agents.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-agents.mjs')));
|
|
388
|
+
|
|
389
|
+
// Tier model selection. Construct ships with no default — at least
|
|
390
|
+
// one tier must be configured (registry.json primary OR CX_MODEL_*
|
|
391
|
+
// env override) before any LLM-backed workflow can run.
|
|
392
|
+
// Skip in CI — model tiers are user-specific, not repo-wide.
|
|
393
|
+
const inCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
394
|
+
try {
|
|
395
|
+
const { resolveModelTiers } = await import('../lib/model-registry.mjs');
|
|
396
|
+
const resolved = resolveModelTiers({ env: process.env });
|
|
397
|
+
if (resolved.complete && !resolved.errors) {
|
|
398
|
+
add('Models — all tiers configured', true);
|
|
399
|
+
} else if (resolved.configured === 0) {
|
|
400
|
+
add('Models — no tier configured (pick one in the dashboard or run `construct models --apply`)', inCI, inCI);
|
|
401
|
+
} else {
|
|
402
|
+
const missing = Object.entries(resolved.models)
|
|
403
|
+
.filter(([, v]) => !v)
|
|
404
|
+
.map(([k]) => k);
|
|
405
|
+
add(`Models — only ${resolved.configured}/3 tiers configured (missing: ${missing.join(', ')})`, inCI, inCI);
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
add(`Models — check failed: ${err.message}`, inCI, inCI);
|
|
409
|
+
}
|
|
410
|
+
|
|
129
411
|
add('Node.js 20+ (recommended)', Number.parseInt(process.versions.node.split('.')[0], 10) >= 20);
|
|
130
412
|
add('npm available', true);
|
|
131
413
|
const constructOnPath = spawnSync('zsh', ['-lc', 'command -v construct'], { encoding: 'utf8', env: process.env });
|
|
@@ -147,6 +429,132 @@ async function cmdDoctor() {
|
|
|
147
429
|
fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
|
|
148
430
|
add('~/.cx/performance-reviews/ ready', true);
|
|
149
431
|
|
|
432
|
+
// construct.config.json — optional today (config is permitted to be absent;
|
|
433
|
+
// env vars + defaults still work). The check enforces that when the file
|
|
434
|
+
// *is* present it parses and validates against the v1 schema.
|
|
435
|
+
try {
|
|
436
|
+
const projectCfg = loadProjectConfig(process.cwd(), process.env);
|
|
437
|
+
if (projectCfg.source === 'invalid') {
|
|
438
|
+
add(`construct.config.json valid (${projectCfg.errors.length} error${projectCfg.errors.length === 1 ? '' : 's'} — run \`construct config validate\`)`, false, false);
|
|
439
|
+
} else if (projectCfg.source === 'file') {
|
|
440
|
+
add(`construct.config.json valid`, true, true);
|
|
441
|
+
} else {
|
|
442
|
+
add('construct.config.json (optional)', true, true);
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
add(`construct.config.json check failed: ${err.message}`, false, true);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Claude Code Stop-hook installation check. The settings template
|
|
449
|
+
// wires several Stop hooks (policy-engine / stop-typecheck /
|
|
450
|
+
// stop-notify / readme-age-check) that reference
|
|
451
|
+
// $HOME/.construct/lib/hooks/*. If that path doesn't resolve, every
|
|
452
|
+
// Stop hook fails silently — including stop-notify, which is what
|
|
453
|
+
// appends session cost data. Result: the dashboard stops updating
|
|
454
|
+
// and the user sees stale data. Surface loudly so the fix is one
|
|
455
|
+
// command away.
|
|
456
|
+
try {
|
|
457
|
+
const settingsPath = path.join(HOME, '.claude', 'settings.json');
|
|
458
|
+
if (fs.existsSync(settingsPath)) {
|
|
459
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
460
|
+
const stopHooks = settings?.hooks?.Stop || [];
|
|
461
|
+
const installRoot = path.join(HOME, '.construct', 'lib', 'hooks');
|
|
462
|
+
const installed = fs.existsSync(installRoot);
|
|
463
|
+
const referenced = [];
|
|
464
|
+
for (const group of stopHooks) {
|
|
465
|
+
for (const h of (group?.hooks || [])) {
|
|
466
|
+
const cmd = String(h?.command || '');
|
|
467
|
+
const m = cmd.match(/\.construct\/lib\/hooks\/([a-zA-Z0-9_-]+\.mjs)/);
|
|
468
|
+
if (m) referenced.push(m[1]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const missing = installed
|
|
472
|
+
? referenced.filter((f) => !fs.existsSync(path.join(installRoot, f)))
|
|
473
|
+
: referenced;
|
|
474
|
+
if (referenced.length > 0 && missing.length > 0) {
|
|
475
|
+
add(
|
|
476
|
+
`Claude Stop hooks broken: ${missing.length} hook${missing.length === 1 ? '' : 's'} missing in ~/.construct/lib/hooks/. Symlink the install: ln -sfn ${ROOT_DIR}/lib ~/.construct/lib`,
|
|
477
|
+
false,
|
|
478
|
+
false,
|
|
479
|
+
);
|
|
480
|
+
} else if (referenced.length > 0) {
|
|
481
|
+
add(`Claude Stop hooks installed (${referenced.length} hooks resolve)`, true, true);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
add(`Claude Stop hooks check failed: ${err.message}`, false, true);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Handoff hygiene. Files in .cx/handoffs/ accumulate per session; this
|
|
489
|
+
// probe surfaces stale files past retention and files whose bead refs
|
|
490
|
+
// have all closed (safe to prune). Advisory — graceful when no .cx/
|
|
491
|
+
// handoffs/ directory exists.
|
|
492
|
+
try {
|
|
493
|
+
const { summarizeHandoffs } = await import('../lib/handoffs/inventory.mjs');
|
|
494
|
+
const h = summarizeHandoffs(process.cwd(), process.env);
|
|
495
|
+
if (h.state === 'empty') {
|
|
496
|
+
add('Handoffs: empty', true, true);
|
|
497
|
+
} else if (h.pastRetentionCount > 0) {
|
|
498
|
+
add(`Handoffs: ${h.pastRetentionCount} past ${h.maxDays}d retention — \`construct prune\``, false, true);
|
|
499
|
+
} else if (h.resolvedCount > 0) {
|
|
500
|
+
add(`Handoffs: ${h.total} files (${h.resolvedCount} reference only closed beads)`, true, true);
|
|
501
|
+
} else {
|
|
502
|
+
add(`Handoffs: ${h.total} files (oldest ${h.oldestAgeDays?.toFixed(0)}d)`, true, true);
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
add(`Handoff check failed: ${err.message}`, false, true);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Disk resource budgets. Construct writes .cx/traces/, .cx/intake/,
|
|
509
|
+
// task graphs, backups, worker logs continuously. Without ceilings a
|
|
510
|
+
// long-lived install can fill the user's disk. Doctor surfaces usage
|
|
511
|
+
// vs cap as advisory at <=80%, warn at 80-100%, fail at >100%.
|
|
512
|
+
try {
|
|
513
|
+
const { measureUsage } = await import('../lib/resources/budget.mjs');
|
|
514
|
+
const u = measureUsage(process.cwd(), process.env);
|
|
515
|
+
const usedMb = u.totalCxBytes / 1024 / 1024;
|
|
516
|
+
const capMb = u.totalCxCap / 1024 / 1024;
|
|
517
|
+
const pct = (u.totalCxUsageRatio * 100).toFixed(0);
|
|
518
|
+
if (u.totalCxUsageRatio > 1) {
|
|
519
|
+
add(`.cx/ ${usedMb.toFixed(0)}MB / ${capMb.toFixed(0)}MB cap (${pct}% — over) — run \`construct prune\``, false, false);
|
|
520
|
+
} else if (u.totalCxUsageRatio > 0.8) {
|
|
521
|
+
add(`.cx/ ${usedMb.toFixed(1)}MB / ${capMb.toFixed(0)}MB cap (${pct}% — warning) — \`construct prune\` recommended`, false, true);
|
|
522
|
+
} else {
|
|
523
|
+
add(`.cx/ ${usedMb.toFixed(1)}MB / ${capMb.toFixed(0)}MB cap (${pct}%)`, true, true);
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
add(`Resource budget check failed: ${err.message}`, false, true);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Pricing-catalog freshness + divergence. Cost ceilings (Phase 7b)
|
|
530
|
+
// are only as trustworthy as the catalog underneath. Stale catalog →
|
|
531
|
+
// drift between computed cost and actual provider bill. Divergence
|
|
532
|
+
// (>25%) → LiteLLM and static disagree on a known model, surface so
|
|
533
|
+
// the operator can verify which is current. Both advisory: degraded
|
|
534
|
+
// pricing still falls back to a usable source.
|
|
535
|
+
try {
|
|
536
|
+
const { describePricingCatalogFreshness, checkPricingDivergence } = await import('../lib/telemetry/model-pricing-catalog.mjs');
|
|
537
|
+
const freshness = describePricingCatalogFreshness();
|
|
538
|
+
if (freshness.stale) {
|
|
539
|
+
add(`Pricing catalog: ${freshness.message}`, false, true);
|
|
540
|
+
} else {
|
|
541
|
+
add(`Pricing catalog fresh (${freshness.ageHours?.toFixed(1) || '0.0'}h old)`, true, true);
|
|
542
|
+
}
|
|
543
|
+
if (freshness.present) {
|
|
544
|
+
try {
|
|
545
|
+
const cached = JSON.parse(fs.readFileSync(path.join(HOME, '.cx', 'pricing-cache.json'), 'utf8'));
|
|
546
|
+
const divergences = checkPricingDivergence(cached.models || []);
|
|
547
|
+
if (divergences.length === 0) {
|
|
548
|
+
add('Pricing matches static fallback (no >25% divergence)', true, true);
|
|
549
|
+
} else {
|
|
550
|
+
add(`Pricing divergence on ${divergences.length} model(s) — run \`construct pricing check\``, false, true);
|
|
551
|
+
}
|
|
552
|
+
} catch { /* divergence is advisory */ }
|
|
553
|
+
}
|
|
554
|
+
} catch (err) {
|
|
555
|
+
add(`Pricing catalog check failed: ${err.message}`, false, true);
|
|
556
|
+
}
|
|
557
|
+
|
|
150
558
|
// Validate promptFile references and skill bindings
|
|
151
559
|
try {
|
|
152
560
|
const registry = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'agents', 'registry.json'), 'utf8'));
|
|
@@ -161,6 +569,9 @@ async function cmdDoctor() {
|
|
|
161
569
|
add('Headhunt classifier agents exist in registry', classifierOrphans.length === 0);
|
|
162
570
|
const skillAudit = auditSkills({ rootDir: ROOT_DIR, silent: true });
|
|
163
571
|
add('No declared skills missing on disk', skillAudit.pass, false);
|
|
572
|
+
const { lintResearchRepo } = await import('../lib/research-lint.mjs');
|
|
573
|
+
const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
|
|
574
|
+
add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
|
|
164
575
|
|
|
165
576
|
const sqlConfig = describeSqlStore(process.env);
|
|
166
577
|
const sqlClient = createSqlClient(process.env);
|
|
@@ -169,16 +580,254 @@ async function cmdDoctor() {
|
|
|
169
580
|
await sqlClient`select 1 as ok`;
|
|
170
581
|
add('SQL backend reachable', true);
|
|
171
582
|
} catch {
|
|
172
|
-
add('SQL backend reachable', false);
|
|
583
|
+
add('SQL backend reachable', false, true);
|
|
173
584
|
} finally {
|
|
174
585
|
await closeSqlClient(sqlClient);
|
|
175
586
|
}
|
|
176
587
|
} else {
|
|
177
|
-
add('SQL backend configured', sqlConfig.fallbackAvailable);
|
|
588
|
+
add('SQL backend configured', sqlConfig.fallbackAvailable, true);
|
|
178
589
|
}
|
|
179
590
|
|
|
180
591
|
const vectorConfig = describeVectorStore(process.env);
|
|
181
592
|
add('Vector backend configured', Boolean(vectorConfig.endpoint || vectorConfig.indexPath || vectorConfig.mode === 'file'));
|
|
593
|
+
|
|
594
|
+
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
595
|
+
const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
|
|
596
|
+
add('AUTO docs up to date', stale.length === 0, true);
|
|
597
|
+
|
|
598
|
+
const settingsPath = path.join(ROOT_DIR, 'platforms', 'claude', 'settings.template.json');
|
|
599
|
+
if (fs.existsSync(settingsPath)) {
|
|
600
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
601
|
+
const refs = [...raw.matchAll(/lib\/hooks\/([\w-]+\.m?js)/g)].map((m) => m[1]);
|
|
602
|
+
const uniq = [...new Set(refs)];
|
|
603
|
+
const missing = uniq.filter((f) => !fs.existsSync(path.join(ROOT_DIR, 'lib', 'hooks', f)));
|
|
604
|
+
const label = missing.length === 0
|
|
605
|
+
? `Hook registrations resolve (${uniq.length} hooks)`
|
|
606
|
+
: `Hook registrations resolve (${missing.length} phantom: ${missing.join(', ')})`;
|
|
607
|
+
add(label, missing.length === 0);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const beadsPreCommit = path.join(ROOT_DIR, '.beads', 'hooks', 'pre-commit');
|
|
611
|
+
const inCI = process.env.CI === 'true' || process.env.CI === '1';
|
|
612
|
+
if (fs.existsSync(beadsPreCommit) && !inCI) {
|
|
613
|
+
const hp = spawnSync('git', ['config', '--get', 'core.hooksPath'], { cwd: ROOT_DIR, encoding: 'utf8' });
|
|
614
|
+
const value = hp.status === 0 ? (hp.stdout || '').trim() : '';
|
|
615
|
+
const wired = value === '.beads/hooks';
|
|
616
|
+
const label = wired
|
|
617
|
+
? 'Git hooks wired (core.hooksPath = .beads/hooks)'
|
|
618
|
+
: value
|
|
619
|
+
? `Git hooks unwired (core.hooksPath = '${value}', expected '.beads/hooks') — pre-commit policy gates inactive`
|
|
620
|
+
: 'Git hooks unwired (core.hooksPath unset) — pre-commit policy gates inactive. Fix: git config core.hooksPath .beads/hooks';
|
|
621
|
+
add(label, wired);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const pluginRegistry = loadPluginRegistry({ cwd: process.cwd(), homeDir: HOME, rootDir: ROOT_DIR, env: process.env });
|
|
625
|
+
add('Plugin manifests valid', pluginRegistry.valid);
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
const { describeEngine } = await import('../lib/engine/index.mjs');
|
|
629
|
+
const engine = await describeEngine({ rootDir: process.cwd() });
|
|
630
|
+
add('Retrieval engine layers resolve', engine.errors.length === 0, true);
|
|
631
|
+
} catch {
|
|
632
|
+
add('Retrieval engine layers resolve', false, true);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
const { checkParity } = await import('../lib/parity.mjs');
|
|
637
|
+
const parity = checkParity({ rootDir: ROOT_DIR, homeDir: HOME });
|
|
638
|
+
add(`Cross-surface adapter parity (${parity.summary.join(' · ')})`, parity.ok);
|
|
639
|
+
} catch {
|
|
640
|
+
add('Cross-surface adapter parity', false);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
const { validateSkills } = await import('../lib/validators/skills.mjs');
|
|
645
|
+
const skillsRoot = path.join(ROOT_DIR, 'skills');
|
|
646
|
+
const skillReport = validateSkills(skillsRoot);
|
|
647
|
+
const label = skillReport.warnings.length > 0
|
|
648
|
+
? `Skill structure (${skillReport.skills.length} skills, ${skillReport.warnings.length} authoring warnings)`
|
|
649
|
+
: `Skill structure (${skillReport.skills.length} skills)`;
|
|
650
|
+
add(label, skillReport.valid, true);
|
|
651
|
+
} catch {
|
|
652
|
+
add('Skill structure', false, true);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const { describeBreakers, STATES } = await import('../lib/providers/circuit-breaker.mjs');
|
|
657
|
+
const breakers = describeBreakers();
|
|
658
|
+
const open = breakers.filter((b) => b.state === STATES.OPEN);
|
|
659
|
+
const label = open.length === 0
|
|
660
|
+
? `Provider circuit breakers (${breakers.length} tracked, none open)`
|
|
661
|
+
: `Provider circuit breakers (${open.length} open: ${open.map((b) => b.key).join(', ')})`;
|
|
662
|
+
add(label, open.length === 0, true);
|
|
663
|
+
} catch {
|
|
664
|
+
add('Provider circuit breakers', false, true);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const { recentViolations } = await import('../lib/agent-contracts-enforce.mjs');
|
|
669
|
+
const violations = recentViolations({ windowMs: 24 * 60 * 60 * 1000 });
|
|
670
|
+
const label = violations.length === 0
|
|
671
|
+
? 'Contract violations (none in last 24h)'
|
|
672
|
+
: `Contract violations (${violations.length} in last 24h — see ~/.cx/contract-violations.jsonl)`;
|
|
673
|
+
add(label, violations.length === 0, true);
|
|
674
|
+
} catch {
|
|
675
|
+
add('Contract violations', false, true);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const capWarnPath = path.join(process.cwd(), '.cx', 'observation-cap-warnings.jsonl');
|
|
680
|
+
if (fs.existsSync(capWarnPath)) {
|
|
681
|
+
const lines = fs.readFileSync(capWarnPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
682
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
683
|
+
const recent = lines.filter((l) => {
|
|
684
|
+
try { return new Date(JSON.parse(l).ts).getTime() >= cutoff; } catch { return false; }
|
|
685
|
+
});
|
|
686
|
+
const totalDropped = recent.reduce((acc, l) => {
|
|
687
|
+
try { return acc + (JSON.parse(l).dropped || 0); } catch { return acc; }
|
|
688
|
+
}, 0);
|
|
689
|
+
const label = recent.length === 0
|
|
690
|
+
? 'Observation cap (no recent drops)'
|
|
691
|
+
: `Observation cap (${totalDropped} obs dropped in last 7d — run \`construct memory consolidate\`)`;
|
|
692
|
+
add(label, recent.length === 0, true);
|
|
693
|
+
} else {
|
|
694
|
+
add('Observation cap (no recent drops)', true, true);
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
add('Observation cap', false, true);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
const { checkObservationsSize } = await import('../lib/observation-store.mjs');
|
|
702
|
+
const sz = checkObservationsSize(process.cwd());
|
|
703
|
+
const mb = (sz.size / 1024 / 1024).toFixed(1);
|
|
704
|
+
const capMb = (sz.cap / 1024 / 1024).toFixed(0);
|
|
705
|
+
const sizeLabel = sz.ok
|
|
706
|
+
? `Observation size (${mb} MB / ${capMb} MB cap)`
|
|
707
|
+
: `Observation size (${mb} MB exceeds ${capMb} MB cap — run \`construct memory consolidate\`)`;
|
|
708
|
+
add(sizeLabel, sz.ok, true);
|
|
709
|
+
} catch {
|
|
710
|
+
add('Observation size', false, true);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (sqlConfig.configured) {
|
|
714
|
+
try {
|
|
715
|
+
const migClient = createSqlClient(process.env);
|
|
716
|
+
if (migClient) {
|
|
717
|
+
const { describeMigrations } = await import('../lib/storage/migrations.mjs');
|
|
718
|
+
const m = await describeMigrations(migClient);
|
|
719
|
+
await closeSqlClient(migClient);
|
|
720
|
+
const ok = !!m.ok && m.drift.length === 0;
|
|
721
|
+
let label = 'Schema migrations applied + no drift';
|
|
722
|
+
if (!ok && m.drift && m.drift.length > 0) {
|
|
723
|
+
const repairable = m.drift.filter((d) => d.idempotent).length;
|
|
724
|
+
const notRepairable = m.drift.length - repairable;
|
|
725
|
+
if (notRepairable === 0) {
|
|
726
|
+
label = `Schema migrations: ${m.drift.length} drifted (all idempotent — run \`construct storage repair-migrations --yes\`)`;
|
|
727
|
+
} else {
|
|
728
|
+
label = `Schema migrations: ${m.drift.length} drifted (${notRepairable} non-idempotent — write a new migration file)`;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
add(label, ok, true);
|
|
732
|
+
}
|
|
733
|
+
} catch {
|
|
734
|
+
add('Schema migrations applied + no drift', false, true);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const { detectBeadsDrift } = await import('../lib/beads/drift.mjs');
|
|
740
|
+
const drift = detectBeadsDrift();
|
|
741
|
+
const ok = drift.counts.stuckInProgress === 0 && drift.counts.mergeDrift === 0;
|
|
742
|
+
let label = 'Beads hygiene: no drift';
|
|
743
|
+
if (!ok) {
|
|
744
|
+
const parts = [];
|
|
745
|
+
if (drift.counts.stuckInProgress > 0) parts.push(`${drift.counts.stuckInProgress} stuck in_progress`);
|
|
746
|
+
if (drift.counts.mergeDrift > 0) parts.push(`${drift.counts.mergeDrift} possible merge-drift`);
|
|
747
|
+
label = `Beads hygiene: ${parts.join(', ')} — run \`construct beads drift\``;
|
|
748
|
+
} else if (drift.counts.staleOpen > 0) {
|
|
749
|
+
label = `Beads hygiene: ${drift.counts.staleOpen} stale-open (advisory) — run \`construct beads drift\``;
|
|
750
|
+
}
|
|
751
|
+
add(label, ok, true);
|
|
752
|
+
} catch {
|
|
753
|
+
// bd not on PATH or repo not a bd workspace — skip silently.
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// cm and Docker are optional but downstream services (memory MCP,
|
|
757
|
+
// managed Postgres) depend on them. Surface missing tools
|
|
758
|
+
// as warnings rather than failures so doctor doesn't flip-flop.
|
|
759
|
+
try {
|
|
760
|
+
const cmProbe = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['cm'], { stdio: 'pipe' });
|
|
761
|
+
const cmAvailable = cmProbe.status === 0;
|
|
762
|
+
add(cmAvailable ? 'cm available' : 'cm available (optional — memory MCP at 127.0.0.1:8765 needs it)', cmAvailable, true);
|
|
763
|
+
} catch {
|
|
764
|
+
add('cm available', false, true);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
try {
|
|
768
|
+
const dockerCli = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['docker'], { stdio: 'pipe' });
|
|
769
|
+
let dockerOk = false;
|
|
770
|
+
let dockerNote = 'Docker not installed (optional — managed Postgres needs it)';
|
|
771
|
+
if (dockerCli.status === 0) {
|
|
772
|
+
const dockerInfo = spawnSync('docker', ['info'], { stdio: 'pipe' });
|
|
773
|
+
if (dockerInfo.status === 0) {
|
|
774
|
+
dockerOk = true;
|
|
775
|
+
dockerNote = 'Docker daemon reachable';
|
|
776
|
+
} else {
|
|
777
|
+
dockerNote = 'Docker CLI installed but daemon not reachable — run `construct up` on macOS to auto-start, or start Docker manually';
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
add(dockerOk ? 'Docker daemon reachable' : `Docker daemon reachable (${dockerNote})`, dockerOk, true);
|
|
781
|
+
} catch {
|
|
782
|
+
add('Docker daemon reachable', false, true);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
const failuresPath = path.join(HOME, '.cx', 'hook-failures.jsonl');
|
|
787
|
+
if (fs.existsSync(failuresPath)) {
|
|
788
|
+
const lines = fs.readFileSync(failuresPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
789
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
790
|
+
const recent = lines.filter((l) => {
|
|
791
|
+
try { return new Date(JSON.parse(l).ts).getTime() >= cutoff; } catch { return false; }
|
|
792
|
+
});
|
|
793
|
+
add(`No recent hook failures (${recent.length} in last 24h)`, recent.length === 0, true);
|
|
794
|
+
} else {
|
|
795
|
+
add('No recent hook failures (0 in last 24h)', true, true);
|
|
796
|
+
}
|
|
797
|
+
} catch {
|
|
798
|
+
add('No recent hook failures (24h)', false, true);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Agent contracts integrity
|
|
802
|
+
const { getAllContracts, summarize } = await import('../lib/agent-contracts.mjs');
|
|
803
|
+
const contracts = getAllContracts();
|
|
804
|
+
add('Agent contracts loaded', contracts.length > 0);
|
|
805
|
+
if (contracts.length > 0) {
|
|
806
|
+
const missingTerminals = contracts.filter((c) =>
|
|
807
|
+
c.consumer && !c.consumer.startsWith('cx-') && c.consumer !== 'construct' && c.consumer !== '*'
|
|
808
|
+
? false : false,
|
|
809
|
+
);
|
|
810
|
+
add('Agent contract schema intact', missingTerminals.length === 0);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Skills profile — if present, check staleness against current tags
|
|
814
|
+
const profilePath = path.join(process.cwd(), '.cx', 'skills-profile.json');
|
|
815
|
+
if (fs.existsSync(profilePath)) {
|
|
816
|
+
try {
|
|
817
|
+
const { detectProjectProfile } = await import('../lib/project-profile.mjs');
|
|
818
|
+
const current = detectProjectProfile(process.cwd());
|
|
819
|
+
const saved = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
|
820
|
+
const savedTags = new Set(saved?.profile?.tags || []);
|
|
821
|
+
const currentTags = new Set(current.tags);
|
|
822
|
+
const drift =
|
|
823
|
+
savedTags.size !== currentTags.size ||
|
|
824
|
+
[...savedTags].some((t) => !currentTags.has(t)) ||
|
|
825
|
+
[...currentTags].some((t) => !savedTags.has(t));
|
|
826
|
+
add('Skills profile matches current project stack', !drift, true);
|
|
827
|
+
} catch {
|
|
828
|
+
add('Skills profile matches current project stack', false, true);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
182
831
|
} catch {
|
|
183
832
|
add('Registry integrity check', false);
|
|
184
833
|
}
|
|
@@ -200,7 +849,25 @@ async function cmdDoctor() {
|
|
|
200
849
|
}
|
|
201
850
|
|
|
202
851
|
async function cmdUp() {
|
|
203
|
-
|
|
852
|
+
// Auto-magic health check — catch missing prerequisites before starting services
|
|
853
|
+
const { quickHealthCheck, detectCredentials, resolveCredentials } = await import('../lib/health-check.mjs');
|
|
854
|
+
|
|
855
|
+
// Resolve credentials from env, .env files, and 1Password before starting services
|
|
856
|
+
const resolvedCreds = await resolveCredentials({ homeDir: HOME });
|
|
857
|
+
|
|
858
|
+
const health = await quickHealthCheck({ homeDir: HOME, showCredentials: true });
|
|
859
|
+
|
|
860
|
+
if (!health.ok) {
|
|
861
|
+
errorln('Prerequisites missing for starting services:');
|
|
862
|
+
for (const item of health.missing) {
|
|
863
|
+
errorln(` ✗ ${item}`);
|
|
864
|
+
}
|
|
865
|
+
println('');
|
|
866
|
+
info('Run `construct install --yes` to install missing dependencies.');
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const { results, recovery } = await startServices({ rootDir: ROOT_DIR, homeDir: HOME });
|
|
204
871
|
for (const svc of results) {
|
|
205
872
|
const label = svc.url ? `${svc.name} → ${svc.url}` : svc.name;
|
|
206
873
|
if (svc.status === 'started') ok(svc.note ? `${label} (${svc.note})` : label);
|
|
@@ -209,17 +876,119 @@ async function cmdUp() {
|
|
|
209
876
|
else if (svc.status === 'failed') errorln(`${svc.name}: ${svc.note}`);
|
|
210
877
|
else warn(`${svc.name}: ${svc.note}`);
|
|
211
878
|
}
|
|
879
|
+
|
|
880
|
+
if (recovery) {
|
|
881
|
+
println('');
|
|
882
|
+
info(`Recovery: ${recovery.message}`);
|
|
883
|
+
const anchors = [
|
|
884
|
+
recovery.durable?.plan,
|
|
885
|
+
recovery.durable?.context,
|
|
886
|
+
recovery.durable?.latestHandoff,
|
|
887
|
+
recovery.durable?.beads,
|
|
888
|
+
].filter(Boolean);
|
|
889
|
+
if (anchors.length) info(`Durable anchors: ${anchors.join(', ')}`);
|
|
890
|
+
if (recovery.degraded?.length) {
|
|
891
|
+
info(`Degraded services: ${recovery.degraded.map((entry) => `${entry.name} (${entry.status})`).join(', ')}`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Auto-start embed daemon if embed.yaml exists and autoEmbed is enabled
|
|
896
|
+
const { loadProjectConfig } = await import('../lib/config/project-config.mjs');
|
|
897
|
+
const { existsSync } = await import('node:fs');
|
|
898
|
+
const { join } = await import('node:path');
|
|
899
|
+
|
|
900
|
+
const embedConfigExists = existsSync(join(ROOT_DIR, 'embed.yaml'));
|
|
901
|
+
const config = loadProjectConfig(ROOT_DIR);
|
|
902
|
+
const autoEmbed = config.config?.autoEmbed;
|
|
903
|
+
|
|
904
|
+
if (embedConfigExists && autoEmbed) {
|
|
905
|
+
try {
|
|
906
|
+
const { runEmbedCli } = await import('../lib/embed/cli.mjs');
|
|
907
|
+
await runEmbedCli(['start']);
|
|
908
|
+
} catch {
|
|
909
|
+
// best effort
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Post-start summary
|
|
914
|
+
println('');
|
|
915
|
+
println('═══════════════════════════════════════════════════════════');
|
|
916
|
+
println(' SERVICES RUNNING');
|
|
917
|
+
println('═══════════════════════════════════════════════════════════');
|
|
918
|
+
println('');
|
|
919
|
+
for (const svc of results) {
|
|
920
|
+
if (svc.status === 'started' || svc.status === 'reused') {
|
|
921
|
+
const label = svc.url ? `${svc.url}` : svc.name;
|
|
922
|
+
println(` ${svc.name.padEnd(14)} ${label}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Show credentials summary
|
|
927
|
+
const creds = health.credentials;
|
|
928
|
+
const credKeys = Object.keys(resolvedCreds);
|
|
929
|
+
if (creds || credKeys.length > 0) {
|
|
930
|
+
const configured = [];
|
|
931
|
+
if (creds.github_cli) configured.push('GitHub CLI');
|
|
932
|
+
if (creds.copilot) configured.push('Copilot');
|
|
933
|
+
if (creds.anthropic_key || credKeys.includes('ANTHROPIC_API_KEY')) configured.push('Anthropic');
|
|
934
|
+
if (creds.openai_key || credKeys.includes('OPENAI_API_KEY')) configured.push('OpenAI');
|
|
935
|
+
if (creds.openrouter_key || credKeys.includes('OPENROUTER_API_KEY')) configured.push('OpenRouter');
|
|
936
|
+
if (creds.ollama) configured.push('Ollama');
|
|
937
|
+
if (creds.vscode) configured.push('VS Code');
|
|
938
|
+
if (creds.claude_code) configured.push('Claude Code');
|
|
939
|
+
if (creds.opencode) configured.push('OpenCode');
|
|
940
|
+
if (creds.github_token || credKeys.includes('GITHUB_TOKEN') || credKeys.includes('GH_TOKEN')) configured.push('GitHub token');
|
|
941
|
+
|
|
942
|
+
if (configured.length > 0) {
|
|
943
|
+
println('');
|
|
944
|
+
println(' Credentials:');
|
|
945
|
+
println(` ${configured.join(', ')}`);
|
|
946
|
+
if (credKeys.length > 0) {
|
|
947
|
+
println(` ${credKeys.length} key${credKeys.length === 1 ? '' : 's'} resolved`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
println('');
|
|
953
|
+
println('Dashboard: http://127.0.0.1:4242');
|
|
954
|
+
println('Run \x1b[32mconstruct\x1b[0m for commands');
|
|
955
|
+
println('');
|
|
212
956
|
}
|
|
213
957
|
|
|
214
958
|
async function cmdDown() {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
959
|
+
const { stopServices } = await import('../lib/service-manager.mjs');
|
|
960
|
+
const result = await stopServices({ homeDir: HOME, rootDir: ROOT_DIR });
|
|
961
|
+
for (const svc of result.results) {
|
|
962
|
+
if (svc.status === 'stopped') ok(`${svc.name} stopped${svc.note ? ` (${svc.note})` : ''}.`);
|
|
963
|
+
else if (svc.status === 'cleaned') warn(`${svc.name}: ${svc.note}.`);
|
|
964
|
+
else if (svc.status === 'error') errorln(`${svc.name}: ${svc.note}.`);
|
|
965
|
+
else if (svc.status === 'skipped') info(`${svc.name}: skipped — ${svc.note}.`);
|
|
966
|
+
else info(`${svc.name}: not running.`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
try {
|
|
970
|
+
const { autoCleanHandoffs } = await import('../lib/handoffs/cleanup.mjs');
|
|
971
|
+
const { result: cleanResult } = autoCleanHandoffs(process.cwd(), process.env);
|
|
972
|
+
const total = (cleanResult.moved?.length || 0) + (cleanResult.deleted?.length || 0);
|
|
973
|
+
if (total > 0) info(`Handoff cleanup: ${cleanResult.moved?.length || 0} archived, ${cleanResult.deleted?.length || 0} deleted.`);
|
|
974
|
+
} catch { /* best effort */ }
|
|
220
975
|
}
|
|
221
976
|
|
|
222
977
|
async function cmdServe() {
|
|
978
|
+
if (restArgsCache.includes('--token')) {
|
|
979
|
+
const { generateToken, setDashboardToken, getDashboardToken } = await import('../lib/server/auth.mjs');
|
|
980
|
+
const existing = getDashboardToken();
|
|
981
|
+
if (existing) {
|
|
982
|
+
println(`Dashboard token already set. To rotate it, remove CONSTRUCT_DASHBOARD_TOKEN from ~/.construct/config.env and re-run.`);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const token = generateToken();
|
|
986
|
+
setDashboardToken(token);
|
|
987
|
+
println(`Dashboard token generated and saved to ~/.construct/config.env`);
|
|
988
|
+
println(`CONSTRUCT_DASHBOARD_TOKEN=${token}`);
|
|
989
|
+
println(`Pass this token in the dashboard login prompt or as: Authorization: Bearer <token>`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
223
992
|
const dashboard = await startDashboard({ rootDir: ROOT_DIR, homeDir: HOME });
|
|
224
993
|
if (dashboard.reused) {
|
|
225
994
|
println(`Construct dashboard already running on ${dashboard.url} (pid ${dashboard.pid})`);
|
|
@@ -235,59 +1004,54 @@ async function cmdVersion() {
|
|
|
235
1004
|
|
|
236
1005
|
async function cmdReview(args) {
|
|
237
1006
|
fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
|
|
238
|
-
|
|
239
|
-
errorln('Error: LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set.');
|
|
240
|
-
errorln('Configure Langfuse credentials in ~/.construct/config.env or .env.');
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
243
|
-
runNodeScript(path.join(ROOT_DIR, 'lib', 'review.mjs'), args);
|
|
244
|
-
}
|
|
1007
|
+
const sub = args[0];
|
|
245
1008
|
|
|
246
|
-
|
|
247
|
-
|
|
1009
|
+
if (!sub || sub === 'run') {
|
|
1010
|
+
const { runGenerator } = await import('../lib/performance/generate.mjs');
|
|
1011
|
+
const result = await runGenerator({ env: process.env });
|
|
1012
|
+
info(`Wrote ${result.filename} · ${result.agentCount} agent${result.agentCount === 1 ? '' : 's'}`);
|
|
1013
|
+
println(` ${result.path}`);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
248
1016
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const [key, ...valueParts] = arg.slice(2).split('=');
|
|
255
|
-
flags[key] = valueParts.length ? valueParts.join('=') : true;
|
|
256
|
-
} else {
|
|
257
|
-
queryParts.push(arg);
|
|
1017
|
+
if (sub === 'legacy') {
|
|
1018
|
+
if (!ENV.CONSTRUCT_TELEMETRY_PUBLIC_KEY || !ENV.CONSTRUCT_TELEMETRY_SECRET_KEY) {
|
|
1019
|
+
errorln('Error: CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY must be set.');
|
|
1020
|
+
errorln('Configure telemetry credentials in ~/.construct/config.env or .env.');
|
|
1021
|
+
process.exit(1);
|
|
258
1022
|
}
|
|
1023
|
+
runNodeScript(path.join(ROOT_DIR, 'scripts', 'review.mjs'), args.slice(1), ENV);
|
|
1024
|
+
return;
|
|
259
1025
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
ext: typeof flags.ext === 'string' ? flags.ext : defaults.ext,
|
|
264
|
-
depth: typeof flags.depth === 'string' ? flags.depth : defaults.depth,
|
|
265
|
-
};
|
|
1026
|
+
|
|
1027
|
+
errorln('Usage: construct review [run|legacy]');
|
|
1028
|
+
process.exit(1);
|
|
266
1029
|
}
|
|
267
1030
|
|
|
268
|
-
async function
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1031
|
+
async function cmdInit(args) { runNodeScript(path.join(ROOT_DIR, 'lib', 'init-unified.mjs'), args); }
|
|
1032
|
+
async function cmdDocsVerify(args) { runNodeScript(path.join(ROOT_DIR, 'lib', 'docs-verify.mjs'), args); }
|
|
1033
|
+
async function cmdInitUpdate(args) {
|
|
1034
|
+
runNodeScript(path.join(ROOT_DIR, 'lib', 'init-update.mjs'), args);
|
|
1035
|
+
}
|
|
1036
|
+
async function cmdDistill(args) { runDistillCli(args); }
|
|
1037
|
+
async function cmdIngest(args) {
|
|
1038
|
+
try {
|
|
1039
|
+
const result = await runIngestCli(args, { cwd: process.cwd(), env: process.env });
|
|
1040
|
+
println(JSON.stringify(result, null, 2));
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
errorln(error?.message || 'document ingest failed');
|
|
273
1043
|
process.exit(1);
|
|
274
1044
|
}
|
|
275
|
-
const distillArgs = [parsed.dir, '--format=extract', `--query=${query}`, '--mode=json'];
|
|
276
|
-
if (parsed.ext) distillArgs.push(`--ext=${parsed.ext}`);
|
|
277
|
-
if (parsed.depth) distillArgs.push(`--depth=${parsed.depth}`);
|
|
278
|
-
await runDistillCli(distillArgs);
|
|
279
1045
|
}
|
|
280
1046
|
|
|
281
|
-
async function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1047
|
+
async function cmdInfer(args) {
|
|
1048
|
+
try {
|
|
1049
|
+
const result = await runInferCli(args, { cwd: process.cwd() });
|
|
1050
|
+
println(JSON.stringify(result, null, 2));
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
errorln(error?.message || 'schema inference failed');
|
|
286
1053
|
process.exit(1);
|
|
287
1054
|
}
|
|
288
|
-
const distillArgs = [parsed.dir, '--format=extract', `--query=${query}`, '--mode=json', `--ext=${parsed.ext}`];
|
|
289
|
-
if (parsed.depth) distillArgs.push(`--depth=${parsed.depth}`);
|
|
290
|
-
await runDistillCli(distillArgs);
|
|
291
1055
|
}
|
|
292
1056
|
|
|
293
1057
|
async function cmdSearch(args) {
|
|
@@ -304,87 +1068,1350 @@ async function cmdSearch(args) {
|
|
|
304
1068
|
}
|
|
305
1069
|
const limitFlag = [...flags].find((flag) => flag.startsWith('--limit='));
|
|
306
1070
|
const limit = limitFlag ? Number.parseInt(limitFlag.split('=')[1], 10) : 10;
|
|
307
|
-
const results = await buildHybridSearchResultsAsync(
|
|
1071
|
+
const results = await buildHybridSearchResultsAsync(process.cwd(), query, { limit: Number.isFinite(limit) ? limit : 10, env: process.env });
|
|
308
1072
|
println(JSON.stringify(results, null, 2));
|
|
309
1073
|
}
|
|
310
1074
|
|
|
311
1075
|
async function cmdStorage(args) {
|
|
312
1076
|
const [action, ...rest] = args;
|
|
313
1077
|
if (!action) {
|
|
314
|
-
errorln('Usage: construct storage <sync|status>');
|
|
1078
|
+
errorln('Usage: construct storage <sync|status|reset|delete-ingested|repair-migrations|migrations>');
|
|
315
1079
|
process.exit(1);
|
|
316
1080
|
}
|
|
1081
|
+
if (action === 'migrations') {
|
|
1082
|
+
const { describeMigrations } = await import('../lib/storage/migrations.mjs');
|
|
1083
|
+
const sqlClient = createSqlClient(process.env);
|
|
1084
|
+
if (!sqlClient) { errorln('No DATABASE_URL configured.'); process.exit(1); }
|
|
1085
|
+
try {
|
|
1086
|
+
const report = await describeMigrations(sqlClient);
|
|
1087
|
+
println(JSON.stringify(report, null, 2));
|
|
1088
|
+
if (report.drift?.length > 0) process.exit(2);
|
|
1089
|
+
} finally {
|
|
1090
|
+
await closeSqlClient(sqlClient);
|
|
1091
|
+
}
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (action === 'repair-migrations') {
|
|
1095
|
+
const confirm = rest.includes('--yes');
|
|
1096
|
+
if (!confirm) {
|
|
1097
|
+
errorln('This re-applies drifted migrations (idempotent only) and updates their recorded SHA.');
|
|
1098
|
+
errorln('Destructive migrations (DROP / TRUNCATE / ALTER…DROP / DELETE) are refused.');
|
|
1099
|
+
errorln('Re-run with --yes to proceed.');
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
const { runMigrations } = await import('../lib/storage/migrations.mjs');
|
|
1103
|
+
const sqlClient = createSqlClient(process.env);
|
|
1104
|
+
if (!sqlClient) { errorln('No DATABASE_URL configured.'); process.exit(1); }
|
|
1105
|
+
try {
|
|
1106
|
+
const result = await runMigrations(sqlClient, { repair: true });
|
|
1107
|
+
println(JSON.stringify({
|
|
1108
|
+
applied: result.applied,
|
|
1109
|
+
repaired: result.repaired,
|
|
1110
|
+
skipped: result.skipped,
|
|
1111
|
+
drift: result.drift,
|
|
1112
|
+
}, null, 2));
|
|
1113
|
+
if (result.drift.length > 0) {
|
|
1114
|
+
errorln('');
|
|
1115
|
+
errorln('Refused to repair non-idempotent migrations:');
|
|
1116
|
+
for (const d of result.drift) errorln(` ${d.filename}`);
|
|
1117
|
+
process.exit(2);
|
|
1118
|
+
}
|
|
1119
|
+
info(`Repaired ${result.repaired.length} migration(s); applied ${result.applied.length} new.`);
|
|
1120
|
+
} finally {
|
|
1121
|
+
await closeSqlClient(sqlClient);
|
|
1122
|
+
}
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
317
1125
|
if (action === 'sync') {
|
|
318
|
-
const result = await syncFileStateToSql(process.cwd(), { env: process.env, project:
|
|
1126
|
+
const result = await syncFileStateToSql(process.cwd(), { env: process.env, project: inferProjectName(process.cwd()) });
|
|
319
1127
|
println(JSON.stringify(result, null, 2));
|
|
320
1128
|
return;
|
|
321
1129
|
}
|
|
322
1130
|
if (action === 'status') {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1131
|
+
const result = await getStorageStatus(process.cwd(), { env: process.env, project: inferProjectName(process.cwd()) });
|
|
1132
|
+
println(JSON.stringify(result, null, 2));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (action === 'reset') {
|
|
1136
|
+
const confirm = rest.includes('--yes');
|
|
1137
|
+
const includeIngested = rest.includes('--include-ingested');
|
|
1138
|
+
const sqlOnly = rest.includes('--sql-only');
|
|
1139
|
+
const vectorOnly = rest.includes('--vector-only');
|
|
1140
|
+
if (!confirm) {
|
|
1141
|
+
errorln('Usage: construct storage reset --yes [--include-ingested] [--sql-only|--vector-only]');
|
|
1142
|
+
process.exit(1);
|
|
335
1143
|
}
|
|
336
|
-
|
|
1144
|
+
const result = await resetStorage(process.cwd(), {
|
|
1145
|
+
env: process.env,
|
|
1146
|
+
project: inferProjectName(process.cwd()),
|
|
1147
|
+
resetSql: vectorOnly ? false : true,
|
|
1148
|
+
resetVector: sqlOnly ? false : true,
|
|
1149
|
+
resetIngested: includeIngested,
|
|
1150
|
+
confirm: true,
|
|
1151
|
+
});
|
|
1152
|
+
println(JSON.stringify(result, null, 2));
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (action === 'delete-ingested') {
|
|
1156
|
+
const confirm = rest.includes('--yes');
|
|
1157
|
+
const files = rest.filter((arg) => !arg.startsWith('--'));
|
|
1158
|
+
if (!confirm) {
|
|
1159
|
+
errorln('Usage: construct storage delete-ingested --yes [relative-file ...]');
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
const result = deleteIngestedArtifacts(process.cwd(), { files, confirm: true });
|
|
1163
|
+
println(JSON.stringify(result, null, 2));
|
|
337
1164
|
return;
|
|
338
1165
|
}
|
|
339
|
-
errorln('Usage: construct storage <sync|status>');
|
|
1166
|
+
errorln('Usage: construct storage <sync|status|reset|delete-ingested|repair-migrations|migrations>');
|
|
340
1167
|
process.exit(1);
|
|
341
1168
|
}
|
|
342
|
-
async function cmdHeadhunt(args) { await runHeadhunt({ args, cwd: process.cwd(), homeDir: HOME }); }
|
|
343
|
-
async function cmdHosts() { printHostCapabilities(); }
|
|
344
|
-
async function cmdSetup(args) { await runSetup({ rootDir: ROOT_DIR, args, homeDir: HOME }); }
|
|
345
1169
|
|
|
346
|
-
async function
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
1170
|
+
async function cmdPrune(args) {
|
|
1171
|
+
const dryRun = args.includes('--dry-run');
|
|
1172
|
+
const { planPrune, executePrune, measureUsage } = await import('../lib/resources/budget.mjs');
|
|
1173
|
+
const actions = planPrune(process.cwd(), process.env);
|
|
1174
|
+
if (actions.length === 0) { info('Nothing to prune. .cx/ is within all configured budgets.'); return; }
|
|
1175
|
+
const byCat = {};
|
|
1176
|
+
let totalBytes = 0;
|
|
1177
|
+
for (const a of actions) {
|
|
1178
|
+
byCat[a.category] = (byCat[a.category] || 0) + 1;
|
|
1179
|
+
totalBytes += a.bytes || 0;
|
|
351
1180
|
}
|
|
352
|
-
println(
|
|
1181
|
+
println(`Plan: prune ${actions.length} file(s), free ${Math.round(totalBytes / 1024)}KB`);
|
|
1182
|
+
for (const [cat, count] of Object.entries(byCat)) println(` ${cat}: ${count} file(s)`);
|
|
1183
|
+
if (dryRun) { info('--dry-run: no files removed.'); return; }
|
|
1184
|
+
const result = executePrune(actions);
|
|
1185
|
+
info(`Pruned ${result.removed.length} file(s), freed ${Math.round(result.bytesFreed / 1024)}KB.`);
|
|
1186
|
+
const after = measureUsage(process.cwd(), process.env);
|
|
1187
|
+
println(`.cx/ now at ${Math.round(after.totalCxBytes / 1024 / 1024)}MB / ${Math.round(after.totalCxCap / 1024 / 1024)}MB cap (${Math.round(after.totalCxUsageRatio * 100)}%).`);
|
|
353
1188
|
}
|
|
354
1189
|
|
|
355
|
-
async function
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1190
|
+
async function cmdCosts(args) {
|
|
1191
|
+
const sub = args[0] || 'status';
|
|
1192
|
+
const {
|
|
1193
|
+
getDailySpend,
|
|
1194
|
+
getTotalDailySpend,
|
|
1195
|
+
personaBudget,
|
|
1196
|
+
totalBudget,
|
|
1197
|
+
enforcementActive,
|
|
1198
|
+
checkBudget,
|
|
1199
|
+
} = await import('../lib/cost-ledger.mjs');
|
|
1200
|
+
|
|
1201
|
+
if (sub === 'status') {
|
|
1202
|
+
const total = getTotalDailySpend();
|
|
1203
|
+
const cap = totalBudget();
|
|
1204
|
+
const pct = cap > 0 ? Math.round((total.costUsd / cap) * 100) : 0;
|
|
1205
|
+
println(`Today's total spend: $${total.costUsd.toFixed(4)} / $${cap.toFixed(2)} cap (${pct}%)`);
|
|
1206
|
+
println(`Enforcement: ${enforcementActive() ? 'on (hard-stop at cap)' : 'advisory (warning only)'}`);
|
|
1207
|
+
println('');
|
|
1208
|
+
println('Per-persona daily spend:');
|
|
1209
|
+
const personas = Object.keys(total.byPersona || {});
|
|
1210
|
+
if (personas.length === 0) {
|
|
1211
|
+
println(' (no recorded spend today)');
|
|
1212
|
+
} else {
|
|
1213
|
+
for (const p of personas.sort()) {
|
|
1214
|
+
const spend = getDailySpend({ personaId: p });
|
|
1215
|
+
const personaCap = personaBudget(p);
|
|
1216
|
+
const personaPct = personaCap > 0 ? Math.round((spend.costUsd / personaCap) * 100) : 0;
|
|
1217
|
+
println(` ${p.padEnd(22)} $${spend.costUsd.toFixed(4).padStart(10)} / $${personaCap.toFixed(2)} (${personaPct}%)`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
363
1220
|
return;
|
|
364
1221
|
}
|
|
365
|
-
|
|
366
|
-
if (
|
|
1222
|
+
|
|
1223
|
+
if (sub === 'check') {
|
|
1224
|
+
const persona = args[1];
|
|
1225
|
+
if (!persona) { errorln('Usage: construct costs check <personaId>'); process.exit(1); }
|
|
1226
|
+
const result = checkBudget({ personaId: persona });
|
|
1227
|
+
println(JSON.stringify(result, null, 2));
|
|
1228
|
+
if (!result.allowed) process.exit(2);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
errorln('Usage: construct costs <status|check <personaId>>');
|
|
367
1233
|
process.exit(1);
|
|
368
1234
|
}
|
|
369
1235
|
|
|
370
|
-
async function
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
println(
|
|
377
|
-
|
|
1236
|
+
async function cmdHandoffs(args) {
|
|
1237
|
+
const sub = args[0] || 'status';
|
|
1238
|
+
const { summarizeHandoffs } = await import('../lib/handoffs/inventory.mjs');
|
|
1239
|
+
const summary = summarizeHandoffs(process.cwd(), process.env);
|
|
1240
|
+
if (sub === 'status') {
|
|
1241
|
+
if (summary.state === 'empty') { info('No handoffs in .cx/handoffs/.'); return; }
|
|
1242
|
+
println(`Handoffs: ${summary.total} files · ${Math.round(summary.bytes / 1024)}KB · oldest ${summary.oldestAgeDays?.toFixed(1)}d`);
|
|
1243
|
+
println(`Retention: ${summary.maxDays}d / ${summary.maxItems} items`);
|
|
1244
|
+
println('');
|
|
1245
|
+
if (summary.pastRetentionCount > 0) {
|
|
1246
|
+
println(`⚠ ${summary.pastRetentionCount} file(s) past retention — run \`construct prune\``);
|
|
1247
|
+
}
|
|
1248
|
+
if (summary.resolvedCount > 0) {
|
|
1249
|
+
println(`ℹ ${summary.resolvedCount} file(s) reference only closed beads (resolved-but-kept)`);
|
|
1250
|
+
}
|
|
1251
|
+
if (!summary.pastRetentionCount && !summary.resolvedCount) {
|
|
1252
|
+
println(`✓ All handoffs within retention and reference open work.`);
|
|
1253
|
+
}
|
|
1254
|
+
println('');
|
|
1255
|
+
println('Recent:');
|
|
1256
|
+
for (const r of summary.recent) {
|
|
1257
|
+
println(` ${r.ageDays.toFixed(1).padStart(5)}d ${r.filename} ${r.beadRefs.length ? `(${r.beadRefs.join(', ')})` : ''}`);
|
|
1258
|
+
}
|
|
378
1259
|
return;
|
|
379
1260
|
}
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1261
|
+
if (sub === 'list') {
|
|
1262
|
+
if (summary.state === 'empty') { info('No handoffs.'); return; }
|
|
1263
|
+
for (const f of summary.files) {
|
|
1264
|
+
const tag = f.beadRefs.length ? ` (${f.beadRefs.join(', ')})` : '';
|
|
1265
|
+
println(`${f.ageDays.toFixed(1).padStart(5)}d ${f.status.padEnd(8)} ${f.filename}${tag}`);
|
|
1266
|
+
}
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (sub === 'show') {
|
|
1271
|
+
const target = args[1];
|
|
1272
|
+
if (!target) { errorln('Usage: construct handoffs show <filename-or-id>'); process.exit(1); }
|
|
1273
|
+
const match = summary.files?.find((f) => f.filename === target || f.filename.startsWith(target) || f.id === target);
|
|
1274
|
+
if (!match) { errorln(`No handoff matching "${target}".`); process.exit(1); }
|
|
1275
|
+
const { parseHandoffFile } = await import('../lib/handoffs/contract.mjs');
|
|
1276
|
+
const parsed = parseHandoffFile(match.path);
|
|
1277
|
+
println(`--- ${match.filename} ---`);
|
|
1278
|
+
println(`Status: ${parsed.status}`);
|
|
1279
|
+
if (parsed.frontmatter?.title) println(`Title: ${parsed.frontmatter.title}`);
|
|
1280
|
+
if (parsed.frontmatter?.beads?.length) println(`Beads: ${parsed.frontmatter.beads.join(', ')}`);
|
|
1281
|
+
if (parsed.frontmatter?.created) println(`Created: ${parsed.frontmatter.created}`);
|
|
1282
|
+
println('');
|
|
1283
|
+
for (const [heading, content] of Object.entries(parsed.sections)) {
|
|
1284
|
+
println(`## ${heading}`);
|
|
1285
|
+
println(content);
|
|
1286
|
+
println('');
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (sub === 'validate') {
|
|
1292
|
+
const { parseHandoffFile, validateHandoff } = await import('../lib/handoffs/contract.mjs');
|
|
1293
|
+
if (summary.state === 'empty') { info('No handoffs to validate.'); return; }
|
|
1294
|
+
let hasErrors = false;
|
|
1295
|
+
for (const f of summary.files) {
|
|
1296
|
+
const parsed = parseHandoffFile(f.path);
|
|
1297
|
+
const { valid, errors } = validateHandoff(parsed);
|
|
1298
|
+
if (valid) {
|
|
1299
|
+
println(`✓ ${f.filename}`);
|
|
1300
|
+
} else {
|
|
1301
|
+
hasErrors = true;
|
|
1302
|
+
println(`✗ ${f.filename}`);
|
|
1303
|
+
for (const e of errors) println(` ${e}`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (hasErrors) process.exit(1);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (sub === 'prune') {
|
|
1311
|
+
const { autoCleanHandoffs } = await import('../lib/handoffs/cleanup.mjs');
|
|
1312
|
+
const { result } = autoCleanHandoffs(process.cwd(), process.env);
|
|
1313
|
+
if (result.moved.length === 0 && result.deleted.length === 0) {
|
|
1314
|
+
info('Nothing to prune.');
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
for (const a of result.moved) println(`archived ${path.basename(a.from)}`);
|
|
1318
|
+
for (const a of result.deleted) println(`deleted ${path.basename(a.path)}`);
|
|
1319
|
+
println(`Done: ${result.moved.length} archived, ${result.deleted.length} deleted.`);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
errorln('Usage: construct handoffs [status|list|show|validate|prune]');
|
|
1324
|
+
process.exit(1);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function cmdResources(args) {
|
|
1328
|
+
const sub = args[0] || 'status';
|
|
1329
|
+
const { measureUsage } = await import('../lib/resources/budget.mjs');
|
|
1330
|
+
if (sub === 'status') {
|
|
1331
|
+
const u = measureUsage(process.cwd(), process.env);
|
|
1332
|
+
const totalMb = u.totalCxBytes / 1024 / 1024;
|
|
1333
|
+
const capMb = u.totalCxCap / 1024 / 1024;
|
|
1334
|
+
println(`.cx/ total: ${totalMb.toFixed(1)}MB / ${capMb.toFixed(0)}MB (${(u.totalCxUsageRatio * 100).toFixed(0)}%)`);
|
|
1335
|
+
println('');
|
|
1336
|
+
println('Category Files Size Enforcement');
|
|
1337
|
+
for (const [name, c] of Object.entries(u.categories)) {
|
|
1338
|
+
const sizeKb = (c.bytes / 1024).toFixed(1);
|
|
1339
|
+
println(` ${name.padEnd(22)} ${String(c.files).padStart(5)} ${sizeKb.padStart(8)}KB ${c.enforcement}`);
|
|
1340
|
+
}
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
errorln('Usage: construct resources status');
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
async function cmdOverrides(args) {
|
|
1348
|
+
const sub = args[0] || 'list';
|
|
1349
|
+
const {
|
|
1350
|
+
describeOverrides,
|
|
1351
|
+
listBackups,
|
|
1352
|
+
pruneBackups,
|
|
1353
|
+
resolveOverride,
|
|
1354
|
+
SUPPORTED_CATEGORIES,
|
|
1355
|
+
} = await import('../lib/overrides/resolver.mjs');
|
|
1356
|
+
|
|
1357
|
+
if (sub === 'list') {
|
|
1358
|
+
const summary = describeOverrides(process.cwd());
|
|
1359
|
+
let total = 0;
|
|
1360
|
+
for (const category of SUPPORTED_CATEGORIES) {
|
|
1361
|
+
const entries = summary[category] || [];
|
|
1362
|
+
if (entries.length === 0) continue;
|
|
1363
|
+
println(`${category}/`);
|
|
1364
|
+
for (const entry of entries) println(` ${entry}`);
|
|
1365
|
+
total += entries.length;
|
|
1366
|
+
}
|
|
1367
|
+
if (total === 0) info('No user overrides — running on shipped defaults.');
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (sub === 'backups') {
|
|
1372
|
+
const category = args[1];
|
|
1373
|
+
const name = args[2];
|
|
1374
|
+
if (!category || !name) { errorln('Usage: construct overrides backups <category> <name>'); process.exit(1); }
|
|
1375
|
+
const backups = listBackups(process.cwd(), category, name);
|
|
1376
|
+
if (backups.length === 0) { info(`No backups for ${category}/${name}.`); return; }
|
|
1377
|
+
for (const b of backups) {
|
|
1378
|
+
const age = Math.floor((Date.now() - b.mtimeMs) / 1000);
|
|
1379
|
+
println(`${b.filename} ${b.size}B ${age}s ago`);
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (sub === 'prune') {
|
|
1385
|
+
const daysFlag = args.find((a) => a.startsWith('--max-days='));
|
|
1386
|
+
const maxDays = daysFlag ? Number.parseInt(daysFlag.split('=')[1], 10) : 60;
|
|
1387
|
+
const result = pruneBackups(process.cwd(), { maxDays });
|
|
1388
|
+
info(`Pruned ${result.pruned.length} backup file(s) older than ${maxDays}d (kept ${result.kept}).`);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (sub === 'resolve') {
|
|
1393
|
+
const category = args[1];
|
|
1394
|
+
const name = args[2];
|
|
1395
|
+
if (!category || !name) { errorln('Usage: construct overrides resolve <category> <name>'); process.exit(1); }
|
|
1396
|
+
const r = resolveOverride(process.cwd(), category, name);
|
|
1397
|
+
println(JSON.stringify(r, null, 2));
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
errorln('Usage: construct overrides <list|backups|prune|resolve>');
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
async function cmdPricing(args) {
|
|
1406
|
+
const sub = args[0] || 'status';
|
|
1407
|
+
const {
|
|
1408
|
+
describePricingCatalogFreshness,
|
|
1409
|
+
refreshPricingCatalog,
|
|
1410
|
+
checkPricingDivergence,
|
|
1411
|
+
resolveModelPricing,
|
|
1412
|
+
} = await import('../lib/telemetry/model-pricing-catalog.mjs');
|
|
1413
|
+
|
|
1414
|
+
if (sub === 'status') {
|
|
1415
|
+
const f = describePricingCatalogFreshness();
|
|
1416
|
+
println(JSON.stringify(f, null, 2));
|
|
1417
|
+
const tiers = ['CX_MODEL_REASONING', 'CX_MODEL_STANDARD', 'CX_MODEL_FAST'];
|
|
1418
|
+
println('');
|
|
1419
|
+
println('Resolved pricing per tier:');
|
|
1420
|
+
for (const env of tiers) {
|
|
1421
|
+
const model = process.env[env];
|
|
1422
|
+
if (!model) { println(` ${env}: (unset)`); continue; }
|
|
1423
|
+
const p = resolveModelPricing(model);
|
|
1424
|
+
if (!p) { println(` ${env} (${model}): unavailable`); continue; }
|
|
1425
|
+
println(` ${env} (${model}): in $${p.inputPrice.toFixed(8)}/tok · out $${p.outputPrice.toFixed(8)}/tok · source: ${p.source}`);
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (sub === 'refresh') {
|
|
1431
|
+
const force = args.includes('--force');
|
|
1432
|
+
info(`Refreshing LiteLLM pricing catalog${force ? ' (force)' : ''}…`);
|
|
1433
|
+
const result = await refreshPricingCatalog(globalThis.fetch, { force });
|
|
1434
|
+
info(`Fetched ${result.fetched} models.`);
|
|
1435
|
+
if (result.divergences?.length) {
|
|
1436
|
+
println('');
|
|
1437
|
+
println(`⚠ ${result.divergences.length} model(s) diverge >25% from static fallback:`);
|
|
1438
|
+
for (const d of result.divergences) println(` ${d.message}`);
|
|
1439
|
+
} else {
|
|
1440
|
+
info('No pricing divergences from static fallback (>25%).');
|
|
1441
|
+
}
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (sub === 'check' || sub === 'divergence') {
|
|
1446
|
+
const f = describePricingCatalogFreshness();
|
|
1447
|
+
if (!f.present) { info(f.message); return; }
|
|
1448
|
+
try {
|
|
1449
|
+
const cached = JSON.parse(fs.readFileSync(path.join(HOME, '.cx', 'pricing-cache.json'), 'utf8'));
|
|
1450
|
+
const divergences = checkPricingDivergence(cached.models || []);
|
|
1451
|
+
if (divergences.length === 0) { info('No pricing divergences (>25%) between LiteLLM and the static fallback.'); return; }
|
|
1452
|
+
println(`⚠ ${divergences.length} divergence(s):`);
|
|
1453
|
+
for (const d of divergences) println(` ${d.message}`);
|
|
1454
|
+
process.exit(2);
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
errorln(`pricing cache read failed: ${err.message}`);
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
errorln(`Usage: construct pricing <status|refresh|check>`);
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
async function cmdHeadhunt(args) { await runHeadhunt({ args, cwd: process.cwd(), homeDir: HOME }); }
|
|
1466
|
+
async function cmdHosts() { printHostCapabilities(); }
|
|
1467
|
+
async function cmdSetup(args) { await runSetup({ rootDir: ROOT_DIR, args, homeDir: HOME }); }
|
|
1468
|
+
|
|
1469
|
+
function printConfigModeHelp() {
|
|
1470
|
+
println('Construct config — inspect and set deployment posture.');
|
|
1471
|
+
println('');
|
|
1472
|
+
println('Usage:');
|
|
1473
|
+
println(' construct config Show active deployment mode and resource topology.');
|
|
1474
|
+
println(' construct config mode Print the active deployment mode.');
|
|
1475
|
+
println(' construct config set deployment.mode <m> Set mode to solo | team | enterprise (writes construct.config.json).');
|
|
1476
|
+
println(' construct config get deployment.mode Read the configured mode from construct.config.json.');
|
|
1477
|
+
println('');
|
|
1478
|
+
println('Notes:');
|
|
1479
|
+
println(' • Single setter: `construct config set` is the only way to persist mode changes.');
|
|
1480
|
+
println(' • Env var CX_DEPLOYMENT_MODE still wins at runtime for one-off overrides (export it for the session).');
|
|
1481
|
+
println('');
|
|
1482
|
+
println('Modes:');
|
|
1483
|
+
for (const mode of DEPLOYMENT_MODES) {
|
|
1484
|
+
println(` ${mode.padEnd(11)} ${describeDeploymentMode(mode)}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function printActiveConfig() {
|
|
1489
|
+
const mode = getDeploymentMode(process.env);
|
|
1490
|
+
const topology = resolveResourceMode(mode);
|
|
1491
|
+
println(`Deployment mode: ${mode}`);
|
|
1492
|
+
println(` ${describeDeploymentMode(mode)}`);
|
|
1493
|
+
println('');
|
|
1494
|
+
println('Resource topology:');
|
|
1495
|
+
for (const [key, value] of Object.entries(topology)) {
|
|
1496
|
+
println(` ${key.padEnd(11)} ${value}`);
|
|
1497
|
+
}
|
|
1498
|
+
println('');
|
|
1499
|
+
println(`Persisted at: ${getUserEnvPath(HOME)} (${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function printIntakeHelp() {
|
|
1503
|
+
println('Construct intake — inspect and process the R&D intake queue.');
|
|
1504
|
+
println('');
|
|
1505
|
+
println('Usage:');
|
|
1506
|
+
println(' construct intake list List pending intake packets (ID, type, stage, owner, action).');
|
|
1507
|
+
println(' construct intake show <id> Show a single packet: triage, source, excerpt, related docs.');
|
|
1508
|
+
println(' construct intake done <id> [--notes=…] Mark a pending packet processed.');
|
|
1509
|
+
println(' construct intake skip <id> [--reason=…] Mark a pending packet skipped (preserves audit trail).');
|
|
1510
|
+
println(' construct intake reopen <id> Move a processed or skipped packet back to pending.');
|
|
1511
|
+
println(' construct intake metrics Show pipeline volume, velocity, and processing stats.');
|
|
1512
|
+
println(' construct intake integrate <id> <system> Push intake to external system (github, jira, confluence).');
|
|
1513
|
+
println(' construct intake integrate <id> <system> --create-issue Create an issue from the intake packet.');
|
|
1514
|
+
println(' construct intake config Show current watch dirs + scan depth.');
|
|
1515
|
+
println(' construct intake config set --depth=N [--add-dir=PATH] [--remove-dir=PATH]');
|
|
1516
|
+
println(' Update intake config. --depth caps at the hard limit.');
|
|
1517
|
+
println(' construct intake config set --include-project-inbox=on|off');
|
|
1518
|
+
println(' construct intake config set --include-docs-intake=on|off');
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function parseKvFlags(args, keys) {
|
|
1522
|
+
const out = {};
|
|
1523
|
+
for (const arg of args) {
|
|
1524
|
+
for (const key of keys) {
|
|
1525
|
+
const prefix = `--${key}=`;
|
|
1526
|
+
if (arg.startsWith(prefix)) out[key] = arg.slice(prefix.length);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return out;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function formatIntakeTable(rows) {
|
|
1533
|
+
if (rows.length === 0) {
|
|
1534
|
+
println('No pending intake packets.');
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const headers = ['ID', 'Type', 'Stage', 'Owner', 'Action'];
|
|
1538
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] || '').length)));
|
|
1539
|
+
const fmt = (cells) => cells.map((c, i) => String(c || '').padEnd(widths[i])).join(' ');
|
|
1540
|
+
println(fmt(headers));
|
|
1541
|
+
println(widths.map((w) => '─'.repeat(w)).join(' '));
|
|
1542
|
+
for (const row of rows) println(fmt(row));
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async function cmdIntake(args) {
|
|
1546
|
+
const sub = args[0];
|
|
1547
|
+
|
|
1548
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
1549
|
+
printIntakeHelp();
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
1554
|
+
const cwd = process.cwd();
|
|
1555
|
+
const queue = createIntakeQueue(cwd, process.env);
|
|
1556
|
+
|
|
1557
|
+
if (sub === 'list') {
|
|
1558
|
+
const pending = queue.listPending();
|
|
1559
|
+
const rows = pending.map((p) => {
|
|
1560
|
+
const t = p.triage || {};
|
|
1561
|
+
return [p.id, t.intakeType || 'unknown', t.rdStage || 'unknown', t.primaryOwner || '—', t.recommendedAction || '—'];
|
|
1562
|
+
});
|
|
1563
|
+
formatIntakeTable(rows);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const id = args[1];
|
|
1568
|
+
if (sub === 'show') {
|
|
1569
|
+
if (!id) { errorln('Usage: construct intake show <id>'); process.exit(1); }
|
|
1570
|
+
const entry = queue.read(id);
|
|
1571
|
+
if (!entry) { errorln(`No intake packet with id: ${id}`); process.exit(1); }
|
|
1572
|
+
const t = entry.triage || {};
|
|
1573
|
+
println(`ID: ${entry.id}`);
|
|
1574
|
+
println(`Status: ${entry.status}`);
|
|
1575
|
+
println(`Source: ${entry.intake?.sourcePath || '(none)'}`);
|
|
1576
|
+
if (entry.processedAt) println(`Processed: ${entry.processedAt} by ${entry.processedBy || 'unknown'}`);
|
|
1577
|
+
if (entry.skippedAt) println(`Skipped: ${entry.skippedAt} by ${entry.skippedBy || 'unknown'}${entry.reason ? ` — ${entry.reason}` : ''}`);
|
|
1578
|
+
println('');
|
|
1579
|
+
println('Triage:');
|
|
1580
|
+
println(` intakeType: ${t.intakeType || 'unknown'}`);
|
|
1581
|
+
println(` rdStage: ${t.rdStage || 'unknown'}`);
|
|
1582
|
+
println(` primaryOwner: ${t.primaryOwner || '—'}`);
|
|
1583
|
+
println(` recommendedChain: ${(t.recommendedChain || []).join(' → ') || '—'}`);
|
|
1584
|
+
println(` recommendedAction: ${t.recommendedAction || '—'}`);
|
|
1585
|
+
println(` risk: ${t.risk || '—'}`);
|
|
1586
|
+
println(` requiresApproval: ${t.requiresApproval ? 'yes' : 'no'}`);
|
|
1587
|
+
println(` confidence: ${typeof t.confidence === 'number' ? t.confidence.toFixed(2) : '—'}`);
|
|
1588
|
+
if (t.rationale) println(` rationale: ${t.rationale}`);
|
|
1589
|
+
if (entry.customers?.length > 0) {
|
|
1590
|
+
println('');
|
|
1591
|
+
println(`Linked customers: ${entry.customers.join(', ')}`);
|
|
1592
|
+
}
|
|
1593
|
+
if (entry.externalRefs) {
|
|
1594
|
+
println('');
|
|
1595
|
+
println('External references:');
|
|
1596
|
+
for (const [system, ref] of Object.entries(entry.externalRefs)) {
|
|
1597
|
+
println(` ${system}: ${ref.url} (id: ${ref.id})`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (entry.suggestion?.lane) {
|
|
1601
|
+
println('');
|
|
1602
|
+
println(`Suggested lane: ${entry.suggestion.lane} (${entry.suggestion.source})`);
|
|
1603
|
+
}
|
|
1604
|
+
if ((entry.related || []).length > 0) {
|
|
1605
|
+
println('');
|
|
1606
|
+
println('Related artifacts:');
|
|
1607
|
+
for (const r of entry.related) {
|
|
1608
|
+
const score = typeof r.score === 'number' ? ` · score ${r.score.toFixed(2)}` : '';
|
|
1609
|
+
println(` - ${r.path || r.title}${score}`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (entry.excerpt) {
|
|
1613
|
+
println('');
|
|
1614
|
+
println('Excerpt:');
|
|
1615
|
+
for (const line of String(entry.excerpt).split('\n').slice(0, 12)) println(` ${line}`);
|
|
1616
|
+
}
|
|
1617
|
+
// Show similar items if there are multiple pending packets
|
|
1618
|
+
try {
|
|
1619
|
+
const pendingDir = path.join(cwd || process.cwd(), '.cx', 'intake', 'pending');
|
|
1620
|
+
if (fs.existsSync(pendingDir)) {
|
|
1621
|
+
const allPackets = [];
|
|
1622
|
+
for (const f of fs.readdirSync(pendingDir)) {
|
|
1623
|
+
if (!f.endsWith('.json')) continue;
|
|
1624
|
+
if (f === `${entry.id}.json`) continue;
|
|
1625
|
+
try {
|
|
1626
|
+
const pkt = JSON.parse(fs.readFileSync(path.join(pendingDir, f), 'utf8'));
|
|
1627
|
+
allPackets.push({ id: f.replace('.json', ''), text: (pkt.excerpt || '') + ' ' + (pkt.triage?.intakeType || '') });
|
|
1628
|
+
} catch { /* skip */ }
|
|
1629
|
+
}
|
|
1630
|
+
if (allPackets.length > 0) {
|
|
1631
|
+
println('');
|
|
1632
|
+
println('Similar signals (semantic):');
|
|
1633
|
+
println(' (run `construct intake list` to see all pending)');
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
} catch { /* dir not available */ }
|
|
1637
|
+
|
|
1638
|
+
// Show active recommendations related to this packet
|
|
1639
|
+
try {
|
|
1640
|
+
const { listActiveRecommendations } = await import('../lib/embed/recommendation-store.mjs');
|
|
1641
|
+
const active = listActiveRecommendations({ limit: 5 });
|
|
1642
|
+
if (active.length > 0) {
|
|
1643
|
+
println('');
|
|
1644
|
+
println('Active recommendations:');
|
|
1645
|
+
for (const rec of active) {
|
|
1646
|
+
println(` ${rec.priority} ${rec.type.toUpperCase()}: ${rec.title} (${rec.totalSignalCount} signal(s) · score ${rec.score})`);
|
|
1647
|
+
}
|
|
1648
|
+
println(' Run `construct recommendations list` for details');
|
|
1649
|
+
}
|
|
1650
|
+
} catch { /* recommendations not available */ }
|
|
1651
|
+
|
|
1652
|
+
println('');
|
|
1653
|
+
println('Next steps:');
|
|
1654
|
+
println(` construct graph from-intake ${entry.id} Generate task graph from triage`);
|
|
1655
|
+
if (entry.suggestion?.lane) {
|
|
1656
|
+
println(` Edit docs/${entry.suggestion.lane}/ Review/update the suggested lane`);
|
|
1657
|
+
}
|
|
1658
|
+
println(` construct intake done ${entry.id} Mark as processed`);
|
|
1659
|
+
println(` construct intake skip ${entry.id} Mark as skipped`);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (sub === 'done') {
|
|
1664
|
+
if (!id) { errorln('Usage: construct intake done <id> [--notes=…]'); process.exit(1); }
|
|
1665
|
+
const flags = parseKvFlags(args.slice(2), ['notes']);
|
|
1666
|
+
try {
|
|
1667
|
+
const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes: flags.notes || '' });
|
|
1668
|
+
info(`Marked ${r.id} processed.`);
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
errorln(err.message || 'mark processed failed');
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (sub === 'skip') {
|
|
1677
|
+
if (!id) { errorln('Usage: construct intake skip <id> [--reason=…]'); process.exit(1); }
|
|
1678
|
+
const flags = parseKvFlags(args.slice(2), ['reason']);
|
|
1679
|
+
try {
|
|
1680
|
+
const r = queue.markSkipped(id, { skippedBy: 'construct-intake-cli', reason: flags.reason || '' });
|
|
1681
|
+
info(`Marked ${r.id} skipped.`);
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
errorln(err.message || 'mark skipped failed');
|
|
1684
|
+
process.exit(1);
|
|
1685
|
+
}
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (sub === 'reopen') {
|
|
1690
|
+
if (!id) { errorln('Usage: construct intake reopen <id>'); process.exit(1); }
|
|
1691
|
+
try {
|
|
1692
|
+
const r = queue.reopen(id);
|
|
1693
|
+
info(`Reopened ${r.id} (from ${r.from}).`);
|
|
1694
|
+
} catch (err) {
|
|
1695
|
+
errorln(err.message || 'reopen failed');
|
|
1696
|
+
process.exit(1);
|
|
1697
|
+
}
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if (sub === 'metrics') {
|
|
1702
|
+
return await cmdIntakeMetrics();
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
if (sub === 'integrate') {
|
|
1706
|
+
return await cmdIntakeIntegrate(id, args.slice(2), cwd);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (sub === 'config') {
|
|
1710
|
+
return await cmdIntakeConfig(args.slice(1));
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
errorln(`Unknown intake subcommand: ${sub}. Available: list, show, done, skip, reopen, integrate, config`);
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
async function cmdIntakeConfig(args) {
|
|
1718
|
+
const { loadIntakeConfig, saveIntakeConfig, INTAKE_DEPTH_GUIDANCE, INTAKE_HARD_MAX_DEPTH, describeIntakeDepth } =
|
|
1719
|
+
await import('../lib/intake/intake-config.mjs');
|
|
1720
|
+
const cwd = process.cwd();
|
|
1721
|
+
|
|
1722
|
+
const action = args[0];
|
|
1723
|
+
if (!action || action === 'show') {
|
|
1724
|
+
const cfg = loadIntakeConfig(cwd, process.env);
|
|
1725
|
+
const desc = describeIntakeDepth(cfg.maxDepth);
|
|
1726
|
+
println(`Intake config (root: ${cwd})`);
|
|
1727
|
+
println('');
|
|
1728
|
+
println(` includeProjectInbox: ${cfg.includeProjectInbox ? 'on' : 'off'} (.cx/inbox)`);
|
|
1729
|
+
println(` includeDocsIntake: ${cfg.includeDocsIntake ? 'on' : 'off'} (docs/intake)`);
|
|
1730
|
+
println(` maxDepth: ${cfg.maxDepth} — ${desc.label}`);
|
|
1731
|
+
println(` ${desc.detail}`);
|
|
1732
|
+
println(` hardMaxDepth: ${INTAKE_HARD_MAX_DEPTH}`);
|
|
1733
|
+
if (cfg.parentDirs.length === 0) {
|
|
1734
|
+
println(' parentDirs: (none — only the built-in zones are watched)');
|
|
1735
|
+
} else {
|
|
1736
|
+
println(' parentDirs:');
|
|
1737
|
+
for (const dir of cfg.parentDirs) println(` - ${dir}`);
|
|
1738
|
+
}
|
|
1739
|
+
println('');
|
|
1740
|
+
println('Depth guide:');
|
|
1741
|
+
for (const g of INTAKE_DEPTH_GUIDANCE) {
|
|
1742
|
+
println(` ${String(g.value).padStart(2)} ${g.label} — ${g.detail}`);
|
|
1743
|
+
}
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (action !== 'set') {
|
|
1748
|
+
errorln(`Unknown intake config action: ${action}. Available: show, set`);
|
|
1749
|
+
process.exit(1);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
const patch = {};
|
|
1753
|
+
const tail = args.slice(1);
|
|
1754
|
+
const depthFlag = tail.find((a) => a.startsWith('--depth='));
|
|
1755
|
+
if (depthFlag) patch.maxDepth = Number(depthFlag.split('=')[1]);
|
|
1756
|
+
|
|
1757
|
+
const projectFlag = tail.find((a) => a.startsWith('--include-project-inbox='));
|
|
1758
|
+
if (projectFlag) patch.includeProjectInbox = /^(on|true|1|yes)$/i.test(projectFlag.split('=')[1] || '');
|
|
1759
|
+
|
|
1760
|
+
const docsFlag = tail.find((a) => a.startsWith('--include-docs-intake='));
|
|
1761
|
+
if (docsFlag) patch.includeDocsIntake = /^(on|true|1|yes)$/i.test(docsFlag.split('=')[1] || '');
|
|
1762
|
+
|
|
1763
|
+
const addFlags = tail.filter((a) => a.startsWith('--add-dir='));
|
|
1764
|
+
const removeFlags = tail.filter((a) => a.startsWith('--remove-dir='));
|
|
1765
|
+
|
|
1766
|
+
if (addFlags.length || removeFlags.length) {
|
|
1767
|
+
const current = loadIntakeConfig(cwd, process.env);
|
|
1768
|
+
const toAdd = addFlags.map((a) => a.split('=').slice(1).join('=')).filter(Boolean);
|
|
1769
|
+
const toRemove = new Set(removeFlags.map((a) => a.split('=').slice(1).join('=')).filter(Boolean));
|
|
1770
|
+
const next = [...current.parentDirs.filter((d) => !toRemove.has(d)), ...toAdd];
|
|
1771
|
+
patch.parentDirs = Array.from(new Set(next));
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
if (Object.keys(patch).length === 0) {
|
|
1775
|
+
errorln('Nothing to change. Pass --depth=N, --add-dir=PATH, --remove-dir=PATH, --include-project-inbox=on|off, or --include-docs-intake=on|off.');
|
|
1776
|
+
process.exit(1);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
try {
|
|
1780
|
+
const saved = saveIntakeConfig(cwd, patch);
|
|
1781
|
+
println('Saved intake config:');
|
|
1782
|
+
println(JSON.stringify(saved, null, 2));
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
errorln(err.message || 'failed to save intake config');
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
async function cmdIntakeMetrics() {
|
|
1790
|
+
try {
|
|
1791
|
+
const { computeIntakeMetrics, pendingAge } = await import('../lib/embed/intake-metrics.mjs');
|
|
1792
|
+
const metrics = computeIntakeMetrics({ rootDir: cwd });
|
|
1793
|
+
const age = pendingAge({ rootDir: cwd });
|
|
1794
|
+
|
|
1795
|
+
println(`${COLORS.bold}Intake pipeline metrics:${COLORS.reset}`);
|
|
1796
|
+
println('');
|
|
1797
|
+
println(` ${COLORS.cyan}Volume${COLORS.reset}`);
|
|
1798
|
+
println(` Pending: ${metrics.volume.pending}`);
|
|
1799
|
+
println(` Processed: ${metrics.volume.processed}`);
|
|
1800
|
+
println(` Skipped: ${metrics.volume.skipped}`);
|
|
1801
|
+
println(` Total: ${metrics.volume.total}`);
|
|
1802
|
+
println('');
|
|
1803
|
+
println(` ${COLORS.cyan}Velocity${COLORS.reset}`);
|
|
1804
|
+
println(` Rate: ${metrics.velocity} items/day (trailing 7d)`);
|
|
1805
|
+
if (metrics.avgProcessingTimeFormatted) {
|
|
1806
|
+
println(` Avg process: ${metrics.avgProcessingTimeFormatted}`);
|
|
1807
|
+
}
|
|
1808
|
+
if (metrics.acceptRate !== null) {
|
|
1809
|
+
println(` Accept rate: ${metrics.acceptRate}%`);
|
|
1810
|
+
}
|
|
1811
|
+
println('');
|
|
1812
|
+
println(` ${COLORS.cyan}Customer intelligence${COLORS.reset}`);
|
|
1813
|
+
println(` Customer-linked items: ${metrics.customerLinked}`);
|
|
1814
|
+
println('');
|
|
1815
|
+
println(` ${COLORS.cyan}Recommendations${COLORS.reset}`);
|
|
1816
|
+
println(` Active: ${metrics.recommendations.active}`);
|
|
1817
|
+
println(` Dismissed: ${metrics.recommendations.dismissed}`);
|
|
1818
|
+
println(` Total: ${metrics.recommendations.total}`);
|
|
1819
|
+
println('');
|
|
1820
|
+
println(` ${COLORS.cyan}Backlog${COLORS.reset}`);
|
|
1821
|
+
println(` Pending items: ${age.count}`);
|
|
1822
|
+
if (age.oldest) {
|
|
1823
|
+
println(` Oldest: ${age.oldest} (${age.ageHours}h old)`);
|
|
1824
|
+
println(` Stale (>24h): ${age.oldItemsCount}`);
|
|
1825
|
+
}
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
errorln(`Intake metrics error: ${err.message}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
async function cmdIntakeIntegrate(id, args, cwd) {
|
|
1832
|
+
if (!id) { errorln('Usage: construct intake integrate <id> <github|jira|confluence>'); process.exit(1); }
|
|
1833
|
+
|
|
1834
|
+
const system = args[0];
|
|
1835
|
+
if (!system || !['github', 'jira', 'confluence'].includes(system)) {
|
|
1836
|
+
errorln('Usage: construct intake integrate <id> <github|jira|confluence>');
|
|
1837
|
+
errorln('Available systems: github, jira, confluence');
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
1842
|
+
const queue = createIntakeQueue(cwd, process.env);
|
|
1843
|
+
const entry = queue.read(id);
|
|
1844
|
+
if (!entry) { errorln(`No intake packet with id: ${id}`); process.exit(1); }
|
|
1845
|
+
|
|
1846
|
+
let result;
|
|
1847
|
+
|
|
1848
|
+
if (system === 'github') {
|
|
1849
|
+
const { createGitHubIssue, tagIntakeWithExternalRef } = await import('../lib/integrations/intake-integrations.mjs');
|
|
1850
|
+
info(`Creating GitHub issue from intake ${id}...`);
|
|
1851
|
+
result = await createGitHubIssue(entry);
|
|
1852
|
+
if (result.ok) {
|
|
1853
|
+
ok(`GitHub issue created: ${result.externalUrl}`);
|
|
1854
|
+
tagIntakeWithExternalRef(cwd, id, 'github', result.externalUrl, result.externalId);
|
|
1855
|
+
} else {
|
|
1856
|
+
errorln(`Failed: ${result.error}`);
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
if (system === 'jira') {
|
|
1862
|
+
const { createJiraTicket, tagIntakeWithExternalRef } = await import('../lib/integrations/intake-integrations.mjs');
|
|
1863
|
+
info(`Creating Jira ticket from intake ${id}...`);
|
|
1864
|
+
result = await createJiraTicket(entry);
|
|
1865
|
+
if (result.ok) {
|
|
1866
|
+
ok(`Jira ticket created: ${result.externalUrl}`);
|
|
1867
|
+
tagIntakeWithExternalRef(cwd, id, 'jira', result.externalUrl, result.externalId);
|
|
1868
|
+
} else {
|
|
1869
|
+
errorln(`Failed: ${result.error}`);
|
|
1870
|
+
process.exit(1);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (system === 'confluence') {
|
|
1875
|
+
const { publishArtifactToConfluence, tagIntakeWithExternalRef } = await import('../lib/integrations/intake-integrations.mjs');
|
|
1876
|
+
const artifact = {
|
|
1877
|
+
type: entry?.triage?.intakeType === 'requirement' ? 'prd' : 'adr',
|
|
1878
|
+
number: 1,
|
|
1879
|
+
title: entry?.excerpt?.slice(0, 60) || `Intake ${id}`,
|
|
1880
|
+
content: `# Intake: ${id}\n\n${entry?.excerpt || ''}\n\nSee also: ${entry?.intake?.sourcePath || ''}`,
|
|
1881
|
+
};
|
|
1882
|
+
info(`Publishing Confluence page from intake ${id}...`);
|
|
1883
|
+
result = await publishArtifactToConfluence(artifact);
|
|
1884
|
+
if (result.ok) {
|
|
1885
|
+
ok(`Confluence page: ${result.externalUrl}`);
|
|
1886
|
+
tagIntakeWithExternalRef(cwd, id, 'confluence', result.externalUrl, result.externalId);
|
|
1887
|
+
} else {
|
|
1888
|
+
errorln(`Failed: ${result.error}`);
|
|
1889
|
+
process.exit(1);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function printGraphHelp() {
|
|
1895
|
+
println('Construct graph — inspect and generate task graphs derived from R&D intake.');
|
|
1896
|
+
println('');
|
|
1897
|
+
println('Usage:');
|
|
1898
|
+
println(' construct graph list List all task graphs in this project.');
|
|
1899
|
+
println(' construct graph show <graph-id> Print a graph: nodes, edges, status, verification.');
|
|
1900
|
+
println(' construct graph from-intake <intake-id> Generate a task graph from a pending intake packet.');
|
|
1901
|
+
println(' construct graph status <graph-id> <node-id> <status> [--evidence=text]');
|
|
1902
|
+
println(' Update a node status. Valid: pending, claimed, in-progress, done, blocked, needs-input, skipped.');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
async function cmdIntegrations(args) {
|
|
1906
|
+
const sub = args[0];
|
|
1907
|
+
if (sub === '--help' || sub === '-h') {
|
|
1908
|
+
println('construct integrations — check and manage external system integrations.');
|
|
1909
|
+
println('');
|
|
1910
|
+
println('Usage:');
|
|
1911
|
+
println(' construct integrations status Check which integrations are configured');
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
if (sub === 'status') {
|
|
1916
|
+
const { detectIntegrationConfig } = await import('../lib/integrations/intake-integrations.mjs');
|
|
1917
|
+
const config = detectIntegrationConfig();
|
|
1918
|
+
println(`${COLORS.bold}Integration status:${COLORS.reset}`);
|
|
1919
|
+
const ghMethod = config.githubMethod === 'gh' ? ' (gh CLI)' : config.githubMethod === 'env' ? ' (env vars)' : '';
|
|
1920
|
+
const ghLabel = `GitHub Issues${ghMethod}`;
|
|
1921
|
+
if (config.github) {
|
|
1922
|
+
println(` ${COLORS.green}✓${COLORS.reset} ${ghLabel} — repo: ${config.githubRepo}`);
|
|
1923
|
+
} else {
|
|
1924
|
+
println(` ${COLORS.yellow}⚠${COLORS.reset} GitHub Issues not configured (run gh auth login, or set GITHUB_TOKEN)`);
|
|
1925
|
+
}
|
|
1926
|
+
println(` ${config.jira ? COLORS.green + '✓' : COLORS.yellow + '⚠'}${COLORS.reset} Jira${config.jira ? ` — ${config.jiraHost}` : ' not configured'}`);
|
|
1927
|
+
println(` ${config.confluence ? COLORS.green + '✓' : COLORS.yellow + '⚠'}${COLORS.reset} Confluence${config.confluence ? ` — ${config.confluenceSpace}` : ' not configured'}`);
|
|
1928
|
+
println('');
|
|
1929
|
+
println('Auth methods (tried in order):');
|
|
1930
|
+
println(' GitHub: gh CLI → GITHUB_TOKEN env → .env → config.env → 1Password');
|
|
1931
|
+
println(' Jira/Conf: JIRA_/CONFLUENCE_ env → ~/.env → config.env → shell rc → 1Password');
|
|
1932
|
+
println('');
|
|
1933
|
+
println('Configure: construct intake integrate <id> <github|jira|confluence>');
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
errorln(`Unknown subcommand: ${sub}. Available: status`);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function printCustomerHelp() {
|
|
1941
|
+
println('');
|
|
1942
|
+
println('Construct customer — manage customer profiles for product intelligence.');
|
|
1943
|
+
println('');
|
|
1944
|
+
println('Usage:');
|
|
1945
|
+
println(' construct customer list List all customer profiles');
|
|
1946
|
+
println(' construct customer show <id> Show detailed profile');
|
|
1947
|
+
println(' construct customer add --name=Acme [--owner=Jane] [--domain=acme.com]');
|
|
1948
|
+
println(' construct customer search <query> Search by name, domain, or alias');
|
|
1949
|
+
println(' construct customer update <id> --status=inactive Update profile status');
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
async function cmdCustomer(args) {
|
|
1953
|
+
const sub = args[0];
|
|
1954
|
+
if (!sub || sub === '--help' || sub === '-h') { printCustomerHelp(); return; }
|
|
1955
|
+
|
|
1956
|
+
const { createCustomerProfile, getCustomerProfile, listCustomerProfiles, searchCustomerProfiles, updateCustomerProfile } = await import('../lib/embed/customer-profiles.mjs');
|
|
1957
|
+
|
|
1958
|
+
if (sub === 'list') {
|
|
1959
|
+
const profiles = listCustomerProfiles();
|
|
1960
|
+
if (!profiles.length) { println('No customer profiles. Create one with: construct customer add --name=Acme'); return; }
|
|
1961
|
+
println(`${COLORS.bold}Customer profiles:${COLORS.reset}`);
|
|
1962
|
+
for (const p of profiles) {
|
|
1963
|
+
println(` ${COLORS.cyan}${p.id}${COLORS.reset} ${COLORS.bold}${p.name}${COLORS.reset} [${p.status}] ${p.workspace} updated ${p.updatedAt?.slice(0, 10)}`);
|
|
1964
|
+
}
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
if (sub === 'show') {
|
|
1969
|
+
const id = args[1];
|
|
1970
|
+
if (!id) { errorln('Usage: construct customer show <id>'); process.exit(1); }
|
|
1971
|
+
const profile = getCustomerProfile(id);
|
|
1972
|
+
if (!profile) { errorln(`Customer profile not found: ${id}`); process.exit(1); }
|
|
1973
|
+
println(`ID: ${profile.id}`);
|
|
1974
|
+
println(`Name: ${profile.name}`);
|
|
1975
|
+
println(`Status: ${profile.status}`);
|
|
1976
|
+
println(`Owner: ${profile.owner}`);
|
|
1977
|
+
println(`Workspace: ${profile.workspace}`);
|
|
1978
|
+
println(`Updated: ${profile.updatedAt?.slice(0, 10) || '—'}`);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
if (sub === 'add') {
|
|
1983
|
+
const name = args.find(a => a.startsWith('--name='))?.split('=').slice(1).join('=');
|
|
1984
|
+
const owner = args.find(a => a.startsWith('--owner='))?.split('=').slice(1).join('=');
|
|
1985
|
+
const domain = args.find(a => a.startsWith('--domain='))?.split('=').slice(1).join('=');
|
|
1986
|
+
if (!name) { errorln('Usage: construct customer add --name=Acme [--owner=Jane] [--domain=acme.com]'); process.exit(1); }
|
|
1987
|
+
const result = createCustomerProfile({ name, owner, domain });
|
|
1988
|
+
println(`Created customer profile: ${result.id} — ${result.profile.name}`);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
if (sub === 'search') {
|
|
1993
|
+
const query = args.slice(1).join(' ');
|
|
1994
|
+
if (!query) { errorln('Usage: construct customer search <query>'); process.exit(1); }
|
|
1995
|
+
const results = searchCustomerProfiles(query);
|
|
1996
|
+
if (!results.length) { println('No matches.'); return; }
|
|
1997
|
+
for (const r of results) {
|
|
1998
|
+
println(` ${r.id} ${r.name} [${r.status}]`);
|
|
1999
|
+
}
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
if (sub === 'update') {
|
|
2004
|
+
const id = args[1];
|
|
2005
|
+
const status = args.find(a => a.startsWith('--status='))?.split('=').slice(1).join('=');
|
|
2006
|
+
if (!id || !status) { errorln('Usage: construct customer update <id> --status=active|inactive|archived'); process.exit(1); }
|
|
2007
|
+
updateCustomerProfile(id, { status, changeLogEntry: `Status changed to ${status} via CLI` });
|
|
2008
|
+
println(`Updated: ${id}`);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
errorln(`Unknown customer subcommand: ${sub}. Available: list, show, add, search, update`);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function printWorkspaceHelp() {
|
|
2016
|
+
println('');
|
|
2017
|
+
println('Construct workspace — manage PM workspaces for signal routing.');
|
|
2018
|
+
println('');
|
|
2019
|
+
println('Usage:');
|
|
2020
|
+
println(' construct workspace list List all workspaces');
|
|
2021
|
+
println(' construct workspace create --name=Auth --owner=Jane Create a new workspace');
|
|
2022
|
+
println(' construct workspace show <id> Show workspace details');
|
|
2023
|
+
println(' construct workspace assign --customer=X --workspace=Y Assign customer to workspace');
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
async function cmdWorkspace(args) {
|
|
2027
|
+
const sub = args[0];
|
|
2028
|
+
if (!sub || sub === '--help' || sub === '-h') { printWorkspaceHelp(); return; }
|
|
2029
|
+
|
|
2030
|
+
const { createWorkspace, getWorkspace, listWorkspaces, updateWorkspace } = await import('../lib/embed/workspaces.mjs');
|
|
2031
|
+
|
|
2032
|
+
if (sub === 'list') {
|
|
2033
|
+
const workspaces = listWorkspaces();
|
|
2034
|
+
if (!workspaces.length) { println('No workspaces.'); return; }
|
|
2035
|
+
println(`${COLORS.bold}Workspaces:${COLORS.reset}`);
|
|
2036
|
+
for (const ws of workspaces) {
|
|
2037
|
+
println(` ${COLORS.cyan}${ws.id}${COLORS.reset} ${COLORS.bold}${ws.name}${COLORS.reset} owner: ${ws.owner} [${ws.status}] areas: ${(ws.productAreas || []).join(', ') || '—'}`);
|
|
2038
|
+
}
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
if (sub === 'create') {
|
|
2043
|
+
const name = args.find(a => a.startsWith('--name='))?.split('=').slice(1).join('=');
|
|
2044
|
+
const owner = args.find(a => a.startsWith('--owner='))?.split('=').slice(1).join('=');
|
|
2045
|
+
const areas = args.find(a => a.startsWith('--areas='))?.split('=').slice(1).join('=')?.split(',') || [];
|
|
2046
|
+
if (!name || !owner) { errorln('Usage: construct workspace create --name=Auth --owner=Jane [--areas=auth,users]'); process.exit(1); }
|
|
2047
|
+
const result = createWorkspace({ name, owner, productAreas: areas });
|
|
2048
|
+
println(`Created workspace: ${result.id} — ${result.workspace.name}`);
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (sub === 'show') {
|
|
2053
|
+
const id = args[1];
|
|
2054
|
+
if (!id) { errorln('Usage: construct workspace show <id>'); process.exit(1); }
|
|
2055
|
+
const ws = getWorkspace(id);
|
|
2056
|
+
if (!ws) { errorln(`Workspace not found: ${id}`); process.exit(1); }
|
|
2057
|
+
println(`ID: ${ws.id}`);
|
|
2058
|
+
println(`Name: ${ws.name}`);
|
|
2059
|
+
println(`Owner: ${ws.owner}`);
|
|
2060
|
+
println(`Status: ${ws.status}`);
|
|
2061
|
+
println(`Product areas: ${(ws.productAreas || []).join(', ') || '—'}`);
|
|
2062
|
+
println(`Customers: ${(ws.customerIds || []).join(', ') || '—'}`);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
errorln(`Unknown workspace subcommand: ${sub}. Available: list, create, show`);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function printRecommendationsHelp() {
|
|
2070
|
+
println('');
|
|
2071
|
+
println('Construct recommendations — view and manage artifact recommendations.');
|
|
2072
|
+
println('');
|
|
2073
|
+
println('These are generated by the daemon\'s docs-lifecycle job based on intake');
|
|
2074
|
+
println('signals, snapshot analysis, and doc gap detection. Recommendations are');
|
|
2075
|
+
println('deduplicated, prioritized, and auto-suppressed after 7 days without new signals.');
|
|
2076
|
+
println('');
|
|
2077
|
+
println('Usage:');
|
|
2078
|
+
println(' construct recommendations list [--type=prd] [--priority=P0]');
|
|
2079
|
+
println(' List active recommendations, sorted by priority.');
|
|
2080
|
+
println(' construct recommendations stats Show recommendation store stats.');
|
|
2081
|
+
println(' construct recommendations dismiss <type> <title> [--reason=…]');
|
|
2082
|
+
println(' Permanently dismiss a recommendation.');
|
|
2083
|
+
println(' construct recommendations dismiss <type> <title> --suppress-days=30');
|
|
2084
|
+
println(' Temporarily suppress a recommendation.');
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
async function cmdRecommendations(args) {
|
|
2088
|
+
const sub = args[0];
|
|
2089
|
+
if (!sub || sub === '--help' || sub === '-h') { printRecommendationsHelp(); return; }
|
|
2090
|
+
|
|
2091
|
+
if (sub === 'list') {
|
|
2092
|
+
const typeFlag = args.find(a => a.startsWith('--type='))?.split('=')[1];
|
|
2093
|
+
const priorityFlag = args.find(a => a.startsWith('--priority='))?.split('=')[1];
|
|
2094
|
+
try {
|
|
2095
|
+
const { listActiveRecommendations, recommendationStats } = await import('../lib/embed/recommendation-store.mjs');
|
|
2096
|
+
const recs = listActiveRecommendations({ type: typeFlag, priority: priorityFlag, limit: 30 });
|
|
2097
|
+
if (!recs.length) {
|
|
2098
|
+
println('No active recommendations.');
|
|
2099
|
+
const stats = recommendationStats();
|
|
2100
|
+
println(`(${stats.total} total in store: ${stats.active} active, ${stats.dismissed} dismissed, ${stats.superseded} superseded)`);
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
println(`${COLORS.bold}Active recommendations:${COLORS.reset}`);
|
|
2104
|
+
for (const rec of recs) {
|
|
2105
|
+
const priorityColor = rec.priority === 'P0' ? COLORS.red : rec.priority === 'P1' ? COLORS.yellow : COLORS.reset;
|
|
2106
|
+
println(` ${priorityColor}${rec.priority}${COLORS.reset} ${COLORS.bold}${rec.type.toUpperCase()}${COLORS.reset}: ${rec.title}`);
|
|
2107
|
+
println(` Score: ${rec.score} · Signals: ${rec.totalSignalCount} · Impact: ${rec.customerImpact}/3 · ${rec.reason?.slice(0, 80)}`);
|
|
2108
|
+
}
|
|
2109
|
+
const stats = recommendationStats();
|
|
2110
|
+
println(`\n(${stats.active} active · ${stats.dismissed} dismissed · ${stats.superseded} superseded)`);
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
errorln(`Recommendations error: ${err.message}`);
|
|
2113
|
+
}
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
if (sub === 'stats') {
|
|
2118
|
+
try {
|
|
2119
|
+
const { recommendationStats } = await import('../lib/embed/recommendation-store.mjs');
|
|
2120
|
+
const stats = recommendationStats();
|
|
2121
|
+
println('Recommendation store:');
|
|
2122
|
+
println(` Total: ${stats.total}`);
|
|
2123
|
+
println(` Active: ${stats.active}`);
|
|
2124
|
+
println(` Dismissed: ${stats.dismissed}`);
|
|
2125
|
+
println(` Superseded: ${stats.superseded}`);
|
|
2126
|
+
println(` By priority: ${JSON.stringify(stats.byPriority)}`);
|
|
2127
|
+
println(` By type: ${JSON.stringify(stats.byType)}`);
|
|
2128
|
+
} catch (err) {
|
|
2129
|
+
errorln(`Recommendations stats error: ${err.message}`);
|
|
2130
|
+
}
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (sub === 'dismiss') {
|
|
2135
|
+
const recType = args[1];
|
|
2136
|
+
const recTitle = args[2];
|
|
2137
|
+
if (!recType || !recTitle) {
|
|
2138
|
+
errorln('Usage: construct recommendations dismiss <type> <title> [--reason=…] [--suppress-days=N]');
|
|
2139
|
+
process.exit(1);
|
|
2140
|
+
}
|
|
2141
|
+
const reasonFlag = args.find(a => a.startsWith('--reason='))?.split('=').slice(1).join('=');
|
|
2142
|
+
const suppressDaysFlag = args.find(a => a.startsWith('--suppress-days='))?.split('=')[1];
|
|
2143
|
+
try {
|
|
2144
|
+
const { dedupKey: computeKey } = await import('../lib/embed/recommendation-store.mjs');
|
|
2145
|
+
const key = `${recType}::${recTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60)}`;
|
|
2146
|
+
const { dismissRecommendation } = await import('../lib/embed/recommendation-store.mjs');
|
|
2147
|
+
dismissRecommendation(key, {
|
|
2148
|
+
reason: reasonFlag || 'CLI dismiss',
|
|
2149
|
+
suppressDays: suppressDaysFlag ? Number(suppressDaysFlag) : undefined,
|
|
2150
|
+
});
|
|
2151
|
+
println(`Dismissed: ${recType} / ${recTitle}`);
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
errorln(`Dismiss error: ${err.message}`);
|
|
2154
|
+
}
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
errorln(`Unknown recommendations subcommand: ${sub}. Available: list, stats, dismiss`);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
async function cmdGraph(args) {
|
|
2162
|
+
const sub = args[0];
|
|
2163
|
+
if (!sub || sub === '--help' || sub === '-h') { printGraphHelp(); return; }
|
|
2164
|
+
|
|
2165
|
+
const { FilesystemTaskGraphStore } = await import('../lib/task-graph/store.mjs');
|
|
2166
|
+
const { generateTaskGraphFromTriage } = await import('../lib/task-graph/generate.mjs');
|
|
2167
|
+
const cwd = process.cwd();
|
|
2168
|
+
const store = new FilesystemTaskGraphStore(cwd);
|
|
2169
|
+
|
|
2170
|
+
if (sub === 'list') {
|
|
2171
|
+
const graphs = store.list();
|
|
2172
|
+
if (graphs.length === 0) { println('No task graphs.'); return; }
|
|
2173
|
+
println('Graph ID Nodes Triage');
|
|
2174
|
+
println('-'.repeat(80));
|
|
2175
|
+
for (const g of graphs) {
|
|
2176
|
+
const triage = g.triage ? `${g.triage.intakeType}/${g.triage.rdStage}` : '—';
|
|
2177
|
+
println(`${g.id.padEnd(56)} ${String(g.nodes.length).padStart(5)} ${triage}`);
|
|
2178
|
+
}
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
if (sub === 'show') {
|
|
2183
|
+
const id = args[1];
|
|
2184
|
+
if (!id) { errorln('Usage: construct graph show <graph-id>'); process.exit(1); }
|
|
2185
|
+
const g = store.read(id);
|
|
2186
|
+
if (!g) { errorln(`No graph: ${id}`); process.exit(1); }
|
|
2187
|
+
println(`Graph: ${g.id}`);
|
|
2188
|
+
println(`Created: ${g.createdAt}`);
|
|
2189
|
+
if (g.triage) println(`Triage: ${g.triage.intakeType} / ${g.triage.rdStage} · owner ${g.triage.primaryOwner}`);
|
|
2190
|
+
if (g.intake?.sourcePath) println(`Source: ${g.intake.sourcePath}`);
|
|
2191
|
+
if (g.verificationRequirements?.length) println(`Verification: ${g.verificationRequirements.join(', ')}`);
|
|
2192
|
+
println('');
|
|
2193
|
+
println('Nodes:');
|
|
2194
|
+
for (const node of g.nodes) {
|
|
2195
|
+
const deps = (node.dependsOn || []).length > 0 ? ` ← ${node.dependsOn.length} dep(s)` : '';
|
|
2196
|
+
println(` [${node.status.padEnd(11)}] ${node.title}${deps}`);
|
|
2197
|
+
if ((node.evidence || []).length > 0) {
|
|
2198
|
+
for (const ev of node.evidence) println(` evidence: ${typeof ev === 'string' ? ev : JSON.stringify(ev)}`);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
if ((g.edges || []).length > 0) {
|
|
2202
|
+
println('');
|
|
2203
|
+
println(`Edges: ${g.edges.length}`);
|
|
2204
|
+
}
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
if (sub === 'from-intake') {
|
|
2209
|
+
const intakeId = args[1];
|
|
2210
|
+
if (!intakeId) { errorln('Usage: construct graph from-intake <intake-id>'); process.exit(1); }
|
|
2211
|
+
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
2212
|
+
const queue = createIntakeQueue(cwd, process.env);
|
|
2213
|
+
const entry = queue.read(intakeId);
|
|
2214
|
+
if (!entry) { errorln(`No intake packet: ${intakeId}`); process.exit(1); }
|
|
2215
|
+
const graph = generateTaskGraphFromTriage({
|
|
2216
|
+
triage: entry.triage,
|
|
2217
|
+
project: path.basename(path.resolve(cwd)),
|
|
2218
|
+
request: entry.excerpt || '',
|
|
2219
|
+
intake: entry.intake,
|
|
2220
|
+
});
|
|
2221
|
+
const r = store.save(graph);
|
|
2222
|
+
info(`Generated ${graph.nodes.length}-node graph ${r.id}`);
|
|
2223
|
+
println(`Wrote: ${r.filePath}`);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
if (sub === 'status') {
|
|
2228
|
+
const graphId = args[1];
|
|
2229
|
+
const nodeId = args[2];
|
|
2230
|
+
const status = args[3];
|
|
2231
|
+
if (!graphId || !nodeId || !status) {
|
|
2232
|
+
errorln('Usage: construct graph status <graph-id> <node-id> <status> [--evidence=…]');
|
|
2233
|
+
process.exit(1);
|
|
2234
|
+
}
|
|
2235
|
+
const flags = parseKvFlags(args.slice(4), ['evidence']);
|
|
2236
|
+
try {
|
|
2237
|
+
const patch = flags.evidence ? { addEvidence: flags.evidence } : {};
|
|
2238
|
+
const updated = store.updateNodeStatus(graphId, nodeId, status, patch);
|
|
2239
|
+
info(`Updated ${nodeId} → ${updated.status}`);
|
|
2240
|
+
} catch (err) {
|
|
2241
|
+
errorln(err.message || 'status update failed');
|
|
2242
|
+
process.exit(1);
|
|
2243
|
+
}
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
errorln(`Unknown graph subcommand: ${sub}. Available: list, show, from-intake, status`);
|
|
2248
|
+
process.exit(1);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
async function cmdConfig(args) {
|
|
2252
|
+
const sub = args[0];
|
|
2253
|
+
|
|
2254
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
2255
|
+
if (!sub) { printActiveConfig(); return; }
|
|
2256
|
+
printConfigModeHelp();
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
if (sub === 'mode') {
|
|
2261
|
+
const value = args[1];
|
|
2262
|
+
|
|
2263
|
+
if (!value) {
|
|
2264
|
+
println(getDeploymentMode(process.env));
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
if (value === '--help' || value === '-h') { printConfigModeHelp(); return; }
|
|
2269
|
+
|
|
2270
|
+
// Setter consolidated into `construct config set`. Refuse here with a one-line redirect
|
|
2271
|
+
// so users learn the single canonical setter instead of two slightly-divergent paths.
|
|
2272
|
+
errorln(`\`construct config mode <value>\` is no longer a setter. Use the single canonical path:`);
|
|
2273
|
+
errorln(` construct config set deployment.mode ${value}`);
|
|
2274
|
+
errorln('');
|
|
2275
|
+
errorln('For ephemeral runtime overrides (CI, scripts), export CX_DEPLOYMENT_MODE instead.');
|
|
2276
|
+
process.exit(1);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
if (sub === 'init') {
|
|
2280
|
+
try {
|
|
2281
|
+
const filePath = initProjectConfig(process.cwd());
|
|
2282
|
+
info(`Created ${filePath}`);
|
|
2283
|
+
} catch (err) {
|
|
2284
|
+
errorln(err.message);
|
|
2285
|
+
process.exit(1);
|
|
2286
|
+
}
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
if (sub === 'show') {
|
|
2291
|
+
const { path: cfgPath, config, source, errors } = loadProjectConfig(process.cwd(), process.env);
|
|
2292
|
+
if (errors.length) {
|
|
2293
|
+
errorln(`Config has ${errors.length} error${errors.length === 1 ? '' : 's'}:`);
|
|
2294
|
+
for (const e of errors) errorln(` - ${e}`);
|
|
2295
|
+
}
|
|
2296
|
+
println(`# source: ${source}${cfgPath ? ` (${cfgPath})` : ''}`);
|
|
2297
|
+
println(JSON.stringify(config, null, 2));
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
if (sub === 'get') {
|
|
2302
|
+
const keyPath = args[1];
|
|
2303
|
+
if (!keyPath) { errorln('Usage: construct config get <dotted.key>'); process.exit(1); }
|
|
2304
|
+
const { config } = loadProjectConfig(process.cwd(), process.env);
|
|
2305
|
+
const value = getConfigValue(config, keyPath, null);
|
|
2306
|
+
println(value === null ? '' : (typeof value === 'string' ? value : JSON.stringify(value)));
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (sub === 'set') {
|
|
2311
|
+
const keyPath = args[1];
|
|
2312
|
+
const rawValue = args[2];
|
|
2313
|
+
if (!keyPath || rawValue === undefined) {
|
|
2314
|
+
errorln('Usage: construct config set <dotted.key> <value>');
|
|
2315
|
+
process.exit(1);
|
|
2316
|
+
}
|
|
2317
|
+
let parsedValue = rawValue;
|
|
2318
|
+
if (rawValue === 'true') parsedValue = true;
|
|
2319
|
+
else if (rawValue === 'false') parsedValue = false;
|
|
2320
|
+
else if (rawValue === 'null') parsedValue = null;
|
|
2321
|
+
else if (!Number.isNaN(Number(rawValue)) && rawValue.trim() !== '') parsedValue = Number(rawValue);
|
|
2322
|
+
const cfgPath = findProjectConfigPath(process.cwd()) || path.join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
2323
|
+
const { config } = loadProjectConfig(process.cwd(), process.env);
|
|
2324
|
+
const next = setConfigValue(config, keyPath, parsedValue);
|
|
2325
|
+
try {
|
|
2326
|
+
writeProjectConfig(cfgPath, next);
|
|
2327
|
+
info(`set ${keyPath} = ${JSON.stringify(parsedValue)} in ${cfgPath}`);
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
errorln(err.message);
|
|
2330
|
+
process.exit(1);
|
|
2331
|
+
}
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
if (sub === 'validate') {
|
|
2336
|
+
const cfgPath = findProjectConfigPath(process.cwd());
|
|
2337
|
+
if (!cfgPath) {
|
|
2338
|
+
info(`no ${PROJECT_CONFIG_FILENAME} found — using defaults`);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
let raw;
|
|
2342
|
+
try { raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); }
|
|
2343
|
+
catch (err) { errorln(`parse error: ${err.message}`); process.exit(1); }
|
|
2344
|
+
const result = validateProjectConfig(raw);
|
|
2345
|
+
if (result.valid) {
|
|
2346
|
+
info(`${cfgPath} is valid`);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
errorln(`${cfgPath} has ${result.errors.length} error${result.errors.length === 1 ? '' : 's'}:`);
|
|
2350
|
+
for (const e of result.errors) errorln(` - ${e}`);
|
|
2351
|
+
process.exit(1);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
errorln(`Unknown config subcommand: ${sub}. Available: mode, init, show, get, set, validate`);
|
|
2355
|
+
process.exit(1);
|
|
2356
|
+
}
|
|
2357
|
+
async function cmdUninstall(args) {
|
|
2358
|
+
try {
|
|
2359
|
+
const result = await runUninstall(args);
|
|
2360
|
+
if (result?.canceled) process.exit(130);
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
errorln(err?.message || 'uninstall failed');
|
|
2363
|
+
process.exit(1);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
async function cmdUpdate() {
|
|
2368
|
+
try {
|
|
2369
|
+
runUpdate({ cwd: process.cwd(), env: process.env, stdout: process.stdout });
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
errorln(error?.message || 'construct update failed');
|
|
2372
|
+
process.exit(1);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
async function cmdUpgrade(args) {
|
|
2377
|
+
try {
|
|
2378
|
+
const yes = args.includes('--yes');
|
|
2379
|
+
const result = runUpgrade({ cwd: process.cwd(), env: process.env, stdout: process.stdout, yes });
|
|
2380
|
+
if (!result.success) process.exit(1);
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
errorln(error?.message || 'construct upgrade failed');
|
|
2383
|
+
process.exit(1);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
async function cmdValidate() {
|
|
2387
|
+
const reg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'agents', 'registry.json'), 'utf8'));
|
|
2388
|
+
const result = validateRegistry(reg, { rootDir: ROOT_DIR });
|
|
2389
|
+
if (result.valid) {
|
|
2390
|
+
println(`✓ Registry valid (${result.summary})`);
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
if (!result.valid) result.errors.forEach((e, i) => errorln(`${i + 1}. ${e}`));
|
|
2394
|
+
process.exit(1);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
async function cmdModels(args) {
|
|
2398
|
+
const tier = args.find((arg) => arg.startsWith('--tier='))?.split('=')[1] ?? '';
|
|
2399
|
+
const setModel = args.find((arg) => arg.startsWith('--set='))?.split('=')[1] ?? '';
|
|
2400
|
+
const envPath = getUserEnvPath(HOME);
|
|
2401
|
+
if (args.includes('--reset')) {
|
|
2402
|
+
resetEnv(envPath);
|
|
2403
|
+
println('Removed CX_MODEL_* overrides from ~/.construct/config.env.');
|
|
2404
|
+
await cmdSync([]);
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
if (tier && setModel) {
|
|
2408
|
+
const preferFree = args.includes('--prefer-free');
|
|
2409
|
+
const preferFreeSameFamily = args.includes('--prefer-free-same-family');
|
|
2410
|
+
const inferred = setModelWithTierInference(envPath, tier, setModel, registry.models ?? {}, { preferFree, preferFreeSameFamily });
|
|
2411
|
+
println(`Set ${tier} -> ${setModel} in ~/.construct/config.env`);
|
|
2412
|
+
if (preferFreeSameFamily) println('Prefer-free-same-family mode: enabled');
|
|
2413
|
+
if (preferFree) println('Prefer-free mode: enabled');
|
|
2414
|
+
println(`Resolved tier set:`);
|
|
388
2415
|
println(` reasoning ${String(inferred.reasoning)}`);
|
|
389
2416
|
println(` standard ${String(inferred.standard)}`);
|
|
390
2417
|
println(` fast ${String(inferred.fast)}`);
|
|
@@ -424,9 +2451,20 @@ async function cmdModels(args) {
|
|
|
424
2451
|
}
|
|
425
2452
|
const registry = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'agents', 'registry.json'), 'utf8'));
|
|
426
2453
|
const result = readCurrentModels(envPath, registry.models ?? {});
|
|
2454
|
+
const nothingSelected = ['reasoning', 'standard', 'fast'].every((t) => !result[t]);
|
|
427
2455
|
println('Current model assignments:');
|
|
428
2456
|
for (const currentTier of ['reasoning', 'standard', 'fast']) {
|
|
429
|
-
|
|
2457
|
+
const id = result[currentTier];
|
|
2458
|
+
const source = result.sources?.[currentTier] || (id ? 'registry' : 'not configured');
|
|
2459
|
+
println(` ${currentTier.padEnd(11)}${id ? `${id} [${source}]` : '(not configured)'}`);
|
|
2460
|
+
}
|
|
2461
|
+
if (nothingSelected) {
|
|
2462
|
+
println('');
|
|
2463
|
+
println('No tier has a model selected. Construct ships with no default.');
|
|
2464
|
+
println('Pick one of:');
|
|
2465
|
+
println(' • Dashboard → Models page (single source of truth)');
|
|
2466
|
+
println(' • construct models --set=<provider/model-id> --tier=reasoning|standard|fast');
|
|
2467
|
+
println(' • construct models --apply Poll OpenRouter free catalog and seed all tiers');
|
|
430
2468
|
}
|
|
431
2469
|
}
|
|
432
2470
|
|
|
@@ -441,16 +2479,202 @@ async function cmdMcp(args) {
|
|
|
441
2479
|
process.exit(1);
|
|
442
2480
|
}
|
|
443
2481
|
|
|
2482
|
+
async function cmdOllamaCmd(args) {
|
|
2483
|
+
return cmdOllama(args);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
async function cmdPlugin(args) {
|
|
2487
|
+
const [subcmd = 'list', id, ...rest] = args;
|
|
2488
|
+
const registry = loadPluginRegistry({ cwd: process.cwd(), homeDir: HOME, rootDir: ROOT_DIR, env: process.env });
|
|
2489
|
+
|
|
2490
|
+
if (subcmd === 'list') {
|
|
2491
|
+
println('Construct Plugins');
|
|
2492
|
+
println('═════════════════');
|
|
2493
|
+
println('');
|
|
2494
|
+
for (const plugin of registry.plugins) {
|
|
2495
|
+
const source = plugin.builtIn ? 'built-in' : path.relative(process.cwd(), plugin.manifestPath);
|
|
2496
|
+
println(` ${plugin.id.padEnd(22)} ${plugin.version.padEnd(8)} ${plugin.name}`);
|
|
2497
|
+
println(` ${source}`);
|
|
2498
|
+
if ((plugin.mcps ?? []).length) println(` mcps: ${(plugin.mcps ?? []).map((mcp) => mcp.id).join(', ')}`);
|
|
2499
|
+
}
|
|
2500
|
+
if (registry.errors.length) {
|
|
2501
|
+
println('');
|
|
2502
|
+
errorln('Registry errors:');
|
|
2503
|
+
for (const error of registry.errors) errorln(` ✗ ${error}`);
|
|
2504
|
+
process.exit(1);
|
|
2505
|
+
}
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
if (subcmd === 'validate') {
|
|
2510
|
+
if (registry.valid) {
|
|
2511
|
+
println(`✓ Plugin registry valid (${registry.plugins.length} plugins, ${registry.mcps.length} MCP entries)`);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
for (const error of registry.errors) errorln(`✗ ${error}`);
|
|
2515
|
+
process.exit(1);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if (subcmd === 'info') {
|
|
2519
|
+
const plugin = getPluginById(id, { cwd: process.cwd(), homeDir: HOME, rootDir: ROOT_DIR, env: process.env });
|
|
2520
|
+
if (!plugin) {
|
|
2521
|
+
errorln(`Unknown plugin: ${id}`);
|
|
2522
|
+
process.exit(1);
|
|
2523
|
+
}
|
|
2524
|
+
println(`${plugin.name} (${plugin.id})`);
|
|
2525
|
+
println('─'.repeat(48));
|
|
2526
|
+
println(`Version: ${plugin.version}`);
|
|
2527
|
+
println(`Capabilities: ${(plugin.capabilities ?? []).join(', ') || '(none)'}`);
|
|
2528
|
+
println(`Source: ${plugin.builtIn ? 'built-in' : plugin.manifestPath}`);
|
|
2529
|
+
println(`MCPs: ${(plugin.mcps ?? []).map((mcp) => mcp.id).join(', ') || '(none)'}`);
|
|
2530
|
+
println(`\n${plugin.description}`);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
if (subcmd === 'init') {
|
|
2535
|
+
if (!id) {
|
|
2536
|
+
errorln('Usage: construct plugin init <plugin-id> [--force]');
|
|
2537
|
+
process.exit(1);
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
const filePath = initPluginManifest(id, { cwd: process.cwd(), force: rest.includes('--force') });
|
|
2541
|
+
println(`✓ Created plugin manifest at ${path.relative(process.cwd(), filePath)}`);
|
|
2542
|
+
return;
|
|
2543
|
+
} catch (error) {
|
|
2544
|
+
errorln(error?.message || 'plugin init failed');
|
|
2545
|
+
process.exit(1);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
if (subcmd === 'engine') {
|
|
2550
|
+
return await cmdPluginEngine([id, ...rest].filter(Boolean));
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
errorln(`Unknown plugin subcommand: ${subcmd}`);
|
|
2554
|
+
process.exit(1);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
async function cmdPluginEngine(args) {
|
|
2558
|
+
const [action = 'list', ...rest] = args;
|
|
2559
|
+
const isGlobal = rest.includes('--global');
|
|
2560
|
+
const targetPath = isGlobal
|
|
2561
|
+
? path.join(HOME, '.construct', 'plugins.json')
|
|
2562
|
+
: path.join(process.cwd(), '.cx', 'plugins.json');
|
|
2563
|
+
|
|
2564
|
+
function readPluginsJson() {
|
|
2565
|
+
if (!fs.existsSync(targetPath)) return { plugins: [] };
|
|
2566
|
+
try { return JSON.parse(fs.readFileSync(targetPath, 'utf8')); } catch { return { plugins: [] }; }
|
|
2567
|
+
}
|
|
2568
|
+
function writePluginsJson(data) {
|
|
2569
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
2570
|
+
fs.writeFileSync(targetPath, JSON.stringify(data, null, 2) + '\n');
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
const { describeEngine, LAYERS } = await import('../lib/engine/index.mjs');
|
|
2574
|
+
|
|
2575
|
+
if (action === 'list') {
|
|
2576
|
+
const desc = await describeEngine({ rootDir: process.cwd() });
|
|
2577
|
+
println('Engine plugins');
|
|
2578
|
+
println('══════════════');
|
|
2579
|
+
for (const entry of desc.summary) {
|
|
2580
|
+
println(` ${entry.layer.padEnd(11)} ${entry.id.padEnd(32)} (${entry.source})`);
|
|
2581
|
+
}
|
|
2582
|
+
if (desc.errors.length) {
|
|
2583
|
+
println('');
|
|
2584
|
+
errorln('Override errors:');
|
|
2585
|
+
for (const e of desc.errors) errorln(` ✗ [${e.layer}] ${e.error}`);
|
|
2586
|
+
process.exit(1);
|
|
2587
|
+
}
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
if (action === 'add') {
|
|
2592
|
+
const layer = rest[0];
|
|
2593
|
+
const pkg = rest[1];
|
|
2594
|
+
if (!layer || !pkg) {
|
|
2595
|
+
errorln('Usage: construct plugin engine add <layer> <package> [--global]');
|
|
2596
|
+
errorln(` layer ∈ {${LAYERS.join(', ')}}`);
|
|
2597
|
+
errorln(' package: npm name, absolute path, or github:owner/repo#sha');
|
|
2598
|
+
process.exit(1);
|
|
2599
|
+
}
|
|
2600
|
+
if (!LAYERS.includes(layer)) {
|
|
2601
|
+
errorln(`Unknown layer '${layer}'. Allowed: ${LAYERS.join(', ')}`);
|
|
2602
|
+
process.exit(1);
|
|
2603
|
+
}
|
|
2604
|
+
const data = readPluginsJson();
|
|
2605
|
+
data.plugins ??= [];
|
|
2606
|
+
if (data.plugins.some((p) => p.layer === layer && p.package === pkg)) {
|
|
2607
|
+
println(`Already present: ${layer} → ${pkg}`);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
data.plugins.push({ layer, package: pkg, options: {} });
|
|
2611
|
+
writePluginsJson(data);
|
|
2612
|
+
println(`✓ Added ${layer} → ${pkg} (${path.relative(process.cwd(), targetPath)})`);
|
|
2613
|
+
println(' Run `construct plugin engine list` to verify it loads.');
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
if (action === 'remove') {
|
|
2618
|
+
const layer = rest[0];
|
|
2619
|
+
const pkg = rest[1];
|
|
2620
|
+
if (!layer || !pkg) {
|
|
2621
|
+
errorln('Usage: construct plugin engine remove <layer> <package> [--global]');
|
|
2622
|
+
process.exit(1);
|
|
2623
|
+
}
|
|
2624
|
+
const data = readPluginsJson();
|
|
2625
|
+
const before = (data.plugins ?? []).length;
|
|
2626
|
+
data.plugins = (data.plugins ?? []).filter((p) => !(p.layer === layer && p.package === pkg));
|
|
2627
|
+
if (data.plugins.length === before) {
|
|
2628
|
+
println(`No entry to remove for ${layer} → ${pkg}`);
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
writePluginsJson(data);
|
|
2632
|
+
println(`✓ Removed ${layer} → ${pkg}`);
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
errorln(`Unknown plugin engine action: ${action}. Available: list, add, remove`);
|
|
2637
|
+
process.exit(1);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
444
2640
|
async function cmdDiff() {
|
|
445
2641
|
warn('construct diff is not yet ported to the Node CLI surface. Use git diff for now.');
|
|
446
2642
|
}
|
|
447
2643
|
|
|
448
|
-
async function cmdOptimize() {
|
|
449
|
-
|
|
2644
|
+
async function cmdOptimize(args) {
|
|
2645
|
+
runNodeScript(path.join(ROOT_DIR, 'scripts', 'optimize.mjs'), args, ENV);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
async function cmdSeedTraces(args) {
|
|
2649
|
+
runNodeScript(path.join(ROOT_DIR, 'scripts', 'seed-traces.mjs'), args, ENV);
|
|
450
2650
|
}
|
|
451
2651
|
|
|
452
2652
|
async function cmdCleanup() {
|
|
453
|
-
|
|
2653
|
+
const force = restArgsCache.includes('--pressure-release');
|
|
2654
|
+
const quiet = restArgsCache.includes('--quiet');
|
|
2655
|
+
const report = runPressureRelease({ env: process.env, force });
|
|
2656
|
+
|
|
2657
|
+
if (!quiet) {
|
|
2658
|
+
const swap = report.swapUsage
|
|
2659
|
+
? `${(report.swapUsage.usedBytes / (1024 ** 3)).toFixed(1)} GiB used`
|
|
2660
|
+
: 'swap unavailable';
|
|
2661
|
+
println(`Pressure guard: ${report.pressureTriggered ? 'triggered' : 'standby'} · ${swap}`);
|
|
2662
|
+
for (const entry of report.warnings ?? []) {
|
|
2663
|
+
const grace = entry.graceMinutes ? `; will terminate after ${entry.graceMinutes}m if still matched` : '';
|
|
2664
|
+
println(`Warning pid ${entry.pid} (${entry.reason})${grace}`);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
if (!report.killed.length) {
|
|
2669
|
+
if (!quiet) println('No stale dev-agent processes needed cleanup.');
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
if (!quiet) {
|
|
2674
|
+
for (const entry of report.killed) {
|
|
2675
|
+
println(`Stopped pid ${entry.pid} (${entry.reason})`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
454
2678
|
}
|
|
455
2679
|
|
|
456
2680
|
async function cmdCost(args) {
|
|
@@ -490,11 +2714,53 @@ async function cmdEfficiency(args) {
|
|
|
490
2714
|
}
|
|
491
2715
|
|
|
492
2716
|
async function cmdEvals(args) {
|
|
2717
|
+
const sub = args[0];
|
|
2718
|
+
|
|
2719
|
+
if (sub === 'retrieval') {
|
|
2720
|
+
const jsonOutput = args.includes('--json');
|
|
2721
|
+
const fixtureArg = args.find((a) => a.startsWith('--fixture='));
|
|
2722
|
+
const defaultFixture = path.join(ROOT_DIR, 'tests', 'fixtures', 'retrieval-eval', 'queries.json');
|
|
2723
|
+
const fixturePath = fixtureArg ? fixtureArg.split('=')[1] : defaultFixture;
|
|
2724
|
+
|
|
2725
|
+
const { evaluateFixture, formatReport } = await import('../lib/engine/eval-retrieval.mjs');
|
|
2726
|
+
const report = await evaluateFixture(fixturePath);
|
|
2727
|
+
|
|
2728
|
+
try {
|
|
2729
|
+
const cacheDir = path.join(HOME, '.cx', 'evals');
|
|
2730
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
2731
|
+
const ts = new Date().toISOString();
|
|
2732
|
+
fs.writeFileSync(
|
|
2733
|
+
path.join(cacheDir, 'retrieval-latest.json'),
|
|
2734
|
+
JSON.stringify({ ts, fixture: fixturePath, report }, null, 2),
|
|
2735
|
+
);
|
|
2736
|
+
const sum = report?.summary || report || {};
|
|
2737
|
+
const historyEntry = {
|
|
2738
|
+
ts,
|
|
2739
|
+
fixture: path.basename(fixturePath),
|
|
2740
|
+
recallAt5: sum.recallAt5 ?? sum.recall_at_5 ?? null,
|
|
2741
|
+
precisionAt5: sum.precisionAt5 ?? sum.precision_at_5 ?? null,
|
|
2742
|
+
mrr: sum.mrr ?? null,
|
|
2743
|
+
p50LatencyMs: sum.p50LatencyMs ?? sum.p50_latency_ms ?? null,
|
|
2744
|
+
p95LatencyMs: sum.p95LatencyMs ?? sum.p95_latency_ms ?? null,
|
|
2745
|
+
};
|
|
2746
|
+
fs.appendFileSync(path.join(cacheDir, 'retrieval-history.jsonl'), JSON.stringify(historyEntry) + '\n');
|
|
2747
|
+
} catch { /* cache write is best-effort */ }
|
|
2748
|
+
|
|
2749
|
+
if (jsonOutput) {
|
|
2750
|
+
println(JSON.stringify(report, null, 2));
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
println(`Retrieval evaluation — ${path.relative(ROOT_DIR, fixturePath)}`);
|
|
2754
|
+
println('═══════════════════════════════════════════════════════');
|
|
2755
|
+
println(formatReport(report));
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
493
2759
|
const jsonOutput = args.includes('--json');
|
|
494
2760
|
const catalog = {
|
|
495
|
-
description: 'Evaluators are managed via
|
|
496
|
-
backendUrl: process.env.
|
|
497
|
-
configured: Boolean(process.env.
|
|
2761
|
+
description: 'Evaluators are managed via telemetry quality scores. Use cx_score MCP tool or POST /api/public/scores to record quality scores. Run `construct evals retrieval` for the local IR-metric harness against a fixture corpus.',
|
|
2762
|
+
backendUrl: process.env.CONSTRUCT_TELEMETRY_BASEURL ?? 'http://localhost:3000',
|
|
2763
|
+
configured: Boolean(process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY && process.env.CONSTRUCT_TELEMETRY_SECRET_KEY),
|
|
498
2764
|
};
|
|
499
2765
|
if (jsonOutput) {
|
|
500
2766
|
println(JSON.stringify(catalog, null, 2));
|
|
@@ -503,11 +2769,14 @@ async function cmdEvals(args) {
|
|
|
503
2769
|
println('Evaluator Catalog');
|
|
504
2770
|
println('═════════════════');
|
|
505
2771
|
println('');
|
|
506
|
-
println(`Backend:
|
|
507
|
-
println(`Configured: ${catalog.configured ? 'yes' : 'no — set
|
|
2772
|
+
println(`Backend: Telemetry (${catalog.backendUrl})`);
|
|
2773
|
+
println(`Configured: ${catalog.configured ? 'yes' : 'no — set CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY'}`);
|
|
508
2774
|
println('');
|
|
509
2775
|
println('Quality scores are recorded by agents via the cx_score MCP tool.');
|
|
510
|
-
println('View and manage scores at your
|
|
2776
|
+
println('View and manage scores at your telemetry dashboard.');
|
|
2777
|
+
println('');
|
|
2778
|
+
println('Local retrieval evaluation:');
|
|
2779
|
+
println(' construct evals retrieval [--fixture=PATH] [--json]');
|
|
511
2780
|
}
|
|
512
2781
|
|
|
513
2782
|
async function cmdDocsUpdate(args) {
|
|
@@ -531,62 +2800,539 @@ async function cmdDocsUpdate(args) {
|
|
|
531
2800
|
}
|
|
532
2801
|
}
|
|
533
2802
|
|
|
2803
|
+
async function cmdDocsCheck(args) {
|
|
2804
|
+
const jsonOutput = args.includes('--json');
|
|
2805
|
+
const { checkDocsCoverage } = await import('../lib/auto-docs.mjs');
|
|
2806
|
+
const result = checkDocsCoverage({ rootDir: ROOT_DIR });
|
|
2807
|
+
if (jsonOutput) {
|
|
2808
|
+
println(JSON.stringify(result, null, 2));
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
println(`Doc coverage: ${result.covered.length}/${result.total} commands have a how-to guide`);
|
|
2812
|
+
if (result.uncovered.length === 0) {
|
|
2813
|
+
ok('All user-facing commands are covered by a how-to guide.');
|
|
2814
|
+
} else {
|
|
2815
|
+
println('');
|
|
2816
|
+
println('Commands with no linked how-to in docs/README.md:');
|
|
2817
|
+
for (const name of result.uncovered) println(` ✗ construct ${name}`);
|
|
2818
|
+
println('');
|
|
2819
|
+
println(`Add cookbook recipes under docs/cookbook/ and link them from the cookbook index.`);
|
|
2820
|
+
process.exitCode = 1;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
534
2824
|
async function cmdDocsSite() {
|
|
535
|
-
const {
|
|
536
|
-
|
|
537
|
-
|
|
2825
|
+
const { buildFumadocsReference } = await import('../lib/auto-docs.mjs');
|
|
2826
|
+
const { written } = buildFumadocsReference({ rootDir: ROOT_DIR });
|
|
2827
|
+
if (written.length === 0) {
|
|
2828
|
+
ok('docs/reference/ already up to date.');
|
|
2829
|
+
} else {
|
|
2830
|
+
ok(`docs/reference/ regenerated (${written.length} file${written.length === 1 ? '' : 's'}).`);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
async function cmdDashboardSync(args) {
|
|
2835
|
+
const { runDashboardStaticCli } = await import('../lib/dashboard-static.mjs');
|
|
2836
|
+
const exitCode = await runDashboardStaticCli(args, { rootDir: ROOT_DIR });
|
|
2837
|
+
if (exitCode) process.exit(exitCode);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
async function cmdBootstrap(args) {
|
|
2841
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
2842
|
+
const { runBootstrap } = await import('../lib/bootstrap.mjs');
|
|
2843
|
+
const { imported, skipped, error } = runBootstrap(process.cwd(), { verbose });
|
|
2844
|
+
if (error) { errorln(error); process.exit(1); }
|
|
2845
|
+
ok(`Bootstrap complete: ${imported} imported, ${skipped} skipped (already present).`);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
async function cmdMemory(args) {
|
|
2849
|
+
const sub = args[0];
|
|
2850
|
+
if (sub === 'stats') {
|
|
2851
|
+
const { computeStats, formatStats } = await import('../lib/memory-stats.mjs');
|
|
2852
|
+
const projectArg = args.find((a) => a.startsWith('--project='));
|
|
2853
|
+
const project = projectArg ? projectArg.split('=')[1] : null;
|
|
2854
|
+
const lastNArg = args.find((a) => a.startsWith('--last='));
|
|
2855
|
+
const lastN = lastNArg ? Number.parseInt(lastNArg.split('=')[1], 10) || 50 : 50;
|
|
2856
|
+
const stats = computeStats(process.cwd(), { project, lastN });
|
|
2857
|
+
process.stdout.write(formatStats(stats));
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
if (sub === 'consolidate') {
|
|
2861
|
+
const { consolidate } = await import('../lib/engine/consolidate.mjs');
|
|
2862
|
+
const { getEngine } = await import('../lib/engine/index.mjs');
|
|
2863
|
+
const engine = await getEngine({ rootDir: process.cwd() });
|
|
2864
|
+
const summariser = engine.layers.compressor;
|
|
2865
|
+
const thresholdArg = args.find((a) => a.startsWith('--threshold='));
|
|
2866
|
+
const archiveDaysArg = args.find((a) => a.startsWith('--archive-days='));
|
|
2867
|
+
const opts = { summariser };
|
|
2868
|
+
if (thresholdArg) opts.similarityThreshold = Number(thresholdArg.split('=')[1]);
|
|
2869
|
+
if (archiveDaysArg) opts.archiveAfterDays = Number(archiveDaysArg.split('=')[1]);
|
|
2870
|
+
const result = await consolidate(process.cwd(), opts);
|
|
2871
|
+
process.stdout.write(
|
|
2872
|
+
`consolidate: ${result.clustersBefore} observations → ${result.clusters} insights, ` +
|
|
2873
|
+
`${result.archived.length} archived, ${result.archivePruned ?? 0} pruned from archive\n`
|
|
2874
|
+
);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
errorln(`Unknown memory subcommand: ${sub || '(none)'}. Available: stats, consolidate`);
|
|
2878
|
+
process.exit(1);
|
|
538
2879
|
}
|
|
539
2880
|
|
|
540
2881
|
async function cmdLintComments(args) {
|
|
541
2882
|
const fix = args.includes('--fix');
|
|
542
|
-
const
|
|
543
|
-
const
|
|
2883
|
+
const staged = args.includes('--staged');
|
|
2884
|
+
const { lintRepo: lint, lintFile, formatResults: fmt } = await import('../lib/comment-lint.mjs');
|
|
2885
|
+
|
|
2886
|
+
let results;
|
|
2887
|
+
if (staged) {
|
|
2888
|
+
const { execSync } = await import('node:child_process');
|
|
2889
|
+
let stagedList = '';
|
|
2890
|
+
try {
|
|
2891
|
+
stagedList = execSync('git diff --cached --name-only --diff-filter=ACMR', {
|
|
2892
|
+
cwd: process.cwd(),
|
|
2893
|
+
timeout: 3000,
|
|
2894
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2895
|
+
}).toString();
|
|
2896
|
+
} catch {
|
|
2897
|
+
process.stdout.write(' ✓ No staged files to lint.\n');
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
const rels = stagedList.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
2901
|
+
.filter((f) => /\.(mjs|js|md|sh)$/.test(f));
|
|
2902
|
+
if (!rels.length) {
|
|
2903
|
+
process.stdout.write(' ✓ No staged comment-eligible files.\n');
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
const path = (await import('node:path')).default;
|
|
2907
|
+
results = rels
|
|
2908
|
+
.map((rel) => lintFile(path.join(process.cwd(), rel), { rootDir: process.cwd(), fix }))
|
|
2909
|
+
.filter((r) => r.errors.length || r.warnings.length);
|
|
2910
|
+
} else {
|
|
2911
|
+
results = lint({ rootDir: ROOT_DIR, fix });
|
|
2912
|
+
}
|
|
2913
|
+
|
|
544
2914
|
const { output, exitCode } = fmt(results);
|
|
545
2915
|
process.stdout.write(output);
|
|
546
2916
|
if (exitCode !== 0) process.exit(exitCode);
|
|
547
2917
|
}
|
|
548
2918
|
|
|
2919
|
+
async function cmdGatesAudit(args) {
|
|
2920
|
+
const { auditGates, formatReport } = await import('../lib/gates-audit.mjs');
|
|
2921
|
+
const report = auditGates({ rootDir: ROOT_DIR });
|
|
2922
|
+
if (args.includes('--json')) {
|
|
2923
|
+
println(JSON.stringify(report, null, 2));
|
|
2924
|
+
} else {
|
|
2925
|
+
process.stdout.write(formatReport(report));
|
|
2926
|
+
}
|
|
2927
|
+
if (!report.ok) process.exit(1);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
async function cmdLintTemplates(args) {
|
|
2931
|
+
const bodyFileArg = args.find((a) => a.startsWith('--body-file='))?.split('=').slice(1).join('=');
|
|
2932
|
+
const bodyArgIdx = args.indexOf('--body-file');
|
|
2933
|
+
const bodyFile = bodyFileArg || (bodyArgIdx >= 0 ? args[bodyArgIdx + 1] : null);
|
|
2934
|
+
const inlineBody = args.find((a) => a.startsWith('--body='))?.split('=').slice(1).join('=');
|
|
2935
|
+
const env = { ...process.env };
|
|
2936
|
+
if (bodyFile) env.PR_BODY_FILE = path.resolve(process.cwd(), bodyFile);
|
|
2937
|
+
if (inlineBody) env.PR_BODY = inlineBody;
|
|
2938
|
+
const result = spawnSync(
|
|
2939
|
+
process.execPath,
|
|
2940
|
+
[path.join(ROOT_DIR, 'scripts', 'lint-commits-pr.mjs')],
|
|
2941
|
+
{ stdio: 'inherit', env },
|
|
2942
|
+
);
|
|
2943
|
+
process.exit(result.status ?? 0);
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
async function cmdLintResearch() {
|
|
2947
|
+
const { lintResearchRepo, formatResearchLintResults } = await import('../lib/research-lint.mjs');
|
|
2948
|
+
const results = lintResearchRepo({ rootDir: process.cwd() });
|
|
2949
|
+
const { output, exitCode } = formatResearchLintResults(results);
|
|
2950
|
+
process.stdout.write(output);
|
|
2951
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
async function cmdLintAgents() {
|
|
2955
|
+
const { validateRegistryFile } = await import('../lib/agents/schema.mjs');
|
|
2956
|
+
const registryPath = path.join(ROOT_DIR, 'agents', 'registry.json');
|
|
2957
|
+
const { errors, agentCount } = validateRegistryFile({ registryPath, rootDir: ROOT_DIR });
|
|
2958
|
+
if (errors.length === 0) {
|
|
2959
|
+
console.log(`agents/registry.json: ${agentCount} agents, 0 errors`);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
console.error(`agents/registry.json: ${errors.length} error(s) across ${agentCount} agents`);
|
|
2963
|
+
for (const err of errors) console.error(` ${err}`);
|
|
2964
|
+
process.exit(1);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
async function cmdBackup(args) {
|
|
2968
|
+
const sub = args[0];
|
|
2969
|
+
|
|
2970
|
+
if (!sub || sub === 'create') {
|
|
2971
|
+
const includeSecrets = args.includes('--include-secrets');
|
|
2972
|
+
const noPrune = args.includes('--no-prune');
|
|
2973
|
+
const keepArg = args.find((a) => a.startsWith('--keep='));
|
|
2974
|
+
const keep = keepArg
|
|
2975
|
+
? Number(keepArg.split('=')[1])
|
|
2976
|
+
: Number(process.env.CONSTRUCT_BACKUP_RETAIN || 10);
|
|
2977
|
+
const { createBackup, pruneBackups } = await import('../lib/storage/backup.mjs');
|
|
2978
|
+
println('Creating backup…');
|
|
2979
|
+
try {
|
|
2980
|
+
const result = await createBackup({ includeSecrets });
|
|
2981
|
+
ok(`Backup created: ${result.path}`);
|
|
2982
|
+
println(` Contents: ${result.manifest.contents.join(', ') || '(empty)'}`);
|
|
2983
|
+
if (!includeSecrets) println(' Secrets redacted. Use --include-secrets to include them.');
|
|
2984
|
+
if (!noPrune) {
|
|
2985
|
+
const pruned = pruneBackups({ keep });
|
|
2986
|
+
if (pruned.removed.length) {
|
|
2987
|
+
println(` Auto-pruned ${pruned.removed.length} old backup(s); kept ${pruned.kept}.`);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
} catch (err) {
|
|
2991
|
+
errorln(`Backup failed: ${err.message}`);
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
}
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
if (sub === 'prune') {
|
|
2998
|
+
const keepArg = args.find((a) => a.startsWith('--keep='));
|
|
2999
|
+
const keep = keepArg
|
|
3000
|
+
? Number(keepArg.split('=')[1])
|
|
3001
|
+
: Number(process.env.CONSTRUCT_BACKUP_RETAIN || 10);
|
|
3002
|
+
const { pruneBackups } = await import('../lib/storage/backup.mjs');
|
|
3003
|
+
const result = pruneBackups({ keep });
|
|
3004
|
+
if (result.removed.length === 0) {
|
|
3005
|
+
println(`Nothing to prune (have ${result.kept}, keeping ${keep}).`);
|
|
3006
|
+
} else {
|
|
3007
|
+
ok(`Pruned ${result.removed.length} backup(s); kept ${result.kept}.`);
|
|
3008
|
+
for (const r of result.removed) println(` removed ${r}`);
|
|
3009
|
+
}
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
if (sub === 'verify') {
|
|
3014
|
+
const archivePath = args[1];
|
|
3015
|
+
if (!archivePath) { errorln('Usage: construct backup verify <archive>'); process.exit(1); }
|
|
3016
|
+
const { verifyBackup } = await import('../lib/storage/backup.mjs');
|
|
3017
|
+
const result = await verifyBackup(archivePath);
|
|
3018
|
+
if (result.ok) {
|
|
3019
|
+
ok('Backup verified — all checksums match.');
|
|
3020
|
+
} else {
|
|
3021
|
+
errorln(`Backup verification failed:\n ${result.errors.join('\n ')}`);
|
|
3022
|
+
process.exit(1);
|
|
3023
|
+
}
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
if (sub === 'restore') {
|
|
3028
|
+
const archivePath = args[1];
|
|
3029
|
+
const yes = args.includes('--yes');
|
|
3030
|
+
if (!archivePath) { errorln('Usage: construct backup restore <archive> [--yes]'); process.exit(1); }
|
|
3031
|
+
if (!yes) {
|
|
3032
|
+
println(`About to restore from: ${archivePath}`);
|
|
3033
|
+
println('This will overwrite observations, sessions, config.env, and registry.json.');
|
|
3034
|
+
println('Press Ctrl+C to cancel, or re-run with --yes to proceed.');
|
|
3035
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
3036
|
+
}
|
|
3037
|
+
const { restoreBackup } = await import('../lib/storage/backup.mjs');
|
|
3038
|
+
const result = await restoreBackup(archivePath, { yes });
|
|
3039
|
+
if (result.ok) {
|
|
3040
|
+
ok(`Restored: ${result.restored.join(', ')}`);
|
|
3041
|
+
} else {
|
|
3042
|
+
if (result.restored.length) println(`Partially restored: ${result.restored.join(', ')}`);
|
|
3043
|
+
errorln(`Errors:\n ${result.errors.join('\n ')}`);
|
|
3044
|
+
process.exit(1);
|
|
3045
|
+
}
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
if (sub === 'list') {
|
|
3050
|
+
const { listBackups } = await import('../lib/storage/backup.mjs');
|
|
3051
|
+
const backups = listBackups();
|
|
3052
|
+
if (!backups.length) { println('No backups found.'); return; }
|
|
3053
|
+
for (const b of backups) {
|
|
3054
|
+
const mb = (b.size / 1024 / 1024).toFixed(1);
|
|
3055
|
+
println(` ${b.name} (${mb} MB) ${b.mtime.toISOString().slice(0, 19)}`);
|
|
3056
|
+
}
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
errorln(`Unknown backup subcommand: ${sub}. Available: create, verify, restore, list, prune`);
|
|
3061
|
+
process.exit(1);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
async function cmdProvider(args) {
|
|
3065
|
+
const sub = args[0];
|
|
3066
|
+
|
|
3067
|
+
if (!sub || sub === 'list') {
|
|
3068
|
+
const { describeProviders } = await import('../lib/providers/registry.mjs');
|
|
3069
|
+
const desc = await describeProviders({ rootDir: process.cwd(), env: process.env });
|
|
3070
|
+
println('Data-source providers');
|
|
3071
|
+
println('═════════════════════');
|
|
3072
|
+
for (const entry of desc.summary) {
|
|
3073
|
+
const status = entry.health.ok ? '✓' : '○';
|
|
3074
|
+
println(` ${status} ${entry.id.padEnd(24)} ${entry.displayName}`);
|
|
3075
|
+
println(` capabilities: ${entry.capabilities.join(', ')}`);
|
|
3076
|
+
if (entry.health.detail) println(` health: ${entry.health.detail}`);
|
|
3077
|
+
if (entry.source && entry.source !== 'built-in') println(` source: ${entry.source}`);
|
|
3078
|
+
}
|
|
3079
|
+
if (desc.errors.length) {
|
|
3080
|
+
println('');
|
|
3081
|
+
errorln('Errors loading providers:');
|
|
3082
|
+
for (const e of desc.errors) errorln(` ✗ [${e.id}] ${e.error}`);
|
|
3083
|
+
process.exit(1);
|
|
3084
|
+
}
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
if (sub === 'info') {
|
|
3089
|
+
const id = args[1];
|
|
3090
|
+
if (!id) { errorln('Usage: construct provider info <id>'); process.exit(1); }
|
|
3091
|
+
const { resolveProviders } = await import('../lib/providers/registry.mjs');
|
|
3092
|
+
const { providers, errors } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
|
|
3093
|
+
if (!providers[id]) {
|
|
3094
|
+
errorln(`Unknown provider: ${id}`);
|
|
3095
|
+
if (errors.find((e) => e.id === id)) errorln(errors.find((e) => e.id === id).error);
|
|
3096
|
+
process.exit(1);
|
|
3097
|
+
}
|
|
3098
|
+
const p = providers[id];
|
|
3099
|
+
println(`${p.meta.displayName} (${p.meta.id})`);
|
|
3100
|
+
println('─'.repeat(48));
|
|
3101
|
+
println(`Capabilities: ${p.meta.capabilities.join(', ')}`);
|
|
3102
|
+
if (p.meta.description) println(`Description: ${p.meta.description}`);
|
|
3103
|
+
if (p.configSchema) {
|
|
3104
|
+
println('');
|
|
3105
|
+
println('Config schema:');
|
|
3106
|
+
println(JSON.stringify(p.configSchema, null, 2));
|
|
3107
|
+
}
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
if (sub === 'test') {
|
|
3112
|
+
const id = args[1];
|
|
3113
|
+
if (!id) { errorln('Usage: construct provider test <id>'); process.exit(1); }
|
|
3114
|
+
const { resolveProviders } = await import('../lib/providers/registry.mjs');
|
|
3115
|
+
const { providers } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
|
|
3116
|
+
if (!providers[id]) { errorln(`Unknown provider: ${id}`); process.exit(1); }
|
|
3117
|
+
const health = await providers[id].health({});
|
|
3118
|
+
println(`${id}: ${health.ok ? 'ok' : 'unhealthy'}`);
|
|
3119
|
+
if (health.detail) println(` ${health.detail}`);
|
|
3120
|
+
process.exit(health.ok ? 0 : 1);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
if (sub === 'plugins') {
|
|
3124
|
+
const action = args[1];
|
|
3125
|
+
if (action === 'add' || action === 'remove') {
|
|
3126
|
+
const id = args[2];
|
|
3127
|
+
const pkg = args[3];
|
|
3128
|
+
if (!id) { errorln(`Usage: construct provider plugins ${action} <id> [<package>]`); process.exit(1); }
|
|
3129
|
+
const isGlobal = args.includes('--global');
|
|
3130
|
+
const targetPath = isGlobal
|
|
3131
|
+
? path.join(HOME, '.construct', 'providers.json')
|
|
3132
|
+
: path.join(process.cwd(), '.cx', 'providers.json');
|
|
3133
|
+
let data = { providers: [] };
|
|
3134
|
+
try { data = JSON.parse(fs.readFileSync(targetPath, 'utf8')); } catch { /* fresh */ }
|
|
3135
|
+
data.providers ??= [];
|
|
3136
|
+
if (action === 'add') {
|
|
3137
|
+
if (!pkg) { errorln(`Usage: construct provider plugins add <id> <package>`); process.exit(1); }
|
|
3138
|
+
if (data.providers.some((p) => p.id === id && p.package === pkg)) {
|
|
3139
|
+
println(`Already present: ${id} → ${pkg}`);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
data.providers.push({ id, package: pkg, options: {} });
|
|
3143
|
+
} else {
|
|
3144
|
+
data.providers = data.providers.filter((p) => p.id !== id);
|
|
3145
|
+
}
|
|
3146
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
3147
|
+
fs.writeFileSync(targetPath, JSON.stringify(data, null, 2) + '\n');
|
|
3148
|
+
println(`✓ ${action === 'add' ? 'Added' : 'Removed'} ${id}${pkg ? ` → ${pkg}` : ''} (${path.relative(process.cwd(), targetPath)})`);
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
errorln(`Usage: construct provider plugins <add|remove> <id> [<package>] [--global]`);
|
|
3152
|
+
process.exit(1);
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
errorln(`Unknown provider subcommand: ${sub}. Available: list, info, test, plugins`);
|
|
3156
|
+
process.exit(1);
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
async function cmdHook(args) {
|
|
3160
|
+
const name = args[0];
|
|
3161
|
+
if (!name) {
|
|
3162
|
+
errorln('Usage: construct hook <name> (e.g. construct hook session-start)');
|
|
3163
|
+
process.exit(1);
|
|
3164
|
+
}
|
|
3165
|
+
const safe = name.replace(/[^a-z0-9-]/gi, '');
|
|
3166
|
+
if (safe !== name) {
|
|
3167
|
+
errorln(`Invalid hook name '${name}': use lowercase letters, digits, or hyphens.`);
|
|
3168
|
+
process.exit(1);
|
|
3169
|
+
}
|
|
3170
|
+
const hookPath = path.join(ROOT_DIR, 'lib', 'hooks', `${safe}.mjs`);
|
|
3171
|
+
if (!fs.existsSync(hookPath)) {
|
|
3172
|
+
errorln(`Hook not found: ${hookPath}`);
|
|
3173
|
+
process.exit(1);
|
|
3174
|
+
}
|
|
3175
|
+
const result = spawnSync(process.execPath, [hookPath, ...args.slice(1)], { stdio: 'inherit' });
|
|
3176
|
+
process.exit(result.status ?? 0);
|
|
3177
|
+
}
|
|
3178
|
+
|
|
549
3179
|
const [command, ...rest] = process.argv.slice(2);
|
|
550
3180
|
const restArgsCache = rest;
|
|
551
3181
|
|
|
552
3182
|
const handlers = new Map([
|
|
553
|
-
|
|
554
|
-
['
|
|
3183
|
+
// Core
|
|
3184
|
+
['dev', cmdUp],
|
|
3185
|
+
['stop', cmdDown],
|
|
555
3186
|
['status', cmdStatus],
|
|
556
|
-
['
|
|
557
|
-
['
|
|
558
|
-
['
|
|
3187
|
+
['install', cmdSetup],
|
|
3188
|
+
['config', cmdConfig],
|
|
3189
|
+
['intake', cmdIntake],
|
|
3190
|
+
['recommendations', cmdRecommendations],
|
|
3191
|
+
['integrations', cmdIntegrations],
|
|
3192
|
+
['customer', cmdCustomer],
|
|
3193
|
+
['workspace', cmdWorkspace],
|
|
3194
|
+
['graph', cmdGraph],
|
|
3195
|
+
['uninstall', cmdUninstall],
|
|
3196
|
+
['update', cmdUpdate],
|
|
3197
|
+
['upgrade', cmdUpgrade],
|
|
3198
|
+
['completions', cmdCompletions],
|
|
559
3199
|
['sync', cmdSync],
|
|
560
3200
|
['list', cmdList],
|
|
561
3201
|
['doctor', cmdDoctor],
|
|
562
3202
|
['validate', cmdValidate],
|
|
563
3203
|
['diff', cmdDiff],
|
|
564
|
-
['do', cmdDo],
|
|
565
3204
|
['distill', cmdDistill],
|
|
566
|
-
['
|
|
567
|
-
['
|
|
3205
|
+
['ingest', cmdIngest],
|
|
3206
|
+
['infer', cmdInfer],
|
|
568
3207
|
['search', cmdSearch],
|
|
569
3208
|
['storage', cmdStorage],
|
|
3209
|
+
['pricing', cmdPricing],
|
|
3210
|
+
['overrides', cmdOverrides],
|
|
3211
|
+
['prune', cmdPrune],
|
|
3212
|
+
['resources', cmdResources],
|
|
3213
|
+
['costs', cmdCosts],
|
|
3214
|
+
['handoffs', cmdHandoffs],
|
|
570
3215
|
['headhunt', cmdHeadhunt],
|
|
571
|
-
['
|
|
572
|
-
['
|
|
3216
|
+
['init', cmdInit],
|
|
3217
|
+
['docs:verify', cmdDocsVerify],
|
|
3218
|
+
['init:update', cmdInitUpdate],
|
|
573
3219
|
['models', cmdModels],
|
|
3220
|
+
['beads:stats', async (args) => {
|
|
3221
|
+
const { getContentionStats, getHumanStatus } = await import('../lib/beads-optimistic.mjs');
|
|
3222
|
+
const jsonOutput = args.includes('--json');
|
|
3223
|
+
if (args.includes('--health')) {
|
|
3224
|
+
process.stdout.write(await getHumanStatus(process.cwd()));
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
const stats = await getContentionStats(process.cwd());
|
|
3228
|
+
if (jsonOutput) {
|
|
3229
|
+
println(JSON.stringify(stats, null, 2));
|
|
3230
|
+
} else {
|
|
3231
|
+
println('Beads Contention Statistics');
|
|
3232
|
+
println('============================');
|
|
3233
|
+
println(`Total operations: ${stats.totalOperations || 0}`);
|
|
3234
|
+
println(`Conflicts: ${stats.conflicts || 0}`);
|
|
3235
|
+
println(`Retries: ${stats.retries || 0}`);
|
|
3236
|
+
println(`Average attempts: ${stats.avgAttempts?.toFixed(2) || '1.00'}`);
|
|
3237
|
+
if (stats.operations) {
|
|
3238
|
+
println('\nPer-operation breakdown:');
|
|
3239
|
+
for (const [op, data] of Object.entries(stats.operations)) {
|
|
3240
|
+
println(` ${op}: ${data.count} calls, ${data.conflicts} conflicts`);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}],
|
|
3245
|
+
['hooks:health', async (args) => {
|
|
3246
|
+
const { checkAllHooksHealth, formatHealthStatus } = await import('../lib/hook-health.mjs');
|
|
3247
|
+
const health = await checkAllHooksHealth({
|
|
3248
|
+
verbose: true,
|
|
3249
|
+
fix: args.includes('--fix')
|
|
3250
|
+
});
|
|
3251
|
+
if (args.includes('--json')) {
|
|
3252
|
+
println(JSON.stringify(health, null, 2));
|
|
3253
|
+
} else {
|
|
3254
|
+
process.stdout.write(formatHealthStatus(health));
|
|
3255
|
+
}
|
|
3256
|
+
if (!health.healthy) {
|
|
3257
|
+
process.exit(1);
|
|
3258
|
+
}
|
|
3259
|
+
}],
|
|
3260
|
+
['policy:list', async (args) => {
|
|
3261
|
+
const { listPolicies, formatPolicyList } = await import('../lib/policy/unified-gates.mjs');
|
|
3262
|
+
if (args.includes('--json')) {
|
|
3263
|
+
println(JSON.stringify(listPolicies(), null, 2));
|
|
3264
|
+
} else {
|
|
3265
|
+
process.stdout.write(formatPolicyList());
|
|
3266
|
+
}
|
|
3267
|
+
}],
|
|
3268
|
+
['auth:status', async (args) => {
|
|
3269
|
+
const { getTokenStatus } = await import('../lib/providers/auth-manager.mjs');
|
|
3270
|
+
const providers = ['github', 'slack', 'jira', 'salesforce', 'linear'];
|
|
3271
|
+
const results = {};
|
|
3272
|
+
for (const provider of providers) {
|
|
3273
|
+
results[provider] = getTokenStatus(provider);
|
|
3274
|
+
}
|
|
3275
|
+
if (args.includes('--json')) {
|
|
3276
|
+
println(JSON.stringify(results, null, 2));
|
|
3277
|
+
} else {
|
|
3278
|
+
println('Provider Auth Status');
|
|
3279
|
+
println('====================');
|
|
3280
|
+
for (const [provider, status] of Object.entries(results)) {
|
|
3281
|
+
const icon = status.valid ? '✓' : '✗';
|
|
3282
|
+
println(`${icon} ${provider}: ${status.valid ? 'valid' : status.error}`);
|
|
3283
|
+
if (status.expiresIn) {
|
|
3284
|
+
println(` Expires in: ${Math.floor(status.expiresIn / 60)} minutes`);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}],
|
|
574
3289
|
['mcp', cmdMcp],
|
|
3290
|
+
['ollama', cmdOllamaCmd],
|
|
3291
|
+
['plugin', cmdPlugin],
|
|
575
3292
|
['hosts', cmdHosts],
|
|
576
3293
|
['review', cmdReview],
|
|
577
3294
|
['optimize', cmdOptimize],
|
|
3295
|
+
['seed-traces', cmdSeedTraces],
|
|
578
3296
|
['cost', cmdCost],
|
|
579
3297
|
['efficiency', cmdEfficiency],
|
|
580
3298
|
['evals', cmdEvals],
|
|
581
3299
|
['cleanup', cmdCleanup],
|
|
582
3300
|
['version', cmdVersion],
|
|
583
3301
|
['docs:update', cmdDocsUpdate],
|
|
3302
|
+
['docs:check', cmdDocsCheck],
|
|
584
3303
|
['docs:site', cmdDocsSite],
|
|
3304
|
+
['dashboard:sync', cmdDashboardSync],
|
|
585
3305
|
['lint:comments', cmdLintComments],
|
|
3306
|
+
['lint:templates', cmdLintTemplates],
|
|
3307
|
+
['gates:audit', cmdGatesAudit],
|
|
3308
|
+
['bootstrap', cmdBootstrap],
|
|
3309
|
+
['memory', cmdMemory],
|
|
3310
|
+
['hook', cmdHook],
|
|
3311
|
+
['backup', cmdBackup],
|
|
3312
|
+
['provider', cmdProvider],
|
|
3313
|
+
['lint:research', cmdLintResearch],
|
|
3314
|
+
['lint:agents', cmdLintAgents],
|
|
586
3315
|
['audit', async (args) => {
|
|
587
3316
|
const sub = args[0];
|
|
588
3317
|
if (sub === 'skills') return runAuditSkillsCli(args.slice(1));
|
|
589
|
-
|
|
3318
|
+
if (sub === 'trail' || !sub) {
|
|
3319
|
+
const { runAuditTrailCli } = await import('../lib/audit-trail.mjs');
|
|
3320
|
+
return runAuditTrailCli(args.slice(sub === 'trail' ? 1 : 0));
|
|
3321
|
+
}
|
|
3322
|
+
errorln(`Unknown audit subcommand: ${sub}. Available: skills, trail`);
|
|
3323
|
+
process.exit(1);
|
|
3324
|
+
}],
|
|
3325
|
+
['doc', async (args) => {
|
|
3326
|
+
const sub = args[0];
|
|
3327
|
+
if (sub === 'verify' || !sub) {
|
|
3328
|
+
const { runDocVerifyCli } = await import('../lib/doc-verify.mjs');
|
|
3329
|
+
return runDocVerifyCli(args.slice(sub === 'verify' ? 1 : 0));
|
|
3330
|
+
}
|
|
3331
|
+
if (sub === 'install-hooks') {
|
|
3332
|
+
const { runDocInstallHooksCli } = await import('../lib/doc-verify.mjs');
|
|
3333
|
+
return runDocInstallHooksCli(args.slice(1));
|
|
3334
|
+
}
|
|
3335
|
+
errorln(`Unknown doc subcommand: ${sub}. Available: verify, install-hooks`);
|
|
590
3336
|
process.exit(1);
|
|
591
3337
|
}],
|
|
592
3338
|
['team', async (args) => {
|
|
@@ -596,9 +3342,456 @@ const handlers = new Map([
|
|
|
596
3342
|
errorln(`Unknown team subcommand: ${sub}. Available: review, templates`);
|
|
597
3343
|
process.exit(1);
|
|
598
3344
|
}],
|
|
3345
|
+
['role', async (args) => {
|
|
3346
|
+
const { runCli } = await import('../lib/roles/cli.mjs');
|
|
3347
|
+
const code = await runCli(args);
|
|
3348
|
+
if (code) process.exit(code);
|
|
3349
|
+
}],
|
|
3350
|
+
['roles:list', async (args) => {
|
|
3351
|
+
const { listRoles, formatRoleList } = await import('../lib/roles/catalog.mjs');
|
|
3352
|
+
const json = args.includes('--json');
|
|
3353
|
+
const departments = args.includes('--departments');
|
|
3354
|
+
const consolidated = args.includes('--consolidated');
|
|
3355
|
+
|
|
3356
|
+
if (json) {
|
|
3357
|
+
println(JSON.stringify(listRoles({ departments, consolidated }), null, 2));
|
|
3358
|
+
} else {
|
|
3359
|
+
process.stdout.write(formatRoleList({ departments, consolidated }));
|
|
3360
|
+
}
|
|
3361
|
+
}],
|
|
3362
|
+
['roles:set', async (args) => {
|
|
3363
|
+
const { setRolePreference, getRolePreference } = await import('../lib/roles/preference.mjs');
|
|
3364
|
+
const primaryIdx = args.indexOf('--primary');
|
|
3365
|
+
const secondaryIdx = args.indexOf('--secondary');
|
|
3366
|
+
const primary = primaryIdx !== -1 ? args[primaryIdx + 1] : null;
|
|
3367
|
+
const secondary = secondaryIdx !== -1 ? args[secondaryIdx + 1] : null;
|
|
3368
|
+
|
|
3369
|
+
if (primary) {
|
|
3370
|
+
const result = setRolePreference('primary', primary);
|
|
3371
|
+
println(`Primary role set: ${result.role}`);
|
|
3372
|
+
}
|
|
3373
|
+
if (secondary) {
|
|
3374
|
+
const result = setRolePreference('secondary', secondary);
|
|
3375
|
+
println(`Secondary role set: ${result.role}`);
|
|
3376
|
+
}
|
|
3377
|
+
if (!primary && !secondary) {
|
|
3378
|
+
const prefs = { primary: getRolePreference('primary'), secondary: getRolePreference('secondary') };
|
|
3379
|
+
println(`Current: primary=${prefs.primary || 'auto'}, secondary=${prefs.secondary || 'auto'}`);
|
|
3380
|
+
}
|
|
3381
|
+
}],
|
|
3382
|
+
['feedback:record', async (args) => {
|
|
3383
|
+
const { createClassificationFeedback, recordFeedback, getAccuracyStats } = await import('../lib/intake/feedback.mjs');
|
|
3384
|
+
const json = args.includes('--json');
|
|
3385
|
+
const intakeIdIdx = args.indexOf('--intake');
|
|
3386
|
+
const reasonIdx = args.indexOf('--reason');
|
|
3387
|
+
const correctedIdx = args.indexOf('--corrected');
|
|
3388
|
+
|
|
3389
|
+
if (intakeIdIdx === -1) {
|
|
3390
|
+
// Show stats instead
|
|
3391
|
+
const stats = getAccuracyStats(process.cwd());
|
|
3392
|
+
if (json) {
|
|
3393
|
+
println(JSON.stringify(stats, null, 2));
|
|
3394
|
+
} else {
|
|
3395
|
+
println('Classification Feedback Stats');
|
|
3396
|
+
println('===========================');
|
|
3397
|
+
println(`Total feedback: ${stats.overall.total}`);
|
|
3398
|
+
println(`Corrections: ${stats.overall.corrected}`);
|
|
3399
|
+
println(`Accuracy: ${(stats.overall.accuracy * 100).toFixed(1)}%`);
|
|
3400
|
+
}
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
const intakeId = args[intakeIdIdx + 1];
|
|
3405
|
+
const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : 'other';
|
|
3406
|
+
const corrected = correctedIdx !== -1 ? JSON.parse(args[correctedIdx + 1]) : null;
|
|
3407
|
+
|
|
3408
|
+
if (!corrected) {
|
|
3409
|
+
errorln('Usage: construct feedback:record --intake <id> --corrected \'{"intakeType":"bug","primaryOwner":"debugger"}\' [--reason wrong-owner]');
|
|
3410
|
+
process.exit(1);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
const feedback = createClassificationFeedback({
|
|
3414
|
+
intakeId,
|
|
3415
|
+
original: { intakeType: 'unknown', primaryOwner: 'unknown' }, // Would load from intake
|
|
3416
|
+
corrected,
|
|
3417
|
+
reason,
|
|
3418
|
+
});
|
|
3419
|
+
|
|
3420
|
+
recordFeedback(process.cwd(), feedback);
|
|
3421
|
+
println(`Feedback recorded for intake ${intakeId}`);
|
|
3422
|
+
}],
|
|
3423
|
+
['feedback:history', async (args) => {
|
|
3424
|
+
const { getFeedbackHistory, getKeywordAdjustments } = await import('../lib/intake/feedback.mjs');
|
|
3425
|
+
const json = args.includes('--json');
|
|
3426
|
+
const adjustments = args.includes('--adjustments');
|
|
3427
|
+
const limitIdx = args.indexOf('--limit');
|
|
3428
|
+
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 20;
|
|
3429
|
+
|
|
3430
|
+
if (adjustments) {
|
|
3431
|
+
const adj = getKeywordAdjustments(process.cwd());
|
|
3432
|
+
if (json) {
|
|
3433
|
+
println(JSON.stringify(adj, null, 2));
|
|
3434
|
+
} else {
|
|
3435
|
+
println('Keyword Adjustment Suggestions');
|
|
3436
|
+
println('==============================');
|
|
3437
|
+
if (adj.length === 0) {
|
|
3438
|
+
println('No patterns detected yet (need more feedback)');
|
|
3439
|
+
} else {
|
|
3440
|
+
for (const a of adj) {
|
|
3441
|
+
println(`${a.frequency}x: ${a.suggestion}`);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
const history = getFeedbackHistory(process.cwd(), { limit });
|
|
3449
|
+
if (json) {
|
|
3450
|
+
println(JSON.stringify(history, null, 2));
|
|
3451
|
+
} else {
|
|
3452
|
+
println(`Recent Feedback (last ${history.length})`);
|
|
3453
|
+
for (const fb of history) {
|
|
3454
|
+
println(`[${fb.timestamp}] ${fb.intakeId}: ${fb.reason}`);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}],
|
|
3458
|
+
['evaluator:rubrics', async (args) => {
|
|
3459
|
+
const { listRubrics } = await import('../lib/evaluator-optimizer.mjs');
|
|
3460
|
+
const json = args.includes('--json');
|
|
3461
|
+
const rubrics = listRubrics();
|
|
3462
|
+
|
|
3463
|
+
if (json) {
|
|
3464
|
+
println(JSON.stringify(rubrics, null, 2));
|
|
3465
|
+
} else {
|
|
3466
|
+
println('Available Document Rubrics');
|
|
3467
|
+
println('==========================');
|
|
3468
|
+
for (const r of rubrics) {
|
|
3469
|
+
println(`\n${r.docType.toUpperCase()} — ${r.name}`);
|
|
3470
|
+
println(`Criteria (${r.criteriaCount}):`);
|
|
3471
|
+
for (const c of r.criteria) {
|
|
3472
|
+
println(` - ${c.name} (${(c.weight * 100).toFixed(0)}%)`);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
}],
|
|
3477
|
+
['activation:status', async (args) => {
|
|
3478
|
+
const { getActivationSummary, getActivationStats } = await import('../lib/hooks/proactive-activation.mjs');
|
|
3479
|
+
const json = args.includes('--json');
|
|
3480
|
+
const specialistIdx = args.indexOf('--specialist');
|
|
3481
|
+
|
|
3482
|
+
if (specialistIdx !== -1) {
|
|
3483
|
+
const specialist = args[specialistIdx + 1];
|
|
3484
|
+
const stats = getActivationStats(specialist);
|
|
3485
|
+
if (json) {
|
|
3486
|
+
println(JSON.stringify(stats, null, 2));
|
|
3487
|
+
} else {
|
|
3488
|
+
println(`Activation Stats: ${specialist}`);
|
|
3489
|
+
println(`Recent: ${stats.recentActivations}/${stats.rateLimit} (rate limit)`);
|
|
3490
|
+
println(`Total: ${stats.totalActivations}`);
|
|
3491
|
+
println(`Last: ${stats.lastActivation ? new Date(stats.lastActivation).toISOString() : 'never'}`);
|
|
3492
|
+
}
|
|
3493
|
+
} else {
|
|
3494
|
+
const summary = getActivationSummary();
|
|
3495
|
+
if (json) {
|
|
3496
|
+
println(JSON.stringify(summary, null, 2));
|
|
3497
|
+
} else {
|
|
3498
|
+
println('Proactive Activation Summary');
|
|
3499
|
+
println('============================');
|
|
3500
|
+
if (Object.keys(summary).length === 0) {
|
|
3501
|
+
println('No activations recorded yet');
|
|
3502
|
+
} else {
|
|
3503
|
+
for (const [specialist, data] of Object.entries(summary)) {
|
|
3504
|
+
println(`${specialist}: ${data.recentActivations} recent, ${data.totalActivations} total`);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
}],
|
|
3510
|
+
['claude:allow', async (args) => {
|
|
3511
|
+
const { listAllowEntries, addAllowEntries, removeAllowEntries, detectAllowlistGaps } =
|
|
3512
|
+
await import('../lib/claude-allow.mjs');
|
|
3513
|
+
const sub = args[0];
|
|
3514
|
+
if (!sub || sub === 'list') {
|
|
3515
|
+
const entries = listAllowEntries();
|
|
3516
|
+
if (entries.length === 0) { console.log('(no permissions.allow entries)'); return; }
|
|
3517
|
+
for (const e of entries) console.log(e);
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
if (sub === 'add') {
|
|
3521
|
+
const patterns = args.slice(1).filter(Boolean);
|
|
3522
|
+
if (patterns.length === 0) { errorln('Usage: construct claude:allow add <pattern> [...]'); process.exit(1); }
|
|
3523
|
+
const result = addAllowEntries(patterns);
|
|
3524
|
+
for (const p of result.added) console.log(`+ ${p}`);
|
|
3525
|
+
for (const p of result.existing) console.log(`= ${p} (already present)`);
|
|
3526
|
+
console.log(`\n${result.added.length} added · ${result.existing.length} unchanged · ${result.total} total · ${result.path}`);
|
|
3527
|
+
if (result.added.length > 0) console.log('Restart Claude Code so the classifier sees the new entries.');
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
if (sub === 'remove') {
|
|
3531
|
+
const patterns = args.slice(1).filter(Boolean);
|
|
3532
|
+
if (patterns.length === 0) { errorln('Usage: construct claude:allow remove <pattern> [...]'); process.exit(1); }
|
|
3533
|
+
const result = removeAllowEntries(patterns);
|
|
3534
|
+
for (const p of result.removed) console.log(`- ${p}`);
|
|
3535
|
+
for (const p of result.notFound) console.log(`? ${p} (not found)`);
|
|
3536
|
+
console.log(`\n${result.removed.length} removed · ${result.notFound.length} not found · ${result.total} total`);
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
if (sub === 'check') {
|
|
3540
|
+
const apply = args.includes('--apply');
|
|
3541
|
+
const gaps = detectAllowlistGaps({ cwd: process.cwd() });
|
|
3542
|
+
if (gaps.length === 0) { console.log('No gaps detected — branch prefixes covered by current allowlist.'); return; }
|
|
3543
|
+
console.log(`Detected ${gaps.length} gap${gaps.length === 1 ? '' : 's'} based on local branch prefixes:`);
|
|
3544
|
+
for (const g of gaps) console.log(` ${g.prefix}/ → ${g.pattern}`);
|
|
3545
|
+
if (!apply) {
|
|
3546
|
+
console.log(`\nRun \`construct claude:allow check --apply\` to add all ${gaps.length}, or \`construct claude:allow add '<pattern>'\` selectively.`);
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
const result = addAllowEntries(gaps.map((g) => g.pattern));
|
|
3550
|
+
console.log(`\n${result.added.length} added · ${result.total} total · ${result.path}`);
|
|
3551
|
+
console.log('Restart Claude Code so the classifier sees the new entries.');
|
|
3552
|
+
return;
|
|
3553
|
+
}
|
|
3554
|
+
errorln(`Unknown claude:allow subcommand: ${sub}. Available: list, add, remove, check`);
|
|
3555
|
+
process.exit(1);
|
|
3556
|
+
}],
|
|
3557
|
+
['telemetry-backfill', async (args) => {
|
|
3558
|
+
const { runTelemetryBackfillCli } = await import('../lib/telemetry/backfill.mjs');
|
|
3559
|
+
return runTelemetryBackfillCli(args);
|
|
3560
|
+
}],
|
|
3561
|
+
['eval-datasets', async (args) => {
|
|
3562
|
+
const { runEvalDatasetsCli } = await import('../lib/telemetry/eval-datasets.mjs');
|
|
3563
|
+
return runEvalDatasetsCli(args);
|
|
3564
|
+
}],
|
|
3565
|
+
['llm-judge', async (args) => {
|
|
3566
|
+
const { runLLMJudgeCli } = await import('../lib/telemetry/llm-judge.mjs');
|
|
3567
|
+
return runLLMJudgeCli(args);
|
|
3568
|
+
}],
|
|
3569
|
+
['telemetry-setup', async (args) => {
|
|
3570
|
+
const { runTelemetrySetupCli } = await import('../lib/telemetry/setup.mjs');
|
|
3571
|
+
return runTelemetrySetupCli(args);
|
|
3572
|
+
}],
|
|
3573
|
+
['beads', async (args) => {
|
|
3574
|
+
const sub = args[0];
|
|
3575
|
+
const subArgs = args.slice(1);
|
|
3576
|
+
|
|
3577
|
+
if (!sub || sub === 'status') {
|
|
3578
|
+
const jsonOutput = subArgs.includes('--json');
|
|
3579
|
+
const status = getLockStatus({ cwd: process.cwd() });
|
|
3580
|
+
if (jsonOutput) {
|
|
3581
|
+
println(JSON.stringify(status, null, 2));
|
|
3582
|
+
} else {
|
|
3583
|
+
process.stdout.write(await getHumanStatus(process.cwd()));
|
|
3584
|
+
}
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
if (sub === 'cleanup') {
|
|
3589
|
+
const staleLocks = cleanupStaleLock({ cwd: process.cwd() });
|
|
3590
|
+
const staleQueue = cleanupStaleQueue({ cwd: process.cwd() });
|
|
3591
|
+
println(`Cleaned up: ${staleLocks ? '1 stale lock' : 'no stale locks'}, ${staleQueue} stale queue entries`);
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
if (sub === 'drift') {
|
|
3596
|
+
const { detectBeadsDrift, formatDriftReport } = await import('../lib/beads/drift.mjs');
|
|
3597
|
+
const jsonOutput = subArgs.includes('--json');
|
|
3598
|
+
const opts = {};
|
|
3599
|
+
for (const arg of subArgs) {
|
|
3600
|
+
const m = arg.match(/^--([a-z-]+)=(\d+)$/);
|
|
3601
|
+
if (!m) continue;
|
|
3602
|
+
if (m[1] === 'stale-open-days') opts.staleOpenDays = Number(m[2]);
|
|
3603
|
+
if (m[1] === 'stuck-in-progress-days') opts.stuckInProgressDays = Number(m[2]);
|
|
3604
|
+
if (m[1] === 'merge-lookback') opts.mergeLookback = Number(m[2]);
|
|
3605
|
+
}
|
|
3606
|
+
const report = detectBeadsDrift(opts);
|
|
3607
|
+
if (jsonOutput) {
|
|
3608
|
+
println(JSON.stringify(report, null, 2));
|
|
3609
|
+
} else {
|
|
3610
|
+
process.stdout.write(formatDriftReport(report));
|
|
3611
|
+
}
|
|
3612
|
+
if (report.counts.stuckInProgress > 0 || report.counts.mergeDrift > 0) {
|
|
3613
|
+
process.exit(2);
|
|
3614
|
+
}
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
if (sub === 'drift-report') {
|
|
3619
|
+
// Writes a snapshot to .cx/handoffs/beads-drift-<date>.md for the
|
|
3620
|
+
// weekly drift report mechanism the bd hygiene rule promises.
|
|
3621
|
+
const { detectBeadsDrift, formatDriftReport } = await import('../lib/beads/drift.mjs');
|
|
3622
|
+
const report = detectBeadsDrift();
|
|
3623
|
+
const handoffsDir = path.join(process.cwd(), '.cx', 'handoffs');
|
|
3624
|
+
fs.mkdirSync(handoffsDir, { recursive: true });
|
|
3625
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
3626
|
+
const file = path.join(handoffsDir, `beads-drift-${date}.md`);
|
|
3627
|
+
fs.writeFileSync(file, `# Beads drift report — ${date}\n\n${formatDriftReport(report)}\n`);
|
|
3628
|
+
info(`Wrote ${file}`);
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
if (sub === 'queue') {
|
|
3633
|
+
const jsonOutput = subArgs.includes('--json');
|
|
3634
|
+
const queue = readQueue({ cwd: process.cwd() });
|
|
3635
|
+
if (jsonOutput) {
|
|
3636
|
+
println(JSON.stringify(queue, null, 2));
|
|
3637
|
+
} else {
|
|
3638
|
+
if (!queue.length) {
|
|
3639
|
+
println('No requests in queue');
|
|
3640
|
+
} else {
|
|
3641
|
+
println(`Queue (${queue.length}):`);
|
|
3642
|
+
queue.forEach((entry, idx) => {
|
|
3643
|
+
const alive = entry.pid && (() => {
|
|
3644
|
+
try { process.kill(entry.pid, 0); return true; } catch { return false; }
|
|
3645
|
+
})();
|
|
3646
|
+
println(` ${idx + 1}. ${entry.actor} – ${entry.command || entry.args?.join(' ')}${alive ? '' : ' ⚠️ dead'}`);
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
if (sub === 'lock-status' || sub === 'lock') {
|
|
3654
|
+
const status = getLockStatus({ cwd: process.cwd() });
|
|
3655
|
+
println(JSON.stringify(status, null, 2));
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
if (sub === 'help' || sub === '--help' || sub === '-h') {
|
|
3660
|
+
println('construct beads — manage beads lock and queue');
|
|
3661
|
+
println('');
|
|
3662
|
+
println('Subcommands:');
|
|
3663
|
+
println(' status Show lock and queue status (default)');
|
|
3664
|
+
println(' lock-status Detailed lock info in JSON');
|
|
3665
|
+
println(' cleanup Remove stale locks and dead queue entries');
|
|
3666
|
+
println(' queue Show pending requests');
|
|
3667
|
+
println(' help This help');
|
|
3668
|
+
println('');
|
|
3669
|
+
println('Examples:');
|
|
3670
|
+
println(' construct beads # Show status');
|
|
3671
|
+
println(' construct beads --json # JSON status');
|
|
3672
|
+
println(' construct beads queue # Show queue');
|
|
3673
|
+
println(' construct beads cleanup # Clean stale locks');
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// Default: run the bd command through our wrapper
|
|
3678
|
+
const actor = process.env.USER || process.env.LOGNAME || 'construct';
|
|
3679
|
+
const needsMergeSlot = sub === 'dolt' && subArgs[0] === 'push';
|
|
3680
|
+
let mergeSlotHeld = false;
|
|
3681
|
+
let result;
|
|
3682
|
+
try {
|
|
3683
|
+
if (needsMergeSlot) {
|
|
3684
|
+
const mergeSlot = await acquireMergeSlot({
|
|
3685
|
+
actor,
|
|
3686
|
+
cwd: process.cwd(),
|
|
3687
|
+
silent: subArgs.includes('--silent'),
|
|
3688
|
+
});
|
|
3689
|
+
mergeSlotHeld = Boolean(mergeSlot?.success);
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
result = await runBd(args, {
|
|
3693
|
+
actor,
|
|
3694
|
+
cwd: process.cwd(),
|
|
3695
|
+
silent: subArgs.includes('--silent'),
|
|
3696
|
+
});
|
|
3697
|
+
} finally {
|
|
3698
|
+
if (mergeSlotHeld) {
|
|
3699
|
+
await releaseMergeSlot({
|
|
3700
|
+
actor,
|
|
3701
|
+
cwd: process.cwd(),
|
|
3702
|
+
silent: subArgs.includes('--silent'),
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
if (result.success) {
|
|
3708
|
+
process.stdout.write(result.output || '');
|
|
3709
|
+
} else {
|
|
3710
|
+
process.stderr.write(`bd failed: ${result.error || 'Unknown error'}\n`);
|
|
3711
|
+
process.exit(result.exitCode || 1);
|
|
3712
|
+
}
|
|
3713
|
+
}],
|
|
3714
|
+
['wireframe', async (args) => {
|
|
3715
|
+
const { runWireframeCli } = await import('../lib/wireframe.mjs');
|
|
3716
|
+
return runWireframeCli(args);
|
|
3717
|
+
}],
|
|
3718
|
+
['skills', async (args) => {
|
|
3719
|
+
const sub = args[0];
|
|
3720
|
+
if (sub === 'scope' || !sub) {
|
|
3721
|
+
const { runSkillsScopeCli } = await import('../lib/skills-scope.mjs');
|
|
3722
|
+
return runSkillsScopeCli(args.slice(sub === 'scope' ? 1 : 0));
|
|
3723
|
+
}
|
|
3724
|
+
if (sub === 'apply') {
|
|
3725
|
+
const { runSkillsApplyCli } = await import('../lib/skills-apply.mjs');
|
|
3726
|
+
return runSkillsApplyCli(args.slice(1));
|
|
3727
|
+
}
|
|
3728
|
+
errorln(`Unknown skills subcommand: ${sub}. Available: scope, apply`);
|
|
3729
|
+
process.exit(1);
|
|
3730
|
+
}],
|
|
3731
|
+
['ask', async (args) => {
|
|
3732
|
+
const question = args.join(' ').trim();
|
|
3733
|
+
if (!question) {
|
|
3734
|
+
errorln('Usage: construct ask "<question>"');
|
|
3735
|
+
errorln('Example: construct ask "what are the biggest risks?"');
|
|
3736
|
+
process.exit(1);
|
|
3737
|
+
}
|
|
3738
|
+
const { ask, buildCorpus } = await import('../lib/knowledge/rag.mjs');
|
|
3739
|
+
const rootDir = process.cwd();
|
|
3740
|
+
process.stdout.write('Building knowledge index…\n');
|
|
3741
|
+
const corpus = buildCorpus(rootDir);
|
|
3742
|
+
process.stdout.write(`Indexed ${corpus.length} chunks. Retrieving…\n\n`);
|
|
3743
|
+
const result = await ask(question, { rootDir, corpus });
|
|
3744
|
+
if (result.cliMissing) {
|
|
3745
|
+
process.stdout.write('[claude CLI not available — showing retrieved context]\n\n');
|
|
3746
|
+
}
|
|
3747
|
+
process.stdout.write(`${result.answer}\n`);
|
|
3748
|
+
if (result.sources?.length) {
|
|
3749
|
+
process.stdout.write('\n--- Sources ---\n');
|
|
3750
|
+
for (const s of result.sources.slice(0, 5)) {
|
|
3751
|
+
process.stdout.write(` • [${s.source}] ${s.title} (score: ${s.score?.toFixed(3)})\n`);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
}],
|
|
3755
|
+
['embed', async (args) => {
|
|
3756
|
+
const { runEmbedCli } = await import('../lib/embed/cli.mjs');
|
|
3757
|
+
return runEmbedCli(args, { rootDir: new URL('..', import.meta.url).pathname });
|
|
3758
|
+
}],
|
|
3759
|
+
['reflect', async (args) => {
|
|
3760
|
+
const { runReflectCli } = await import('../lib/reflect.mjs');
|
|
3761
|
+
return runReflectCli(args);
|
|
3762
|
+
}],
|
|
3763
|
+
['knowledge', async (args) => {
|
|
3764
|
+
const sub = args[0] || 'trends';
|
|
3765
|
+
if (sub === 'trends') {
|
|
3766
|
+
const { buildTrendReport } = await import('../lib/knowledge/trends.mjs');
|
|
3767
|
+
const report = buildTrendReport(process.cwd());
|
|
3768
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
if (sub === 'index') {
|
|
3772
|
+
const { buildCorpus } = await import('../lib/knowledge/rag.mjs');
|
|
3773
|
+
const corpus = buildCorpus(process.cwd());
|
|
3774
|
+
process.stdout.write(`Indexed ${corpus.length} chunks from: observations, artifacts, snapshots\n`);
|
|
3775
|
+
const sources = {};
|
|
3776
|
+
for (const c of corpus) sources[c.source] = (sources[c.source] || 0) + 1;
|
|
3777
|
+
for (const [src, count] of Object.entries(sources)) {
|
|
3778
|
+
process.stdout.write(` ${src}: ${count}\n`);
|
|
3779
|
+
}
|
|
3780
|
+
return;
|
|
3781
|
+
}
|
|
3782
|
+
errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index`);
|
|
3783
|
+
process.exit(1);
|
|
3784
|
+
}],
|
|
599
3785
|
]);
|
|
600
3786
|
|
|
601
|
-
|
|
3787
|
+
// No command provided → show interactive menu
|
|
3788
|
+
if (!command) {
|
|
3789
|
+
await showInteractiveMenu();
|
|
3790
|
+
process.exit(0);
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
// Help flags → show usage
|
|
3794
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
602
3795
|
usage();
|
|
603
3796
|
process.exit(0);
|
|
604
3797
|
}
|
|
@@ -606,8 +3799,19 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
606
3799
|
const handler = handlers.get(command);
|
|
607
3800
|
if (!handler) {
|
|
608
3801
|
errorln(`Unknown command: ${command}`);
|
|
609
|
-
|
|
3802
|
+
println('');
|
|
3803
|
+
println(`${COLORS.dim}Run 'construct --help' for available commands${COLORS.reset}`);
|
|
610
3804
|
process.exit(1);
|
|
611
3805
|
}
|
|
612
3806
|
|
|
3807
|
+
// First-invocation resource probe — skipped for hooks, setup, uninstall,
|
|
3808
|
+
// doctor, and once BOOTSTRAP_CHECKED=1 is cached in ~/.construct/config.env.
|
|
3809
|
+
// On TTY: if anything's missing, prints the status table and offers to run
|
|
3810
|
+
// setup. Silent on success; never blocks the command.
|
|
3811
|
+
|
|
3812
|
+
const probe = await maybeFirstInvocationProbe({ command, homeDir: HOME, env: process.env });
|
|
3813
|
+
if (probe?.runSetup) {
|
|
3814
|
+
await cmdSetup([]);
|
|
3815
|
+
}
|
|
3816
|
+
|
|
613
3817
|
await handler(rest);
|