@geraldmaron/construct 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +57 -45
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +273 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +104 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
package/bin/construct
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* bin/construct — construct CLI entrypoint.
|
|
4
4
|
*
|
|
5
5
|
* Parses the command string, dispatches to the appropriate handler, and
|
|
6
|
-
* loads environment config from
|
|
6
|
+
* loads environment config from the resolved project root's .env (nearest
|
|
7
|
+
* .cx/ or package.json ancestor of cwd, per lib/roots.mjs) and the user
|
|
8
|
+
* config.env ($XDG_CONFIG_HOME/construct/config.env), with project .env
|
|
9
|
+
* winning per lib/env-config.mjs's tier order. This is the only
|
|
7
10
|
* public-facing binary; all commands are exported from lib/.
|
|
8
11
|
*/
|
|
9
12
|
|
|
@@ -79,12 +82,29 @@ import { syncFileStateToSql } from '../lib/storage/sync.mjs';
|
|
|
79
82
|
import { deleteIngestedArtifacts, getStorageStatus, inferProjectName, resetStorage } from '../lib/storage/admin.mjs';
|
|
80
83
|
import { runPressureRelease } from '../lib/runtime-pressure.mjs';
|
|
81
84
|
import { loadRegistry, listSpecialists } from '../lib/registry/loader.mjs';
|
|
85
|
+
import { resolveProjectRoot, resolveInstallRoot } from '../lib/roots.mjs';
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
// See lib/roots.mjs's resolveInstallRoot for why: under a Bun-compiled
|
|
88
|
+
// binary, import.meta.dirname resolves to the virtual /$bunfs/root rather
|
|
89
|
+
// than a real path, so every sibling data-dir read below would throw ENOENT.
|
|
90
|
+
|
|
91
|
+
const ROOT_DIR = resolveInstallRoot(path.resolve(import.meta.dirname, '..'));
|
|
92
|
+
const PROJECT_ROOT = resolveProjectRoot(process.cwd());
|
|
84
93
|
const HOME = os.homedir();
|
|
85
94
|
enableSecretAuditTrail();
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
|
|
96
|
+
// The project .env tier binds to PROJECT_ROOT (the user's repo), not
|
|
97
|
+
// ROOT_DIR (the toolkit install checkout) — ROOT_DIR stays reserved for
|
|
98
|
+
// locating the toolkit's own assets (registry, skills, hooks) below.
|
|
99
|
+
|
|
100
|
+
ensureConstructCredentials({ env: process.env, cwd: PROJECT_ROOT, home: HOME });
|
|
101
|
+
|
|
102
|
+
// The CLI only fills keys missing from process.env, so a conflicting shell
|
|
103
|
+
// export wins here — the opposite of the MCP server's file-wins merge.
|
|
104
|
+
// shadowWinner: 'shell' keeps the warning honest about which value the CLI
|
|
105
|
+
// actually applies (construct-6y6w.3 / construct-xj96.12).
|
|
106
|
+
|
|
107
|
+
const ENV = loadConstructEnv({ rootDir: PROJECT_ROOT, homeDir: HOME, env: process.env, shadowWinner: 'shell' });
|
|
88
108
|
for (const [key, value] of Object.entries(ENV)) {
|
|
89
109
|
if (!(key in process.env)) process.env[key] = value;
|
|
90
110
|
}
|
|
@@ -247,6 +267,12 @@ async function cmdRulesUsage(args = []) {
|
|
|
247
267
|
process.exit(1);
|
|
248
268
|
}
|
|
249
269
|
|
|
270
|
+
async function cmdApprovals(args = []) {
|
|
271
|
+
const { runApprovalsCli } = await import('../lib/cli/approvals.mjs');
|
|
272
|
+
const code = await runApprovalsCli(args, { rootDir: ROOT_DIR, homeDir: HOME, env: process.env, println, errorln });
|
|
273
|
+
if (code) process.exit(code);
|
|
274
|
+
}
|
|
275
|
+
|
|
250
276
|
async function cmdStatus() {
|
|
251
277
|
const jsonOutput = restArgsCache.includes('--json');
|
|
252
278
|
const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
|
|
@@ -295,6 +321,45 @@ async function cmdStatus() {
|
|
|
295
321
|
} catch { /* best-effort surface; never block status */ }
|
|
296
322
|
}
|
|
297
323
|
|
|
324
|
+
async function cmdWorkers(args = []) {
|
|
325
|
+
const sub = args.find((a) => !a.startsWith('--')) || 'list';
|
|
326
|
+
const wantsJson = args.includes('--json');
|
|
327
|
+
if (sub !== 'list') {
|
|
328
|
+
errorln('Usage: construct workers <list> [--json]');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { createSqlClient, closeSqlClient } = await import('../lib/storage/backend.mjs');
|
|
333
|
+
const { WorkerRegistry } = await import('../lib/orchestration/worker-runtime.mjs');
|
|
334
|
+
const { resolveIntakeTenantId, INTAKE_PROJECT_ENV_KEY } = await import('../lib/intake/queue.mjs');
|
|
335
|
+
const sql = createSqlClient(process.env);
|
|
336
|
+
const project = process.env[INTAKE_PROJECT_ENV_KEY] || path.basename(path.resolve(process.cwd())) || 'construct';
|
|
337
|
+
const tenantId = resolveIntakeTenantId(process.env);
|
|
338
|
+
|
|
339
|
+
if (!sql) {
|
|
340
|
+
const payload = { status: 'unavailable', reason: 'postgres-unavailable', workers: [] };
|
|
341
|
+
if (wantsJson) { println(JSON.stringify(payload, null, 2)); return; }
|
|
342
|
+
println('Workers unavailable: DATABASE_URL or CONSTRUCT_DATABASE_URL is not configured.');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const registry = new WorkerRegistry({ sql, project, tenantId });
|
|
348
|
+
const workers = await registry.list();
|
|
349
|
+
const payload = { status: 'available', project, tenantId, workers };
|
|
350
|
+
if (wantsJson) { println(JSON.stringify(payload, null, 2)); return; }
|
|
351
|
+
if (!workers.length) {
|
|
352
|
+
println(`No workers registered for ${project}/${tenantId}.`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const worker of workers) {
|
|
356
|
+
println(`${worker.workerId} ${worker.status} heartbeat=${worker.heartbeatAt || 'never'} ttl=${worker.ttlSeconds}s`);
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
await closeSqlClient(sql);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
298
363
|
async function cmdSync(args) {
|
|
299
364
|
const { startOpLog } = await import('../lib/op-log.mjs');
|
|
300
365
|
const opLog = startOpLog('sync', { homeDir: HOME });
|
|
@@ -345,6 +410,20 @@ async function cmdSync(args) {
|
|
|
345
410
|
}
|
|
346
411
|
}
|
|
347
412
|
|
|
413
|
+
// Advisory only: workflow-defs.mjs (LMCP-D2) can only drift from the
|
|
414
|
+
// manifests via a source regression, which release:check's --check run
|
|
415
|
+
// treats as a hard failure. Sync surfaces it early without blocking.
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const driftStatus = runNodeScript(path.join(ROOT_DIR, 'scripts', 'check-workflow-defs-drift.mjs'), ['--check'], {}, { exitOnError: false });
|
|
419
|
+
if (driftStatus !== 0) {
|
|
420
|
+
errorln('workflow-defs.mjs drift detected — run: npm run workflow-defs:check');
|
|
421
|
+
}
|
|
422
|
+
opLog.event('workflow-defs-check', { status: driftStatus });
|
|
423
|
+
} catch (err) {
|
|
424
|
+
opLog.event('workflow-defs-check-error', { message: err.message });
|
|
425
|
+
}
|
|
426
|
+
|
|
348
427
|
if (args.includes('--no-docs')) {
|
|
349
428
|
opLog.close('ok', { docs: 'skipped' });
|
|
350
429
|
return;
|
|
@@ -521,6 +600,17 @@ async function cmdDoctor() {
|
|
|
521
600
|
add('specialists/org exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'org')));
|
|
522
601
|
add('sync-specialists.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs')));
|
|
523
602
|
|
|
603
|
+
// Extension manifests check
|
|
604
|
+
try {
|
|
605
|
+
const extManifestsDir = path.join(ROOT_DIR, 'lib', 'extensions', 'manifests');
|
|
606
|
+
const extManifestFiles = fs.existsSync(extManifestsDir)
|
|
607
|
+
? fs.readdirSync(extManifestsDir).filter((f) => f.endsWith('.manifest.json'))
|
|
608
|
+
: [];
|
|
609
|
+
add(`Extension manifests: ${extManifestFiles.length} loaded`, extManifestFiles.length > 0);
|
|
610
|
+
} catch {
|
|
611
|
+
add('Extension manifests: check failed', false);
|
|
612
|
+
}
|
|
613
|
+
|
|
524
614
|
// Project .gitignore must cover .cx/. Auto-fix via --fix-gitignore.
|
|
525
615
|
// Only checks when cwd looks like a Construct project (has .cx/ or
|
|
526
616
|
// .construct/); non-project cwds get no warning.
|
|
@@ -659,6 +749,31 @@ async function cmdDoctor() {
|
|
|
659
749
|
);
|
|
660
750
|
}
|
|
661
751
|
|
|
752
|
+
// Legacy in-project heavy state (ADR-0066): runtime/traces/lancedb now
|
|
753
|
+
// resolve through the machine-scoped state root, so a project that still
|
|
754
|
+
// carries these directories is holding pre-refit state at the old
|
|
755
|
+
// project-local path. No auto-fix — the ADR calls for manual deletion, not
|
|
756
|
+
// a migration shim, since the durable copy already lives at the state root.
|
|
757
|
+
|
|
758
|
+
if (isProject) {
|
|
759
|
+
const legacyHeavyDirs = [
|
|
760
|
+
{ rel: path.join('.cx', 'runtime'), label: '.cx/runtime' },
|
|
761
|
+
{ rel: path.join('.cx', 'traces'), label: '.cx/traces' },
|
|
762
|
+
{ rel: path.join('.cx', 'lancedb'), label: '.cx/lancedb' },
|
|
763
|
+
]
|
|
764
|
+
.map((d) => ({ ...d, full: path.join(cwd, d.rel) }))
|
|
765
|
+
.filter((d) => fs.existsSync(d.full) && fs.readdirSync(d.full).length > 0);
|
|
766
|
+
|
|
767
|
+
add(
|
|
768
|
+
legacyHeavyDirs.length === 0
|
|
769
|
+
? 'No legacy in-project heavy state (runtime/traces/lancedb)'
|
|
770
|
+
: `Legacy in-project heavy state found: ${legacyHeavyDirs.map((d) => d.label).join(', ')} — `
|
|
771
|
+
+ 'durable state now lives at ~/.construct/projects/<key>/ (ADR-0066); delete the old directories manually to reclaim disk',
|
|
772
|
+
true,
|
|
773
|
+
legacyHeavyDirs.length !== 0,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
662
777
|
// Mirror model tier overrides stranded in the pre-XDG legacy config forward
|
|
663
778
|
// before resolving tiers, so an install upgraded across the XDG move reports
|
|
664
779
|
// configured without a manual copy. Applied to process.env too so the check
|
|
@@ -688,7 +803,7 @@ async function cmdDoctor() {
|
|
|
688
803
|
try {
|
|
689
804
|
const { resolveModelTiers } = await import('../lib/model-registry.mjs');
|
|
690
805
|
const resolved = resolveModelTiers({ env: process.env });
|
|
691
|
-
if (resolved.complete
|
|
806
|
+
if (resolved.complete) {
|
|
692
807
|
add('Models — all tiers configured', true);
|
|
693
808
|
} else if (resolved.configured === 0) {
|
|
694
809
|
add('Models — no tier configured (run `construct models --apply`)', false, true);
|
|
@@ -702,8 +817,79 @@ async function cmdDoctor() {
|
|
|
702
817
|
add(`Models — check failed: ${err.message}`, false, true);
|
|
703
818
|
}
|
|
704
819
|
|
|
820
|
+
// Worker-backend and web-capability visibility (construct-1yhp.2,
|
|
821
|
+
// construct-1yhp.8): reuse buildOrchestrationReadiness — the same
|
|
822
|
+
// resolution orchestration_run itself calls — so doctor can never
|
|
823
|
+
// advertise a backend/web mode that diverges from what a run would
|
|
824
|
+
// actually get. Both lines are advisory only; the resolved values never
|
|
825
|
+
// fail the gate.
|
|
826
|
+
try {
|
|
827
|
+
const { buildOrchestrationReadiness } = await import('../lib/orchestration/readiness.mjs');
|
|
828
|
+
const orchReadiness = buildOrchestrationReadiness({}, { env: process.env, cwd });
|
|
829
|
+
const backendNote = orchReadiness.workerBackend === 'inline'
|
|
830
|
+
? 'inline (prepare-only — no specialist LLM reasoning runs; set orchestration.workerBackend=provider for real execution)'
|
|
831
|
+
: orchReadiness.workerBackend;
|
|
832
|
+
add(`Worker backend: ${backendNote}`, true, true);
|
|
833
|
+
|
|
834
|
+
const webNote = orchReadiness.webMode === 'unavailable'
|
|
835
|
+
? 'unavailable (set WEB_SEARCH_URL for governed mode, or CONSTRUCT_ORCHESTRATION_WEB_DELEGATE=1 to delegate to a tool-capable host)'
|
|
836
|
+
: orchReadiness.webMode;
|
|
837
|
+
add(`Web capability: ${webNote}`, true, true);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
add(`Worker backend / web capability check failed: ${err.message}`, false, true);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Provider worker reliability settings (construct-5wkl): the effective
|
|
843
|
+
// per-call timeout and retry policy are resolved the same way the worker
|
|
844
|
+
// itself resolves them (resolveNonNegativeSetting over CONSTRUCT_PROVIDER_*),
|
|
845
|
+
// so this can never drift from what a real provider-backed run applies, and
|
|
846
|
+
// a test-scale or otherwise unintended override is never invisible. Advisory only.
|
|
847
|
+
try {
|
|
848
|
+
const { resolveNonNegativeSetting } = await import('../lib/env-config.mjs');
|
|
849
|
+
const { PROVIDER_TIMEOUT_DEFAULT_MS } = await import('../lib/orchestration/worker.mjs');
|
|
850
|
+
const timeoutMs = resolveNonNegativeSetting(process.env, 'CONSTRUCT_PROVIDER_TIMEOUT_MS', PROVIDER_TIMEOUT_DEFAULT_MS);
|
|
851
|
+
const maxAttempts = resolveNonNegativeSetting(process.env, 'CONSTRUCT_PROVIDER_MAX_ATTEMPTS', 3);
|
|
852
|
+
const retryBaseMs = resolveNonNegativeSetting(process.env, 'CONSTRUCT_PROVIDER_RETRY_BASE_MS', 250);
|
|
853
|
+
add(`Provider timeout: ${timeoutMs}ms · retry attempts: ${maxAttempts} (base backoff ${retryBaseMs}ms)`, true, true);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
add(`Provider reliability settings check failed: ${err.message}`, false, true);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
// Opt-in deep credential check (construct-1yhp.5). Default doctor stays
|
|
860
|
+
// presence-first (construct-trxz: a default-on op:// probe would fight the
|
|
861
|
+
// prompt-storm mitigation) — `--probe-providers` performs the real network
|
|
862
|
+
// auth probe only for providers already presence-configured.
|
|
863
|
+
if (args.includes('--probe-providers')) {
|
|
864
|
+
try {
|
|
865
|
+
const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
|
|
866
|
+
const { probeProviderConnection } = await import('../lib/providers/connection-probe.mjs');
|
|
867
|
+
const { providers } = getProviderModelCatalog({ env: process.env });
|
|
868
|
+
const probeable = providers.filter((p) => p.configured && ['openrouter', 'anthropic', 'openai'].includes(p.id));
|
|
869
|
+
if (probeable.length === 0) {
|
|
870
|
+
add('Provider auth probe — no presence-configured openrouter/anthropic/openai provider to probe', true, true);
|
|
871
|
+
}
|
|
872
|
+
for (const p of probeable) {
|
|
873
|
+
const result = await probeProviderConnection(p.id, { env: process.env, cwd: process.cwd() });
|
|
874
|
+
if (result.ok) {
|
|
875
|
+
add(`Provider auth probe — ${p.id}: auth-verified (SERVING)`, true, true);
|
|
876
|
+
} else if (result.code === 'auth_error') {
|
|
877
|
+
add(`Provider auth probe — ${p.id}: ${result.status || '401/403'} (NOT_SERVING) — rotate or re-authenticate the key`, false, true);
|
|
878
|
+
} else if (result.code === 'missing_key') {
|
|
879
|
+
add(`Provider auth probe — ${p.id}: credential did not materialize (op:// ref unresolved) — check the ref or run \`op signin\``, false, true);
|
|
880
|
+
} else {
|
|
881
|
+
add(`Provider auth probe — ${p.id}: ${result.code}${result.status ? ` (${result.status})` : ''} (unverified)`, false, true);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} catch (err) {
|
|
885
|
+
add(`Provider auth probe failed: ${err.message}`, false, true);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const { resolveCommandOnPath } = await import('../lib/doctor/command-on-path.mjs');
|
|
890
|
+
|
|
705
891
|
add('Node.js 20+ (recommended)', Number.parseInt(process.versions.node.split('.')[0], 10) >= 20);
|
|
706
|
-
add('npm available',
|
|
892
|
+
add('npm available', resolveCommandOnPath('npm').status === 0);
|
|
707
893
|
const npmDevdir = process.env.npm_config_devdir ?? process.env.NPM_CONFIG_DEVDIR;
|
|
708
894
|
if (npmDevdir) {
|
|
709
895
|
add(
|
|
@@ -712,7 +898,7 @@ async function cmdDoctor() {
|
|
|
712
898
|
true,
|
|
713
899
|
);
|
|
714
900
|
}
|
|
715
|
-
const constructOnPath =
|
|
901
|
+
const constructOnPath = resolveCommandOnPath('construct');
|
|
716
902
|
const constructPath = constructOnPath.status === 0 ? constructOnPath.stdout.trim() : '';
|
|
717
903
|
const constructVersion = constructPath
|
|
718
904
|
? spawnSync(constructPath, ['version'], { encoding: 'utf8', env: process.env })
|
|
@@ -763,16 +949,53 @@ async function cmdDoctor() {
|
|
|
763
949
|
// Absence is not a doctor finding — VS Code adapter support hasn't landed
|
|
764
950
|
// yet, so "create one or wait for support" isn't actionable. The line is
|
|
765
951
|
// suppressed entirely when missing; doctor surfaces facts, not roadmap.
|
|
766
|
-
const
|
|
952
|
+
const vscodeSettingsPath = vscodeSettingsPaths.find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
953
|
+
const vscodeSettingsOk = Boolean(vscodeSettingsPath);
|
|
767
954
|
if (vscodeSettingsOk) add('VS Code settings file', true, true);
|
|
955
|
+
|
|
956
|
+
// classifyHostReadiness distinguishes the discrete host-config states
|
|
957
|
+
// (missing_config/stale_path/jsonc_unpatched/wrong_key/disabled/healthy)
|
|
958
|
+
// instead of collapsing readiness to settings.json presence above — only
|
|
959
|
+
// meaningful once VS Code has a settings file or a project mcp.json to read.
|
|
960
|
+
const vscodeProjectMcpPath = path.join(process.cwd(), '.vscode', 'mcp.json');
|
|
961
|
+
const vscodeMcpPath = fs.existsSync(vscodeProjectMcpPath) ? vscodeProjectMcpPath : null;
|
|
962
|
+
if (vscodeSettingsPath || vscodeMcpPath) {
|
|
963
|
+
const { classifyHostReadiness, HOST_READINESS_NEXT_STEPS } = await import('../lib/host/readiness.mjs');
|
|
964
|
+
const reasonCode = classifyHostReadiness({
|
|
965
|
+
host: 'vscode',
|
|
966
|
+
settingsPath: vscodeSettingsPath,
|
|
967
|
+
mcpPath: vscodeMcpPath,
|
|
968
|
+
root: ROOT_DIR,
|
|
969
|
+
});
|
|
970
|
+
const nextStep = HOST_READINESS_NEXT_STEPS[reasonCode];
|
|
971
|
+
add(
|
|
972
|
+
reasonCode === 'healthy'
|
|
973
|
+
? 'VS Code MCP host readiness: healthy'
|
|
974
|
+
: `VS Code MCP host readiness: ${reasonCode} — ${nextStep}`,
|
|
975
|
+
reasonCode === 'healthy',
|
|
976
|
+
true,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
768
979
|
}
|
|
769
980
|
add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(configDir(HOME)), true);
|
|
770
981
|
add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
|
|
771
982
|
add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
|
|
772
983
|
add('specialists/prompts/ directory', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'prompts')));
|
|
773
984
|
add('Hybrid storage env keys documented', fs.existsSync(path.join(ROOT_DIR, '.env.example')));
|
|
774
|
-
|
|
775
|
-
|
|
985
|
+
const perfReviewsDir = path.join(doctorRoot(), 'performance-reviews');
|
|
986
|
+
fs.mkdirSync(perfReviewsDir, { recursive: true });
|
|
987
|
+
add('performance-reviews/ ready', fs.existsSync(perfReviewsDir));
|
|
988
|
+
|
|
989
|
+
// Living-graph truth: doctor runs `graph validate` so workflow/manifest drift
|
|
990
|
+
// surfaces as a health check, not only via the standalone command (LMCP-C7).
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
const { checkGraphValidateForDoctor } = await import('../lib/doctor/graph-validate.mjs');
|
|
994
|
+
const graphCheck = checkGraphValidateForDoctor({ rootDir: process.cwd() });
|
|
995
|
+
add(graphCheck.label, graphCheck.ok, graphCheck.warning);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
add(`Graph validate check failed: ${err.message}`, false, true);
|
|
998
|
+
}
|
|
776
999
|
|
|
777
1000
|
// construct.config.json — optional today (config is permitted to be absent;
|
|
778
1001
|
// env vars + defaults still work). The check enforces that when the file
|
|
@@ -780,7 +1003,9 @@ async function cmdDoctor() {
|
|
|
780
1003
|
try {
|
|
781
1004
|
const projectCfg = loadProjectConfig(process.cwd(), process.env);
|
|
782
1005
|
if (projectCfg.source === 'invalid') {
|
|
783
|
-
|
|
1006
|
+
const detail = projectCfg.errors[0] || 'unknown error';
|
|
1007
|
+
const more = projectCfg.errors.length > 1 ? ` (+${projectCfg.errors.length - 1} more)` : '';
|
|
1008
|
+
add(`construct.config.json invalid: ${detail}${more} — defaults applied; run \`construct config validate\``, false, false);
|
|
784
1009
|
} else if (projectCfg.source === 'file') {
|
|
785
1010
|
add(`construct.config.json valid`, true, true);
|
|
786
1011
|
} else {
|
|
@@ -879,7 +1104,7 @@ async function cmdDoctor() {
|
|
|
879
1104
|
return !fs.existsSync(path.join(ROOT_DIR, s.promptFile));
|
|
880
1105
|
});
|
|
881
1106
|
add('All specialist promptFiles resolve', brokenPromptFiles.length === 0);
|
|
882
|
-
const headhuntClassifierAgents = ['cx-
|
|
1107
|
+
const headhuntClassifierAgents = ['cx-operations', 'cx-researcher', 'cx-architect', 'cx-security', 'cx-engineer'];
|
|
883
1108
|
const registryNames = new Set(Object.values(registry.specialists ?? {}).map((s) => `cx-${s.name}`));
|
|
884
1109
|
const classifierOrphans = headhuntClassifierAgents.filter((n) => !registryNames.has(n));
|
|
885
1110
|
add('Headhunt classifier agents exist in registry', classifierOrphans.length === 0);
|
|
@@ -913,10 +1138,33 @@ async function cmdDoctor() {
|
|
|
913
1138
|
const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
|
|
914
1139
|
add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
|
|
915
1140
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1141
|
+
// isHealthy() connects to LanceDB, which creates the index directory on
|
|
1142
|
+
// first touch — checking existence first keeps doctor from provisioning
|
|
1143
|
+
// the vector store on a project that has never run semantic search
|
|
1144
|
+
// (construct-rf26.17: lazy provisioning, not "on every project touch").
|
|
1145
|
+
const { resolveStateDir } = await import('../lib/state-root.mjs');
|
|
1146
|
+
const lancedbPath = process.env.CONSTRUCT_LANCEDB_PATH || resolveStateDir(process.cwd(), 'lancedb', { ensureDir: false });
|
|
1147
|
+
if (fs.existsSync(lancedbPath)) {
|
|
1148
|
+
const { VectorClient } = await import('../lib/storage/vector-client.mjs');
|
|
1149
|
+
const vectorClient = new VectorClient({ env: process.env });
|
|
1150
|
+
const vectorOk = await vectorClient.isHealthy();
|
|
1151
|
+
add('Vector backend (LanceDB) reachable', vectorOk);
|
|
1152
|
+
|
|
1153
|
+
const { purgeExpiredData } = await import('../lib/storage/admin.mjs');
|
|
1154
|
+
const purge = await purgeExpiredData(process.cwd(), { env: process.env });
|
|
1155
|
+
if (purge.status === 'ok') {
|
|
1156
|
+
const sizeMb = (purge.sizeBytes / 1024 / 1024).toFixed(1);
|
|
1157
|
+
const oldest = purge.oldestRetainedAt ? new Date(purge.oldestRetainedAt).toISOString().slice(0, 10) : 'n/a';
|
|
1158
|
+
add(
|
|
1159
|
+
`Observations (vector index): ${sizeMb}MB, ${purge.remainingCount} row(s), oldest ${oldest}` +
|
|
1160
|
+
(purge.evictedCount > 0 ? ` — evicted ${purge.evictedCount} past ${purge.maxAgeDays}d/${purge.maxRows} rows` : ''),
|
|
1161
|
+
true,
|
|
1162
|
+
true,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
add('Vector backend (LanceDB): not yet provisioned (lazy — first semantic-search use creates it)', true, true);
|
|
1167
|
+
}
|
|
920
1168
|
|
|
921
1169
|
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
922
1170
|
const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
|
|
@@ -949,6 +1197,34 @@ async function cmdDoctor() {
|
|
|
949
1197
|
add(label, wired);
|
|
950
1198
|
}
|
|
951
1199
|
|
|
1200
|
+
// Half-initialized Beads: .claude/settings.json references bd but .beads/ is
|
|
1201
|
+
// absent or has no database file. A project set up before D3 (fail-closed Beads)
|
|
1202
|
+
// fired the hooks without a tracker, so bd exits non-zero with no database to read.
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const projectSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
|
1206
|
+
let hasHooksRef = false;
|
|
1207
|
+
if (fs.existsSync(projectSettingsPath)) {
|
|
1208
|
+
hasHooksRef = /\bbd\b/.test(fs.readFileSync(projectSettingsPath, 'utf8'));
|
|
1209
|
+
}
|
|
1210
|
+
if (hasHooksRef) {
|
|
1211
|
+
const beadsDir = path.join(process.cwd(), '.beads');
|
|
1212
|
+
const beadsHasDb = fs.existsSync(beadsDir) && (
|
|
1213
|
+
fs.existsSync(path.join(beadsDir, 'issues.jsonl')) ||
|
|
1214
|
+
fs.existsSync(path.join(beadsDir, 'metadata.json'))
|
|
1215
|
+
);
|
|
1216
|
+
add(
|
|
1217
|
+
beadsHasDb
|
|
1218
|
+
? 'Beads tracker initialized'
|
|
1219
|
+
: 'Beads tracker not initialized (hooks reference bd but .beads/ missing — run: bd init)',
|
|
1220
|
+
beadsHasDb,
|
|
1221
|
+
false,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
add(`Beads half-staged check failed: ${err.message}`, false, true);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
952
1228
|
const pluginRegistry = loadPluginRegistry({ cwd: process.cwd(), homeDir: HOME, rootDir: ROOT_DIR, env: process.env });
|
|
953
1229
|
add('Plugin manifests valid', pluginRegistry.valid);
|
|
954
1230
|
|
|
@@ -1121,16 +1397,17 @@ async function cmdDoctor() {
|
|
|
1121
1397
|
add('No recent hook failures (24h)', false, true);
|
|
1122
1398
|
}
|
|
1123
1399
|
|
|
1124
|
-
// Agent contracts integrity
|
|
1400
|
+
// Agent contracts integrity. A contract is broken if it lacks id,
|
|
1401
|
+
// producer, or consumer, or declares neither postconditions nor output —
|
|
1402
|
+
// mirrors the predicate in tests/doctor/contract-validation.test.mjs.
|
|
1125
1403
|
const { getAllContracts } = await import('../lib/specialist-contracts.mjs');
|
|
1126
1404
|
const contracts = getAllContracts();
|
|
1127
1405
|
add('Agent contracts loaded', contracts.length > 0);
|
|
1128
1406
|
if (contracts.length > 0) {
|
|
1129
|
-
const
|
|
1130
|
-
c.
|
|
1131
|
-
? false : false,
|
|
1407
|
+
const brokenContracts = contracts.filter((c) =>
|
|
1408
|
+
!c.id || !c.producer || !c.consumer || (!c.postconditions && !c.output),
|
|
1132
1409
|
);
|
|
1133
|
-
add('Agent contract schema intact',
|
|
1410
|
+
add('Agent contract schema intact', brokenContracts.length === 0);
|
|
1134
1411
|
}
|
|
1135
1412
|
|
|
1136
1413
|
// Skills profile — if present, check staleness against current tags
|
|
@@ -1155,6 +1432,67 @@ async function cmdDoctor() {
|
|
|
1155
1432
|
add('Registry integrity check', false);
|
|
1156
1433
|
}
|
|
1157
1434
|
|
|
1435
|
+
// Deployment-mode capability check: surfaces stub/not-implemented capabilities
|
|
1436
|
+
// for team/enterprise modes so users know which advertised features are unbuilt.
|
|
1437
|
+
// Solo mode is fully-implemented; this check is a no-op there.
|
|
1438
|
+
//
|
|
1439
|
+
// Enterprise mode additionally applies ADR-0057's status contract: a
|
|
1440
|
+
// FAIL-CLOSED capability (tenant-isolation, isolated-workers) is architecturally
|
|
1441
|
+
// non-trivial and "must not be a warning; it must be a hard error" surfaced
|
|
1442
|
+
// "at startup (during construct doctor)". A LATER capability (rbac,
|
|
1443
|
+
// signed-mcp-allowlists) has no runtime effect and must not read as a doctor
|
|
1444
|
+
// failure at all — it is reported informationally only.
|
|
1445
|
+
|
|
1446
|
+
try {
|
|
1447
|
+
const { getModeCapabilityStatus, getUnsupportedCapabilities, getCapabilities, categorizeEnterpriseCapability } =
|
|
1448
|
+
await import('../lib/mode-capabilities.mjs');
|
|
1449
|
+
const currentMode = getDeploymentMode(process.env);
|
|
1450
|
+
const capStatus = getModeCapabilityStatus(currentMode);
|
|
1451
|
+
if (currentMode === 'enterprise') {
|
|
1452
|
+
const allCaps = getCapabilities('enterprise');
|
|
1453
|
+
const failClosed = allCaps.filter((cap) => categorizeEnterpriseCapability(cap) === 'fail-closed');
|
|
1454
|
+
const later = allCaps.filter((cap) => categorizeEnterpriseCapability(cap) === 'later');
|
|
1455
|
+
if (failClosed.length > 0) {
|
|
1456
|
+
add(
|
|
1457
|
+
`Mode capabilities (enterprise): ${failClosed.length} fail-closed, not yet implemented (${failClosed.map((c) => c.label).join(', ')}) — ADR-0057 refuses these hard, not just a warning`,
|
|
1458
|
+
false,
|
|
1459
|
+
false,
|
|
1460
|
+
);
|
|
1461
|
+
} else {
|
|
1462
|
+
add('Mode capabilities (enterprise): no fail-closed gaps', true, true);
|
|
1463
|
+
}
|
|
1464
|
+
if (later.length > 0) {
|
|
1465
|
+
add(`Mode capabilities (enterprise): ${later.length} deferred to a later wave, no runtime effect (${later.map((c) => c.label).join(', ')})`, true, true);
|
|
1466
|
+
}
|
|
1467
|
+
} else if (capStatus !== 'fully-implemented') {
|
|
1468
|
+
const unsupported = getUnsupportedCapabilities(currentMode);
|
|
1469
|
+
add(
|
|
1470
|
+
`Mode capabilities (${currentMode}): ${unsupported.length} stub/not-implemented (${unsupported.map(c => c.label).join(', ')})`,
|
|
1471
|
+
false,
|
|
1472
|
+
true,
|
|
1473
|
+
);
|
|
1474
|
+
} else {
|
|
1475
|
+
add(`Mode capabilities (${currentMode})`, true, true);
|
|
1476
|
+
}
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
add(`Mode capabilities check failed: ${err.message}`, false, true);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
try {
|
|
1482
|
+
const currentMode = getDeploymentMode(process.env);
|
|
1483
|
+
if (currentMode === 'team' || currentMode === 'enterprise') {
|
|
1484
|
+
const { summarizeTeamHealth } = await import('../lib/team/health.mjs');
|
|
1485
|
+
const teamHealth = await summarizeTeamHealth({ rootDir: process.cwd(), env: process.env });
|
|
1486
|
+
add(`Team queue/worker health: ${teamHealth.summary}`, teamHealth.status !== 'unavailable');
|
|
1487
|
+
if (teamHealth.status !== 'unavailable') {
|
|
1488
|
+
add(`Team queue DLQ depth: ${teamHealth.deadLetter}`, teamHealth.deadLetter === 0, teamHealth.deadLetter > 0);
|
|
1489
|
+
add(`Team stale workers: ${teamHealth.staleWorkers}`, teamHealth.staleWorkers === 0, teamHealth.staleWorkers > 0);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
add(`Team health check failed: ${err.message}`, false);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1158
1496
|
// Reconciliation drift (ADR-0027). Any task reporting needsRepair is surfaced
|
|
1159
1497
|
// with its summary and the exact `construct sync --reconcile=<id>` command.
|
|
1160
1498
|
// Auto tasks that still show drift (e.g. a run with --no-reconcile) and all
|
|
@@ -1347,7 +1685,7 @@ async function cmdDev(args = []) {
|
|
|
1347
1685
|
process.exit(1);
|
|
1348
1686
|
}
|
|
1349
1687
|
|
|
1350
|
-
const { results, recovery } = await startServices({ rootDir:
|
|
1688
|
+
const { results, recovery } = await startServices({ rootDir: PROJECT_ROOT, homeDir: HOME, selected });
|
|
1351
1689
|
opLog.event('services', { results: results.map((r) => ({ name: r.name, status: r.status })) });
|
|
1352
1690
|
for (const svc of results) {
|
|
1353
1691
|
const label = svc.url ? `${svc.name} → ${svc.url}` : svc.name;
|
|
@@ -1388,14 +1726,14 @@ async function cmdDev(args = []) {
|
|
|
1388
1726
|
const { existsSync } = await import('node:fs');
|
|
1389
1727
|
const { join } = await import('node:path');
|
|
1390
1728
|
|
|
1391
|
-
const embedConfigExists = existsSync(join(
|
|
1392
|
-
const config = loadProjectConfig(
|
|
1729
|
+
const embedConfigExists = existsSync(join(PROJECT_ROOT, 'embed.yaml'));
|
|
1730
|
+
const config = loadProjectConfig(PROJECT_ROOT);
|
|
1393
1731
|
const autoEmbed = config.config?.autoEmbed;
|
|
1394
|
-
|
|
1732
|
+
|
|
1395
1733
|
if (embedConfigExists && autoEmbed) {
|
|
1396
1734
|
try {
|
|
1397
1735
|
const { runEmbedCli } = await import('../lib/embed/cli.mjs');
|
|
1398
|
-
await runEmbedCli(['start'], { rootDir:
|
|
1736
|
+
await runEmbedCli(['start'], { rootDir: PROJECT_ROOT });
|
|
1399
1737
|
opLog.event('embed', { status: 'started' });
|
|
1400
1738
|
} catch (err) {
|
|
1401
1739
|
warn(`embed start failed: ${err.message} — check ~/.cx/runtime/embed-daemon.log`);
|
|
@@ -1748,18 +2086,38 @@ async function cmdPrune(args) {
|
|
|
1748
2086
|
const dryRun = args.includes('--dry-run');
|
|
1749
2087
|
const { planPrune, executePrune, measureUsage } = await import('../lib/resources/budget.mjs');
|
|
1750
2088
|
const actions = planPrune(process.cwd(), process.env);
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2089
|
+
let pruned = false;
|
|
2090
|
+
if (actions.length > 0) {
|
|
2091
|
+
pruned = true;
|
|
2092
|
+
const byCat = {};
|
|
2093
|
+
let totalBytes = 0;
|
|
2094
|
+
for (const a of actions) {
|
|
2095
|
+
byCat[a.category] = (byCat[a.category] || 0) + 1;
|
|
2096
|
+
totalBytes += a.bytes || 0;
|
|
2097
|
+
}
|
|
2098
|
+
println(`Plan: prune ${actions.length} file(s), free ${Math.round(totalBytes / 1024)}KB`);
|
|
2099
|
+
for (const [cat, count] of Object.entries(byCat)) println(` ${cat}: ${count} file(s)`);
|
|
2100
|
+
if (!dryRun) {
|
|
2101
|
+
const result = executePrune(actions);
|
|
2102
|
+
info(`Pruned ${result.removed.length} file(s), freed ${Math.round(result.bytesFreed / 1024)}KB.`);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// Vector-indexed observations live in LanceDB, not flat files, so they
|
|
2107
|
+
// fall outside planPrune/executePrune's file-list model — purgeExpiredData
|
|
2108
|
+
// is a no-op when the index was never provisioned, matching the lazy
|
|
2109
|
+
// vector-index contract (construct-rf26.17).
|
|
2110
|
+
if (!dryRun) {
|
|
2111
|
+
const { purgeExpiredData } = await import('../lib/storage/admin.mjs');
|
|
2112
|
+
const purge = await purgeExpiredData(process.cwd(), { env: process.env });
|
|
2113
|
+
if (purge.status === 'ok' && purge.evictedCount > 0) {
|
|
2114
|
+
pruned = true;
|
|
2115
|
+
info(`Evicted ${purge.evictedCount} observation(s) past ${purge.maxAgeDays}d/${purge.maxRows}-row cap (${purge.remainingCount} retained).`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
if (!pruned) { info('Nothing to prune. .cx/ is within all configured budgets.'); return; }
|
|
1760
2120
|
if (dryRun) { info('--dry-run: no files removed.'); return; }
|
|
1761
|
-
const result = executePrune(actions);
|
|
1762
|
-
info(`Pruned ${result.removed.length} file(s), freed ${Math.round(result.bytesFreed / 1024)}KB.`);
|
|
1763
2121
|
const after = measureUsage(process.cwd(), process.env);
|
|
1764
2122
|
println(`.cx/ now at ${Math.round(after.totalCxBytes / 1024 / 1024)}MB / ${Math.round(after.totalCxCap / 1024 / 1024)}MB cap (${Math.round(after.totalCxUsageRatio * 100)}%).`);
|
|
1765
2123
|
}
|
|
@@ -2223,7 +2581,7 @@ async function cmdIntake(args) {
|
|
|
2223
2581
|
const queue = createIntakeQueue(cwd, process.env);
|
|
2224
2582
|
|
|
2225
2583
|
if (sub === 'list') {
|
|
2226
|
-
const pending = queue.listPending();
|
|
2584
|
+
const pending = await queue.listPending();
|
|
2227
2585
|
const rows = pending.map((p) => {
|
|
2228
2586
|
const t = p.triage || {};
|
|
2229
2587
|
return [p.id, t.intakeType || 'unknown', t.rdStage || 'unknown', t.primaryOwner || '—', t.recommendedAction || '—'];
|
|
@@ -2235,7 +2593,7 @@ async function cmdIntake(args) {
|
|
|
2235
2593
|
const id = args[1];
|
|
2236
2594
|
if (sub === 'show') {
|
|
2237
2595
|
if (!id) { errorln('Usage: construct intake show <id>'); process.exit(1); }
|
|
2238
|
-
const entry = queue.read(id);
|
|
2596
|
+
const entry = await queue.read(id);
|
|
2239
2597
|
if (!entry) { errorln(`No ${rebrand.signalNoun} with id: ${id}`); process.exit(1); }
|
|
2240
2598
|
const t = entry.triage || {};
|
|
2241
2599
|
println(`${rebrand.signalNoun.charAt(0).toUpperCase() + rebrand.signalNoun.slice(1)} ${entry.id}`);
|
|
@@ -2349,7 +2707,7 @@ async function cmdIntake(args) {
|
|
|
2349
2707
|
|
|
2350
2708
|
let stamped = null;
|
|
2351
2709
|
if (outputPath) {
|
|
2352
|
-
const packet = queue.read(id);
|
|
2710
|
+
const packet = await queue.read(id);
|
|
2353
2711
|
if (!packet) {
|
|
2354
2712
|
errorln(`No intake packet found: ${id}`);
|
|
2355
2713
|
process.exit(1);
|
|
@@ -2375,7 +2733,7 @@ async function cmdIntake(args) {
|
|
|
2375
2733
|
|| (outputPath ? `output: ${outputPath}` : '');
|
|
2376
2734
|
|
|
2377
2735
|
try {
|
|
2378
|
-
const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
|
|
2736
|
+
const r = await queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
|
|
2379
2737
|
if (stamped) {
|
|
2380
2738
|
info(`Marked ${r.id} processed. Stamped ${outputPath} with intake_id=${id} (confidence=${stamped.intake_confidence ?? 'n/a'}).`);
|
|
2381
2739
|
} else if (noOutputOutcome) {
|
|
@@ -2394,7 +2752,7 @@ async function cmdIntake(args) {
|
|
|
2394
2752
|
if (!id) { errorln('Usage: construct intake skip <id> [--reason=…]'); process.exit(1); }
|
|
2395
2753
|
const flags = parseKvFlags(args.slice(2), ['reason']);
|
|
2396
2754
|
try {
|
|
2397
|
-
const r = queue.markSkipped(id, { skippedBy: 'construct-intake-cli', reason: flags.reason || '' });
|
|
2755
|
+
const r = await queue.markSkipped(id, { skippedBy: 'construct-intake-cli', reason: flags.reason || '' });
|
|
2398
2756
|
info(`Marked ${r.id} skipped.`);
|
|
2399
2757
|
} catch (err) {
|
|
2400
2758
|
errorln(err.message || 'mark skipped failed');
|
|
@@ -2406,7 +2764,7 @@ async function cmdIntake(args) {
|
|
|
2406
2764
|
if (sub === 'reopen') {
|
|
2407
2765
|
if (!id) { errorln('Usage: construct intake reopen <id>'); process.exit(1); }
|
|
2408
2766
|
try {
|
|
2409
|
-
const r = queue.reopen(id);
|
|
2767
|
+
const r = await queue.reopen(id);
|
|
2410
2768
|
info(`Reopened ${r.id} (from ${r.from}).`);
|
|
2411
2769
|
} catch (err) {
|
|
2412
2770
|
errorln(err.message || 'reopen failed');
|
|
@@ -3095,7 +3453,7 @@ async function cmdIntakeIntegrate(id, args, cwd) {
|
|
|
3095
3453
|
|
|
3096
3454
|
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
3097
3455
|
const queue = createIntakeQueue(cwd, process.env);
|
|
3098
|
-
const entry = queue.read(id);
|
|
3456
|
+
const entry = await queue.read(id);
|
|
3099
3457
|
if (!entry) { errorln(`No intake packet with id: ${id}`); process.exit(1); }
|
|
3100
3458
|
|
|
3101
3459
|
let result;
|
|
@@ -3471,7 +3829,7 @@ async function cmdGraph(args) {
|
|
|
3471
3829
|
if (!intakeId) { errorln('Usage: construct graph from-intake <intake-id>'); process.exit(1); }
|
|
3472
3830
|
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
3473
3831
|
const queue = createIntakeQueue(cwd, process.env);
|
|
3474
|
-
const entry = queue.read(intakeId);
|
|
3832
|
+
const entry = await queue.read(intakeId);
|
|
3475
3833
|
if (!entry) { errorln(`No intake packet: ${intakeId}`); process.exit(1); }
|
|
3476
3834
|
const graph = generateTaskGraphFromTriage({
|
|
3477
3835
|
triage: entry.triage,
|
|
@@ -3509,6 +3867,76 @@ async function cmdGraph(args) {
|
|
|
3509
3867
|
process.exit(1);
|
|
3510
3868
|
}
|
|
3511
3869
|
|
|
3870
|
+
function printFlowHelp() {
|
|
3871
|
+
println('Construct flow — resume a checkpointed flow-engine run (ADR-0067).');
|
|
3872
|
+
println('');
|
|
3873
|
+
println('Usage:');
|
|
3874
|
+
println(' construct flow resume <run-id> --flow=<path> [--state=<json>]');
|
|
3875
|
+
println(' Resume <run-id> from its last checkpoint (or start it if none');
|
|
3876
|
+
println(' exists yet) and drive it to completion. <path> loads a JS');
|
|
3877
|
+
println(' flow module (default export from defineFlow). --state supplies');
|
|
3878
|
+
println(' the initial state as a JSON string, used only when starting fresh.');
|
|
3879
|
+
println(' construct flow status <run-id> Print a checkpoint\'s persisted status without driving it.');
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
async function cmdFlow(args) {
|
|
3883
|
+
const sub = args[0];
|
|
3884
|
+
if (!sub || sub === '--help' || sub === '-h') { printFlowHelp(); return; }
|
|
3885
|
+
|
|
3886
|
+
const { loadCheckpoint, runCheckpointed } = await import('../lib/flows/checkpoint.mjs');
|
|
3887
|
+
const cwd = process.cwd();
|
|
3888
|
+
|
|
3889
|
+
if (sub === 'status') {
|
|
3890
|
+
const runId = args[1];
|
|
3891
|
+
if (!runId) { errorln('Usage: construct flow status <run-id>'); process.exit(1); }
|
|
3892
|
+
const checkpoint = loadCheckpoint(cwd, runId);
|
|
3893
|
+
if (!checkpoint) { errorln(`No checkpoint for run "${runId}".`); process.exit(1); }
|
|
3894
|
+
println(`Run: ${checkpoint.runId}`);
|
|
3895
|
+
println(`Flow: ${checkpoint.flowId ?? '(unnamed)'}`);
|
|
3896
|
+
println(`Updated: ${checkpoint.updatedAt}`);
|
|
3897
|
+
println(`Status: ${checkpoint.run.status}`);
|
|
3898
|
+
println(`Completed steps: ${checkpoint.run.completed.join(', ') || '(none)'}`);
|
|
3899
|
+
println(`Frontier: ${checkpoint.run.frontier.join(', ') || '(empty)'}`);
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
if (sub === 'resume') {
|
|
3904
|
+
const runId = args[1];
|
|
3905
|
+
const flags = parseKvFlags(args.slice(2), ['flow', 'state']);
|
|
3906
|
+
if (!runId || !flags.flow) { errorln('Usage: construct flow resume <run-id> --flow=<path> [--state=<json>]'); process.exit(1); }
|
|
3907
|
+
const { loadFlow } = await import('../lib/flows/define.mjs');
|
|
3908
|
+
let flow;
|
|
3909
|
+
try {
|
|
3910
|
+
flow = await loadFlow(path.resolve(cwd, flags.flow));
|
|
3911
|
+
} catch (err) {
|
|
3912
|
+
errorln(`construct flow resume: failed to load flow "${flags.flow}": ${err.message}`);
|
|
3913
|
+
process.exit(1);
|
|
3914
|
+
}
|
|
3915
|
+
let initialState = {};
|
|
3916
|
+
if (flags.state) {
|
|
3917
|
+
try {
|
|
3918
|
+
initialState = JSON.parse(flags.state);
|
|
3919
|
+
} catch (err) {
|
|
3920
|
+
errorln(`construct flow resume: --state is not valid JSON: ${err.message}`);
|
|
3921
|
+
process.exit(1);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
const existed = Boolean(loadCheckpoint(cwd, runId));
|
|
3925
|
+
const run = await runCheckpointed(cwd, runId, flow, initialState);
|
|
3926
|
+
info(`${existed ? 'Resumed' : 'Started'} run ${runId} (flow "${flow.id ?? '(unnamed)'}")`);
|
|
3927
|
+
println(`Status: ${run.status}`);
|
|
3928
|
+
println(`Steps completed (cumulative): ${run.history.length}`);
|
|
3929
|
+
if (run.status === 'error' || run.status === 'budget-exhausted') {
|
|
3930
|
+
println(`Error: ${JSON.stringify(run.error)}`);
|
|
3931
|
+
process.exit(1);
|
|
3932
|
+
}
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
errorln(`Unknown flow subcommand: ${sub}. Available: resume, status`);
|
|
3937
|
+
process.exit(1);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3512
3940
|
async function cmdConfig(args) {
|
|
3513
3941
|
const sub = args[0];
|
|
3514
3942
|
|
|
@@ -3695,6 +4123,7 @@ async function cmdOrchestrate(args) {
|
|
|
3695
4123
|
const timeoutMs = Number(eqFlag('--timeout-ms') || 8000);
|
|
3696
4124
|
let observedTools = splitCsv(eqFlag('--observed-tools'));
|
|
3697
4125
|
let reachableTools = splitCsv(eqFlag('--reachable-tools'));
|
|
4126
|
+
const callerSuppliedObservations = Boolean(observedTools || reachableTools);
|
|
3698
4127
|
let probeError = null;
|
|
3699
4128
|
|
|
3700
4129
|
if (!rest.includes('--no-probe') && !observedTools && !reachableTools) {
|
|
@@ -3719,21 +4148,52 @@ async function cmdOrchestrate(args) {
|
|
|
3719
4148
|
reachableTools,
|
|
3720
4149
|
requiredTools: splitCsv(eqFlag('--required-tools')),
|
|
3721
4150
|
clientContractVersion: eqFlag('--client-contract-version'),
|
|
3722
|
-
observationScope: eqFlag('--observation-scope') || (observedTools || reachableTools ? '
|
|
4151
|
+
observationScope: eqFlag('--observation-scope') || (callerSuppliedObservations ? 'host-session' : (observedTools || reachableTools ? 'local-probe' : 'local-config')),
|
|
3723
4152
|
probeError,
|
|
3724
4153
|
authRequired: rest.includes('--auth-required'),
|
|
3725
4154
|
}, { env: process.env, cwd: process.cwd() });
|
|
3726
4155
|
let eventResult = null;
|
|
3727
4156
|
try { eventResult = recordOrchestrationReadinessEvent(readiness, { homeDir: HOME }); } catch { /* telemetry must not break preflight */ }
|
|
3728
4157
|
|
|
4158
|
+
// Opt-in deep credential check (construct-1yhp.5). Default preflight stays
|
|
4159
|
+
// presence-first — `--deep` performs the real network auth probe, only for
|
|
4160
|
+
// providers the presence-based catalog already marks configured.
|
|
4161
|
+
let deepProbes = null;
|
|
4162
|
+
if (rest.includes('--deep')) {
|
|
4163
|
+
try {
|
|
4164
|
+
const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
|
|
4165
|
+
const { probeProviderConnection } = await import('../lib/providers/connection-probe.mjs');
|
|
4166
|
+
const { providers } = getProviderModelCatalog({ env: process.env });
|
|
4167
|
+
const probeable = providers.filter((p) => p.configured && ['openrouter', 'anthropic', 'openai'].includes(p.id));
|
|
4168
|
+
deepProbes = [];
|
|
4169
|
+
for (const p of probeable) {
|
|
4170
|
+
const result = await probeProviderConnection(p.id, { env: process.env, cwd: process.cwd() });
|
|
4171
|
+
deepProbes.push({ provider: p.id, ...result });
|
|
4172
|
+
}
|
|
4173
|
+
} catch (err) {
|
|
4174
|
+
deepProbes = { error: err.message };
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
|
|
3729
4178
|
if (wantsJson) {
|
|
3730
|
-
println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null }, null, 2));
|
|
4179
|
+
println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null, deepProbes }, null, 2));
|
|
3731
4180
|
process.exit(readiness.attached ? 0 : 1);
|
|
3732
4181
|
}
|
|
3733
4182
|
|
|
3734
4183
|
println(summarizeOrchestrationReadiness(readiness));
|
|
3735
4184
|
println(`Reason: ${readiness.reasonCode}`);
|
|
3736
4185
|
println(`Next: ${readiness.nextStep}`);
|
|
4186
|
+
const modelLine = ['reasoning', 'standard', 'fast']
|
|
4187
|
+
.map((tier) => `${tier}=${readiness.modelResolved?.[tier] ? 'resolved' : 'unresolved'}`)
|
|
4188
|
+
.join(' ');
|
|
4189
|
+
println(`Execution: workerBackend=${readiness.workerBackend} webMode=${readiness.webMode} credentialMaterializable=${readiness.credentialMaterializable} (${modelLine})`);
|
|
4190
|
+
if (Array.isArray(deepProbes)) {
|
|
4191
|
+
if (deepProbes.length === 0) println('Deep provider probe: no presence-configured openrouter/anthropic/openai provider to probe.');
|
|
4192
|
+
for (const probe of deepProbes) {
|
|
4193
|
+
if (probe.ok) println(`Deep provider probe — ${probe.provider}: auth-verified (SERVING)`);
|
|
4194
|
+
else println(`Deep provider probe — ${probe.provider}: ${probe.code}${probe.status ? ` (${probe.status})` : ''} (unverified/failing)`);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
3737
4197
|
println(`Diagnostic id: ${readiness.diagnosticBundle?.diagnosticId}`);
|
|
3738
4198
|
process.exit(readiness.attached ? 0 : 1);
|
|
3739
4199
|
}
|
|
@@ -3751,6 +4211,10 @@ async function cmdOrchestrate(args) {
|
|
|
3751
4211
|
if (sub === 'run') {
|
|
3752
4212
|
const text = flag('--text') || positional.join(' ').trim();
|
|
3753
4213
|
if (!text) { errorln('Usage: construct orchestrate run "<request>" --remote [--strategy S] [--worker-backend provider] [--json]'); process.exit(1); }
|
|
4214
|
+
if (flag('--worker-backend') === 'host') {
|
|
4215
|
+
errorln('--worker-backend=host requires an attached MCP host session to execute the materialized prompts and submit results back — the CLI (including --remote) has no such session. Use --worker-backend=inline or --worker-backend=provider, or invoke orchestration_run through an MCP host instead.');
|
|
4216
|
+
process.exit(1);
|
|
4217
|
+
}
|
|
3754
4218
|
const res = await fetch(`${base}/api/orchestration/runs`, { method: 'POST', headers, body: JSON.stringify({ request: text, workflowType: flag('--workflow-type'), requestedStrategy: flag('--strategy') || 'auto', host: flag('--host'), hostModel: flag('--host-model'), hostProvider: flag('--host-provider'), workerBackend: flag('--worker-backend'), fileCount: Number(flag('--file-count') || 0), moduleCount: Number(flag('--module-count') || 0) }) });
|
|
3755
4219
|
const env = await res.json();
|
|
3756
4220
|
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
@@ -3759,6 +4223,7 @@ async function cmdOrchestrate(args) {
|
|
|
3759
4223
|
return;
|
|
3760
4224
|
}
|
|
3761
4225
|
if (sub === 'status') {
|
|
4226
|
+
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
3762
4227
|
const runId = positional[0];
|
|
3763
4228
|
const path = runId ? `/api/orchestration/runs/${encodeURIComponent(runId)}` : '/api/orchestration/runs';
|
|
3764
4229
|
const res = await fetch(`${base}${path}`, { headers });
|
|
@@ -3766,10 +4231,16 @@ async function cmdOrchestrate(args) {
|
|
|
3766
4231
|
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
3767
4232
|
if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
|
|
3768
4233
|
if (runId) {
|
|
3769
|
-
|
|
4234
|
+
// AP5.4: a remote run payload is just as subject to the honest
|
|
4235
|
+
// terminal-state taxonomy as a local one — never echo its raw status.
|
|
4236
|
+
const shaped = shapeRun(env.data);
|
|
4237
|
+
println(`Run ${env.data.runId} — ${shaped.status} · executionMode=${shaped.executionMode}${shaped.degraded ? ` · degraded: ${shaped.degradationReason}` : ''}`);
|
|
3770
4238
|
for (const t of env.data.tasks || []) println(` ${t.id} ${t.role} — ${t.status}${t.output ? ` (${t.output.length} chars)` : ''}`);
|
|
3771
4239
|
} else {
|
|
3772
|
-
for (const r of env.data.runs || [])
|
|
4240
|
+
for (const r of env.data.runs || []) {
|
|
4241
|
+
const shaped = shapeRun(r);
|
|
4242
|
+
println(` ${r.runId} — ${shaped.status}${shaped.degraded ? ' (degraded)' : ''} · ${shaped.executionMode || r.executionMode || ''} · ${r.createdAt}`);
|
|
4243
|
+
}
|
|
3773
4244
|
}
|
|
3774
4245
|
return;
|
|
3775
4246
|
}
|
|
@@ -3781,9 +4252,21 @@ async function cmdOrchestrate(args) {
|
|
|
3781
4252
|
|
|
3782
4253
|
if (sub === 'run') {
|
|
3783
4254
|
const { runOrchestration, planRun, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
4255
|
+
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
3784
4256
|
const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
|
|
3785
4257
|
const text = flag('--text') || positional.join(' ').trim();
|
|
3786
4258
|
if (!text) { errorln('Usage: construct orchestrate run "<request>" [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]'); process.exit(1); }
|
|
4259
|
+
const cliWorkerBackend = flag('--worker-backend');
|
|
4260
|
+
// The host worker backend materializes prompts for an attached MCP host
|
|
4261
|
+
// session to execute and submit back via orchestration_task_result — a
|
|
4262
|
+
// bare CLI invocation has no such session on the other end, so a run
|
|
4263
|
+
// planned this way would stand at 'awaiting-host' forever with nobody to
|
|
4264
|
+
// submit results. Fail loud here instead of producing that silent
|
|
4265
|
+
// abandoned run (LMCP host-execution).
|
|
4266
|
+
if (cliWorkerBackend === 'host') {
|
|
4267
|
+
errorln('--worker-backend=host requires an attached MCP host session to execute the materialized prompts and submit results back — the CLI has no such session. Use --worker-backend=inline (prepare only) or --worker-backend=provider (needs a provider key), or invoke orchestration_run through an MCP host instead.');
|
|
4268
|
+
process.exit(1);
|
|
4269
|
+
}
|
|
3787
4270
|
const request = {
|
|
3788
4271
|
request: text,
|
|
3789
4272
|
workflowType: flag('--workflow-type'),
|
|
@@ -3792,14 +4275,25 @@ async function cmdOrchestrate(args) {
|
|
|
3792
4275
|
host: flag('--host'),
|
|
3793
4276
|
hostModel: flag('--host-model'),
|
|
3794
4277
|
hostProvider: flag('--host-provider'),
|
|
3795
|
-
workerBackend:
|
|
4278
|
+
workerBackend: cliWorkerBackend,
|
|
3796
4279
|
fileCount: Number(flag('--file-count') || 0),
|
|
3797
4280
|
moduleCount: Number(flag('--module-count') || 0),
|
|
3798
4281
|
};
|
|
3799
|
-
|
|
4282
|
+
// runOrchestration/executeRun resolve the worker backend from opts.workerBackend,
|
|
4283
|
+
// not from request — the flag must reach opts explicitly or an override is silently
|
|
4284
|
+
// dropped (construct-1xlz).
|
|
4285
|
+
|
|
4286
|
+
const runOpts = cliWorkerBackend ? { workerBackend: cliWorkerBackend } : {};
|
|
4287
|
+
const run = rest.includes('--no-execute') ? await planRun(request, runOpts) : await runOrchestration(request, runOpts);
|
|
3800
4288
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3801
|
-
|
|
3802
|
-
|
|
4289
|
+
|
|
4290
|
+
// AP5.1/AP5.4: the CLI must never print the raw persisted status — a
|
|
4291
|
+
// pre-taxonomy run record can still carry a bare 'completed' despite being
|
|
4292
|
+
// degraded or prepare-only, so route through the same honest read-model
|
|
4293
|
+
// (shapeRun) the MCP orchestration_status tool uses.
|
|
4294
|
+
const shaped = shapeRun(run);
|
|
4295
|
+
println(`Run ${run.runId} — ${shaped.status} · executionMode=${shaped.executionMode} · backend=${run.workerBackend} · hostRole=${run.hostRole}`);
|
|
4296
|
+
if (shaped.degraded) println(` degraded: ${shaped.degradationReason}`);
|
|
3803
4297
|
if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
|
|
3804
4298
|
else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
|
|
3805
4299
|
for (const t of run.tasks) {
|
|
@@ -3810,12 +4304,14 @@ async function cmdOrchestrate(args) {
|
|
|
3810
4304
|
|
|
3811
4305
|
if (sub === 'status') {
|
|
3812
4306
|
const { getRun, getRuns, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
4307
|
+
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
3813
4308
|
const runId = rest.find((a) => !a.startsWith('--'));
|
|
3814
4309
|
if (runId) {
|
|
3815
4310
|
const run = await getRun(process.cwd(), runId);
|
|
3816
4311
|
if (!run) { errorln(`Run not found: ${runId}`); process.exit(1); }
|
|
3817
4312
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3818
|
-
|
|
4313
|
+
const shaped = shapeRun(run);
|
|
4314
|
+
println(`Run ${run.runId} — ${shaped.status} · executionMode=${shaped.executionMode}${shaped.degraded ? ` · degraded: ${shaped.degradationReason}` : ''}`);
|
|
3819
4315
|
for (const t of run.tasks) {
|
|
3820
4316
|
println(` ${t.id} ${t.role} — ${t.status}`);
|
|
3821
4317
|
if (t.reasoning) println(` reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
@@ -3825,7 +4321,10 @@ async function cmdOrchestrate(args) {
|
|
|
3825
4321
|
const runs = await getRuns(process.cwd());
|
|
3826
4322
|
if (wantsJson) { println(JSON.stringify(runs, null, 2)); return; }
|
|
3827
4323
|
if (!runs.length) { println('No orchestration runs recorded.'); return; }
|
|
3828
|
-
for (const r of runs)
|
|
4324
|
+
for (const r of runs) {
|
|
4325
|
+
const shaped = shapeRun(r);
|
|
4326
|
+
println(` ${r.runId} — ${shaped.status}${shaped.degraded ? ' (degraded)' : ''} · ${shaped.executionMode || r.executionMode || ''} · ${r.createdAt}`);
|
|
4327
|
+
}
|
|
3829
4328
|
return;
|
|
3830
4329
|
}
|
|
3831
4330
|
|
|
@@ -3835,6 +4334,85 @@ async function cmdOrchestrate(args) {
|
|
|
3835
4334
|
println(' preflight [--host H] [--json] [--no-probe] [--observed-tools=a,b] [--reachable-tools=a,b]');
|
|
3836
4335
|
}
|
|
3837
4336
|
|
|
4337
|
+
async function cmdDb(args) {
|
|
4338
|
+
const sub = args[0] || 'status';
|
|
4339
|
+
const wantsJson = args.includes('--json');
|
|
4340
|
+
const sql = createSqlClient(process.env);
|
|
4341
|
+
|
|
4342
|
+
async function finish(code = 0) {
|
|
4343
|
+
await closeSqlClient(sql);
|
|
4344
|
+
if (code) process.exit(code);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
if (!sql) {
|
|
4348
|
+
const result = {
|
|
4349
|
+
status: 'unavailable',
|
|
4350
|
+
reason: 'sql-client-unavailable',
|
|
4351
|
+
message: 'Set DATABASE_URL or CONSTRUCT_DATABASE_URL and install optional dependency "postgres".',
|
|
4352
|
+
migrations: [],
|
|
4353
|
+
};
|
|
4354
|
+
if (wantsJson) println(JSON.stringify(result, null, 2));
|
|
4355
|
+
else {
|
|
4356
|
+
println('Database unavailable.');
|
|
4357
|
+
println(`Reason: ${result.message}`);
|
|
4358
|
+
}
|
|
4359
|
+
return finish(1);
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
try {
|
|
4363
|
+
const { probeSqlClient } = await import('../lib/storage/backend.mjs');
|
|
4364
|
+
const { getMigrationStatus, applyMigrations } = await import('../lib/db/migrate.mjs');
|
|
4365
|
+
const probe = await probeSqlClient(sql);
|
|
4366
|
+
if (probe.status !== 'available') {
|
|
4367
|
+
const result = { status: probe.status, reason: 'postgres-unavailable', message: probe.message, migrations: [] };
|
|
4368
|
+
if (wantsJson) println(JSON.stringify(result, null, 2));
|
|
4369
|
+
else {
|
|
4370
|
+
println('Database unavailable.');
|
|
4371
|
+
println(`Reason: ${probe.message}`);
|
|
4372
|
+
}
|
|
4373
|
+
return finish(1);
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
if (sub === 'status') {
|
|
4377
|
+
const migrations = await getMigrationStatus(sql);
|
|
4378
|
+
const result = {
|
|
4379
|
+
status: 'available',
|
|
4380
|
+
pending: migrations.filter((m) => !m.applied).map((m) => m.id),
|
|
4381
|
+
migrations: migrations.map((m) => ({ id: m.id, applied: m.applied })),
|
|
4382
|
+
};
|
|
4383
|
+
if (wantsJson) println(JSON.stringify(result, null, 2));
|
|
4384
|
+
else {
|
|
4385
|
+
println(`Database available. ${result.pending.length} pending migration${result.pending.length === 1 ? '' : 's'}.`);
|
|
4386
|
+
for (const migration of result.migrations) println(` ${migration.applied ? 'applied' : 'pending'} ${migration.id}`);
|
|
4387
|
+
}
|
|
4388
|
+
return finish(0);
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
if (sub === 'migrate') {
|
|
4392
|
+
const result = await applyMigrations(sql);
|
|
4393
|
+
const output = {
|
|
4394
|
+
status: 'available',
|
|
4395
|
+
applied: result.applied,
|
|
4396
|
+
pending: result.status.filter((m) => !m.applied).map((m) => m.id),
|
|
4397
|
+
migrations: result.status.map((m) => ({ id: m.id, applied: m.applied })),
|
|
4398
|
+
};
|
|
4399
|
+
if (wantsJson) println(JSON.stringify(output, null, 2));
|
|
4400
|
+
else {
|
|
4401
|
+
println(`Applied ${output.applied.length} migration${output.applied.length === 1 ? '' : 's'}.`);
|
|
4402
|
+
for (const id of output.applied) println(` ${id}`);
|
|
4403
|
+
}
|
|
4404
|
+
return finish(0);
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
errorln('Usage: construct db <status|migrate> [--json]');
|
|
4408
|
+
return finish(1);
|
|
4409
|
+
} catch (err) {
|
|
4410
|
+
await closeSqlClient(sql);
|
|
4411
|
+
errorln(err?.message || String(err));
|
|
4412
|
+
process.exit(1);
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
|
|
3838
4416
|
async function cmdModels(args) {
|
|
3839
4417
|
const envPath = getUserEnvPath(HOME);
|
|
3840
4418
|
const sub = args[0] && !args[0].startsWith('-') ? args[0] : null;
|
|
@@ -4903,6 +5481,10 @@ async function cmdSpecialist(args) {
|
|
|
4903
5481
|
const sub = args[0];
|
|
4904
5482
|
if (sub === 'lint') return cmdLintPrompts();
|
|
4905
5483
|
|
|
5484
|
+
if (sub === 'create' && args.includes('--custom')) {
|
|
5485
|
+
return cmdSpecialistCreateCustom(args.slice(1));
|
|
5486
|
+
}
|
|
5487
|
+
|
|
4906
5488
|
if (sub === 'create') {
|
|
4907
5489
|
const role = args[1];
|
|
4908
5490
|
if (!role) { errorln('Usage: construct specialist create <role>'); process.exit(1); }
|
|
@@ -4939,6 +5521,91 @@ async function cmdSpecialist(args) {
|
|
|
4939
5521
|
process.exit(1);
|
|
4940
5522
|
}
|
|
4941
5523
|
|
|
5524
|
+
// `construct specialist create <id> --custom` scaffolds a user-authored specialist
|
|
5525
|
+
// (construct-rf26.13) — distinct from the maintainer-facing `specialist create <role>`
|
|
5526
|
+
// above, which only stubs a prompt file for the built-in roster. `--custom` writes a
|
|
5527
|
+
// full registry-shaped JSON record (role, skills, fence, modelTier, delegation) into
|
|
5528
|
+
// the project's `.cx/org/` layer by default, or `~/.construct/org/` with `--user`.
|
|
5529
|
+
|
|
5530
|
+
async function cmdSpecialistCreateCustom(args) {
|
|
5531
|
+
const id = args.find((a) => !a.startsWith('--'));
|
|
5532
|
+
const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
5533
|
+
const list = (name) => (flag(name) || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
5534
|
+
const usage = 'Usage: construct specialist create <id> --custom --role=<role> --team=<team-id> --description="…" --skills=<bundle/skill,…> --fence-paths=<glob,…> [--model-tier=fast|standard|reasoning] [--handoff=role,…] [--tools=Read,Grep,…] [--user] [--force] [--root=<dir>]';
|
|
5535
|
+
|
|
5536
|
+
const role = flag('role');
|
|
5537
|
+
const team = flag('team');
|
|
5538
|
+
const description = flag('description');
|
|
5539
|
+
const skills = list('skills');
|
|
5540
|
+
const fencePaths = list('fence-paths');
|
|
5541
|
+
const missing = [
|
|
5542
|
+
!id && 'id (positional)', !role && '--role', !team && '--team',
|
|
5543
|
+
!description && '--description', !skills.length && '--skills', !fencePaths.length && '--fence-paths',
|
|
5544
|
+
].filter(Boolean);
|
|
5545
|
+
if (missing.length) {
|
|
5546
|
+
errorln(usage);
|
|
5547
|
+
errorln(`Missing required: ${missing.join(', ')}`);
|
|
5548
|
+
process.exit(1);
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5551
|
+
const rootDir = flag('root') ? path.resolve(flag('root')) : ROOT_DIR;
|
|
5552
|
+
const scope = args.includes('--user') ? 'user' : 'project';
|
|
5553
|
+
const { createCustomSpecialist } = await import('../lib/registry/custom-scaffold.mjs');
|
|
5554
|
+
try {
|
|
5555
|
+
const { relPath, promptPath, scope: writtenScope } = createCustomSpecialist({
|
|
5556
|
+
rootDir, scope, id, role, description,
|
|
5557
|
+
modelTier: flag('model-tier') || 'standard',
|
|
5558
|
+
skills, fence: { allowedPaths: fencePaths },
|
|
5559
|
+
team, handoffCandidates: list('handoff'),
|
|
5560
|
+
claudeTools: flag('tools') || undefined,
|
|
5561
|
+
force: args.includes('--force'),
|
|
5562
|
+
});
|
|
5563
|
+
ok(`Created ${relPath} (${writtenScope} scope) and ${path.relative(rootDir, promptPath)}.`);
|
|
5564
|
+
println('Run `construct sync` to pick it up — it then resolves through orchestration the same way a built-in specialist does.');
|
|
5565
|
+
} catch (e) {
|
|
5566
|
+
errorln(e.message);
|
|
5567
|
+
process.exit(1);
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
|
|
5571
|
+
async function cmdTeamCreateCustom(args) {
|
|
5572
|
+
const id = args.find((a) => !a.startsWith('--'));
|
|
5573
|
+
const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
5574
|
+
const list = (name) => (flag(name) || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
5575
|
+
const usage = 'Usage: construct team create <id> --owner=<role> --charter="…" [--roles=role,…] [--specialists=cx-a,…] [--decision-rights=a,…] [--forbidden=a,…] [--escalation=role,orchestrator] [--group=<group-id>] [--user] [--force] [--root=<dir>]';
|
|
5576
|
+
|
|
5577
|
+
const owner = flag('owner');
|
|
5578
|
+
const charter = flag('charter');
|
|
5579
|
+
const missing = [!id && 'id (positional)', !owner && '--owner', !charter && '--charter'].filter(Boolean);
|
|
5580
|
+
if (missing.length) {
|
|
5581
|
+
errorln(usage);
|
|
5582
|
+
errorln(`Missing required: ${missing.join(', ')}`);
|
|
5583
|
+
process.exit(1);
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
const rootDir = flag('root') ? path.resolve(flag('root')) : ROOT_DIR;
|
|
5587
|
+
const scope = args.includes('--user') ? 'user' : 'project';
|
|
5588
|
+
const { createCustomTeam } = await import('../lib/registry/custom-scaffold.mjs');
|
|
5589
|
+
try {
|
|
5590
|
+
const { relPath, scope: writtenScope } = createCustomTeam({
|
|
5591
|
+
rootDir, scope, id, owner, charter,
|
|
5592
|
+
name: flag('name'),
|
|
5593
|
+
roles: list('roles'),
|
|
5594
|
+
specialists: list('specialists'),
|
|
5595
|
+
decisionRights: list('decision-rights'),
|
|
5596
|
+
forbiddenDecisions: list('forbidden'),
|
|
5597
|
+
escalationPath: list('escalation'),
|
|
5598
|
+
groupId: flag('group'),
|
|
5599
|
+
force: args.includes('--force'),
|
|
5600
|
+
});
|
|
5601
|
+
ok(`Created ${relPath} (${writtenScope} scope).`);
|
|
5602
|
+
println(`Run \`construct specialist create <id> --custom --role=${owner} --team=${id} …\` to give it an owner specialist, then \`construct sync\`.`);
|
|
5603
|
+
} catch (e) {
|
|
5604
|
+
errorln(e.message);
|
|
5605
|
+
process.exit(1);
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
|
|
4942
5609
|
async function cmdBackup(args) {
|
|
4943
5610
|
// Postgres-era backups were removed with the SQL backend (lib/storage/backup.mjs).
|
|
4944
5611
|
// State is now Git-backed (.cx/) plus the local LanceDB index under .cx/lancedb.
|
|
@@ -4947,6 +5614,14 @@ async function cmdBackup(args) {
|
|
|
4947
5614
|
process.exit(1);
|
|
4948
5615
|
}
|
|
4949
5616
|
|
|
5617
|
+
// `provider add|configure` persist to `.cx/providers/<id>.json`; every
|
|
5618
|
+
// success message names the path relative to cwd so output is copy-pasteable
|
|
5619
|
+
// and consistent whether the command ran from the project root or a subdir.
|
|
5620
|
+
|
|
5621
|
+
function instanceConfigRelPath(id) {
|
|
5622
|
+
return path.join('.cx', 'providers', `${id}.json`);
|
|
5623
|
+
}
|
|
5624
|
+
|
|
4950
5625
|
async function cmdProvider(args) {
|
|
4951
5626
|
const sub = args[0];
|
|
4952
5627
|
|
|
@@ -4971,6 +5646,164 @@ async function cmdProvider(args) {
|
|
|
4971
5646
|
return;
|
|
4972
5647
|
}
|
|
4973
5648
|
|
|
5649
|
+
if (sub === 'status') {
|
|
5650
|
+
const jsonOutput = args.includes('--json');
|
|
5651
|
+
const { getState } = await import('../lib/providers/circuit-breaker.mjs');
|
|
5652
|
+
const { describeProviders } = await import('../lib/providers/registry.mjs');
|
|
5653
|
+
const { readInstanceConfig } = await import('../lib/providers/instance-config.mjs');
|
|
5654
|
+
const desc = await describeProviders({ rootDir: process.cwd(), env: process.env });
|
|
5655
|
+
|
|
5656
|
+
// Breaker state is registered under `provider:<id>` (see registry.mjs
|
|
5657
|
+
// wrapWithBreaker); look up with that prefix or the row reads CLOSED for
|
|
5658
|
+
// every provider regardless of actual state.
|
|
5659
|
+
const rows = desc.summary.map((entry) => {
|
|
5660
|
+
const breaker = getState(`provider:${entry.id}`);
|
|
5661
|
+
const breakerState = breaker ? breaker.state : 'CLOSED';
|
|
5662
|
+
const degraded = !entry.health.ok || breakerState !== 'CLOSED';
|
|
5663
|
+
const instance = readInstanceConfig(process.cwd(), entry.id);
|
|
5664
|
+
const filter = instance?.config?.filter ?? null;
|
|
5665
|
+
return {
|
|
5666
|
+
id: entry.id,
|
|
5667
|
+
enabled: entry.health.ok,
|
|
5668
|
+
breaker: breakerState,
|
|
5669
|
+
failures: breaker ? breaker.failures : 0,
|
|
5670
|
+
degraded,
|
|
5671
|
+
source: entry.source,
|
|
5672
|
+
filter,
|
|
5673
|
+
};
|
|
5674
|
+
});
|
|
5675
|
+
|
|
5676
|
+
if (jsonOutput) {
|
|
5677
|
+
println(JSON.stringify({ providers: rows, errors: desc.errors }, null, 2));
|
|
5678
|
+
if (desc.errors.length) process.exit(1);
|
|
5679
|
+
return;
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
println('Provider status');
|
|
5683
|
+
println('════════════════');
|
|
5684
|
+
println(` ${'ID'.padEnd(24)} ${'ENABLED'.padEnd(9)} ${'BREAKER'.padEnd(18)} DEGRADED`);
|
|
5685
|
+
for (const row of rows) {
|
|
5686
|
+
const breakerCol = row.failures > 0 ? `${row.breaker} (${row.failures})` : row.breaker;
|
|
5687
|
+
println(` ${row.id.padEnd(24)} ${(row.enabled ? 'yes' : 'no').padEnd(9)} ${breakerCol.padEnd(18)} ${row.degraded ? 'yes' : 'no'}`);
|
|
5688
|
+
if (row.filter) println(` filter: ${JSON.stringify(row.filter)}`);
|
|
5689
|
+
}
|
|
5690
|
+
if (desc.errors.length) {
|
|
5691
|
+
println('');
|
|
5692
|
+
errorln('Errors loading providers:');
|
|
5693
|
+
for (const e of desc.errors) errorln(` ✗ [${e.id}] ${e.error}`);
|
|
5694
|
+
process.exit(1);
|
|
5695
|
+
}
|
|
5696
|
+
return;
|
|
5697
|
+
}
|
|
5698
|
+
|
|
5699
|
+
if (sub === 'health') {
|
|
5700
|
+
const jsonOutput = args.includes('--json');
|
|
5701
|
+
const id = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
5702
|
+
const { resolveProviders } = await import('../lib/providers/registry.mjs');
|
|
5703
|
+
const { providers, errors } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
|
|
5704
|
+
|
|
5705
|
+
if (id && !providers[id]) {
|
|
5706
|
+
const loadError = errors.find((e) => e.id === id);
|
|
5707
|
+
if (jsonOutput) {
|
|
5708
|
+
println(JSON.stringify({ id, ok: false, error: loadError ? loadError.error : `unknown provider: ${id}` }, null, 2));
|
|
5709
|
+
} else {
|
|
5710
|
+
errorln(`Unknown provider: ${id}`);
|
|
5711
|
+
if (loadError) errorln(loadError.error);
|
|
5712
|
+
}
|
|
5713
|
+
process.exit(1);
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5716
|
+
const targetIds = id ? [id] : Object.keys(providers);
|
|
5717
|
+
const results = await Promise.all(
|
|
5718
|
+
targetIds.map(async (pid) => {
|
|
5719
|
+
try {
|
|
5720
|
+
const health = await providers[pid].health({});
|
|
5721
|
+
return { id: pid, ok: !!health.ok, detail: health.detail || null };
|
|
5722
|
+
} catch (err) {
|
|
5723
|
+
return { id: pid, ok: false, detail: err.message };
|
|
5724
|
+
}
|
|
5725
|
+
})
|
|
5726
|
+
);
|
|
5727
|
+
|
|
5728
|
+
const anyFailed = results.some((r) => !r.ok) || errors.length > 0;
|
|
5729
|
+
|
|
5730
|
+
if (jsonOutput) {
|
|
5731
|
+
println(JSON.stringify({ results, loadErrors: errors }, null, 2));
|
|
5732
|
+
process.exit(anyFailed ? 1 : 0);
|
|
5733
|
+
}
|
|
5734
|
+
|
|
5735
|
+
println('Provider health');
|
|
5736
|
+
println('════════════════');
|
|
5737
|
+
for (const r of results) {
|
|
5738
|
+
println(` ${r.ok ? '✓' : '✗'} ${r.id.padEnd(24)} ${r.ok ? 'ok' : 'unhealthy'}`);
|
|
5739
|
+
if (r.detail) println(` ${r.detail}`);
|
|
5740
|
+
}
|
|
5741
|
+
if (errors.length) {
|
|
5742
|
+
println('');
|
|
5743
|
+
errorln('Errors loading providers:');
|
|
5744
|
+
for (const e of errors) errorln(` ✗ [${e.id}] ${e.error}`);
|
|
5745
|
+
}
|
|
5746
|
+
process.exit(anyFailed ? 1 : 0);
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
if (sub === 'validate') {
|
|
5750
|
+
const jsonOutput = args.includes('--json');
|
|
5751
|
+
const target = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
5752
|
+
if (!target) { errorln('Usage: construct provider validate <path|id>'); process.exit(1); }
|
|
5753
|
+
|
|
5754
|
+
const { validateManifest } = await import('../lib/extensions/validate.mjs');
|
|
5755
|
+
const { loadManifestsFromDir, mergeManifests, resolveManifestDirs } = await import('../lib/extensions/loader.mjs');
|
|
5756
|
+
|
|
5757
|
+
let manifest;
|
|
5758
|
+
let filePath;
|
|
5759
|
+
const looksLikePath = target.endsWith('.json') || target.includes('/') || target.startsWith('.');
|
|
5760
|
+
|
|
5761
|
+
if (looksLikePath) {
|
|
5762
|
+
filePath = path.isAbsolute(target) ? target : path.resolve(process.cwd(), target);
|
|
5763
|
+
if (!fs.existsSync(filePath)) {
|
|
5764
|
+
const result = { valid: false, errors: [`${filePath}: file not found`] };
|
|
5765
|
+
if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.errors[0]); }
|
|
5766
|
+
process.exit(1);
|
|
5767
|
+
}
|
|
5768
|
+
try {
|
|
5769
|
+
manifest = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
5770
|
+
} catch (err) {
|
|
5771
|
+
const result = { valid: false, errors: [`${filePath}: failed to parse JSON (${err.message})`] };
|
|
5772
|
+
if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.errors[0]); }
|
|
5773
|
+
process.exit(1);
|
|
5774
|
+
}
|
|
5775
|
+
} else {
|
|
5776
|
+
const dirs = resolveManifestDirs({ rootDir: process.cwd(), homeDir: HOME });
|
|
5777
|
+
const builtin = loadManifestsFromDir(dirs.builtin).manifests;
|
|
5778
|
+
const user = loadManifestsFromDir(dirs.user).manifests;
|
|
5779
|
+
const project = loadManifestsFromDir(dirs.project).manifests;
|
|
5780
|
+
const merged = mergeManifests(builtin, user, project);
|
|
5781
|
+
manifest = merged.find((m) => m.id === target);
|
|
5782
|
+
if (!manifest) {
|
|
5783
|
+
const result = { valid: false, errors: [`no manifest found for provider id '${target}'`] };
|
|
5784
|
+
if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.errors[0]); }
|
|
5785
|
+
process.exit(1);
|
|
5786
|
+
}
|
|
5787
|
+
filePath = manifest._filePath;
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
const strict = args.includes('--strict');
|
|
5791
|
+
const result = validateManifest(manifest, { filePath, strict });
|
|
5792
|
+
|
|
5793
|
+
if (jsonOutput) {
|
|
5794
|
+
println(JSON.stringify(result, null, 2));
|
|
5795
|
+
process.exit(result.valid ? 0 : 1);
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5798
|
+
if (result.valid) {
|
|
5799
|
+
println(`✓ ${filePath || target} is a valid manifest`);
|
|
5800
|
+
process.exit(0);
|
|
5801
|
+
}
|
|
5802
|
+
errorln(`✗ ${filePath || target} failed validation:`);
|
|
5803
|
+
for (const e of result.errors) errorln(` ${e}`);
|
|
5804
|
+
process.exit(1);
|
|
5805
|
+
}
|
|
5806
|
+
|
|
4974
5807
|
if (sub === 'info') {
|
|
4975
5808
|
const id = args[1];
|
|
4976
5809
|
if (!id) { errorln('Usage: construct provider info <id>'); process.exit(1); }
|
|
@@ -5052,7 +5885,105 @@ async function cmdProvider(args) {
|
|
|
5052
5885
|
return;
|
|
5053
5886
|
}
|
|
5054
5887
|
|
|
5055
|
-
|
|
5888
|
+
if (sub === 'add') {
|
|
5889
|
+
const id = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
5890
|
+
const jsonOutput = args.includes('--json');
|
|
5891
|
+
if (!id) { errorln('Usage: construct provider add <id>'); process.exit(1); }
|
|
5892
|
+
|
|
5893
|
+
const { resolveProviders } = await import('../lib/providers/registry.mjs');
|
|
5894
|
+
const { providers, errors } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
|
|
5895
|
+
if (!providers[id]) {
|
|
5896
|
+
const loadError = errors.find((e) => e.id === id);
|
|
5897
|
+
const result = { ok: false, error: loadError ? loadError.error : `unknown provider: ${id}` };
|
|
5898
|
+
if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(`Unknown provider: ${id}`); if (loadError) errorln(loadError.error); }
|
|
5899
|
+
process.exit(1);
|
|
5900
|
+
}
|
|
5901
|
+
|
|
5902
|
+
const { readInstanceConfig, writeInstanceConfig, defaultsFromSchema } = await import('../lib/providers/instance-config.mjs');
|
|
5903
|
+
const existing = readInstanceConfig(process.cwd(), id);
|
|
5904
|
+
if (existing) {
|
|
5905
|
+
const result = { ok: false, error: `instance config already exists for '${id}'; use 'provider configure ${id}' to change it` };
|
|
5906
|
+
if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(result.error); }
|
|
5907
|
+
process.exit(1);
|
|
5908
|
+
}
|
|
5909
|
+
|
|
5910
|
+
const configSchema = providers[id].configSchema || {};
|
|
5911
|
+
const defaults = defaultsFromSchema(configSchema);
|
|
5912
|
+
const record = writeInstanceConfig(process.cwd(), id, defaults);
|
|
5913
|
+
|
|
5914
|
+
if (jsonOutput) {
|
|
5915
|
+
println(JSON.stringify({ ok: true, providerId: id, config: record.config, path: instanceConfigRelPath(id) }, null, 2));
|
|
5916
|
+
return;
|
|
5917
|
+
}
|
|
5918
|
+
println(`Added ${id} (${instanceConfigRelPath(id)})`);
|
|
5919
|
+
println(JSON.stringify(record.config, null, 2));
|
|
5920
|
+
return;
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
if (sub === 'configure') {
|
|
5924
|
+
const id = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
5925
|
+
const jsonOutput = args.includes('--json');
|
|
5926
|
+
if (!id) { errorln('Usage: construct provider configure <id> [--key value ...]'); process.exit(1); }
|
|
5927
|
+
|
|
5928
|
+
const { resolveProviders } = await import('../lib/providers/registry.mjs');
|
|
5929
|
+
const { providers, errors } = await resolveProviders({ rootDir: process.cwd(), env: process.env });
|
|
5930
|
+
if (!providers[id]) {
|
|
5931
|
+
const loadError = errors.find((e) => e.id === id);
|
|
5932
|
+
const result = { ok: false, error: loadError ? loadError.error : `unknown provider: ${id}` };
|
|
5933
|
+
if (jsonOutput) { println(JSON.stringify(result, null, 2)); } else { errorln(`Unknown provider: ${id}`); if (loadError) errorln(loadError.error); }
|
|
5934
|
+
process.exit(1);
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
const {
|
|
5938
|
+
readInstanceConfig, writeInstanceConfig, defaultsFromSchema, applyOverrides, validateInstanceConfig,
|
|
5939
|
+
} = await import('../lib/providers/instance-config.mjs');
|
|
5940
|
+
|
|
5941
|
+
// `--json` is the one bare boolean flag this subcommand accepts; every
|
|
5942
|
+
// other `--foo` is a key.path expecting a value, so it is excluded from
|
|
5943
|
+
// the key/value scan below rather than mis-parsed as a dangling key.
|
|
5944
|
+
const BARE_FLAGS = new Set(['--json']);
|
|
5945
|
+
|
|
5946
|
+
const overrides = [];
|
|
5947
|
+
for (let i = 2; i < args.length; i++) {
|
|
5948
|
+
const a = args[i];
|
|
5949
|
+
if (!a.startsWith('--') || BARE_FLAGS.has(a)) continue;
|
|
5950
|
+
const keyPath = a.slice(2);
|
|
5951
|
+
const value = args[i + 1];
|
|
5952
|
+
if (value === undefined || value.startsWith('--')) {
|
|
5953
|
+
errorln(`Usage: construct provider configure <id> --<key.path> <value> (missing value for --${keyPath})`);
|
|
5954
|
+
process.exit(1);
|
|
5955
|
+
}
|
|
5956
|
+
overrides.push({ keyPath, value });
|
|
5957
|
+
i++;
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5960
|
+
const configSchema = providers[id].configSchema || {};
|
|
5961
|
+
const existing = readInstanceConfig(process.cwd(), id);
|
|
5962
|
+
const base = existing ? existing.config : defaultsFromSchema(configSchema);
|
|
5963
|
+
const merged = applyOverrides(base, overrides);
|
|
5964
|
+
|
|
5965
|
+
const result = validateInstanceConfig(id, configSchema, merged);
|
|
5966
|
+
if (!result.valid) {
|
|
5967
|
+
if (jsonOutput) {
|
|
5968
|
+
println(JSON.stringify({ ok: false, providerId: id, errors: result.errors }, null, 2));
|
|
5969
|
+
} else {
|
|
5970
|
+
errorln(`✗ configure ${id} failed validation:`);
|
|
5971
|
+
for (const e of result.errors) errorln(` ${e}`);
|
|
5972
|
+
}
|
|
5973
|
+
process.exit(1);
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
const record = writeInstanceConfig(process.cwd(), id, merged);
|
|
5977
|
+
if (jsonOutput) {
|
|
5978
|
+
println(JSON.stringify({ ok: true, providerId: id, config: record.config, path: instanceConfigRelPath(id) }, null, 2));
|
|
5979
|
+
return;
|
|
5980
|
+
}
|
|
5981
|
+
println(`✓ Configured ${id} (${instanceConfigRelPath(id)})`);
|
|
5982
|
+
println(JSON.stringify(record.config, null, 2));
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
|
|
5986
|
+
errorln(`Unknown provider subcommand: ${sub}. Available: list, status, health, validate, info, test, add, configure, plugins, new`);
|
|
5056
5987
|
process.exit(1);
|
|
5057
5988
|
}
|
|
5058
5989
|
|
|
@@ -5211,9 +6142,9 @@ async function cmdCreds(args) {
|
|
|
5211
6142
|
try {
|
|
5212
6143
|
const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
|
|
5213
6144
|
const { providers } = getProviderModelCatalog({ env: process.env });
|
|
5214
|
-
println('\nLLM provider readiness (op:// references resolve at call time)\n');
|
|
6145
|
+
println('\nLLM provider readiness (presence-only — op:// references resolve at call time; run `construct creds test <provider>` to verify auth)\n');
|
|
5215
6146
|
for (const p of providers) {
|
|
5216
|
-
println(` ${p.configured ? '✓' : '·'} ${p.id}`);
|
|
6147
|
+
println(` ${p.configured ? '✓ configured (unverified ref)' : '· not configured'} ${p.id}`);
|
|
5217
6148
|
}
|
|
5218
6149
|
println(`\n Set keys as values or op:// refs in ${getUserEnvPath(HOME)}; run \`construct creds login copilot\` for GitHub Copilot.`);
|
|
5219
6150
|
} catch { /* readiness view is informational */ }
|
|
@@ -5609,15 +6540,16 @@ const handlers = new Map([
|
|
|
5609
6540
|
['dev', cmdDev],
|
|
5610
6541
|
['stop', cmdStop],
|
|
5611
6542
|
['status', cmdStatus],
|
|
6543
|
+
['workers', cmdWorkers],
|
|
5612
6544
|
['install', cmdInstall],
|
|
5613
6545
|
['config', cmdConfig],
|
|
6546
|
+
['approvals', cmdApprovals],
|
|
5614
6547
|
['sources', cmdSources],
|
|
5615
6548
|
['intake', cmdIntake],
|
|
5616
6549
|
['recommendations', cmdRecommendations],
|
|
5617
6550
|
['integrations', cmdIntegrations],
|
|
5618
6551
|
['customer', cmdCustomer],
|
|
5619
6552
|
['workspace', cmdWorkspace],
|
|
5620
|
-
['graph', cmdGraph],
|
|
5621
6553
|
['uninstall', cmdUninstall],
|
|
5622
6554
|
['update', cmdUpdate],
|
|
5623
6555
|
['upgrade', cmdUpgrade],
|
|
@@ -5892,12 +6824,13 @@ const handlers = new Map([
|
|
|
5892
6824
|
const sub = args[0];
|
|
5893
6825
|
if (sub === 'review') return runTeamReviewCli(args.slice(1));
|
|
5894
6826
|
if (sub === 'templates') { listTeamTemplates(); return; }
|
|
6827
|
+
if (sub === 'create') return cmdTeamCreateCustom(args.slice(1));
|
|
5895
6828
|
if (sub === 'list' || sub === 'show') {
|
|
5896
6829
|
const { runTeamList, runTeamShow } = await import('../lib/registry/cli.mjs');
|
|
5897
6830
|
if (sub === 'list') return runTeamList(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
|
|
5898
6831
|
return runTeamShow(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
|
|
5899
6832
|
}
|
|
5900
|
-
errorln(`Unknown team subcommand: ${sub}. Available: list, show, review, templates`);
|
|
6833
|
+
errorln(`Unknown team subcommand: ${sub}. Available: list, show, create, review, templates`);
|
|
5901
6834
|
process.exit(1);
|
|
5902
6835
|
}],
|
|
5903
6836
|
['role', async (args) => {
|
|
@@ -6128,6 +7061,9 @@ const handlers = new Map([
|
|
|
6128
7061
|
['telemetry', async (args) => {
|
|
6129
7062
|
return cmdTelemetryQuery(args);
|
|
6130
7063
|
}],
|
|
7064
|
+
['db', async (args) => {
|
|
7065
|
+
return cmdDb(args);
|
|
7066
|
+
}],
|
|
6131
7067
|
['beads', async (args) => {
|
|
6132
7068
|
const sub = args[0];
|
|
6133
7069
|
const subArgs = args.slice(1);
|
|
@@ -6356,15 +7292,42 @@ const handlers = new Map([
|
|
|
6356
7292
|
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
6357
7293
|
}],
|
|
6358
7294
|
['matrix', async (args) => {
|
|
7295
|
+
if (!globalThis._matrixDeprecated) {
|
|
7296
|
+
process.stderr.write('[deprecation] `construct matrix` is deprecated; use `construct graph` instead\n');
|
|
7297
|
+
globalThis._matrixDeprecated = true;
|
|
7298
|
+
}
|
|
6359
7299
|
const { runGraphCli } = await import('../lib/graph/cli.mjs');
|
|
6360
7300
|
const code = runGraphCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
|
|
6361
7301
|
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
6362
7302
|
}],
|
|
7303
|
+
// `graph` carries two disjoint subcommand families: the living dependency
|
|
7304
|
+
// graph (build/stat/query/validate/impacted/missing-tests/missing-docs/
|
|
7305
|
+
// stale/dependencies/providers/surfaces/explain → lib/graph/cli.mjs) and the
|
|
7306
|
+
// intake task graph (list/show/from-intake/status/recommend → cmdGraph).
|
|
7307
|
+
['graph', async (args) => {
|
|
7308
|
+
const DEPENDENCY_GRAPH_SUBCOMMANDS = new Set([
|
|
7309
|
+
'build', 'stat', 'query', 'validate', 'impacted',
|
|
7310
|
+
'missing-tests', 'missing-docs', 'stale', 'dependencies', 'providers', 'surfaces', 'explain', 'owasp',
|
|
7311
|
+
]);
|
|
7312
|
+
if (DEPENDENCY_GRAPH_SUBCOMMANDS.has(args[0])) {
|
|
7313
|
+
const { runGraphCli } = await import('../lib/graph/cli.mjs');
|
|
7314
|
+
const code = runGraphCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
|
|
7315
|
+
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
7316
|
+
return;
|
|
7317
|
+
}
|
|
7318
|
+
return cmdGraph(args);
|
|
7319
|
+
}],
|
|
7320
|
+
['flow', async (args) => cmdFlow(args)],
|
|
6363
7321
|
['impact', async (args) => {
|
|
6364
7322
|
const { runImpactCli } = await import('../lib/graph/impact-cli.mjs');
|
|
6365
7323
|
const code = await runImpactCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
|
|
6366
7324
|
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
6367
7325
|
}],
|
|
7326
|
+
['pack', async (args) => {
|
|
7327
|
+
const { runPackCli } = await import('../lib/packs/cli.mjs');
|
|
7328
|
+
const code = runPackCli(args, { rootDir: process.cwd(), packageRoot: ROOT_DIR });
|
|
7329
|
+
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
7330
|
+
}],
|
|
6368
7331
|
['reflect', async (args) => {
|
|
6369
7332
|
const { runReflectCli } = await import('../lib/reflect.mjs');
|
|
6370
7333
|
return runReflectCli(args);
|
|
@@ -6628,7 +7591,7 @@ const handlers = new Map([
|
|
|
6628
7591
|
|
|
6629
7592
|
println('');
|
|
6630
7593
|
println(`Scaffolding a new scope draft. Three questions, then a preview.`);
|
|
6631
|
-
println(`Background: docs/guides/concepts/persona-research.md and docs/guides/concepts/
|
|
7594
|
+
println(`Background: docs/guides/concepts/persona-research.md and docs/guides/concepts/profile-lifecycle.md`);
|
|
6632
7595
|
println('');
|
|
6633
7596
|
if (!displayName) displayName = await ask('Display name?', id);
|
|
6634
7597
|
if (seedDepartments.length === 0) {
|
|
@@ -6687,11 +7650,11 @@ const handlers = new Map([
|
|
|
6687
7650
|
println('');
|
|
6688
7651
|
println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
|
|
6689
7652
|
println('');
|
|
6690
|
-
println(`Next steps (in order, per docs/guides/concepts/
|
|
6691
|
-
println(` 1. Discover (cx-
|
|
7653
|
+
println(`Next steps (in order, per docs/guides/concepts/profile-lifecycle.md):`);
|
|
7654
|
+
println(` 1. Discover (cx-researcher): fill personas/<role>.md from interviews + primary sources.`);
|
|
6692
7655
|
println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
|
|
6693
7656
|
println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in scope.json.`);
|
|
6694
|
-
println(` 4. Validate (cx-
|
|
7657
|
+
println(` 4. Validate (cx-reviewer): run persona-eval and classifier-eval against representative signals.`);
|
|
6695
7658
|
println('');
|
|
6696
7659
|
println(`Inspect the brief: open ${pathMod.relative(process.cwd(), result.briefPath)}`);
|
|
6697
7660
|
return;
|