@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
|
@@ -19,19 +19,81 @@
|
|
|
19
19
|
* exercised end to end against a mock provider without a live key. Failures
|
|
20
20
|
* surface as structured codes (PROVIDER_KEY_MISSING, PROVIDER_MODEL_UNRESOLVED,
|
|
21
21
|
* PROVIDER_EXECUTION_FAILED) rather than opaque HTTP errors.
|
|
22
|
+
*
|
|
23
|
+
* Persona resolution (LMCP-E2): the persona system prompt is resolved ONLY
|
|
24
|
+
* through the pack registry (lib/packs/prompts.mjs resolvePersonaPrompt), with
|
|
25
|
+
* no fallback to reading specialists/prompts/ directly. Team/enterprise packs
|
|
26
|
+
* with a missing or malformed prompt already fail at pack load
|
|
27
|
+
* (lib/packs/loader.mjs); a team/enterprise run that somehow still reaches a
|
|
28
|
+
* miss is refused outright (PERSONA_UNAVAILABLE) rather than executed, so a
|
|
29
|
+
* governed run can never proceed silently under the wrong persona. Solo mode
|
|
30
|
+
* degrades visibly on a miss instead: the task result carries `degraded:
|
|
31
|
+
* 'persona-fallback'` and `personaAvailable: false` rather than a silent
|
|
32
|
+
* generic substitution.
|
|
33
|
+
*
|
|
34
|
+
* Execution provenance (LMCP-F1): every provider-executed task result also
|
|
35
|
+
* carries `specialistId` (the resolved `cx-<role>` id), `packId` (which pack
|
|
36
|
+
* in the registry declared the persona, or null on a solo-mode fallback),
|
|
37
|
+
* `promptVersion` (a 12-char sha256 prefix of the resolved persona body — the
|
|
38
|
+
* same fingerprint convention as lib/prompt-metadata.mjs), `toolGrants` (the
|
|
39
|
+
* specialist's declared claudeTools from the org registry, or `[]` when the
|
|
40
|
+
* registry has no entry for the role), and `executionState` (`executed` on a
|
|
41
|
+
* real persona, `degraded-executed` on the solo-mode fallback; a provider call
|
|
42
|
+
* that throws is caught and recorded `failed` by the caller, runtime.mjs). This
|
|
43
|
+
* answers who ran, under which prompt, with which grants — the basis for audit
|
|
44
|
+
* and evaluation — without requiring a reader to cross-reference the pack
|
|
45
|
+
* registry after the fact.
|
|
46
|
+
*
|
|
47
|
+
* Contract boundary (LMCP-F2): specialists/org/contracts/ declares, per role
|
|
48
|
+
* pair, the shape a handoff packet must carry (validatePacket,
|
|
49
|
+
* lib/specialist-contracts.mjs). A task opts into enforcement by carrying
|
|
50
|
+
* `task.packet` (its inbound handoff) and/or `task.outputPacket` (its
|
|
51
|
+
* produced result); a task without either stays unvalidated rather than
|
|
52
|
+
* being failed against a fabricated contract. An invalid input packet throws
|
|
53
|
+
* CONTRACT_VIOLATION_INPUT before any provider call (hard fail, same catch
|
|
54
|
+
* path as PROVIDER_KEY_MISSING); an invalid output never throws — real,
|
|
55
|
+
* already-paid-for model output rides the result with `contractStatus:
|
|
56
|
+
* 'contract-failed'` and `contractViolations` instead of being discarded.
|
|
57
|
+
* Every violation, input or output, is appended to the tamper-evident
|
|
58
|
+
* `.cx/contract-violations.jsonl` log (lib/contracts/violation-log.mjs).
|
|
22
59
|
*/
|
|
23
60
|
|
|
24
|
-
import {
|
|
25
|
-
import { dirname, join } from 'node:path';
|
|
61
|
+
import { dirname, resolve } from 'node:path';
|
|
26
62
|
import { fileURLToPath } from 'node:url';
|
|
27
|
-
import {
|
|
63
|
+
import { createHash } from 'node:crypto';
|
|
64
|
+
import { resolveSecretAsync } from '../providers/secret-resolver.mjs';
|
|
65
|
+
import { resolveNonNegativeSetting } from '../env-config.mjs';
|
|
28
66
|
import { webSearch } from '../mcp/tools/web-search.mjs';
|
|
29
67
|
import { governWebResults } from '../mcp/tools/web-search-governance.mjs';
|
|
30
68
|
import { roleHoldsWebCapability, resolveWebCapability } from './web-capability.mjs';
|
|
69
|
+
import {
|
|
70
|
+
PROVIDER_ERROR_CODES,
|
|
71
|
+
classifyHttpFailure,
|
|
72
|
+
classifyContentOutcome,
|
|
73
|
+
extractChoiceMeta,
|
|
74
|
+
extractUsage,
|
|
75
|
+
findUnverifiedCitations,
|
|
76
|
+
isMalformedChoice,
|
|
77
|
+
isTimeoutError,
|
|
78
|
+
withProviderRetry,
|
|
79
|
+
} from './provider-outcome.mjs';
|
|
80
|
+
import { loadAllPacks } from '../packs/loader.mjs';
|
|
81
|
+
import { resolvePersonaPrompt } from '../packs/prompts.mjs';
|
|
82
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
83
|
+
import { getSpecialist } from '../registry/loader.mjs';
|
|
84
|
+
import { validatePacket, getIncomingContracts, getOutgoingContracts } from '../specialist-contracts.mjs';
|
|
85
|
+
import { logViolation } from '../contracts/violation-log.mjs';
|
|
31
86
|
|
|
32
87
|
export const INLINE = 'inline';
|
|
33
88
|
export const PROVIDER = 'provider';
|
|
34
|
-
export const
|
|
89
|
+
export const HOST = 'host';
|
|
90
|
+
export const WORKER_BACKEND_SET = [INLINE, PROVIDER, HOST];
|
|
91
|
+
|
|
92
|
+
// LLM completion calls run seconds-to-minutes; openai-python and anthropic-sdk-python
|
|
93
|
+
// both default the overall request timeout to 600s. 120s is a conservative floor that
|
|
94
|
+
// clears real responses without over-committing before the full retry/backoff policy
|
|
95
|
+
// (construct-5wkl AC#4) lands. Exported so tests assert the real default, not a copy.
|
|
96
|
+
export const PROVIDER_TIMEOUT_DEFAULT_MS = 120000;
|
|
35
97
|
|
|
36
98
|
const MAX_OUTPUT_TOKENS = 2048;
|
|
37
99
|
|
|
@@ -41,13 +103,28 @@ const MAX_OUTPUT_TOKENS = 2048;
|
|
|
41
103
|
|
|
42
104
|
const REASONING_BUDGET_TOKENS = 1024;
|
|
43
105
|
|
|
106
|
+
// Bounded retry policy (construct-5wkl AC#4): 3 total attempts (1 original +
|
|
107
|
+
// 2 retries) with exponential backoff, applied only to transport-classified
|
|
108
|
+
// retryable outcomes (rate limit, 5xx, timeout) — never to a content-policy
|
|
109
|
+
// refusal or a malformed body. Configurable so a test or a constrained
|
|
110
|
+
// environment can shrink the window without patching the module.
|
|
111
|
+
|
|
112
|
+
function providerRetryOptions(env) {
|
|
113
|
+
return {
|
|
114
|
+
maxAttempts: resolveNonNegativeSetting(env, 'CONSTRUCT_PROVIDER_MAX_ATTEMPTS', 3),
|
|
115
|
+
baseDelayMs: resolveNonNegativeSetting(env, 'CONSTRUCT_PROVIDER_RETRY_BASE_MS', 250),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
44
119
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
|
-
const
|
|
120
|
+
const PACKAGE_ROOT = resolve(HERE, '..', '..');
|
|
46
121
|
|
|
47
|
-
function providerError(code, reason, remediation) {
|
|
122
|
+
function providerError(code, reason, remediation, { retryable = false, meta = null } = {}) {
|
|
48
123
|
const err = new Error(reason);
|
|
49
124
|
err.code = code;
|
|
50
125
|
err.remediation = remediation;
|
|
126
|
+
err.retryable = retryable;
|
|
127
|
+
if (meta) err.providerMeta = meta;
|
|
51
128
|
return err;
|
|
52
129
|
}
|
|
53
130
|
|
|
@@ -70,6 +147,86 @@ function classifyWorkerProvider(provider, model) {
|
|
|
70
147
|
return 'openrouter';
|
|
71
148
|
}
|
|
72
149
|
|
|
150
|
+
// ── Contract boundary (LMCP-F2) ──────────────────────────────────────────────
|
|
151
|
+
// specialists/org/contracts/ declares, per producer→consumer pair, the shape a
|
|
152
|
+
// handoff packet must carry (validatePacket, lib/specialist-contracts.mjs). The
|
|
153
|
+
// worker is the last place a specialist runs before its output leaves
|
|
154
|
+
// Construct's control, so it is the enforcement point: an invalid input packet
|
|
155
|
+
// must never reach a model call (wasted spend, garbage propagated downstream),
|
|
156
|
+
// and an invalid output must never look identical to a conforming one. Both
|
|
157
|
+
// sides are opt-in on the task (`task.packet` / `task.outputPacket`) — a task
|
|
158
|
+
// that carries no packet has not adopted a structured handoff yet, and skipping
|
|
159
|
+
// validation for it (rather than failing closed on absence) is what keeps every
|
|
160
|
+
// pre-F2 caller's happy path unchanged.
|
|
161
|
+
|
|
162
|
+
// A role's input contract is the one where it is the CONSUMER (what it must
|
|
163
|
+
// receive); ambiguity (zero or multiple matches) is not this bead's job to
|
|
164
|
+
// resolve silently, so callers wanting a specific contract set
|
|
165
|
+
// `task.inputContractId` explicitly. Symmetrically, its output contract is the
|
|
166
|
+
// one where it is the PRODUCER, and runtime.mjs already resolves that id onto
|
|
167
|
+
// `task.handoffContract` (buildTasks, LMCP-F1) — reused here rather than
|
|
168
|
+
// re-deriving it.
|
|
169
|
+
|
|
170
|
+
function resolveInputContractId(task) {
|
|
171
|
+
if (task?.inputContractId) return task.inputContractId;
|
|
172
|
+
const role = `cx-${String(task?.role || '').replace(/^cx-/, '')}`;
|
|
173
|
+
const incoming = getIncomingContracts(role);
|
|
174
|
+
return incoming.length === 1 ? incoming[0].id : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveOutputContractId(task) {
|
|
178
|
+
if (task?.outputContractId) return task.outputContractId;
|
|
179
|
+
if (task?.handoffContract) return task.handoffContract;
|
|
180
|
+
const role = `cx-${String(task?.role || '').replace(/^cx-/, '')}`;
|
|
181
|
+
const outgoing = getOutgoingContracts(role);
|
|
182
|
+
return outgoing.length === 1 ? outgoing[0].id : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Validate a task's incoming handoff packet before any provider call. Throws
|
|
187
|
+
* CONTRACT_VIOLATION_INPUT (hard fail, blocks execution) when a packet is
|
|
188
|
+
* present, a contract resolves, and the packet fails shape validation. A task
|
|
189
|
+
* with no packet or no resolvable contract stays unvalidated: fabricating a
|
|
190
|
+
* contract id to fail an unopted-in task against would turn absence of data
|
|
191
|
+
* into a manufactured violation, so that case is the pre-F2 caller's status
|
|
192
|
+
* quo rather than a new failure mode.
|
|
193
|
+
*/
|
|
194
|
+
export function validateInputPacket(task, { cwd = process.cwd() } = {}) {
|
|
195
|
+
if (task?.packet == null) return { checked: false };
|
|
196
|
+
const contractId = resolveInputContractId(task);
|
|
197
|
+
if (!contractId) return { checked: false };
|
|
198
|
+
|
|
199
|
+
const result = validatePacket(contractId, task.packet, 'input');
|
|
200
|
+
if (result.ok) return { checked: true, ok: true, contractId };
|
|
201
|
+
|
|
202
|
+
logViolation(contractId, 'input', result.missing, task.packet, { verdict: 'CONTRACT_VIOLATION', repoRoot: cwd });
|
|
203
|
+
throw providerError(
|
|
204
|
+
'CONTRACT_VIOLATION_INPUT',
|
|
205
|
+
`Handoff packet for ${task?.role} fails contract '${contractId}': missing ${result.missing.join(', ')}`,
|
|
206
|
+
'Repair the producer packet to include the missing fields, or update the contract definition if the requirement is wrong.',
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate a task's output packet against its role's output contract after a
|
|
212
|
+
* provider call. Never throws — a contract-failed output is recorded and
|
|
213
|
+
* reported to the caller (per policy the caller marks the task), rather than
|
|
214
|
+
* discarding real, already-paid-for model output. A task with no output
|
|
215
|
+
* packet or no resolvable contract is not validated (same opt-in rule as
|
|
216
|
+
* validateInputPacket).
|
|
217
|
+
*/
|
|
218
|
+
export function validateOutputPacket(task, { cwd = process.cwd() } = {}) {
|
|
219
|
+
if (task?.outputPacket == null) return { checked: false, contractStatus: 'unchecked' };
|
|
220
|
+
const contractId = resolveOutputContractId(task);
|
|
221
|
+
if (!contractId) return { checked: false, contractStatus: 'unchecked' };
|
|
222
|
+
|
|
223
|
+
const result = validatePacket(contractId, task.outputPacket, 'output');
|
|
224
|
+
if (result.ok) return { checked: true, contractStatus: 'ok', contractId };
|
|
225
|
+
|
|
226
|
+
logViolation(contractId, 'output', result.missing, task.outputPacket, { verdict: 'CONTRACT_VIOLATION', repoRoot: cwd });
|
|
227
|
+
return { checked: true, contractStatus: 'contract-failed', contractId, violations: result.missing };
|
|
228
|
+
}
|
|
229
|
+
|
|
73
230
|
const WORKER_KEY_VAR = {
|
|
74
231
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
75
232
|
openai: 'OPENAI_API_KEY',
|
|
@@ -90,7 +247,17 @@ const WORKER_PROVIDER_LABEL = {
|
|
|
90
247
|
// empty text. (Source: claude-api skill — extended/adaptive thinking; ADR-0030.)
|
|
91
248
|
|
|
92
249
|
function anthropicThinkingConfig(model) {
|
|
93
|
-
|
|
250
|
+
// Version-agnostic: parse family-major-minor instead of enumerating version
|
|
251
|
+
// ranges, so a newly released model never silently falls back to the legacy
|
|
252
|
+
// budget form (which adaptive-only models reject with a 400). Modern ids put
|
|
253
|
+
// the family first (claude-opus-4-8); legacy ids put the version first
|
|
254
|
+
// (claude-3-5-sonnet) and never match the modern pattern, and any unmatched
|
|
255
|
+
// id is a post-4.6 family that ships adaptive-first.
|
|
256
|
+
const m = String(model || '').toLowerCase();
|
|
257
|
+
const ver = m.match(/(?:opus|sonnet|haiku)-(\d+)(?:[.-](\d+))?/);
|
|
258
|
+
const adaptive = ver
|
|
259
|
+
? Number(ver[1]) > 4 || (Number(ver[1]) === 4 && Number(ver[2] ?? 0) >= 6)
|
|
260
|
+
: !/claude-3/.test(m);
|
|
94
261
|
return adaptive
|
|
95
262
|
? { type: 'adaptive', display: 'summarized' }
|
|
96
263
|
: { type: 'enabled', budget_tokens: REASONING_BUDGET_TOKENS };
|
|
@@ -102,21 +269,123 @@ function anthropicThinkingConfig(model) {
|
|
|
102
269
|
// hermetic switch: a caller that injects its own env (embedded callers, tests)
|
|
103
270
|
// suppresses file/rc discovery so a developer's real key never bleeds into a run.
|
|
104
271
|
|
|
105
|
-
function resolveKey(varName, env, allowAmbient) {
|
|
106
|
-
return
|
|
272
|
+
async function resolveKey(varName, env, allowAmbient) {
|
|
273
|
+
return resolveSecretAsync(varName, { env, allowAmbient });
|
|
107
274
|
}
|
|
108
275
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
276
|
+
// Cached per process, keyed by deployment mode + project cwd: loadAllPacks
|
|
277
|
+
// reads and validates every manifest tier from disk (including the per-project
|
|
278
|
+
// .cx/packs/ tier), which per-task would be wasted work across a
|
|
279
|
+
// multi-specialist run. _resetPackRegistryCache exists for tests that vary
|
|
280
|
+
// either dimension.
|
|
112
281
|
|
|
113
|
-
|
|
282
|
+
const packRegistryCache = new Map();
|
|
283
|
+
|
|
284
|
+
// ADR-0055 prompt resolution order: project packs before user packs, both
|
|
285
|
+
// before builtin. mergePackTiers dedupes by pack id but does not reorder
|
|
286
|
+
// distinct packs across tiers, so the merged list is re-sorted here before a
|
|
287
|
+
// role lookup walks it — a project pack's prompt for a role must win over a
|
|
288
|
+
// builtin pack's prompt for the same role, not just over another version of
|
|
289
|
+
// the same pack id.
|
|
290
|
+
|
|
291
|
+
const PACK_TIER_RANK = { project: 0, user: 1, builtin: 2, unknown: 3 };
|
|
292
|
+
|
|
293
|
+
function sortByTierPrecedence(packs) {
|
|
294
|
+
return [...packs].sort((a, b) => (PACK_TIER_RANK[a._tier] ?? 3) - (PACK_TIER_RANK[b._tier] ?? 3));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getPackRegistry(deploymentMode, env, cwd) {
|
|
298
|
+
const cacheKey = `${deploymentMode}::${cwd}`;
|
|
299
|
+
if (packRegistryCache.has(cacheKey)) return packRegistryCache.get(cacheKey);
|
|
300
|
+
const { packs, errors } = loadAllPacks({ deploymentMode, env, rootDir: cwd, packageRoot: PACKAGE_ROOT });
|
|
301
|
+
const registry = { packs: sortByTierPrecedence(packs), errors };
|
|
302
|
+
packRegistryCache.set(cacheKey, registry);
|
|
303
|
+
return registry;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Internal: clear the cached pack registry. Test-only.
|
|
308
|
+
*/
|
|
309
|
+
export function _resetPackRegistryCache() {
|
|
310
|
+
packRegistryCache.clear();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// The persona prompt is the specialist's system context, resolved ONLY through
|
|
314
|
+
// the pack registry (LMCP-E2) — no fallback to reading specialists/prompts/
|
|
315
|
+
// directly, so a pack boundary violation cannot bypass hard-fail validation
|
|
316
|
+
// done at load time. Team/enterprise packs with a missing prompt already fail
|
|
317
|
+
// at pack load (lib/packs/loader.mjs); PERSONA_UNAVAILABLE here is the
|
|
318
|
+
// defense-in-depth refusal if one somehow still reaches this call. Solo mode
|
|
319
|
+
// returns a visible degraded marker instead of executing under a silently
|
|
320
|
+
// substituted generic persona.
|
|
321
|
+
|
|
322
|
+
// promptVersion is a content fingerprint, not a semantic version — it changes the
|
|
323
|
+
// instant the resolved persona body changes (pack edit, tier override, fallback vs
|
|
324
|
+
// real prompt) so two traces can be diffed for "did the same prompt actually run."
|
|
325
|
+
// Matches the sha256-prefix convention lib/prompt-metadata.mjs already uses for
|
|
326
|
+
// telemetry so a promptVersion string means the same thing everywhere in the repo.
|
|
327
|
+
|
|
328
|
+
function hashPromptVersion(content) {
|
|
329
|
+
return createHash('sha256').update(String(content || '')).digest('hex').slice(0, 12);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function loadPersona(role, { env = process.env, deploymentMode = getDeploymentMode(env), cwd = process.cwd() } = {}) {
|
|
114
333
|
const slug = String(role || '').replace(/^cx-/, '');
|
|
115
|
-
const
|
|
334
|
+
const specialistId = `cx-${slug}`;
|
|
335
|
+
const { packs, errors } = getPackRegistry(deploymentMode, env, cwd);
|
|
336
|
+
|
|
337
|
+
if ((deploymentMode === 'team' || deploymentMode === 'enterprise') && errors.length > 0) {
|
|
338
|
+
throw providerError(
|
|
339
|
+
'PERSONA_UNAVAILABLE',
|
|
340
|
+
`Pack registry failed to load under ${deploymentMode} mode: ${errors.join('; ')}`,
|
|
341
|
+
'Fix the named prompt file or manifest, or set orchestration.workerBackend to "inline".',
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const resolved = resolvePersonaPrompt(specialistId, { packs, packageRoot: PACKAGE_ROOT });
|
|
346
|
+
if (resolved.found) {
|
|
347
|
+
return {
|
|
348
|
+
content: resolved.content,
|
|
349
|
+
available: true,
|
|
350
|
+
specialistId,
|
|
351
|
+
packId: resolved.packId ?? 'unknown',
|
|
352
|
+
promptVersion: hashPromptVersion(resolved.content),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (deploymentMode === 'team' || deploymentMode === 'enterprise') {
|
|
357
|
+
throw providerError(
|
|
358
|
+
'PERSONA_UNAVAILABLE',
|
|
359
|
+
`No pack in the registry declares a prompt for 'cx-${slug}' under ${deploymentMode} mode.`,
|
|
360
|
+
'Add the specialist prompt to a pack manifest, or set orchestration.workerBackend to "inline".',
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const fallbackContent = `You are the ${slug} specialist. Execute your part of the request within your role and return your result directly.`;
|
|
365
|
+
return {
|
|
366
|
+
content: fallbackContent,
|
|
367
|
+
available: false,
|
|
368
|
+
specialistId,
|
|
369
|
+
packId: null,
|
|
370
|
+
promptVersion: hashPromptVersion(fallbackContent),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Tool grants are the specialist's declared claudeTools string (org registry,
|
|
375
|
+
// specialists/org/**) — the least-privilege surface OWASP GenAI excessive-agency
|
|
376
|
+
// audits need per actor. A role the registry does not know (fallback persona,
|
|
377
|
+
// custom pack-only role) reports an empty grant list rather than throwing, since
|
|
378
|
+
// absence of a registry entry does not mean absence of an execution.
|
|
379
|
+
|
|
380
|
+
function resolveToolGrants(specialistId, { cwd = process.cwd() } = {}) {
|
|
116
381
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
382
|
+
const spec = getSpecialist(specialistId, { rootDir: cwd });
|
|
383
|
+
const raw = spec?.claudeTools;
|
|
384
|
+
if (typeof raw !== 'string' || !raw.trim()) return [];
|
|
385
|
+
return raw.split(',').map((t) => t.trim()).filter(Boolean);
|
|
386
|
+
} catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
120
389
|
}
|
|
121
390
|
|
|
122
391
|
function buildUserPrompt({ task, run }) {
|
|
@@ -126,6 +395,40 @@ function buildUserPrompt({ task, run }) {
|
|
|
126
395
|
return `Request: ${summary}${reason}${handoff}\n\nDo your part of this request as the ${String(task?.role || '').replace(/^cx-/, '')} specialist. Return your result directly.`;
|
|
127
396
|
}
|
|
128
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Materialize the specialist prompt (persona + user turn) and provenance a task
|
|
400
|
+
* would run under, without calling any model. Shared by the provider executor
|
|
401
|
+
* (which then hands the result to a real model call) and the host worker
|
|
402
|
+
* backend (which hands the same result to the calling host to execute in its
|
|
403
|
+
* own session) — a host and a provider must never see two different prompts
|
|
404
|
+
* for the same task (LMCP host-execution).
|
|
405
|
+
*
|
|
406
|
+
* @param {object} opts
|
|
407
|
+
* @param {object} opts.task
|
|
408
|
+
* @param {object} opts.run
|
|
409
|
+
* @param {string} [opts.cwd]
|
|
410
|
+
* @param {Record<string,string>} [opts.env]
|
|
411
|
+
* @returns {{system:string, user:string, specialistId:string, packId:string|null,
|
|
412
|
+
* promptVersion:string, toolGrants:string[], personaAvailable:boolean, degraded?:string}}
|
|
413
|
+
*/
|
|
414
|
+
export function materializeTaskPrompt({ task, run, cwd = process.cwd(), env = process.env } = {}) {
|
|
415
|
+
const deploymentMode = run?.execution?.deploymentMode || getDeploymentMode(env);
|
|
416
|
+
const persona = loadPersona(task?.role, { env, deploymentMode, cwd });
|
|
417
|
+
const system = persona.content;
|
|
418
|
+
const user = buildUserPrompt({ task, run });
|
|
419
|
+
const toolGrants = resolveToolGrants(persona.specialistId, { cwd });
|
|
420
|
+
return {
|
|
421
|
+
system,
|
|
422
|
+
user,
|
|
423
|
+
specialistId: persona.specialistId,
|
|
424
|
+
packId: persona.packId,
|
|
425
|
+
promptVersion: persona.promptVersion,
|
|
426
|
+
toolGrants,
|
|
427
|
+
personaAvailable: persona.available,
|
|
428
|
+
...(persona.available ? {} : { degraded: 'persona-fallback' }),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
129
432
|
async function bodyExcerpt(res) {
|
|
130
433
|
try {
|
|
131
434
|
return (await res.text()).slice(0, 200);
|
|
@@ -157,7 +460,54 @@ function timedFetch(fetchImpl, url, opts, timeoutMs) {
|
|
|
157
460
|
]);
|
|
158
461
|
}
|
|
159
462
|
|
|
160
|
-
|
|
463
|
+
function remediationForCode(code, label, keyVar) {
|
|
464
|
+
if (code === PROVIDER_ERROR_CODES.RATE_LIMITED) return `${label} rate-limited this request and retries were exhausted; wait before retrying, or switch tiers/providers.`;
|
|
465
|
+
if (code === PROVIDER_ERROR_CODES.SERVER_ERROR) return `${label} returned a server error and retries were exhausted; retry later or switch providers.`;
|
|
466
|
+
if (code === PROVIDER_ERROR_CODES.AUTH_ERROR) return `Verify ${keyVar} is a valid, current credential for ${label}, or re-run with worker_backend "host" to execute specialists in the calling session with no provider key at all.`;
|
|
467
|
+
if (code === PROVIDER_ERROR_CODES.NO_CREDITS) return `${label} reports insufficient credits (HTTP 402). Add credits, or re-run with worker_backend "host" — the calling agent executes each specialist prompt in its own session at no API cost.`;
|
|
468
|
+
return `Verify the model id and ${keyVar}, or set orchestration.workerBackend to "inline".`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// One transport call, wrapped in the bounded retry policy (construct-5wkl
|
|
472
|
+
// AC#4): a rate limit, a 5xx, or a timeout each classify as retryable and get
|
|
473
|
+
// re-attempted with backoff; anything else (auth failure, 4xx) fails on the
|
|
474
|
+
// first attempt. Returns the parsed JSON body plus elapsedMs/retryCount so the
|
|
475
|
+
// caller can attach provider metadata regardless of whether content
|
|
476
|
+
// classification later succeeds or fails.
|
|
477
|
+
|
|
478
|
+
async function postJsonRetryable(fetchImpl, url, headers, body, timeoutMs, { label, keyVar, env }) {
|
|
479
|
+
const startedAt = Date.now();
|
|
480
|
+
const { maxAttempts, baseDelayMs } = providerRetryOptions(env);
|
|
481
|
+
const data = await withProviderRetry(async () => {
|
|
482
|
+
let res;
|
|
483
|
+
try {
|
|
484
|
+
res = await timedFetch(fetchImpl, url, { method: 'POST', headers, body: JSON.stringify(body) }, timeoutMs);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (isTimeoutError(err)) {
|
|
487
|
+
throw providerError(PROVIDER_ERROR_CODES.TIMEOUT, `${label} specialist execution timed out after ${timeoutMs}ms`, `${label} did not respond in time and retries were exhausted; raise CONSTRUCT_PROVIDER_TIMEOUT_MS or switch providers.`, { retryable: true });
|
|
488
|
+
}
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
491
|
+
if (!res.ok) {
|
|
492
|
+
const { code, retryable } = classifyHttpFailure(res.status);
|
|
493
|
+
throw providerError(code, `${label} specialist execution failed (HTTP ${res.status}): ${await bodyExcerpt(res)}`, remediationForCode(code, label, keyVar), { retryable });
|
|
494
|
+
}
|
|
495
|
+
return res.json();
|
|
496
|
+
}, { maxAttempts, baseDelayMs });
|
|
497
|
+
return { data, elapsedMs: Date.now() - startedAt, retryCount: data.retryCount ?? 0 };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Anthropic's own stop-reason vocabulary (end_turn/max_tokens/stop_sequence/
|
|
501
|
+
// tool_use/refusal) maps onto the shared content-outcome codes so
|
|
502
|
+
// classifyContentOutcome can treat every provider family the same way.
|
|
503
|
+
|
|
504
|
+
function normalizeAnthropicStopReason(stopReason) {
|
|
505
|
+
if (stopReason === 'max_tokens') return 'length';
|
|
506
|
+
if (stopReason === 'refusal') return 'content_filter';
|
|
507
|
+
return stopReason ?? null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function callAnthropic({ model, apiKey, system, user, fetchImpl, reasoning, timeoutMs, env }) {
|
|
161
511
|
const thinking = reasoning ? anthropicThinkingConfig(model) : null;
|
|
162
512
|
const body = {
|
|
163
513
|
model: model.replace(/^anthropic\//, ''),
|
|
@@ -166,78 +516,113 @@ async function callAnthropic({ model, apiKey, system, user, fetchImpl, reasoning
|
|
|
166
516
|
messages: [{ role: 'user', content: [{ type: 'text', text: user }] }],
|
|
167
517
|
};
|
|
168
518
|
if (thinking) body.thinking = thinking;
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
body:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
519
|
+
const { data, elapsedMs, retryCount } = await postJsonRetryable(
|
|
520
|
+
fetchImpl, 'https://api.anthropic.com/v1/messages',
|
|
521
|
+
{ 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },
|
|
522
|
+
body, timeoutMs, { label: 'Anthropic', keyVar: 'ANTHROPIC_API_KEY', env },
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
if (!Array.isArray(data.content)) {
|
|
526
|
+
throw providerError(PROVIDER_ERROR_CODES.MALFORMED_RESPONSE, 'Anthropic response is missing content blocks', 'Retry, or set orchestration.workerBackend to "inline".', {
|
|
527
|
+
meta: { provider: 'anthropic', model, elapsedMs, retryCount },
|
|
528
|
+
});
|
|
176
529
|
}
|
|
177
|
-
const
|
|
178
|
-
const blocks = data.content || [];
|
|
530
|
+
const blocks = data.content;
|
|
179
531
|
const output = blocks.filter((b) => b && b.type === 'text').map((b) => b.text).join('');
|
|
180
532
|
const thought = blocks.filter((b) => b && b.type === 'thinking').map((b) => b.thinking || '').join('').trim();
|
|
181
|
-
|
|
533
|
+
const finishReason = normalizeAnthropicStopReason(data.stop_reason);
|
|
534
|
+
const usage = data.usage ? {
|
|
535
|
+
promptTokens: data.usage.input_tokens,
|
|
536
|
+
completionTokens: data.usage.output_tokens,
|
|
537
|
+
totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
|
|
538
|
+
} : null;
|
|
539
|
+
const meta = {
|
|
540
|
+
provider: 'anthropic', model, finishReason, nativeFinishReason: data.stop_reason ?? null, usage,
|
|
541
|
+
elapsedMs, retryCount, reasoningRequested: Boolean(reasoning), reasoningReturned: Boolean(thought),
|
|
542
|
+
};
|
|
543
|
+
const outcome = classifyContentOutcome({ content: output, finishReason, reasoning: thought, wantsReasoning: reasoning });
|
|
544
|
+
if (!outcome.ok) {
|
|
545
|
+
throw providerError(outcome.code, `Anthropic returned no usable answer (${outcome.code}, finish_reason=${finishReason})`, contentOutcomeRemediation(outcome.code), { retryable: outcome.retryable, meta });
|
|
546
|
+
}
|
|
547
|
+
return { output, reasoning: thought, meta };
|
|
182
548
|
}
|
|
183
549
|
|
|
184
|
-
async function callOpenRouter({ model, apiKey, system, user, fetchImpl, reasoning, timeoutMs }) {
|
|
550
|
+
async function callOpenRouter({ model, apiKey, system, user, fetchImpl, reasoning, timeoutMs, env }) {
|
|
185
551
|
const body = {
|
|
186
552
|
model: model.replace(/^openrouter\//, ''),
|
|
187
|
-
|
|
553
|
+
// Reasoning tokens draw from the same completion budget on OpenRouter as
|
|
554
|
+
// the visible answer — without the extra headroom a reasoning-heavy model
|
|
555
|
+
// can spend the entire budget on reasoning and return empty content
|
|
556
|
+
// (construct-5wkl AC#6), mirroring the Anthropic thinking-budget headroom.
|
|
557
|
+
max_tokens: reasoning ? MAX_OUTPUT_TOKENS + REASONING_BUDGET_TOKENS : MAX_OUTPUT_TOKENS,
|
|
188
558
|
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
|
|
189
559
|
};
|
|
190
560
|
// Enable reasoning when wanted; explicitly exclude it otherwise — some models
|
|
191
561
|
// (e.g. Gemini) return reasoning by default, which would leak in hidden mode.
|
|
192
562
|
// An empty `{}` is a no-op on OpenRouter; `{enabled:true}` is the on switch.
|
|
193
563
|
body.reasoning = reasoning ? { enabled: true } : { exclude: true };
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
body:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
throw providerError('PROVIDER_EXECUTION_FAILED', `OpenRouter specialist execution failed (HTTP ${res.status}): ${await bodyExcerpt(res)}`, 'Verify the model id and OPENROUTER_API_KEY, or set orchestration.workerBackend to "inline".');
|
|
201
|
-
}
|
|
202
|
-
const data = await res.json();
|
|
203
|
-
const message = data.choices?.[0]?.message || {};
|
|
204
|
-
return { output: message.content || '', reasoning: reasoning ? extractOpenRouterReasoning(message) : '' };
|
|
564
|
+
const { data, elapsedMs, retryCount } = await postJsonRetryable(
|
|
565
|
+
fetchImpl, 'https://openrouter.ai/api/v1/chat/completions',
|
|
566
|
+
{ Authorization: `Bearer ${apiKey}`, 'content-type': 'application/json', 'HTTP-Referer': 'https://github.com/geraldmaron/construct' },
|
|
567
|
+
body, timeoutMs, { label: 'OpenRouter', keyVar: 'OPENROUTER_API_KEY', env },
|
|
568
|
+
);
|
|
569
|
+
return finishOpenAiStyleCall(data, { provider: 'openrouter', model, elapsedMs, retryCount, reasoning, label: 'OpenRouter' });
|
|
205
570
|
}
|
|
206
571
|
|
|
207
|
-
async function callCopilot({ model, system, user, fetchImpl, timeoutMs }) {
|
|
572
|
+
async function callCopilot({ model, system, user, fetchImpl, timeoutMs, env }) {
|
|
208
573
|
const { getCopilotToken, copilotApiHeaders, COPILOT_API_BASE } = await import('../providers/copilot-auth.mjs');
|
|
209
574
|
const token = await getCopilotToken({ fetchImpl });
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
575
|
+
const { data, elapsedMs, retryCount } = await postJsonRetryable(
|
|
576
|
+
fetchImpl, `${COPILOT_API_BASE}/chat/completions`,
|
|
577
|
+
{ Authorization: `Bearer ${token}`, 'content-type': 'application/json', ...copilotApiHeaders() },
|
|
578
|
+
{ model: model.replace(/^github-copilot\//, ''), messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },
|
|
579
|
+
timeoutMs, { label: 'GitHub Copilot', keyVar: 'a Copilot login', env },
|
|
580
|
+
);
|
|
581
|
+
return finishOpenAiStyleCall(data, { provider: 'github-copilot', model, elapsedMs, retryCount, reasoning: false, label: 'GitHub Copilot' });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function callOpenAI({ model, apiKey, system, user, fetchImpl, timeoutMs, env }) {
|
|
585
|
+
const { data, elapsedMs, retryCount } = await postJsonRetryable(
|
|
586
|
+
fetchImpl, 'https://api.openai.com/v1/chat/completions',
|
|
587
|
+
{ Authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
|
588
|
+
{ model: model.replace(/^openai\//, ''), messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },
|
|
589
|
+
timeoutMs, { label: 'OpenAI', keyVar: 'OPENAI_API_KEY', env },
|
|
590
|
+
);
|
|
591
|
+
return finishOpenAiStyleCall(data, { provider: 'openai', model, elapsedMs, retryCount, reasoning: false, label: 'OpenAI' });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function contentOutcomeRemediation(code) {
|
|
595
|
+
if (code === PROVIDER_ERROR_CODES.CONTENT_FILTERED) return 'The provider refused on content-policy grounds; rephrase the task or route it to a different model — retrying the same request will not help.';
|
|
596
|
+
if (code === PROVIDER_ERROR_CODES.REASONING_ONLY) return 'The model spent its entire output budget on reasoning and returned no visible answer; this should not recur now that the reasoning budget reserves headroom for the answer, but if it does, raise the output-token ceiling.';
|
|
597
|
+
if (code === PROVIDER_ERROR_CODES.EMPTY_CONTENT) return 'The provider returned a 2xx response with no usable answer content; retry, or switch providers/models.';
|
|
598
|
+
return 'Retry, or set orchestration.workerBackend to "inline".';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Shared tail for every OpenAI-shaped (choices[0].message) response:
|
|
602
|
+
// OpenRouter, OpenAI, and GitHub Copilot all use this shape. Malformed-choice,
|
|
603
|
+
// finish_reason, usage, and reasoning-only classification are identical across
|
|
604
|
+
// the three; only the reasoning-extraction source differs (OpenRouter alone
|
|
605
|
+
// returns a `reasoning`/`reasoning_details` field).
|
|
606
|
+
|
|
607
|
+
function finishOpenAiStyleCall(data, { provider, model, elapsedMs, retryCount, reasoning, label }) {
|
|
608
|
+
if (isMalformedChoice(data)) {
|
|
609
|
+
throw providerError(PROVIDER_ERROR_CODES.MALFORMED_RESPONSE, `${label} response is missing choices[0].message`, 'Retry, or set orchestration.workerBackend to "inline".', {
|
|
610
|
+
meta: { provider, model, elapsedMs, retryCount },
|
|
611
|
+
});
|
|
220
612
|
}
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
|
|
233
|
-
}),
|
|
234
|
-
}, timeoutMs);
|
|
235
|
-
if (!res.ok) {
|
|
236
|
-
throw providerError('PROVIDER_EXECUTION_FAILED', `OpenAI specialist execution failed (HTTP ${res.status}): ${await bodyExcerpt(res)}`, 'Verify the model id and OPENAI_API_KEY, or set orchestration.workerBackend to "inline".');
|
|
613
|
+
const { message, finishReason, nativeFinishReason } = extractChoiceMeta(data);
|
|
614
|
+
const output = message.content || '';
|
|
615
|
+
const thought = provider === 'openrouter' && reasoning ? extractOpenRouterReasoning(message) : '';
|
|
616
|
+
const usage = extractUsage(data);
|
|
617
|
+
const meta = {
|
|
618
|
+
provider, model, finishReason, nativeFinishReason, usage, elapsedMs, retryCount,
|
|
619
|
+
reasoningRequested: Boolean(reasoning), reasoningReturned: Boolean(thought),
|
|
620
|
+
};
|
|
621
|
+
const outcome = classifyContentOutcome({ content: output, finishReason, reasoning: thought, wantsReasoning: reasoning });
|
|
622
|
+
if (!outcome.ok) {
|
|
623
|
+
throw providerError(outcome.code, `${label} returned no usable answer (${outcome.code}, finish_reason=${finishReason})`, contentOutcomeRemediation(outcome.code), { retryable: outcome.retryable, meta });
|
|
237
624
|
}
|
|
238
|
-
|
|
239
|
-
const message = data.choices?.[0]?.message || {};
|
|
240
|
-
return { output: message.content || '', reasoning: '' };
|
|
625
|
+
return { output, reasoning: thought, meta };
|
|
241
626
|
}
|
|
242
627
|
|
|
243
628
|
// ── Web-capable specialist execution (ADR-0050) ──────────────────────────────
|
|
@@ -411,18 +796,18 @@ async function runNativeOpenRouter({ model, apiKey, system, user, fetchImpl, tim
|
|
|
411
796
|
// Single-shot honest fallback: no web tool, the no-web clause appended to the system
|
|
412
797
|
// prompt so the specialist refuses rather than fabricates.
|
|
413
798
|
|
|
414
|
-
async function runHonestNoWeb({ family, model, apiKey, system, user, fetchImpl, timeoutMs }) {
|
|
799
|
+
async function runHonestNoWeb({ family, model, apiKey, system, user, fetchImpl, timeoutMs, env }) {
|
|
415
800
|
const honestSystem = system + NO_WEB_CLAUSE;
|
|
416
|
-
if (family === 'github-copilot') return callCopilot({ model, system: honestSystem, user, fetchImpl, timeoutMs });
|
|
417
|
-
if (family === 'anthropic') return callAnthropic({ model, apiKey, system: honestSystem, user, fetchImpl, reasoning: false, timeoutMs });
|
|
418
|
-
if (family === 'openai') return callOpenAI({ model, apiKey, system: honestSystem, user, fetchImpl, timeoutMs });
|
|
419
|
-
return callOpenRouter({ model, apiKey, system: honestSystem, user, fetchImpl, reasoning: false, timeoutMs });
|
|
801
|
+
if (family === 'github-copilot') return callCopilot({ model, system: honestSystem, user, fetchImpl, timeoutMs, env });
|
|
802
|
+
if (family === 'anthropic') return callAnthropic({ model, apiKey, system: honestSystem, user, fetchImpl, reasoning: false, timeoutMs, env });
|
|
803
|
+
if (family === 'openai') return callOpenAI({ model, apiKey, system: honestSystem, user, fetchImpl, timeoutMs, env });
|
|
804
|
+
return callOpenRouter({ model, apiKey, system: honestSystem, user, fetchImpl, reasoning: false, timeoutMs, env });
|
|
420
805
|
}
|
|
421
806
|
|
|
422
807
|
async function runWebCapableTask({ family, model, apiKey, system, user, fetchImpl, env, timeoutMs, now, defaultClaim }) {
|
|
423
808
|
const grant = resolveWebCapability({ family, env });
|
|
424
|
-
const maxRounds =
|
|
425
|
-
const loopCap =
|
|
809
|
+
const maxRounds = resolveNonNegativeSetting(env, 'CONSTRUCT_WORKER_TOOL_ROUNDS', 4);
|
|
810
|
+
const loopCap = resolveNonNegativeSetting(env, 'CONSTRUCT_WORKER_WEB_LOOP_CAP', maxRounds);
|
|
426
811
|
|
|
427
812
|
if (grant.mode === 'governed') {
|
|
428
813
|
if (family === 'anthropic') {
|
|
@@ -450,8 +835,8 @@ async function runWebCapableTask({ family, model, apiKey, system, user, fetchImp
|
|
|
450
835
|
}
|
|
451
836
|
|
|
452
837
|
// host-delegated and unavailable both mean this worker cannot fetch: answer honestly.
|
|
453
|
-
const r = await runHonestNoWeb({ family, model, apiKey, system, user, fetchImpl, timeoutMs });
|
|
454
|
-
return { output: r.output, reasoning: '', webCapability: grant.mode === 'host-delegated' ? 'host-delegated' : 'unavailable', webEvidence: [], webCalls: 0, webSearchRequests: 0 };
|
|
838
|
+
const r = await runHonestNoWeb({ family, model, apiKey, system, user, fetchImpl, timeoutMs, env });
|
|
839
|
+
return { output: r.output, reasoning: '', meta: r.meta, webCapability: grant.mode === 'host-delegated' ? 'host-delegated' : 'unavailable', webEvidence: [], webCalls: 0, webSearchRequests: 0 };
|
|
455
840
|
}
|
|
456
841
|
|
|
457
842
|
/**
|
|
@@ -467,16 +852,33 @@ async function runWebCapableTask({ family, model, apiKey, system, user, fetchImp
|
|
|
467
852
|
* @param {string} [opts.chainOfThought] reasoning disclosure mode: `hidden` (default,
|
|
468
853
|
* no reasoning requested) | `surface` | `telemetry_only` (both request and
|
|
469
854
|
* return the model's reasoning for the caller to display or record)
|
|
470
|
-
* @
|
|
855
|
+
* @param {string} [opts.cwd] project root, for project-tier pack resolution (.cx/packs/)
|
|
856
|
+
* @returns {Promise<{output:string, reasoning:string, model:string, provider:string, characters:number,
|
|
857
|
+
* personaAvailable:boolean, degraded?:string, specialistId:string, packId:string|null,
|
|
858
|
+
* promptVersion:string, toolGrants:string[], executionState:string, contractStatus:string,
|
|
859
|
+
* contractId?:string, contractViolations?:string[]}>}
|
|
471
860
|
*/
|
|
472
|
-
export async function runTaskViaProvider({ task, run, model, provider = null, env = process.env, fetchImpl = globalThis.fetch, chainOfThought = 'hidden' } = {}) {
|
|
861
|
+
export async function runTaskViaProvider({ task, run, model, provider = null, env = process.env, fetchImpl = globalThis.fetch, chainOfThought = 'hidden', cwd = process.cwd() } = {}) {
|
|
473
862
|
if (!model) throw providerError('PROVIDER_MODEL_UNRESOLVED', 'Provider worker backend selected but no model resolved.', 'Configure the model tier registry so a model resolves, or set orchestration.workerBackend to "inline".');
|
|
474
863
|
if (typeof fetchImpl !== 'function') throw providerError('PROVIDER_NO_FETCH', 'No fetch implementation available for provider execution.', 'Run on a runtime with global fetch (Node 18+) or inject fetchImpl.');
|
|
475
864
|
|
|
476
|
-
|
|
865
|
+
// Packet validation runs before any model call: an invalid handoff must never
|
|
866
|
+
// reach the provider (LMCP-F2). Throws CONTRACT_VIOLATION_INPUT, caught by
|
|
867
|
+
// the same path that already handles PROVIDER_KEY_MISSING etc.
|
|
868
|
+
validateInputPacket(task, { cwd });
|
|
869
|
+
|
|
870
|
+
// The shared resolver honors an explicit 0 (near-instant abort) and falls back to the
|
|
871
|
+
// minute-scale default for unset/empty/garbage/negative values — a bare `Number(env)`
|
|
872
|
+
// would turn "abc" into NaN and every real provider call would abort immediately.
|
|
873
|
+
const timeoutMs = resolveNonNegativeSetting(env, 'CONSTRUCT_PROVIDER_TIMEOUT_MS', PROVIDER_TIMEOUT_DEFAULT_MS);
|
|
477
874
|
const family = classifyWorkerProvider(provider, model);
|
|
478
|
-
|
|
479
|
-
|
|
875
|
+
// The same materialization the host worker backend uses (LMCP host-execution):
|
|
876
|
+
// persona + user turn + provenance, resolved once here and handed to the model
|
|
877
|
+
// call below — a provider-executed task and a host-executed task run under
|
|
878
|
+
// byte-identical prompts for the same task.
|
|
879
|
+
const prompt = materializeTaskPrompt({ task, run, cwd, env });
|
|
880
|
+
const system = prompt.system;
|
|
881
|
+
const user = prompt.user;
|
|
480
882
|
const wantsReasoning = chainOfThought !== 'hidden';
|
|
481
883
|
const webCapable = roleHoldsWebCapability(task?.role);
|
|
482
884
|
const now = Date.now();
|
|
@@ -486,7 +888,7 @@ export async function runTaskViaProvider({ task, run, model, provider = null, en
|
|
|
486
888
|
let apiKey = null;
|
|
487
889
|
if (family !== 'github-copilot') {
|
|
488
890
|
const keyVar = WORKER_KEY_VAR[family];
|
|
489
|
-
apiKey = resolveKey(keyVar, env, env === process.env);
|
|
891
|
+
apiKey = await resolveKey(keyVar, env, env === process.env);
|
|
490
892
|
if (!apiKey) {
|
|
491
893
|
throw providerError('PROVIDER_KEY_MISSING', `No API key for ${WORKER_PROVIDER_LABEL[family]} specialist execution.`, `Set ${keyVar} (a value or an op:// reference), or set orchestration.workerBackend to "inline".`);
|
|
492
894
|
}
|
|
@@ -494,27 +896,64 @@ export async function runTaskViaProvider({ task, run, model, provider = null, en
|
|
|
494
896
|
|
|
495
897
|
let output;
|
|
496
898
|
let reasoning;
|
|
899
|
+
let meta = null;
|
|
497
900
|
let web = null;
|
|
498
901
|
if (webCapable) {
|
|
499
902
|
const defaultClaim = String(run?.request?.summary || run?.request || task?.reason || 'the subject under research').slice(0, 200);
|
|
500
903
|
web = await runWebCapableTask({ family, model, apiKey, system, user, fetchImpl, env, timeoutMs, now, defaultClaim });
|
|
501
|
-
({ output, reasoning } = web);
|
|
904
|
+
({ output, reasoning, meta } = web);
|
|
502
905
|
} else if (family === 'github-copilot') {
|
|
503
|
-
({ output, reasoning } = await callCopilot({ model, system, user, fetchImpl, timeoutMs }));
|
|
906
|
+
({ output, reasoning, meta } = await callCopilot({ model, system, user, fetchImpl, timeoutMs, env }));
|
|
504
907
|
} else {
|
|
505
908
|
const call = family === 'anthropic' ? callAnthropic
|
|
506
909
|
: family === 'openai' ? callOpenAI
|
|
507
910
|
: callOpenRouter;
|
|
508
|
-
({ output, reasoning } = await call({ model, apiKey, system, user, fetchImpl, reasoning: wantsReasoning, timeoutMs }));
|
|
911
|
+
({ output, reasoning, meta } = await call({ model, apiKey, system, user, fetchImpl, reasoning: wantsReasoning, timeoutMs, env }));
|
|
509
912
|
}
|
|
510
913
|
|
|
511
914
|
const text = output || '';
|
|
915
|
+
const toolGrants = prompt.toolGrants;
|
|
916
|
+
// executionState is task-scoped here: the provider call above either returned
|
|
917
|
+
// (this function only reaches this line on success) or threw (the caller,
|
|
918
|
+
// runtime.mjs executeTaskViaProvider, catches it and records 'failed' itself).
|
|
919
|
+
// A degraded persona still genuinely executed the model call, so it is
|
|
920
|
+
// 'degraded-executed' rather than 'failed' — the run did not fail, the
|
|
921
|
+
// specialist prompt it ran under was the visible fallback.
|
|
922
|
+
const executionState = prompt.personaAvailable ? 'executed' : 'degraded-executed';
|
|
923
|
+
|
|
924
|
+
// Output validation runs after the provider call and never throws: a
|
|
925
|
+
// contract-failed output is still real, already-paid-for model output, so it
|
|
926
|
+
// rides the result with contractStatus rather than being discarded
|
|
927
|
+
// (LMCP-F2). Checked only when the task opted in via task.outputPacket.
|
|
928
|
+
const outputCheck = validateOutputPacket(task, { cwd });
|
|
929
|
+
|
|
930
|
+
// Evidence grounding (construct-5wkl AC#5): a web-capable specialist's only
|
|
931
|
+
// observed evidence is its own governed webEvidence (ADR-0050 trust/Admiralty
|
|
932
|
+
// labeling). A URL cited in the answer that is not among that evidence was
|
|
933
|
+
// never actually retrieved through Construct's governed path — either
|
|
934
|
+
// fabricated or recalled from ungoverned model memory — so the output is
|
|
935
|
+
// downgraded with a warning rather than presented as verified evidence.
|
|
936
|
+
const evidenceUrls = web?.webEvidence ? web.webEvidence.map((e) => e?.url).filter(Boolean) : null;
|
|
937
|
+
const unverifiedCitations = evidenceUrls ? findUnverifiedCitations(text, evidenceUrls) : [];
|
|
938
|
+
|
|
512
939
|
return {
|
|
513
940
|
output: text,
|
|
514
941
|
reasoning: wantsReasoning ? (reasoning || '') : '',
|
|
515
942
|
model,
|
|
516
943
|
provider: family,
|
|
517
944
|
characters: text.length,
|
|
945
|
+
personaAvailable: prompt.personaAvailable,
|
|
946
|
+
specialistId: prompt.specialistId,
|
|
947
|
+
packId: prompt.packId,
|
|
948
|
+
promptVersion: prompt.promptVersion,
|
|
949
|
+
toolGrants,
|
|
950
|
+
executionState,
|
|
951
|
+
contractStatus: outputCheck.contractStatus,
|
|
952
|
+
...(outputCheck.checked ? { contractId: outputCheck.contractId } : {}),
|
|
953
|
+
...(outputCheck.contractStatus === 'contract-failed' ? { contractViolations: outputCheck.violations } : {}),
|
|
954
|
+
...(prompt.degraded ? { degraded: prompt.degraded } : {}),
|
|
518
955
|
...(web ? { webCapability: web.webCapability, webEvidence: web.webEvidence || [], webCalls: web.webCalls || 0, webSearchRequests: web.webSearchRequests || 0 } : {}),
|
|
956
|
+
...(unverifiedCitations.length ? { evidenceStatus: 'unverified-citations', unverifiedCitations } : {}),
|
|
957
|
+
...(meta ? { providerMeta: meta } : {}),
|
|
519
958
|
};
|
|
520
959
|
}
|