@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
package/lib/policy/engine.mjs
CHANGED
|
@@ -8,16 +8,24 @@
|
|
|
8
8
|
* engine returns allow-without-approval everywhere.
|
|
9
9
|
*
|
|
10
10
|
* Decision precedence:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* 0. Mandatory-audit gate (LMCP-H5) — enterprise only: if the audit sink is
|
|
12
|
+
* down, every action is denied fail-closed before any other check runs.
|
|
13
|
+
* 1. Identity-anchored role authority (LMCP-I6) — in team/enterprise, an
|
|
14
|
+
* env-claimed role not among the identity's registered grants is
|
|
15
|
+
* denied before any manifest is consulted.
|
|
16
|
+
* 2. Team-level decisions gate specialist decisions — forbidden by team blocks the action.
|
|
17
|
+
* 3. Explicit deny in the role's fence → denied + typed reason.
|
|
18
|
+
* 4. Action falls in the role's approvalRequired list → allowed but
|
|
14
19
|
* approvalRequired = true (UI must collect human consent).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* 5. risk === 'high' and role is not in HIGH_RISK_AUTONOMOUS → approval required.
|
|
21
|
+
* 6. Deny-by-default: team → deny unless explicit grant; enterprise → deny always.
|
|
22
|
+
* 7. solo → allowed.
|
|
17
23
|
*/
|
|
18
24
|
|
|
19
25
|
import { readFileSync, existsSync } from 'node:fs';
|
|
20
26
|
import { loadRegistry } from '../registry/loader.mjs';
|
|
27
|
+
import { authorizeRoleClaim } from './role-authority.mjs';
|
|
28
|
+
import { enforceMandatoryAudit } from './audit-gate.mjs';
|
|
21
29
|
|
|
22
30
|
const HIGH_RISK_AUTONOMOUS = new Set(['security', 'sre', 'release-manager']);
|
|
23
31
|
|
|
@@ -97,6 +105,25 @@ function manifestFor(role, manifests) {
|
|
|
97
105
|
return manifests?.personas?.[role] || null;
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Return the default decision for unclassified (no matching explicit rule) tool calls.
|
|
110
|
+
*
|
|
111
|
+
* - solo → 'allow' (unchanged permissive behaviour)
|
|
112
|
+
* - team → 'deny-unclassified' (deny unless an explicit grant exists or the
|
|
113
|
+
* user escapes with an approval flow)
|
|
114
|
+
* - enterprise → 'deny' (hard deny regardless of approval escape)
|
|
115
|
+
*
|
|
116
|
+
* @param {'solo'|'team'|'enterprise'} deploymentMode
|
|
117
|
+
* @returns {'allow'|'deny-unclassified'|'deny'}
|
|
118
|
+
*/
|
|
119
|
+
export function getDefaultDecision(deploymentMode) {
|
|
120
|
+
switch (deploymentMode) {
|
|
121
|
+
case 'enterprise': return 'deny';
|
|
122
|
+
case 'team': return 'deny-unclassified';
|
|
123
|
+
default: return 'allow'; // solo and unknown → permissive
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
function actionMatches(pattern, action) {
|
|
101
128
|
if (!pattern || !action) return false;
|
|
102
129
|
if (pattern === action) return true;
|
|
@@ -125,15 +152,41 @@ function needsApprovalFromManifest(action, manifest) {
|
|
|
125
152
|
* @param {string} input.action
|
|
126
153
|
* @param {string} [input.decision] — optional decision id for team-level gate
|
|
127
154
|
* @param {string} [input.risk]
|
|
155
|
+
* @param {'solo'|'team'|'enterprise'} [input.deploymentMode='solo'] — controls deny-by-default behaviour
|
|
156
|
+
* @param {object} [input.identity] — resolved actor/service identity (lib/identity.mjs). In
|
|
157
|
+
* team/enterprise, `input.role` is validated as a claim against this identity's registered
|
|
158
|
+
* grants (see lib/policy/role-authority.mjs) before any manifest lookup.
|
|
128
159
|
* @param {object} [opts]
|
|
160
|
+
* @param {function} [opts.checkSink] — injectable audit-sink probe for LMCP-H5 tests;
|
|
161
|
+
* defaults to the real fs-backed checkAuditSinkAvailable.
|
|
129
162
|
* @returns {{allowed: boolean, reason: string, approvalRequired: boolean, source: string}}
|
|
130
163
|
*/
|
|
131
164
|
export function policyDecision(input = {}, opts = {}) {
|
|
132
|
-
const { role, tool, action, decision, risk = 'low' } = input;
|
|
165
|
+
const { role, tool, action, decision, risk = 'low', deploymentMode = 'solo', identity } = input;
|
|
133
166
|
if (!role) return { allowed: false, reason: 'role is required', approvalRequired: false, source: 'engine' };
|
|
134
167
|
if (!tool) return { allowed: false, reason: 'tool is required', approvalRequired: false, source: 'engine' };
|
|
135
168
|
if (!action) return { allowed: false, reason: 'action is required', approvalRequired: false, source: 'engine' };
|
|
136
169
|
|
|
170
|
+
const auditGateDecision = enforceMandatoryAudit({
|
|
171
|
+
deploymentMode,
|
|
172
|
+
...(opts.checkSink ? { checkSink: opts.checkSink } : {}),
|
|
173
|
+
});
|
|
174
|
+
if (auditGateDecision) return auditGateDecision;
|
|
175
|
+
|
|
176
|
+
if ((deploymentMode === 'team' || deploymentMode === 'enterprise') && identity) {
|
|
177
|
+
if (identity.source === 'implicit-solo') {
|
|
178
|
+
return { allowed: false, reason: `Identity not resolved for ${deploymentMode} mode`, approvalRequired: false, source: 'identity-boundary' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Identity-anchored authority (LMCP-I6): the role claim under evaluation
|
|
182
|
+
// must be among the identity's registered grants — an env-declared role
|
|
183
|
+
// with no matching grant is denied before any manifest lookup runs.
|
|
184
|
+
const authority = authorizeRoleClaim({ identity, role, deploymentMode });
|
|
185
|
+
if (!authority.authorized) {
|
|
186
|
+
return { allowed: false, reason: authority.reason, approvalRequired: false, source: authority.source };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
137
190
|
const manifests = opts.manifests || loadRoleManifests(opts.manifestPath);
|
|
138
191
|
const manifest = manifestFor(role, manifests);
|
|
139
192
|
|
|
@@ -186,6 +239,28 @@ export function policyDecision(input = {}, opts = {}) {
|
|
|
186
239
|
};
|
|
187
240
|
}
|
|
188
241
|
|
|
242
|
+
// Deny-by-default for team and enterprise modes.
|
|
243
|
+
// No explicit rule granted this action — consult the deployment-mode default.
|
|
244
|
+
const defaultDecision = getDefaultDecision(deploymentMode);
|
|
245
|
+
if (defaultDecision === 'deny') {
|
|
246
|
+
return {
|
|
247
|
+
allowed: false,
|
|
248
|
+
reason: 'deny-by-default',
|
|
249
|
+
approvalRequired: false,
|
|
250
|
+
mode: deploymentMode,
|
|
251
|
+
source: 'deny-by-default',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (defaultDecision === 'deny-unclassified') {
|
|
255
|
+
return {
|
|
256
|
+
allowed: false,
|
|
257
|
+
reason: 'deny-by-default',
|
|
258
|
+
approvalRequired: false,
|
|
259
|
+
mode: deploymentMode,
|
|
260
|
+
source: 'deny-by-default',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
189
264
|
return {
|
|
190
265
|
allowed: true,
|
|
191
266
|
reason: `action "${action}" permitted for role "${role}"`,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/policy/role-authority.mjs — identity-anchored role claim authority.
|
|
3
|
+
*
|
|
4
|
+
* A6/ADR-0056 requires policy decisions to bind to actor/service identity
|
|
5
|
+
* (H2, lib/identity.mjs), not to a self-declared env var. Prior to this
|
|
6
|
+
* module, `CONSTRUCT_ROLE` alone selected the role manifest a caller was
|
|
7
|
+
* evaluated against — a worker could export the env var and claim any
|
|
8
|
+
* role's authority, including `security` or `release-manager`, with no
|
|
9
|
+
* check against who the identity actually is.
|
|
10
|
+
*
|
|
11
|
+
* `authorizeRoleClaim` closes that gap: it takes the resolved identity
|
|
12
|
+
* (from resolveIdentity()) and the role claim under evaluation, and
|
|
13
|
+
* decides whether the identity is entitled to act as that role.
|
|
14
|
+
*
|
|
15
|
+
* Registered-grants source: `identity.grants` — a list of role ids the
|
|
16
|
+
* identity is registered for. This is the seam LMCP-G5 (worker
|
|
17
|
+
* registration/heartbeat) and pack `toolGrantsRequested` (LMCP-E1) populate;
|
|
18
|
+
* neither exists as a live registration store yet, so until G5 lands the
|
|
19
|
+
* only way an identity acquires a grant is an explicit `identity.grants`
|
|
20
|
+
* array set by the caller that resolved the identity (e.g. from a signed
|
|
21
|
+
* header, a worker registration record, or test fixture). No implicit
|
|
22
|
+
* grants are invented here — an identity with no `grants` array has none.
|
|
23
|
+
*
|
|
24
|
+
* Mode behavior:
|
|
25
|
+
* - solo: always authorized. CONSTRUCT_ROLE remains a documented
|
|
26
|
+
* solo-only convenience (lib/identity.mjs env fallback); there is no
|
|
27
|
+
* registration authority to check against in single-user mode.
|
|
28
|
+
* - team / enterprise: the claimed role must be present in the
|
|
29
|
+
* identity's `grants`. A role claim sourced from `env-fallback` (an
|
|
30
|
+
* env var with no actor identity headers at all) is never itself
|
|
31
|
+
* trusted as a grant — it is a claim, and claims are checked, not
|
|
32
|
+
* trusted. Missing or non-matching grants are DENIED (fail closed).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export class RoleAuthorityError extends Error {
|
|
36
|
+
constructor(message) { super(message); this.name = 'RoleAuthorityError'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return the set of role ids an identity is registered for.
|
|
41
|
+
*
|
|
42
|
+
* `identity.grants` is the only source read today (see file header for
|
|
43
|
+
* why). Absent/malformed values resolve to an empty set — never to "all
|
|
44
|
+
* roles" — so a caller who forgets to populate grants fails closed rather
|
|
45
|
+
* than silently trusting the claim.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} identity
|
|
48
|
+
* @returns {Set<string>}
|
|
49
|
+
*/
|
|
50
|
+
export function resolveIdentityGrants(identity) {
|
|
51
|
+
const grants = identity?.grants;
|
|
52
|
+
if (!Array.isArray(grants)) return new Set();
|
|
53
|
+
return new Set(grants.filter((g) => typeof g === 'string' && g.length > 0));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decide whether `role` is an authorized claim for `identity` under
|
|
58
|
+
* `deploymentMode`.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} input
|
|
61
|
+
* @param {object} input.identity - resolved identity (lib/identity.mjs shape)
|
|
62
|
+
* @param {string} input.role - the role claim under evaluation
|
|
63
|
+
* @param {'solo'|'team'|'enterprise'} [input.deploymentMode='solo']
|
|
64
|
+
* @returns {{authorized: boolean, reason: string, source: string}}
|
|
65
|
+
*/
|
|
66
|
+
export function authorizeRoleClaim({ identity, role, deploymentMode = 'solo' } = {}) {
|
|
67
|
+
if (!role) {
|
|
68
|
+
return { authorized: false, reason: 'role claim is required', source: 'role-authority' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (deploymentMode !== 'team' && deploymentMode !== 'enterprise') {
|
|
72
|
+
return { authorized: true, reason: 'solo mode: env-claimed role permitted (documented convenience)', source: 'solo-ergonomics' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!identity) {
|
|
76
|
+
return { authorized: false, reason: `no identity resolved to validate role claim "${role}" in ${deploymentMode} mode`, source: 'role-authority' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const grants = resolveIdentityGrants(identity);
|
|
80
|
+
if (grants.has(role)) {
|
|
81
|
+
return { authorized: true, reason: `role "${role}" is a registered grant for this identity`, source: 'identity-grant' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
authorized: false,
|
|
86
|
+
reason: `role claim "${role}" is not in this identity's registered grants in ${deploymentMode} mode`,
|
|
87
|
+
source: 'role-authority.ungranted',
|
|
88
|
+
};
|
|
89
|
+
}
|
package/lib/prompt-composer.js
CHANGED
|
@@ -39,7 +39,7 @@ import { estimateTokens, estimatePromptTokens, estimateTokensSync } from './toke
|
|
|
39
39
|
import { cxDir } from './paths.mjs';
|
|
40
40
|
import { getStrategyDigestSync } from './strategy-store.mjs';
|
|
41
41
|
import { loadRegistry } from './registry/loader.mjs';
|
|
42
|
-
import { bindingForSpecialist, resolveRoleOverlayId } from './roles/flavor-bindings.mjs';
|
|
42
|
+
import { bindingForSpecialist, bindingsForSpecialistId, resolveRoleOverlayId } from './roles/flavor-bindings.mjs';
|
|
43
43
|
|
|
44
44
|
const MAX_OBSERVATIONS = 3;
|
|
45
45
|
|
|
@@ -254,12 +254,23 @@ export function composePrompt(agentName, {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
const shortName = String(agentName).replace(/^cx-/, '');
|
|
257
|
-
const flavorMapping = bindingForSpecialist(shortName);
|
|
258
257
|
|
|
259
258
|
// When workspaceType is provided and roleFlavors doesn't already specify this agent's flavor,
|
|
260
259
|
// auto-select the matching overlay so agents get domain guidance without requiring explicit caller config.
|
|
261
260
|
const effectiveRoleFlavors = resolveRoleFlavors(roleFlavors, workspaceType, shortName);
|
|
262
261
|
|
|
262
|
+
// Since construct-rf26.11, one specialist can carry several flavor bindings
|
|
263
|
+
// (e.g. cx-engineer owns engineer/ai-engineer/platform-engineer/data-engineer
|
|
264
|
+
// overlays). Prefer whichever candidate binding's classifierKey the caller
|
|
265
|
+
// actually named in roleFlavors over the specialist's own bare-name binding,
|
|
266
|
+
// so a flavor-specific request (roleFlavors.platformEngineer) still resolves
|
|
267
|
+
// even though bindingForSpecialist(shortName) alone would only ever return
|
|
268
|
+
// the base 'engineer' entry.
|
|
269
|
+
const candidates = bindingsForSpecialistId(agentName);
|
|
270
|
+
const flavorMapping = (effectiveRoleFlavors
|
|
271
|
+
&& candidates.find((b) => effectiveRoleFlavors[b.classifierKey]))
|
|
272
|
+
|| bindingForSpecialist(shortName);
|
|
273
|
+
|
|
263
274
|
if (flavorMapping && effectiveRoleFlavors) {
|
|
264
275
|
const flavor = effectiveRoleFlavors[flavorMapping.classifierKey];
|
|
265
276
|
if (flavor) {
|
|
@@ -276,7 +287,12 @@ export function composePrompt(agentName, {
|
|
|
276
287
|
type: 'role-flavor',
|
|
277
288
|
priority: PRIORITY['role-flavor'],
|
|
278
289
|
label: roleId,
|
|
279
|
-
|
|
290
|
+
// flavorMapping.rolePrefix (not the agent's own shortName) names the
|
|
291
|
+
// domain guidance: since construct-rf26.11 one specialist can carry
|
|
292
|
+
// several flavor bindings (e.g. cx-engineer), and shortName only
|
|
293
|
+
// ever reflects the specialist's own bare name, not which of its
|
|
294
|
+
// several possible overlays actually matched.
|
|
295
|
+
content: `### ${flavor === 'core' ? flavorMapping.rolePrefix : flavor} domain guidance\n\n${overlayContent}`,
|
|
280
296
|
tokenBudget: selectedProfile.roleFlavorTokens,
|
|
281
297
|
});
|
|
282
298
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/contract/adapters/confluence/governed-write.mjs —
|
|
3
|
+
* envelope-shaped Confluence write adapter.
|
|
4
|
+
*
|
|
5
|
+
* Wraps a Confluence REST transport in the provider shape
|
|
6
|
+
* lib/writes/envelope.mjs expects: write(config, payload), meta.id,
|
|
7
|
+
* search(). This is the only exposed entry point for Confluence writes —
|
|
8
|
+
* specialists and CLI callers route through writeWithEnvelope(), never
|
|
9
|
+
* through a raw Confluence client, so dedup, retry, dry-run, approval, and
|
|
10
|
+
* audit stay centralized in the envelope (LMCP-J2) rather than being
|
|
11
|
+
* reimplemented per-adapter.
|
|
12
|
+
*
|
|
13
|
+
* Adds the three behaviors a generic envelope cannot provide:
|
|
14
|
+
*
|
|
15
|
+
* - Duplicate detection by title+space: before create, searches the
|
|
16
|
+
* target space for an existing page with the same title (via the
|
|
17
|
+
* injected `confluenceTransport.searchPagesByTitle`). A match returns a
|
|
18
|
+
* `page-duplicate` result carrying a `linkback` to the existing page
|
|
19
|
+
* instead of creating a second page — mirrors the GitHub adapter's
|
|
20
|
+
* marker-search dedup (LMCP-J-github) but keyed on title+space rather
|
|
21
|
+
* than a hidden marker, since Confluence page search has no analogue to
|
|
22
|
+
* GitHub's full-text issue search over a hidden comment.
|
|
23
|
+
* - Version-conflict recovery on update: Confluence page updates require
|
|
24
|
+
* the current `version.number`; if the page changed since the caller
|
|
25
|
+
* last fetched it, the API returns 409. On 409 this adapter refetches
|
|
26
|
+
* the live page, re-renders the caller's update against the fresh
|
|
27
|
+
* version, and returns a `version-conflict` result carrying the fresh
|
|
28
|
+
* page and a `retryPayload` — the caller (or a human via the envelope's
|
|
29
|
+
* approval path) re-approves before the adapter retries the write. The
|
|
30
|
+
* adapter never blindly overwrites a page that changed underneath it.
|
|
31
|
+
* - Linkback comment: after a successful create or update, posts a footer
|
|
32
|
+
* comment on the page recording the write's idempotency key, so anyone
|
|
33
|
+
* reading the page in Confluence can trace which governed write
|
|
34
|
+
* produced or last touched it.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { AuthError } from '../../errors.mjs';
|
|
38
|
+
|
|
39
|
+
const LINKBACK_PREFIX = 'construct:write:';
|
|
40
|
+
|
|
41
|
+
function linkbackComment(idempotencyKey) {
|
|
42
|
+
return `<p>Published via Construct governed write (${LINKBACK_PREFIX}${idempotencyKey}).</p>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} opts
|
|
47
|
+
* @param {object} opts.confluenceTransport - underlying Confluence REST transport; must implement
|
|
48
|
+
* `searchPagesByTitle(spaceId, title)`, `getPage(pageId)`, `createPage(fields)`,
|
|
49
|
+
* `updatePage(fields)`, `createFooterComment(pageId, body)`.
|
|
50
|
+
*/
|
|
51
|
+
export function createGovernedConfluenceProvider({ confluenceTransport } = {}) {
|
|
52
|
+
if (!confluenceTransport) throw new Error('createGovernedConfluenceProvider: confluenceTransport is required');
|
|
53
|
+
|
|
54
|
+
async function postLinkback(pageId, idempotencyKey) {
|
|
55
|
+
if (!idempotencyKey) return;
|
|
56
|
+
try {
|
|
57
|
+
await confluenceTransport.createFooterComment(pageId, linkbackComment(idempotencyKey));
|
|
58
|
+
} catch {
|
|
59
|
+
// Linkback is best-effort audit trail on the page itself; the
|
|
60
|
+
// envelope's sent-log is the authoritative record, so a comment
|
|
61
|
+
// failure must not fail an otherwise-successful publish.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function writePage(payload) {
|
|
66
|
+
const { spaceId, title, body, parentId, idempotencyKey } = payload;
|
|
67
|
+
if (!spaceId) throw new Error('confluence governed write: payload.spaceId is required for type "page"');
|
|
68
|
+
if (!title) throw new Error('confluence governed write: payload.title is required for type "page"');
|
|
69
|
+
|
|
70
|
+
let existing;
|
|
71
|
+
try {
|
|
72
|
+
existing = await confluenceTransport.searchPagesByTitle(spaceId, title);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw mapWriteTransportError(err, { spaceId });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const match = (existing ?? []).find((p) => p.title === title);
|
|
78
|
+
if (match) {
|
|
79
|
+
return {
|
|
80
|
+
type: 'page-duplicate',
|
|
81
|
+
id: match.id,
|
|
82
|
+
title: match.title,
|
|
83
|
+
url: match.url,
|
|
84
|
+
linkback: match.url,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const result = await confluenceTransport.createPage({ spaceId, title, body, parentId });
|
|
90
|
+
await postLinkback(result.id, idempotencyKey);
|
|
91
|
+
return { type: 'page-created', id: result.id, title: result.title, version: result.version, url: result.url };
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw mapWriteTransportError(err, { spaceId });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function writePageUpdate(payload) {
|
|
98
|
+
const { pageId, title, body, version, idempotencyKey } = payload;
|
|
99
|
+
if (!pageId) throw new Error('confluence governed write: payload.pageId is required for type "page-update"');
|
|
100
|
+
if (version === undefined || version === null) {
|
|
101
|
+
throw new Error('confluence governed write: payload.version is required for type "page-update"');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await confluenceTransport.updatePage({ pageId, title, body, version });
|
|
106
|
+
await postLinkback(pageId, idempotencyKey);
|
|
107
|
+
return { type: 'page-updated', id: result.id, title: result.title, version: result.version, url: result.url };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err?.status === 409) {
|
|
110
|
+
const fresh = await confluenceTransport.getPage(pageId);
|
|
111
|
+
return {
|
|
112
|
+
type: 'version-conflict',
|
|
113
|
+
id: pageId,
|
|
114
|
+
url: fresh.url,
|
|
115
|
+
currentVersion: fresh.version,
|
|
116
|
+
currentBody: fresh.body,
|
|
117
|
+
currentTitle: fresh.title,
|
|
118
|
+
retryPayload: {
|
|
119
|
+
type: 'page-update',
|
|
120
|
+
pageId,
|
|
121
|
+
title: title ?? fresh.title,
|
|
122
|
+
body,
|
|
123
|
+
version: fresh.version,
|
|
124
|
+
idempotencyKey,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
throw mapWriteTransportError(err, { pageId });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
meta: {
|
|
134
|
+
id: 'confluence',
|
|
135
|
+
displayName: 'Confluence (governed)',
|
|
136
|
+
capabilities: ['write', 'search'],
|
|
137
|
+
description: 'Envelope-routed Confluence writes with title+space dedup, version-conflict recovery, and linkback comments.',
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async write(config, payload) {
|
|
141
|
+
if (payload?.type === 'page') return writePage(payload);
|
|
142
|
+
if (payload?.type === 'page-update') return writePageUpdate(payload);
|
|
143
|
+
throw new Error(`confluence governed write: unsupported type "${payload?.type}" (only 'page' and 'page-update' are supported)`);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async search(config, query) {
|
|
147
|
+
const spaceId = typeof query === 'object' ? query?.spaceId : undefined;
|
|
148
|
+
const title = typeof query === 'object' ? query?.title : query;
|
|
149
|
+
return confluenceTransport.searchPagesByTitle(spaceId, title);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Render the payload the envelope would submit, without calling search,
|
|
154
|
+
* getPage, or the transport. Feeds the envelope's dry-run path
|
|
155
|
+
* (lib/writes/envelope.mjs `dryRun: true`), giving a human reviewing a
|
|
156
|
+
* pending write the exact storage-format body and target coordinates
|
|
157
|
+
* before anything is sent.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} payload
|
|
160
|
+
* @returns {{ type: string, fields: object }}
|
|
161
|
+
*/
|
|
162
|
+
renderDryRun(payload) {
|
|
163
|
+
if (payload?.type === 'page') {
|
|
164
|
+
return {
|
|
165
|
+
type: 'page',
|
|
166
|
+
fields: {
|
|
167
|
+
spaceId: payload.spaceId,
|
|
168
|
+
title: payload.title,
|
|
169
|
+
parentId: payload.parentId,
|
|
170
|
+
body: { representation: 'storage', value: payload.body },
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (payload?.type === 'page-update') {
|
|
175
|
+
return {
|
|
176
|
+
type: 'page-update',
|
|
177
|
+
fields: {
|
|
178
|
+
pageId: payload.pageId,
|
|
179
|
+
title: payload.title,
|
|
180
|
+
version: { number: payload.version },
|
|
181
|
+
body: { representation: 'storage', value: payload.body },
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`confluence governed write: cannot render dry-run for unsupported type "${payload?.type}"`);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Map a create/update/search transport failure (401/403/404) to an
|
|
192
|
+
* actionable message. 409 (version conflict) is handled by the caller
|
|
193
|
+
* directly and never reaches this mapper.
|
|
194
|
+
*
|
|
195
|
+
* @param {Error & {status?: number}} err
|
|
196
|
+
* @param {{ spaceId?: string, pageId?: string }} ctx
|
|
197
|
+
* @returns {Error}
|
|
198
|
+
*/
|
|
199
|
+
function mapWriteTransportError(err, ctx) {
|
|
200
|
+
const status = err?.status;
|
|
201
|
+
if (status === 401) {
|
|
202
|
+
return new AuthError('Confluence authentication failed. Check CONFLUENCE_EMAIL / CONFLUENCE_TOKEN.', { provider: 'confluence' });
|
|
203
|
+
}
|
|
204
|
+
if (status === 403) {
|
|
205
|
+
const target = ctx.spaceId ? `space "${ctx.spaceId}"` : `page "${ctx.pageId}"`;
|
|
206
|
+
return new Error(`Forbidden: the authenticated account lacks permission to write to ${target}. Check the confluence-content:write scope.`);
|
|
207
|
+
}
|
|
208
|
+
if (status === 404) {
|
|
209
|
+
const target = ctx.spaceId ? `Space "${ctx.spaceId}"` : `Page "${ctx.pageId}"`;
|
|
210
|
+
return new Error(`${target} was not found, or the authenticated account cannot access it.`);
|
|
211
|
+
}
|
|
212
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default createGovernedConfluenceProvider;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "confluence-write",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"kind": "data-source",
|
|
5
|
+
"capabilities": ["write", "search"],
|
|
6
|
+
"secretEnvKeys": ["CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_TOKEN"],
|
|
7
|
+
"operations": ["write:page", "write:page-update", "search"],
|
|
8
|
+
"securityClassification": "trusted",
|
|
9
|
+
"owner": "construct-core",
|
|
10
|
+
"compatVersion": 1,
|
|
11
|
+
"installSource": "builtin",
|
|
12
|
+
"exposure": "envelope-only",
|
|
13
|
+
"envelopeModule": "lib/writes/envelope.mjs",
|
|
14
|
+
"adapterModule": "lib/providers/contract/adapters/confluence/governed-write.mjs",
|
|
15
|
+
"transportModule": "lib/providers/contract/adapters/confluence/transport.mjs",
|
|
16
|
+
"notes": "Colocated write-capability manifest for LMCP-J4. Follows the lib/extensions/manifests/*.manifest.json schema (see atlassian-confluence.manifest.json, which declares read/search only). Wiring this into the shared lib/extensions/manifests/ discovery directory is out of scope for this bead; capabilities here are reachable only via writeWithEnvelope(), never a direct provider-registry lookup."
|
|
17
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/contract/adapters/confluence/transport.mjs — real Confluence
|
|
3
|
+
* Cloud REST v2 transport for the governed write adapter.
|
|
4
|
+
*
|
|
5
|
+
* Implements the `confluenceTransport` shape governed-write.mjs depends on:
|
|
6
|
+
* `searchPagesByTitle`, `getPage`, `createPage`, `updatePage`,
|
|
7
|
+
* `createFooterComment`. Kept separate from governed-write.mjs so tests can
|
|
8
|
+
* inject a fake transport (tests/fakes/fake-confluence-transport.mjs)
|
|
9
|
+
* without any network dependency, and separate from the read-oriented
|
|
10
|
+
* adapter in ./index.mjs so this write path never shares mutable state with
|
|
11
|
+
* the read path.
|
|
12
|
+
*
|
|
13
|
+
* Auth: API token via CONFLUENCE_URL + CONFLUENCE_EMAIL + CONFLUENCE_TOKEN
|
|
14
|
+
* env vars, or passed directly via config.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { AuthError, RateLimitError } from '../../errors.mjs';
|
|
18
|
+
import { guardedFetch } from '../../../../net-guard.mjs';
|
|
19
|
+
|
|
20
|
+
export function createConfluenceTransport(config = {}) {
|
|
21
|
+
const baseUrl = (config.baseUrl ?? process.env.CONFLUENCE_URL ?? '').replace(/\/$/, '');
|
|
22
|
+
const email = config.email ?? process.env.CONFLUENCE_EMAIL;
|
|
23
|
+
const token = config.token ?? process.env.CONFLUENCE_TOKEN;
|
|
24
|
+
|
|
25
|
+
if (!baseUrl || !email || !token) {
|
|
26
|
+
throw new AuthError(
|
|
27
|
+
'Confluence transport requires CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_TOKEN (or config.baseUrl/email/token)',
|
|
28
|
+
{ provider: 'confluence' },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const auth = `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
|
|
33
|
+
|
|
34
|
+
// Egress runs through the N7 guard (SSRF/rebinding); a private-address
|
|
35
|
+
// self-hosted instance is an explicit, audited opt-in, never the default.
|
|
36
|
+
const allowPrivate = config.allowPrivateEgress ?? process.env.CONSTRUCT_NET_ALLOW_PRIVATE_EGRESS === '1';
|
|
37
|
+
|
|
38
|
+
async function request(path, { method = 'GET', body } = {}) {
|
|
39
|
+
const res = await guardedFetch(`${baseUrl}${path}`, {
|
|
40
|
+
method,
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: auth,
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
Accept: 'application/json',
|
|
45
|
+
},
|
|
46
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
47
|
+
}, { allowPrivate });
|
|
48
|
+
|
|
49
|
+
if (res.status === 429) {
|
|
50
|
+
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10);
|
|
51
|
+
throw new RateLimitError('Confluence rate limit hit', { provider: 'confluence', retryAfter });
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const text = await res.text().catch(() => '');
|
|
55
|
+
const err = new Error(`Confluence API ${method} ${path} failed: ${res.status} ${text.slice(0, 300)}`);
|
|
56
|
+
err.status = res.status;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
if (res.status === 204) return null;
|
|
60
|
+
return res.json();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
async searchPagesByTitle(spaceId, title) {
|
|
65
|
+
const params = new URLSearchParams({ title, 'space-id': spaceId, limit: '10' });
|
|
66
|
+
const data = await request(`/wiki/api/v2/pages?${params.toString()}`);
|
|
67
|
+
return (data.results ?? []).map((p) => ({
|
|
68
|
+
id: p.id,
|
|
69
|
+
title: p.title,
|
|
70
|
+
spaceId: p.spaceId,
|
|
71
|
+
version: p.version?.number ?? null,
|
|
72
|
+
url: `${baseUrl}${p._links?.webui ?? ''}`,
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async getPage(pageId) {
|
|
77
|
+
const data = await request(`/wiki/api/v2/pages/${pageId}?body-format=storage`);
|
|
78
|
+
return {
|
|
79
|
+
id: data.id,
|
|
80
|
+
title: data.title,
|
|
81
|
+
spaceId: data.spaceId,
|
|
82
|
+
version: data.version?.number ?? null,
|
|
83
|
+
body: data.body?.storage?.value ?? '',
|
|
84
|
+
url: `${baseUrl}${data._links?.webui ?? ''}`,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async createPage({ spaceId, title, body, parentId }) {
|
|
89
|
+
const reqBody = {
|
|
90
|
+
spaceId,
|
|
91
|
+
status: 'current',
|
|
92
|
+
title,
|
|
93
|
+
parentId: parentId ?? undefined,
|
|
94
|
+
body: { representation: 'storage', value: body },
|
|
95
|
+
};
|
|
96
|
+
const data = await request('/wiki/api/v2/pages', { method: 'POST', body: reqBody });
|
|
97
|
+
return {
|
|
98
|
+
id: data.id,
|
|
99
|
+
title: data.title,
|
|
100
|
+
version: data.version?.number ?? 1,
|
|
101
|
+
url: `${baseUrl}${data._links?.webui ?? ''}`,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async updatePage({ pageId, title, body, version }) {
|
|
106
|
+
const reqBody = {
|
|
107
|
+
id: pageId,
|
|
108
|
+
status: 'current',
|
|
109
|
+
title,
|
|
110
|
+
version: { number: version },
|
|
111
|
+
body: { representation: 'storage', value: body },
|
|
112
|
+
};
|
|
113
|
+
const data = await request(`/wiki/api/v2/pages/${pageId}`, { method: 'PUT', body: reqBody });
|
|
114
|
+
return {
|
|
115
|
+
id: data.id,
|
|
116
|
+
title: data.title,
|
|
117
|
+
version: data.version?.number ?? version,
|
|
118
|
+
url: `${baseUrl}${data._links?.webui ?? ''}`,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async createFooterComment(pageId, body) {
|
|
123
|
+
const data = await request('/wiki/api/v2/footer-comments', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
body: { pageId, body: { representation: 'storage', value: body } },
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
id: data.id,
|
|
129
|
+
url: `${baseUrl}${data._links?.webui ?? ''}`,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default createConfluenceTransport;
|