@geraldmaron/construct 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1029 -65
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +7 -30
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +109 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +23 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embed/worker.mjs +5 -0
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +60 -11
- package/lib/export-validate.mjs +34 -2
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host/readiness.mjs +109 -0
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +29 -8
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/install/stage-project.mjs +41 -4
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/prepare.mjs +2 -0
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-approval.mjs +57 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +185 -1268
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-rate-limit.mjs +47 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +95 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +197 -19
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/project.mjs +25 -8
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +2 -2
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +5 -76
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +53 -20
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/daemon-entry.mjs +6 -0
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +508 -62
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +60 -0
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +763 -80
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/output-quality.mjs +61 -2
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/path-policy.mjs +56 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +261 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +41 -18
- package/lib/providers/secret-resolver.mjs +263 -31
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/catalog.mjs +6 -0
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +15 -4
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/registry/validate.mjs +3 -3
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/sandbox.mjs +1 -1
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +93 -24
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +84 -7
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +103 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +20 -7
- package/personas/construct.md +12 -3
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1987 -0
- package/rules/common/comments.md +1 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +215 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +546 -0
- package/scripts/sync-specialists.mjs +336 -106
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +41 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +27 -31
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/queue-registry.mjs — kind:'queue' provider resolution.
|
|
3
|
+
*
|
|
4
|
+
* Substrate owns the queue/run-store CONTRACT; git-queue is the zero-dependency
|
|
5
|
+
* DEFAULT provider and Postgres is an OPTIONAL provider. Selection is explicit:
|
|
6
|
+
* a concrete queue backend is chosen by resolving an installed kind:'queue'
|
|
7
|
+
* manifest through the extension registry (construct-9oi4.7.11, supersedes the
|
|
8
|
+
* ADR-0051 git-vs-postgres framing). No silent postgres->git alias exists — a
|
|
9
|
+
* caller that names a backend with no registered kind:'queue' provider gets a
|
|
10
|
+
* typed error, not a swallowed downgrade.
|
|
11
|
+
*
|
|
12
|
+
* Resolution surface:
|
|
13
|
+
* listQueueProviders({ rootDir, env }) → manifest[] of kind:'queue', merged
|
|
14
|
+
* across builtin/user/project tiers (project wins).
|
|
15
|
+
* findQueueProvider(backend, { rootDir, env }) → manifest | null for a backend id.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { loadManifestsFromDir, mergeManifests, resolveManifestDirs } from '../extensions/loader.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The queue backend ids that are always resolvable without a registered
|
|
22
|
+
* manifest. git is the default provider; filesystem is the solo-mode default
|
|
23
|
+
* provider. Both ship in-tree, so they never require an extension manifest.
|
|
24
|
+
*/
|
|
25
|
+
export const BUILTIN_QUEUE_BACKENDS = ['git', 'filesystem'];
|
|
26
|
+
|
|
27
|
+
// Load every kind:'queue' manifest across the three tiers and merge by id so a
|
|
28
|
+
// project- or user-level Postgres queue provider overrides a builtin of the
|
|
29
|
+
// same id. Only manifests declaring kind:'queue' are returned.
|
|
30
|
+
|
|
31
|
+
export function listQueueProviders({ rootDir = process.cwd(), env = process.env } = {}) {
|
|
32
|
+
const homeDir = env?.HOME || env?.USERPROFILE || undefined;
|
|
33
|
+
const dirs = resolveManifestDirs({ rootDir, homeDir });
|
|
34
|
+
const builtin = loadManifestsFromDir(dirs.builtin).manifests;
|
|
35
|
+
const user = loadManifestsFromDir(dirs.user).manifests;
|
|
36
|
+
const project = loadManifestsFromDir(dirs.project).manifests;
|
|
37
|
+
return mergeManifests(builtin, user, project).filter((m) => m.kind === 'queue');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* findQueueProvider(backend, opts)
|
|
42
|
+
*
|
|
43
|
+
* Returns the kind:'queue' manifest whose id matches `backend`, or null when no
|
|
44
|
+
* such provider is registered. Callers use the null result to distinguish an
|
|
45
|
+
* unregistered backend (a hard, explicit error) from a resolvable one.
|
|
46
|
+
*/
|
|
47
|
+
export function findQueueProvider(backend, { rootDir = process.cwd(), env = process.env } = {}) {
|
|
48
|
+
if (!backend) return null;
|
|
49
|
+
return listQueueProviders({ rootDir, env }).find((m) => m.id === backend) || null;
|
|
50
|
+
}
|
package/lib/intake/queue.mjs
CHANGED
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The IntakeQueue contract carries the R&D intake packets from the embed
|
|
5
5
|
* daemon (writer) to the agent in the user's editor (reader/processor).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Substrate owns the queue CONTRACT; git-queue is the zero-dependency default
|
|
7
|
+
* kind:'queue' provider and Postgres is an optional kind:'queue' provider
|
|
8
|
+
* selected through the extension registry (construct-9oi4.7.11). Solo mode
|
|
9
|
+
* resolves to the filesystem provider under `.cx/intake/`; team and enterprise
|
|
10
|
+
* default to the Postgres provider. Callers never branch on backend — they ask for a
|
|
11
|
+
* queue via `createIntakeQueue(rootDir, env)` and use the same six methods.
|
|
12
|
+
* The returned instance also carries `.tenantId` (ADR-0057/A7: resolved once
|
|
13
|
+
* from CONSTRUCT_TENANT_ID/config, 'local' default outside enterprise mode)
|
|
14
|
+
* so a caller can stamp it onto entries without re-resolving it.
|
|
10
15
|
*
|
|
11
16
|
* Queue entry shape:
|
|
12
17
|
* {
|
|
13
18
|
* id, createdAt, status: 'pending'|'processed'|'skipped',
|
|
19
|
+
* tenantId?,
|
|
14
20
|
* intake: { sourcePath, outputPath, characters, knowledgeSubdir },
|
|
15
21
|
* triage: { intakeType, rdStage, primaryOwner, recommendedChain,
|
|
16
22
|
* recommendedAction, risk, requiresApproval, confidence,
|
|
@@ -37,19 +43,60 @@ import path from 'node:path';
|
|
|
37
43
|
|
|
38
44
|
import { FilesystemIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir } from './filesystem-queue.mjs';
|
|
39
45
|
import { GitIntakeQueue } from './git-queue.mjs';
|
|
40
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
getDeploymentMode,
|
|
48
|
+
requireTeamCapabilityOrDegrade,
|
|
49
|
+
} from '../deployment-mode.mjs';
|
|
50
|
+
import { createSqlClient } from '../storage/backend.mjs';
|
|
51
|
+
import { findQueueProvider, BUILTIN_QUEUE_BACKENDS } from './queue-registry.mjs';
|
|
52
|
+
import { resolveTenantContext, TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
|
|
53
|
+
import { PostgresIntakeQueue } from '../queue/pg-queue.mjs';
|
|
41
54
|
|
|
42
55
|
export { FilesystemIntakeQueue, GitIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir };
|
|
43
56
|
|
|
44
57
|
export const INTAKE_QUEUE_BACKEND_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_BACKEND';
|
|
45
58
|
export const INTAKE_PROJECT_ENV_KEY = 'CONSTRUCT_PROJECT_NAME';
|
|
46
59
|
export const INTAKE_TENANT_ENV_KEY = 'CONSTRUCT_TENANT_ID';
|
|
60
|
+
export const INTAKE_QUEUE_NAME_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_NAME';
|
|
61
|
+
export const INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY = 'CONSTRUCT_QUEUE_LEASE_SECONDS';
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
// Re-exported so a caller can resolve the same tenant value the factory
|
|
64
|
+
// stamps on the queue without importing lib/tenant/context.mjs directly.
|
|
65
|
+
export { TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
|
|
66
|
+
|
|
67
|
+
// Thrown when a caller names a queue backend that is neither a builtin provider
|
|
68
|
+
// (git/filesystem) nor a registered kind:'queue' extension. The reframing
|
|
69
|
+
// removed the silent postgres->git alias, so an unknown backend is a hard,
|
|
70
|
+
// explicit error rather than a swallowed downgrade.
|
|
71
|
+
|
|
72
|
+
export class UnregisteredQueueProviderError extends Error {
|
|
73
|
+
constructor(backend) {
|
|
74
|
+
super(
|
|
75
|
+
`Queue backend '${backend}' has no registered kind:'queue' provider. ` +
|
|
76
|
+
`Builtin providers: ${BUILTIN_QUEUE_BACKENDS.join(', ')}. ` +
|
|
77
|
+
`Install a kind:'queue' extension (e.g. a Postgres provider) to select it.`,
|
|
78
|
+
);
|
|
79
|
+
this.name = 'UnregisteredQueueProviderError';
|
|
80
|
+
this.backend = backend;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Selection resolves through the registry by kind. An explicit backend override
|
|
85
|
+
// wins; otherwise deployment mode picks the default provider (solo→filesystem,
|
|
86
|
+
// team/enterprise→postgres). A non-builtin backend must resolve to a registered
|
|
87
|
+
// kind:'queue' provider or the call fails explicitly.
|
|
88
|
+
|
|
89
|
+
function resolveBackend(env, rootDir) {
|
|
49
90
|
const override = env?.[INTAKE_QUEUE_BACKEND_ENV_KEY];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
const requested = override && String(override).trim()
|
|
92
|
+
? String(override).trim()
|
|
93
|
+
: (getDeploymentMode(env, { cwd: rootDir }) === 'solo' ? 'filesystem' : 'postgres');
|
|
94
|
+
|
|
95
|
+
if (BUILTIN_QUEUE_BACKENDS.includes(requested)) return { backend: requested, provider: null };
|
|
96
|
+
|
|
97
|
+
const provider = findQueueProvider(requested, { rootDir, env });
|
|
98
|
+
if (!provider) throw new UnregisteredQueueProviderError(requested);
|
|
99
|
+
return { backend: requested, provider };
|
|
53
100
|
}
|
|
54
101
|
|
|
55
102
|
function resolveProject(rootDir, env) {
|
|
@@ -58,20 +105,89 @@ function resolveProject(rootDir, env) {
|
|
|
58
105
|
return path.basename(path.resolve(rootDir)).trim() || 'construct';
|
|
59
106
|
}
|
|
60
107
|
|
|
108
|
+
// Resolves the tenant context (config + env, validated) for queue scoping.
|
|
109
|
+
// Reads CONSTRUCT_TENANT_ID directly here — the factory is a plumbing
|
|
110
|
+
// boundary, so it does not load construct.config.json itself and passes no
|
|
111
|
+
// config, which mirrors solo/team's config-optional path. Enterprise callers
|
|
112
|
+
// that need config-sourced tenantId should resolve it once at startup via
|
|
113
|
+
// lib/deployment-mode.mjs#validateTenantAtStartup and pass the result through
|
|
114
|
+
// opts.tenantId instead of relying on this fallback.
|
|
115
|
+
|
|
116
|
+
export function resolveIntakeTenantId(env = process.env, mode = getDeploymentMode(env)) {
|
|
117
|
+
return resolveTenantContext({ env, mode }).tenantId;
|
|
118
|
+
}
|
|
119
|
+
|
|
61
120
|
/**
|
|
62
121
|
* Create an IntakeQueue instance for the given project root.
|
|
63
122
|
* Backend selection: CONSTRUCT_INTAKE_QUEUE_BACKEND override wins; otherwise
|
|
64
|
-
* solo mode → filesystem, team/enterprise →
|
|
65
|
-
*
|
|
123
|
+
* solo mode → filesystem, team/enterprise → postgres. A backend that is neither builtin nor a registered
|
|
124
|
+
* kind:'queue' provider throws UnregisteredQueueProviderError — there is no
|
|
125
|
+
* silent postgres->git alias.
|
|
126
|
+
*
|
|
127
|
+
* The returned instance carries a `.tenantId` (opts.tenantId wins, otherwise
|
|
128
|
+
* resolved from CONSTRUCT_TENANT_ID/config via resolveIntakeTenantId) so a
|
|
129
|
+
* caller building a queue entry can stamp tenantId without re-deriving it —
|
|
130
|
+
* the queue classes themselves stay tenant-agnostic (LMCP-H1; tenant-scoped
|
|
131
|
+
* storage is LMCP-H4, not this bead).
|
|
66
132
|
*/
|
|
67
133
|
export function createIntakeQueue(rootDir, env = process.env, opts = {}) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
134
|
+
const resolved = opts.backend
|
|
135
|
+
? (BUILTIN_QUEUE_BACKENDS.includes(opts.backend)
|
|
136
|
+
? { backend: opts.backend, provider: null }
|
|
137
|
+
: (findQueueProvider(opts.backend, { rootDir, env })
|
|
138
|
+
? { backend: opts.backend, provider: findQueueProvider(opts.backend, { rootDir, env }) }
|
|
139
|
+
: (() => { throw new UnregisteredQueueProviderError(opts.backend); })()))
|
|
140
|
+
: resolveBackend(env, rootDir);
|
|
72
141
|
|
|
142
|
+
const tenantId = opts.tenantId ?? resolveIntakeTenantId(env, getDeploymentMode(env, { cwd: rootDir }));
|
|
143
|
+
|
|
144
|
+
if (resolved.backend === 'filesystem') {
|
|
145
|
+
const queue = new FilesystemIntakeQueue(rootDir);
|
|
146
|
+
queue.tenantId = tenantId;
|
|
147
|
+
return queue;
|
|
148
|
+
}
|
|
149
|
+
if (resolved.backend === 'git') {
|
|
73
150
|
const project = opts.project ?? resolveProject(rootDir, env);
|
|
74
|
-
|
|
151
|
+
const queue = new GitIntakeQueue({ project, rootDir });
|
|
152
|
+
queue.tenantId = tenantId;
|
|
153
|
+
return queue;
|
|
75
154
|
}
|
|
76
|
-
|
|
155
|
+
|
|
156
|
+
if (resolved.backend === 'postgres' || resolved.provider?.id === 'postgres') {
|
|
157
|
+
const sql = opts.sql ?? createSqlClient(env);
|
|
158
|
+
if (!sql) {
|
|
159
|
+
const mode = getDeploymentMode(env, { cwd: rootDir });
|
|
160
|
+
if (mode === 'solo') {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Queue backend '${resolved.backend}' requires DATABASE_URL or CONSTRUCT_DATABASE_URL; ` +
|
|
163
|
+
`no Postgres SQL client is configured.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
requireTeamCapabilityOrDegrade('postgres-queue', false, env, { cwd: rootDir });
|
|
167
|
+
const queue = new GitIntakeQueue({ project: opts.project ?? resolveProject(rootDir, env), rootDir });
|
|
168
|
+
queue.tenantId = tenantId;
|
|
169
|
+
queue.backend = 'git';
|
|
170
|
+
queue.requestedBackend = resolved.backend;
|
|
171
|
+
queue.degraded = true;
|
|
172
|
+
queue.degradedReason = 'postgres-unavailable';
|
|
173
|
+
return queue;
|
|
174
|
+
}
|
|
175
|
+
requireTeamCapabilityOrDegrade('postgres-queue', true, env, { cwd: rootDir });
|
|
176
|
+
return new PostgresIntakeQueue({
|
|
177
|
+
sql,
|
|
178
|
+
project: opts.project ?? resolveProject(rootDir, env),
|
|
179
|
+
tenantId,
|
|
180
|
+
queueName: opts.queueName ?? env?.[INTAKE_QUEUE_NAME_ENV_KEY] ?? 'intake',
|
|
181
|
+
leaseSeconds: opts.leaseSeconds ?? env?.[INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// A registered kind:'queue' provider was selected but no in-tree instantiation
|
|
186
|
+
// path exists yet. The selection is honest — it names the resolved provider
|
|
187
|
+
// rather than silently downgrading to git.
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Queue backend '${resolved.backend}' resolves to registered kind:'queue' provider ` +
|
|
191
|
+
`'${resolved.provider?.id}' but no in-tree instantiation path is wired yet.`,
|
|
192
|
+
);
|
|
77
193
|
}
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
import { createIntakeQueue } from './queue.mjs';
|
|
14
14
|
import { formatTriageLine } from './classify.mjs';
|
|
15
15
|
import { isBrokered } from '../mcp/broker.mjs';
|
|
16
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
16
17
|
import { getRebrand } from '../scopes/rebrand.mjs';
|
|
17
18
|
import { homedir as osHomedir } from 'node:os';
|
|
18
19
|
import { readLastTick } from '../oracle/index.mjs';
|
|
19
20
|
import { listPending } from '../oracle/actions.mjs';
|
|
20
21
|
import { readLatestVerdict } from '../oracle/verdicts.mjs';
|
|
21
22
|
import { isVerdictOnlyGap } from '../oracle/policy.mjs';
|
|
23
|
+
import { heartbeatFreshness, formatAge } from '../oracle/heartbeat.mjs';
|
|
22
24
|
|
|
23
25
|
export function buildIntakePrelude({ cwd, env = process.env } = {}) {
|
|
24
26
|
if (!cwd) return '';
|
|
@@ -41,9 +43,9 @@ export function buildIntakePrelude({ cwd, env = process.env } = {}) {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export function buildBrokerStatusLine({ env = process.env } = {}) {
|
|
45
|
-
const active = isBrokered(env);
|
|
46
|
-
const mode = env
|
|
46
|
+
export function buildBrokerStatusLine({ env = process.env, cwd } = {}) {
|
|
47
|
+
const active = isBrokered(env, { cwd });
|
|
48
|
+
const mode = getDeploymentMode(env, { cwd });
|
|
47
49
|
if (!active) {
|
|
48
50
|
return `MCP broker: off · deployment mode: ${mode} (set CONSTRUCT_MCP_BROKER=on to engage in solo mode).`;
|
|
49
51
|
}
|
|
@@ -52,7 +54,7 @@ export function buildBrokerStatusLine({ env = process.env } = {}) {
|
|
|
52
54
|
|
|
53
55
|
export function buildSessionPrelude({ cwd, env = process.env } = {}) {
|
|
54
56
|
const intake = buildIntakePrelude({ cwd, env });
|
|
55
|
-
const broker = buildBrokerStatusLine({ env });
|
|
57
|
+
const broker = buildBrokerStatusLine({ env, cwd });
|
|
56
58
|
const oracle = buildOraclePrelude({ cwd, env });
|
|
57
59
|
if (!intake && !broker && !oracle) return '';
|
|
58
60
|
const parts = [];
|
|
@@ -62,6 +64,26 @@ export function buildSessionPrelude({ cwd, env = process.env } = {}) {
|
|
|
62
64
|
return parts.join('\n\n');
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* lib/oracle/synthesize.mjs renders gap detail text against a 24h lookback
|
|
69
|
+
* window ("in the last 24h"); a verdict older than that window describes a
|
|
70
|
+
* period disjoint from the present, so the same threshold marks it stale.
|
|
71
|
+
* Missing/unparseable `at` resolves to Infinity (stale, not fresh) rather
|
|
72
|
+
* than defaulting to zero age.
|
|
73
|
+
*/
|
|
74
|
+
const VERDICT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
75
|
+
|
|
76
|
+
function verdictFreshness(verdict, now = Date.now()) {
|
|
77
|
+
const at = verdict?.at;
|
|
78
|
+
const parsed = at ? Date.parse(at) : NaN;
|
|
79
|
+
const ageMs = Number.isFinite(parsed) ? now - parsed : Infinity;
|
|
80
|
+
return {
|
|
81
|
+
ageMs,
|
|
82
|
+
stale: !Number.isFinite(ageMs) || ageMs > VERDICT_STALE_MS,
|
|
83
|
+
asOf: Number.isFinite(parsed) ? new Date(parsed).toISOString() : null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
/**
|
|
66
88
|
* Oracle verdict + pending queue for session-start (mirror doctor warnings).
|
|
67
89
|
*/
|
|
@@ -75,7 +97,19 @@ export function buildOraclePrelude({ cwd, env = process.env, homeDir } = {}) {
|
|
|
75
97
|
const pending = listPending(cwd).filter((p) => p.status === 'pending');
|
|
76
98
|
const v = verdict?.verdict ?? last?.verdict ?? 'unknown';
|
|
77
99
|
if (v === 'healthy' && pending.length === 0) return '';
|
|
78
|
-
const
|
|
100
|
+
const source = verdict ?? last;
|
|
101
|
+
const { ageMs, stale, asOf } = verdictFreshness(source);
|
|
102
|
+
const verdictHead = v === 'unknown'
|
|
103
|
+
? `\n## Oracle overseer · verdict: **${v}**`
|
|
104
|
+
: `\n## Oracle overseer · verdict: **${v}** (as of ${asOf ?? 'unknown'}, ${formatAge(ageMs)} ago)`;
|
|
105
|
+
const lines = [verdictHead];
|
|
106
|
+
if (v !== 'unknown' && stale) {
|
|
107
|
+
lines.push(`- STALE: verdict is older than ${formatAge(VERDICT_STALE_MS)}; gap details below describe the verdict date, not now.`);
|
|
108
|
+
}
|
|
109
|
+
const heartbeat = heartbeatFreshness(last);
|
|
110
|
+
if (heartbeat.stale && pending.length > 0) {
|
|
111
|
+
lines.push(`- [high] oracle-producer-stalled: no oracle tick in ${formatAge(heartbeat.ageMs)} while ${pending.length} approval(s) wait — the daemon looks stalled, not idle; run \`construct oracle status\` / \`construct oracle start\`.`);
|
|
112
|
+
}
|
|
79
113
|
const gapSource = verdict?.gaps ?? last?.gaps ?? [];
|
|
80
114
|
const top = gapSource.filter((g) => g.severity === 'high').slice(0, 3);
|
|
81
115
|
for (const g of top) {
|
|
@@ -83,7 +117,7 @@ export function buildOraclePrelude({ cwd, env = process.env, homeDir } = {}) {
|
|
|
83
117
|
const hint = g.id === 'beads-hygiene'
|
|
84
118
|
? 'run `construct beads drift`'
|
|
85
119
|
: g.id === 'workflow-misaligned'
|
|
86
|
-
? 'run `construct workflow
|
|
120
|
+
? 'run `construct workflow new` or `construct init`'
|
|
87
121
|
: 'see `construct oracle gaps`';
|
|
88
122
|
lines.push(`- [${g.severity}] ${g.id} (verdict-only): ${g.detail} — ${hint}`);
|
|
89
123
|
} else {
|
|
@@ -102,7 +136,7 @@ export function buildOraclePrelude({ cwd, env = process.env, homeDir } = {}) {
|
|
|
102
136
|
|
|
103
137
|
export function readOracleDockState({ cwd, env = process.env, homeDir } = {}) {
|
|
104
138
|
if (!cwd || env.CONSTRUCT_ORACLE === 'off' || env.CONSTRUCT_ORACLE === '0') {
|
|
105
|
-
return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '' };
|
|
139
|
+
return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '', asOf: null, stale: false, producerStalled: false, producerAgeMs: null };
|
|
106
140
|
}
|
|
107
141
|
try {
|
|
108
142
|
const home = homeDir ?? osHomedir();
|
|
@@ -113,23 +147,38 @@ export function readOracleDockState({ cwd, env = process.env, homeDir } = {}) {
|
|
|
113
147
|
const gapSource = verdictDoc?.gaps ?? last?.gaps ?? [];
|
|
114
148
|
const topGaps = gapSource.filter((g) => g.severity === 'high').slice(0, 3);
|
|
115
149
|
const visible = verdict !== 'healthy' || pending.length > 0;
|
|
150
|
+
const { stale, asOf } = verdictFreshness(verdictDoc ?? last);
|
|
151
|
+
const heartbeat = heartbeatFreshness(last);
|
|
152
|
+
const producerStalled = heartbeat.stale && pending.length > 0;
|
|
116
153
|
const parts = [`verdict ${verdict}`];
|
|
117
154
|
if (pending.length) parts.push(`${pending.length} pending`);
|
|
155
|
+
if (verdict !== 'unknown' && stale) parts.push('STALE');
|
|
156
|
+
if (producerStalled) parts.push('PRODUCER STALLED');
|
|
118
157
|
return {
|
|
119
158
|
visible,
|
|
120
159
|
verdict,
|
|
121
160
|
pendingCount: pending.length,
|
|
122
161
|
topGaps,
|
|
123
162
|
summary: parts.join(' · '),
|
|
163
|
+
asOf,
|
|
164
|
+
stale: verdict !== 'unknown' && stale,
|
|
165
|
+
producerStalled,
|
|
166
|
+
producerAgeMs: heartbeat.ageMs,
|
|
124
167
|
};
|
|
125
168
|
} catch {
|
|
126
|
-
return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '' };
|
|
169
|
+
return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '', asOf: null, stale: false, producerStalled: false, producerAgeMs: null };
|
|
127
170
|
}
|
|
128
171
|
}
|
|
129
172
|
|
|
130
173
|
export function formatOracleDockDetail(state) {
|
|
131
174
|
if (!state?.visible) return 'Oracle: healthy — no pending approvals.';
|
|
132
175
|
const lines = [`Oracle overseer · ${state.summary}`];
|
|
176
|
+
if (state.stale) {
|
|
177
|
+
lines.push(` STALE: verdict as of ${state.asOf ?? 'unknown'}; gap details describe the verdict date, not now.`);
|
|
178
|
+
}
|
|
179
|
+
if (state.producerStalled) {
|
|
180
|
+
lines.push(` PRODUCER STALLED: no oracle tick in ${formatAge(state.producerAgeMs)} while ${state.pendingCount} approval(s) wait — the daemon may be down.`);
|
|
181
|
+
}
|
|
133
182
|
for (const g of state.topGaps || []) {
|
|
134
183
|
if (isVerdictOnlyGap(g)) {
|
|
135
184
|
lines.push(` [${g.severity}] ${g.id} (verdict-only): ${g.detail}`);
|
|
@@ -19,7 +19,7 @@ import { join } from 'node:path';
|
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { spawnSync } from 'node:child_process';
|
|
21
21
|
import { configDir } from '../config/xdg.mjs';
|
|
22
|
-
import { resolveOpRef } from '../providers/secret-resolver.mjs';
|
|
22
|
+
import { resolveOpRef, hasSecret, hasAnySecret, peekRawCredential } from '../providers/secret-resolver.mjs';
|
|
23
23
|
|
|
24
24
|
// ── Multi-method auth helpers ────────────────────────────────────────────
|
|
25
25
|
|
|
@@ -588,8 +588,13 @@ export function tagIntakeWithExternalRef(rootDir, intakeId, system, externalUrl,
|
|
|
588
588
|
}
|
|
589
589
|
|
|
590
590
|
/**
|
|
591
|
-
*
|
|
592
|
-
*
|
|
591
|
+
* Credential resolver for the actual publish paths (createGitHubIssue,
|
|
592
|
+
* createJiraTicket, publishArtifactToConfluence) that need a real, materialized
|
|
593
|
+
* value to make an API call. Checks env → .env → config.env → shell rc,
|
|
594
|
+
* resolving a 1Password op:// reference through resolveOpRef when the rc tier
|
|
595
|
+
* carries one. Not for status/detection — detectIntegrationConfig below uses
|
|
596
|
+
* hasSecret/hasAnySecret/peekRawCredential instead, which never resolve op:// or
|
|
597
|
+
* source a shell rc file (ADR-0049 §2: presence-first in short-lived paths).
|
|
593
598
|
*/
|
|
594
599
|
function resolveCredentialSync(varName, homeDir) {
|
|
595
600
|
if (process.env[varName]) return process.env[varName];
|
|
@@ -640,126 +645,71 @@ function resolveCredentialSync(varName, homeDir) {
|
|
|
640
645
|
// ── Configuration detection ──────────────────────────────────────────────
|
|
641
646
|
|
|
642
647
|
/**
|
|
643
|
-
* Check which integrations are configured.
|
|
644
|
-
*
|
|
645
|
-
*
|
|
648
|
+
* Check which integrations are configured. A status/detection surface must
|
|
649
|
+
* never materialize a credential (ADR-0049 §2: "never to merely list or
|
|
650
|
+
* check") — this reports presence by shape only: env var set, a project/config/
|
|
651
|
+
* home .env entry exists, or a shell rc `export VAR=...` line matches. It never
|
|
652
|
+
* calls resolveOpRef and never sources a shell rc file (contrast
|
|
653
|
+
* resolveCredentialSync above, used only when an actual API call needs the
|
|
654
|
+
* real value). `gh auth status` / `gh repo view` are read-only CLI queries,
|
|
655
|
+
* not secret materialization — gh manages its own token internally and never
|
|
656
|
+
* hands Construct a plaintext value here.
|
|
646
657
|
*
|
|
647
658
|
* @param {object} [opts]
|
|
648
659
|
* @param {string} [opts.homeDir]
|
|
649
|
-
* @returns {{ github: boolean, jira: boolean, confluence: boolean,
|
|
660
|
+
* @returns {{ github: boolean, jira: boolean, confluence: boolean, githubMethod: string|null, githubRepo: string|null, jiraHost: string|null, confluenceSpace: string|null }}
|
|
650
661
|
*/
|
|
651
662
|
export function detectIntegrationConfig({ homeDir } = {}) {
|
|
652
663
|
const hd = homeDir || homedir();
|
|
653
664
|
|
|
654
|
-
// GitHub
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
// Jira
|
|
658
|
-
const jiraHost = resolveCredentialSync('JIRA_HOST', hd);
|
|
659
|
-
const jiraUser = resolveCredentialSync('JIRA_USER', hd);
|
|
660
|
-
const jiraToken = resolveCredentialSync('JIRA_API_TOKEN', hd);
|
|
661
|
-
const jiraProject = resolveCredentialSync('JIRA_PROJECT', hd);
|
|
662
|
-
|
|
663
|
-
// Confluence
|
|
664
|
-
const confHost = resolveCredentialSync('CONFLUENCE_HOST', hd);
|
|
665
|
-
const confUser = resolveCredentialSync('CONFLUENCE_USER', hd);
|
|
666
|
-
const confToken = resolveCredentialSync('CONFLUENCE_API_TOKEN', hd);
|
|
667
|
-
const confSpace = resolveCredentialSync('CONFLUENCE_SPACE', hd);
|
|
668
|
-
|
|
669
|
-
return {
|
|
670
|
-
github: ghAuth.method === 'gh' || Boolean(ghAuth.token && ghAuth.repo),
|
|
671
|
-
githubMethod: ghAuth.method || null,
|
|
672
|
-
githubRepo: ghAuth.repo || null,
|
|
673
|
-
jira: Boolean(jiraHost && jiraUser && jiraToken && jiraProject),
|
|
674
|
-
jiraHost: jiraHost || null,
|
|
675
|
-
confluence: Boolean(confHost && confUser && confToken && confSpace),
|
|
676
|
-
confluenceSpace: confSpace || null,
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Resolve a credential from env, .env files, config.env, or shell rc files.
|
|
682
|
-
* Mirrors the approach in lib/health-check.mjs.
|
|
683
|
-
*/
|
|
684
|
-
function resolveCredential(varName, homeDir) {
|
|
685
|
-
if (process.env[varName]) return process.env[varName];
|
|
686
|
-
|
|
687
|
-
// Project .env
|
|
665
|
+
// GitHub
|
|
666
|
+
let ghCliAuthenticated = false;
|
|
688
667
|
try {
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
693
|
-
if (m) return m[1].trim();
|
|
694
|
-
}
|
|
695
|
-
} catch { /* skip */ }
|
|
668
|
+
const status = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 5000 });
|
|
669
|
+
ghCliAuthenticated = status.status === 0;
|
|
670
|
+
} catch { /* gh not available */ }
|
|
696
671
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (existsSync(homeEnv)) {
|
|
701
|
-
const content = readFileSync(homeEnv, 'utf8');
|
|
702
|
-
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
703
|
-
if (m) return m[1].trim();
|
|
704
|
-
}
|
|
705
|
-
} catch { /* skip */ }
|
|
672
|
+
const githubTokenPresent = hasAnySecret(['GITHUB_TOKEN', 'GH_TOKEN'], { home: hd });
|
|
673
|
+
let githubRepo = process.env.GITHUB_REPO || peekRawCredential('GITHUB_REPO', { home: hd })?.raw || null;
|
|
674
|
+
let githubMethod = null;
|
|
706
675
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
676
|
+
if (ghCliAuthenticated) {
|
|
677
|
+
githubMethod = 'gh';
|
|
678
|
+
if (!githubRepo) {
|
|
679
|
+
try {
|
|
680
|
+
const remote = spawnSync('gh', ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], { encoding: 'utf8', timeout: 5000 });
|
|
681
|
+
if (remote.status === 0 && remote.stdout?.trim()) githubRepo = remote.stdout.trim();
|
|
682
|
+
} catch { /* gh not available */ }
|
|
714
683
|
}
|
|
715
|
-
}
|
|
684
|
+
} else if (githubTokenPresent) {
|
|
685
|
+
githubMethod = 'env';
|
|
686
|
+
}
|
|
716
687
|
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const directRe = new RegExp(`^\\s*export\\s+${varName}="?([^"'\\$\\(]+)"?$`, 'm');
|
|
726
|
-
const directMatch = content.match(directRe);
|
|
727
|
-
if (directMatch) return directMatch[1].trim();
|
|
728
|
-
|
|
729
|
-
// op:// ref — try 1Password CLI
|
|
730
|
-
const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
|
|
731
|
-
const opMatch = content.match(opRe);
|
|
732
|
-
if (opMatch) {
|
|
733
|
-
// op read failures often mean the rc-file field name doesn't match the
|
|
734
|
-
// actual 1Password item field; surface them under the debug flag.
|
|
735
|
-
try {
|
|
736
|
-
return resolveOpRef(opMatch[1]);
|
|
737
|
-
} catch (err) {
|
|
738
|
-
if (process.env.CONSTRUCT_DEBUG_CREDENTIALS === '1') {
|
|
739
|
-
process.stderr.write(`[credentials] op read failed for ${varName}: ${String(err.message).slice(0, 200)}\n`);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
688
|
+
// Jira
|
|
689
|
+
const jiraConfigured = Boolean(
|
|
690
|
+
hasSecret('JIRA_HOST', { home: hd })
|
|
691
|
+
&& hasSecret('JIRA_USER', { home: hd })
|
|
692
|
+
&& hasSecret('JIRA_API_TOKEN', { home: hd })
|
|
693
|
+
&& hasSecret('JIRA_PROJECT', { home: hd }),
|
|
694
|
+
);
|
|
695
|
+
const jiraHost = peekRawCredential('JIRA_HOST', { home: hd })?.raw || null;
|
|
743
696
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
encoding: 'utf8', timeout: 5000,
|
|
753
|
-
});
|
|
754
|
-
if (shellResult.status === 0 && shellResult.stdout?.trim()) {
|
|
755
|
-
const val = shellResult.stdout.trim().split('\n').pop()?.trim();
|
|
756
|
-
if (val && !val.includes('ERROR') && !val.includes('could not read')) return val;
|
|
757
|
-
}
|
|
758
|
-
} catch { /* shell eval failed */ }
|
|
759
|
-
}
|
|
760
|
-
} catch { /* skip unreadable */ }
|
|
761
|
-
}
|
|
762
|
-
} catch { /* skip */ }
|
|
697
|
+
// Confluence
|
|
698
|
+
const confluenceConfigured = Boolean(
|
|
699
|
+
hasSecret('CONFLUENCE_HOST', { home: hd })
|
|
700
|
+
&& hasSecret('CONFLUENCE_USER', { home: hd })
|
|
701
|
+
&& hasSecret('CONFLUENCE_API_TOKEN', { home: hd })
|
|
702
|
+
&& hasSecret('CONFLUENCE_SPACE', { home: hd }),
|
|
703
|
+
);
|
|
704
|
+
const confluenceSpace = peekRawCredential('CONFLUENCE_SPACE', { home: hd })?.raw || null;
|
|
763
705
|
|
|
764
|
-
return
|
|
706
|
+
return {
|
|
707
|
+
github: ghCliAuthenticated || Boolean(githubTokenPresent && githubRepo),
|
|
708
|
+
githubMethod,
|
|
709
|
+
githubRepo,
|
|
710
|
+
jira: jiraConfigured,
|
|
711
|
+
jiraHost,
|
|
712
|
+
confluence: confluenceConfigured,
|
|
713
|
+
confluenceSpace,
|
|
714
|
+
};
|
|
765
715
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import crypto from 'node:crypto';
|
|
28
28
|
import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig } from './model-router.mjs';
|
|
29
29
|
import { getUserEnvPath } from './env-config.mjs';
|
|
30
|
+
import { extractOpRef, hasSecret, resolveOpRefAsync, resolveSecretAsync } from './providers/secret-resolver.mjs';
|
|
30
31
|
import { logIntentVerification } from './telemetry/intent-verifications.mjs';
|
|
31
32
|
|
|
32
33
|
export const INTENT_VERIFY_TIMEOUT_MS = 3000;
|
|
@@ -66,12 +67,49 @@ export function resetCache() {
|
|
|
66
67
|
CACHE.clear();
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
async function resolveSecretOrNull(varName, { env = process.env, allowAmbient = true } = {}) {
|
|
71
|
+
try {
|
|
72
|
+
return await resolveSecretAsync(varName, { env, allowAmbient });
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function materializeConfiguredKey(rawValue) {
|
|
79
|
+
if (typeof rawValue !== 'string' || rawValue.length === 0) return null;
|
|
80
|
+
const ref = extractOpRef(rawValue);
|
|
81
|
+
try {
|
|
82
|
+
return ref ? await resolveOpRefAsync(ref) : rawValue;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function resolveOpenRouterApiKey() {
|
|
89
|
+
const envKey = await resolveSecretOrNull('OPENROUTER_API_KEY');
|
|
90
|
+
if (envKey) return envKey;
|
|
91
|
+
try {
|
|
92
|
+
return await materializeConfiguredKey(readOpenRouterApiKeyFromOpenCodeConfig());
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function resolveAnthropicApiKey() {
|
|
99
|
+
return resolveSecretOrNull('ANTHROPIC_API_KEY');
|
|
100
|
+
}
|
|
101
|
+
|
|
69
102
|
function isApiKeyConfigured() {
|
|
70
|
-
|
|
71
|
-
|
|
103
|
+
// Presence gate only — hasSecret detects a configured key (including an op://
|
|
104
|
+
// reference) without materializing it, so deciding whether to run the background
|
|
105
|
+
// intent verifier never spawns `op read` or a 1Password biometric prompt on the
|
|
106
|
+
// routing hot path. Materialization happens lazily in defaultModelCaller when a
|
|
107
|
+
// verification call is actually made.
|
|
108
|
+
if (hasSecret('ANTHROPIC_API_KEY')) return true;
|
|
109
|
+
if (hasSecret('OPENROUTER_API_KEY')) return true;
|
|
72
110
|
try {
|
|
73
111
|
if (readOpenRouterApiKeyFromOpenCodeConfig()) return true;
|
|
74
|
-
} catch { /*
|
|
112
|
+
} catch { /* OpenCode provider config is optional */ }
|
|
75
113
|
return false;
|
|
76
114
|
}
|
|
77
115
|
|
|
@@ -115,8 +153,8 @@ async function fetchWithTimeout(url, options, timeoutMs = INTENT_VERIFY_TIMEOUT_
|
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
async function defaultModelCaller({ system, user }) {
|
|
118
|
-
const orKey =
|
|
119
|
-
const anthropicKey =
|
|
156
|
+
const orKey = await resolveOpenRouterApiKey();
|
|
157
|
+
const anthropicKey = await resolveAnthropicApiKey();
|
|
120
158
|
if (!orKey && !anthropicKey) throw new Error('no model api key configured');
|
|
121
159
|
|
|
122
160
|
const envPath = getUserEnvPath();
|