@geraldmaron/construct 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +261 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +103 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/queue/pg-queue.mjs — optional Postgres kind:'queue' provider.
|
|
3
|
+
*
|
|
4
|
+
* Claiming uses a single UPDATE fed by SELECT ... FOR UPDATE SKIP LOCKED so
|
|
5
|
+
* parallel workers contend inside Postgres instead of racing in process memory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
import { applyMigrations } from '../db/migrate.mjs';
|
|
11
|
+
|
|
12
|
+
export const PG_QUEUE_DEFAULT_NAME = 'intake';
|
|
13
|
+
export const PG_QUEUE_DEFAULT_LEASE_SECONDS = 120;
|
|
14
|
+
export const PG_QUEUE_DEFAULT_MAX_ATTEMPTS = 3;
|
|
15
|
+
|
|
16
|
+
export function newQueueId(prefix = 'pkt') {
|
|
17
|
+
return `${prefix}-${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nowIso() {
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeEntry(entry, id) {
|
|
25
|
+
return {
|
|
26
|
+
...entry,
|
|
27
|
+
id,
|
|
28
|
+
status: entry?.status || 'pending',
|
|
29
|
+
createdAt: entry?.createdAt || nowIso(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rowToEntry(row) {
|
|
34
|
+
return row?.payload || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function leaseInterval(sql, seconds) {
|
|
38
|
+
return sql`${Number(seconds)} * interval '1 second'`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function backoffInterval(sql, seconds) {
|
|
42
|
+
return sql`${Math.max(0, Number(seconds) || 0)} * interval '1 second'`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class PostgresIntakeQueue {
|
|
46
|
+
constructor({
|
|
47
|
+
sql,
|
|
48
|
+
project,
|
|
49
|
+
tenantId = 'local',
|
|
50
|
+
queueName = PG_QUEUE_DEFAULT_NAME,
|
|
51
|
+
leaseSeconds = PG_QUEUE_DEFAULT_LEASE_SECONDS,
|
|
52
|
+
} = {}) {
|
|
53
|
+
if (!sql) throw new Error('PostgresIntakeQueue: sql client is required');
|
|
54
|
+
if (!project) throw new Error('PostgresIntakeQueue: project is required');
|
|
55
|
+
this.sql = sql;
|
|
56
|
+
this.project = project;
|
|
57
|
+
this.tenantId = tenantId;
|
|
58
|
+
this.queueName = queueName;
|
|
59
|
+
this.leaseSeconds = Number(leaseSeconds) || PG_QUEUE_DEFAULT_LEASE_SECONDS;
|
|
60
|
+
this._ensured = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async ensureSchema() {
|
|
64
|
+
if (!this._ensured) this._ensured = applyMigrations(this.sql);
|
|
65
|
+
return this._ensured;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async enqueue(entry) {
|
|
69
|
+
if (!entry?.intake?.sourcePath) throw new Error('enqueue: entry.intake.sourcePath is required');
|
|
70
|
+
await this.ensureSchema();
|
|
71
|
+
const id = entry.id || newQueueId();
|
|
72
|
+
const payload = normalizeEntry(entry, id);
|
|
73
|
+
await this.sql`
|
|
74
|
+
INSERT INTO construct_queue_items (
|
|
75
|
+
project, tenant_id, queue_name, item_id, status, payload, created_at, updated_at, max_attempts
|
|
76
|
+
) VALUES (
|
|
77
|
+
${this.project}, ${this.tenantId}, ${this.queueName}, ${id}, 'pending',
|
|
78
|
+
${this.sql.json(payload)}, ${payload.createdAt}, now(),
|
|
79
|
+
${Number(entry.maxAttempts) > 0 ? Math.floor(Number(entry.maxAttempts)) : PG_QUEUE_DEFAULT_MAX_ATTEMPTS}
|
|
80
|
+
)
|
|
81
|
+
ON CONFLICT (project, tenant_id, queue_name, item_id) DO UPDATE SET
|
|
82
|
+
payload = EXCLUDED.payload,
|
|
83
|
+
status = EXCLUDED.status,
|
|
84
|
+
max_attempts = EXCLUDED.max_attempts,
|
|
85
|
+
updated_at = now()
|
|
86
|
+
`;
|
|
87
|
+
return { id };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async listPending() {
|
|
91
|
+
await this.ensureSchema();
|
|
92
|
+
const rows = await this.sql`
|
|
93
|
+
SELECT payload FROM construct_queue_items
|
|
94
|
+
WHERE project = ${this.project}
|
|
95
|
+
AND tenant_id = ${this.tenantId}
|
|
96
|
+
AND queue_name = ${this.queueName}
|
|
97
|
+
AND status = 'pending'
|
|
98
|
+
ORDER BY created_at ASC, item_id ASC
|
|
99
|
+
`;
|
|
100
|
+
return rows.map(rowToEntry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async count() {
|
|
104
|
+
await this.ensureSchema();
|
|
105
|
+
const rows = await this.sql`
|
|
106
|
+
SELECT COUNT(*)::int AS count FROM construct_queue_items
|
|
107
|
+
WHERE project = ${this.project}
|
|
108
|
+
AND tenant_id = ${this.tenantId}
|
|
109
|
+
AND queue_name = ${this.queueName}
|
|
110
|
+
AND status = 'pending'
|
|
111
|
+
`;
|
|
112
|
+
return Number(rows[0]?.count || 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async read(id) {
|
|
116
|
+
if (!id) return null;
|
|
117
|
+
await this.ensureSchema();
|
|
118
|
+
const rows = await this.sql`
|
|
119
|
+
SELECT payload FROM construct_queue_items
|
|
120
|
+
WHERE project = ${this.project}
|
|
121
|
+
AND tenant_id = ${this.tenantId}
|
|
122
|
+
AND queue_name = ${this.queueName}
|
|
123
|
+
AND item_id = ${id}
|
|
124
|
+
LIMIT 1
|
|
125
|
+
`;
|
|
126
|
+
return rowToEntry(rows[0]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async claim({ claimedBy, leaseSeconds = this.leaseSeconds } = {}) {
|
|
130
|
+
if (!claimedBy) throw new Error('claim: claimedBy is required');
|
|
131
|
+
await this.ensureSchema();
|
|
132
|
+
const claimId = newQueueId('claim');
|
|
133
|
+
const rows = await this.sql`
|
|
134
|
+
WITH candidate AS (
|
|
135
|
+
SELECT item_id
|
|
136
|
+
FROM construct_queue_items
|
|
137
|
+
WHERE project = ${this.project}
|
|
138
|
+
AND tenant_id = ${this.tenantId}
|
|
139
|
+
AND queue_name = ${this.queueName}
|
|
140
|
+
AND available_at <= now()
|
|
141
|
+
AND (
|
|
142
|
+
status = 'pending'
|
|
143
|
+
OR (status = 'claimed' AND lease_expires_at <= now() AND attempt < max_attempts)
|
|
144
|
+
)
|
|
145
|
+
ORDER BY created_at ASC, item_id ASC
|
|
146
|
+
FOR UPDATE SKIP LOCKED
|
|
147
|
+
LIMIT 1
|
|
148
|
+
),
|
|
149
|
+
updated AS (
|
|
150
|
+
UPDATE construct_queue_items q
|
|
151
|
+
SET status = 'claimed',
|
|
152
|
+
attempt = q.attempt + 1,
|
|
153
|
+
claimed_by = ${claimedBy},
|
|
154
|
+
lease_expires_at = now() + ${leaseInterval(this.sql, leaseSeconds)},
|
|
155
|
+
updated_at = now(),
|
|
156
|
+
payload = jsonb_set(
|
|
157
|
+
jsonb_set(
|
|
158
|
+
jsonb_set(q.payload, '{status}', to_jsonb('claimed'::text), true),
|
|
159
|
+
'{claimedBy}', to_jsonb(${claimedBy}::text), true
|
|
160
|
+
),
|
|
161
|
+
'{claimedAt}', to_jsonb(now()::text), true
|
|
162
|
+
)
|
|
163
|
+
FROM candidate
|
|
164
|
+
WHERE q.project = ${this.project}
|
|
165
|
+
AND q.tenant_id = ${this.tenantId}
|
|
166
|
+
AND q.queue_name = ${this.queueName}
|
|
167
|
+
AND q.item_id = candidate.item_id
|
|
168
|
+
RETURNING q.item_id, q.attempt, q.lease_expires_at, q.payload
|
|
169
|
+
)
|
|
170
|
+
INSERT INTO construct_queue_claims (
|
|
171
|
+
claim_id, project, tenant_id, queue_name, item_id, worker_id, attempt, lease_expires_at
|
|
172
|
+
)
|
|
173
|
+
SELECT ${claimId}, ${this.project}, ${this.tenantId}, ${this.queueName},
|
|
174
|
+
item_id, ${claimedBy}, attempt, lease_expires_at
|
|
175
|
+
FROM updated
|
|
176
|
+
RETURNING item_id, attempt, lease_expires_at,
|
|
177
|
+
(SELECT payload FROM updated WHERE updated.item_id = construct_queue_claims.item_id) AS payload
|
|
178
|
+
`;
|
|
179
|
+
const row = rows[0];
|
|
180
|
+
if (!row) return null;
|
|
181
|
+
return {
|
|
182
|
+
...row.payload,
|
|
183
|
+
id: row.payload?.id || row.item_id,
|
|
184
|
+
status: 'claimed',
|
|
185
|
+
claimedBy,
|
|
186
|
+
attempt: row.attempt,
|
|
187
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
188
|
+
claimId,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async heartbeat(id, { workerId, leaseSeconds = this.leaseSeconds } = {}) {
|
|
193
|
+
if (!id) throw new Error('heartbeat: id is required');
|
|
194
|
+
if (!workerId) throw new Error('heartbeat: workerId is required');
|
|
195
|
+
await this.ensureSchema();
|
|
196
|
+
const rows = await this.sql`
|
|
197
|
+
UPDATE construct_queue_items
|
|
198
|
+
SET lease_expires_at = now() + ${leaseInterval(this.sql, leaseSeconds)},
|
|
199
|
+
updated_at = now()
|
|
200
|
+
WHERE project = ${this.project}
|
|
201
|
+
AND tenant_id = ${this.tenantId}
|
|
202
|
+
AND queue_name = ${this.queueName}
|
|
203
|
+
AND item_id = ${id}
|
|
204
|
+
AND status = 'claimed'
|
|
205
|
+
AND claimed_by = ${workerId}
|
|
206
|
+
AND lease_expires_at > now()
|
|
207
|
+
AND COALESCE((payload->>'cancelRequested')::boolean, false) = false
|
|
208
|
+
RETURNING item_id, lease_expires_at
|
|
209
|
+
`;
|
|
210
|
+
if (!rows[0]) {
|
|
211
|
+
const current = await this.read(id);
|
|
212
|
+
if (current?.cancelRequested) return { id, renewed: false, cancelled: true, reason: current.cancellationReason || null };
|
|
213
|
+
return { id, renewed: false };
|
|
214
|
+
}
|
|
215
|
+
return { id, renewed: true, leaseExpiresAt: rows[0].lease_expires_at };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async fail(id, { workerId, reason, backoffSeconds = 0 } = {}) {
|
|
219
|
+
if (!id) throw new Error('fail: id is required');
|
|
220
|
+
await this.ensureSchema();
|
|
221
|
+
const rows = await this.sql`
|
|
222
|
+
UPDATE construct_queue_items
|
|
223
|
+
SET status = CASE WHEN attempt >= max_attempts THEN 'dead_letter' ELSE 'pending' END,
|
|
224
|
+
available_at = CASE WHEN attempt >= max_attempts THEN available_at ELSE now() + ${backoffInterval(this.sql, backoffSeconds)} END,
|
|
225
|
+
terminal_reason = ${reason || null},
|
|
226
|
+
claimed_by = NULL,
|
|
227
|
+
lease_expires_at = NULL,
|
|
228
|
+
updated_at = now(),
|
|
229
|
+
payload = jsonb_set(
|
|
230
|
+
jsonb_set(
|
|
231
|
+
jsonb_set(payload, '{status}', to_jsonb(CASE WHEN attempt >= max_attempts THEN 'dead_letter'::text ELSE 'pending'::text END), true),
|
|
232
|
+
'{lastFailureReason}', to_jsonb(${reason || ''}::text), true
|
|
233
|
+
),
|
|
234
|
+
'{failedBy}', to_jsonb(${workerId || ''}::text), true
|
|
235
|
+
)
|
|
236
|
+
WHERE project = ${this.project}
|
|
237
|
+
AND tenant_id = ${this.tenantId}
|
|
238
|
+
AND queue_name = ${this.queueName}
|
|
239
|
+
AND item_id = ${id}
|
|
240
|
+
AND status = 'claimed'
|
|
241
|
+
AND (${workerId || null} IS NULL OR claimed_by = ${workerId || null})
|
|
242
|
+
RETURNING item_id, status, attempt, max_attempts, available_at
|
|
243
|
+
`;
|
|
244
|
+
if (!rows[0]) return null;
|
|
245
|
+
return {
|
|
246
|
+
id,
|
|
247
|
+
status: rows[0].status,
|
|
248
|
+
attempt: rows[0].attempt,
|
|
249
|
+
maxAttempts: rows[0].max_attempts,
|
|
250
|
+
deadLettered: rows[0].status === 'dead_letter',
|
|
251
|
+
availableAt: rows[0].available_at,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async requestCancellation(id, { requestedBy, reason } = {}) {
|
|
256
|
+
if (!id) throw new Error('requestCancellation: id is required');
|
|
257
|
+
await this.ensureSchema();
|
|
258
|
+
const rows = await this.sql`
|
|
259
|
+
UPDATE construct_queue_items
|
|
260
|
+
SET terminal_reason = ${reason || null},
|
|
261
|
+
updated_at = now(),
|
|
262
|
+
payload = jsonb_set(
|
|
263
|
+
jsonb_set(
|
|
264
|
+
jsonb_set(payload, '{cancelRequested}', 'true'::jsonb, true),
|
|
265
|
+
'{cancellationReason}', to_jsonb(${reason || ''}::text), true
|
|
266
|
+
),
|
|
267
|
+
'{cancelRequestedBy}', to_jsonb(${requestedBy || ''}::text), true
|
|
268
|
+
)
|
|
269
|
+
WHERE project = ${this.project}
|
|
270
|
+
AND tenant_id = ${this.tenantId}
|
|
271
|
+
AND queue_name = ${this.queueName}
|
|
272
|
+
AND item_id = ${id}
|
|
273
|
+
AND status IN ('pending', 'claimed', 'awaiting_approval')
|
|
274
|
+
RETURNING item_id
|
|
275
|
+
`;
|
|
276
|
+
return rows[0] ? { id, cancellationRequested: true } : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async markProcessed(id, { processedBy, notes, executionKey } = {}) {
|
|
280
|
+
if (!id) throw new Error('markProcessed: id is required');
|
|
281
|
+
await this.ensureSchema();
|
|
282
|
+
if (executionKey) {
|
|
283
|
+
const current = await this.read(id);
|
|
284
|
+
if (current?.status === 'processed' && current.executionKey === executionKey) {
|
|
285
|
+
return { id, idempotent: true };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const rows = await this.sql`
|
|
289
|
+
UPDATE construct_queue_items
|
|
290
|
+
SET status = 'processed',
|
|
291
|
+
processed_at = now(),
|
|
292
|
+
processed_by = ${processedBy || null},
|
|
293
|
+
terminal_reason = ${notes || null},
|
|
294
|
+
claimed_by = NULL,
|
|
295
|
+
lease_expires_at = NULL,
|
|
296
|
+
updated_at = now(),
|
|
297
|
+
payload = jsonb_set(
|
|
298
|
+
jsonb_set(
|
|
299
|
+
jsonb_set(payload, '{status}', to_jsonb('processed'::text), true),
|
|
300
|
+
'{processedAt}', to_jsonb(now()::text), true
|
|
301
|
+
),
|
|
302
|
+
'{executionKey}', to_jsonb(${executionKey || ''}::text), true
|
|
303
|
+
)
|
|
304
|
+
WHERE project = ${this.project}
|
|
305
|
+
AND tenant_id = ${this.tenantId}
|
|
306
|
+
AND queue_name = ${this.queueName}
|
|
307
|
+
AND item_id = ${id}
|
|
308
|
+
AND status <> 'processed'
|
|
309
|
+
RETURNING item_id
|
|
310
|
+
`;
|
|
311
|
+
return rows[0] ? { id } : null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async markSkipped(id, { skippedBy, reason } = {}) {
|
|
315
|
+
if (!id) throw new Error('markSkipped: id is required');
|
|
316
|
+
await this.ensureSchema();
|
|
317
|
+
const rows = await this.sql`
|
|
318
|
+
UPDATE construct_queue_items
|
|
319
|
+
SET status = 'skipped',
|
|
320
|
+
skipped_at = now(),
|
|
321
|
+
skipped_by = ${skippedBy || null},
|
|
322
|
+
terminal_reason = ${reason || null},
|
|
323
|
+
claimed_by = NULL,
|
|
324
|
+
lease_expires_at = NULL,
|
|
325
|
+
updated_at = now(),
|
|
326
|
+
payload = jsonb_set(
|
|
327
|
+
jsonb_set(payload, '{status}', to_jsonb('skipped'::text), true),
|
|
328
|
+
'{skippedAt}', to_jsonb(now()::text), true
|
|
329
|
+
)
|
|
330
|
+
WHERE project = ${this.project}
|
|
331
|
+
AND tenant_id = ${this.tenantId}
|
|
332
|
+
AND queue_name = ${this.queueName}
|
|
333
|
+
AND item_id = ${id}
|
|
334
|
+
RETURNING item_id
|
|
335
|
+
`;
|
|
336
|
+
return rows[0] ? { id } : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async reopen(id) {
|
|
340
|
+
if (!id) throw new Error('reopen: id is required');
|
|
341
|
+
await this.ensureSchema();
|
|
342
|
+
const rows = await this.sql`
|
|
343
|
+
UPDATE construct_queue_items
|
|
344
|
+
SET status = 'pending',
|
|
345
|
+
claimed_by = NULL,
|
|
346
|
+
lease_expires_at = NULL,
|
|
347
|
+
processed_at = NULL,
|
|
348
|
+
processed_by = NULL,
|
|
349
|
+
skipped_at = NULL,
|
|
350
|
+
skipped_by = NULL,
|
|
351
|
+
terminal_reason = NULL,
|
|
352
|
+
updated_at = now(),
|
|
353
|
+
payload = jsonb_set(payload, '{status}', to_jsonb('pending'::text), true)
|
|
354
|
+
WHERE project = ${this.project}
|
|
355
|
+
AND tenant_id = ${this.tenantId}
|
|
356
|
+
AND queue_name = ${this.queueName}
|
|
357
|
+
AND item_id = ${id}
|
|
358
|
+
RETURNING item_id
|
|
359
|
+
`;
|
|
360
|
+
return rows[0] ? { id, from: 'postgres' } : null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async queueStats() {
|
|
364
|
+
await this.ensureSchema();
|
|
365
|
+
const rows = await this.sql`
|
|
366
|
+
SELECT status, COUNT(*)::int AS count
|
|
367
|
+
FROM construct_queue_items
|
|
368
|
+
WHERE project = ${this.project}
|
|
369
|
+
AND tenant_id = ${this.tenantId}
|
|
370
|
+
AND queue_name = ${this.queueName}
|
|
371
|
+
GROUP BY status
|
|
372
|
+
`;
|
|
373
|
+
const byStatus = {
|
|
374
|
+
pending: 0,
|
|
375
|
+
claimed: 0,
|
|
376
|
+
processed: 0,
|
|
377
|
+
skipped: 0,
|
|
378
|
+
awaiting_approval: 0,
|
|
379
|
+
dead_letter: 0,
|
|
380
|
+
};
|
|
381
|
+
for (const row of rows) {
|
|
382
|
+
if (Object.hasOwn(byStatus, row.status)) byStatus[row.status] = Number(row.count || 0);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
project: this.project,
|
|
386
|
+
tenantId: this.tenantId,
|
|
387
|
+
queueName: this.queueName,
|
|
388
|
+
byStatus,
|
|
389
|
+
pending: byStatus.pending,
|
|
390
|
+
claimed: byStatus.claimed,
|
|
391
|
+
deadLetter: byStatus.dead_letter,
|
|
392
|
+
total: Object.values(byStatus).reduce((sum, n) => sum + n, 0),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
package/lib/reflect.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS, inferKnowledgeTarget } from './knowl
|
|
|
14
14
|
import { extractSessionObservation } from './reflect/extractor.mjs';
|
|
15
15
|
import { readContextState, contextSummaryLine } from './context-state.mjs';
|
|
16
16
|
import { configDir } from './config/xdg.mjs';
|
|
17
|
+
import { isMainModule } from './roots.mjs';
|
|
17
18
|
|
|
18
19
|
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
19
20
|
const USER_ENV_PATH = join(configDir(HOME), 'config.env');
|
|
@@ -253,7 +254,7 @@ function parseTranscript(path) {
|
|
|
253
254
|
/**
|
|
254
255
|
* CLI entry point for direct execution.
|
|
255
256
|
*/
|
|
256
|
-
if (import.meta.url
|
|
257
|
+
if (isMainModule(import.meta.url)) {
|
|
257
258
|
const args = process.argv.slice(2);
|
|
258
259
|
runReflectCli(args).catch((err) => {
|
|
259
260
|
console.error(`Error: ${err.message}`);
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/registry/assemble.mjs — Runtime merge of modular org files into registry v3.
|
|
3
3
|
*
|
|
4
|
-
* Walks specialists/org/{groups,teams,specialists,contracts,policies},
|
|
5
|
-
*
|
|
4
|
+
* Walks specialists/org/{groups,teams,specialists,contracts,policies}, then
|
|
5
|
+
* merges two user/project extension tiers on top, builtin -> user -> project
|
|
6
|
+
* (ADR-0052's precedence model, applied here to the org registry): a home-level
|
|
7
|
+
* custom org tree (~/.construct/org/**, construct-rf26.13 — a user's personal
|
|
8
|
+
* custom specialists/teams shared across all their projects), then the existing
|
|
9
|
+
* .cx/org project overlay (highest precedence). Later tiers win on id collision.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import fs from 'node:fs';
|
|
9
13
|
import path from 'node:path';
|
|
14
|
+
import { homeDir } from '../paths.mjs';
|
|
15
|
+
|
|
16
|
+
function customOrgRoot() {
|
|
17
|
+
return path.join(homeDir(), '.construct', 'org');
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
function readJsonDir(dir) {
|
|
12
21
|
const out = {};
|
|
@@ -66,6 +75,8 @@ export function orgDirMtime(rootDir) {
|
|
|
66
75
|
}
|
|
67
76
|
};
|
|
68
77
|
walk(orgRoot);
|
|
78
|
+
const homeOrg = customOrgRoot();
|
|
79
|
+
if (fs.existsSync(homeOrg)) walk(homeOrg);
|
|
69
80
|
const overlay = path.join(rootDir, '.cx', 'org');
|
|
70
81
|
if (fs.existsSync(overlay)) walk(overlay);
|
|
71
82
|
return max;
|
|
@@ -85,6 +96,21 @@ export function assembleRegistry(rootDir) {
|
|
|
85
96
|
const contracts = readJsonDir(path.join(orgRoot, 'contracts'));
|
|
86
97
|
const policies = readJsonDir(path.join(orgRoot, 'policies'));
|
|
87
98
|
|
|
99
|
+
const homeOrgRoot = customOrgRoot();
|
|
100
|
+
if (fs.existsSync(homeOrgRoot)) {
|
|
101
|
+
for (const [id, val] of Object.entries(readJsonDir(path.join(homeOrgRoot, 'groups')))) {
|
|
102
|
+
groups[id] = { ...groups[id], ...val };
|
|
103
|
+
}
|
|
104
|
+
for (const [id, val] of Object.entries(readJsonDir(path.join(homeOrgRoot, 'teams')))) {
|
|
105
|
+
squads[id] = { ...squads[id], ...val };
|
|
106
|
+
}
|
|
107
|
+
for (const [id, val] of Object.entries(readJsonDir(path.join(homeOrgRoot, 'specialists')))) {
|
|
108
|
+
specialists[id] = { ...specialists[id], ...val };
|
|
109
|
+
}
|
|
110
|
+
mergeOverlaySection(contracts, readJsonDir(path.join(homeOrgRoot, 'contracts')));
|
|
111
|
+
mergeOverlaySection(policies, readJsonDir(path.join(homeOrgRoot, 'policies')));
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
if (fs.existsSync(overlayRoot)) {
|
|
89
115
|
for (const [id, val] of Object.entries(readJsonDir(path.join(overlayRoot, 'groups')))) {
|
|
90
116
|
groups[id] = { ...groups[id], ...val };
|
|
@@ -9,6 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { loadRegistry } from '../registry/loader.mjs';
|
|
12
|
+
import { bindingForSpecialist } from '../roles/flavor-bindings.mjs';
|
|
12
13
|
|
|
13
14
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..');
|
|
@@ -81,12 +82,18 @@ export function triageBoundOrphans({ rootDir = REPO_ROOT } = {}) {
|
|
|
81
82
|
} else if (base && profileRoles.has(base)) {
|
|
82
83
|
category = 'B-composer';
|
|
83
84
|
recommendation = `Profile role "${base}" — may load via profile overlay; verify before merge`;
|
|
85
|
+
} else if (base && bindingForSpecialist(base)?.specialistId) {
|
|
86
|
+
// Base name doesn't match a specialist's own name, but
|
|
87
|
+
// lib/roles/flavor-bindings.mjs still routes it onto a live anchor —
|
|
88
|
+
// reachable via prompt-composer under that anchor's identity.
|
|
89
|
+
category = 'B-composer';
|
|
90
|
+
recommendation = `Role flavor bound to ${bindingForSpecialist(base).specialistId} via lib/roles/flavor-bindings.mjs — reachable via prompt-composer; document in registry or bind explicitly`;
|
|
84
91
|
} else if (skill.startsWith('roles/')) {
|
|
85
92
|
category = 'C-merge';
|
|
86
93
|
recommendation = 'Consider merging into parent roles/<base>.md or binding to specialist';
|
|
87
94
|
} else if (skill.startsWith('docs/')) {
|
|
88
95
|
category = 'A-bind';
|
|
89
|
-
recommendation = 'Bind to cx-
|
|
96
|
+
recommendation = 'Bind to cx-operations or domain owner specialist';
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
items.push({ skill, category, recommendation });
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/registry/custom-scaffold.mjs — CLI-facing scaffolding for user-authored
|
|
3
|
+
* custom specialists and teams (construct-rf26.13).
|
|
4
|
+
*
|
|
5
|
+
* Writes into one of two extension tiers, both read back by
|
|
6
|
+
* lib/registry/assemble.mjs at the same precedence ADR-0052 establishes for
|
|
7
|
+
* provider manifests (builtin -> user -> project):
|
|
8
|
+
* - "user" ~/.construct/org/** — usable across every project on the machine
|
|
9
|
+
* - "project" <rootDir>/.cx/org/** — git-tracked, highest precedence, scoped
|
|
10
|
+
* to one project (the existing overlay
|
|
11
|
+
* introduced by ADR-0046)
|
|
12
|
+
*
|
|
13
|
+
* Refuses to overwrite an existing file unless { force: true }, mirroring
|
|
14
|
+
* lib/specialists/scaffold.mjs's createSpecialistDraft convention. Validates
|
|
15
|
+
* with lib/registry/custom-schema.mjs before writing anything, so a scaffolded
|
|
16
|
+
* record is guaranteed registry-loadable the moment it lands on disk.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { homeDir } from '../paths.mjs';
|
|
22
|
+
import { assembleRegistry } from './assemble.mjs';
|
|
23
|
+
import { validateCustomSpecialist, validateCustomTeam } from './custom-schema.mjs';
|
|
24
|
+
|
|
25
|
+
const ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
26
|
+
|
|
27
|
+
export function customOrgDir(scope, { rootDir = process.cwd() } = {}) {
|
|
28
|
+
if (scope === 'user') return path.join(homeDir(), '.construct', 'org');
|
|
29
|
+
return path.join(rootDir, '.cx', 'org');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function existingTeamIds(rootDir) {
|
|
33
|
+
try {
|
|
34
|
+
return Object.keys(assembleRegistry(rootDir).teams);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderPromptStub({ id, role, description }) {
|
|
41
|
+
return [
|
|
42
|
+
`# cx-${id}`,
|
|
43
|
+
'',
|
|
44
|
+
`Role: ${role}`,
|
|
45
|
+
'',
|
|
46
|
+
description || 'TODO: describe this specialist\'s perspective and output format.',
|
|
47
|
+
'',
|
|
48
|
+
'## Anti-fabrication contract',
|
|
49
|
+
'',
|
|
50
|
+
'Every load-bearing claim cites a source the reader can re-verify. When a fact is not in the source, write `unknown` or `[unverified]`. See `rules/common/no-fabrication.md`.',
|
|
51
|
+
'',
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Scaffold a custom specialist JSON record (+ a minimal prompt stub) into the
|
|
57
|
+
* requested tier. Throws with actionable, field-named errors on any failure.
|
|
58
|
+
* @returns {{ path: string, relPath: string, promptPath: string, record: object, scope: string }}
|
|
59
|
+
*/
|
|
60
|
+
export function createCustomSpecialist({
|
|
61
|
+
rootDir = process.cwd(),
|
|
62
|
+
scope = 'project',
|
|
63
|
+
id,
|
|
64
|
+
role,
|
|
65
|
+
description,
|
|
66
|
+
modelTier = 'standard',
|
|
67
|
+
reasoningEffort,
|
|
68
|
+
skills = [],
|
|
69
|
+
fence,
|
|
70
|
+
team,
|
|
71
|
+
teamId,
|
|
72
|
+
handoffCandidates = [],
|
|
73
|
+
claudeTools = 'Read,Grep,Glob,LS',
|
|
74
|
+
displayName,
|
|
75
|
+
force = false,
|
|
76
|
+
} = {}) {
|
|
77
|
+
if (!id || !ID_RE.test(id)) {
|
|
78
|
+
throw new Error(`invalid specialist id "${id}" — use lowercase kebab-case (e.g. "widget-specialist")`);
|
|
79
|
+
}
|
|
80
|
+
if (scope !== 'user' && scope !== 'project') {
|
|
81
|
+
throw new Error(`invalid scope "${scope}" — use "user" or "project"`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const orgDir = customOrgDir(scope, { rootDir });
|
|
85
|
+
const specialistsDir = path.join(orgDir, 'specialists');
|
|
86
|
+
const filePath = path.join(specialistsDir, `cx-${id}.json`);
|
|
87
|
+
if (fs.existsSync(filePath) && !force) {
|
|
88
|
+
throw new Error(`${filePath} already exists — pass force to overwrite`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolvedTeam = team || teamId;
|
|
92
|
+
const knownTeams = existingTeamIds(rootDir);
|
|
93
|
+
if (resolvedTeam && knownTeams && !knownTeams.includes(resolvedTeam)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`team "${resolvedTeam}" does not exist — create it first with \`construct team create ${resolvedTeam} --owner=${role || id}\`, ` +
|
|
96
|
+
`or reference one of: ${knownTeams.slice(0, 10).join(', ')}${knownTeams.length > 10 ? ', …' : ''}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const promptRelPath = path.posix.join(
|
|
101
|
+
path.relative(rootDir, orgDir).split(path.sep).join('/'),
|
|
102
|
+
'prompts',
|
|
103
|
+
`cx-${id}.md`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const record = {
|
|
107
|
+
name: id,
|
|
108
|
+
displayName: displayName || description,
|
|
109
|
+
description,
|
|
110
|
+
role: role || id,
|
|
111
|
+
modelTier,
|
|
112
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
113
|
+
skills,
|
|
114
|
+
fence: fence || { allowedPaths: [] },
|
|
115
|
+
claudeTools,
|
|
116
|
+
promptFile: promptRelPath,
|
|
117
|
+
handoffCandidates,
|
|
118
|
+
team: resolvedTeam,
|
|
119
|
+
teamId: resolvedTeam,
|
|
120
|
+
internal: true,
|
|
121
|
+
custom: true,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const errors = validateCustomSpecialist(record, { rootDir, checkPromptFileExists: false });
|
|
125
|
+
if (errors.length) {
|
|
126
|
+
throw new Error(`custom specialist "${id}" failed validation:\n ${errors.join('\n ')}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const promptDir = path.join(orgDir, 'prompts');
|
|
130
|
+
const promptPath = path.join(rootDir, promptRelPath);
|
|
131
|
+
fs.mkdirSync(promptDir, { recursive: true });
|
|
132
|
+
if (!fs.existsSync(promptPath) || force) {
|
|
133
|
+
fs.writeFileSync(promptPath, renderPromptStub({ id, role: record.role, description }));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fs.mkdirSync(specialistsDir, { recursive: true });
|
|
137
|
+
fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + '\n');
|
|
138
|
+
|
|
139
|
+
return { path: filePath, relPath: path.relative(rootDir, filePath), promptPath, record, scope };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Scaffold a custom team JSON record into the requested tier. Throws with
|
|
144
|
+
* actionable, field-named errors on any failure.
|
|
145
|
+
* @returns {{ path: string, relPath: string, record: object, scope: string }}
|
|
146
|
+
*/
|
|
147
|
+
export function createCustomTeam({
|
|
148
|
+
rootDir = process.cwd(),
|
|
149
|
+
scope = 'project',
|
|
150
|
+
id,
|
|
151
|
+
name,
|
|
152
|
+
owner,
|
|
153
|
+
roles,
|
|
154
|
+
specialists = [],
|
|
155
|
+
charter,
|
|
156
|
+
decisionRights = [],
|
|
157
|
+
forbiddenDecisions = [],
|
|
158
|
+
escalationPath,
|
|
159
|
+
contact = {},
|
|
160
|
+
groupId,
|
|
161
|
+
force = false,
|
|
162
|
+
} = {}) {
|
|
163
|
+
if (!id || !ID_RE.test(id)) {
|
|
164
|
+
throw new Error(`invalid team id "${id}" — use lowercase kebab-case (e.g. "widget-team")`);
|
|
165
|
+
}
|
|
166
|
+
if (scope !== 'user' && scope !== 'project') {
|
|
167
|
+
throw new Error(`invalid scope "${scope}" — use "user" or "project"`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const orgDir = customOrgDir(scope, { rootDir });
|
|
171
|
+
const teamsDir = path.join(orgDir, 'teams');
|
|
172
|
+
const filePath = path.join(teamsDir, `${id}.json`);
|
|
173
|
+
if (fs.existsSync(filePath) && !force) {
|
|
174
|
+
throw new Error(`${filePath} already exists — pass force to overwrite`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const resolvedRoles = roles && roles.length ? roles : (owner ? [owner] : []);
|
|
178
|
+
const record = {
|
|
179
|
+
name: name || id,
|
|
180
|
+
owner,
|
|
181
|
+
roles: resolvedRoles,
|
|
182
|
+
specialists,
|
|
183
|
+
decisionRights,
|
|
184
|
+
forbiddenDecisions,
|
|
185
|
+
escalationPath: escalationPath && escalationPath.length ? escalationPath : [owner, 'orchestrator'].filter(Boolean),
|
|
186
|
+
charter,
|
|
187
|
+
contact,
|
|
188
|
+
...(groupId ? { groupId } : {}),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const errors = validateCustomTeam({ id, ...record });
|
|
192
|
+
if (errors.length) {
|
|
193
|
+
throw new Error(`custom team "${id}" failed validation:\n ${errors.join('\n ')}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.mkdirSync(teamsDir, { recursive: true });
|
|
197
|
+
fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + '\n');
|
|
198
|
+
|
|
199
|
+
return { path: filePath, relPath: path.relative(rootDir, filePath), record, scope };
|
|
200
|
+
}
|