@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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/watchers/oracle-liveness.mjs — oracle producer-liveness watcher.
|
|
3
|
+
*
|
|
4
|
+
* Applies the same idle-safe gate as lib/intake/session-prelude.mjs's
|
|
5
|
+
* buildOraclePrelude()/readOracleDockState(): the oracle daemon idles itself
|
|
6
|
+
* out after a few no-work ticks, so a stale last-tick record alone is not
|
|
7
|
+
* death — only heartbeatFreshness().stale AND at least one pending oracle
|
|
8
|
+
* item (lib/oracle/actions.mjs listPending(projectDir), filtered to
|
|
9
|
+
* status==='pending') together mean the producer looks stalled rather than
|
|
10
|
+
* quiet. Escalates once per stale episode (deduped by the last-tick record's
|
|
11
|
+
* own `asOf`, so a producer stuck stale for hours doesn't spam every tick);
|
|
12
|
+
* a fresh tick or an empty pending queue clears the episode. Tick: 60s.
|
|
13
|
+
*
|
|
14
|
+
* Needs the project directory (not just homeDir) to call listPending — the
|
|
15
|
+
* doctor daemon's tick loop (lib/doctor/index.mjs) threads `projectDir`
|
|
16
|
+
* through runWatcher()/tick(ctx) for this reason; every other watcher
|
|
17
|
+
* ignores that extra argument.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readHeartbeatStatus, formatAge } from '../../oracle/heartbeat.mjs';
|
|
21
|
+
import { listPending } from '../../oracle/actions.mjs';
|
|
22
|
+
import { record } from '../audit.mjs';
|
|
23
|
+
import { escalate } from '../escalate.mjs';
|
|
24
|
+
|
|
25
|
+
export const name = 'oracle-liveness';
|
|
26
|
+
export const intervalMs = 60 * 1000;
|
|
27
|
+
|
|
28
|
+
let lastEscalatedAsOf = null;
|
|
29
|
+
|
|
30
|
+
export async function tick({ projectDir = process.cwd(), env = process.env, homeDir } = {}) {
|
|
31
|
+
const actions = [];
|
|
32
|
+
const escalations = [];
|
|
33
|
+
const notes = [];
|
|
34
|
+
|
|
35
|
+
const heartbeat = readHeartbeatStatus({ homeDir, env });
|
|
36
|
+
if (!heartbeat.enabled) {
|
|
37
|
+
notes.push({ enabled: false });
|
|
38
|
+
return { actions, escalations, notes };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let pending;
|
|
42
|
+
try {
|
|
43
|
+
pending = listPending(projectDir).filter((p) => p.status === 'pending');
|
|
44
|
+
} catch (err) {
|
|
45
|
+
notes.push({ error: `listPending failed: ${err.message}` });
|
|
46
|
+
return { actions, escalations, notes };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
notes.push({ stale: heartbeat.stale, ageMs: heartbeat.ageMs, asOf: heartbeat.asOf, pendingCount: pending.length });
|
|
50
|
+
|
|
51
|
+
const stalled = heartbeat.stale && pending.length > 0;
|
|
52
|
+
|
|
53
|
+
if (!stalled) {
|
|
54
|
+
if (lastEscalatedAsOf) {
|
|
55
|
+
record({
|
|
56
|
+
kind: 'recovery',
|
|
57
|
+
watcher: name,
|
|
58
|
+
summary: `oracle producer recovered (fresh tick or pending queue cleared; was stalled as of ${lastEscalatedAsOf})`,
|
|
59
|
+
context: { previousAsOf: lastEscalatedAsOf },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
lastEscalatedAsOf = null;
|
|
63
|
+
return { actions, escalations, notes };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const episodeKey = heartbeat.asOf || 'unknown';
|
|
67
|
+
if (lastEscalatedAsOf === episodeKey) return { actions, escalations, notes };
|
|
68
|
+
|
|
69
|
+
const summary = `oracle-producer-stalled: no oracle tick in ${formatAge(heartbeat.ageMs)} while ${pending.length} pending oracle work item(s) wait (last tick as of ${heartbeat.asOf ?? 'unknown'})`;
|
|
70
|
+
record({
|
|
71
|
+
kind: 'sample',
|
|
72
|
+
watcher: name,
|
|
73
|
+
result: 'stalled',
|
|
74
|
+
summary,
|
|
75
|
+
context: { ageMs: heartbeat.ageMs, asOf: heartbeat.asOf, pendingCount: pending.length },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const result = await escalate({
|
|
79
|
+
watcher: name,
|
|
80
|
+
eventType: 'oracle.producer_stalled',
|
|
81
|
+
summary,
|
|
82
|
+
context: { ageMs: heartbeat.ageMs, asOf: heartbeat.asOf, pendingCount: pending.length },
|
|
83
|
+
});
|
|
84
|
+
lastEscalatedAsOf = episodeKey;
|
|
85
|
+
escalations.push({ eventType: 'oracle.producer_stalled', result });
|
|
86
|
+
|
|
87
|
+
return { actions, escalations, notes };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function __resetOracleLivenessWatcherState() {
|
|
91
|
+
lastEscalatedAsOf = null;
|
|
92
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/watchers/orchestration-runs.mjs — surfaces error/degraded and
|
|
3
|
+
* stale awaiting-host orchestration runs to the doctor audit log.
|
|
4
|
+
*
|
|
5
|
+
* lib/orchestration/runtime.mjs persists a terminal 'error' status (thrown
|
|
6
|
+
* mid-execution) or a 'degraded'/'completed-with-failures' status (a run
|
|
7
|
+
* that finished but not cleanly) to the run store via getRun/getRuns. Each
|
|
8
|
+
* tick here scans the most recent runs for the project the daemon is
|
|
9
|
+
* running in and records a finding when any carry an error or
|
|
10
|
+
* degraded/failed terminal status, so `construct doctor logs
|
|
11
|
+
* --watcher=orchestration-runs` (and the audit trail generally) reflects
|
|
12
|
+
* runs that did not complete cleanly, not just clean completions.
|
|
13
|
+
*
|
|
14
|
+
* A host-backend run standing at 'awaiting-host' is not itself an error — it
|
|
15
|
+
* is the designed non-terminal state while a host executes the materialized
|
|
16
|
+
* prompts — but a run whose `updatedAt` has not moved in
|
|
17
|
+
* CONSTRUCT_AWAITING_HOST_STALE_MS (default 30 minutes) means the host that
|
|
18
|
+
* requested it likely disconnected before submitting every task result.
|
|
19
|
+
* That is flagged as a separate advisory finding, never a hard failure, so an
|
|
20
|
+
* abandoned run stays visible rather than silently invisible.
|
|
21
|
+
*
|
|
22
|
+
* Read-only and advisory: this watcher never mutates a run or retries it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { record } from '../audit.mjs';
|
|
26
|
+
import { resolveNonNegativeSetting } from '../../env-config.mjs';
|
|
27
|
+
|
|
28
|
+
const ATTENTION_STATUSES = new Set(['error', 'degraded', 'completed-with-failures']);
|
|
29
|
+
|
|
30
|
+
// 30 minutes: long enough that a host actively working through a multi-task
|
|
31
|
+
// run (each submission round-trips the calling agent's own reasoning) is never
|
|
32
|
+
// falsely flagged, short enough that a genuinely disconnected session surfaces
|
|
33
|
+
// within one doctor watch cycle or two.
|
|
34
|
+
|
|
35
|
+
const DEFAULT_AWAITING_HOST_STALE_MS = 30 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
export const name = 'orchestration-runs';
|
|
38
|
+
export const intervalMs = 10 * 60 * 1000;
|
|
39
|
+
|
|
40
|
+
function projectRoot() {
|
|
41
|
+
return process.env.CONSTRUCT_PROJECT_ROOT || process.cwd();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function needsAttention(run) {
|
|
45
|
+
return ATTENTION_STATUSES.has(run.status) || run.degraded === true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isStaleAwaitingHost(run, staleMs, now) {
|
|
49
|
+
if (run.status !== 'awaiting-host') return false;
|
|
50
|
+
const touchedAt = Date.parse(run.updatedAt || run.createdAt || '');
|
|
51
|
+
if (!Number.isFinite(touchedAt)) return false;
|
|
52
|
+
return now - touchedAt >= staleMs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function tick() {
|
|
56
|
+
const actions = [];
|
|
57
|
+
const escalations = [];
|
|
58
|
+
const root = projectRoot();
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const staleMs = resolveNonNegativeSetting(process.env, 'CONSTRUCT_AWAITING_HOST_STALE_MS', DEFAULT_AWAITING_HOST_STALE_MS);
|
|
61
|
+
|
|
62
|
+
const { getRuns } = await import('../../orchestration/runtime.mjs');
|
|
63
|
+
const runs = await getRuns(root, { env: process.env, limit: 50 });
|
|
64
|
+
const affected = runs.filter(needsAttention);
|
|
65
|
+
const staleAwaitingHost = runs.filter((r) => isStaleAwaitingHost(r, staleMs, now));
|
|
66
|
+
|
|
67
|
+
if (affected.length > 0) {
|
|
68
|
+
// getRuns/listRuns sorts newest-first (createdAt desc) across every store
|
|
69
|
+
// backend, so the first affected entry is the newest.
|
|
70
|
+
const newest = affected[0];
|
|
71
|
+
record({
|
|
72
|
+
kind: 'finding',
|
|
73
|
+
watcher: name,
|
|
74
|
+
result: 'attention',
|
|
75
|
+
summary: `${affected.length} of ${runs.length} recent run(s) carry an error/degraded status; newest is ${newest.runId} (${newest.status})`,
|
|
76
|
+
context: {
|
|
77
|
+
scanned: runs.length,
|
|
78
|
+
affectedCount: affected.length,
|
|
79
|
+
newestRunId: newest.runId,
|
|
80
|
+
newestStatus: newest.status,
|
|
81
|
+
affectedRunIds: affected.map((r) => r.runId).slice(0, 10),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (staleAwaitingHost.length > 0) {
|
|
87
|
+
const newest = staleAwaitingHost[0];
|
|
88
|
+
record({
|
|
89
|
+
kind: 'finding',
|
|
90
|
+
watcher: name,
|
|
91
|
+
result: 'attention',
|
|
92
|
+
summary: `${staleAwaitingHost.length} of ${runs.length} recent run(s) have stood 'awaiting-host' for over ${Math.round(staleMs / 60000)} minute(s) with no submitted result; newest is ${newest.runId}`,
|
|
93
|
+
context: {
|
|
94
|
+
scanned: runs.length,
|
|
95
|
+
staleAwaitingHostCount: staleAwaitingHost.length,
|
|
96
|
+
staleThresholdMs: staleMs,
|
|
97
|
+
newestRunId: newest.runId,
|
|
98
|
+
staleRunIds: staleAwaitingHost.map((r) => r.runId).slice(0, 10),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (affected.length === 0 && staleAwaitingHost.length === 0) {
|
|
104
|
+
return { actions, escalations, notes: [{ scanned: runs.length, affected: 0, staleAwaitingHost: 0 }] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { actions, escalations, notes: [{ scanned: runs.length, affected: affected.length, staleAwaitingHost: staleAwaitingHost.length }] };
|
|
108
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/watchers/provider-breaker.mjs — surfaces open provider circuit breakers.
|
|
3
|
+
*
|
|
4
|
+
* Reads lib/providers/circuit-breaker.mjs's describeBreakers(), which lib/
|
|
5
|
+
* providers/registry.mjs populates by wrapping every resolved provider's read/
|
|
6
|
+
* search/watch/write/webhook methods (failureThreshold=5, cooldownMs=30_000).
|
|
7
|
+
* Surfaces that breaker state continuously, so a downed provider shows as
|
|
8
|
+
* "provider X circuit OPEN since T, N failures" in the doctor audit log and
|
|
9
|
+
* role-escalation path instead of a mystery failure.
|
|
10
|
+
*
|
|
11
|
+
* Escalates once per open episode (keyed by the breaker's own `openedAt`), not
|
|
12
|
+
* once per tick, so a provider stuck OPEN for an hour doesn't spam. Recovery
|
|
13
|
+
* to CLOSED/HALF_OPEN logs a plain recovery record. Tick: 60s.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { record } from '../audit.mjs';
|
|
17
|
+
import { escalate } from '../escalate.mjs';
|
|
18
|
+
import { describeBreakers } from '../../providers/circuit-breaker.mjs';
|
|
19
|
+
|
|
20
|
+
export const name = 'provider-breaker';
|
|
21
|
+
export const intervalMs = 60 * 1000;
|
|
22
|
+
|
|
23
|
+
const PROVIDER_KEY_PREFIX = 'provider:';
|
|
24
|
+
const escalatedOpenedAt = {};
|
|
25
|
+
|
|
26
|
+
export async function tick() {
|
|
27
|
+
const actions = [];
|
|
28
|
+
const escalations = [];
|
|
29
|
+
const notes = [];
|
|
30
|
+
|
|
31
|
+
const breakers = describeBreakers().filter((b) => b.key.startsWith(PROVIDER_KEY_PREFIX));
|
|
32
|
+
|
|
33
|
+
for (const breaker of breakers) {
|
|
34
|
+
const providerId = breaker.key.slice(PROVIDER_KEY_PREFIX.length);
|
|
35
|
+
notes.push({ id: providerId, state: breaker.state, failureCount: breaker.failureCount });
|
|
36
|
+
|
|
37
|
+
if (breaker.state !== 'open') {
|
|
38
|
+
if (escalatedOpenedAt[providerId]) {
|
|
39
|
+
record({
|
|
40
|
+
kind: 'recovery',
|
|
41
|
+
watcher: name,
|
|
42
|
+
target: providerId,
|
|
43
|
+
summary: `${providerId} provider circuit recovered (was OPEN since ${new Date(escalatedOpenedAt[providerId]).toISOString()})`,
|
|
44
|
+
context: { previousOpenedAt: escalatedOpenedAt[providerId] },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
delete escalatedOpenedAt[providerId];
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (escalatedOpenedAt[providerId] === breaker.openedAt) continue;
|
|
52
|
+
|
|
53
|
+
const summary = `${providerId} provider circuit OPEN since ${new Date(breaker.openedAt).toISOString()} (${breaker.failureCount} consecutive failures)`;
|
|
54
|
+
record({
|
|
55
|
+
kind: 'sample',
|
|
56
|
+
watcher: name,
|
|
57
|
+
target: providerId,
|
|
58
|
+
result: 'open',
|
|
59
|
+
summary,
|
|
60
|
+
context: { openedAt: breaker.openedAt, failureCount: breaker.failureCount },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await escalate({
|
|
64
|
+
watcher: name,
|
|
65
|
+
eventType: 'provider.circuit_open',
|
|
66
|
+
summary,
|
|
67
|
+
context: { provider: providerId, openedAt: breaker.openedAt, failureCount: breaker.failureCount },
|
|
68
|
+
});
|
|
69
|
+
escalatedOpenedAt[providerId] = breaker.openedAt;
|
|
70
|
+
escalations.push({ eventType: 'provider.circuit_open', provider: providerId, result });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { actions, escalations, notes };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function __resetProviderBreakerWatcherState() {
|
|
77
|
+
for (const key of Object.keys(escalatedOpenedAt)) delete escalatedOpenedAt[key];
|
|
78
|
+
}
|
package/lib/document-export.mjs
CHANGED
|
@@ -29,28 +29,45 @@ import { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
|
|
|
29
29
|
import { convertDocxToDoc, libreOfficeInstallHint, libreOfficePresent, resolveLibreOfficeBin } from './libreoffice-export.mjs';
|
|
30
30
|
import { resolveExportBranding } from './export-branding.mjs';
|
|
31
31
|
|
|
32
|
-
export const EXPORT_FORMATS = ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'];
|
|
33
|
-
|
|
34
32
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
35
33
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
34
|
+
// LMCP-B7: the format→engine map ships as lib/registry/manifests/format-engines.default.json
|
|
35
|
+
// (Pandoc drives typeset formats; pptxgenjs builds slide decks; legacy `.doc` has no
|
|
36
|
+
// pandoc writer — export DOCX first, then LibreOffice headless down-converts), with an
|
|
37
|
+
// optional `.cx/registry/format-engines.json` project override merged in per-resolve.
|
|
38
|
+
// An override entry adds a format or replaces a default one; every other default format
|
|
39
|
+
// survives untouched. EXPORT_FORMATS stays the default-only key list (byte-identical to
|
|
40
|
+
// the prior hardcoded array) since it's a static, non-cwd-aware export consumed by CLI
|
|
41
|
+
// help text; detect()/exportMarkdown() resolve the cwd-aware merged map per call instead.
|
|
42
|
+
|
|
43
|
+
const DEFAULT_FORMAT_ENGINES_PATH = path.join(REPO_ROOT, 'lib', 'registry', 'manifests', 'format-engines.default.json');
|
|
44
|
+
|
|
45
|
+
function readJsonIfExists(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(filePath)) return null;
|
|
48
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadDefaultFormatEngines() {
|
|
55
|
+
const manifest = readJsonIfExists(DEFAULT_FORMAT_ENGINES_PATH);
|
|
56
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
57
|
+
throw new Error(`lib/document-export.mjs: missing or malformed default manifest at ${DEFAULT_FORMAT_ENGINES_PATH}`);
|
|
58
|
+
}
|
|
59
|
+
return manifest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_FORMAT_ENGINES = loadDefaultFormatEngines();
|
|
63
|
+
|
|
64
|
+
export const EXPORT_FORMATS = Object.keys(DEFAULT_FORMAT_ENGINES);
|
|
65
|
+
|
|
66
|
+
export function resolveFormatEngines({ repoRoot = REPO_ROOT, cwd = process.cwd() } = {}) {
|
|
67
|
+
const base = repoRoot === REPO_ROOT ? DEFAULT_FORMAT_ENGINES : loadDefaultFormatEngines();
|
|
68
|
+
const override = readJsonIfExists(path.join(cwd, '.cx', 'registry', 'format-engines.json'));
|
|
69
|
+
return override && typeof override === 'object' ? { ...base, ...override } : base;
|
|
70
|
+
}
|
|
54
71
|
|
|
55
72
|
export function htmlTemplatePath(repoRoot = REPO_ROOT) {
|
|
56
73
|
const p = path.join(repoRoot, 'templates', 'distribution', 'construct-web.html');
|
|
@@ -190,7 +207,11 @@ function binVersion(name, args = ['--version'], env = process.env) {
|
|
|
190
207
|
return line || null;
|
|
191
208
|
}
|
|
192
209
|
|
|
193
|
-
|
|
210
|
+
// Exported so lib/doctor/engine-health.mjs (LMCP-K3) can surface the same
|
|
211
|
+
// per-binary install hint construct doctor reports, without duplicating the
|
|
212
|
+
// hint strings.
|
|
213
|
+
|
|
214
|
+
export function installHint(name) {
|
|
194
215
|
if (name === 'pandoc') return 'Install pandoc to enable document export (e.g. `brew install pandoc` on macOS, `apt install pandoc` on Debian/Ubuntu, or https://pandoc.org/installing.html).';
|
|
195
216
|
if (name === 'typst') return 'Install typst to enable PDF export via Pandoc (`brew install typst` on macOS, https://github.com/typst/typst/releases for binaries).';
|
|
196
217
|
if (name === 'construct-pdf.typ') return 'Bundled PDF template missing from templates/distribution/construct-pdf.typ (Construct install integrity issue).';
|
|
@@ -209,12 +230,14 @@ export function detect(format, env = process.env, {
|
|
|
209
230
|
branding = 'construct',
|
|
210
231
|
cwd = process.cwd(),
|
|
211
232
|
repoRoot = REPO_ROOT,
|
|
233
|
+
libreOfficeExistsSyncFn,
|
|
212
234
|
} = {}) {
|
|
213
235
|
const brandingPolicy = resolveExportBranding(format, branding);
|
|
236
|
+
const formatEngines = resolveFormatEngines({ repoRoot, cwd });
|
|
214
237
|
const config = format === 'pptx' && brandingPolicy.applied === 'plain'
|
|
215
238
|
? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
|
|
216
|
-
:
|
|
217
|
-
if (!config) return { ok: false, format, branding: brandingPolicy, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${
|
|
239
|
+
: formatEngines[format];
|
|
240
|
+
if (!config) return { ok: false, format, branding: brandingPolicy, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${Object.keys(formatEngines).join(', ')}.` };
|
|
218
241
|
if (config.engine === 'copy') {
|
|
219
242
|
return { ok: true, format, branding: brandingPolicy, figures: false, present: true, binaries: [], missing: [], message: `Ready: source emit (${format})` };
|
|
220
243
|
}
|
|
@@ -237,7 +260,7 @@ export function detect(format, env = process.env, {
|
|
|
237
260
|
// the export passes --reference-doc only when present, so unlike the PDF/HTML/deck templates a
|
|
238
261
|
// missing reference degrades gracefully rather than blocking the export.
|
|
239
262
|
|
|
240
|
-
if (format === 'doc' && !libreOfficePresent(env)) {
|
|
263
|
+
if (format === 'doc' && !libreOfficePresent(env, libreOfficeExistsSyncFn ? { existsSyncFn: libreOfficeExistsSyncFn } : undefined)) {
|
|
241
264
|
required.push('libreoffice');
|
|
242
265
|
}
|
|
243
266
|
if (figures) {
|
|
@@ -257,7 +280,8 @@ export function detect(format, env = process.env, {
|
|
|
257
280
|
return { name, path: pptxgenPresent() ? 'bundled' : null, version: null };
|
|
258
281
|
}
|
|
259
282
|
if (name === 'libreoffice') {
|
|
260
|
-
|
|
283
|
+
const loOpts = libreOfficeExistsSyncFn ? { existsSyncFn: libreOfficeExistsSyncFn } : undefined;
|
|
284
|
+
return { name, path: libreOfficePresent(env, loOpts) ? resolveLibreOfficeBin(env, loOpts) : null, version: null };
|
|
261
285
|
}
|
|
262
286
|
return { name, path: whichBin(name, env), version: null };
|
|
263
287
|
});
|
|
@@ -356,7 +380,8 @@ export function exportMarkdown({
|
|
|
356
380
|
repoRoot = REPO_ROOT,
|
|
357
381
|
} = {}) {
|
|
358
382
|
if (!inputPath) return { ok: false, format, message: 'exportMarkdown: inputPath is required.' };
|
|
359
|
-
|
|
383
|
+
const formatEngines = resolveFormatEngines({ repoRoot, cwd });
|
|
384
|
+
if (!formatEngines[format]) return { ok: false, format, inputPath, message: `Unsupported format: ${format}. Supported: ${Object.keys(formatEngines).join(', ')}.` };
|
|
360
385
|
if (!fs.existsSync(inputPath)) return { ok: false, format, inputPath, message: `exportMarkdown: input does not exist: ${inputPath}` };
|
|
361
386
|
|
|
362
387
|
const metadata = parseArtifactMetadata(inputPath);
|
|
@@ -371,7 +396,7 @@ export function exportMarkdown({
|
|
|
371
396
|
|
|
372
397
|
const config = format === 'pptx' && brandingPolicy.applied === 'plain'
|
|
373
398
|
? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
|
|
374
|
-
:
|
|
399
|
+
: formatEngines[format];
|
|
375
400
|
const target = path.resolve(outputPath || defaultOutputPath(inputPath, format));
|
|
376
401
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
377
402
|
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/document-extract/docling-client.mjs — Node side of the docling sidecar.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Implements the ingestion sidecar process contract (ADR-0068): spawns one
|
|
5
|
+
* long-lived Python process per Node session, framed by newline-delimited
|
|
6
|
+
* JSON over stdin/stdout, as a direct child (so the sidecar's own parent-PID
|
|
7
|
+
* watch resolves the right PID with no explicit handoff needed). Avoids
|
|
8
|
+
* per-call uv/venv warm-up (~2s otherwise) and keeps the docling model
|
|
9
|
+
* resident.
|
|
7
10
|
*
|
|
8
11
|
* Public API:
|
|
9
12
|
* extractViaDocling(filePath) → { markdown, metadata, droppedInfo }
|
|
10
13
|
* shutdownDoclingSidecar() → idempotent process exit
|
|
11
14
|
*
|
|
12
|
-
* The first call provisions the venv on demand via
|
|
13
|
-
*
|
|
15
|
+
* The first call provisions the shared, machine-scoped venv on demand via
|
|
16
|
+
* ensureDoclingVenv() — never at `construct init`, only on first extraction
|
|
17
|
+
* (construct-rf26.16). Subsequent calls reuse the running sidecar.
|
|
14
18
|
*/
|
|
15
19
|
import { spawn } from 'node:child_process';
|
|
16
20
|
import path from 'node:path';
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
"""
|
|
3
3
|
lib/document-extract/docling-sidecar.py — long-lived JSON-RPC wrapper around docling.
|
|
4
4
|
|
|
5
|
+
Governed by the ingestion sidecar process contract (ADR-0068): one JSON-RPC
|
|
6
|
+
surface, no business logic beyond the extraction call itself — routing,
|
|
7
|
+
fallback selection, and degradation-chain decisions all live on the Node
|
|
8
|
+
side (lib/document-extract/docling-client.mjs, lib/document-ingest.mjs).
|
|
9
|
+
|
|
5
10
|
Protocol: newline-delimited JSON over stdin/stdout. Each request is
|
|
6
11
|
{"id": <int>, "method": <str>, "params": <obj>}. Each response is
|
|
7
12
|
{"id": <int>, "result": <obj>} or {"id": <int>, "error": {"code": <str>, "message": <str>}}.
|
|
@@ -11,6 +16,15 @@ Methods:
|
|
|
11
16
|
- extract {path} → {"markdown": "...", "metadata": {...}, "droppedInfo": [...]}
|
|
12
17
|
- shutdown → {"ok": true}; process exits after acknowledgement
|
|
13
18
|
|
|
19
|
+
Parent-PID watch (ADR-0068): spawned as a direct child of the Node process
|
|
20
|
+
via child_process.spawn, so os.getppid() at start is that Node process's PID.
|
|
21
|
+
A background thread polls it every PARENT_POLL_INTERVAL_S; a `kill -9` (or
|
|
22
|
+
any other death Node's own `process.on('exit')` handler cannot run for) still
|
|
23
|
+
leaves this sidecar exiting on its own rather than orphaned. POSIX reparents
|
|
24
|
+
an orphan to init (or the platform's subreaper), so a ppid change is a
|
|
25
|
+
reliable signal; on Windows, ppid does not get reparented the same way, so
|
|
26
|
+
this is best-effort there rather than a guarantee.
|
|
27
|
+
|
|
14
28
|
Best-practice notes (2026-06):
|
|
15
29
|
- One sidecar per Node session, kept warm to avoid uv/venv startup (~2s).
|
|
16
30
|
- Stdio JSON-RPC is the leanest Python↔Node IPC for non-LLM use cases;
|
|
@@ -20,10 +34,23 @@ Best-practice notes (2026-06):
|
|
|
20
34
|
loss is observable to the CLI, not silent.
|
|
21
35
|
"""
|
|
22
36
|
import json
|
|
37
|
+
import os
|
|
23
38
|
import sys
|
|
39
|
+
import threading
|
|
40
|
+
import time
|
|
24
41
|
import traceback
|
|
25
42
|
from pathlib import Path
|
|
26
43
|
|
|
44
|
+
PARENT_POLL_INTERVAL_S = 2.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def watch_parent(initial_ppid):
|
|
48
|
+
while True:
|
|
49
|
+
time.sleep(PARENT_POLL_INTERVAL_S)
|
|
50
|
+
if os.getppid() != initial_ppid:
|
|
51
|
+
os._exit(3)
|
|
52
|
+
|
|
53
|
+
|
|
27
54
|
try:
|
|
28
55
|
from docling.document_converter import DocumentConverter
|
|
29
56
|
from importlib.metadata import version as _pkg_version
|
|
@@ -125,6 +152,9 @@ def handle(request):
|
|
|
125
152
|
|
|
126
153
|
|
|
127
154
|
def main():
|
|
155
|
+
watcher = threading.Thread(target=watch_parent, args=(os.getppid(),), daemon=True)
|
|
156
|
+
watcher.start()
|
|
157
|
+
|
|
128
158
|
for line in sys.stdin:
|
|
129
159
|
line = line.strip()
|
|
130
160
|
if not line:
|
package/lib/document-extract.mjs
CHANGED
|
@@ -513,7 +513,7 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
|
|
|
513
513
|
// Throw a typed error so callers can route to .cx/intake/needs-asr/.
|
|
514
514
|
if (AUDIO_VIDEO_EXTS.has(extension)) {
|
|
515
515
|
const err = new Error(
|
|
516
|
-
`Audio/video extraction requires ASR.
|
|
516
|
+
`Audio/video extraction requires ASR. Install a local whisper ASR (brew install whisper-cpp). See docs/guides/intake/audio-video.md.`,
|
|
517
517
|
);
|
|
518
518
|
err.code = 'ASR_REQUIRED';
|
|
519
519
|
err.extension = extension;
|
package/lib/document-ingest.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'nod
|
|
|
11
11
|
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
|
|
12
12
|
import { extractDocumentTextAsync, extractDocumentTextNodeNative, extractDocumentMetadata, isExtractableDocumentPath } from './document-extract.mjs';
|
|
13
13
|
import { syncFileStateToSql } from './storage/sync.mjs';
|
|
14
|
+
import { purgeExpiredData } from './storage/admin.mjs';
|
|
14
15
|
import { stampFrontmatter } from './doc-stamp.mjs';
|
|
15
16
|
import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS } from './knowledge/layout.mjs';
|
|
16
17
|
import { loadProjectConfig } from './config/project-config.mjs';
|
|
@@ -338,6 +339,14 @@ export async function ingestDocuments(inputPaths, {
|
|
|
338
339
|
env,
|
|
339
340
|
project: inferProjectName(cwd),
|
|
340
341
|
});
|
|
342
|
+
|
|
343
|
+
// Retention purge on every real sync, matching the MCP storage_sync tool
|
|
344
|
+
// (lib/mcp/tools/storage.mjs) — best-effort since eviction must never
|
|
345
|
+
// block a successful ingest.
|
|
346
|
+
try {
|
|
347
|
+
const purge = await purgeExpiredData(cwd, { env, project: inferProjectName(cwd) });
|
|
348
|
+
if (purge.status === 'ok') syncResult = { ...syncResult, retention: purge };
|
|
349
|
+
} catch { /* best effort */ }
|
|
341
350
|
}
|
|
342
351
|
|
|
343
352
|
if (strict && totalDrops > 0) {
|