@geraldmaron/construct 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +57 -45
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +273 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +104 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/team/health.mjs — team-mode queue and worker health read model.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { createSqlClient, closeSqlClient } from '../storage/backend.mjs';
|
|
8
|
+
import { PostgresIntakeQueue } from '../queue/pg-queue.mjs';
|
|
9
|
+
import { WorkerRegistry } from '../orchestration/worker-runtime.mjs';
|
|
10
|
+
import { resolveIntakeTenantId, INTAKE_PROJECT_ENV_KEY, INTAKE_QUEUE_NAME_ENV_KEY } from '../intake/queue.mjs';
|
|
11
|
+
|
|
12
|
+
function resolveProject(rootDir, env = process.env) {
|
|
13
|
+
const explicit = env?.[INTAKE_PROJECT_ENV_KEY];
|
|
14
|
+
if (explicit && String(explicit).trim()) return String(explicit).trim();
|
|
15
|
+
return path.basename(path.resolve(rootDir || process.cwd())).trim() || 'construct';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unavailable(reason) {
|
|
19
|
+
return {
|
|
20
|
+
status: 'unavailable',
|
|
21
|
+
reason,
|
|
22
|
+
queue: null,
|
|
23
|
+
workers: [],
|
|
24
|
+
summary: reason === 'postgres-unavailable'
|
|
25
|
+
? 'Postgres unavailable; team queue/worker health cannot be read'
|
|
26
|
+
: reason,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function summarizeTeamHealth({
|
|
31
|
+
rootDir = process.cwd(),
|
|
32
|
+
env = process.env,
|
|
33
|
+
sql = null,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const ownSql = sql || createSqlClient(env);
|
|
36
|
+
if (!ownSql) return unavailable('postgres-unavailable');
|
|
37
|
+
|
|
38
|
+
const project = resolveProject(rootDir, env);
|
|
39
|
+
const tenantId = resolveIntakeTenantId(env);
|
|
40
|
+
const queueName = env?.[INTAKE_QUEUE_NAME_ENV_KEY] || 'intake';
|
|
41
|
+
try {
|
|
42
|
+
const queue = new PostgresIntakeQueue({ sql: ownSql, project, tenantId, queueName });
|
|
43
|
+
const registry = new WorkerRegistry({ sql: ownSql, project, tenantId });
|
|
44
|
+
const [queueStats, workers] = await Promise.all([
|
|
45
|
+
queue.queueStats(),
|
|
46
|
+
registry.list({ includeStale: true }),
|
|
47
|
+
]);
|
|
48
|
+
const staleWorkers = workers.filter((w) => w.stale || w.status === 'stale').length;
|
|
49
|
+
const activeWorkers = workers.filter((w) => w.status === 'active' && !w.stale).length;
|
|
50
|
+
const status = queueStats.deadLetter > 0 || staleWorkers > 0
|
|
51
|
+
? 'degraded'
|
|
52
|
+
: 'healthy';
|
|
53
|
+
return {
|
|
54
|
+
status,
|
|
55
|
+
project,
|
|
56
|
+
tenantId,
|
|
57
|
+
queue: queueStats,
|
|
58
|
+
workers,
|
|
59
|
+
activeWorkers,
|
|
60
|
+
staleWorkers,
|
|
61
|
+
deadLetter: queueStats.deadLetter,
|
|
62
|
+
summary: `${queueStats.pending} pending · ${queueStats.claimed} claimed · ${queueStats.deadLetter} dead-letter · ${activeWorkers} active workers · ${staleWorkers} stale workers`,
|
|
63
|
+
};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {
|
|
66
|
+
...unavailable('team-health-read-failed'),
|
|
67
|
+
error: err?.message || String(err),
|
|
68
|
+
};
|
|
69
|
+
} finally {
|
|
70
|
+
if (!sql) await closeSqlClient(ownSql);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/telemetry/backends/local.mjs — Local JSONL trace backend.
|
|
3
3
|
*
|
|
4
|
-
* Reads trace events from
|
|
4
|
+
* Reads trace events from the calling project's machine-scoped state root
|
|
5
|
+
* (ADR-0066: `<stateRoot>/traces/*.jsonl`, keyed off cwd — the same location
|
|
6
|
+
* lib/worker/trace.mjs and lib/telemetry/client.mjs write to).
|
|
5
7
|
* No remote calls. Always available. Used when CONSTRUCT_TRACE_BACKEND=local.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
9
11
|
import { join } from 'node:path';
|
|
10
12
|
|
|
13
|
+
import { resolveStateDir } from '../../state-root.mjs';
|
|
14
|
+
|
|
11
15
|
export const name = 'local';
|
|
12
16
|
|
|
13
|
-
function tracesDir(
|
|
14
|
-
|
|
17
|
+
function tracesDir() {
|
|
18
|
+
// ensureDir:false — listTraces is read-only and already guards on
|
|
19
|
+
// existsSync(); it must not conjure a traces/ dir just by being called.
|
|
20
|
+
return resolveStateDir(process.cwd(), 'traces', { ensureDir: false });
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export async function isAvailable() {
|
package/lib/telemetry/client.mjs
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Local JSONL trace capture is the default. Remote export is opt-in through
|
|
5
5
|
* CONSTRUCT_TRACE_BACKEND and never throws into callers.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { appendFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
|
|
11
11
|
import { createIngestClient } from './ingest.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { resolveStateDir } from '../state-root.mjs';
|
|
13
13
|
|
|
14
14
|
const DEFAULT_MAX_BATCH = 50;
|
|
15
15
|
|
|
@@ -53,12 +53,8 @@ function traceShard() {
|
|
|
53
53
|
function appendLocalTelemetry(type, body, { rootDir, env = process.env, onError = () => {} } = {}) {
|
|
54
54
|
if (!localTraceEnabled(env)) return;
|
|
55
55
|
const projectRoot = resolveProjectRoot({ rootDir, env });
|
|
56
|
-
const dir =
|
|
56
|
+
const dir = resolveStateDir(projectRoot, 'traces');
|
|
57
57
|
try {
|
|
58
|
-
if (!existsSync(dir)) {
|
|
59
|
-
ensureCxDir(projectRoot);
|
|
60
|
-
mkdirSync(dir, { recursive: true });
|
|
61
|
-
}
|
|
62
58
|
appendFileSync(join(dir, `${traceShard()}.jsonl`), `${JSON.stringify({
|
|
63
59
|
traceId: body?.traceId || body?.id || randomUUID(),
|
|
64
60
|
spanId: body?.id || randomUUID(),
|
|
@@ -16,26 +16,24 @@
|
|
|
16
16
|
* shipped name.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// construct-rf26.11 folded 15 of the 29 legacy specialists into 11 surviving
|
|
20
|
+
// worker anchors. Each folded specialist's template ownership below moved to
|
|
21
|
+
// its anchor's entry (devil-advocate/evaluator/trace-reviewer -> reviewer;
|
|
22
|
+
// legal-compliance -> security; sre -> operations; accessibility -> designer)
|
|
23
|
+
// rather than being dropped, so the reverse-coverage gate still pins these
|
|
24
|
+
// artifacts to a real, callable get_template() reference in the anchor's prompt.
|
|
19
25
|
export const SPECIALIST_TEMPLATES = {
|
|
20
|
-
|
|
21
|
-
'cx-
|
|
22
|
-
'cx-security': ['security-audit-report'],
|
|
26
|
+
'cx-reviewer': ['code-review-report', 'verdict'],
|
|
27
|
+
'cx-security': ['security-audit-report', 'verdict'],
|
|
23
28
|
'cx-qa': ['qa-report', 'test-plan', 'qa-strategy'],
|
|
24
29
|
'cx-debugger': ['debug-investigation'],
|
|
25
|
-
'cx-devil-advocate': ['verdict'],
|
|
26
|
-
'cx-evaluator': ['verdict'],
|
|
27
|
-
'cx-trace-reviewer': ['verdict'],
|
|
28
|
-
'cx-explorer': ['verdict'],
|
|
29
|
-
'cx-legal-compliance':['verdict'],
|
|
30
30
|
|
|
31
31
|
// Already-wired specialists pinned here so the gate guards them against drift.
|
|
32
32
|
'cx-architect': ['adr', 'rfc', 'architecture-review'],
|
|
33
33
|
'cx-researcher': ['research-brief', 'evidence-brief'],
|
|
34
|
-
'cx-
|
|
34
|
+
'cx-operations': ['runbook', 'incident-report', 'postmortem'],
|
|
35
35
|
'cx-product-manager': ['prd', 'meta-prd'],
|
|
36
|
-
|
|
37
|
-
// Tranche 2 — three lower-frequency artifacts.
|
|
38
|
-
'cx-accessibility': ['accessibility-audit'],
|
|
36
|
+
'cx-designer': ['accessibility-audit'],
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
// The orchestrator owns the task-packet shape and emits it at dispatch time
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tenant/context.mjs — tenant context resolution for the control plane.
|
|
3
|
+
*
|
|
4
|
+
* ADR-0057 (A7) IMPLEMENT-NOW: tenantId is data plumbing, not isolation.
|
|
5
|
+
* resolveTenantContext() is the single place a tenant id is derived from
|
|
6
|
+
* config + env and validated, so run/task/queue/audit records all carry the
|
|
7
|
+
* same value rather than each call site re-deriving it ad hoc. Solo and team
|
|
8
|
+
* modes default to the explicit tenant id 'local'. Enterprise mode has no
|
|
9
|
+
* default: a missing/blank tenant id is a fail-closed TenantResolutionError.
|
|
10
|
+
* No tenant isolation (worker/storage separation) is implied or attempted
|
|
11
|
+
* here — see LMCP-H4.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const TENANT_ID_ENV_KEY = 'CONSTRUCT_TENANT_ID';
|
|
15
|
+
export const DEFAULT_TENANT_ID = 'local';
|
|
16
|
+
|
|
17
|
+
// A resolvable tenant id is a non-empty, trimmed string. Enterprise mode
|
|
18
|
+
// treats anything else (missing, blank, whitespace-only) as unresolved.
|
|
19
|
+
|
|
20
|
+
function normalizeTenantId(value) {
|
|
21
|
+
if (typeof value !== 'string') return null;
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed ? trimmed : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TenantResolutionError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'TenantResolutionError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the tenant context once, from config + env, validated against the
|
|
35
|
+
* deployment mode. Returns `{ tenantId, source, mode }`.
|
|
36
|
+
*
|
|
37
|
+
* - solo/team: an explicit id from env/config wins; otherwise defaults to
|
|
38
|
+
* 'local' (source: 'default'). Never throws.
|
|
39
|
+
* - enterprise: an explicit id from env/config is required. A missing or
|
|
40
|
+
* blank id throws TenantResolutionError — enterprise mode fails closed at
|
|
41
|
+
* startup rather than silently running unlabeled multi-tenant traffic.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* @param {Record<string,string>} [opts.env]
|
|
45
|
+
* @param {object} [opts.config] loaded project config (reads deployment.tenantId)
|
|
46
|
+
* @param {string} [opts.mode] deployment mode; if omitted, callers must pass it
|
|
47
|
+
* explicitly — this module does not import deployment-mode.mjs to avoid a
|
|
48
|
+
* config-loading cycle (deployment-mode.mjs itself loads project config).
|
|
49
|
+
* @returns {{tenantId: string, source: 'env'|'config'|'default', mode: string}}
|
|
50
|
+
*/
|
|
51
|
+
export function resolveTenantContext({ env = process.env, config = null, mode = 'solo' } = {}) {
|
|
52
|
+
const fromEnv = normalizeTenantId(env?.[TENANT_ID_ENV_KEY]);
|
|
53
|
+
const fromConfig = !fromEnv ? normalizeTenantId(config?.deployment?.tenantId) : null;
|
|
54
|
+
const resolved = fromEnv || fromConfig;
|
|
55
|
+
|
|
56
|
+
if (resolved) {
|
|
57
|
+
return { tenantId: resolved, source: fromEnv ? 'env' : 'config', mode };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (mode === 'enterprise') {
|
|
61
|
+
throw new TenantResolutionError(
|
|
62
|
+
`enterprise mode requires a resolvable tenant id; set ${TENANT_ID_ENV_KEY} or deployment.tenantId in construct.config.json. ` +
|
|
63
|
+
'Enterprise mode cannot start without a tenant — see ADR-0057 (A7).',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { tenantId: DEFAULT_TENANT_ID, source: 'default', mode };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fail-closed startup guard: call once at process/runtime start for enterprise
|
|
72
|
+
* mode. Re-throws TenantResolutionError with the same actionable message;
|
|
73
|
+
* solo/team never throw here since resolveTenantContext always resolves for
|
|
74
|
+
* them. Exposed separately so startup call sites can guard without needing
|
|
75
|
+
* the resolved value.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} [opts] same shape as resolveTenantContext
|
|
78
|
+
* @returns {{tenantId: string, source: string, mode: string}}
|
|
79
|
+
*/
|
|
80
|
+
export function requireTenantContext(opts = {}) {
|
|
81
|
+
return resolveTenantContext(opts);
|
|
82
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tenant/isolation.mjs — cross-tenant read guard for the control plane (LMCP-H5).
|
|
3
|
+
*
|
|
4
|
+
* lib/tenant/context.mjs (H1) resolves a tenant id; lib/identity.mjs (H2) resolves
|
|
5
|
+
* an actor; I3 landed durable audit/denied records that carry both. None of that
|
|
6
|
+
* enforces a boundary — a record tagged tenant "acme" is still just a JSON line a
|
|
7
|
+
* caller with tenant "other" can read if nothing stops them. Physical per-tenant
|
|
8
|
+
* storage backends are LMCP-H4 (not yet landed; queue/store today are scoped by
|
|
9
|
+
* rootDir, not tenant). Until H4 lands, this module is the enforcement layer that
|
|
10
|
+
* makes the tenant tag load-bearing everywhere it already exists:
|
|
11
|
+
*
|
|
12
|
+
* - assertTenantMatch(recordTenant, contextTenant) — throws TenantIsolationViolation
|
|
13
|
+
* (fail-closed) on any mismatch, and on a missing/blank tenant on either side.
|
|
14
|
+
* - scopeToTenant(records, tenantId) — filters a record list down to exactly the
|
|
15
|
+
* records whose `tenant`/`tenantId` field matches, fail-closed (throws) if
|
|
16
|
+
* tenantId itself is unresolved rather than silently returning everything.
|
|
17
|
+
* - readTenantScoped(readAll, tenantId) — composes a raw "read everything" API
|
|
18
|
+
* (queue.listPending, DeniedStore.readAll, readAuditTrail, a store's list*) with
|
|
19
|
+
* scopeToTenant so a caller gets a tenant-safe view over today's rootDir-scoped
|
|
20
|
+
* backends without waiting on H4's physical isolation.
|
|
21
|
+
*
|
|
22
|
+
* Scope: no physical isolation (no per-tenant encryption, no separate
|
|
23
|
+
* database/schema) — see H4 for that. The narrower, verifiable guarantee: any
|
|
24
|
+
* record path that stamps a tenant field cannot be read across a tenant boundary
|
|
25
|
+
* through these accessors, and is fail-closed (throws) rather than fail-open when
|
|
26
|
+
* the tenant dimension is missing or unresolved on either side.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export class TenantIsolationViolation extends Error {
|
|
30
|
+
constructor(message, { recordTenant, contextTenant } = {}) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'TenantIsolationViolation';
|
|
33
|
+
this.recordTenant = recordTenant ?? null;
|
|
34
|
+
this.contextTenant = contextTenant ?? null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalize(value) {
|
|
39
|
+
if (typeof value !== 'string') return null;
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
return trimmed ? trimmed : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fail-closed equality check between a record's tenant and the requesting
|
|
46
|
+
* context's tenant. Throws TenantIsolationViolation on mismatch, and also on
|
|
47
|
+
* either side being missing/blank — an unresolved tenant is never treated as
|
|
48
|
+
* a wildcard that matches everything.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} recordTenant
|
|
51
|
+
* @param {string} contextTenant
|
|
52
|
+
*/
|
|
53
|
+
export function assertTenantMatch(recordTenant, contextTenant) {
|
|
54
|
+
const rec = normalize(recordTenant);
|
|
55
|
+
const ctx = normalize(contextTenant);
|
|
56
|
+
|
|
57
|
+
if (!ctx) {
|
|
58
|
+
throw new TenantIsolationViolation(
|
|
59
|
+
'tenant isolation: requesting context has no resolvable tenant id — refusing read fail-closed',
|
|
60
|
+
{ recordTenant: rec, contextTenant: ctx },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!rec) {
|
|
64
|
+
throw new TenantIsolationViolation(
|
|
65
|
+
'tenant isolation: record has no resolvable tenant id — refusing read fail-closed',
|
|
66
|
+
{ recordTenant: rec, contextTenant: ctx },
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (rec !== ctx) {
|
|
70
|
+
throw new TenantIsolationViolation(
|
|
71
|
+
`tenant isolation violation: record tenant "${rec}" is not readable from tenant "${ctx}" context`,
|
|
72
|
+
{ recordTenant: rec, contextTenant: ctx },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Records observed across queue/audit/denied-store use either `tenant` (I3
|
|
78
|
+
// decision-record schema: audit-trail, denied-store) or `tenantId` (H1 queue
|
|
79
|
+
// entries, orchestration run/task records) — checked in that order so one
|
|
80
|
+
// helper covers both shapes without callers needing to know which field name
|
|
81
|
+
// their backend uses.
|
|
82
|
+
|
|
83
|
+
function tenantFieldOf(record) {
|
|
84
|
+
if (record && typeof record === 'object') {
|
|
85
|
+
if ('tenant' in record) return record.tenant;
|
|
86
|
+
if ('tenantId' in record) return record.tenantId;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Filter a record list down to exactly the records belonging to tenantId.
|
|
93
|
+
* Fail-closed: throws TenantIsolationViolation if tenantId itself is
|
|
94
|
+
* unresolved (missing/blank) rather than returning the unfiltered list —
|
|
95
|
+
* an isolation filter that no-ops on missing context is not a filter.
|
|
96
|
+
* Records with no resolvable tenant field are dropped, not leaked through.
|
|
97
|
+
*
|
|
98
|
+
* @param {object[]} records
|
|
99
|
+
* @param {string} tenantId
|
|
100
|
+
* @returns {object[]}
|
|
101
|
+
*/
|
|
102
|
+
export function scopeToTenant(records, tenantId) {
|
|
103
|
+
const ctx = normalize(tenantId);
|
|
104
|
+
if (!ctx) {
|
|
105
|
+
throw new TenantIsolationViolation(
|
|
106
|
+
'tenant isolation: scopeToTenant called with no resolvable tenant id — refusing to read any records',
|
|
107
|
+
{ recordTenant: null, contextTenant: null },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return (records || []).filter((record) => normalize(tenantFieldOf(record)) === ctx);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compose a raw "read everything" accessor with scopeToTenant, so a caller
|
|
115
|
+
* gets a tenant-safe view over a backend that has no native tenant filter
|
|
116
|
+
* (today's rootDir-scoped queue/store/audit readers). `readAll` may be sync
|
|
117
|
+
* or return a promise; the tenant filter is applied to whatever it resolves.
|
|
118
|
+
*
|
|
119
|
+
* @param {function(): (object[]|Promise<object[]>)} readAll
|
|
120
|
+
* @param {string} tenantId
|
|
121
|
+
* @returns {object[]|Promise<object[]>}
|
|
122
|
+
*/
|
|
123
|
+
export function readTenantScoped(readAll, tenantId) {
|
|
124
|
+
const result = readAll();
|
|
125
|
+
if (result && typeof result.then === 'function') {
|
|
126
|
+
return result.then((records) => scopeToTenant(records, tenantId));
|
|
127
|
+
}
|
|
128
|
+
return scopeToTenant(result, tenantId);
|
|
129
|
+
}
|
|
@@ -80,6 +80,7 @@ export function defaultCorpusInventoryPath(rootDir = process.cwd()) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function listTestFiles(testsDir) {
|
|
83
|
+
if (!fs.existsSync(testsDir)) return [];
|
|
83
84
|
const files = [];
|
|
84
85
|
const walk = (dir) => {
|
|
85
86
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -140,6 +141,101 @@ function extractCapabilities(content) {
|
|
|
140
141
|
return [...content.matchAll(/@capability\s+([a-z][a-z0-9]*(?:[.-][a-z0-9]+)*)/g)].map((match) => match[1]);
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
// `@embed <manifest-id>` is the embed-node analog of `@capability` (LMCP-P6):
|
|
145
|
+
// an acceptance test declares which embed preset it validates, so the graph can
|
|
146
|
+
// draw a test --validates--> embed edge without the id colliding with the
|
|
147
|
+
// capability id-space walked by extractCapabilities above.
|
|
148
|
+
|
|
149
|
+
function extractEmbedIds(content) {
|
|
150
|
+
return [...content.matchAll(/@embed\s+([a-z][a-z0-9]*(?:[.-][a-z0-9]+)*)/g)].map((match) => match[1]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Graph builders (lib/graph/build-from-corpus.mjs) need the raw path→capability
|
|
154
|
+
// pairs, not the full classified inventory, so this stays a thin extraction step
|
|
155
|
+
// callers can run over the live tree without regenerating the whole inventory.
|
|
156
|
+
|
|
157
|
+
export function extractCapabilityTestEdges({ rootDir } = {}) {
|
|
158
|
+
const root = findConstructRoot(rootDir);
|
|
159
|
+
const testsDir = path.join(root, 'tests');
|
|
160
|
+
const edges = [];
|
|
161
|
+
for (const absolute of listTestFiles(testsDir)) {
|
|
162
|
+
const rel = path.relative(root, absolute).replace(/\\/g, '/');
|
|
163
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
164
|
+
for (const capabilityId of extractCapabilities(content)) edges.push({ testPath: rel, capabilityId });
|
|
165
|
+
}
|
|
166
|
+
return edges;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Raw path→embed-id pairs from `@embed` annotations, mirroring
|
|
171
|
+
* extractCapabilityTestEdges for the P6 embed-node graph seeder.
|
|
172
|
+
*
|
|
173
|
+
* @param {{ rootDir?: string }} [opts]
|
|
174
|
+
* @returns {Array<{ testPath: string, embedId: string }>}
|
|
175
|
+
*/
|
|
176
|
+
export function extractEmbedTestEdges({ rootDir } = {}) {
|
|
177
|
+
const root = findConstructRoot(rootDir);
|
|
178
|
+
const testsDir = path.join(root, 'tests');
|
|
179
|
+
if (!fs.existsSync(testsDir)) return [];
|
|
180
|
+
const edges = [];
|
|
181
|
+
for (const absolute of listTestFiles(testsDir)) {
|
|
182
|
+
const rel = path.relative(root, absolute).replace(/\\/g, '/');
|
|
183
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
184
|
+
for (const embedId of extractEmbedIds(content)) edges.push({ testPath: rel, embedId });
|
|
185
|
+
}
|
|
186
|
+
return edges;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// `@owasp LLM01[, LLM06]` tags a test with one or more OWASP GenAI Top-10
|
|
190
|
+
// category ids; `@secures <workflow-id>` names the workflow the test protects
|
|
191
|
+
// (LMCP-N8). Together they let the graph carry security coverage as
|
|
192
|
+
// test --secures--> workflow edges with the category recorded on the test node.
|
|
193
|
+
|
|
194
|
+
function extractOwaspCategories(content) {
|
|
195
|
+
const cats = new Set();
|
|
196
|
+
for (const m of content.matchAll(/@owasp\s+([A-Za-z0-9,\s]+)/g)) {
|
|
197
|
+
for (const raw of m[1].split(',')) {
|
|
198
|
+
const id = raw.trim().toUpperCase();
|
|
199
|
+
if (/^LLM(0[1-9]|10)$/.test(id)) cats.add(id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [...cats].sort();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function extractSecuredWorkflows(content) {
|
|
206
|
+
const ids = new Set();
|
|
207
|
+
for (const m of content.matchAll(/@secures\s+([a-z0-9,\s-]+)/g)) {
|
|
208
|
+
for (const raw of m[1].split(',')) {
|
|
209
|
+
const id = raw.trim();
|
|
210
|
+
if (/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(id)) ids.add(id);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return [...ids];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Security-test annotations for the N8 graph seeder: one entry per test that
|
|
218
|
+
* carries at least one `@owasp` tag, with its categories and any `@secures`
|
|
219
|
+
* workflow ids.
|
|
220
|
+
*
|
|
221
|
+
* @param {{ rootDir?: string }} [opts]
|
|
222
|
+
* @returns {Array<{ testPath: string, owasp: string[], secures: string[] }>}
|
|
223
|
+
*/
|
|
224
|
+
export function extractSecurityTestEdges({ rootDir } = {}) {
|
|
225
|
+
const root = findConstructRoot(rootDir);
|
|
226
|
+
const testsDir = path.join(root, 'tests');
|
|
227
|
+
if (!fs.existsSync(testsDir)) return [];
|
|
228
|
+
const entries = [];
|
|
229
|
+
for (const absolute of listTestFiles(testsDir)) {
|
|
230
|
+
const content = fs.readFileSync(absolute, 'utf8');
|
|
231
|
+
const owasp = extractOwaspCategories(content);
|
|
232
|
+
if (owasp.length === 0) continue;
|
|
233
|
+
const rel = path.relative(root, absolute).replace(/\\/g, '/');
|
|
234
|
+
entries.push({ testPath: rel, owasp, secures: extractSecuredWorkflows(content) });
|
|
235
|
+
}
|
|
236
|
+
return entries;
|
|
237
|
+
}
|
|
238
|
+
|
|
143
239
|
function detectSkipped(content) {
|
|
144
240
|
return /\btest\.skip\s*\(|\bit\.skip\s*\(|\bdescribe\.skip\s*\(/.test(content);
|
|
145
241
|
}
|
|
@@ -236,14 +332,20 @@ export function formatAuditAtAGlance(inventory) {
|
|
|
236
332
|
export function refreshAuditMarkdown(inventory, { rootDir } = {}) {
|
|
237
333
|
const root = findConstructRoot(rootDir);
|
|
238
334
|
const auditPath = path.join(root, 'tests', 'AUDIT.md');
|
|
239
|
-
|
|
335
|
+
let markdown = fs.readFileSync(auditPath, 'utf8');
|
|
240
336
|
const start = '## At a glance';
|
|
241
337
|
const end = '## Categories';
|
|
242
338
|
const startIdx = markdown.indexOf(start);
|
|
243
339
|
const endIdx = markdown.indexOf(end);
|
|
244
340
|
if (startIdx === -1 || endIdx === -1) throw new Error('tests/AUDIT.md missing At a glance section');
|
|
245
341
|
const replacement = `${start}\n\n${formatAuditAtAGlance(inventory)}\n\n`;
|
|
246
|
-
|
|
342
|
+
markdown = `${markdown.slice(0, startIdx)}${replacement}${markdown.slice(endIdx)}`;
|
|
343
|
+
const functional = inventory.summary.byCategory.functional ?? 0;
|
|
344
|
+
markdown = markdown.replace(
|
|
345
|
+
/### 5\. Functional layer \(\d+ tests, \d+ files\)/,
|
|
346
|
+
`### 5. Functional layer (${functional} tests, ${functional} files)`,
|
|
347
|
+
);
|
|
348
|
+
return markdown;
|
|
247
349
|
}
|
|
248
350
|
|
|
249
351
|
export function validateCorpusInventory({ rootDir, inventory: supplied } = {}) {
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* `core.hooksPath` are reversed because they are state install/init created and
|
|
21
21
|
* own; a user-customized hooksPath is left untouched.
|
|
22
22
|
*
|
|
23
|
+
* Since construct-ranh moved Claude Code's MCP write path off
|
|
24
|
+
* ~/.claude/settings.json (never read by the CLI for server definitions),
|
|
25
|
+
* MCP cleanup checks both the new locations (~/.claude.json for machine
|
|
26
|
+
* scope, <project>/.mcp.json for project scope) and the legacy
|
|
27
|
+
* settings.json path, so both pre- and post-fix installs are covered.
|
|
28
|
+
*
|
|
23
29
|
* Flags:
|
|
24
30
|
* --dry-run print the plan, change nothing
|
|
25
31
|
* --yes non-interactive; remove auto-risk items, skip ask-risk items
|
|
@@ -95,9 +101,11 @@ function buildCategories({ cwd, home, withImages = false }) {
|
|
|
95
101
|
const userCxDir = doctorRoot(userHome);
|
|
96
102
|
const launchAgentPlist = path.join(userHome, 'Library', 'LaunchAgents', `${PRESSURE_GUARD_LABEL}.plist`);
|
|
97
103
|
const claudeSettings = path.join(userHome, '.claude', 'settings.json');
|
|
104
|
+
const claudeUserConfig = path.join(userHome, '.claude.json');
|
|
98
105
|
const opencodeConfig = path.join(userHome, '.config', 'opencode', 'opencode.json');
|
|
99
106
|
const codexConfig = path.join(userHome, '.codex', 'config.toml');
|
|
100
|
-
const memoryConfigPaths = { claudeSettings, opencodeConfig, codexConfig };
|
|
107
|
+
const memoryConfigPaths = { claudeSettings, claudeUserConfig, opencodeConfig, codexConfig };
|
|
108
|
+
const dotMcpJson = path.join(cwd, '.mcp.json');
|
|
101
109
|
|
|
102
110
|
return [
|
|
103
111
|
{
|
|
@@ -143,10 +151,15 @@ function buildCategories({ cwd, home, withImages = false }) {
|
|
|
143
151
|
id: 'project-settings',
|
|
144
152
|
scope: 'project',
|
|
145
153
|
risk: 'auto',
|
|
146
|
-
label: '.claude/settings.json (un-merge Construct keys)',
|
|
147
|
-
detect: () => detectConstructSettings(dotClaudeSettings),
|
|
148
|
-
describe: () => 'Strips Construct hooks + known mcpServers from settings.json; preserves any user-added keys. Deletes
|
|
149
|
-
execute: () =>
|
|
154
|
+
label: '.claude/settings.json + .mcp.json (un-merge Construct keys)',
|
|
155
|
+
detect: () => detectConstructSettings(dotClaudeSettings) || detectConstructMcpJson(dotMcpJson),
|
|
156
|
+
describe: () => 'Strips Construct hooks + known mcpServers from settings.json (legacy) and Construct-managed servers from .mcp.json (construct-ranh scope); preserves any user-added keys. Deletes a file if it becomes empty.',
|
|
157
|
+
execute: () => {
|
|
158
|
+
const parts = [unmergeSettings(dotClaudeSettings)];
|
|
159
|
+
const mcpJsonResult = unmergeMcpJson(dotMcpJson);
|
|
160
|
+
if (mcpJsonResult) parts.push(mcpJsonResult);
|
|
161
|
+
return parts.join('; ');
|
|
162
|
+
},
|
|
150
163
|
},
|
|
151
164
|
{
|
|
152
165
|
id: 'project-cx',
|
|
@@ -263,7 +276,7 @@ function buildCategories({ cwd, home, withImages = false }) {
|
|
|
263
276
|
risk: 'auto',
|
|
264
277
|
label: 'memory MCP registration (Claude, OpenCode, Codex)',
|
|
265
278
|
detect: () => memoryMcpRegistered(memoryConfigPaths),
|
|
266
|
-
describe: () => 'Strips the Construct memory MCP bridge from ~/.claude/settings.json, ~/.config/opencode/opencode.json
|
|
279
|
+
describe: () => 'Strips the Construct memory MCP bridge from ~/.claude.json (construct-ranh scope) and ~/.claude/settings.json (legacy), plus ~/.config/opencode/opencode.json and ~/.codex/config.toml. Preserves all other entries.',
|
|
267
280
|
execute: () => removeMemoryMcp(memoryConfigPaths),
|
|
268
281
|
},
|
|
269
282
|
{
|
|
@@ -472,6 +485,54 @@ function constructMcpKeys() {
|
|
|
472
485
|
} catch { return []; }
|
|
473
486
|
}
|
|
474
487
|
|
|
488
|
+
// Since construct-ranh, `construct sync` writes project-scope MCP servers to
|
|
489
|
+
// .mcp.json rather than .claude/settings.json — mirroring the same
|
|
490
|
+
// core-category + construct-mcp allowlist scripts/sync-specialists.mjs's
|
|
491
|
+
// PROJECT_DEFAULT_MCP_IDS derives from lib/mcp-catalog.json, so uninstall
|
|
492
|
+
// strips exactly what sync would have written, never a user's own project-scope
|
|
493
|
+
// `claude mcp add` entries.
|
|
494
|
+
|
|
495
|
+
function projectManagedMcpIds() {
|
|
496
|
+
try {
|
|
497
|
+
const catalog = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'lib', 'mcp-catalog.json'), 'utf8'));
|
|
498
|
+
const arr = catalog.mcps || catalog.servers || [];
|
|
499
|
+
return new Set([...arr.filter((m) => m.category === 'core').map((m) => m.id), 'construct-mcp']);
|
|
500
|
+
} catch { return new Set(['context7', 'construct-mcp']); }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function detectConstructMcpJson(filePath) {
|
|
504
|
+
if (!fs.existsSync(filePath)) return false;
|
|
505
|
+
let config;
|
|
506
|
+
try { config = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
507
|
+
catch { return false; }
|
|
508
|
+
if (!config || typeof config !== 'object' || !config.mcpServers) return false;
|
|
509
|
+
const ids = projectManagedMcpIds();
|
|
510
|
+
return Object.keys(config.mcpServers).some((k) => ids.has(k));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function unmergeMcpJson(filePath) {
|
|
514
|
+
if (!fs.existsSync(filePath)) return null;
|
|
515
|
+
let config;
|
|
516
|
+
try { config = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
517
|
+
catch { return '.mcp.json was malformed; left untouched'; }
|
|
518
|
+
if (!config || typeof config !== 'object' || !config.mcpServers) return null;
|
|
519
|
+
|
|
520
|
+
let removedCount = 0;
|
|
521
|
+
for (const key of projectManagedMcpIds()) {
|
|
522
|
+
if (key in config.mcpServers) { delete config.mcpServers[key]; removedCount += 1; }
|
|
523
|
+
}
|
|
524
|
+
if (removedCount === 0) return null;
|
|
525
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
526
|
+
|
|
527
|
+
const remaining = Object.keys(config);
|
|
528
|
+
if (remaining.length === 0) {
|
|
529
|
+
fs.unlinkSync(filePath);
|
|
530
|
+
return `.mcp.json removed (file was Construct-only) [stripped ${removedCount} mcpServers]`;
|
|
531
|
+
}
|
|
532
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
533
|
+
return `.mcp.json stripped ${removedCount} mcpServers; preserved ${remaining.join(', ')}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
475
536
|
function dockerContainerExists(name) {
|
|
476
537
|
const probe = spawnSync('docker', ['ps', '-a', '--filter', `name=^/${name}$`, '--format', '{{.Names}}'], {
|
|
477
538
|
encoding: 'utf8',
|
|
@@ -545,8 +606,9 @@ function removePressureGuardLaunchAgent(plistPath) {
|
|
|
545
606
|
return removed ? 'unregistered and removed plist' : 'unregistered (plist already gone)';
|
|
546
607
|
}
|
|
547
608
|
|
|
548
|
-
function memoryMcpRegistered({ claudeSettings, opencodeConfig, codexConfig }) {
|
|
609
|
+
function memoryMcpRegistered({ claudeSettings, claudeUserConfig, opencodeConfig, codexConfig }) {
|
|
549
610
|
return claudeHasMemoryMcp(claudeSettings)
|
|
611
|
+
|| claudeHasMemoryMcp(claudeUserConfig)
|
|
550
612
|
|| opencodeHasMemoryMcp(opencodeConfig)
|
|
551
613
|
|| codexHasMemoryMcp(codexConfig);
|
|
552
614
|
}
|
|
@@ -569,13 +631,17 @@ function codexHasMemoryMcp(filePath) {
|
|
|
569
631
|
return MEMORY_MCP_KEYS.some((key) => text.includes(`[mcp_servers.${key}]`) || text.includes(`[mcp_servers.${tomlString(key)}]`));
|
|
570
632
|
}
|
|
571
633
|
|
|
572
|
-
// The memory MCP is registered into
|
|
573
|
-
// memory` during install; each host is stripped independently so a
|
|
574
|
-
// hand-edited config never blocks the others.
|
|
634
|
+
// The memory MCP is registered into Claude, OpenCode, and Codex by `construct
|
|
635
|
+
// mcp add memory` during install; each host is stripped independently so a
|
|
636
|
+
// missing or hand-edited config never blocks the others. Claude has two
|
|
637
|
+
// possible locations: ~/.claude.json is where construct-ranh moved the write
|
|
638
|
+
// path (Claude Code never reads MCP servers from settings.json), and
|
|
639
|
+
// ~/.claude/settings.json is stripped too for installs made before that fix.
|
|
575
640
|
|
|
576
|
-
function removeMemoryMcp({ claudeSettings, opencodeConfig, codexConfig }) {
|
|
641
|
+
function removeMemoryMcp({ claudeSettings, claudeUserConfig, opencodeConfig, codexConfig }) {
|
|
577
642
|
const stripped = [];
|
|
578
|
-
if (stripMemoryFromJsonMcp(claudeSettings, 'mcpServers')) stripped.push('claude');
|
|
643
|
+
if (stripMemoryFromJsonMcp(claudeSettings, 'mcpServers')) stripped.push('claude settings.json (legacy)');
|
|
644
|
+
if (stripMemoryFromJsonMcp(claudeUserConfig, 'mcpServers')) stripped.push('claude.json');
|
|
579
645
|
if (stripMemoryFromJsonMcp(opencodeConfig, 'mcp')) stripped.push('opencode');
|
|
580
646
|
if (stripMemoryFromCodex(codexConfig)) stripped.push('codex');
|
|
581
647
|
return stripped.length ? `stripped from ${stripped.join(', ')}` : 'nothing to strip';
|