@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
package/lib/intake/daemon.mjs
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* Packets past their TTL move to `.cx/intake/dead-letter/`; failed
|
|
7
7
|
* classification retries up to the budget then dead-letters with the reason.
|
|
8
8
|
*
|
|
9
|
+
* Every tick also sweeps the persisted `.cx/intake/pending/` packets
|
|
10
|
+
* (sweepPendingPackets): a packet whose `intake.sourcePath` is missing from
|
|
11
|
+
* disk, or whose age exceeds classifyPacket's TTL, moves to
|
|
12
|
+
* `.cx/intake/dead-letter/` with a `deadLetterReason`. This covers persisted
|
|
13
|
+
* packets, not just in-flight inbox files, so the TTL/dead-letter contract
|
|
14
|
+
* above applies to the whole pending queue.
|
|
15
|
+
*
|
|
9
16
|
* Atomic handoff (ADR-0045 §C): only complete top-level files are enqueued.
|
|
10
17
|
* Dotfiles and the `inbox/.staging/` assembly directory are skipped, so a file
|
|
11
18
|
* a writer is still staging is invisible until it is renamed into `inbox/`.
|
|
@@ -14,18 +21,19 @@
|
|
|
14
21
|
* idle shutdown, heartbeat, killswitch, single-writer lock) applies.
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
|
-
import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
|
|
18
25
|
import { join, basename } from 'node:path';
|
|
19
26
|
|
|
20
27
|
import { createDaemon, classifyPacket } from '../daemons/contract.mjs';
|
|
21
28
|
import { memoryCapMbFor } from '../resources/process-budget.mjs';
|
|
29
|
+
import { resolveStatePath } from '../state-root.mjs';
|
|
22
30
|
|
|
23
31
|
const KILLSWITCH_ENV = 'CONSTRUCT_INTAKE_DAEMON';
|
|
24
32
|
|
|
25
33
|
function inboxDir(cwd) { return join(cwd, 'inbox'); }
|
|
26
34
|
function pendingDir(cwd) { return join(cwd, '.cx', 'intake', 'pending'); }
|
|
27
35
|
function deadLetterDir(cwd) { return join(cwd, '.cx', 'intake', 'dead-letter'); }
|
|
28
|
-
function heartbeatPath(cwd) { return
|
|
36
|
+
function heartbeatPath(cwd) { return resolveStatePath(cwd, 'runtime', 'intake-daemon.heartbeat'); }
|
|
29
37
|
|
|
30
38
|
function ensureDir(dir) {
|
|
31
39
|
try { mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
@@ -60,9 +68,55 @@ function listInboxFiles(cwd) {
|
|
|
60
68
|
.filter((full) => isSizeStable(full));
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
function pendingPacketFiles(cwd) {
|
|
72
|
+
const dir = pendingDir(cwd);
|
|
73
|
+
if (!existsSync(dir)) return [];
|
|
74
|
+
try { return readdirSync(dir).filter((name) => name.endsWith('.json')); }
|
|
75
|
+
catch { return []; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readPacketFile(cwd, name) {
|
|
79
|
+
try { return JSON.parse(readFileSync(join(pendingDir(cwd), name), 'utf8')); }
|
|
80
|
+
catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function packetSourcePath(packet) {
|
|
84
|
+
return packet?.intake?.sourcePath || packet?.sourcePath || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function packetFirstSeenAt(packet) {
|
|
88
|
+
return packet?.firstSeenAt || packet?.createdAt || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// processInboxFile renames its source into a `.<name>.consumed` marker inside
|
|
92
|
+
// pendingDir once a packet is written, so the original sourcePath disappears
|
|
93
|
+
// the moment ingestion succeeds. That disappearance is expected, not an
|
|
94
|
+
// orphan — the sweep must not dead-letter a packet on the very next tick
|
|
95
|
+
// just because its own consumption already moved the source out of the way.
|
|
96
|
+
|
|
97
|
+
function hasConsumedMarker(cwd, sourcePath) {
|
|
98
|
+
return existsSync(join(pendingDir(cwd), `.${basename(sourcePath)}.consumed`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A pending packet is keyed by the source file it came from, independent of
|
|
102
|
+
// which ingest path wrote it (flat `sourcePath` here, nested
|
|
103
|
+
// `intake.sourcePath` for packets from lib/intake/prepare.mjs). Finding a
|
|
104
|
+
// live match lets re-ingestion refresh in place instead of writing a sibling
|
|
105
|
+
// packet for the same source.
|
|
106
|
+
|
|
107
|
+
function findExistingPendingBySource(cwd, sourcePath) {
|
|
108
|
+
for (const name of pendingPacketFiles(cwd)) {
|
|
109
|
+
const data = readPacketFile(cwd, name);
|
|
110
|
+
if (data && packetSourcePath(data) === sourcePath) return { name, data };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
63
115
|
/**
|
|
64
116
|
* Process one inbox file: classify, persist packet to pending, remove the
|
|
65
117
|
* inbox source on success, or move to dead-letter on persistent failure.
|
|
118
|
+
* A file whose sourcePath already has a live pending packet refreshes that
|
|
119
|
+
* packet in place rather than writing a second one for the same source.
|
|
66
120
|
*
|
|
67
121
|
* Returns one of:
|
|
68
122
|
* { didWork: true, route: 'pending'|'dead-letter', packetId }
|
|
@@ -76,10 +130,11 @@ export async function processInboxFile(filePath, { cwd, classify, now = () => ne
|
|
|
76
130
|
try { body = readFileSync(filePath, 'utf8'); }
|
|
77
131
|
catch (err) { return { didWork: false, reason: `read failed: ${err.message}` }; }
|
|
78
132
|
|
|
79
|
-
const
|
|
133
|
+
const existing = findExistingPendingBySource(cwd, filePath);
|
|
134
|
+
const id = existing?.data?.id || `intake-${now().getTime()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
80
135
|
const packet = {
|
|
81
136
|
id,
|
|
82
|
-
firstSeenAt: new Date(statSync(filePath).mtimeMs).toISOString(),
|
|
137
|
+
firstSeenAt: existing?.data?.firstSeenAt || new Date(statSync(filePath).mtimeMs).toISOString(),
|
|
83
138
|
sourcePath: filePath,
|
|
84
139
|
excerpt: body.slice(0, 500),
|
|
85
140
|
attempts: 0,
|
|
@@ -101,13 +156,15 @@ export async function processInboxFile(filePath, { cwd, classify, now = () => ne
|
|
|
101
156
|
return { didWork: false, reason: `classify failed, will retry: ${err.message}` };
|
|
102
157
|
}
|
|
103
158
|
|
|
104
|
-
|
|
159
|
+
packet.lastSeenAt = now().toISOString();
|
|
160
|
+
const packetName = existing?.name || `${id}.json`;
|
|
161
|
+
const packetPath = join(pendingDir(cwd), packetName);
|
|
105
162
|
writeFileSync(packetPath, JSON.stringify(packet, null, 2));
|
|
106
163
|
try {
|
|
107
164
|
const consumedPath = join(pendingDir(cwd), `.${basename(filePath)}.consumed`);
|
|
108
165
|
renameSync(filePath, consumedPath);
|
|
109
166
|
} catch { /* ignore consume failure */ }
|
|
110
|
-
return { didWork: true, route: 'pending', packetId: id };
|
|
167
|
+
return { didWork: true, route: 'pending', packetId: id, refreshed: !!existing };
|
|
111
168
|
}
|
|
112
169
|
|
|
113
170
|
function persistDeadLetter(filePath, packet, reason, cwd) {
|
|
@@ -117,6 +174,40 @@ function persistDeadLetter(filePath, packet, reason, cwd) {
|
|
|
117
174
|
return { didWork: true, route: 'dead-letter', packetId: packet.id, reason };
|
|
118
175
|
}
|
|
119
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Sweep persisted `.cx/intake/pending/` packets: one whose source file is
|
|
179
|
+
* missing from disk, or whose age exceeds classifyPacket's TTL, moves to
|
|
180
|
+
* `.cx/intake/dead-letter/` with a `deadLetterReason`. Runs every tick
|
|
181
|
+
* alongside the inbox scan so packets written by any ingest path (not just
|
|
182
|
+
* processInboxFile) are covered — the daemon's TTL/dead-letter contract
|
|
183
|
+
* otherwise only reaches packets still in flight through the inbox.
|
|
184
|
+
*
|
|
185
|
+
* Returns the list of { id, reason } packets moved.
|
|
186
|
+
*/
|
|
187
|
+
export function sweepPendingPackets(cwd, { now = () => new Date() } = {}) {
|
|
188
|
+
const swept = [];
|
|
189
|
+
for (const name of pendingPacketFiles(cwd)) {
|
|
190
|
+
const packet = readPacketFile(cwd, name);
|
|
191
|
+
if (!packet) continue;
|
|
192
|
+
|
|
193
|
+
const sourcePath = packetSourcePath(packet);
|
|
194
|
+
const sourceMissing = !!sourcePath && !existsSync(sourcePath) && !hasConsumedMarker(cwd, sourcePath);
|
|
195
|
+
const decision = classifyPacket({ firstSeenAt: packetFirstSeenAt(packet), attempts: packet.attempts || 0 });
|
|
196
|
+
|
|
197
|
+
let reason = null;
|
|
198
|
+
if (sourceMissing) reason = 'source-missing';
|
|
199
|
+
else if (decision.route === 'dead-letter') reason = 'ttl-expired';
|
|
200
|
+
if (!reason) continue;
|
|
201
|
+
|
|
202
|
+
ensureDir(deadLetterDir(cwd));
|
|
203
|
+
const dlPath = join(deadLetterDir(cwd), name);
|
|
204
|
+
writeFileSync(dlPath, JSON.stringify({ ...packet, deadLetterReason: reason, deadLetteredAt: now().toISOString() }, null, 2));
|
|
205
|
+
try { unlinkSync(join(pendingDir(cwd), name)); } catch { /* leave the dead-letter copy even if cleanup fails */ }
|
|
206
|
+
swept.push({ id: packet.id || name.replace(/\.json$/, ''), reason });
|
|
207
|
+
}
|
|
208
|
+
return swept;
|
|
209
|
+
}
|
|
210
|
+
|
|
120
211
|
/**
|
|
121
212
|
* Build a DaemonRunner for the intake daemon. Caller is responsible for
|
|
122
213
|
* calling .run() — typically construct intake daemon start.
|
|
@@ -132,9 +223,9 @@ export function buildIntakeDaemon({ cwd = process.cwd(), intervalMs = 60_000, cl
|
|
|
132
223
|
maxIdleTicks: 6,
|
|
133
224
|
memoryCapMb: memoryCapMbFor('intake', cwd),
|
|
134
225
|
async tick() {
|
|
226
|
+
const swept = sweepPendingPackets(cwd);
|
|
135
227
|
const files = listInboxFiles(cwd);
|
|
136
|
-
|
|
137
|
-
let workedAny = false;
|
|
228
|
+
let workedAny = swept.length > 0;
|
|
138
229
|
for (const file of files) {
|
|
139
230
|
const result = await processInboxFile(file, { cwd, classify: classifyFn });
|
|
140
231
|
if (result.didWork) workedAny = true;
|
package/lib/intake/git-queue.mjs
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* Backs team/enterprise modes with the filesystem and Git for state
|
|
5
5
|
* synchronization and conflict resolution, with no central database server.
|
|
6
6
|
*
|
|
7
|
+
* git-queue is the zero-dependency DEFAULT kind:'queue' provider
|
|
8
|
+
* (construct-9oi4.7.11); substrate owns the queue contract and Postgres is an
|
|
9
|
+
* optional provider selected through the extension registry.
|
|
10
|
+
*
|
|
11
|
+
* Correctness envelope: single-writer-preferred, eventual consistency.
|
|
12
|
+
* Concurrent claims from multiple agents may result in duplicate claim
|
|
13
|
+
* attempts; the dolt commit history provides reconciliation. A push failure is
|
|
14
|
+
* NOT swallowed or ratified as success: claim() returns a typed disposition
|
|
15
|
+
* ({ durable: false, disposition: 'local-only', reason }) so callers see that
|
|
16
|
+
* the claim is local-only until the push is confirmed in the dolt remote,
|
|
17
|
+
* rather than mistaking it for a durable claim.
|
|
18
|
+
*
|
|
7
19
|
* Structure:
|
|
8
20
|
* .cx/team-inbox/
|
|
9
21
|
* pending/ - JSON files for unclaimed tasks
|
|
@@ -34,9 +46,11 @@ function timestamp() {
|
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
export class GitIntakeQueue {
|
|
37
|
-
constructor({ project, rootDir = process.cwd() } = {}) {
|
|
49
|
+
constructor({ project, rootDir = process.cwd(), _exec } = {}) {
|
|
38
50
|
this.project = project;
|
|
39
51
|
this.inboxRoot = path.join(rootDir, '.cx', 'team-inbox');
|
|
52
|
+
// Allow tests to inject a fake exec without monkey-patching the module.
|
|
53
|
+
this._exec = _exec || execSync;
|
|
40
54
|
this._ensureDirs();
|
|
41
55
|
}
|
|
42
56
|
|
|
@@ -48,8 +62,8 @@ export class GitIntakeQueue {
|
|
|
48
62
|
|
|
49
63
|
_gitAddAndCommit(filePath, message) {
|
|
50
64
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
this._exec(`git add "${filePath}"`, { stdio: 'ignore' });
|
|
66
|
+
this._exec(`git commit -m "${message}"`, { stdio: 'ignore' });
|
|
53
67
|
} catch (err) {
|
|
54
68
|
// It's okay if commit fails because of no changes
|
|
55
69
|
}
|
|
@@ -93,17 +107,24 @@ export class GitIntakeQueue {
|
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
async read(id) {
|
|
96
|
-
for (const dir of ['pending', '
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const searchPath = dir === 'claimed'
|
|
100
|
-
? path.join(this.inboxRoot, dir) // needs recursive search
|
|
101
|
-
: path.join(this.inboxRoot, dir, `${id}.json`);
|
|
102
|
-
|
|
103
|
-
if (dir !== 'claimed' && fs.existsSync(searchPath)) {
|
|
110
|
+
for (const dir of ['pending', 'processed', 'skipped', 'quarantine']) {
|
|
111
|
+
const searchPath = path.join(this.inboxRoot, dir, `${id}.json`);
|
|
112
|
+
if (fs.existsSync(searchPath)) {
|
|
104
113
|
return JSON.parse(fs.readFileSync(searchPath, 'utf8'));
|
|
105
114
|
}
|
|
106
115
|
}
|
|
116
|
+
|
|
117
|
+
// 'claimed' uses per-worker subdirectories — search recursively.
|
|
118
|
+
const claimedBase = path.join(this.inboxRoot, 'claimed');
|
|
119
|
+
if (fs.existsSync(claimedBase)) {
|
|
120
|
+
for (const worker of fs.readdirSync(claimedBase)) {
|
|
121
|
+
const candidate = path.join(claimedBase, worker, `${id}.json`);
|
|
122
|
+
if (fs.existsSync(candidate)) {
|
|
123
|
+
return JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
107
128
|
return null;
|
|
108
129
|
}
|
|
109
130
|
|
|
@@ -111,8 +132,13 @@ export class GitIntakeQueue {
|
|
|
111
132
|
if (!claimedBy) throw new Error('claim: claimedBy is required');
|
|
112
133
|
|
|
113
134
|
// Pull latest to minimize conflicts
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
try {
|
|
136
|
+
this._exec('git pull --rebase', { stdio: 'ignore' });
|
|
137
|
+
} catch (pullErr) {
|
|
138
|
+
// Pull failure is non-fatal — we proceed with local state.
|
|
139
|
+
// Log a reconciliation record so operators can detect divergence.
|
|
140
|
+
console.warn(`[git-queue] pull failed before claim (proceeding with local state): ${pullErr.message}`);
|
|
141
|
+
}
|
|
116
142
|
|
|
117
143
|
const pendingDir = path.join(this.inboxRoot, 'pending');
|
|
118
144
|
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.json')).sort();
|
|
@@ -125,31 +151,34 @@ export class GitIntakeQueue {
|
|
|
125
151
|
fs.mkdirSync(workerDir, { recursive: true });
|
|
126
152
|
const claimedPath = path.join(workerDir, fileName);
|
|
127
153
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fs.writeFileSync(claimedPath, JSON.stringify(data, null, 2));
|
|
138
|
-
|
|
139
|
-
// Commit and push the claim so peers see it
|
|
140
|
-
|
|
141
|
-
execSync(`git add .cx/team-inbox/pending/${fileName} .cx/team-inbox/claimed/${claimedBy}/${fileName}`, { stdio: 'ignore' });
|
|
142
|
-
execSync(`git commit -m "Claim task ${data.id} by ${claimedBy}"`, { stdio: 'ignore' });
|
|
143
|
-
execSync('git push', { stdio: 'ignore' });
|
|
144
|
-
|
|
145
|
-
return data;
|
|
146
|
-
} catch (err) {
|
|
147
|
-
// A push failure means another worker claimed it first or Git is out of
|
|
148
|
-
// sync; the local move is left for the next 'git pull --rebase' to reconcile.
|
|
154
|
+
// Atomic move on filesystem
|
|
155
|
+
fs.renameSync(pendingPath, claimedPath);
|
|
156
|
+
|
|
157
|
+
// Update metadata
|
|
158
|
+
const data = JSON.parse(fs.readFileSync(claimedPath, 'utf8'));
|
|
159
|
+
data.status = 'claimed';
|
|
160
|
+
data.claimedBy = claimedBy;
|
|
161
|
+
data.claimedAt = new Date().toISOString();
|
|
162
|
+
fs.writeFileSync(claimedPath, JSON.stringify(data, null, 2));
|
|
149
163
|
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
// Commit and push the claim so peers see it. A push failure is reported as a
|
|
165
|
+
// typed non-durable disposition on the returned claim rather than swallowed:
|
|
166
|
+
// the local move stands for the next 'git pull --rebase' to reconcile, but
|
|
167
|
+
// the caller is told the claim is local-only, not durable.
|
|
168
|
+
data.durable = true;
|
|
169
|
+
data.disposition = 'pushed';
|
|
170
|
+
try {
|
|
171
|
+
this._exec(`git add .cx/team-inbox/pending/${fileName} .cx/team-inbox/claimed/${claimedBy}/${fileName}`, { stdio: 'ignore' });
|
|
172
|
+
this._exec(`git commit -m "Claim task ${data.id} by ${claimedBy}"`, { stdio: 'ignore' });
|
|
173
|
+
this._exec('git push', { stdio: 'ignore' });
|
|
174
|
+
} catch (pushErr) {
|
|
175
|
+
data.durable = false;
|
|
176
|
+
data.disposition = 'local-only';
|
|
177
|
+
data.dispositionReason = pushErr.message;
|
|
178
|
+
console.warn(`[git-queue] push failed after claim of ${data.id} by ${claimedBy} (claim is local-only until reconciled): ${pushErr.message}`);
|
|
152
179
|
}
|
|
180
|
+
|
|
181
|
+
return data;
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
async markProcessed(id, { processedBy = 'unknown', notes = '' } = {}) {
|
|
@@ -187,10 +216,53 @@ export class GitIntakeQueue {
|
|
|
187
216
|
fs.writeFileSync(processedPath, JSON.stringify(data, null, 2));
|
|
188
217
|
|
|
189
218
|
this._gitAddAndCommit(this.inboxRoot, `Mark task ${id} as processed`);
|
|
190
|
-
try {
|
|
219
|
+
try { this._exec('git push', { stdio: 'ignore' }); } catch (e) {}
|
|
191
220
|
|
|
192
221
|
return { id };
|
|
193
222
|
}
|
|
194
223
|
|
|
195
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Mark an issue as skipped in local state WITHOUT a push.
|
|
226
|
+
*
|
|
227
|
+
* Used for graceful degradation when a push fails or when the caller wants
|
|
228
|
+
* to skip without touching the remote. The skip is advisory — it survives
|
|
229
|
+
* only as a local file move until the next reconciliation pull.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} issueId
|
|
232
|
+
* @returns {{ id: string, skippedAt: string }}
|
|
233
|
+
*/
|
|
234
|
+
async markSkipped(issueId) {
|
|
235
|
+
if (!issueId) throw new Error('markSkipped: issueId is required');
|
|
236
|
+
|
|
237
|
+
// Locate the file across pending and claimed directories.
|
|
238
|
+
let foundPath = null;
|
|
239
|
+
|
|
240
|
+
const pendingCandidate = path.join(this.inboxRoot, 'pending', `${issueId}.json`);
|
|
241
|
+
if (fs.existsSync(pendingCandidate)) {
|
|
242
|
+
foundPath = pendingCandidate;
|
|
243
|
+
} else {
|
|
244
|
+
const claimedBase = path.join(this.inboxRoot, 'claimed');
|
|
245
|
+
if (fs.existsSync(claimedBase)) {
|
|
246
|
+
for (const worker of fs.readdirSync(claimedBase)) {
|
|
247
|
+
const candidate = path.join(claimedBase, worker, `${issueId}.json`);
|
|
248
|
+
if (fs.existsSync(candidate)) { foundPath = candidate; break; }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!foundPath) throw new Error(`markSkipped: no entry ${issueId} found`);
|
|
254
|
+
|
|
255
|
+
const data = JSON.parse(fs.readFileSync(foundPath, 'utf8'));
|
|
256
|
+
data.status = 'skipped';
|
|
257
|
+
data.skippedAt = new Date().toISOString();
|
|
258
|
+
|
|
259
|
+
const skippedPath = path.join(this.inboxRoot, 'skipped', `${issueId}.json`);
|
|
260
|
+
fs.renameSync(foundPath, skippedPath);
|
|
261
|
+
fs.writeFileSync(skippedPath, JSON.stringify(data, null, 2));
|
|
262
|
+
|
|
263
|
+
// No push — this is a local-only operation (graceful degradation).
|
|
264
|
+
return { id: issueId, skippedAt: data.skippedAt };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// reopen would follow similar logic...
|
|
196
268
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/queue-registry.mjs — kind:'queue' provider resolution.
|
|
3
|
+
*
|
|
4
|
+
* Substrate owns the queue/run-store CONTRACT; git-queue is the zero-dependency
|
|
5
|
+
* DEFAULT provider and Postgres is an OPTIONAL provider. Selection is explicit:
|
|
6
|
+
* a concrete queue backend is chosen by resolving an installed kind:'queue'
|
|
7
|
+
* manifest through the extension registry (construct-9oi4.7.11, supersedes the
|
|
8
|
+
* ADR-0051 git-vs-postgres framing). No silent postgres->git alias exists — a
|
|
9
|
+
* caller that names a backend with no registered kind:'queue' provider gets a
|
|
10
|
+
* typed error, not a swallowed downgrade.
|
|
11
|
+
*
|
|
12
|
+
* Resolution surface:
|
|
13
|
+
* listQueueProviders({ rootDir, env }) → manifest[] of kind:'queue', merged
|
|
14
|
+
* across builtin/user/project tiers (project wins).
|
|
15
|
+
* findQueueProvider(backend, { rootDir, env }) → manifest | null for a backend id.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { loadManifestsFromDir, mergeManifests, resolveManifestDirs } from '../extensions/loader.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The queue backend ids that are always resolvable without a registered
|
|
22
|
+
* manifest. git is the default provider; filesystem is the solo-mode default
|
|
23
|
+
* provider. Both ship in-tree, so they never require an extension manifest.
|
|
24
|
+
*/
|
|
25
|
+
export const BUILTIN_QUEUE_BACKENDS = ['git', 'filesystem'];
|
|
26
|
+
|
|
27
|
+
// Load every kind:'queue' manifest across the three tiers and merge by id so a
|
|
28
|
+
// project- or user-level Postgres queue provider overrides a builtin of the
|
|
29
|
+
// same id. Only manifests declaring kind:'queue' are returned.
|
|
30
|
+
|
|
31
|
+
export function listQueueProviders({ rootDir = process.cwd(), env = process.env } = {}) {
|
|
32
|
+
const homeDir = env?.HOME || env?.USERPROFILE || undefined;
|
|
33
|
+
const dirs = resolveManifestDirs({ rootDir, homeDir });
|
|
34
|
+
const builtin = loadManifestsFromDir(dirs.builtin).manifests;
|
|
35
|
+
const user = loadManifestsFromDir(dirs.user).manifests;
|
|
36
|
+
const project = loadManifestsFromDir(dirs.project).manifests;
|
|
37
|
+
return mergeManifests(builtin, user, project).filter((m) => m.kind === 'queue');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* findQueueProvider(backend, opts)
|
|
42
|
+
*
|
|
43
|
+
* Returns the kind:'queue' manifest whose id matches `backend`, or null when no
|
|
44
|
+
* such provider is registered. Callers use the null result to distinguish an
|
|
45
|
+
* unregistered backend (a hard, explicit error) from a resolvable one.
|
|
46
|
+
*/
|
|
47
|
+
export function findQueueProvider(backend, { rootDir = process.cwd(), env = process.env } = {}) {
|
|
48
|
+
if (!backend) return null;
|
|
49
|
+
return listQueueProviders({ rootDir, env }).find((m) => m.id === backend) || null;
|
|
50
|
+
}
|
package/lib/intake/queue.mjs
CHANGED
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The IntakeQueue contract carries the R&D intake packets from the embed
|
|
5
5
|
* daemon (writer) to the agent in the user's editor (reader/processor).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Substrate owns the queue CONTRACT; git-queue is the zero-dependency default
|
|
7
|
+
* kind:'queue' provider and Postgres is an optional kind:'queue' provider
|
|
8
|
+
* selected through the extension registry (construct-9oi4.7.11). Solo mode
|
|
9
|
+
* resolves to the filesystem provider under `.cx/intake/`; team and enterprise
|
|
10
|
+
* default to the Postgres provider. Callers never branch on backend — they ask for a
|
|
11
|
+
* queue via `createIntakeQueue(rootDir, env)` and use the same six methods.
|
|
12
|
+
* The returned instance also carries `.tenantId` (ADR-0057/A7: resolved once
|
|
13
|
+
* from CONSTRUCT_TENANT_ID/config, 'local' default outside enterprise mode)
|
|
14
|
+
* so a caller can stamp it onto entries without re-resolving it.
|
|
10
15
|
*
|
|
11
16
|
* Queue entry shape:
|
|
12
17
|
* {
|
|
13
18
|
* id, createdAt, status: 'pending'|'processed'|'skipped',
|
|
19
|
+
* tenantId?,
|
|
14
20
|
* intake: { sourcePath, outputPath, characters, knowledgeSubdir },
|
|
15
21
|
* triage: { intakeType, rdStage, primaryOwner, recommendedChain,
|
|
16
22
|
* recommendedAction, risk, requiresApproval, confidence,
|
|
@@ -37,19 +43,60 @@ import path from 'node:path';
|
|
|
37
43
|
|
|
38
44
|
import { FilesystemIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir } from './filesystem-queue.mjs';
|
|
39
45
|
import { GitIntakeQueue } from './git-queue.mjs';
|
|
40
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
getDeploymentMode,
|
|
48
|
+
requireTeamCapabilityOrDegrade,
|
|
49
|
+
} from '../deployment-mode.mjs';
|
|
50
|
+
import { createSqlClient } from '../storage/backend.mjs';
|
|
51
|
+
import { findQueueProvider, BUILTIN_QUEUE_BACKENDS } from './queue-registry.mjs';
|
|
52
|
+
import { resolveTenantContext, TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
|
|
53
|
+
import { PostgresIntakeQueue } from '../queue/pg-queue.mjs';
|
|
41
54
|
|
|
42
55
|
export { FilesystemIntakeQueue, GitIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir };
|
|
43
56
|
|
|
44
57
|
export const INTAKE_QUEUE_BACKEND_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_BACKEND';
|
|
45
58
|
export const INTAKE_PROJECT_ENV_KEY = 'CONSTRUCT_PROJECT_NAME';
|
|
46
59
|
export const INTAKE_TENANT_ENV_KEY = 'CONSTRUCT_TENANT_ID';
|
|
60
|
+
export const INTAKE_QUEUE_NAME_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_NAME';
|
|
61
|
+
export const INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY = 'CONSTRUCT_QUEUE_LEASE_SECONDS';
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
// Re-exported so a caller can resolve the same tenant value the factory
|
|
64
|
+
// stamps on the queue without importing lib/tenant/context.mjs directly.
|
|
65
|
+
export { TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
|
|
66
|
+
|
|
67
|
+
// Thrown when a caller names a queue backend that is neither a builtin provider
|
|
68
|
+
// (git/filesystem) nor a registered kind:'queue' extension. The reframing
|
|
69
|
+
// removed the silent postgres->git alias, so an unknown backend is a hard,
|
|
70
|
+
// explicit error rather than a swallowed downgrade.
|
|
71
|
+
|
|
72
|
+
export class UnregisteredQueueProviderError extends Error {
|
|
73
|
+
constructor(backend) {
|
|
74
|
+
super(
|
|
75
|
+
`Queue backend '${backend}' has no registered kind:'queue' provider. ` +
|
|
76
|
+
`Builtin providers: ${BUILTIN_QUEUE_BACKENDS.join(', ')}. ` +
|
|
77
|
+
`Install a kind:'queue' extension (e.g. a Postgres provider) to select it.`,
|
|
78
|
+
);
|
|
79
|
+
this.name = 'UnregisteredQueueProviderError';
|
|
80
|
+
this.backend = backend;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Selection resolves through the registry by kind. An explicit backend override
|
|
85
|
+
// wins; otherwise deployment mode picks the default provider (solo→filesystem,
|
|
86
|
+
// team/enterprise→postgres). A non-builtin backend must resolve to a registered
|
|
87
|
+
// kind:'queue' provider or the call fails explicitly.
|
|
88
|
+
|
|
89
|
+
function resolveBackend(env, rootDir) {
|
|
49
90
|
const override = env?.[INTAKE_QUEUE_BACKEND_ENV_KEY];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
const requested = override && String(override).trim()
|
|
92
|
+
? String(override).trim()
|
|
93
|
+
: (getDeploymentMode(env, { cwd: rootDir }) === 'solo' ? 'filesystem' : 'postgres');
|
|
94
|
+
|
|
95
|
+
if (BUILTIN_QUEUE_BACKENDS.includes(requested)) return { backend: requested, provider: null };
|
|
96
|
+
|
|
97
|
+
const provider = findQueueProvider(requested, { rootDir, env });
|
|
98
|
+
if (!provider) throw new UnregisteredQueueProviderError(requested);
|
|
99
|
+
return { backend: requested, provider };
|
|
53
100
|
}
|
|
54
101
|
|
|
55
102
|
function resolveProject(rootDir, env) {
|
|
@@ -58,20 +105,89 @@ function resolveProject(rootDir, env) {
|
|
|
58
105
|
return path.basename(path.resolve(rootDir)).trim() || 'construct';
|
|
59
106
|
}
|
|
60
107
|
|
|
108
|
+
// Resolves the tenant context (config + env, validated) for queue scoping.
|
|
109
|
+
// Reads CONSTRUCT_TENANT_ID directly here — the factory is a plumbing
|
|
110
|
+
// boundary, so it does not load construct.config.json itself and passes no
|
|
111
|
+
// config, which mirrors solo/team's config-optional path. Enterprise callers
|
|
112
|
+
// that need config-sourced tenantId should resolve it once at startup via
|
|
113
|
+
// lib/deployment-mode.mjs#validateTenantAtStartup and pass the result through
|
|
114
|
+
// opts.tenantId instead of relying on this fallback.
|
|
115
|
+
|
|
116
|
+
export function resolveIntakeTenantId(env = process.env, mode = getDeploymentMode(env)) {
|
|
117
|
+
return resolveTenantContext({ env, mode }).tenantId;
|
|
118
|
+
}
|
|
119
|
+
|
|
61
120
|
/**
|
|
62
121
|
* Create an IntakeQueue instance for the given project root.
|
|
63
122
|
* Backend selection: CONSTRUCT_INTAKE_QUEUE_BACKEND override wins; otherwise
|
|
64
|
-
* solo mode → filesystem, team/enterprise →
|
|
65
|
-
*
|
|
123
|
+
* solo mode → filesystem, team/enterprise → postgres. A backend that is neither builtin nor a registered
|
|
124
|
+
* kind:'queue' provider throws UnregisteredQueueProviderError — there is no
|
|
125
|
+
* silent postgres->git alias.
|
|
126
|
+
*
|
|
127
|
+
* The returned instance carries a `.tenantId` (opts.tenantId wins, otherwise
|
|
128
|
+
* resolved from CONSTRUCT_TENANT_ID/config via resolveIntakeTenantId) so a
|
|
129
|
+
* caller building a queue entry can stamp tenantId without re-deriving it —
|
|
130
|
+
* the queue classes themselves stay tenant-agnostic (LMCP-H1; tenant-scoped
|
|
131
|
+
* storage is LMCP-H4, not this bead).
|
|
66
132
|
*/
|
|
67
133
|
export function createIntakeQueue(rootDir, env = process.env, opts = {}) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
134
|
+
const resolved = opts.backend
|
|
135
|
+
? (BUILTIN_QUEUE_BACKENDS.includes(opts.backend)
|
|
136
|
+
? { backend: opts.backend, provider: null }
|
|
137
|
+
: (findQueueProvider(opts.backend, { rootDir, env })
|
|
138
|
+
? { backend: opts.backend, provider: findQueueProvider(opts.backend, { rootDir, env }) }
|
|
139
|
+
: (() => { throw new UnregisteredQueueProviderError(opts.backend); })()))
|
|
140
|
+
: resolveBackend(env, rootDir);
|
|
72
141
|
|
|
142
|
+
const tenantId = opts.tenantId ?? resolveIntakeTenantId(env, getDeploymentMode(env, { cwd: rootDir }));
|
|
143
|
+
|
|
144
|
+
if (resolved.backend === 'filesystem') {
|
|
145
|
+
const queue = new FilesystemIntakeQueue(rootDir);
|
|
146
|
+
queue.tenantId = tenantId;
|
|
147
|
+
return queue;
|
|
148
|
+
}
|
|
149
|
+
if (resolved.backend === 'git') {
|
|
73
150
|
const project = opts.project ?? resolveProject(rootDir, env);
|
|
74
|
-
|
|
151
|
+
const queue = new GitIntakeQueue({ project, rootDir });
|
|
152
|
+
queue.tenantId = tenantId;
|
|
153
|
+
return queue;
|
|
75
154
|
}
|
|
76
|
-
|
|
155
|
+
|
|
156
|
+
if (resolved.backend === 'postgres' || resolved.provider?.id === 'postgres') {
|
|
157
|
+
const sql = opts.sql ?? createSqlClient(env);
|
|
158
|
+
if (!sql) {
|
|
159
|
+
const mode = getDeploymentMode(env, { cwd: rootDir });
|
|
160
|
+
if (mode === 'solo') {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Queue backend '${resolved.backend}' requires DATABASE_URL or CONSTRUCT_DATABASE_URL; ` +
|
|
163
|
+
`no Postgres SQL client is configured.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
requireTeamCapabilityOrDegrade('postgres-queue', false, env, { cwd: rootDir });
|
|
167
|
+
const queue = new GitIntakeQueue({ project: opts.project ?? resolveProject(rootDir, env), rootDir });
|
|
168
|
+
queue.tenantId = tenantId;
|
|
169
|
+
queue.backend = 'git';
|
|
170
|
+
queue.requestedBackend = resolved.backend;
|
|
171
|
+
queue.degraded = true;
|
|
172
|
+
queue.degradedReason = 'postgres-unavailable';
|
|
173
|
+
return queue;
|
|
174
|
+
}
|
|
175
|
+
requireTeamCapabilityOrDegrade('postgres-queue', true, env, { cwd: rootDir });
|
|
176
|
+
return new PostgresIntakeQueue({
|
|
177
|
+
sql,
|
|
178
|
+
project: opts.project ?? resolveProject(rootDir, env),
|
|
179
|
+
tenantId,
|
|
180
|
+
queueName: opts.queueName ?? env?.[INTAKE_QUEUE_NAME_ENV_KEY] ?? 'intake',
|
|
181
|
+
leaseSeconds: opts.leaseSeconds ?? env?.[INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// A registered kind:'queue' provider was selected but no in-tree instantiation
|
|
186
|
+
// path exists yet. The selection is honest — it names the resolved provider
|
|
187
|
+
// rather than silently downgrading to git.
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Queue backend '${resolved.backend}' resolves to registered kind:'queue' provider ` +
|
|
191
|
+
`'${resolved.provider?.id}' but no in-tree instantiation path is wired yet.`,
|
|
192
|
+
);
|
|
77
193
|
}
|