@geraldmaron/construct 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +261 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +103 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/contract/adapters/github/governed-write.mjs — envelope-shaped
|
|
3
|
+
* GitHub write adapter.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the CLI-backed github adapter (./index.mjs) in the provider shape
|
|
6
|
+
* lib/writes/envelope.mjs expects: write(config, payload), meta.id, search().
|
|
7
|
+
* Adds two behaviors the raw CLI adapter cannot provide on its own:
|
|
8
|
+
*
|
|
9
|
+
* - Cross-run duplicate detection: every created issue carries a hidden
|
|
10
|
+
* idempotency marker (HTML comment) in its body. Before creating, this
|
|
11
|
+
* wrapper searches GitHub itself for an issue carrying that marker, so
|
|
12
|
+
* dedup survives across process restarts and machines, not just the
|
|
13
|
+
* local sent-log.
|
|
14
|
+
* - Secondary-rate-limit backoff: GitHub content-creation endpoints return
|
|
15
|
+
* 403 with a "secondary rate limit" message (no clean HTTP Retry-After
|
|
16
|
+
* on the gh CLI transport). This wrapper retries a bounded number of
|
|
17
|
+
* times honoring the parsed retry-after, then surfaces the error rather
|
|
18
|
+
* than spinning silently — the envelope's own generic retry/backoff is
|
|
19
|
+
* bypassed here because it is not retry-after aware.
|
|
20
|
+
*
|
|
21
|
+
* Only `type: 'issue'` writes get marker + dedup treatment; other write
|
|
22
|
+
* types pass straight through to the underlying adapter.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { RateLimitError } from '../../errors.mjs';
|
|
26
|
+
|
|
27
|
+
const MARKER_PREFIX = 'construct:idempotency:';
|
|
28
|
+
|
|
29
|
+
function markerFor(key) {
|
|
30
|
+
return `<!-- ${MARKER_PREFIX}${key} -->`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractMarkerKey(text) {
|
|
34
|
+
const match = new RegExp(`${MARKER_PREFIX}([a-zA-Z0-9_-]+)`).exec(text || '');
|
|
35
|
+
return match ? match[1] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sleep(ms) {
|
|
39
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} opts
|
|
44
|
+
* @param {object} opts.ghAdapter - underlying CLI adapter (default export of ./index.mjs)
|
|
45
|
+
* @param {number} [opts.maxRateLimitRetries] - bounded retry-after retries before surfacing (default 2)
|
|
46
|
+
* @param {function} [opts.sleepFn] - injectable sleep for tests
|
|
47
|
+
*/
|
|
48
|
+
export function createGovernedGithubProvider({ ghAdapter, maxRateLimitRetries = 2, sleepFn = sleep } = {}) {
|
|
49
|
+
if (!ghAdapter) throw new Error('createGovernedGithubProvider: ghAdapter is required');
|
|
50
|
+
|
|
51
|
+
async function findExistingByMarker(config, { title, markerKey }) {
|
|
52
|
+
const results = await ghAdapter.search(`${title} in:title`, { scope: 'issues' });
|
|
53
|
+
for (const candidate of results || []) {
|
|
54
|
+
const body = candidate.body ?? candidate.textMatches?.map((m) => m.fragment).join(' ') ?? '';
|
|
55
|
+
const haystack = `${candidate.title ?? ''} ${body}`;
|
|
56
|
+
if (extractMarkerKey(haystack) === markerKey || candidate.title === title) {
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeIssueWithRetry(config, payload, markerKey) {
|
|
64
|
+
const bodyWithMarker = `${payload.body ?? ''}\n\n${markerFor(markerKey)}`;
|
|
65
|
+
let attempt = 0;
|
|
66
|
+
for (;;) {
|
|
67
|
+
try {
|
|
68
|
+
return await ghAdapter.write({
|
|
69
|
+
type: 'issue',
|
|
70
|
+
title: payload.title,
|
|
71
|
+
body: bodyWithMarker,
|
|
72
|
+
labels: payload.labels,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const isSecondaryRateLimit = err instanceof RateLimitError || err.status === 403;
|
|
76
|
+
if (!isSecondaryRateLimit || attempt >= maxRateLimitRetries) {
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
attempt += 1;
|
|
80
|
+
const retryAfterMs = (err.retryAfter ?? 60) * 1000;
|
|
81
|
+
await sleepFn(retryAfterMs);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
meta: {
|
|
88
|
+
id: 'github',
|
|
89
|
+
displayName: 'GitHub (governed)',
|
|
90
|
+
capabilities: ['write', 'search'],
|
|
91
|
+
description: 'Envelope-routed GitHub writes with marker-based dedup and retry-after backoff.',
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async write(config, payload) {
|
|
95
|
+
if (payload?.type !== 'issue') {
|
|
96
|
+
return ghAdapter.write(payload);
|
|
97
|
+
}
|
|
98
|
+
if (!payload.title) throw new Error('github governed write: payload.title is required for type "issue"');
|
|
99
|
+
|
|
100
|
+
const markerKey = payload.idempotencyKey || payload.title;
|
|
101
|
+
const existing = await findExistingByMarker(config, { title: payload.title, markerKey });
|
|
102
|
+
if (existing) {
|
|
103
|
+
return {
|
|
104
|
+
type: 'issue-duplicate',
|
|
105
|
+
id: existing.number ?? existing.id,
|
|
106
|
+
number: existing.number,
|
|
107
|
+
url: existing.url,
|
|
108
|
+
linkback: existing.url,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return writeIssueWithRetry(config, payload, markerKey);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async search(config, query) {
|
|
116
|
+
return ghAdapter.search(query, { scope: 'issues' });
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default createGovernedGithubProvider;
|
|
@@ -27,7 +27,26 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { spawnSync } from 'node:child_process';
|
|
30
|
-
import { AuthError, NotFoundError } from '../../errors.mjs';
|
|
30
|
+
import { AuthError, NotFoundError, RateLimitError } from '../../errors.mjs';
|
|
31
|
+
|
|
32
|
+
// gh CLI has no HTTP header access; secondary rate limits surface as a 403
|
|
33
|
+
// with "secondary rate limit" text in stderr and sometimes a suggested wait
|
|
34
|
+
// in seconds. Default to GitHub's documented minimum backoff when absent.
|
|
35
|
+
const SECONDARY_RATE_LIMIT_DEFAULT_RETRY_AFTER = 60;
|
|
36
|
+
|
|
37
|
+
function parseRetryAfterSeconds(stderr) {
|
|
38
|
+
const match = /retry.{0,10}?(\d+)\s*second/i.exec(stderr || '');
|
|
39
|
+
return match ? parseInt(match[1], 10) : SECONDARY_RATE_LIMIT_DEFAULT_RETRY_AFTER;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function throwIfRateLimited(stderr) {
|
|
43
|
+
if (/secondary rate limit|403/i.test(stderr || '') && /rate limit/i.test(stderr || '')) {
|
|
44
|
+
throw new RateLimitError(stderr.trim(), {
|
|
45
|
+
provider: 'github',
|
|
46
|
+
retryAfter: parseRetryAfterSeconds(stderr),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
31
50
|
|
|
32
51
|
function gh(args, { json = true, input } = {}) {
|
|
33
52
|
const fullArgs = json ? [...args, '--json', ...ghJsonFields(args)] : args;
|
|
@@ -115,6 +134,10 @@ export default {
|
|
|
115
134
|
const args = ['issue', 'create', ...repoFlag, '--title', item.title, '--body', item.body ?? ''];
|
|
116
135
|
if (item.labels?.length) args.push('--label', item.labels.join(','));
|
|
117
136
|
const out = spawnSync('gh', args, { encoding: 'utf8', env: process.env });
|
|
137
|
+
if (out.status !== 0) {
|
|
138
|
+
throwIfRateLimited(out.stderr);
|
|
139
|
+
throw new Error(`gh issue create failed: ${out.stderr?.trim() || `exit ${out.status}`}`);
|
|
140
|
+
}
|
|
118
141
|
return { type: 'issue-created', url: out.stdout.trim() };
|
|
119
142
|
}
|
|
120
143
|
|
|
@@ -127,21 +150,33 @@ export default {
|
|
|
127
150
|
];
|
|
128
151
|
if (item.draft) args.push('--draft');
|
|
129
152
|
const out = spawnSync('gh', args, { encoding: 'utf8', env: process.env });
|
|
153
|
+
if (out.status !== 0) {
|
|
154
|
+
throwIfRateLimited(out.stderr);
|
|
155
|
+
throw new Error(`gh pr create failed: ${out.stderr?.trim() || `exit ${out.status}`}`);
|
|
156
|
+
}
|
|
130
157
|
return { type: 'pr-created', url: out.stdout.trim() };
|
|
131
158
|
}
|
|
132
159
|
|
|
133
160
|
if (item.type === 'comment') {
|
|
134
161
|
const args = ['issue', 'comment', String(item.issue_number), ...repoFlag, '--body', item.body];
|
|
135
|
-
spawnSync('gh', args, { encoding: 'utf8', env: process.env });
|
|
162
|
+
const out = spawnSync('gh', args, { encoding: 'utf8', env: process.env });
|
|
163
|
+
if (out.status !== 0) {
|
|
164
|
+
throwIfRateLimited(out.stderr);
|
|
165
|
+
throw new Error(`gh issue comment failed: ${out.stderr?.trim() || `exit ${out.status}`}`);
|
|
166
|
+
}
|
|
136
167
|
return { type: 'comment-created', issue_number: item.issue_number };
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
if (item.type === 'pr-merge') {
|
|
140
171
|
const method = item.merge_method ?? 'squash';
|
|
141
|
-
spawnSync('gh', ['pr', 'merge', String(item.number), ...repoFlag, `--${method}`], {
|
|
172
|
+
const out = spawnSync('gh', ['pr', 'merge', String(item.number), ...repoFlag, `--${method}`], {
|
|
142
173
|
encoding: 'utf8',
|
|
143
174
|
env: process.env,
|
|
144
175
|
});
|
|
176
|
+
if (out.status !== 0) {
|
|
177
|
+
throwIfRateLimited(out.stderr);
|
|
178
|
+
throw new Error(`gh pr merge failed: ${out.stderr?.trim() || `exit ${out.status}`}`);
|
|
179
|
+
}
|
|
145
180
|
return { type: 'pr-merged', number: item.number };
|
|
146
181
|
}
|
|
147
182
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/contract/adapters/jira/adf.mjs — Atlassian Document Format
|
|
3
|
+
* (ADF) construction for Jira Cloud write fields.
|
|
4
|
+
*
|
|
5
|
+
* Jira Cloud REST API v3 requires `description`, `comment.body`, and any
|
|
6
|
+
* multiline custom field of type `doc` to be submitted as an ADF document
|
|
7
|
+
* rather than plain text. Covers the minimal ADF subset issue/comment write
|
|
8
|
+
* ops need: paragraphs split on blank lines, single-level bullet lists for
|
|
9
|
+
* lines starting with "- " or "* ", and inline text nodes. Full markdown
|
|
10
|
+
* fidelity (headings, tables, nested lists) is out of scope.
|
|
11
|
+
*
|
|
12
|
+
* `renderAdfPreview(adf)` renders an ADF document back to a readable plain
|
|
13
|
+
* string for dry-run display, so a human reviewing a dry-run payload sees
|
|
14
|
+
* text rather than a raw ADF tree.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const ADF_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
function textNode(text) {
|
|
20
|
+
return { type: 'text', text: String(text) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function paragraphNode(text) {
|
|
24
|
+
return { type: 'paragraph', content: text ? [textNode(text)] : [] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bulletListNode(items) {
|
|
28
|
+
return {
|
|
29
|
+
type: 'bulletList',
|
|
30
|
+
content: items.map((item) => ({
|
|
31
|
+
type: 'listItem',
|
|
32
|
+
content: [paragraphNode(item)],
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Blank-line-separated blocks become paragraphs; a contiguous run of lines
|
|
38
|
+
// each starting with "- " or "* " becomes a single bullet list. This covers
|
|
39
|
+
// the shapes specialists actually produce (short prose + a checklist) without
|
|
40
|
+
// pretending to be a markdown parser.
|
|
41
|
+
|
|
42
|
+
function blocksFromText(text) {
|
|
43
|
+
const lines = String(text ?? '').split('\n');
|
|
44
|
+
const blocks = [];
|
|
45
|
+
let paragraphLines = [];
|
|
46
|
+
let listItems = [];
|
|
47
|
+
|
|
48
|
+
function flushParagraph() {
|
|
49
|
+
if (paragraphLines.length) {
|
|
50
|
+
blocks.push(paragraphNode(paragraphLines.join(' ').trim()));
|
|
51
|
+
paragraphLines = [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function flushList() {
|
|
55
|
+
if (listItems.length) {
|
|
56
|
+
blocks.push(bulletListNode(listItems));
|
|
57
|
+
listItems = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const rawLine of lines) {
|
|
62
|
+
const line = rawLine.trim();
|
|
63
|
+
const bulletMatch = /^[-*]\s+(.*)$/.exec(line);
|
|
64
|
+
if (bulletMatch) {
|
|
65
|
+
flushParagraph();
|
|
66
|
+
listItems.push(bulletMatch[1]);
|
|
67
|
+
} else if (line === '') {
|
|
68
|
+
flushParagraph();
|
|
69
|
+
flushList();
|
|
70
|
+
} else {
|
|
71
|
+
flushList();
|
|
72
|
+
paragraphLines.push(line);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
flushParagraph();
|
|
76
|
+
flushList();
|
|
77
|
+
|
|
78
|
+
return blocks.length ? blocks : [paragraphNode('')];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a full ADF document from a plain-text (or already-ADF) input.
|
|
83
|
+
* Passing an object that already looks like an ADF doc (`{ type: 'doc' }`)
|
|
84
|
+
* returns it unchanged so callers can pass through pre-built documents.
|
|
85
|
+
*
|
|
86
|
+
* @param {string|object} content
|
|
87
|
+
* @returns {object} ADF document
|
|
88
|
+
*/
|
|
89
|
+
export function buildAdfDocument(content) {
|
|
90
|
+
if (content && typeof content === 'object' && content.type === 'doc') {
|
|
91
|
+
return content;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
type: 'doc',
|
|
95
|
+
version: ADF_VERSION,
|
|
96
|
+
content: blocksFromText(content),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render an ADF document (or plain text) back to a flat human-readable
|
|
102
|
+
* string, for dry-run display. Not a full ADF renderer — only understands
|
|
103
|
+
* the node types buildAdfDocument produces plus a defensive fallback for
|
|
104
|
+
* unknown node types (renders as `[<type>]`).
|
|
105
|
+
*
|
|
106
|
+
* @param {object|string} adf
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
export function renderAdfPreview(adf) {
|
|
110
|
+
if (typeof adf === 'string') return adf;
|
|
111
|
+
if (!adf || typeof adf !== 'object') return '';
|
|
112
|
+
|
|
113
|
+
function renderNode(node) {
|
|
114
|
+
if (!node || typeof node !== 'object') return '';
|
|
115
|
+
switch (node.type) {
|
|
116
|
+
case 'doc':
|
|
117
|
+
return (node.content ?? []).map(renderNode).join('\n');
|
|
118
|
+
case 'paragraph':
|
|
119
|
+
return (node.content ?? []).map(renderNode).join('');
|
|
120
|
+
case 'text':
|
|
121
|
+
return node.text ?? '';
|
|
122
|
+
case 'bulletList':
|
|
123
|
+
return (node.content ?? []).map((item) => `- ${renderNode(item)}`).join('\n');
|
|
124
|
+
case 'listItem':
|
|
125
|
+
return (node.content ?? []).map(renderNode).join('').trim();
|
|
126
|
+
default:
|
|
127
|
+
return `[${node.type ?? 'unknown'}]`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return renderNode(adf);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* True when the given value is a well-formed ADF document envelope
|
|
136
|
+
* (`{ type: 'doc', version, content: [...] }`). Distinguishes a doc node
|
|
137
|
+
* from a bare string, for rejecting fields the API requires as ADF but
|
|
138
|
+
* were submitted as plain text.
|
|
139
|
+
*
|
|
140
|
+
* @param {unknown} value
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
export function isAdfDocument(value) {
|
|
144
|
+
return (
|
|
145
|
+
!!value &&
|
|
146
|
+
typeof value === 'object' &&
|
|
147
|
+
value.type === 'doc' &&
|
|
148
|
+
typeof value.version === 'number' &&
|
|
149
|
+
Array.isArray(value.content)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/contract/adapters/jira/createmeta.mjs — Jira Cloud createmeta
|
|
3
|
+
* validation for write payloads.
|
|
4
|
+
*
|
|
5
|
+
* Jira Cloud's `GET /rest/api/3/issue/createmeta` (expanded with
|
|
6
|
+
* `projects.issuetypes.fields`) reports, per project + issue type, which
|
|
7
|
+
* fields exist, which are required, and which accept a `doc` (ADF) shape
|
|
8
|
+
* versus a plain scalar. `validateAgainstCreatemeta` runs before any issue
|
|
9
|
+
* create/comment submit so a bad project key, unknown issue type, missing
|
|
10
|
+
* required field, or wrong ADF/plain-text shape surfaces as a client-side
|
|
11
|
+
* error with an actionable message instead of a raw 400 from Jira after the
|
|
12
|
+
* request has already gone out.
|
|
13
|
+
*
|
|
14
|
+
* Providers inject a `fetchCreatemeta(projectKey, issueTypeName)` function
|
|
15
|
+
* so this module has no transport dependency and the fake-jira test double
|
|
16
|
+
* can simulate arbitrary createmeta shapes and failure modes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { isAdfDocument } from './adf.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a raw createmeta API response into a lookup map of
|
|
23
|
+
* `{ [fieldKey]: { required, schema } }` for the requested issue type.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} raw - createmeta response body
|
|
26
|
+
* @param {string} projectKey
|
|
27
|
+
* @param {string} issueTypeName
|
|
28
|
+
* @returns {{ issueTypeId: string|null, fields: Record<string, {required: boolean, schema: object}> } | null}
|
|
29
|
+
*/
|
|
30
|
+
export function extractIssueTypeMeta(raw, projectKey, issueTypeName) {
|
|
31
|
+
const project = (raw?.projects ?? []).find((p) => p.key === projectKey);
|
|
32
|
+
if (!project) return null;
|
|
33
|
+
|
|
34
|
+
const issueType = (project.issuetypes ?? []).find(
|
|
35
|
+
(it) => it.name?.toLowerCase() === issueTypeName?.toLowerCase(),
|
|
36
|
+
);
|
|
37
|
+
if (!issueType) return null;
|
|
38
|
+
|
|
39
|
+
const fields = {};
|
|
40
|
+
for (const [key, def] of Object.entries(issueType.fields ?? {})) {
|
|
41
|
+
fields[key] = { required: !!def.required, schema: def.schema ?? {} };
|
|
42
|
+
}
|
|
43
|
+
return { issueTypeId: issueType.id ?? null, fields };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate an issue-create payload against createmeta for its project +
|
|
48
|
+
* issue type before submit. Returns a structured result rather than
|
|
49
|
+
* throwing, so callers can map failures to actionable messages themselves.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} opts
|
|
52
|
+
* @param {function(string, string): Promise<object>} opts.fetchCreatemeta
|
|
53
|
+
* @param {string} opts.projectKey
|
|
54
|
+
* @param {string} opts.issueTypeName
|
|
55
|
+
* @param {object} opts.fieldValues - candidate field values keyed by createmeta field key
|
|
56
|
+
* (e.g. { summary, description, labels }). `description` is checked for
|
|
57
|
+
* ADF shape when createmeta reports its schema type as `doc`.
|
|
58
|
+
* @returns {Promise<{ ok: true } | { ok: false, reason: string, errors: string[] }>}
|
|
59
|
+
*/
|
|
60
|
+
export async function validateAgainstCreatemeta({ fetchCreatemeta, projectKey, issueTypeName, fieldValues }) {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = await fetchCreatemeta(projectKey, issueTypeName);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
reason: 'createmeta-fetch-failed',
|
|
68
|
+
errors: [mapCreatemetaTransportError(err, { projectKey })],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const meta = extractIssueTypeMeta(raw, projectKey, issueTypeName);
|
|
73
|
+
if (!meta) {
|
|
74
|
+
const knownProjects = (raw?.projects ?? []).map((p) => p.key);
|
|
75
|
+
if (!knownProjects.includes(projectKey)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: 'unknown-project',
|
|
79
|
+
errors: [
|
|
80
|
+
`Project "${projectKey}" was not found or you lack the Browse Projects / Create Issues permission for it.` +
|
|
81
|
+
(knownProjects.length ? ` Accessible projects: ${knownProjects.join(', ')}.` : ''),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: 'unknown-issue-type',
|
|
88
|
+
errors: [`Issue type "${issueTypeName}" is not available in project "${projectKey}".`],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const errors = [];
|
|
93
|
+
|
|
94
|
+
for (const [fieldKey, def] of Object.entries(meta.fields)) {
|
|
95
|
+
if (!def.required) continue;
|
|
96
|
+
const present = fieldValues[fieldKey] !== undefined && fieldValues[fieldKey] !== null && fieldValues[fieldKey] !== '';
|
|
97
|
+
if (!present) {
|
|
98
|
+
errors.push(`Field "${fieldKey}" is required by project "${projectKey}" / issue type "${issueTypeName}" but was not provided.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const [fieldKey, value] of Object.entries(fieldValues)) {
|
|
103
|
+
const def = meta.fields[fieldKey];
|
|
104
|
+
if (!def) continue;
|
|
105
|
+
if (def.schema?.type === 'doc' && value !== undefined && value !== null && !isAdfDocument(value)) {
|
|
106
|
+
errors.push(`Field "${fieldKey}" requires an Atlassian Document Format (ADF) body, but a plain value was submitted.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (errors.length) {
|
|
111
|
+
return { ok: false, reason: 'field-validation-failed', errors };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ok: true, issueTypeId: meta.issueTypeId };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Map a createmeta transport failure (401/403/404/5xx) to an actionable
|
|
119
|
+
* message. Jira Cloud returns 404 for both "project does not exist" and
|
|
120
|
+
* "no permission to view project" — the same ambiguity the create-issue
|
|
121
|
+
* endpoint itself has — so the message names both possibilities rather
|
|
122
|
+
* than asserting one.
|
|
123
|
+
*
|
|
124
|
+
* @param {Error & {status?: number}} err
|
|
125
|
+
* @param {{ projectKey: string }} ctx
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function mapCreatemetaTransportError(err, { projectKey }) {
|
|
129
|
+
const status = err?.status;
|
|
130
|
+
if (status === 401) {
|
|
131
|
+
return 'Jira authentication failed while fetching createmeta. Check JIRA_EMAIL / JIRA_TOKEN.';
|
|
132
|
+
}
|
|
133
|
+
if (status === 403) {
|
|
134
|
+
return `Forbidden: the authenticated account lacks permission to create issues in project "${projectKey}".`;
|
|
135
|
+
}
|
|
136
|
+
if (status === 404) {
|
|
137
|
+
return `Project "${projectKey}" was not found, or the authenticated account cannot browse it.`;
|
|
138
|
+
}
|
|
139
|
+
if (status && status >= 500) {
|
|
140
|
+
return `Jira createmeta endpoint returned a server error (${status}); retry later.`;
|
|
141
|
+
}
|
|
142
|
+
return `Failed to fetch createmeta for project "${projectKey}": ${err?.message ?? String(err)}`;
|
|
143
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/contract/adapters/jira/governed-write.mjs — envelope-shaped
|
|
3
|
+
* Jira write adapter.
|
|
4
|
+
*
|
|
5
|
+
* Wraps a Jira REST transport in the provider shape lib/writes/envelope.mjs
|
|
6
|
+
* expects: write(config, payload), meta.id, search(). This is the only
|
|
7
|
+
* exposed entry point for Jira writes — specialists and CLI callers route
|
|
8
|
+
* through writeWithEnvelope(), never through a raw Jira client, so dedup,
|
|
9
|
+
* retry, dry-run, approval, and audit stay centralized in the envelope
|
|
10
|
+
* (LMCP-J2) rather than being reimplemented per-adapter.
|
|
11
|
+
*
|
|
12
|
+
* Adds the two behaviors createmeta + ADF require that a generic envelope
|
|
13
|
+
* cannot provide:
|
|
14
|
+
*
|
|
15
|
+
* - createmeta validation before submit: fetches
|
|
16
|
+
* `issue/createmeta?projectKeys=X&issuetypeNames=Y` (via the injected
|
|
17
|
+
* `jiraTransport`) and checks required fields and ADF-vs-plain field
|
|
18
|
+
* shape before making the create/comment call, so a bad project key,
|
|
19
|
+
* unknown issue type, missing required field, or wrong field shape
|
|
20
|
+
* surfaces as an actionable client-side error rather than a raw Jira
|
|
21
|
+
* 400.
|
|
22
|
+
* - ADF body construction: `description` (issue create) and `body`
|
|
23
|
+
* (comment) are converted from plain text to Atlassian Document Format
|
|
24
|
+
* via lib/providers/contract/adapters/jira/adf.mjs before submit.
|
|
25
|
+
*
|
|
26
|
+
* Only `type: 'issue'` and `type: 'comment'` writes get createmeta + ADF
|
|
27
|
+
* treatment; other write types are rejected — this adapter does not
|
|
28
|
+
* support transitions or field updates (non-goal per LMCP-J3).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { AuthError } from '../../errors.mjs';
|
|
32
|
+
import { buildAdfDocument, renderAdfPreview } from './adf.mjs';
|
|
33
|
+
import { validateAgainstCreatemeta } from './createmeta.mjs';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} opts
|
|
37
|
+
* @param {object} opts.jiraTransport - underlying Jira REST transport; must implement
|
|
38
|
+
* `fetchCreatemeta(projectKey, issueTypeName)`, `createIssue(fields)`,
|
|
39
|
+
* `createComment(issueKey, adfBody)`, and `searchIssues(jql)`.
|
|
40
|
+
*/
|
|
41
|
+
export function createGovernedJiraProvider({ jiraTransport } = {}) {
|
|
42
|
+
if (!jiraTransport) throw new Error('createGovernedJiraProvider: jiraTransport is required');
|
|
43
|
+
|
|
44
|
+
async function writeIssue(payload) {
|
|
45
|
+
const { project, issueType = 'Task', summary, description, labels, assignee } = payload;
|
|
46
|
+
if (!project) throw new Error('jira governed write: payload.project is required for type "issue"');
|
|
47
|
+
if (!summary) throw new Error('jira governed write: payload.summary is required for type "issue"');
|
|
48
|
+
|
|
49
|
+
const descriptionAdf = description !== undefined ? buildAdfDocument(description) : undefined;
|
|
50
|
+
|
|
51
|
+
const validation = await validateAgainstCreatemeta({
|
|
52
|
+
fetchCreatemeta: jiraTransport.fetchCreatemeta.bind(jiraTransport),
|
|
53
|
+
projectKey: project,
|
|
54
|
+
issueTypeName: issueType,
|
|
55
|
+
fieldValues: {
|
|
56
|
+
summary,
|
|
57
|
+
description: descriptionAdf,
|
|
58
|
+
labels,
|
|
59
|
+
assignee,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!validation.ok) {
|
|
64
|
+
const err = new Error(validation.errors.join(' '));
|
|
65
|
+
err.code = validation.reason;
|
|
66
|
+
err.jiraValidation = validation;
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = await jiraTransport.createIssue({
|
|
72
|
+
project,
|
|
73
|
+
issueType,
|
|
74
|
+
summary,
|
|
75
|
+
description: descriptionAdf,
|
|
76
|
+
labels,
|
|
77
|
+
assignee,
|
|
78
|
+
});
|
|
79
|
+
return { type: 'issue-created', id: result.id, key: result.key, url: result.url };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw mapWriteTransportError(err, { project });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function writeComment(payload) {
|
|
86
|
+
const { issueKey, body } = payload;
|
|
87
|
+
if (!issueKey) throw new Error('jira governed write: payload.issueKey is required for type "comment"');
|
|
88
|
+
if (!body) throw new Error('jira governed write: payload.body is required for type "comment"');
|
|
89
|
+
|
|
90
|
+
const bodyAdf = buildAdfDocument(body);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const result = await jiraTransport.createComment(issueKey, bodyAdf);
|
|
94
|
+
return { type: 'comment-created', id: result.id, issueKey, url: result.url };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
throw mapWriteTransportError(err, { issueKey });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
meta: {
|
|
102
|
+
id: 'jira',
|
|
103
|
+
displayName: 'Jira (governed)',
|
|
104
|
+
capabilities: ['write', 'search'],
|
|
105
|
+
description: 'Envelope-routed Jira writes with createmeta pre-validation and ADF body construction.',
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async write(config, payload) {
|
|
109
|
+
if (payload?.type === 'issue') return writeIssue(payload);
|
|
110
|
+
if (payload?.type === 'comment') return writeComment(payload);
|
|
111
|
+
throw new Error(`jira governed write: unsupported type "${payload?.type}" (only 'issue' and 'comment' are supported)`);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async search(config, query) {
|
|
115
|
+
return jiraTransport.searchIssues(query);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Render the ADF-shaped payload the envelope would submit, without
|
|
120
|
+
* calling createmeta or the transport. Feeds the envelope's dry-run
|
|
121
|
+
* path (lib/writes/envelope.mjs `dryRun: true`), giving a human
|
|
122
|
+
* reviewing a pending write both the raw ADF tree and a flattened
|
|
123
|
+
* preview string, not just the plain-text input.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} payload
|
|
126
|
+
* @returns {{ type: string, fields: object, adfPreview: Record<string,string> }}
|
|
127
|
+
*/
|
|
128
|
+
renderDryRun(payload) {
|
|
129
|
+
if (payload?.type === 'issue') {
|
|
130
|
+
const descriptionAdf = payload.description !== undefined ? buildAdfDocument(payload.description) : undefined;
|
|
131
|
+
return {
|
|
132
|
+
type: 'issue',
|
|
133
|
+
fields: {
|
|
134
|
+
project: { key: payload.project },
|
|
135
|
+
issuetype: { name: payload.issueType ?? 'Task' },
|
|
136
|
+
summary: payload.summary,
|
|
137
|
+
...(descriptionAdf ? { description: descriptionAdf } : {}),
|
|
138
|
+
...(payload.labels ? { labels: payload.labels } : {}),
|
|
139
|
+
...(payload.assignee ? { assignee: payload.assignee } : {}),
|
|
140
|
+
},
|
|
141
|
+
adfPreview: descriptionAdf ? { description: renderAdfPreview(descriptionAdf) } : {},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (payload?.type === 'comment') {
|
|
145
|
+
const bodyAdf = buildAdfDocument(payload.body);
|
|
146
|
+
return {
|
|
147
|
+
type: 'comment',
|
|
148
|
+
fields: { issueKey: payload.issueKey, body: bodyAdf },
|
|
149
|
+
adfPreview: { body: renderAdfPreview(bodyAdf) },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`jira governed write: cannot render dry-run for unsupported type "${payload?.type}"`);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Map a create/comment transport failure (401/403/404) to an actionable
|
|
159
|
+
* message. Distinct from mapCreatemetaTransportError since it covers the
|
|
160
|
+
* write call itself, not the pre-flight createmeta fetch.
|
|
161
|
+
*
|
|
162
|
+
* @param {Error & {status?: number}} err
|
|
163
|
+
* @param {{ project?: string, issueKey?: string }} ctx
|
|
164
|
+
* @returns {Error}
|
|
165
|
+
*/
|
|
166
|
+
function mapWriteTransportError(err, ctx) {
|
|
167
|
+
const status = err?.status;
|
|
168
|
+
if (status === 401) {
|
|
169
|
+
return new AuthError('Jira authentication failed. Check JIRA_EMAIL / JIRA_TOKEN.', { provider: 'jira' });
|
|
170
|
+
}
|
|
171
|
+
if (status === 403) {
|
|
172
|
+
const target = ctx.project ? `project "${ctx.project}"` : `issue "${ctx.issueKey}"`;
|
|
173
|
+
return new Error(`Forbidden: the authenticated account lacks permission to write to ${target}.`);
|
|
174
|
+
}
|
|
175
|
+
if (status === 404) {
|
|
176
|
+
const target = ctx.project ? `Project "${ctx.project}"` : `Issue "${ctx.issueKey}"`;
|
|
177
|
+
return new Error(`${target} was not found, or the authenticated account cannot access it.`);
|
|
178
|
+
}
|
|
179
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default createGovernedJiraProvider;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "jira-write",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"kind": "data-source",
|
|
5
|
+
"capabilities": ["write", "search"],
|
|
6
|
+
"secretEnvKeys": ["JIRA_URL", "JIRA_EMAIL", "JIRA_TOKEN"],
|
|
7
|
+
"operations": ["write:issue", "write:comment", "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/jira/governed-write.mjs",
|
|
15
|
+
"transportModule": "lib/providers/contract/adapters/jira/transport.mjs",
|
|
16
|
+
"notes": "Colocated write-capability manifest for LMCP-J3. Follows the lib/extensions/manifests/*.manifest.json schema (see atlassian-jira.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
|
+
}
|