@geraldmaron/construct 1.4.2 → 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 +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +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 +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +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 +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/host-sampling.mjs — Phase 2 of host-subscription execution:
|
|
3
|
+
* construct-mcp drives the awaiting-host loop itself via MCP sampling
|
|
4
|
+
* (`sampling/createMessage`), instead of leaving each materialized prompt for
|
|
5
|
+
* the calling agent to execute and submit back through orchestration_task_result
|
|
6
|
+
* (the Phase 1 pickup loop, lib/orchestration/runtime.mjs submitHostTaskResult).
|
|
7
|
+
*
|
|
8
|
+
* Both phases share the exact same materialization (worker.mjs
|
|
9
|
+
* materializeTaskPrompt) and the exact same recording path
|
|
10
|
+
* (submitHostTaskResult) — sampling only changes WHO executes each prompt: the
|
|
11
|
+
* client's own model via the MCP sampling capability, called by this server,
|
|
12
|
+
* rather than the calling agent's own reasoning turn. A sampling-executed
|
|
13
|
+
* task is still host-reported (provenanceSource 'host-reported'), since the
|
|
14
|
+
* client, not Construct, ran the model — Construct only relayed the call.
|
|
15
|
+
*
|
|
16
|
+
* Verified against the installed SDK (@modelcontextprotocol/sdk@1.29.0,
|
|
17
|
+
* package.json declares ^1.12.0): Server#createMessage and
|
|
18
|
+
* Server#getClientCapabilities both exist (node_modules/@modelcontextprotocol/
|
|
19
|
+
* sdk/dist/esm/server/index.js) and are the accessors this module depends on.
|
|
20
|
+
* If a future SDK removes or renames either, resolveHostExecutionMode still
|
|
21
|
+
* degrades to 'pickup' (Phase 1) rather than a hard failure — sampling is
|
|
22
|
+
* additive, pickup is the primary path by design (Claude Code's own sampling
|
|
23
|
+
* support is unconfirmed at the time of writing; VS Code is the surface
|
|
24
|
+
* expected to exercise this path).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { submitHostTaskResult } from './runtime.mjs';
|
|
28
|
+
|
|
29
|
+
// A specialist answer is a single turn, same ceiling the provider worker uses
|
|
30
|
+
// for a non-reasoning call (worker.mjs MAX_OUTPUT_TOKENS) — kept independent
|
|
31
|
+
// here since a client's sampling budget is negotiated per-request, not a
|
|
32
|
+
// process-wide constant shared with the provider path.
|
|
33
|
+
|
|
34
|
+
export const SAMPLING_MAX_TOKENS = 2048;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decide which loop drives an awaiting-host run's remaining tasks: the
|
|
38
|
+
* client-driven MCP sampling loop, or the Phase 1 pickup loop (the calling
|
|
39
|
+
* agent executes and submits manually). `auto` (the config default) prefers
|
|
40
|
+
* sampling only when the connected client actually declared the capability at
|
|
41
|
+
* initialize time; an explicit `sampling` request against a client that never
|
|
42
|
+
* declared it still falls back to pickup rather than issuing a request the
|
|
43
|
+
* client would reject.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} opts
|
|
46
|
+
* @param {object} [opts.config] loaded project config (orchestration.hostExecution)
|
|
47
|
+
* @param {object} [opts.clientCapabilities] server.getClientCapabilities() result
|
|
48
|
+
* @returns {'sampling'|'pickup'}
|
|
49
|
+
*/
|
|
50
|
+
export function resolveHostExecutionMode({ config, clientCapabilities } = {}) {
|
|
51
|
+
const configured = config?.orchestration?.hostExecution || 'auto';
|
|
52
|
+
const samplingDeclared = Boolean(clientCapabilities?.sampling);
|
|
53
|
+
if (configured === 'pickup') return 'pickup';
|
|
54
|
+
if (configured === 'sampling' || configured === 'auto') return samplingDeclared ? 'sampling' : 'pickup';
|
|
55
|
+
return 'pickup';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractSamplingText(sampled) {
|
|
59
|
+
const content = sampled?.content;
|
|
60
|
+
if (Array.isArray(content)) {
|
|
61
|
+
return content.filter((c) => c?.type === 'text').map((c) => c.text || '').join('');
|
|
62
|
+
}
|
|
63
|
+
if (content?.type === 'text') return content.text || '';
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Drive every `awaiting-host` task on a run through the client's MCP sampling
|
|
69
|
+
* capability, recording each result through submitHostTaskResult (the same
|
|
70
|
+
* path the Phase 1 pickup loop uses) so the run finalizes through the one
|
|
71
|
+
* honest terminal-status computation regardless of which phase closed it out.
|
|
72
|
+
*
|
|
73
|
+
* A sampling call that throws (client rejects, network drop, malformed
|
|
74
|
+
* response) stops the loop early rather than fabricating a result — the
|
|
75
|
+
* remaining tasks stay `awaiting-host` exactly as Phase 1 leaves them, so the
|
|
76
|
+
* run is still honestly resumable by a manual pickup loop or a later retry,
|
|
77
|
+
* never silently marked done with invented output.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} opts
|
|
80
|
+
* @param {object} opts.server the MCP Server instance (server.createMessage)
|
|
81
|
+
* @param {object} opts.run the awaiting-host run (or any run with awaiting-host tasks)
|
|
82
|
+
* @param {string} opts.cwd
|
|
83
|
+
* @param {Record<string,string>} [opts.env]
|
|
84
|
+
* @returns {Promise<object>} the run after every reachable task was sampled and recorded
|
|
85
|
+
*/
|
|
86
|
+
export async function driveHostSamplingLoop({ server, run, cwd, env = process.env } = {}) {
|
|
87
|
+
let latestRun = run;
|
|
88
|
+
let task = (latestRun.tasks || []).find((t) => t.status === 'awaiting-host');
|
|
89
|
+
|
|
90
|
+
while (task) {
|
|
91
|
+
const { system = '', user = '' } = task.hostPrompt || {};
|
|
92
|
+
let sampled;
|
|
93
|
+
try {
|
|
94
|
+
sampled = await server.createMessage({
|
|
95
|
+
messages: [{ role: 'user', content: { type: 'text', text: user } }],
|
|
96
|
+
systemPrompt: system,
|
|
97
|
+
maxTokens: SAMPLING_MAX_TOKENS,
|
|
98
|
+
modelPreferences: { intelligencePriority: 0.6, speedPriority: 0.4, costPriority: 0.4 },
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(`[construct-mcp] sampling/createMessage failed for task ${task.id} on run ${latestRun.runId}: ${err.message}`);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const output = extractSamplingText(sampled);
|
|
106
|
+
if (!output.trim()) {
|
|
107
|
+
console.error(`[construct-mcp] sampling/createMessage returned empty content for task ${task.id} on run ${latestRun.runId}; leaving it awaiting-host.`);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { run: updated, nextTask } = await submitHostTaskResult(
|
|
112
|
+
cwd, latestRun.runId, task.id,
|
|
113
|
+
{ output, model: sampled.model || null, provider: 'mcp-sampling' },
|
|
114
|
+
{ env },
|
|
115
|
+
);
|
|
116
|
+
latestRun = updated;
|
|
117
|
+
task = nextTask ? (updated.tasks || []).find((t) => t.id === nextTask.id) : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return latestRun;
|
|
121
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/policy-constants.mjs — shared enums for orchestration
|
|
3
|
+
* routing policy.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from lib/orchestration-policy.mjs (construct-rf26.10) so
|
|
6
|
+
* classification.mjs, gates.mjs, and flow-selection.mjs can each import the
|
|
7
|
+
* enums without importing the whole policy module (avoids a cycle back
|
|
8
|
+
* through orchestration-policy.mjs's re-export layer). orchestration-policy.mjs
|
|
9
|
+
* re-exports these unchanged for existing callers.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const EXECUTION_TRACKS = {
|
|
13
|
+
immediate: 'immediate',
|
|
14
|
+
focused: 'focused',
|
|
15
|
+
orchestrated: 'orchestrated',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const INTENT_CLASSES = {
|
|
19
|
+
research: 'research',
|
|
20
|
+
implementation: 'implementation',
|
|
21
|
+
investigation: 'investigation',
|
|
22
|
+
evaluation: 'evaluation',
|
|
23
|
+
fix: 'fix',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// RFC-0004 §2: each intent class maps to the team that naturally owns that work.
|
|
27
|
+
// The primary team drives ownership in teamRouting; specialist selection stays
|
|
28
|
+
// flavor-driven. Domain refinements the RFC notes (investigation/fix can fall to
|
|
29
|
+
// operations-group, research to product-group) layer on as the decision matrix
|
|
30
|
+
// grows — the base mapping is deterministic so routing is testable.
|
|
31
|
+
|
|
32
|
+
export const INTENT_TO_TEAM = Object.freeze({
|
|
33
|
+
research: 'strategy-team',
|
|
34
|
+
implementation: 'engineering-team',
|
|
35
|
+
investigation: 'engineering-team',
|
|
36
|
+
evaluation: 'quality-team',
|
|
37
|
+
fix: 'engineering-team',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const WORK_CATEGORIES = {
|
|
41
|
+
visual: 'visual',
|
|
42
|
+
deep: 'deep',
|
|
43
|
+
quick: 'quick',
|
|
44
|
+
writing: 'writing',
|
|
45
|
+
analysis: 'analysis',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const TERMINAL_STATES = ['DONE', 'BLOCKED', 'NEEDS_MAIN_INPUT'];
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/provider-outcome.mjs — Typed provider outcome classification,
|
|
3
|
+
* bounded retry policy, and citation-grounding checks for the provider worker
|
|
4
|
+
* (construct-5wkl).
|
|
5
|
+
*
|
|
6
|
+
* A 2xx HTTP response is transport success, not task success: the body can
|
|
7
|
+
* still carry an empty answer, a content-policy refusal, or a reasoning-only
|
|
8
|
+
* response with nothing in the visible channel. This module turns both
|
|
9
|
+
* transport failures (rate limits, 5xx, timeouts) and content-shaped failures
|
|
10
|
+
* into the same stable, machine-readable code shape so
|
|
11
|
+
* lib/orchestration/worker.mjs can throw one error type that
|
|
12
|
+
* lib/orchestration/runtime.mjs's existing catch path already records as
|
|
13
|
+
* task.status='failed' with task.error.code — no runtime.mjs change needed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const PROVIDER_ERROR_CODES = Object.freeze({
|
|
17
|
+
RATE_LIMITED: 'PROVIDER_RATE_LIMITED',
|
|
18
|
+
SERVER_ERROR: 'PROVIDER_SERVER_ERROR',
|
|
19
|
+
TIMEOUT: 'PROVIDER_TIMEOUT',
|
|
20
|
+
AUTH_ERROR: 'PROVIDER_AUTH_ERROR',
|
|
21
|
+
NO_CREDITS: 'PROVIDER_NO_CREDITS',
|
|
22
|
+
CONTENT_FILTERED: 'PROVIDER_CONTENT_FILTERED',
|
|
23
|
+
EMPTY_CONTENT: 'PROVIDER_EMPTY_CONTENT',
|
|
24
|
+
REASONING_ONLY: 'PROVIDER_REASONING_ONLY',
|
|
25
|
+
MALFORMED_RESPONSE: 'PROVIDER_MALFORMED_RESPONSE',
|
|
26
|
+
EXECUTION_FAILED: 'PROVIDER_EXECUTION_FAILED',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Only genuinely transient, infrastructure-level conditions are retryable —
|
|
30
|
+
// a content-policy refusal or a malformed body will not change on retry, and
|
|
31
|
+
// retrying an auth failure just spends another call to fail the same way.
|
|
32
|
+
|
|
33
|
+
const RETRYABLE_CODES = new Set([
|
|
34
|
+
PROVIDER_ERROR_CODES.RATE_LIMITED,
|
|
35
|
+
PROVIDER_ERROR_CODES.SERVER_ERROR,
|
|
36
|
+
PROVIDER_ERROR_CODES.TIMEOUT,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export function isRetryableCode(code) {
|
|
40
|
+
return RETRYABLE_CODES.has(code);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// HTTP-status classification. OpenRouter, OpenAI, and Anthropic all use 429
|
|
44
|
+
// for rate limiting and 5xx for upstream/provider errors; 401/403 are
|
|
45
|
+
// credential problems no retry can fix; 402 is the insufficient-credits
|
|
46
|
+
// status, which gets its own code so the remediation can steer the caller to
|
|
47
|
+
// the credit-free host backend instead of another spend path. Anything else
|
|
48
|
+
// keeps the prior generic code so existing callers/tests are unaffected.
|
|
49
|
+
|
|
50
|
+
export function classifyHttpFailure(status) {
|
|
51
|
+
if (status === 429) return { code: PROVIDER_ERROR_CODES.RATE_LIMITED, retryable: true };
|
|
52
|
+
if (status >= 500 && status < 600) return { code: PROVIDER_ERROR_CODES.SERVER_ERROR, retryable: true };
|
|
53
|
+
if (status === 401 || status === 403) return { code: PROVIDER_ERROR_CODES.AUTH_ERROR, retryable: false };
|
|
54
|
+
if (status === 402) return { code: PROVIDER_ERROR_CODES.NO_CREDITS, retryable: false };
|
|
55
|
+
return { code: PROVIDER_ERROR_CODES.EXECUTION_FAILED, retryable: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// AbortSignal.timeout firing surfaces as a DOMException/Error named
|
|
59
|
+
// TimeoutError or AbortError; timedFetch's own fallback throws a plain Error
|
|
60
|
+
// with "timed out" in the message when a stub fetchImpl ignores the signal.
|
|
61
|
+
|
|
62
|
+
export function isTimeoutError(err) {
|
|
63
|
+
if (!err) return false;
|
|
64
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError') return true;
|
|
65
|
+
return /timed out|ETIMEDOUT/i.test(String(err.message || ''));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Normalizes an OpenAI/OpenRouter-shaped chat-completions body's first choice.
|
|
69
|
+
// OpenRouter additionally reports native_finish_reason (the upstream model's
|
|
70
|
+
// own value before OpenRouter's own normalization).
|
|
71
|
+
|
|
72
|
+
export function extractChoiceMeta(data) {
|
|
73
|
+
const choice = data?.choices?.[0];
|
|
74
|
+
return {
|
|
75
|
+
message: choice?.message && typeof choice.message === 'object' ? choice.message : null,
|
|
76
|
+
finishReason: choice?.finish_reason ?? null,
|
|
77
|
+
nativeFinishReason: choice?.native_finish_reason ?? null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function extractUsage(data) {
|
|
82
|
+
const usage = data?.usage;
|
|
83
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
84
|
+
const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens);
|
|
85
|
+
const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens);
|
|
86
|
+
const totalTokens = Number(usage.total_tokens ?? (promptTokens + completionTokens));
|
|
87
|
+
if (!Number.isFinite(promptTokens) && !Number.isFinite(completionTokens) && !Number.isFinite(totalTokens)) return null;
|
|
88
|
+
return {
|
|
89
|
+
promptTokens: Number.isFinite(promptTokens) ? promptTokens : undefined,
|
|
90
|
+
completionTokens: Number.isFinite(completionTokens) ? completionTokens : undefined,
|
|
91
|
+
totalTokens: Number.isFinite(totalTokens) ? totalTokens : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// After a 2xx response, transport succeeded but the answer may still be
|
|
96
|
+
// unusable. A content-policy refusal and a reasoning-only response (visible
|
|
97
|
+
// answer empty, model reasoning non-empty, finish_reason length — the model
|
|
98
|
+
// spent its entire output budget on reasoning tokens and never reached the
|
|
99
|
+
// answer) are both distinguishable from a genuinely empty response.
|
|
100
|
+
|
|
101
|
+
export function classifyContentOutcome({ content, finishReason, reasoning, wantsReasoning }) {
|
|
102
|
+
const text = typeof content === 'string' ? content.trim() : '';
|
|
103
|
+
const hasReasoning = typeof reasoning === 'string' && reasoning.trim().length > 0;
|
|
104
|
+
|
|
105
|
+
if (finishReason === 'content_filter') {
|
|
106
|
+
return { ok: false, code: PROVIDER_ERROR_CODES.CONTENT_FILTERED, retryable: false };
|
|
107
|
+
}
|
|
108
|
+
if (!text && wantsReasoning && hasReasoning) {
|
|
109
|
+
return { ok: false, code: PROVIDER_ERROR_CODES.REASONING_ONLY, retryable: false };
|
|
110
|
+
}
|
|
111
|
+
if (!text) {
|
|
112
|
+
return { ok: false, code: PROVIDER_ERROR_CODES.EMPTY_CONTENT, retryable: false };
|
|
113
|
+
}
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Malformed-choices guard: a 2xx body missing choices[0].message entirely is
|
|
118
|
+
// not the same failure as an empty string answer — it means the provider's
|
|
119
|
+
// response shape itself did not match what the caller expects.
|
|
120
|
+
|
|
121
|
+
export function isMalformedChoice(data) {
|
|
122
|
+
const choice = data?.choices?.[0];
|
|
123
|
+
return !choice || typeof choice !== 'object' || !choice.message || typeof choice.message !== 'object';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Exponential backoff with a fixed small ceiling — provider calls already run
|
|
127
|
+
// seconds-to-minutes (PROVIDER_TIMEOUT_DEFAULT_MS), so the retry delay only
|
|
128
|
+
// needs to clear a rate-limit window's typical reset granularity, not model
|
|
129
|
+
// latency. attempt is 1-indexed (the delay before the 2nd, 3rd, ... call).
|
|
130
|
+
|
|
131
|
+
export function computeBackoffMs(attempt, baseMs = 250) {
|
|
132
|
+
return baseMs * 2 ** Math.max(0, attempt - 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function defaultSleep(ms) {
|
|
136
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Retry wrapper: calls `attemptFn(attemptNumber)` up to `maxAttempts` times
|
|
141
|
+
* (1-indexed). `attemptFn` must throw an error carrying `.code` and
|
|
142
|
+
* `.retryable` (as produced by classifyHttpFailure/isTimeoutError callers) on
|
|
143
|
+
* failure. Retries only when `.retryable` is true and attempts remain;
|
|
144
|
+
* preserves the final error's full chain by attaching `.attempts` and
|
|
145
|
+
* `.retryCount` rather than replacing it, so the caller sees exactly what the
|
|
146
|
+
* last real attempt failed with (construct-5wkl AC#4).
|
|
147
|
+
*/
|
|
148
|
+
export async function withProviderRetry(attemptFn, { maxAttempts = 3, baseDelayMs = 250, sleep = defaultSleep } = {}) {
|
|
149
|
+
let lastErr;
|
|
150
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
151
|
+
try {
|
|
152
|
+
const result = await attemptFn(attempt);
|
|
153
|
+
if (result && typeof result === 'object') result.retryCount = attempt - 1;
|
|
154
|
+
return result;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
lastErr = err;
|
|
157
|
+
const canRetry = err?.retryable === true && attempt < maxAttempts;
|
|
158
|
+
if (!canRetry) {
|
|
159
|
+
if (err && typeof err === 'object') err.retryCount = attempt - 1;
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
await sleep(computeBackoffMs(attempt, baseDelayMs));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw lastErr;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Evidence grounding (construct-5wkl AC#5) ─────────────────────────────────
|
|
169
|
+
// A web-capable specialist's governed evidence (task.webEvidence, ADR-0050) is
|
|
170
|
+
// the only citation source Construct has actually observed. Any URL the model
|
|
171
|
+
// writes into its answer that does not appear in that governed evidence is
|
|
172
|
+
// either fabricated outright or drawn from ungoverned model memory — either
|
|
173
|
+
// way it is not verified evidence and must not be presented as such.
|
|
174
|
+
|
|
175
|
+
const URL_PATTERN = /https?:\/\/[^\s)\]}"'<>]+/gi;
|
|
176
|
+
|
|
177
|
+
function normalizeUrl(url) {
|
|
178
|
+
return String(url || '').trim().replace(/[.,;:!?)\]}'"]+$/, '').replace(/\/$/, '').toLowerCase();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function extractCitedUrls(text) {
|
|
182
|
+
if (typeof text !== 'string' || !text) return [];
|
|
183
|
+
const matches = text.match(URL_PATTERN) || [];
|
|
184
|
+
return [...new Set(matches.map(normalizeUrl))];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Returns the subset of URLs cited in `text` that do not appear among
|
|
189
|
+
* `evidenceUrls` (the governed web-evidence list). An empty return means every
|
|
190
|
+
* citation in the text traces to observed evidence.
|
|
191
|
+
*/
|
|
192
|
+
export function findUnverifiedCitations(text, evidenceUrls = []) {
|
|
193
|
+
const cited = extractCitedUrls(text);
|
|
194
|
+
if (!cited.length) return [];
|
|
195
|
+
const known = new Set((evidenceUrls || []).map(normalizeUrl));
|
|
196
|
+
return cited.filter((url) => !known.has(url));
|
|
197
|
+
}
|
|
@@ -13,6 +13,25 @@ import { getInstalledVersion } from '../version.mjs';
|
|
|
13
13
|
import { CONTRACT_VERSION, MIN_CLIENT_CONTRACT_VERSION } from '../embedded-contract/contract-version.mjs';
|
|
14
14
|
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
15
15
|
import { doctorRoot } from '../config/xdg.mjs';
|
|
16
|
+
import { resolveExecution } from '../embedded-contract/execution.mjs';
|
|
17
|
+
import { resolveEmbeddedModel } from '../embedded-contract/model-resolve.mjs';
|
|
18
|
+
import { resolveWorkerBackend } from './runtime.mjs';
|
|
19
|
+
import { resolveWebCapability } from './web-capability.mjs';
|
|
20
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
21
|
+
|
|
22
|
+
// A run planned with no explicit backend only actually reaches the host worker
|
|
23
|
+
// backend when TWO things are true: the call is MCP-originated (so
|
|
24
|
+
// orchestration_run's own host-default applies at all — a bare CLI call keeps
|
|
25
|
+
// the inline default) and a real host session is attached to receive the
|
|
26
|
+
// materialized prompts and submit results back. `host-session` scope with a
|
|
27
|
+
// named host is the strongest signal readiness has for the second condition —
|
|
28
|
+
// a self-report or a bare local-probe/local-config check proves no live host is
|
|
29
|
+
// on the other end of this call, so host execution would only ever produce an
|
|
30
|
+
// abandoned awaiting-host run with nobody to submit results.
|
|
31
|
+
|
|
32
|
+
function hostExecutionViability({ observationScope, host }) {
|
|
33
|
+
return observationScope === 'host-session' && Boolean(host);
|
|
34
|
+
}
|
|
16
35
|
|
|
17
36
|
export const ORCHESTRATION_READINESS_REASONS = Object.freeze([
|
|
18
37
|
'attached',
|
|
@@ -23,6 +42,8 @@ export const ORCHESTRATION_READINESS_REASONS = Object.freeze([
|
|
|
23
42
|
'capability_negotiation_failed',
|
|
24
43
|
'version_mismatch',
|
|
25
44
|
'tool_unlisted',
|
|
45
|
+
'model_unresolved',
|
|
46
|
+
'execution_degraded',
|
|
26
47
|
'unknown',
|
|
27
48
|
]);
|
|
28
49
|
|
|
@@ -35,12 +56,14 @@ const NEXT_STEPS = Object.freeze({
|
|
|
35
56
|
attached: 'Proceed: call orchestration_policy, then orchestration_run when policy indicates an orchestrated or focused run.',
|
|
36
57
|
host_not_attached: 'Refresh the host adapter with `construct sync`, restart the host session, then rerun `construct orchestrate preflight --json` inside the target project.',
|
|
37
58
|
server_unreachable: 'Run `construct doctor`, verify the generated MCP command points at this checkout, then rerun `construct sync` if the server path is stale.',
|
|
38
|
-
auth_unavailable: '
|
|
59
|
+
auth_unavailable: 'Set CONSTRUCT_ORCHESTRATION_TOKEN or CONSTRUCT_DASHBOARD_TOKEN so the MCP process serving CONSTRUCT_ORCHESTRATION_URL inherits it, restart the host, then rerun preflight.',
|
|
39
60
|
profile_mismatch: 'Check the active Construct scope/profile and host adapter selection, then rerun `construct sync` from the project root.',
|
|
40
|
-
capability_negotiation_failed: 'Restart the host session so MCP initialize/tools.list negotiation runs again; if it persists,
|
|
61
|
+
capability_negotiation_failed: 'Restart the host session so MCP initialize/tools.list negotiation runs again; if it persists, report the issue with the printed Diagnostic id at https://github.com/geraldmaron/construct/issues.',
|
|
41
62
|
version_mismatch: 'Upgrade Construct and refresh adapters with `construct sync` so the host and server use compatible contract versions.',
|
|
42
63
|
tool_unlisted: 'Refresh adapters with `construct sync`; if using a reduced local tool surface, ensure orchestration_run is reachable through the `call` gateway enum.',
|
|
43
|
-
|
|
64
|
+
model_unresolved: 'Set CX_MODEL_REASONING/STANDARD/FAST, or configure a provider credential (e.g. ANTHROPIC_API_KEY, OPENROUTER_API_KEY), then rerun preflight.',
|
|
65
|
+
execution_degraded: 'Run `construct orchestrate preflight --json` and inspect the modelResolved/workerBackend/webMode fields to see which one degrades this env; a same-family-fallback or config-error there predicts the same degradation orchestration_run would report.',
|
|
66
|
+
unknown: 'Run `construct doctor` and report the issue with the printed Diagnostic id at https://github.com/geraldmaron/construct/issues.',
|
|
44
67
|
});
|
|
45
68
|
|
|
46
69
|
function normalizeList(values) {
|
|
@@ -65,13 +88,20 @@ function hasTool(tool, observed, reachable) {
|
|
|
65
88
|
return observed.includes(tool) || reachable.includes(tool);
|
|
66
89
|
}
|
|
67
90
|
|
|
91
|
+
// An unresolved `op://vault/item/field` literal is a configured *reference*,
|
|
92
|
+
// not a materialized credential — counting it as a present key would tell the
|
|
93
|
+
// verdict chain a provider is usable when the secret was never actually read.
|
|
94
|
+
function isMaterializedSecret(value) {
|
|
95
|
+
return Boolean(value) && !/^op:\/\//.test(String(value).trim());
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
function redactedEnvFacts(env) {
|
|
69
99
|
return {
|
|
70
100
|
hasRemoteOrchestrationUrl: Boolean(env.CONSTRUCT_ORCHESTRATION_URL),
|
|
71
101
|
hasRemoteOrchestrationToken: Boolean(env.CONSTRUCT_ORCHESTRATION_TOKEN || env.CONSTRUCT_DASHBOARD_TOKEN),
|
|
72
|
-
hasOpenRouterKey:
|
|
73
|
-
hasAnthropicKey:
|
|
74
|
-
hasOpenAiKey:
|
|
102
|
+
hasOpenRouterKey: isMaterializedSecret(env.OPENROUTER_API_KEY),
|
|
103
|
+
hasAnthropicKey: isMaterializedSecret(env.ANTHROPIC_API_KEY),
|
|
104
|
+
hasOpenAiKey: isMaterializedSecret(env.OPENAI_API_KEY),
|
|
75
105
|
};
|
|
76
106
|
}
|
|
77
107
|
|
|
@@ -81,12 +111,19 @@ export function buildOrchestrationReadiness(input = {}, { env = process.env, cwd
|
|
|
81
111
|
const reachableTools = normalizeList(input.reachableTools ?? input.reachable_tools);
|
|
82
112
|
const host = input.host ?? null;
|
|
83
113
|
const sessionId = input.sessionId ?? input.session_id ?? null;
|
|
84
|
-
const observationScope = input.observationScope ?? input.observation_scope ?? (observedTools.length || reachableTools.length ? '
|
|
114
|
+
const observationScope = input.observationScope ?? input.observation_scope ?? (observedTools.length || reachableTools.length ? 'local-probe' : 'local-config');
|
|
85
115
|
const probeError = input.probeError ?? input.probe_error ?? null;
|
|
86
|
-
|
|
116
|
+
// No self-default to CONTRACT_VERSION: a caller that never reports its own
|
|
117
|
+
// contract version is unknown, not "assumed current" — assuming current
|
|
118
|
+
// made the version_mismatch check untrippable by construction.
|
|
119
|
+
const clientContractVersion = input.clientContractVersion ?? input.client_contract_version ?? null;
|
|
87
120
|
const profileExpected = input.expectedProfile ?? input.expected_profile ?? null;
|
|
88
121
|
const profileActual = input.actualProfile ?? input.actual_profile ?? null;
|
|
89
|
-
const
|
|
122
|
+
const envFacts = redactedEnvFacts(env);
|
|
123
|
+
// A configured CONSTRUCT_ORCHESTRATION_URL means orchestration_run WILL hit
|
|
124
|
+
// a remote that needs a token; auth is required by that fact alone, not
|
|
125
|
+
// only when a caller opts in with --auth-required.
|
|
126
|
+
const authRequired = Boolean(input.authRequired ?? input.auth_required ?? envFacts.hasRemoteOrchestrationUrl);
|
|
90
127
|
|
|
91
128
|
const missingTools = requiredTools.filter((tool) => !hasTool(tool, observedTools, reachableTools));
|
|
92
129
|
const constructVersion = getInstalledVersion()?.version || 'unknown';
|
|
@@ -98,15 +135,17 @@ export function buildOrchestrationReadiness(input = {}, { env = process.env, cwd
|
|
|
98
135
|
if (probeError) {
|
|
99
136
|
reasonCode = 'server_unreachable';
|
|
100
137
|
detail = String(probeError);
|
|
101
|
-
} else if (compareSemver(clientContractVersion, MIN_CLIENT_CONTRACT_VERSION) < 0) {
|
|
138
|
+
} else if (clientContractVersion && compareSemver(clientContractVersion, MIN_CLIENT_CONTRACT_VERSION) < 0) {
|
|
102
139
|
reasonCode = 'version_mismatch';
|
|
103
140
|
detail = `Client contract ${clientContractVersion} is older than minimum ${MIN_CLIENT_CONTRACT_VERSION}.`;
|
|
104
141
|
} else if (profileExpected && profileActual && profileExpected !== profileActual) {
|
|
105
142
|
reasonCode = 'profile_mismatch';
|
|
106
143
|
detail = `Expected profile ${profileExpected}, observed ${profileActual}.`;
|
|
107
|
-
} else if (authRequired && !
|
|
144
|
+
} else if (authRequired && !envFacts.hasRemoteOrchestrationToken) {
|
|
108
145
|
reasonCode = 'auth_unavailable';
|
|
109
|
-
detail =
|
|
146
|
+
detail = envFacts.hasRemoteOrchestrationUrl
|
|
147
|
+
? 'CONSTRUCT_ORCHESTRATION_URL is configured but CONSTRUCT_ORCHESTRATION_TOKEN/CONSTRUCT_DASHBOARD_TOKEN is not set.'
|
|
148
|
+
: 'A remote/team orchestration token is required but not configured.';
|
|
110
149
|
} else if (observationScope === 'host-session' && observedTools.length === 0 && reachableTools.length === 0) {
|
|
111
150
|
reasonCode = 'host_not_attached';
|
|
112
151
|
detail = 'No observed MCP tools were provided for the active host session.';
|
|
@@ -115,7 +154,55 @@ export function buildOrchestrationReadiness(input = {}, { env = process.env, cwd
|
|
|
115
154
|
detail = `Missing required tool(s): ${missingTools.join(', ')}.`;
|
|
116
155
|
}
|
|
117
156
|
|
|
157
|
+
// Attachment/version/auth/tool checks above answer liveness ("is the tool
|
|
158
|
+
// reachable?"). None of them predict whether orchestration_run on this same
|
|
159
|
+
// env would actually serve, so a green verdict here could still precede a
|
|
160
|
+
// "No model could be resolved" degradation. Resolve the identical serve
|
|
161
|
+
// path orchestration_run uses — model tiers, worker backend, web grant —
|
|
162
|
+
// env/config-local, no network calls, so a reason code already set above
|
|
163
|
+
// (a real liveness failure) is never overwritten by a serve-ability check.
|
|
164
|
+
const config = (() => { try { return loadProjectConfig(cwd, env).config || {}; } catch { return {}; } })();
|
|
165
|
+
const workerBackend = resolveWorkerBackend({ config });
|
|
166
|
+
const modelResolved = {};
|
|
167
|
+
let firstUnresolvedTier = null;
|
|
168
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
169
|
+
const resolved = resolveEmbeddedModel({ requestedTier: tier }, { env });
|
|
170
|
+
modelResolved[tier] = resolved.resolutionSource !== 'config-error';
|
|
171
|
+
if (!modelResolved[tier] && !firstUnresolvedTier) firstUnresolvedTier = tier;
|
|
172
|
+
}
|
|
173
|
+
const execData = resolveExecution({ requestedStrategy: 'orchestrated' }, { env, cwd });
|
|
174
|
+
const providerFamily = (execData.selectedProvider || '').replace(/^openrouter-.*/, 'openrouter');
|
|
175
|
+
// resolveWebCapability checks env.WEB_SEARCH_URL before ever looking at
|
|
176
|
+
// family, so a governed grant must not be skipped just because no LLM
|
|
177
|
+
// provider resolved — call it unconditionally and let it fall through to
|
|
178
|
+
// 'unavailable' on its own when neither WEB_SEARCH_URL nor family applies.
|
|
179
|
+
const webGrant = resolveWebCapability({ family: providerFamily, env });
|
|
180
|
+
const credentialMaterializable = execData.modelResolution?.requiresCredential === false || execData.selectedModel != null;
|
|
181
|
+
|
|
182
|
+
if (reasonCode === 'attached' && firstUnresolvedTier) {
|
|
183
|
+
reasonCode = 'model_unresolved';
|
|
184
|
+
const presentKeys = Object.entries({ anthropic: envFacts.hasAnthropicKey, openrouter: envFacts.hasOpenRouterKey, openai: envFacts.hasOpenAiKey })
|
|
185
|
+
.filter(([, present]) => present)
|
|
186
|
+
.map(([name]) => name);
|
|
187
|
+
const keyNote = presentKeys.length ? `Provider key(s) detected: ${presentKeys.join(', ')}.` : 'No provider key detected (ANTHROPIC_API_KEY, OPENROUTER_API_KEY, OPENAI_API_KEY).';
|
|
188
|
+
detail = `No model could be resolved for the ${firstUnresolvedTier} tier — orchestration_run would degrade on this env. ${execData.modelResolution?.error?.remediation || ''} ${keyNote}`.trim();
|
|
189
|
+
} else if (reasonCode === 'attached' && execData.degraded) {
|
|
190
|
+
reasonCode = 'execution_degraded';
|
|
191
|
+
detail = execData.degradationReason || 'The execution contract would degrade on this env.';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// A server-self-report catalog is a liveness-shaped fact ("the process
|
|
195
|
+
// exposes tools"), not a readiness-shaped one ("this session can serve
|
|
196
|
+
// orchestration_run") — disclose it on every verdict, pass or fail, rather
|
|
197
|
+
// than laundering it into an unqualified host-session pass. Applied last so
|
|
198
|
+
// a later-computed reasonCode/detail (model_unresolved, execution_degraded)
|
|
199
|
+
// never silently drops this caveat by overwriting `detail` above it.
|
|
200
|
+
if (observationScope === 'server-self-report') {
|
|
201
|
+
detail = `${detail} Derived from the server self-report catalog, not an observed host session; orchestration_run serve-ability for this session was not independently confirmed.`.trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
118
204
|
const verdict = reasonCode === 'attached' ? 'pass' : 'fail';
|
|
205
|
+
const hostExecutionViable = hostExecutionViability({ observationScope, host });
|
|
119
206
|
const bundle = {
|
|
120
207
|
diagnosticId,
|
|
121
208
|
host,
|
|
@@ -133,7 +220,12 @@ export function buildOrchestrationReadiness(input = {}, { env = process.env, cwd
|
|
|
133
220
|
missingTools,
|
|
134
221
|
reasonCode,
|
|
135
222
|
detail,
|
|
136
|
-
|
|
223
|
+
modelResolved,
|
|
224
|
+
credentialMaterializable,
|
|
225
|
+
workerBackend,
|
|
226
|
+
webMode: webGrant.mode,
|
|
227
|
+
hostExecutionViable,
|
|
228
|
+
env: envFacts,
|
|
137
229
|
};
|
|
138
230
|
|
|
139
231
|
return {
|
|
@@ -151,6 +243,15 @@ export function buildOrchestrationReadiness(input = {}, { env = process.env, cwd
|
|
|
151
243
|
constructVersion,
|
|
152
244
|
contractVersion: CONTRACT_VERSION,
|
|
153
245
|
minClientContractVersion: MIN_CLIENT_CONTRACT_VERSION,
|
|
246
|
+
modelResolved,
|
|
247
|
+
credentialMaterializable,
|
|
248
|
+
workerBackend,
|
|
249
|
+
webMode: webGrant.mode,
|
|
250
|
+
// Whether a run planned with no explicit worker_backend on THIS session
|
|
251
|
+
// would both default to (host) and have a real host attached to execute it
|
|
252
|
+
// — distinct from workerBackend, which reports the config-resolved
|
|
253
|
+
// backend a bare CLI/no-session check would predict.
|
|
254
|
+
hostExecutionViable,
|
|
154
255
|
diagnosticBundle: bundle,
|
|
155
256
|
};
|
|
156
257
|
}
|