@geraldmaron/construct 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +57 -45
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +273 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +104 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/workflows/manifest-schema.mjs — canonical workflow manifest schema descriptor.
|
|
3
|
+
*
|
|
4
|
+
* Defines the vocabulary for workflow manifests as per ADR-0054 (LMCP-A4).
|
|
5
|
+
* A plain JS descriptor, not JSON Schema — the validator in validate.mjs
|
|
6
|
+
* consumes it directly. The schema is intentionally additive: new optional fields
|
|
7
|
+
* may be added in a minor version bump; new required fields or type removals are
|
|
8
|
+
* breaking and require a COMPAT_VERSION increment.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* All valid workflow type values. These correspond to the execution topologies
|
|
13
|
+
* the orchestration runtime can dispatch. `embed` (ADR-0061, LMCP-P1/P2) is a
|
|
14
|
+
* workflow-manifest specialization carrying an `embed` block rather than a
|
|
15
|
+
* dispatchable topology — it reuses this schema's id/version/tiering rules
|
|
16
|
+
* instead of forking a second manifest system.
|
|
17
|
+
*/
|
|
18
|
+
export const WORKFLOW_TYPES = ['linear', 'routing', 'orchestrator-worker', 'evaluator-loop', 'pipeline', 'embed'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fields every workflow manifest must carry. Absence of any required field is a
|
|
22
|
+
* hard validation error and the manifest is rejected.
|
|
23
|
+
*/
|
|
24
|
+
export const WORKFLOW_REQUIRED_FIELDS = ['id', 'version', 'type', 'defaultApprovalMode'];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fields a workflow manifest may carry. These are not validated for shape — the
|
|
28
|
+
* validator checks only presence of required fields and semantic constraints.
|
|
29
|
+
* Manifests that add fields outside this list are accepted but their extra
|
|
30
|
+
* fields are ignored by core consumers.
|
|
31
|
+
*
|
|
32
|
+
* `intakeType` (LMCP-D3) lets a pack or project manifest claim a triage
|
|
33
|
+
* classifier label without editing the hardcoded INTAKE_TO_WORKFLOW table in
|
|
34
|
+
* lib/embedded-contract/workflow-defs.mjs — the shim rebuilds that table from
|
|
35
|
+
* every loaded manifest's `intakeType` field, project/pack entries applied
|
|
36
|
+
* after builtins so a contributed manifest can add or remap a label.
|
|
37
|
+
*/
|
|
38
|
+
export const WORKFLOW_OPTIONAL_FIELDS = [
|
|
39
|
+
'modes', 'surfaces', 'inputSchema', 'outputSchema',
|
|
40
|
+
'roleChain', 'packRequirements', 'providerRequirements',
|
|
41
|
+
'toolGrants', 'policyGates', 'durableStateModel',
|
|
42
|
+
'telemetryEvents', 'tests', 'docs', 'examples',
|
|
43
|
+
'degradation', 'owner', 'compatVersion', 'description', 'tier',
|
|
44
|
+
'intakeType',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Current schema version. A manifest with compatVersion > WORKFLOW_COMPAT_VERSION
|
|
49
|
+
* was written for a newer schema than this runtime understands — load is refused
|
|
50
|
+
* to prevent silent misinterpretation. Forward compatibility is not guaranteed.
|
|
51
|
+
*/
|
|
52
|
+
export const WORKFLOW_COMPAT_VERSION = 1;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* All valid approval mode values. These govern whether a workflow run proceeds
|
|
56
|
+
* without human intervention or requires explicit approval at one or more gates.
|
|
57
|
+
*/
|
|
58
|
+
export const APPROVAL_MODES = ['proposal-only', 'requires-human-approval', 'allow-durable-write'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* All valid durable state model values. These describe how the workflow runtime
|
|
62
|
+
* persists intermediate state across invocations for resumability and audit.
|
|
63
|
+
*/
|
|
64
|
+
export const DURABLE_STATE_MODELS = ['none', 'git-queue', 'in-process'];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* All valid model tier values. These hint at the minimum model capability the
|
|
68
|
+
* orchestrator should assign to the workflow, from fastest/cheapest to most
|
|
69
|
+
* capable/expensive.
|
|
70
|
+
*/
|
|
71
|
+
export const TIERS = ['fast', 'standard', 'reasoning'];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/workflows/surface-parity.mjs — workflow manifest surface-parity check (LMCP-D4).
|
|
3
|
+
*
|
|
4
|
+
* Compares each workflow manifest's declared `surfaces` field against its
|
|
5
|
+
* ACTUAL registration, derived from the real dispatch code rather than
|
|
6
|
+
* inferred:
|
|
7
|
+
*
|
|
8
|
+
* - lib/embedded-contract/workflow-invoke.mjs exports one `invokeWorkflow`
|
|
9
|
+
* core that CLI (`construct workflow invoke --workflow-type`, bin/construct
|
|
10
|
+
* cmdWorkflow), MCP (`workflow_invoke`, lib/mcp/tools/embedded-contract.mjs),
|
|
11
|
+
* and SDK (`invokeWorkflow`, lib/embedded-contract/index.mjs) all wrap
|
|
12
|
+
* unchanged (lib/embedded-contract/envelope.mjs even documents the three
|
|
13
|
+
* surfaces as "structurally identical"). That core resolves a workflow id
|
|
14
|
+
* through `getWorkflowDef` (lib/embedded-contract/workflow-defs.mjs),
|
|
15
|
+
* whose DEFS table is generated from every loaded manifest with
|
|
16
|
+
* `type !== 'embed'`.
|
|
17
|
+
* - A manifest with `type !== 'embed'` is therefore registered on cli, mcp,
|
|
18
|
+
* AND sdk simultaneously — there is no dispatch path that can register a
|
|
19
|
+
* workflow id on one of the three without the other two, so per-manifest
|
|
20
|
+
* divergence between them is structurally impossible today. sdk carries
|
|
21
|
+
* no separate opt-in and is reported as an info line, never a checkable
|
|
22
|
+
* declared/actual pair — flagging it as a per-manifest mismatch would
|
|
23
|
+
* fail every manifest for a fact no author can change.
|
|
24
|
+
* - A manifest with `type === 'embed'` (ADR-0061) is scheduled by the embed
|
|
25
|
+
* daemon on a cadence; it is never reached through workflow_invoke /
|
|
26
|
+
* `construct workflow invoke` / SDK invokeWorkflow at all, so its actual
|
|
27
|
+
* registration is the empty set on cli and mcp.
|
|
28
|
+
*
|
|
29
|
+
* checkSurfaceParity() flags two failure shapes as errors:
|
|
30
|
+
* 1. A manifest declares a surface ('cli' or 'mcp') it is not actually
|
|
31
|
+
* registered on (an embed manifest declaring 'cli'/'mcp', or a non-embed
|
|
32
|
+
* manifest whose id is absent from WORKFLOW_TYPES because it failed to
|
|
33
|
+
* load into workflow-defs.mjs).
|
|
34
|
+
* 2. A non-embed manifest omits a surface ('cli' or 'mcp') it IS actually
|
|
35
|
+
* registered on.
|
|
36
|
+
* Both shapes are suppressed, with a passing info line instead, when the
|
|
37
|
+
* manifest carries a `surfaceExceptions` map with a non-empty `reason` string
|
|
38
|
+
* for that surface — `surfaceExceptions` is an optional, unvalidated field
|
|
39
|
+
* (manifests load with strict:false in this caller, so unknown fields pass
|
|
40
|
+
* through) rather than a schema addition, keeping this rule additive.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { WORKFLOW_TYPES } from '../embedded-contract/workflow-defs.mjs';
|
|
44
|
+
|
|
45
|
+
/** The two surfaces a manifest can meaningfully declare/omit per-workflow. */
|
|
46
|
+
export const DECLARABLE_SURFACES = ['cli', 'mcp'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Actual registration for a manifest, derived from workflow-defs.mjs's DEFS
|
|
50
|
+
* table (built from loadAllWorkflows() — the same loader this check itself
|
|
51
|
+
* consumes elsewhere) rather than re-implemented here.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} manifest
|
|
54
|
+
* @returns {string[]} subset of DECLARABLE_SURFACES the manifest is actually
|
|
55
|
+
* dispatchable on; sdk is always additionally true for non-embed manifests
|
|
56
|
+
* but is not part of this declarable set (see module header).
|
|
57
|
+
*/
|
|
58
|
+
export function actualSurfaces(manifest) {
|
|
59
|
+
if (!manifest || manifest.type === 'embed') return [];
|
|
60
|
+
return WORKFLOW_TYPES.includes(manifest.id) ? [...DECLARABLE_SURFACES] : [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Look up a manifest's declared exception reason for a surface, if any.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} manifest
|
|
67
|
+
* @param {string} surface
|
|
68
|
+
* @returns {string|null} the reason string, or null when no valid exception is declared
|
|
69
|
+
*/
|
|
70
|
+
function exceptionReason(manifest, surface) {
|
|
71
|
+
const exceptions = manifest?.surfaceExceptions;
|
|
72
|
+
if (!exceptions || typeof exceptions !== 'object') return null;
|
|
73
|
+
const entry = exceptions[surface];
|
|
74
|
+
if (typeof entry === 'string' && entry.trim()) return entry;
|
|
75
|
+
if (entry && typeof entry === 'object' && typeof entry.reason === 'string' && entry.reason.trim()) return entry.reason;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* checkSurfaceParity(manifests)
|
|
81
|
+
*
|
|
82
|
+
* Runs the surface-parity check over an already-loaded manifest list (each
|
|
83
|
+
* carrying `_filePath` and `id`, per lib/workflows/loader.mjs). Never throws.
|
|
84
|
+
*
|
|
85
|
+
* @param {object[]} manifests
|
|
86
|
+
* @returns {{ errors: string[], infos: string[] }}
|
|
87
|
+
*/
|
|
88
|
+
export function checkSurfaceParity(manifests = []) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
const infos = [];
|
|
91
|
+
|
|
92
|
+
for (const manifest of manifests) {
|
|
93
|
+
const label = manifest._filePath || manifest.id || '(unknown manifest)';
|
|
94
|
+
const declared = new Set(Array.isArray(manifest.surfaces) ? manifest.surfaces : []);
|
|
95
|
+
const actual = new Set(actualSurfaces(manifest));
|
|
96
|
+
|
|
97
|
+
for (const surface of DECLARABLE_SURFACES) {
|
|
98
|
+
const isDeclared = declared.has(surface);
|
|
99
|
+
const isActual = actual.has(surface);
|
|
100
|
+
if (isDeclared === isActual) continue;
|
|
101
|
+
|
|
102
|
+
const reason = exceptionReason(manifest, surface);
|
|
103
|
+
if (reason) {
|
|
104
|
+
const shape = isDeclared ? 'declared but not registered' : 'registered but not declared';
|
|
105
|
+
infos.push(`${label}: workflow '${manifest.id}' surface '${surface}' is ${shape} — declared exception: ${reason}`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isDeclared && !isActual) {
|
|
110
|
+
errors.push(`${label}: workflow '${manifest.id}' declares surface '${surface}' but is not actually registered there (no reason given — add a 'surfaceExceptions.${surface}' reason or remove the declaration)`);
|
|
111
|
+
} else {
|
|
112
|
+
errors.push(`${label}: workflow '${manifest.id}' is registered on surface '${surface}' but does not declare it in 'surfaces' (no reason given — add '${surface}' to 'surfaces' or a 'surfaceExceptions.${surface}' reason)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { errors, infos };
|
|
118
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/workflows/validate.mjs — workflow manifest validator.
|
|
3
|
+
*
|
|
4
|
+
* Validates a plain-JS manifest object against the canonical schema defined in
|
|
5
|
+
* manifest-schema.mjs. All errors are returned in a structured result; this
|
|
6
|
+
* function never throws. Error messages are single-line, actionable, and
|
|
7
|
+
* prefixed with the filePath (when provided) so consumers can surface them
|
|
8
|
+
* directly to users.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
WORKFLOW_TYPES, WORKFLOW_REQUIRED_FIELDS, WORKFLOW_OPTIONAL_FIELDS,
|
|
13
|
+
WORKFLOW_COMPAT_VERSION, APPROVAL_MODES,
|
|
14
|
+
} from './manifest-schema.mjs';
|
|
15
|
+
|
|
16
|
+
/** Basic semver pattern: MAJOR.MINOR.PATCH with optional pre-release/build metadata. */
|
|
17
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+].+)?$/;
|
|
18
|
+
|
|
19
|
+
/** Valid id characters: lowercase alphanumeric, hyphens, dots, forward slashes. */
|
|
20
|
+
const ID_RE = /^[a-z0-9\-./]+$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* validateWorkflowManifest(manifest, { filePath, strict } = {})
|
|
24
|
+
*
|
|
25
|
+
* Returns { valid: true } on success, or { valid: false, errors: string[] }
|
|
26
|
+
* on failure. Every error string is a self-contained, single-line description
|
|
27
|
+
* of the problem. The filePath prefix is included in every error so callers
|
|
28
|
+
* can surface messages without additional decoration.
|
|
29
|
+
*
|
|
30
|
+
* Checks performed (in order):
|
|
31
|
+
* 1. Manifest is a non-null object.
|
|
32
|
+
* 2. Required fields are present.
|
|
33
|
+
* 3. `type` is one of the canonical WORKFLOW_TYPES.
|
|
34
|
+
* 4. `version` matches the semver pattern.
|
|
35
|
+
* 5. `id` is a non-empty string matching [a-z0-9-./]+.
|
|
36
|
+
* 6. `compatVersion` (if present) does not exceed WORKFLOW_COMPAT_VERSION.
|
|
37
|
+
* 7. `defaultApprovalMode` is one of APPROVAL_MODES.
|
|
38
|
+
*
|
|
39
|
+
* When strict is true, unknown fields (outside REQUIRED + OPTIONAL) are rejected.
|
|
40
|
+
*
|
|
41
|
+
* @param {unknown} manifest - The parsed manifest object to validate.
|
|
42
|
+
* @param {{ filePath?: string, strict?: boolean }} [opts]
|
|
43
|
+
* @returns {{ valid: true } | { valid: false, errors: string[] }}
|
|
44
|
+
*/
|
|
45
|
+
export function validateWorkflowManifest(manifest, { filePath, strict = false } = {}) {
|
|
46
|
+
const prefix = filePath ? `${filePath}: ` : '';
|
|
47
|
+
const errors = [];
|
|
48
|
+
|
|
49
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
50
|
+
return { valid: false, errors: [`${prefix}manifest must be a JSON object`] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Required fields
|
|
54
|
+
for (const field of WORKFLOW_REQUIRED_FIELDS) {
|
|
55
|
+
if (!(field in manifest) || manifest[field] === undefined || manifest[field] === null) {
|
|
56
|
+
errors.push(`${prefix}missing required field: ${field}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// type check (only if type is present — otherwise already flagged above)
|
|
61
|
+
if ('type' in manifest && manifest.type !== undefined && manifest.type !== null) {
|
|
62
|
+
if (!WORKFLOW_TYPES.includes(manifest.type)) {
|
|
63
|
+
errors.push(
|
|
64
|
+
`${prefix}unknown type '${manifest.type}'; expected one of: ${WORKFLOW_TYPES.join(', ')}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// version semver check
|
|
70
|
+
if ('version' in manifest && manifest.version !== undefined && manifest.version !== null) {
|
|
71
|
+
if (typeof manifest.version !== 'string' || !SEMVER_RE.test(manifest.version)) {
|
|
72
|
+
errors.push(`${prefix}version must be a semver string`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// id format check
|
|
77
|
+
if ('id' in manifest && manifest.id !== undefined && manifest.id !== null) {
|
|
78
|
+
if (typeof manifest.id !== 'string' || manifest.id.length === 0) {
|
|
79
|
+
errors.push(`${prefix}id must be a non-empty string`);
|
|
80
|
+
} else if (!ID_RE.test(manifest.id)) {
|
|
81
|
+
errors.push(`${prefix}id must match [a-z0-9-./]+ (got '${manifest.id}')`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Forward-compat guard
|
|
86
|
+
if (
|
|
87
|
+
manifest.compatVersion !== undefined &&
|
|
88
|
+
manifest.compatVersion !== null
|
|
89
|
+
) {
|
|
90
|
+
if (typeof manifest.compatVersion !== 'number' || !Number.isInteger(manifest.compatVersion)) {
|
|
91
|
+
errors.push(`${prefix}compatVersion must be an integer`);
|
|
92
|
+
} else if (manifest.compatVersion > WORKFLOW_COMPAT_VERSION) {
|
|
93
|
+
errors.push(
|
|
94
|
+
`${prefix}compatVersion ${manifest.compatVersion} exceeds supported version ${WORKFLOW_COMPAT_VERSION}; upgrade Construct to use this manifest`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// defaultApprovalMode check
|
|
100
|
+
if (
|
|
101
|
+
'defaultApprovalMode' in manifest &&
|
|
102
|
+
manifest.defaultApprovalMode !== undefined &&
|
|
103
|
+
manifest.defaultApprovalMode !== null
|
|
104
|
+
) {
|
|
105
|
+
if (!APPROVAL_MODES.includes(manifest.defaultApprovalMode)) {
|
|
106
|
+
errors.push(
|
|
107
|
+
`${prefix}defaultApprovalMode must be one of: ${APPROVAL_MODES.join(', ')} (got '${manifest.defaultApprovalMode}')`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Strict-mode hardening: reject unknown fields
|
|
113
|
+
if (strict) {
|
|
114
|
+
const knownFields = new Set([...WORKFLOW_REQUIRED_FIELDS, ...WORKFLOW_OPTIONAL_FIELDS]);
|
|
115
|
+
for (const key of Object.keys(manifest)) {
|
|
116
|
+
if (key.startsWith('_')) continue;
|
|
117
|
+
if (!knownFields.has(key)) {
|
|
118
|
+
errors.push(`${prefix}unknown field '${key}' in strict mode`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (errors.length > 0) {
|
|
124
|
+
return { valid: false, errors };
|
|
125
|
+
}
|
|
126
|
+
return { valid: true };
|
|
127
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/writes/control-plane.mjs — the only entry point that turns a queued
|
|
3
|
+
* writeIntent into an executed write (LMCP-J6).
|
|
4
|
+
*
|
|
5
|
+
* A specialist (in-process, embed-invoked, or MCP-invoked) may only ever
|
|
6
|
+
* *recommend* a write: it produces a writeIntent (lib/writes/write-intent.mjs)
|
|
7
|
+
* and hands it to an ApprovalQueue (lib/embed/approval-queue.mjs, the F5/I2
|
|
8
|
+
* durable queue). Nothing else in this file, nor anywhere outside it, is
|
|
9
|
+
* permitted to resolve a governed-write adapter and call
|
|
10
|
+
* lib/writes/envelope.mjs's writeWithEnvelope() for a queue-sourced intent —
|
|
11
|
+
* that resolution + call happens exclusively in executeApprovedWriteIntent()
|
|
12
|
+
* below, gated on the queue record's own state.
|
|
13
|
+
*
|
|
14
|
+
* Concretely, the state machine this module enforces:
|
|
15
|
+
* 1. ApprovalQueue.enqueue() records state 'awaiting_approval' — this
|
|
16
|
+
* module never calls a governed adapter for a record in that state.
|
|
17
|
+
* 2. A human or policy actor calls ApprovalQueue.approve()/deny() out of
|
|
18
|
+
* band (CLI, dashboard, MCP resume) — this module does not decide
|
|
19
|
+
* approval, it only checks the decision already recorded.
|
|
20
|
+
* 3. drainApprovedWriteIntents() scans the queue for state === 'approved'
|
|
21
|
+
* records this module has not yet executed, resolves the matching
|
|
22
|
+
* governed adapter (lib/providers/contract/adapters/*\/governed-write.mjs,
|
|
23
|
+
* built by J3-J5), and calls writeWithEnvelope() exactly once per
|
|
24
|
+
* record — the envelope's own idempotency key + sent-log dedup still
|
|
25
|
+
* apply on top, so a re-drain of an already-sent record is a cache hit,
|
|
26
|
+
* not a second external write.
|
|
27
|
+
*
|
|
28
|
+
* No dependency on lib/embed/daemon.mjs or lib/embed/capability-jobs.mjs —
|
|
29
|
+
* only the ApprovalQueue class shape is required, so a queue instance handed
|
|
30
|
+
* over by the daemon, a CLI command, or a test drains the same way, without
|
|
31
|
+
* reaching into daemon internals.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { writeWithEnvelope } from './envelope.mjs';
|
|
35
|
+
import { WriteSentLog } from './sent-log.mjs';
|
|
36
|
+
import { parseWriteIntentToolName } from './write-intent.mjs';
|
|
37
|
+
import { createGovernedJiraProvider } from '../providers/contract/adapters/jira/governed-write.mjs';
|
|
38
|
+
import { createJiraTransport } from '../providers/contract/adapters/jira/transport.mjs';
|
|
39
|
+
import { createGovernedConfluenceProvider } from '../providers/contract/adapters/confluence/governed-write.mjs';
|
|
40
|
+
import { createConfluenceTransport } from '../providers/contract/adapters/confluence/transport.mjs';
|
|
41
|
+
import { createGovernedGithubProvider } from '../providers/contract/adapters/github/governed-write.mjs';
|
|
42
|
+
import githubAdapter from '../providers/contract/adapters/github/index.mjs';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default adapter factories, one per known governed provider. Lazy per-call
|
|
46
|
+
* construction (not module-load-time) keeps a missing-credential error
|
|
47
|
+
* scoped to the one drain call that needed that provider, matching
|
|
48
|
+
* lib/mcp/tools/provider-write.mjs's resolution strategy.
|
|
49
|
+
*/
|
|
50
|
+
const DEFAULT_ADAPTER_FACTORIES = {
|
|
51
|
+
jira: () => createGovernedJiraProvider({ jiraTransport: createJiraTransport() }),
|
|
52
|
+
confluence: () => createGovernedConfluenceProvider({ confluenceTransport: createConfluenceTransport() }),
|
|
53
|
+
github: () => createGovernedGithubProvider({ ghAdapter: githubAdapter }),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute exactly one approved queue record through the envelope. Throws if
|
|
58
|
+
* the record's state is not 'approved' — this is the structural gate: there
|
|
59
|
+
* is no code path in this module that reaches writeWithEnvelope() without
|
|
60
|
+
* first observing state === 'approved' on the queue record itself.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} record - an ApprovalQueue record (state, toolCall, requestedBy, approvalId)
|
|
63
|
+
* @param {object} [opts]
|
|
64
|
+
* @param {Record<string, () => object>} [opts.adapterFactories] - override adapter resolution (tests)
|
|
65
|
+
* @param {WriteSentLog} [opts.sentLog]
|
|
66
|
+
* @param {string} [opts.rootDir]
|
|
67
|
+
* @returns {Promise<object>} the envelope result ({ status, envelope })
|
|
68
|
+
*/
|
|
69
|
+
export async function executeApprovedWriteIntent(record, opts = {}) {
|
|
70
|
+
if (!record || record.state !== 'approved') {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`executeApprovedWriteIntent: refusing to execute record in state "${record?.state ?? 'unknown'}" — only 'approved' records may reach the envelope`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const parsed = parseWriteIntentToolName(record.toolCall?.tool);
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
throw new Error(`executeApprovedWriteIntent: cannot resolve provider from tool name "${record.toolCall?.tool}"`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const factories = opts.adapterFactories ?? DEFAULT_ADAPTER_FACTORIES;
|
|
82
|
+
const factory = factories[parsed.providerId];
|
|
83
|
+
if (!factory) {
|
|
84
|
+
throw new Error(`executeApprovedWriteIntent: no governed adapter registered for provider "${parsed.providerId}"`);
|
|
85
|
+
}
|
|
86
|
+
const adapter = factory();
|
|
87
|
+
|
|
88
|
+
const sentLog = opts.sentLog ?? new WriteSentLog({ persistPath: WriteSentLog.resolvePersistPath(opts.rootDir ?? process.cwd()) });
|
|
89
|
+
|
|
90
|
+
return writeWithEnvelope({
|
|
91
|
+
provider: adapter,
|
|
92
|
+
config: {},
|
|
93
|
+
payload: { type: parsed.writeKind, ...(record.toolCall?.args ?? {}) },
|
|
94
|
+
dryRun: false,
|
|
95
|
+
sentLog,
|
|
96
|
+
idempotencyKey: record.toolCall?.argsHash,
|
|
97
|
+
requestedBy: record.requestedBy ?? {},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Drain every 'approved' record in an ApprovalQueue through the envelope,
|
|
103
|
+
* marking each executed record so a subsequent drain does not re-execute it
|
|
104
|
+
* (the envelope's sent-log dedup is a second, independent line of defense,
|
|
105
|
+
* not the only one — this in-memory executed-set makes the "exactly one
|
|
106
|
+
* adapter call" property observable per drain call even before sent-log
|
|
107
|
+
* persistence is consulted).
|
|
108
|
+
*
|
|
109
|
+
* Records in any other state ('awaiting_approval', 'denied', 'expired') are
|
|
110
|
+
* left untouched — this is read-only with respect to the queue's approval
|
|
111
|
+
* decision, it only ever appends outcomes for records already decided
|
|
112
|
+
* 'approved' by something outside this module.
|
|
113
|
+
*
|
|
114
|
+
* @param {import('../embed/approval-queue.mjs').ApprovalQueue} approvalQueue
|
|
115
|
+
* @param {object} [opts]
|
|
116
|
+
* @param {Record<string, () => object>} [opts.adapterFactories]
|
|
117
|
+
* @param {WriteSentLog} [opts.sentLog]
|
|
118
|
+
* @param {string} [opts.rootDir]
|
|
119
|
+
* @param {Set<string>} [opts.executedApprovalIds] - approvalIds already drained; mutated in place
|
|
120
|
+
* @returns {Promise<Array<{ approvalId: string, result: object|null, error: string|null }>>}
|
|
121
|
+
*/
|
|
122
|
+
export async function drainApprovedWriteIntents(approvalQueue, opts = {}) {
|
|
123
|
+
const executed = opts.executedApprovalIds ?? new Set();
|
|
124
|
+
const outcomes = [];
|
|
125
|
+
|
|
126
|
+
const approvedRecords = approvalQueue.list('approved');
|
|
127
|
+
for (const record of approvedRecords) {
|
|
128
|
+
if (executed.has(record.approvalId)) continue;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = await executeApprovedWriteIntent(record, opts);
|
|
132
|
+
executed.add(record.approvalId);
|
|
133
|
+
outcomes.push({ approvalId: record.approvalId, result, error: null });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
outcomes.push({ approvalId: record.approvalId, result: null, error: err.message ?? String(err) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return outcomes;
|
|
140
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/writes/envelope.mjs — the single governed path for external writes.
|
|
3
|
+
*
|
|
4
|
+
* writeWithEnvelope() runs every external write through: idempotency key →
|
|
5
|
+
* sent-log dedup → dry-run render → policy/approval gate → rate-limited retry →
|
|
6
|
+
* sent-log append + audit. Specialists never call provider adapters directly;
|
|
7
|
+
* they recommend a write-intent and the control plane executes it here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import { WriteSentLog } from './sent-log.mjs';
|
|
12
|
+
import { ApprovalQueue } from '../embed/approval-queue.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute a governed write through the envelope.
|
|
16
|
+
*
|
|
17
|
+
* Flow:
|
|
18
|
+
* 1. Generate idempotency key (if none provided) from provider + writeType + stable payload hash
|
|
19
|
+
* 2. Check sent-log: if same key already succeeded, return cached result
|
|
20
|
+
* 3. Dry-run mode: return rendered payload without executing
|
|
21
|
+
* 4. Policy gate: if broker provided, run policy check
|
|
22
|
+
* - If approval required, create durable approval record and return awaiting_approval
|
|
23
|
+
* 5. Execute provider.write() with retry
|
|
24
|
+
* 6. Record in sent-log
|
|
25
|
+
* 7. Append audit record
|
|
26
|
+
* 8. Return structured result with linkback
|
|
27
|
+
*
|
|
28
|
+
* @param {object} spec
|
|
29
|
+
* @param {object} spec.provider - Provider instance with write() method
|
|
30
|
+
* @param {object} spec.config - Provider configuration
|
|
31
|
+
* @param {object} spec.payload - Write payload (e.g. { type: 'issue', title, body, ... })
|
|
32
|
+
* @param {string} [spec.idempotencyKey] - Explicit idempotency key (auto-generated if absent)
|
|
33
|
+
* @param {boolean} [spec.dryRun] - If true, return rendered payload without executing
|
|
34
|
+
* @param {object} [spec.broker] - Broker instance for policy gating
|
|
35
|
+
* @param {ApprovalQueue} [spec.approvalQueue] - For durable approval
|
|
36
|
+
* @param {WriteSentLog} [spec.sentLog] - For idempotency tracking
|
|
37
|
+
* @param {object} [spec.requestedBy] - Actor identity { userId, serviceId, role }
|
|
38
|
+
* @param {object} [spec.traceInfo] - { traceId, spanId } for observability
|
|
39
|
+
* @param {number} [spec.maxRetries] - Max retry attempts on rate-limit (default 3)
|
|
40
|
+
* @returns {object} { status: 'sent'|'awaiting_approval'|'denied'|'error'|'cached', envelope: {...} }
|
|
41
|
+
*/
|
|
42
|
+
export async function writeWithEnvelope(spec) {
|
|
43
|
+
const {
|
|
44
|
+
provider, config, payload,
|
|
45
|
+
idempotencyKey: explicitKey,
|
|
46
|
+
dryRun = false,
|
|
47
|
+
broker, approvalQueue, sentLog,
|
|
48
|
+
requestedBy = {},
|
|
49
|
+
traceInfo = {},
|
|
50
|
+
maxRetries = 3,
|
|
51
|
+
} = spec;
|
|
52
|
+
|
|
53
|
+
const payloadStable = JSON.stringify(payload, Object.keys(payload).sort());
|
|
54
|
+
const idempotencyKey = explicitKey || crypto
|
|
55
|
+
.createHash('sha256')
|
|
56
|
+
.update(`${provider.meta?.id || 'unknown'}:${payload.type || 'write'}:${payloadStable}`)
|
|
57
|
+
.digest('hex')
|
|
58
|
+
.slice(0, 24);
|
|
59
|
+
|
|
60
|
+
const writeType = payload.type || 'write';
|
|
61
|
+
const providerId = provider.meta?.id || spec.providerId || 'unknown';
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
|
|
64
|
+
if (sentLog) {
|
|
65
|
+
const existing = sentLog.findByIdempotencyKey(idempotencyKey);
|
|
66
|
+
if (existing && existing.status === 'sent') {
|
|
67
|
+
return {
|
|
68
|
+
status: 'cached',
|
|
69
|
+
envelope: {
|
|
70
|
+
idempotencyKey,
|
|
71
|
+
sentAt: existing.sentAt,
|
|
72
|
+
provider: existing.provider || providerId,
|
|
73
|
+
writeType,
|
|
74
|
+
state: 'sent',
|
|
75
|
+
externalUrl: existing.externalUrl,
|
|
76
|
+
externalId: existing.externalId,
|
|
77
|
+
result: existing.result,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (sentLog) {
|
|
84
|
+
sentLog.record({ idempotencyKey, writeType, provider: providerId, sentAt: now, status: 'pending' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'dry-run',
|
|
90
|
+
envelope: {
|
|
91
|
+
idempotencyKey,
|
|
92
|
+
sentAt: now,
|
|
93
|
+
provider: providerId,
|
|
94
|
+
writeType,
|
|
95
|
+
state: 'dry-run',
|
|
96
|
+
payload,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (broker) {
|
|
102
|
+
try {
|
|
103
|
+
const policyResult = await broker.invoke({
|
|
104
|
+
role: requestedBy.role || 'member',
|
|
105
|
+
tool: `write:${providerId}:${writeType}`,
|
|
106
|
+
action: 'write',
|
|
107
|
+
toolArgs: payload,
|
|
108
|
+
risk: 'write',
|
|
109
|
+
requestedBy,
|
|
110
|
+
resumeToken: spec.resumeToken,
|
|
111
|
+
execute: async () => ({ ok: true, deferred: true }),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (policyResult.status === 'awaiting_approval') {
|
|
115
|
+
if (sentLog) sentLog.record({
|
|
116
|
+
idempotencyKey, writeType, provider: providerId, sentAt: now,
|
|
117
|
+
status: 'awaiting_approval',
|
|
118
|
+
result: { approvalId: policyResult.approvalId },
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
status: 'awaiting_approval',
|
|
122
|
+
envelope: {
|
|
123
|
+
idempotencyKey,
|
|
124
|
+
approvalId: policyResult.approvalId,
|
|
125
|
+
resumeToken: policyResult.resumeToken,
|
|
126
|
+
expiresAt: policyResult.expiresAt,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (policyResult.status === 'denied') {
|
|
132
|
+
if (sentLog) sentLog.record({
|
|
133
|
+
idempotencyKey, writeType, provider: providerId, sentAt: now,
|
|
134
|
+
status: 'denied',
|
|
135
|
+
error: policyResult.reason || 'denied by policy',
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
status: 'denied',
|
|
139
|
+
envelope: { idempotencyKey, reason: policyResult.reason },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (err.name === 'PolicyDenied') {
|
|
144
|
+
return { status: 'denied', envelope: { idempotencyKey, reason: err.message } };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let lastError = null;
|
|
150
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
151
|
+
try {
|
|
152
|
+
const result = await provider.write(config, payload);
|
|
153
|
+
const externalUrl = result?.url || result?.externalUrl || null;
|
|
154
|
+
const externalId = result?.id || result?.key || result?.externalId || null;
|
|
155
|
+
|
|
156
|
+
if (sentLog) {
|
|
157
|
+
sentLog.record({
|
|
158
|
+
idempotencyKey, writeType, provider: providerId, sentAt: now,
|
|
159
|
+
completedAt: new Date().toISOString(),
|
|
160
|
+
status: 'sent',
|
|
161
|
+
externalUrl, externalId, result,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
status: 'sent',
|
|
167
|
+
envelope: {
|
|
168
|
+
idempotencyKey,
|
|
169
|
+
sentAt: now,
|
|
170
|
+
provider: providerId,
|
|
171
|
+
writeType,
|
|
172
|
+
state: 'sent',
|
|
173
|
+
externalUrl,
|
|
174
|
+
externalId,
|
|
175
|
+
result,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
} catch (err) {
|
|
179
|
+
lastError = err;
|
|
180
|
+
const isRateLimit = err.message?.includes('rate') || err.status === 429;
|
|
181
|
+
if (!isRateLimit || attempt >= maxRetries) break;
|
|
182
|
+
await new Promise(r => setTimeout(r, Math.pow(2, attempt - 1) * 1000));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (sentLog) {
|
|
187
|
+
sentLog.record({
|
|
188
|
+
idempotencyKey, writeType, provider: providerId, sentAt: now,
|
|
189
|
+
completedAt: new Date().toISOString(),
|
|
190
|
+
status: 'failed',
|
|
191
|
+
error: lastError?.message || String(lastError),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
status: 'error',
|
|
197
|
+
envelope: {
|
|
198
|
+
idempotencyKey,
|
|
199
|
+
sentAt: now,
|
|
200
|
+
provider: providerId,
|
|
201
|
+
writeType,
|
|
202
|
+
state: 'failed',
|
|
203
|
+
error: lastError?.message || String(lastError),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|