@geraldmaron/construct 1.4.1 → 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 +1029 -65
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- 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 +7 -30
- 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 +109 -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 +23 -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/embed/worker.mjs +5 -0
- 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 +60 -11
- package/lib/export-validate.mjs +34 -2
- 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/readiness.mjs +109 -0
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +29 -8
- 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/install/stage-project.mjs +41 -4
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/prepare.mjs +2 -0
- 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-approval.mjs +57 -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 +185 -1268
- 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-rate-limit.mjs +47 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +95 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +197 -19
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/project.mjs +25 -8
- 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 +2 -2
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +5 -76
- 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 +53 -20
- 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/daemon-entry.mjs +6 -0
- 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 +508 -62
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +60 -0
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +763 -80
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/output-quality.mjs +61 -2
- 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/path-policy.mjs +56 -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 +41 -18
- package/lib/providers/secret-resolver.mjs +263 -31
- 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/catalog.mjs +6 -0
- 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 +15 -4
- 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/registry/validate.mjs +3 -3
- 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/sandbox.mjs +1 -1
- 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 +93 -24
- 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 +84 -7
- 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 +20 -7
- package/personas/construct.md +12 -3
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1987 -0
- package/rules/common/comments.md +1 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +215 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +546 -0
- package/scripts/sync-specialists.mjs +336 -106
- 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 +41 -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 +27 -31
- 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/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
package/lib/init-unified.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { missingIgnorePatterns, isConstructPackageRepo } from './host-dispositio
|
|
|
26
26
|
import { HOST_KEYS, displayNameToKey } from './platforms/capabilities.mjs';
|
|
27
27
|
import { DOC_LANES, LANE_ORDER, DOC_PRESETS } from './init/doc-lanes.mjs';
|
|
28
28
|
import { initWorkflow } from './workflow-state.mjs';
|
|
29
|
+
import { isMainModule } from './roots.mjs';
|
|
29
30
|
|
|
30
31
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
31
32
|
const ROOT_DIR = path.join(__dirname, "..");
|
|
@@ -45,6 +46,7 @@ const withPostmortemsFlag = args.includes("--with-postmortems");
|
|
|
45
46
|
const withArchitectureFlag = args.includes("--with-architecture");
|
|
46
47
|
const withReadmeFlag = args.includes("--with-readme");
|
|
47
48
|
const withDevcontainerFlag = args.includes("--devcontainer");
|
|
49
|
+
const withSeedIndexFlag = args.includes("--seed-index");
|
|
48
50
|
|
|
49
51
|
// Behavior flags
|
|
50
52
|
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
@@ -56,6 +58,10 @@ const skipInteractive = !interactive;
|
|
|
56
58
|
// templates/ already exist (issue #97).
|
|
57
59
|
const forceScaffold = args.includes("--force");
|
|
58
60
|
|
|
61
|
+
// --no-beads skips tracker initialization entirely and marks beads as disabled
|
|
62
|
+
// in the result. Use when the host environment cannot run bd (e.g. ephemeral CI).
|
|
63
|
+
const noBeads = args.includes("--no-beads");
|
|
64
|
+
|
|
59
65
|
// `bd init` is git-native and unconditionally commits its bootstrap; ADR-0027
|
|
60
66
|
// §3 requires init to leave the host repo's commit history untouched unless the
|
|
61
67
|
// user opts in. --commit-bootstrap keeps that commit; the default withholds it.
|
|
@@ -986,41 +992,49 @@ async function main() {
|
|
|
986
992
|
skipped.push('.cx/construct_guide.md');
|
|
987
993
|
}
|
|
988
994
|
|
|
989
|
-
// Auto-init beads
|
|
995
|
+
// Auto-init beads. Fail-closed: a tracker failure leaves the project in an
|
|
996
|
+
// inconsistent state (no hooks, no issue tracking) — better to stop early than
|
|
997
|
+
// silently proceed. Pass --no-beads to opt out for ephemeral/CI environments.
|
|
990
998
|
const beadsMeta = path.join(target, ".beads", "metadata.json");
|
|
991
|
-
|
|
999
|
+
let beadsReady = false;
|
|
1000
|
+
if (noBeads) {
|
|
1001
|
+
console.log(' ⚠️ Skipping task tracker initialization (--no-beads).');
|
|
1002
|
+
skipped.push('.beads/ (--no-beads)');
|
|
1003
|
+
} else if (!fs.existsSync(beadsMeta) && !isToolRepo) {
|
|
992
1004
|
console.log('');
|
|
993
1005
|
console.log('Initializing the task tracker (beads — local-first, git-synced, works offline)…');
|
|
994
1006
|
fs.mkdirSync(path.join(target, ".beads"), { recursive: true });
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
console.log(" Bootstrap files left uncommitted (commit them yourself, or re-run with --commit-bootstrap).");
|
|
1002
|
-
}
|
|
1003
|
-
} catch (e) {
|
|
1004
|
-
console.warn("⚠️ Beads init failed:", e.message);
|
|
1007
|
+
const beads = initializeBeadsTracker(target, { commitBootstrap });
|
|
1008
|
+
created.push(".beads/ (initialized)");
|
|
1009
|
+
const prefixNote = beads?.prefix ? ` — issues prefixed \`${beads.prefix}-\`` : "";
|
|
1010
|
+
console.log(` ✓ Task tracker ready${prefixNote}. Run \`bd quickstart\` to start.`);
|
|
1011
|
+
if (beads?.withheldCommit) {
|
|
1012
|
+
console.log(" Bootstrap files left uncommitted (commit them yourself, or re-run with --commit-bootstrap).");
|
|
1005
1013
|
}
|
|
1006
1014
|
console.log('');
|
|
1015
|
+
beadsReady = true;
|
|
1016
|
+
} else if (fs.existsSync(beadsMeta)) {
|
|
1017
|
+
beadsReady = true;
|
|
1007
1018
|
}
|
|
1008
1019
|
|
|
1009
1020
|
// Wire the project's git hooks to .beads/hooks so Construct's pre-commit
|
|
1010
1021
|
// secret-scan and policy gates activate. Owned by init, not install (ADR-0027
|
|
1011
1022
|
// §3): install must never mutate the cwd repo. Idempotent — no-op when
|
|
1012
1023
|
// .beads/hooks/pre-commit is absent or a non-default custom path is set.
|
|
1024
|
+
// Skipped when beads is disabled or was not successfully initialized.
|
|
1013
1025
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1026
|
+
if (beadsReady) {
|
|
1027
|
+
try {
|
|
1028
|
+
const { ensureGitHooksPath } = await import('./git-hooks-path.mjs');
|
|
1029
|
+
const hooks = ensureGitHooksPath({ cwd: target });
|
|
1030
|
+
if (hooks.status === 'set') {
|
|
1031
|
+
created.push('git core.hooksPath → .beads/hooks');
|
|
1032
|
+
if (!quiet) console.log(` ✓ ${hooks.message}`);
|
|
1033
|
+
} else if (hooks.status === 'warning' && !quiet) {
|
|
1034
|
+
console.log(` ⚠️ ${hooks.message}`);
|
|
1035
|
+
}
|
|
1036
|
+
} catch { /* git-hooks wiring is advisory; init proceeds */ }
|
|
1037
|
+
}
|
|
1024
1038
|
|
|
1025
1039
|
// Inject Construct's integration guidance into AGENTS.md and CLAUDE.md as a
|
|
1026
1040
|
// versioned, hash-stamped marker block (ADR-0027 §2). Runs after bd init so
|
|
@@ -1154,22 +1168,25 @@ async function main() {
|
|
|
1154
1168
|
}
|
|
1155
1169
|
}
|
|
1156
1170
|
|
|
1157
|
-
//
|
|
1158
|
-
// can compare new intake against established PRDs/RFCs/ADRs
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1171
|
+
// Vector-index seeding (embedding the project's existing docs so the agent
|
|
1172
|
+
// can compare new intake against established PRDs/RFCs/ADRs) is opt-in via
|
|
1173
|
+
// --seed-index (construct-rf26.17): the default init path performs zero
|
|
1174
|
+
// embedding work and never touches LanceDB, so a project that never uses
|
|
1175
|
+
// semantic search never pays for the model cache or the index directory.
|
|
1176
|
+
// `construct ingest` provisions both lazily on first real use.
|
|
1177
|
+
|
|
1178
|
+
if (withSeedIndexFlag) {
|
|
1179
|
+
try {
|
|
1180
|
+
const { syncFileStateToSql } = await import('./storage/sync.mjs');
|
|
1181
|
+
const projectName = path.basename(target);
|
|
1182
|
+
const seed = await syncFileStateToSql(target, { env: process.env, project: projectName });
|
|
1183
|
+
if (seed?.status === 'ok' && (seed.documentsSynced || 0) > 0) {
|
|
1184
|
+
console.log(`\n🔍 Indexed existing project material: ${seed.documentsSynced} doc(s), ${seed.embeddingsSynced || 0} embeddings.`);
|
|
1185
|
+
console.log(` The agent can now compare new intake (inbox/) against your existing PRDs, ADRs, and notes.`);
|
|
1186
|
+
}
|
|
1187
|
+
} catch { /* silent — corpus seeding is best-effort, not a setup blocker */ }
|
|
1188
|
+
}
|
|
1162
1189
|
|
|
1163
|
-
try {
|
|
1164
|
-
const { syncFileStateToSql } = await import('./storage/sync.mjs');
|
|
1165
|
-
const projectName = path.basename(target);
|
|
1166
|
-
const seed = await syncFileStateToSql(target, { env: process.env, project: projectName });
|
|
1167
|
-
if (seed?.status === 'ok' && (seed.documentsSynced || 0) > 0) {
|
|
1168
|
-
console.log(`\n🔍 Indexed existing project material: ${seed.documentsSynced} doc(s), ${seed.embeddingsSynced || 0} embeddings.`);
|
|
1169
|
-
console.log(` The agent can now compare new intake (inbox/) against your existing PRDs, ADRs, and notes.`);
|
|
1170
|
-
}
|
|
1171
|
-
} catch { /* silent — corpus seeding is best-effort, not a setup blocker */ }
|
|
1172
|
-
|
|
1173
1190
|
// ── Devcontainer recipe ────────────────────────────────────────────────
|
|
1174
1191
|
if (withDevcontainerFlag) {
|
|
1175
1192
|
const dcDir = path.join(target, '.devcontainer');
|
|
@@ -1252,7 +1269,7 @@ async function main() {
|
|
|
1252
1269
|
}
|
|
1253
1270
|
}
|
|
1254
1271
|
|
|
1255
|
-
if (import.meta.url
|
|
1272
|
+
if (isMainModule(import.meta.url)) {
|
|
1256
1273
|
main().catch((error) => {
|
|
1257
1274
|
console.error(`Error: ${error.message}`);
|
|
1258
1275
|
process.exit(1);
|
package/lib/init-update.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
needsConstructGuideUpdate,
|
|
22
22
|
writeConstructGuideProposal,
|
|
23
23
|
} from "./init-update-guide.mjs";
|
|
24
|
+
import { isMainModule } from './roots.mjs';
|
|
24
25
|
|
|
25
26
|
const args = process.argv.slice(2);
|
|
26
27
|
const dryRun = args.includes("--dry-run");
|
|
@@ -226,7 +227,7 @@ async function main() {
|
|
|
226
227
|
console.log("To replace .cx/construct_guide.md with the shipped template: construct init:update --apply-guide");
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
if (import.meta.url
|
|
230
|
+
if (isMainModule(import.meta.url)) {
|
|
230
231
|
main().catch((error) => {
|
|
231
232
|
console.error(`Error: ${error.message}`);
|
|
232
233
|
process.exit(1);
|
|
@@ -62,6 +62,30 @@ function stripClaudeSettings(homeDir, opts) {
|
|
|
62
62
|
if (changed) writeJson(filePath, settings, { ...opts, label: '~/.claude/settings.json Construct entries' });
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// construct-ranh moved Claude Code's MCP write path off ~/.claude/settings.json
|
|
66
|
+
// (never read by the CLI for server definitions) to ~/.claude.json's top-level
|
|
67
|
+
// `mcpServers`. A post-fix install registers MCP servers there, so this must be
|
|
68
|
+
// stripped alongside settings.json for the memory/cass/construct-mcp bridges to
|
|
69
|
+
// be fully removed.
|
|
70
|
+
|
|
71
|
+
function stripClaudeUserConfig(homeDir, opts) {
|
|
72
|
+
const filePath = path.join(homeDir, '.claude.json');
|
|
73
|
+
const config = readJson(filePath);
|
|
74
|
+
if (!config || typeof config !== 'object') return;
|
|
75
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') return;
|
|
76
|
+
|
|
77
|
+
let changed = false;
|
|
78
|
+
for (const key of CONSTRUCT_MCP_KEYS) {
|
|
79
|
+
if (key in config.mcpServers) {
|
|
80
|
+
delete config.mcpServers[key];
|
|
81
|
+
changed = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
85
|
+
|
|
86
|
+
if (changed) writeJson(filePath, config, { ...opts, label: '~/.claude.json Construct MCP entries' });
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
function stripOpenCode(homeDir, opts) {
|
|
66
90
|
const filePath = path.join(homeDir, '.config', 'opencode', 'opencode.json');
|
|
67
91
|
const config = readJson(filePath);
|
|
@@ -173,6 +197,7 @@ export function cleanupLegacyGlobalConstruct({ homeDir, dryRun = false } = {}) {
|
|
|
173
197
|
});
|
|
174
198
|
|
|
175
199
|
stripClaudeSettings(homeDir, opts);
|
|
200
|
+
stripClaudeUserConfig(homeDir, opts);
|
|
176
201
|
stripOpenCode(homeDir, opts);
|
|
177
202
|
stripVsCode(homeDir, opts);
|
|
178
203
|
stripCodex(homeDir, opts);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import { existsSync, copyFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
14
|
+
import { existsSync, readFileSync, copyFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
17
17
|
export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log, hosts = null }) {
|
|
@@ -27,7 +27,7 @@ export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log
|
|
|
27
27
|
|
|
28
28
|
if (!existsSync(syncScript)) {
|
|
29
29
|
emit(`sync-specialists.mjs not found at ${syncScript}; skipping adapter sync`);
|
|
30
|
-
return {
|
|
30
|
+
return finishStage({ projectRoot, pkgVersion, synced: false, reason: 'sync-script-missing' });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// A `hosts` array restricts which adapter sets sync writes (construct-4xy6);
|
|
@@ -45,9 +45,46 @@ export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log
|
|
|
45
45
|
|
|
46
46
|
if (result.status !== 0) {
|
|
47
47
|
emit(`sync failed (exit ${result.status}); project left in a clean state`);
|
|
48
|
-
return {
|
|
48
|
+
return finishStage({ projectRoot, pkgVersion, synced: false, reason: `sync-exit-${result.status}` });
|
|
49
49
|
}
|
|
50
|
-
return {
|
|
50
|
+
return finishStage({ projectRoot, pkgVersion, synced: true, reason: null });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const STAGE_STATE_FILE = 'stage-state.json';
|
|
54
|
+
|
|
55
|
+
// Record the staging outcome under .construct/ so a half-stage (launcher present,
|
|
56
|
+
// adapters not synced) is detectable and repairable instead of re-derived from
|
|
57
|
+
// filesystem heuristics. The marker write is best-effort — a state-write failure must
|
|
58
|
+
// never turn a successful stage into a reported failure.
|
|
59
|
+
|
|
60
|
+
function finishStage({ projectRoot, pkgVersion, synced, reason }) {
|
|
61
|
+
const dotConstruct = path.join(projectRoot, '.construct');
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(dotConstruct, { recursive: true });
|
|
64
|
+
const state = { staged: true, synced, reason, pkgVersion: pkgVersion ?? null, ts: new Date().toISOString() };
|
|
65
|
+
writeFileSync(path.join(dotConstruct, STAGE_STATE_FILE), JSON.stringify(state, null, 2) + '\n');
|
|
66
|
+
} catch { /* marker is advisory */ }
|
|
67
|
+
return { staged: true, synced };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readStageState(projectRoot) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(path.join(projectRoot, '.construct', STAGE_STATE_FILE), 'utf8'));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Re-drive a half-staged project to a synced state. stageProjectAdapters is idempotent,
|
|
79
|
+
// so re-running the sync is the repair; an already-synced project is a no-op.
|
|
80
|
+
|
|
81
|
+
export function repairStagedProject({ projectRoot, packageRoot, pkgVersion, log, hosts = null }) {
|
|
82
|
+
const state = readStageState(projectRoot);
|
|
83
|
+
if (state && state.synced === true) {
|
|
84
|
+
return { staged: true, synced: true, repaired: false };
|
|
85
|
+
}
|
|
86
|
+
const result = stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log, hosts });
|
|
87
|
+
return { ...result, repaired: result.synced === true };
|
|
51
88
|
}
|
|
52
89
|
|
|
53
90
|
function ensureProjectLauncher({ projectRoot, templateDir, pkgVersion }) {
|
package/lib/intake/daemon.mjs
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* Packets past their TTL move to `.cx/intake/dead-letter/`; failed
|
|
7
7
|
* classification retries up to the budget then dead-letters with the reason.
|
|
8
8
|
*
|
|
9
|
+
* Every tick also sweeps the persisted `.cx/intake/pending/` packets
|
|
10
|
+
* (sweepPendingPackets): a packet whose `intake.sourcePath` is missing from
|
|
11
|
+
* disk, or whose age exceeds classifyPacket's TTL, moves to
|
|
12
|
+
* `.cx/intake/dead-letter/` with a `deadLetterReason`. This covers persisted
|
|
13
|
+
* packets, not just in-flight inbox files, so the TTL/dead-letter contract
|
|
14
|
+
* above applies to the whole pending queue.
|
|
15
|
+
*
|
|
9
16
|
* Atomic handoff (ADR-0045 §C): only complete top-level files are enqueued.
|
|
10
17
|
* Dotfiles and the `inbox/.staging/` assembly directory are skipped, so a file
|
|
11
18
|
* a writer is still staging is invisible until it is renamed into `inbox/`.
|
|
@@ -14,18 +21,19 @@
|
|
|
14
21
|
* idle shutdown, heartbeat, killswitch, single-writer lock) applies.
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
|
-
import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
|
|
18
25
|
import { join, basename } from 'node:path';
|
|
19
26
|
|
|
20
27
|
import { createDaemon, classifyPacket } from '../daemons/contract.mjs';
|
|
21
28
|
import { memoryCapMbFor } from '../resources/process-budget.mjs';
|
|
29
|
+
import { resolveStatePath } from '../state-root.mjs';
|
|
22
30
|
|
|
23
31
|
const KILLSWITCH_ENV = 'CONSTRUCT_INTAKE_DAEMON';
|
|
24
32
|
|
|
25
33
|
function inboxDir(cwd) { return join(cwd, 'inbox'); }
|
|
26
34
|
function pendingDir(cwd) { return join(cwd, '.cx', 'intake', 'pending'); }
|
|
27
35
|
function deadLetterDir(cwd) { return join(cwd, '.cx', 'intake', 'dead-letter'); }
|
|
28
|
-
function heartbeatPath(cwd) { return
|
|
36
|
+
function heartbeatPath(cwd) { return resolveStatePath(cwd, 'runtime', 'intake-daemon.heartbeat'); }
|
|
29
37
|
|
|
30
38
|
function ensureDir(dir) {
|
|
31
39
|
try { mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
@@ -60,9 +68,55 @@ function listInboxFiles(cwd) {
|
|
|
60
68
|
.filter((full) => isSizeStable(full));
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
function pendingPacketFiles(cwd) {
|
|
72
|
+
const dir = pendingDir(cwd);
|
|
73
|
+
if (!existsSync(dir)) return [];
|
|
74
|
+
try { return readdirSync(dir).filter((name) => name.endsWith('.json')); }
|
|
75
|
+
catch { return []; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readPacketFile(cwd, name) {
|
|
79
|
+
try { return JSON.parse(readFileSync(join(pendingDir(cwd), name), 'utf8')); }
|
|
80
|
+
catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function packetSourcePath(packet) {
|
|
84
|
+
return packet?.intake?.sourcePath || packet?.sourcePath || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function packetFirstSeenAt(packet) {
|
|
88
|
+
return packet?.firstSeenAt || packet?.createdAt || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// processInboxFile renames its source into a `.<name>.consumed` marker inside
|
|
92
|
+
// pendingDir once a packet is written, so the original sourcePath disappears
|
|
93
|
+
// the moment ingestion succeeds. That disappearance is expected, not an
|
|
94
|
+
// orphan — the sweep must not dead-letter a packet on the very next tick
|
|
95
|
+
// just because its own consumption already moved the source out of the way.
|
|
96
|
+
|
|
97
|
+
function hasConsumedMarker(cwd, sourcePath) {
|
|
98
|
+
return existsSync(join(pendingDir(cwd), `.${basename(sourcePath)}.consumed`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A pending packet is keyed by the source file it came from, independent of
|
|
102
|
+
// which ingest path wrote it (flat `sourcePath` here, nested
|
|
103
|
+
// `intake.sourcePath` for packets from lib/intake/prepare.mjs). Finding a
|
|
104
|
+
// live match lets re-ingestion refresh in place instead of writing a sibling
|
|
105
|
+
// packet for the same source.
|
|
106
|
+
|
|
107
|
+
function findExistingPendingBySource(cwd, sourcePath) {
|
|
108
|
+
for (const name of pendingPacketFiles(cwd)) {
|
|
109
|
+
const data = readPacketFile(cwd, name);
|
|
110
|
+
if (data && packetSourcePath(data) === sourcePath) return { name, data };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
63
115
|
/**
|
|
64
116
|
* Process one inbox file: classify, persist packet to pending, remove the
|
|
65
117
|
* inbox source on success, or move to dead-letter on persistent failure.
|
|
118
|
+
* A file whose sourcePath already has a live pending packet refreshes that
|
|
119
|
+
* packet in place rather than writing a second one for the same source.
|
|
66
120
|
*
|
|
67
121
|
* Returns one of:
|
|
68
122
|
* { didWork: true, route: 'pending'|'dead-letter', packetId }
|
|
@@ -76,10 +130,11 @@ export async function processInboxFile(filePath, { cwd, classify, now = () => ne
|
|
|
76
130
|
try { body = readFileSync(filePath, 'utf8'); }
|
|
77
131
|
catch (err) { return { didWork: false, reason: `read failed: ${err.message}` }; }
|
|
78
132
|
|
|
79
|
-
const
|
|
133
|
+
const existing = findExistingPendingBySource(cwd, filePath);
|
|
134
|
+
const id = existing?.data?.id || `intake-${now().getTime()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
80
135
|
const packet = {
|
|
81
136
|
id,
|
|
82
|
-
firstSeenAt: new Date(statSync(filePath).mtimeMs).toISOString(),
|
|
137
|
+
firstSeenAt: existing?.data?.firstSeenAt || new Date(statSync(filePath).mtimeMs).toISOString(),
|
|
83
138
|
sourcePath: filePath,
|
|
84
139
|
excerpt: body.slice(0, 500),
|
|
85
140
|
attempts: 0,
|
|
@@ -101,13 +156,15 @@ export async function processInboxFile(filePath, { cwd, classify, now = () => ne
|
|
|
101
156
|
return { didWork: false, reason: `classify failed, will retry: ${err.message}` };
|
|
102
157
|
}
|
|
103
158
|
|
|
104
|
-
|
|
159
|
+
packet.lastSeenAt = now().toISOString();
|
|
160
|
+
const packetName = existing?.name || `${id}.json`;
|
|
161
|
+
const packetPath = join(pendingDir(cwd), packetName);
|
|
105
162
|
writeFileSync(packetPath, JSON.stringify(packet, null, 2));
|
|
106
163
|
try {
|
|
107
164
|
const consumedPath = join(pendingDir(cwd), `.${basename(filePath)}.consumed`);
|
|
108
165
|
renameSync(filePath, consumedPath);
|
|
109
166
|
} catch { /* ignore consume failure */ }
|
|
110
|
-
return { didWork: true, route: 'pending', packetId: id };
|
|
167
|
+
return { didWork: true, route: 'pending', packetId: id, refreshed: !!existing };
|
|
111
168
|
}
|
|
112
169
|
|
|
113
170
|
function persistDeadLetter(filePath, packet, reason, cwd) {
|
|
@@ -117,6 +174,40 @@ function persistDeadLetter(filePath, packet, reason, cwd) {
|
|
|
117
174
|
return { didWork: true, route: 'dead-letter', packetId: packet.id, reason };
|
|
118
175
|
}
|
|
119
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Sweep persisted `.cx/intake/pending/` packets: one whose source file is
|
|
179
|
+
* missing from disk, or whose age exceeds classifyPacket's TTL, moves to
|
|
180
|
+
* `.cx/intake/dead-letter/` with a `deadLetterReason`. Runs every tick
|
|
181
|
+
* alongside the inbox scan so packets written by any ingest path (not just
|
|
182
|
+
* processInboxFile) are covered — the daemon's TTL/dead-letter contract
|
|
183
|
+
* otherwise only reaches packets still in flight through the inbox.
|
|
184
|
+
*
|
|
185
|
+
* Returns the list of { id, reason } packets moved.
|
|
186
|
+
*/
|
|
187
|
+
export function sweepPendingPackets(cwd, { now = () => new Date() } = {}) {
|
|
188
|
+
const swept = [];
|
|
189
|
+
for (const name of pendingPacketFiles(cwd)) {
|
|
190
|
+
const packet = readPacketFile(cwd, name);
|
|
191
|
+
if (!packet) continue;
|
|
192
|
+
|
|
193
|
+
const sourcePath = packetSourcePath(packet);
|
|
194
|
+
const sourceMissing = !!sourcePath && !existsSync(sourcePath) && !hasConsumedMarker(cwd, sourcePath);
|
|
195
|
+
const decision = classifyPacket({ firstSeenAt: packetFirstSeenAt(packet), attempts: packet.attempts || 0 });
|
|
196
|
+
|
|
197
|
+
let reason = null;
|
|
198
|
+
if (sourceMissing) reason = 'source-missing';
|
|
199
|
+
else if (decision.route === 'dead-letter') reason = 'ttl-expired';
|
|
200
|
+
if (!reason) continue;
|
|
201
|
+
|
|
202
|
+
ensureDir(deadLetterDir(cwd));
|
|
203
|
+
const dlPath = join(deadLetterDir(cwd), name);
|
|
204
|
+
writeFileSync(dlPath, JSON.stringify({ ...packet, deadLetterReason: reason, deadLetteredAt: now().toISOString() }, null, 2));
|
|
205
|
+
try { unlinkSync(join(pendingDir(cwd), name)); } catch { /* leave the dead-letter copy even if cleanup fails */ }
|
|
206
|
+
swept.push({ id: packet.id || name.replace(/\.json$/, ''), reason });
|
|
207
|
+
}
|
|
208
|
+
return swept;
|
|
209
|
+
}
|
|
210
|
+
|
|
120
211
|
/**
|
|
121
212
|
* Build a DaemonRunner for the intake daemon. Caller is responsible for
|
|
122
213
|
* calling .run() — typically construct intake daemon start.
|
|
@@ -132,9 +223,9 @@ export function buildIntakeDaemon({ cwd = process.cwd(), intervalMs = 60_000, cl
|
|
|
132
223
|
maxIdleTicks: 6,
|
|
133
224
|
memoryCapMb: memoryCapMbFor('intake', cwd),
|
|
134
225
|
async tick() {
|
|
226
|
+
const swept = sweepPendingPackets(cwd);
|
|
135
227
|
const files = listInboxFiles(cwd);
|
|
136
|
-
|
|
137
|
-
let workedAny = false;
|
|
228
|
+
let workedAny = swept.length > 0;
|
|
138
229
|
for (const file of files) {
|
|
139
230
|
const result = await processInboxFile(file, { cwd, classify: classifyFn });
|
|
140
231
|
if (result.didWork) workedAny = true;
|
package/lib/intake/git-queue.mjs
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
* Backs team/enterprise modes with the filesystem and Git for state
|
|
5
5
|
* synchronization and conflict resolution, with no central database server.
|
|
6
6
|
*
|
|
7
|
+
* git-queue is the zero-dependency DEFAULT kind:'queue' provider
|
|
8
|
+
* (construct-9oi4.7.11); substrate owns the queue contract and Postgres is an
|
|
9
|
+
* optional provider selected through the extension registry.
|
|
10
|
+
*
|
|
11
|
+
* Correctness envelope: single-writer-preferred, eventual consistency.
|
|
12
|
+
* Concurrent claims from multiple agents may result in duplicate claim
|
|
13
|
+
* attempts; the dolt commit history provides reconciliation. A push failure is
|
|
14
|
+
* NOT swallowed or ratified as success: claim() returns a typed disposition
|
|
15
|
+
* ({ durable: false, disposition: 'local-only', reason }) so callers see that
|
|
16
|
+
* the claim is local-only until the push is confirmed in the dolt remote,
|
|
17
|
+
* rather than mistaking it for a durable claim.
|
|
18
|
+
*
|
|
7
19
|
* Structure:
|
|
8
20
|
* .cx/team-inbox/
|
|
9
21
|
* pending/ - JSON files for unclaimed tasks
|
|
@@ -34,9 +46,11 @@ function timestamp() {
|
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
export class GitIntakeQueue {
|
|
37
|
-
constructor({ project, rootDir = process.cwd() } = {}) {
|
|
49
|
+
constructor({ project, rootDir = process.cwd(), _exec } = {}) {
|
|
38
50
|
this.project = project;
|
|
39
51
|
this.inboxRoot = path.join(rootDir, '.cx', 'team-inbox');
|
|
52
|
+
// Allow tests to inject a fake exec without monkey-patching the module.
|
|
53
|
+
this._exec = _exec || execSync;
|
|
40
54
|
this._ensureDirs();
|
|
41
55
|
}
|
|
42
56
|
|
|
@@ -48,8 +62,8 @@ export class GitIntakeQueue {
|
|
|
48
62
|
|
|
49
63
|
_gitAddAndCommit(filePath, message) {
|
|
50
64
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
this._exec(`git add "${filePath}"`, { stdio: 'ignore' });
|
|
66
|
+
this._exec(`git commit -m "${message}"`, { stdio: 'ignore' });
|
|
53
67
|
} catch (err) {
|
|
54
68
|
// It's okay if commit fails because of no changes
|
|
55
69
|
}
|
|
@@ -93,17 +107,24 @@ export class GitIntakeQueue {
|
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
async read(id) {
|
|
96
|
-
for (const dir of ['pending', '
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const searchPath = dir === 'claimed'
|
|
100
|
-
? path.join(this.inboxRoot, dir) // needs recursive search
|
|
101
|
-
: path.join(this.inboxRoot, dir, `${id}.json`);
|
|
102
|
-
|
|
103
|
-
if (dir !== 'claimed' && fs.existsSync(searchPath)) {
|
|
110
|
+
for (const dir of ['pending', 'processed', 'skipped', 'quarantine']) {
|
|
111
|
+
const searchPath = path.join(this.inboxRoot, dir, `${id}.json`);
|
|
112
|
+
if (fs.existsSync(searchPath)) {
|
|
104
113
|
return JSON.parse(fs.readFileSync(searchPath, 'utf8'));
|
|
105
114
|
}
|
|
106
115
|
}
|
|
116
|
+
|
|
117
|
+
// 'claimed' uses per-worker subdirectories — search recursively.
|
|
118
|
+
const claimedBase = path.join(this.inboxRoot, 'claimed');
|
|
119
|
+
if (fs.existsSync(claimedBase)) {
|
|
120
|
+
for (const worker of fs.readdirSync(claimedBase)) {
|
|
121
|
+
const candidate = path.join(claimedBase, worker, `${id}.json`);
|
|
122
|
+
if (fs.existsSync(candidate)) {
|
|
123
|
+
return JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
107
128
|
return null;
|
|
108
129
|
}
|
|
109
130
|
|
|
@@ -111,8 +132,13 @@ export class GitIntakeQueue {
|
|
|
111
132
|
if (!claimedBy) throw new Error('claim: claimedBy is required');
|
|
112
133
|
|
|
113
134
|
// Pull latest to minimize conflicts
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
try {
|
|
136
|
+
this._exec('git pull --rebase', { stdio: 'ignore' });
|
|
137
|
+
} catch (pullErr) {
|
|
138
|
+
// Pull failure is non-fatal — we proceed with local state.
|
|
139
|
+
// Log a reconciliation record so operators can detect divergence.
|
|
140
|
+
console.warn(`[git-queue] pull failed before claim (proceeding with local state): ${pullErr.message}`);
|
|
141
|
+
}
|
|
116
142
|
|
|
117
143
|
const pendingDir = path.join(this.inboxRoot, 'pending');
|
|
118
144
|
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.json')).sort();
|
|
@@ -125,31 +151,34 @@ export class GitIntakeQueue {
|
|
|
125
151
|
fs.mkdirSync(workerDir, { recursive: true });
|
|
126
152
|
const claimedPath = path.join(workerDir, fileName);
|
|
127
153
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fs.writeFileSync(claimedPath, JSON.stringify(data, null, 2));
|
|
138
|
-
|
|
139
|
-
// Commit and push the claim so peers see it
|
|
140
|
-
|
|
141
|
-
execSync(`git add .cx/team-inbox/pending/${fileName} .cx/team-inbox/claimed/${claimedBy}/${fileName}`, { stdio: 'ignore' });
|
|
142
|
-
execSync(`git commit -m "Claim task ${data.id} by ${claimedBy}"`, { stdio: 'ignore' });
|
|
143
|
-
execSync('git push', { stdio: 'ignore' });
|
|
144
|
-
|
|
145
|
-
return data;
|
|
146
|
-
} catch (err) {
|
|
147
|
-
// A push failure means another worker claimed it first or Git is out of
|
|
148
|
-
// sync; the local move is left for the next 'git pull --rebase' to reconcile.
|
|
154
|
+
// Atomic move on filesystem
|
|
155
|
+
fs.renameSync(pendingPath, claimedPath);
|
|
156
|
+
|
|
157
|
+
// Update metadata
|
|
158
|
+
const data = JSON.parse(fs.readFileSync(claimedPath, 'utf8'));
|
|
159
|
+
data.status = 'claimed';
|
|
160
|
+
data.claimedBy = claimedBy;
|
|
161
|
+
data.claimedAt = new Date().toISOString();
|
|
162
|
+
fs.writeFileSync(claimedPath, JSON.stringify(data, null, 2));
|
|
149
163
|
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
// Commit and push the claim so peers see it. A push failure is reported as a
|
|
165
|
+
// typed non-durable disposition on the returned claim rather than swallowed:
|
|
166
|
+
// the local move stands for the next 'git pull --rebase' to reconcile, but
|
|
167
|
+
// the caller is told the claim is local-only, not durable.
|
|
168
|
+
data.durable = true;
|
|
169
|
+
data.disposition = 'pushed';
|
|
170
|
+
try {
|
|
171
|
+
this._exec(`git add .cx/team-inbox/pending/${fileName} .cx/team-inbox/claimed/${claimedBy}/${fileName}`, { stdio: 'ignore' });
|
|
172
|
+
this._exec(`git commit -m "Claim task ${data.id} by ${claimedBy}"`, { stdio: 'ignore' });
|
|
173
|
+
this._exec('git push', { stdio: 'ignore' });
|
|
174
|
+
} catch (pushErr) {
|
|
175
|
+
data.durable = false;
|
|
176
|
+
data.disposition = 'local-only';
|
|
177
|
+
data.dispositionReason = pushErr.message;
|
|
178
|
+
console.warn(`[git-queue] push failed after claim of ${data.id} by ${claimedBy} (claim is local-only until reconciled): ${pushErr.message}`);
|
|
152
179
|
}
|
|
180
|
+
|
|
181
|
+
return data;
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
async markProcessed(id, { processedBy = 'unknown', notes = '' } = {}) {
|
|
@@ -187,10 +216,53 @@ export class GitIntakeQueue {
|
|
|
187
216
|
fs.writeFileSync(processedPath, JSON.stringify(data, null, 2));
|
|
188
217
|
|
|
189
218
|
this._gitAddAndCommit(this.inboxRoot, `Mark task ${id} as processed`);
|
|
190
|
-
try {
|
|
219
|
+
try { this._exec('git push', { stdio: 'ignore' }); } catch (e) {}
|
|
191
220
|
|
|
192
221
|
return { id };
|
|
193
222
|
}
|
|
194
223
|
|
|
195
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Mark an issue as skipped in local state WITHOUT a push.
|
|
226
|
+
*
|
|
227
|
+
* Used for graceful degradation when a push fails or when the caller wants
|
|
228
|
+
* to skip without touching the remote. The skip is advisory — it survives
|
|
229
|
+
* only as a local file move until the next reconciliation pull.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} issueId
|
|
232
|
+
* @returns {{ id: string, skippedAt: string }}
|
|
233
|
+
*/
|
|
234
|
+
async markSkipped(issueId) {
|
|
235
|
+
if (!issueId) throw new Error('markSkipped: issueId is required');
|
|
236
|
+
|
|
237
|
+
// Locate the file across pending and claimed directories.
|
|
238
|
+
let foundPath = null;
|
|
239
|
+
|
|
240
|
+
const pendingCandidate = path.join(this.inboxRoot, 'pending', `${issueId}.json`);
|
|
241
|
+
if (fs.existsSync(pendingCandidate)) {
|
|
242
|
+
foundPath = pendingCandidate;
|
|
243
|
+
} else {
|
|
244
|
+
const claimedBase = path.join(this.inboxRoot, 'claimed');
|
|
245
|
+
if (fs.existsSync(claimedBase)) {
|
|
246
|
+
for (const worker of fs.readdirSync(claimedBase)) {
|
|
247
|
+
const candidate = path.join(claimedBase, worker, `${issueId}.json`);
|
|
248
|
+
if (fs.existsSync(candidate)) { foundPath = candidate; break; }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!foundPath) throw new Error(`markSkipped: no entry ${issueId} found`);
|
|
254
|
+
|
|
255
|
+
const data = JSON.parse(fs.readFileSync(foundPath, 'utf8'));
|
|
256
|
+
data.status = 'skipped';
|
|
257
|
+
data.skippedAt = new Date().toISOString();
|
|
258
|
+
|
|
259
|
+
const skippedPath = path.join(this.inboxRoot, 'skipped', `${issueId}.json`);
|
|
260
|
+
fs.renameSync(foundPath, skippedPath);
|
|
261
|
+
fs.writeFileSync(skippedPath, JSON.stringify(data, null, 2));
|
|
262
|
+
|
|
263
|
+
// No push — this is a local-only operation (graceful degradation).
|
|
264
|
+
return { id: issueId, skippedAt: data.skippedAt };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// reopen would follow similar logic...
|
|
196
268
|
}
|
package/lib/intake/prepare.mjs
CHANGED
|
@@ -123,6 +123,8 @@ export async function prepareIntakeForIngestedFile({
|
|
|
123
123
|
const tagSuggestions = suggestTags(triage, related, vocab);
|
|
124
124
|
|
|
125
125
|
const baseEntry = {
|
|
126
|
+
// OWASP LLM01: externally-ingested document bodies are untrusted data, not instructions
|
|
127
|
+
trust: 'untrusted',
|
|
126
128
|
intake: {
|
|
127
129
|
sourcePath: ingestedFile.sourcePath,
|
|
128
130
|
outputPath: ingestedFile.outputPath,
|