@geraldmaron/construct 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1029 -65
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- 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 +7 -30
- 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 +109 -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 +23 -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/embed/worker.mjs +5 -0
- 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 +60 -11
- package/lib/export-validate.mjs +34 -2
- 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/readiness.mjs +109 -0
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +29 -8
- 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 +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/install/stage-project.mjs +41 -4
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/prepare.mjs +2 -0
- 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-approval.mjs +57 -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 +185 -1268
- 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-rate-limit.mjs +47 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +95 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +197 -19
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/project.mjs +25 -8
- 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 +2 -2
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +5 -76
- 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 +53 -20
- 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/daemon-entry.mjs +6 -0
- 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 +508 -62
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +60 -0
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +763 -80
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/output-quality.mjs +61 -2
- 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/path-policy.mjs +56 -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 +261 -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 +41 -18
- package/lib/providers/secret-resolver.mjs +263 -31
- 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/catalog.mjs +6 -0
- 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 +15 -4
- 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/registry/validate.mjs +3 -3
- 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/sandbox.mjs +1 -1
- 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 +93 -24
- 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 +84 -7
- 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 +103 -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 +20 -7
- package/personas/construct.md +12 -3
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1987 -0
- package/rules/common/comments.md +1 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +215 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +546 -0
- package/scripts/sync-specialists.mjs +336 -106
- 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 +41 -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 +27 -31
- 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/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
- 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;
|
|
@@ -514,12 +593,24 @@ async function cmdDoctor() {
|
|
|
514
593
|
const wantsFixMigrateState = wantsFixAll || args.includes('--fix-migrate-state');
|
|
515
594
|
|
|
516
595
|
const checks = [];
|
|
517
|
-
const
|
|
596
|
+
const { DOCTOR_STATES } = await import('../lib/doctor/diagnosis.mjs');
|
|
597
|
+
const add = (label, pass, optional = false, failureState = pass ? DOCTOR_STATES.healthy : null) => checks.push({ label, pass, optional, failureState });
|
|
518
598
|
const { isConstructSourceCheckout } = await import('../lib/doctor/source-checkout.mjs');
|
|
519
599
|
const isSourceCheckout = isConstructSourceCheckout(ROOT_DIR);
|
|
520
600
|
add('specialists/org exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'org')));
|
|
521
601
|
add('sync-specialists.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs')));
|
|
522
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
|
+
|
|
523
614
|
// Project .gitignore must cover .cx/. Auto-fix via --fix-gitignore.
|
|
524
615
|
// Only checks when cwd looks like a Construct project (has .cx/ or
|
|
525
616
|
// .construct/); non-project cwds get no warning.
|
|
@@ -658,6 +749,31 @@ async function cmdDoctor() {
|
|
|
658
749
|
);
|
|
659
750
|
}
|
|
660
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
|
+
|
|
661
777
|
// Mirror model tier overrides stranded in the pre-XDG legacy config forward
|
|
662
778
|
// before resolving tiers, so an install upgraded across the XDG move reports
|
|
663
779
|
// configured without a manual copy. Applied to process.env too so the check
|
|
@@ -687,7 +803,7 @@ async function cmdDoctor() {
|
|
|
687
803
|
try {
|
|
688
804
|
const { resolveModelTiers } = await import('../lib/model-registry.mjs');
|
|
689
805
|
const resolved = resolveModelTiers({ env: process.env });
|
|
690
|
-
if (resolved.complete
|
|
806
|
+
if (resolved.complete) {
|
|
691
807
|
add('Models — all tiers configured', true);
|
|
692
808
|
} else if (resolved.configured === 0) {
|
|
693
809
|
add('Models — no tier configured (run `construct models --apply`)', false, true);
|
|
@@ -701,8 +817,79 @@ async function cmdDoctor() {
|
|
|
701
817
|
add(`Models — check failed: ${err.message}`, false, true);
|
|
702
818
|
}
|
|
703
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
|
+
|
|
704
891
|
add('Node.js 20+ (recommended)', Number.parseInt(process.versions.node.split('.')[0], 10) >= 20);
|
|
705
|
-
add('npm available',
|
|
892
|
+
add('npm available', resolveCommandOnPath('npm').status === 0);
|
|
706
893
|
const npmDevdir = process.env.npm_config_devdir ?? process.env.NPM_CONFIG_DEVDIR;
|
|
707
894
|
if (npmDevdir) {
|
|
708
895
|
add(
|
|
@@ -711,7 +898,7 @@ async function cmdDoctor() {
|
|
|
711
898
|
true,
|
|
712
899
|
);
|
|
713
900
|
}
|
|
714
|
-
const constructOnPath =
|
|
901
|
+
const constructOnPath = resolveCommandOnPath('construct');
|
|
715
902
|
const constructPath = constructOnPath.status === 0 ? constructOnPath.stdout.trim() : '';
|
|
716
903
|
const constructVersion = constructPath
|
|
717
904
|
? spawnSync(constructPath, ['version'], { encoding: 'utf8', env: process.env })
|
|
@@ -762,16 +949,53 @@ async function cmdDoctor() {
|
|
|
762
949
|
// Absence is not a doctor finding — VS Code adapter support hasn't landed
|
|
763
950
|
// yet, so "create one or wait for support" isn't actionable. The line is
|
|
764
951
|
// suppressed entirely when missing; doctor surfaces facts, not roadmap.
|
|
765
|
-
const
|
|
952
|
+
const vscodeSettingsPath = vscodeSettingsPaths.find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
953
|
+
const vscodeSettingsOk = Boolean(vscodeSettingsPath);
|
|
766
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
|
+
}
|
|
767
979
|
}
|
|
768
980
|
add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(configDir(HOME)), true);
|
|
769
981
|
add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
|
|
770
982
|
add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
|
|
771
983
|
add('specialists/prompts/ directory', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'prompts')));
|
|
772
984
|
add('Hybrid storage env keys documented', fs.existsSync(path.join(ROOT_DIR, '.env.example')));
|
|
773
|
-
|
|
774
|
-
|
|
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
|
+
}
|
|
775
999
|
|
|
776
1000
|
// construct.config.json — optional today (config is permitted to be absent;
|
|
777
1001
|
// env vars + defaults still work). The check enforces that when the file
|
|
@@ -779,7 +1003,9 @@ async function cmdDoctor() {
|
|
|
779
1003
|
try {
|
|
780
1004
|
const projectCfg = loadProjectConfig(process.cwd(), process.env);
|
|
781
1005
|
if (projectCfg.source === 'invalid') {
|
|
782
|
-
|
|
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);
|
|
783
1009
|
} else if (projectCfg.source === 'file') {
|
|
784
1010
|
add(`construct.config.json valid`, true, true);
|
|
785
1011
|
} else {
|
|
@@ -878,7 +1104,7 @@ async function cmdDoctor() {
|
|
|
878
1104
|
return !fs.existsSync(path.join(ROOT_DIR, s.promptFile));
|
|
879
1105
|
});
|
|
880
1106
|
add('All specialist promptFiles resolve', brokenPromptFiles.length === 0);
|
|
881
|
-
const headhuntClassifierAgents = ['cx-
|
|
1107
|
+
const headhuntClassifierAgents = ['cx-operations', 'cx-researcher', 'cx-architect', 'cx-security', 'cx-engineer'];
|
|
882
1108
|
const registryNames = new Set(Object.values(registry.specialists ?? {}).map((s) => `cx-${s.name}`));
|
|
883
1109
|
const classifierOrphans = headhuntClassifierAgents.filter((n) => !registryNames.has(n));
|
|
884
1110
|
add('Headhunt classifier agents exist in registry', classifierOrphans.length === 0);
|
|
@@ -912,10 +1138,33 @@ async function cmdDoctor() {
|
|
|
912
1138
|
const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
|
|
913
1139
|
add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
|
|
914
1140
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
+
}
|
|
919
1168
|
|
|
920
1169
|
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
921
1170
|
const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
|
|
@@ -948,6 +1197,34 @@ async function cmdDoctor() {
|
|
|
948
1197
|
add(label, wired);
|
|
949
1198
|
}
|
|
950
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
|
+
|
|
951
1228
|
const pluginRegistry = loadPluginRegistry({ cwd: process.cwd(), homeDir: HOME, rootDir: ROOT_DIR, env: process.env });
|
|
952
1229
|
add('Plugin manifests valid', pluginRegistry.valid);
|
|
953
1230
|
|
|
@@ -1120,16 +1397,17 @@ async function cmdDoctor() {
|
|
|
1120
1397
|
add('No recent hook failures (24h)', false, true);
|
|
1121
1398
|
}
|
|
1122
1399
|
|
|
1123
|
-
// 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.
|
|
1124
1403
|
const { getAllContracts } = await import('../lib/specialist-contracts.mjs');
|
|
1125
1404
|
const contracts = getAllContracts();
|
|
1126
1405
|
add('Agent contracts loaded', contracts.length > 0);
|
|
1127
1406
|
if (contracts.length > 0) {
|
|
1128
|
-
const
|
|
1129
|
-
c.
|
|
1130
|
-
? false : false,
|
|
1407
|
+
const brokenContracts = contracts.filter((c) =>
|
|
1408
|
+
!c.id || !c.producer || !c.consumer || (!c.postconditions && !c.output),
|
|
1131
1409
|
);
|
|
1132
|
-
add('Agent contract schema intact',
|
|
1410
|
+
add('Agent contract schema intact', brokenContracts.length === 0);
|
|
1133
1411
|
}
|
|
1134
1412
|
|
|
1135
1413
|
// Skills profile — if present, check staleness against current tags
|
|
@@ -1154,6 +1432,67 @@ async function cmdDoctor() {
|
|
|
1154
1432
|
add('Registry integrity check', false);
|
|
1155
1433
|
}
|
|
1156
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
|
+
|
|
1157
1496
|
// Reconciliation drift (ADR-0027). Any task reporting needsRepair is surfaced
|
|
1158
1497
|
// with its summary and the exact `construct sync --reconcile=<id>` command.
|
|
1159
1498
|
// Auto tasks that still show drift (e.g. a run with --no-reconcile) and all
|
|
@@ -1346,7 +1685,7 @@ async function cmdDev(args = []) {
|
|
|
1346
1685
|
process.exit(1);
|
|
1347
1686
|
}
|
|
1348
1687
|
|
|
1349
|
-
const { results, recovery } = await startServices({ rootDir:
|
|
1688
|
+
const { results, recovery } = await startServices({ rootDir: PROJECT_ROOT, homeDir: HOME, selected });
|
|
1350
1689
|
opLog.event('services', { results: results.map((r) => ({ name: r.name, status: r.status })) });
|
|
1351
1690
|
for (const svc of results) {
|
|
1352
1691
|
const label = svc.url ? `${svc.name} → ${svc.url}` : svc.name;
|
|
@@ -1387,14 +1726,14 @@ async function cmdDev(args = []) {
|
|
|
1387
1726
|
const { existsSync } = await import('node:fs');
|
|
1388
1727
|
const { join } = await import('node:path');
|
|
1389
1728
|
|
|
1390
|
-
const embedConfigExists = existsSync(join(
|
|
1391
|
-
const config = loadProjectConfig(
|
|
1729
|
+
const embedConfigExists = existsSync(join(PROJECT_ROOT, 'embed.yaml'));
|
|
1730
|
+
const config = loadProjectConfig(PROJECT_ROOT);
|
|
1392
1731
|
const autoEmbed = config.config?.autoEmbed;
|
|
1393
|
-
|
|
1732
|
+
|
|
1394
1733
|
if (embedConfigExists && autoEmbed) {
|
|
1395
1734
|
try {
|
|
1396
1735
|
const { runEmbedCli } = await import('../lib/embed/cli.mjs');
|
|
1397
|
-
await runEmbedCli(['start'], { rootDir:
|
|
1736
|
+
await runEmbedCli(['start'], { rootDir: PROJECT_ROOT });
|
|
1398
1737
|
opLog.event('embed', { status: 'started' });
|
|
1399
1738
|
} catch (err) {
|
|
1400
1739
|
warn(`embed start failed: ${err.message} — check ~/.cx/runtime/embed-daemon.log`);
|
|
@@ -1747,18 +2086,38 @@ async function cmdPrune(args) {
|
|
|
1747
2086
|
const dryRun = args.includes('--dry-run');
|
|
1748
2087
|
const { planPrune, executePrune, measureUsage } = await import('../lib/resources/budget.mjs');
|
|
1749
2088
|
const actions = planPrune(process.cwd(), process.env);
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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; }
|
|
1759
2120
|
if (dryRun) { info('--dry-run: no files removed.'); return; }
|
|
1760
|
-
const result = executePrune(actions);
|
|
1761
|
-
info(`Pruned ${result.removed.length} file(s), freed ${Math.round(result.bytesFreed / 1024)}KB.`);
|
|
1762
2121
|
const after = measureUsage(process.cwd(), process.env);
|
|
1763
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)}%).`);
|
|
1764
2123
|
}
|
|
@@ -2222,7 +2581,7 @@ async function cmdIntake(args) {
|
|
|
2222
2581
|
const queue = createIntakeQueue(cwd, process.env);
|
|
2223
2582
|
|
|
2224
2583
|
if (sub === 'list') {
|
|
2225
|
-
const pending = queue.listPending();
|
|
2584
|
+
const pending = await queue.listPending();
|
|
2226
2585
|
const rows = pending.map((p) => {
|
|
2227
2586
|
const t = p.triage || {};
|
|
2228
2587
|
return [p.id, t.intakeType || 'unknown', t.rdStage || 'unknown', t.primaryOwner || '—', t.recommendedAction || '—'];
|
|
@@ -2234,7 +2593,7 @@ async function cmdIntake(args) {
|
|
|
2234
2593
|
const id = args[1];
|
|
2235
2594
|
if (sub === 'show') {
|
|
2236
2595
|
if (!id) { errorln('Usage: construct intake show <id>'); process.exit(1); }
|
|
2237
|
-
const entry = queue.read(id);
|
|
2596
|
+
const entry = await queue.read(id);
|
|
2238
2597
|
if (!entry) { errorln(`No ${rebrand.signalNoun} with id: ${id}`); process.exit(1); }
|
|
2239
2598
|
const t = entry.triage || {};
|
|
2240
2599
|
println(`${rebrand.signalNoun.charAt(0).toUpperCase() + rebrand.signalNoun.slice(1)} ${entry.id}`);
|
|
@@ -2348,7 +2707,7 @@ async function cmdIntake(args) {
|
|
|
2348
2707
|
|
|
2349
2708
|
let stamped = null;
|
|
2350
2709
|
if (outputPath) {
|
|
2351
|
-
const packet = queue.read(id);
|
|
2710
|
+
const packet = await queue.read(id);
|
|
2352
2711
|
if (!packet) {
|
|
2353
2712
|
errorln(`No intake packet found: ${id}`);
|
|
2354
2713
|
process.exit(1);
|
|
@@ -2374,7 +2733,7 @@ async function cmdIntake(args) {
|
|
|
2374
2733
|
|| (outputPath ? `output: ${outputPath}` : '');
|
|
2375
2734
|
|
|
2376
2735
|
try {
|
|
2377
|
-
const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
|
|
2736
|
+
const r = await queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
|
|
2378
2737
|
if (stamped) {
|
|
2379
2738
|
info(`Marked ${r.id} processed. Stamped ${outputPath} with intake_id=${id} (confidence=${stamped.intake_confidence ?? 'n/a'}).`);
|
|
2380
2739
|
} else if (noOutputOutcome) {
|
|
@@ -2393,7 +2752,7 @@ async function cmdIntake(args) {
|
|
|
2393
2752
|
if (!id) { errorln('Usage: construct intake skip <id> [--reason=…]'); process.exit(1); }
|
|
2394
2753
|
const flags = parseKvFlags(args.slice(2), ['reason']);
|
|
2395
2754
|
try {
|
|
2396
|
-
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 || '' });
|
|
2397
2756
|
info(`Marked ${r.id} skipped.`);
|
|
2398
2757
|
} catch (err) {
|
|
2399
2758
|
errorln(err.message || 'mark skipped failed');
|
|
@@ -2405,7 +2764,7 @@ async function cmdIntake(args) {
|
|
|
2405
2764
|
if (sub === 'reopen') {
|
|
2406
2765
|
if (!id) { errorln('Usage: construct intake reopen <id>'); process.exit(1); }
|
|
2407
2766
|
try {
|
|
2408
|
-
const r = queue.reopen(id);
|
|
2767
|
+
const r = await queue.reopen(id);
|
|
2409
2768
|
info(`Reopened ${r.id} (from ${r.from}).`);
|
|
2410
2769
|
} catch (err) {
|
|
2411
2770
|
errorln(err.message || 'reopen failed');
|
|
@@ -3094,7 +3453,7 @@ async function cmdIntakeIntegrate(id, args, cwd) {
|
|
|
3094
3453
|
|
|
3095
3454
|
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
3096
3455
|
const queue = createIntakeQueue(cwd, process.env);
|
|
3097
|
-
const entry = queue.read(id);
|
|
3456
|
+
const entry = await queue.read(id);
|
|
3098
3457
|
if (!entry) { errorln(`No intake packet with id: ${id}`); process.exit(1); }
|
|
3099
3458
|
|
|
3100
3459
|
let result;
|
|
@@ -3470,7 +3829,7 @@ async function cmdGraph(args) {
|
|
|
3470
3829
|
if (!intakeId) { errorln('Usage: construct graph from-intake <intake-id>'); process.exit(1); }
|
|
3471
3830
|
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
3472
3831
|
const queue = createIntakeQueue(cwd, process.env);
|
|
3473
|
-
const entry = queue.read(intakeId);
|
|
3832
|
+
const entry = await queue.read(intakeId);
|
|
3474
3833
|
if (!entry) { errorln(`No intake packet: ${intakeId}`); process.exit(1); }
|
|
3475
3834
|
const graph = generateTaskGraphFromTriage({
|
|
3476
3835
|
triage: entry.triage,
|
|
@@ -3508,6 +3867,76 @@ async function cmdGraph(args) {
|
|
|
3508
3867
|
process.exit(1);
|
|
3509
3868
|
}
|
|
3510
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
|
+
|
|
3511
3940
|
async function cmdConfig(args) {
|
|
3512
3941
|
const sub = args[0];
|
|
3513
3942
|
|
|
@@ -3694,6 +4123,7 @@ async function cmdOrchestrate(args) {
|
|
|
3694
4123
|
const timeoutMs = Number(eqFlag('--timeout-ms') || 8000);
|
|
3695
4124
|
let observedTools = splitCsv(eqFlag('--observed-tools'));
|
|
3696
4125
|
let reachableTools = splitCsv(eqFlag('--reachable-tools'));
|
|
4126
|
+
const callerSuppliedObservations = Boolean(observedTools || reachableTools);
|
|
3697
4127
|
let probeError = null;
|
|
3698
4128
|
|
|
3699
4129
|
if (!rest.includes('--no-probe') && !observedTools && !reachableTools) {
|
|
@@ -3718,21 +4148,52 @@ async function cmdOrchestrate(args) {
|
|
|
3718
4148
|
reachableTools,
|
|
3719
4149
|
requiredTools: splitCsv(eqFlag('--required-tools')),
|
|
3720
4150
|
clientContractVersion: eqFlag('--client-contract-version'),
|
|
3721
|
-
observationScope: eqFlag('--observation-scope') || (observedTools || reachableTools ? '
|
|
4151
|
+
observationScope: eqFlag('--observation-scope') || (callerSuppliedObservations ? 'host-session' : (observedTools || reachableTools ? 'local-probe' : 'local-config')),
|
|
3722
4152
|
probeError,
|
|
3723
4153
|
authRequired: rest.includes('--auth-required'),
|
|
3724
4154
|
}, { env: process.env, cwd: process.cwd() });
|
|
3725
4155
|
let eventResult = null;
|
|
3726
4156
|
try { eventResult = recordOrchestrationReadinessEvent(readiness, { homeDir: HOME }); } catch { /* telemetry must not break preflight */ }
|
|
3727
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
|
+
|
|
3728
4178
|
if (wantsJson) {
|
|
3729
|
-
println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null }, null, 2));
|
|
4179
|
+
println(JSON.stringify({ ...readiness, eventPath: eventResult?.path ?? null, deepProbes }, null, 2));
|
|
3730
4180
|
process.exit(readiness.attached ? 0 : 1);
|
|
3731
4181
|
}
|
|
3732
4182
|
|
|
3733
4183
|
println(summarizeOrchestrationReadiness(readiness));
|
|
3734
4184
|
println(`Reason: ${readiness.reasonCode}`);
|
|
3735
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
|
+
}
|
|
3736
4197
|
println(`Diagnostic id: ${readiness.diagnosticBundle?.diagnosticId}`);
|
|
3737
4198
|
process.exit(readiness.attached ? 0 : 1);
|
|
3738
4199
|
}
|
|
@@ -3750,6 +4211,10 @@ async function cmdOrchestrate(args) {
|
|
|
3750
4211
|
if (sub === 'run') {
|
|
3751
4212
|
const text = flag('--text') || positional.join(' ').trim();
|
|
3752
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
|
+
}
|
|
3753
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) }) });
|
|
3754
4219
|
const env = await res.json();
|
|
3755
4220
|
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
@@ -3758,6 +4223,7 @@ async function cmdOrchestrate(args) {
|
|
|
3758
4223
|
return;
|
|
3759
4224
|
}
|
|
3760
4225
|
if (sub === 'status') {
|
|
4226
|
+
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
3761
4227
|
const runId = positional[0];
|
|
3762
4228
|
const path = runId ? `/api/orchestration/runs/${encodeURIComponent(runId)}` : '/api/orchestration/runs';
|
|
3763
4229
|
const res = await fetch(`${base}${path}`, { headers });
|
|
@@ -3765,10 +4231,16 @@ async function cmdOrchestrate(args) {
|
|
|
3765
4231
|
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
3766
4232
|
if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
|
|
3767
4233
|
if (runId) {
|
|
3768
|
-
|
|
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}` : ''}`);
|
|
3769
4238
|
for (const t of env.data.tasks || []) println(` ${t.id} ${t.role} — ${t.status}${t.output ? ` (${t.output.length} chars)` : ''}`);
|
|
3770
4239
|
} else {
|
|
3771
|
-
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
|
+
}
|
|
3772
4244
|
}
|
|
3773
4245
|
return;
|
|
3774
4246
|
}
|
|
@@ -3780,9 +4252,21 @@ async function cmdOrchestrate(args) {
|
|
|
3780
4252
|
|
|
3781
4253
|
if (sub === 'run') {
|
|
3782
4254
|
const { runOrchestration, planRun, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
4255
|
+
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
3783
4256
|
const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
|
|
3784
4257
|
const text = flag('--text') || positional.join(' ').trim();
|
|
3785
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
|
+
}
|
|
3786
4270
|
const request = {
|
|
3787
4271
|
request: text,
|
|
3788
4272
|
workflowType: flag('--workflow-type'),
|
|
@@ -3791,14 +4275,25 @@ async function cmdOrchestrate(args) {
|
|
|
3791
4275
|
host: flag('--host'),
|
|
3792
4276
|
hostModel: flag('--host-model'),
|
|
3793
4277
|
hostProvider: flag('--host-provider'),
|
|
3794
|
-
workerBackend:
|
|
4278
|
+
workerBackend: cliWorkerBackend,
|
|
3795
4279
|
fileCount: Number(flag('--file-count') || 0),
|
|
3796
4280
|
moduleCount: Number(flag('--module-count') || 0),
|
|
3797
4281
|
};
|
|
3798
|
-
|
|
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);
|
|
3799
4288
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3800
|
-
|
|
3801
|
-
|
|
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}`);
|
|
3802
4297
|
if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
|
|
3803
4298
|
else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
|
|
3804
4299
|
for (const t of run.tasks) {
|
|
@@ -3809,12 +4304,14 @@ async function cmdOrchestrate(args) {
|
|
|
3809
4304
|
|
|
3810
4305
|
if (sub === 'status') {
|
|
3811
4306
|
const { getRun, getRuns, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
4307
|
+
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
3812
4308
|
const runId = rest.find((a) => !a.startsWith('--'));
|
|
3813
4309
|
if (runId) {
|
|
3814
4310
|
const run = await getRun(process.cwd(), runId);
|
|
3815
4311
|
if (!run) { errorln(`Run not found: ${runId}`); process.exit(1); }
|
|
3816
4312
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3817
|
-
|
|
4313
|
+
const shaped = shapeRun(run);
|
|
4314
|
+
println(`Run ${run.runId} — ${shaped.status} · executionMode=${shaped.executionMode}${shaped.degraded ? ` · degraded: ${shaped.degradationReason}` : ''}`);
|
|
3818
4315
|
for (const t of run.tasks) {
|
|
3819
4316
|
println(` ${t.id} ${t.role} — ${t.status}`);
|
|
3820
4317
|
if (t.reasoning) println(` reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
@@ -3824,7 +4321,10 @@ async function cmdOrchestrate(args) {
|
|
|
3824
4321
|
const runs = await getRuns(process.cwd());
|
|
3825
4322
|
if (wantsJson) { println(JSON.stringify(runs, null, 2)); return; }
|
|
3826
4323
|
if (!runs.length) { println('No orchestration runs recorded.'); return; }
|
|
3827
|
-
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
|
+
}
|
|
3828
4328
|
return;
|
|
3829
4329
|
}
|
|
3830
4330
|
|
|
@@ -3834,6 +4334,85 @@ async function cmdOrchestrate(args) {
|
|
|
3834
4334
|
println(' preflight [--host H] [--json] [--no-probe] [--observed-tools=a,b] [--reachable-tools=a,b]');
|
|
3835
4335
|
}
|
|
3836
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
|
+
|
|
3837
4416
|
async function cmdModels(args) {
|
|
3838
4417
|
const envPath = getUserEnvPath(HOME);
|
|
3839
4418
|
const sub = args[0] && !args[0].startsWith('-') ? args[0] : null;
|
|
@@ -4902,6 +5481,10 @@ async function cmdSpecialist(args) {
|
|
|
4902
5481
|
const sub = args[0];
|
|
4903
5482
|
if (sub === 'lint') return cmdLintPrompts();
|
|
4904
5483
|
|
|
5484
|
+
if (sub === 'create' && args.includes('--custom')) {
|
|
5485
|
+
return cmdSpecialistCreateCustom(args.slice(1));
|
|
5486
|
+
}
|
|
5487
|
+
|
|
4905
5488
|
if (sub === 'create') {
|
|
4906
5489
|
const role = args[1];
|
|
4907
5490
|
if (!role) { errorln('Usage: construct specialist create <role>'); process.exit(1); }
|
|
@@ -4938,6 +5521,91 @@ async function cmdSpecialist(args) {
|
|
|
4938
5521
|
process.exit(1);
|
|
4939
5522
|
}
|
|
4940
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
|
+
|
|
4941
5609
|
async function cmdBackup(args) {
|
|
4942
5610
|
// Postgres-era backups were removed with the SQL backend (lib/storage/backup.mjs).
|
|
4943
5611
|
// State is now Git-backed (.cx/) plus the local LanceDB index under .cx/lancedb.
|
|
@@ -4946,6 +5614,14 @@ async function cmdBackup(args) {
|
|
|
4946
5614
|
process.exit(1);
|
|
4947
5615
|
}
|
|
4948
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
|
+
|
|
4949
5625
|
async function cmdProvider(args) {
|
|
4950
5626
|
const sub = args[0];
|
|
4951
5627
|
|
|
@@ -4970,6 +5646,164 @@ async function cmdProvider(args) {
|
|
|
4970
5646
|
return;
|
|
4971
5647
|
}
|
|
4972
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
|
+
|
|
4973
5807
|
if (sub === 'info') {
|
|
4974
5808
|
const id = args[1];
|
|
4975
5809
|
if (!id) { errorln('Usage: construct provider info <id>'); process.exit(1); }
|
|
@@ -5051,7 +5885,105 @@ async function cmdProvider(args) {
|
|
|
5051
5885
|
return;
|
|
5052
5886
|
}
|
|
5053
5887
|
|
|
5054
|
-
|
|
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`);
|
|
5055
5987
|
process.exit(1);
|
|
5056
5988
|
}
|
|
5057
5989
|
|
|
@@ -5210,9 +6142,9 @@ async function cmdCreds(args) {
|
|
|
5210
6142
|
try {
|
|
5211
6143
|
const { getProviderModelCatalog } = await import('../lib/model-router.mjs');
|
|
5212
6144
|
const { providers } = getProviderModelCatalog({ env: process.env });
|
|
5213
|
-
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');
|
|
5214
6146
|
for (const p of providers) {
|
|
5215
|
-
println(` ${p.configured ? '✓' : '·'} ${p.id}`);
|
|
6147
|
+
println(` ${p.configured ? '✓ configured (unverified ref)' : '· not configured'} ${p.id}`);
|
|
5216
6148
|
}
|
|
5217
6149
|
println(`\n Set keys as values or op:// refs in ${getUserEnvPath(HOME)}; run \`construct creds login copilot\` for GitHub Copilot.`);
|
|
5218
6150
|
} catch { /* readiness view is informational */ }
|
|
@@ -5608,15 +6540,16 @@ const handlers = new Map([
|
|
|
5608
6540
|
['dev', cmdDev],
|
|
5609
6541
|
['stop', cmdStop],
|
|
5610
6542
|
['status', cmdStatus],
|
|
6543
|
+
['workers', cmdWorkers],
|
|
5611
6544
|
['install', cmdInstall],
|
|
5612
6545
|
['config', cmdConfig],
|
|
6546
|
+
['approvals', cmdApprovals],
|
|
5613
6547
|
['sources', cmdSources],
|
|
5614
6548
|
['intake', cmdIntake],
|
|
5615
6549
|
['recommendations', cmdRecommendations],
|
|
5616
6550
|
['integrations', cmdIntegrations],
|
|
5617
6551
|
['customer', cmdCustomer],
|
|
5618
6552
|
['workspace', cmdWorkspace],
|
|
5619
|
-
['graph', cmdGraph],
|
|
5620
6553
|
['uninstall', cmdUninstall],
|
|
5621
6554
|
['update', cmdUpdate],
|
|
5622
6555
|
['upgrade', cmdUpgrade],
|
|
@@ -5891,12 +6824,13 @@ const handlers = new Map([
|
|
|
5891
6824
|
const sub = args[0];
|
|
5892
6825
|
if (sub === 'review') return runTeamReviewCli(args.slice(1));
|
|
5893
6826
|
if (sub === 'templates') { listTeamTemplates(); return; }
|
|
6827
|
+
if (sub === 'create') return cmdTeamCreateCustom(args.slice(1));
|
|
5894
6828
|
if (sub === 'list' || sub === 'show') {
|
|
5895
6829
|
const { runTeamList, runTeamShow } = await import('../lib/registry/cli.mjs');
|
|
5896
6830
|
if (sub === 'list') return runTeamList(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
|
|
5897
6831
|
return runTeamShow(args.slice(1), { rootDir: ROOT_DIR, println, errorln });
|
|
5898
6832
|
}
|
|
5899
|
-
errorln(`Unknown team subcommand: ${sub}. Available: list, show, review, templates`);
|
|
6833
|
+
errorln(`Unknown team subcommand: ${sub}. Available: list, show, create, review, templates`);
|
|
5900
6834
|
process.exit(1);
|
|
5901
6835
|
}],
|
|
5902
6836
|
['role', async (args) => {
|
|
@@ -6127,6 +7061,9 @@ const handlers = new Map([
|
|
|
6127
7061
|
['telemetry', async (args) => {
|
|
6128
7062
|
return cmdTelemetryQuery(args);
|
|
6129
7063
|
}],
|
|
7064
|
+
['db', async (args) => {
|
|
7065
|
+
return cmdDb(args);
|
|
7066
|
+
}],
|
|
6130
7067
|
['beads', async (args) => {
|
|
6131
7068
|
const sub = args[0];
|
|
6132
7069
|
const subArgs = args.slice(1);
|
|
@@ -6355,15 +7292,42 @@ const handlers = new Map([
|
|
|
6355
7292
|
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
6356
7293
|
}],
|
|
6357
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
|
+
}
|
|
6358
7299
|
const { runGraphCli } = await import('../lib/graph/cli.mjs');
|
|
6359
7300
|
const code = runGraphCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
|
|
6360
7301
|
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
6361
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)],
|
|
6362
7321
|
['impact', async (args) => {
|
|
6363
7322
|
const { runImpactCli } = await import('../lib/graph/impact-cli.mjs');
|
|
6364
7323
|
const code = await runImpactCli(args, { rootDir: ROOT_DIR, projectDir: process.cwd() });
|
|
6365
7324
|
if (typeof code === 'number' && code !== 0) process.exit(code);
|
|
6366
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
|
+
}],
|
|
6367
7331
|
['reflect', async (args) => {
|
|
6368
7332
|
const { runReflectCli } = await import('../lib/reflect.mjs');
|
|
6369
7333
|
return runReflectCli(args);
|
|
@@ -6627,7 +7591,7 @@ const handlers = new Map([
|
|
|
6627
7591
|
|
|
6628
7592
|
println('');
|
|
6629
7593
|
println(`Scaffolding a new scope draft. Three questions, then a preview.`);
|
|
6630
|
-
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`);
|
|
6631
7595
|
println('');
|
|
6632
7596
|
if (!displayName) displayName = await ask('Display name?', id);
|
|
6633
7597
|
if (seedDepartments.length === 0) {
|
|
@@ -6686,11 +7650,11 @@ const handlers = new Map([
|
|
|
6686
7650
|
println('');
|
|
6687
7651
|
println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
|
|
6688
7652
|
println('');
|
|
6689
|
-
println(`Next steps (in order, per docs/guides/concepts/
|
|
6690
|
-
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.`);
|
|
6691
7655
|
println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
|
|
6692
7656
|
println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in scope.json.`);
|
|
6693
|
-
println(` 4. Validate (cx-
|
|
7657
|
+
println(` 4. Validate (cx-reviewer): run persona-eval and classifier-eval against representative signals.`);
|
|
6694
7658
|
println('');
|
|
6695
7659
|
println(`Inspect the brief: open ${pathMod.relative(process.cwd(), result.briefPath)}`);
|
|
6696
7660
|
return;
|