@cleocode/core 2026.9.20 → 2026.9.21
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/dist/adapters/discovery.d.ts.map +1 -1
- package/dist/adapters/discovery.js +3 -10
- package/dist/adapters/discovery.js.map +1 -1
- package/dist/adrs/link-pipeline.js +2 -2
- package/dist/adrs/link-pipeline.js.map +1 -1
- package/dist/adrs/parse.js +3 -3
- package/dist/adrs/parse.js.map +1 -1
- package/dist/agents/invoke-meta-agent.d.ts.map +1 -1
- package/dist/agents/invoke-meta-agent.js +8 -4
- package/dist/agents/invoke-meta-agent.js.map +1 -1
- package/dist/bootstrap.d.ts +5 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +41 -21
- package/dist/bootstrap.js.map +1 -1
- package/dist/cloud/crypto.d.ts +47 -0
- package/dist/cloud/crypto.d.ts.map +1 -0
- package/dist/cloud/crypto.js +223 -0
- package/dist/cloud/crypto.js.map +1 -0
- package/dist/cloud/http.d.ts +52 -0
- package/dist/cloud/http.d.ts.map +1 -0
- package/dist/cloud/http.js +98 -0
- package/dist/cloud/http.js.map +1 -0
- package/dist/cloud/index.d.ts +19 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +19 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/journal.d.ts +169 -0
- package/dist/cloud/journal.d.ts.map +1 -0
- package/dist/cloud/journal.js +428 -0
- package/dist/cloud/journal.js.map +1 -0
- package/dist/cloud/keys.d.ts +213 -0
- package/dist/cloud/keys.d.ts.map +1 -0
- package/dist/cloud/keys.js +362 -0
- package/dist/cloud/keys.js.map +1 -0
- package/dist/cloud/manifest-check.d.ts +36 -0
- package/dist/cloud/manifest-check.d.ts.map +1 -0
- package/dist/cloud/manifest-check.js +70 -0
- package/dist/cloud/manifest-check.js.map +1 -0
- package/dist/cloud/projects.d.ts +13 -0
- package/dist/cloud/projects.d.ts.map +1 -0
- package/dist/cloud/projects.js +11 -0
- package/dist/cloud/projects.js.map +1 -0
- package/dist/cloud/signing.d.ts +105 -0
- package/dist/cloud/signing.d.ts.map +1 -0
- package/dist/cloud/signing.js +117 -0
- package/dist/cloud/signing.js.map +1 -0
- package/dist/cloud/streams.d.ts +6 -0
- package/dist/cloud/streams.d.ts.map +1 -0
- package/dist/cloud/streams.js +5 -0
- package/dist/cloud/streams.js.map +1 -0
- package/dist/decide/audit.d.ts +131 -0
- package/dist/decide/audit.d.ts.map +1 -0
- package/dist/decide/audit.js +98 -0
- package/dist/decide/audit.js.map +1 -0
- package/dist/decide/budget.d.ts +91 -0
- package/dist/decide/budget.d.ts.map +1 -0
- package/dist/decide/budget.js +194 -0
- package/dist/decide/budget.js.map +1 -0
- package/dist/decide/cache.d.ts +49 -0
- package/dist/decide/cache.d.ts.map +1 -0
- package/dist/decide/cache.js +87 -0
- package/dist/decide/cache.js.map +1 -0
- package/dist/decide/client.d.ts +103 -0
- package/dist/decide/client.d.ts.map +1 -0
- package/dist/decide/client.js +262 -0
- package/dist/decide/client.js.map +1 -0
- package/dist/decide/credentials.d.ts +159 -0
- package/dist/decide/credentials.d.ts.map +1 -0
- package/dist/decide/credentials.js +383 -0
- package/dist/decide/credentials.js.map +1 -0
- package/dist/decide/index.d.ts +18 -0
- package/dist/decide/index.d.ts.map +1 -0
- package/dist/decide/index.js +18 -0
- package/dist/decide/index.js.map +1 -0
- package/dist/decide/jev-wire.d.ts +155 -0
- package/dist/decide/jev-wire.d.ts.map +1 -0
- package/dist/decide/jev-wire.js +420 -0
- package/dist/decide/jev-wire.js.map +1 -0
- package/dist/decide/operations.d.ts +152 -0
- package/dist/decide/operations.d.ts.map +1 -0
- package/dist/decide/operations.js +199 -0
- package/dist/decide/operations.js.map +1 -0
- package/dist/decide/provider.d.ts +90 -0
- package/dist/decide/provider.d.ts.map +1 -0
- package/dist/decide/provider.js +43 -0
- package/dist/decide/provider.js.map +1 -0
- package/dist/decide/site.d.ts +148 -0
- package/dist/decide/site.d.ts.map +1 -0
- package/dist/decide/site.js +181 -0
- package/dist/decide/site.js.map +1 -0
- package/dist/decide/transport.d.ts +67 -0
- package/dist/decide/transport.d.ts.map +1 -0
- package/dist/decide/transport.js +350 -0
- package/dist/decide/transport.js.map +1 -0
- package/dist/docs/export-document.js +5908 -2436
- package/dist/docs/export-document.js.map +3 -3
- package/dist/docs/index.d.ts +2 -2
- package/dist/docs/index.d.ts.map +1 -1
- package/dist/docs/index.js +1 -1
- package/dist/docs/index.js.map +1 -1
- package/dist/docs/projection.d.ts.map +1 -1
- package/dist/docs/projection.js +2 -3
- package/dist/docs/projection.js.map +1 -1
- package/dist/docs/publish-pr.d.ts +69 -1
- package/dist/docs/publish-pr.d.ts.map +1 -1
- package/dist/docs/publish-pr.js +112 -10
- package/dist/docs/publish-pr.js.map +1 -1
- package/dist/doctor/db-substrate.d.ts +13 -8
- package/dist/doctor/db-substrate.d.ts.map +1 -1
- package/dist/doctor/db-substrate.js +39 -26
- package/dist/doctor/db-substrate.js.map +1 -1
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/knowledge.d.ts.map +1 -1
- package/dist/doctor/knowledge.js +5 -15
- package/dist/doctor/knowledge.js.map +1 -1
- package/dist/doctor/nexus-schema-residency.d.ts +1 -1
- package/dist/doctor/nexus-schema-residency.d.ts.map +1 -1
- package/dist/doctor/nexus-schema-residency.js +3 -0
- package/dist/doctor/nexus-schema-residency.js.map +1 -1
- package/dist/doctor/project-identity.d.ts +44 -1
- package/dist/doctor/project-identity.d.ts.map +1 -1
- package/dist/doctor/project-identity.js +237 -10
- package/dist/doctor/project-identity.js.map +1 -1
- package/dist/doctor/projects.d.ts +170 -0
- package/dist/doctor/projects.d.ts.map +1 -0
- package/dist/doctor/projects.js +1075 -0
- package/dist/doctor/projects.js.map +1 -0
- package/dist/doctor/twin-collapse.d.ts +97 -0
- package/dist/doctor/twin-collapse.d.ts.map +1 -0
- package/dist/doctor/twin-collapse.js +168 -0
- package/dist/doctor/twin-collapse.js.map +1 -0
- package/dist/doctor/worktree-orphans.d.ts.map +1 -1
- package/dist/doctor/worktree-orphans.js +13 -29
- package/dist/doctor/worktree-orphans.js.map +1 -1
- package/dist/doctor/worktree-stores.d.ts +128 -0
- package/dist/doctor/worktree-stores.d.ts.map +1 -0
- package/dist/doctor/worktree-stores.js +374 -0
- package/dist/doctor/worktree-stores.js.map +1 -0
- package/dist/error-catalog.d.ts.map +1 -1
- package/dist/error-catalog.js +21 -0
- package/dist/error-catalog.js.map +1 -1
- package/dist/error-registry.d.ts.map +1 -1
- package/dist/error-registry.js +40 -0
- package/dist/error-registry.js.map +1 -1
- package/dist/gc/cleanup.d.ts.map +1 -1
- package/dist/gc/cleanup.js +32 -19
- package/dist/gc/cleanup.js.map +1 -1
- package/dist/git/bounded-git.d.ts +170 -0
- package/dist/git/bounded-git.d.ts.map +1 -0
- package/dist/git/bounded-git.js +371 -0
- package/dist/git/bounded-git.js.map +1 -0
- package/dist/git/work-tree.d.ts +20 -0
- package/dist/git/work-tree.d.ts.map +1 -1
- package/dist/git/work-tree.js +20 -2
- package/dist/git/work-tree.js.map +1 -1
- package/dist/hooks/handlers/session-hooks.d.ts +11 -26
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.js +18 -47
- package/dist/hooks/handlers/session-hooks.js.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +2 -2
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/init/engine-ops.d.ts.map +1 -1
- package/dist/init/engine-ops.js +23 -1
- package/dist/init/engine-ops.js.map +1 -1
- package/dist/init.d.ts +158 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +527 -42
- package/dist/init.js.map +1 -1
- package/dist/injection.d.ts +55 -0
- package/dist/injection.d.ts.map +1 -1
- package/dist/injection.js +102 -6
- package/dist/injection.js.map +1 -1
- package/dist/internal.d.ts +9 -5
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +8 -4
- package/dist/internal.js.map +1 -1
- package/dist/issue/diagnostics.d.ts.map +1 -1
- package/dist/issue/diagnostics.js +2 -1
- package/dist/issue/diagnostics.js.map +1 -1
- package/dist/lib/concurrency.d.ts +25 -0
- package/dist/lib/concurrency.d.ts.map +1 -0
- package/dist/lib/concurrency.js +43 -0
- package/dist/lib/concurrency.js.map +1 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lifecycle/engine-ops.d.ts.map +1 -1
- package/dist/lifecycle/engine-ops.js +4 -2
- package/dist/lifecycle/engine-ops.js.map +1 -1
- package/dist/lifecycle/stage-guidance.d.ts +5 -0
- package/dist/lifecycle/stage-guidance.d.ts.map +1 -1
- package/dist/lifecycle/stage-guidance.js +14 -4
- package/dist/lifecycle/stage-guidance.js.map +1 -1
- package/dist/llm/catalog-cache.d.ts +3 -2
- package/dist/llm/catalog-cache.d.ts.map +1 -1
- package/dist/llm/catalog-cache.js +5 -5
- package/dist/llm/catalog-cache.js.map +1 -1
- package/dist/llm/credentials.d.ts +4 -4
- package/dist/llm/credentials.js +6 -6
- package/dist/llm/credentials.js.map +1 -1
- package/dist/llm/plugin-facade.js +4312 -1798
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/llm/stable-device-id.d.ts.map +1 -1
- package/dist/llm/stable-device-id.js +3 -5
- package/dist/llm/stable-device-id.js.map +1 -1
- package/dist/memory/attention.d.ts.map +1 -1
- package/dist/memory/attention.js +30 -30
- package/dist/memory/attention.js.map +1 -1
- package/dist/memory/decision-contradiction.d.ts +152 -0
- package/dist/memory/decision-contradiction.d.ts.map +1 -0
- package/dist/memory/decision-contradiction.js +273 -0
- package/dist/memory/decision-contradiction.js.map +1 -0
- package/dist/memory/decisions.d.ts +42 -11
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/decisions.js +153 -56
- package/dist/memory/decisions.js.map +1 -1
- package/dist/memory/dialectic-evaluator.d.ts +7 -0
- package/dist/memory/dialectic-evaluator.d.ts.map +1 -1
- package/dist/memory/dialectic-evaluator.js +23 -8
- package/dist/memory/dialectic-evaluator.js.map +1 -1
- package/dist/memory/dispatch-trace-format.d.ts +39 -0
- package/dist/memory/dispatch-trace-format.d.ts.map +1 -0
- package/dist/memory/dispatch-trace-format.js +54 -0
- package/dist/memory/dispatch-trace-format.js.map +1 -0
- package/dist/memory/dispatch-trace.d.ts.map +1 -1
- package/dist/memory/dispatch-trace.js +1 -27
- package/dist/memory/dispatch-trace.js.map +1 -1
- package/dist/memory/engine-compat.d.ts +2 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +1 -0
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/import-from-provider.js +3 -3
- package/dist/memory/import-from-provider.js.map +1 -1
- package/dist/memory/learnings.d.ts +2 -2
- package/dist/memory/observation-type-decision.d.ts +96 -0
- package/dist/memory/observation-type-decision.d.ts.map +1 -0
- package/dist/memory/observation-type-decision.js +218 -0
- package/dist/memory/observation-type-decision.js.map +1 -0
- package/dist/memory/redaction.js +2 -0
- package/dist/memory/redaction.js.map +1 -1
- package/dist/memory/retrieval/build-retrieval-bundle.d.ts +9 -2
- package/dist/memory/retrieval/build-retrieval-bundle.d.ts.map +1 -1
- package/dist/memory/retrieval/build-retrieval-bundle.js +42 -11
- package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
- package/dist/memory/retrieval/get-current-session-id.d.ts +5 -0
- package/dist/memory/retrieval/get-current-session-id.d.ts.map +1 -1
- package/dist/memory/retrieval/get-current-session-id.js +7 -2
- package/dist/memory/retrieval/get-current-session-id.js.map +1 -1
- package/dist/memory/retrieval/observe.d.ts.map +1 -1
- package/dist/memory/retrieval/observe.js +30 -29
- package/dist/memory/retrieval/observe.js.map +1 -1
- package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
- package/dist/nexus/analyze-orchestrator.js +42 -29
- package/dist/nexus/analyze-orchestrator.js.map +1 -1
- package/dist/nexus/assessment-projection.d.ts +76 -0
- package/dist/nexus/assessment-projection.d.ts.map +1 -0
- package/dist/nexus/assessment-projection.js +143 -0
- package/dist/nexus/assessment-projection.js.map +1 -0
- package/dist/nexus/assessment-store.d.ts +14 -2
- package/dist/nexus/assessment-store.d.ts.map +1 -1
- package/dist/nexus/assessment-store.js +21 -3
- package/dist/nexus/assessment-store.js.map +1 -1
- package/dist/nexus/checkout-nonce.d.ts +48 -0
- package/dist/nexus/checkout-nonce.d.ts.map +1 -0
- package/dist/nexus/checkout-nonce.js +84 -0
- package/dist/nexus/checkout-nonce.js.map +1 -0
- package/dist/nexus/devices.d.ts +93 -0
- package/dist/nexus/devices.d.ts.map +1 -0
- package/dist/nexus/devices.js +141 -0
- package/dist/nexus/devices.js.map +1 -0
- package/dist/nexus/freshness.d.ts.map +1 -1
- package/dist/nexus/freshness.js +25 -6
- package/dist/nexus/freshness.js.map +1 -1
- package/dist/nexus/git-state.d.ts +225 -0
- package/dist/nexus/git-state.d.ts.map +1 -0
- package/dist/nexus/git-state.js +677 -0
- package/dist/nexus/git-state.js.map +1 -0
- package/dist/nexus/graph-manifest.d.ts +20 -4
- package/dist/nexus/graph-manifest.d.ts.map +1 -1
- package/dist/nexus/graph-manifest.js +40 -5
- package/dist/nexus/graph-manifest.js.map +1 -1
- package/dist/nexus/identity.d.ts +68 -23
- package/dist/nexus/identity.d.ts.map +1 -1
- package/dist/nexus/identity.js +99 -22
- package/dist/nexus/identity.js.map +1 -1
- package/dist/nexus/index.d.ts +5 -2
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/index.js +10 -4
- package/dist/nexus/index.js.map +1 -1
- package/dist/nexus/knowledge.d.ts +5 -0
- package/dist/nexus/knowledge.d.ts.map +1 -1
- package/dist/nexus/knowledge.js +26 -2
- package/dist/nexus/knowledge.js.map +1 -1
- package/dist/nexus/legacy-alias.d.ts +60 -0
- package/dist/nexus/legacy-alias.d.ts.map +1 -0
- package/dist/nexus/legacy-alias.js +68 -0
- package/dist/nexus/legacy-alias.js.map +1 -0
- package/dist/nexus/migrate-json-to-sqlite.js +2 -2
- package/dist/nexus/migrate-json-to-sqlite.js.map +1 -1
- package/dist/nexus/ops.d.ts +1 -0
- package/dist/nexus/ops.d.ts.map +1 -1
- package/dist/nexus/path-map.d.ts +264 -20
- package/dist/nexus/path-map.d.ts.map +1 -1
- package/dist/nexus/path-map.js +580 -44
- package/dist/nexus/path-map.js.map +1 -1
- package/dist/nexus/projects-clean.d.ts +9 -1
- package/dist/nexus/projects-clean.d.ts.map +1 -1
- package/dist/nexus/projects-clean.js +126 -14
- package/dist/nexus/projects-clean.js.map +1 -1
- package/dist/nexus/projects-scan.d.ts +56 -1
- package/dist/nexus/projects-scan.d.ts.map +1 -1
- package/dist/nexus/projects-scan.js +144 -3
- package/dist/nexus/projects-scan.js.map +1 -1
- package/dist/nexus/registry-hygiene.d.ts +18 -2
- package/dist/nexus/registry-hygiene.d.ts.map +1 -1
- package/dist/nexus/registry-hygiene.js +37 -11
- package/dist/nexus/registry-hygiene.js.map +1 -1
- package/dist/nexus/registry-roots.d.ts +47 -0
- package/dist/nexus/registry-roots.d.ts.map +1 -0
- package/dist/nexus/registry-roots.js +79 -0
- package/dist/nexus/registry-roots.js.map +1 -0
- package/dist/nexus/registry.d.ts +50 -9
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +311 -156
- package/dist/nexus/registry.js.map +1 -1
- package/dist/nexus/stored-roots.d.ts +106 -0
- package/dist/nexus/stored-roots.d.ts.map +1 -0
- package/dist/nexus/stored-roots.js +180 -0
- package/dist/nexus/stored-roots.js.map +1 -0
- package/dist/nexus/transfer.d.ts.map +1 -1
- package/dist/nexus/transfer.js +3 -0
- package/dist/nexus/transfer.js.map +1 -1
- package/dist/nexus/user-profile-hygiene.d.ts +168 -0
- package/dist/nexus/user-profile-hygiene.d.ts.map +1 -0
- package/dist/nexus/user-profile-hygiene.js +305 -0
- package/dist/nexus/user-profile-hygiene.js.map +1 -0
- package/dist/nexus/user-profile.d.ts +53 -2
- package/dist/nexus/user-profile.d.ts.map +1 -1
- package/dist/nexus/user-profile.js +106 -4
- package/dist/nexus/user-profile.js.map +1 -1
- package/dist/orchestrate/pivot.d.ts.map +1 -1
- package/dist/orchestrate/pivot.js +5 -1
- package/dist/orchestrate/pivot.js.map +1 -1
- package/dist/orchestrate/spawn-ops.d.ts +12 -0
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +152 -57
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestration/classify-readiness.d.ts +37 -1
- package/dist/orchestration/classify-readiness.d.ts.map +1 -1
- package/dist/orchestration/classify-readiness.js +48 -19
- package/dist/orchestration/classify-readiness.js.map +1 -1
- package/dist/orchestration/index.d.ts +3 -2
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +2 -1
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/owner-decision-readiness.d.ts +88 -0
- package/dist/orchestration/owner-decision-readiness.d.ts.map +1 -0
- package/dist/orchestration/owner-decision-readiness.js +152 -0
- package/dist/orchestration/owner-decision-readiness.js.map +1 -0
- package/dist/orchestration/psyche-memory-budget.d.ts +105 -0
- package/dist/orchestration/psyche-memory-budget.d.ts.map +1 -0
- package/dist/orchestration/psyche-memory-budget.js +383 -0
- package/dist/orchestration/psyche-memory-budget.js.map +1 -0
- package/dist/orchestration/spawn-prompt.d.ts +12 -1
- package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
- package/dist/orchestration/spawn-prompt.js +141 -179
- package/dist/orchestration/spawn-prompt.js.map +1 -1
- package/dist/orchestration/spawn.d.ts +7 -0
- package/dist/orchestration/spawn.d.ts.map +1 -1
- package/dist/orchestration/spawn.js +1 -0
- package/dist/orchestration/spawn.js.map +1 -1
- package/dist/paths.d.ts +24 -8
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +135 -71
- package/dist/paths.js.map +1 -1
- package/dist/platform.d.ts +6 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +8 -19
- package/dist/platform.js.map +1 -1
- package/dist/project-info.d.ts +18 -0
- package/dist/project-info.d.ts.map +1 -1
- package/dist/project-info.js +29 -1
- package/dist/project-info.js.map +1 -1
- package/dist/project-lifecycle.d.ts +91 -32
- package/dist/project-lifecycle.d.ts.map +1 -1
- package/dist/project-lifecycle.js +639 -75
- package/dist/project-lifecycle.js.map +1 -1
- package/dist/project-scope.d.ts +86 -4
- package/dist/project-scope.d.ts.map +1 -1
- package/dist/project-scope.js +226 -13
- package/dist/project-scope.js.map +1 -1
- package/dist/project-tombstone.d.ts +111 -0
- package/dist/project-tombstone.d.ts.map +1 -0
- package/dist/project-tombstone.js +220 -0
- package/dist/project-tombstone.js.map +1 -0
- package/dist/release/artifacts.d.ts.map +1 -1
- package/dist/release/artifacts.js +14 -9
- package/dist/release/artifacts.js.map +1 -1
- package/dist/release/ci-evidence.d.ts +223 -0
- package/dist/release/ci-evidence.d.ts.map +1 -0
- package/dist/release/ci-evidence.js +652 -0
- package/dist/release/ci-evidence.js.map +1 -0
- package/dist/release/engine-ops.d.ts.map +1 -1
- package/dist/release/engine-ops.js +3 -3
- package/dist/release/engine-ops.js.map +1 -1
- package/dist/release/plan.d.ts.map +1 -1
- package/dist/release/plan.js +7 -6
- package/dist/release/plan.js.map +1 -1
- package/dist/release/pr-evidence.d.ts +38 -0
- package/dist/release/pr-evidence.d.ts.map +1 -1
- package/dist/release/pr-evidence.js +67 -11
- package/dist/release/pr-evidence.js.map +1 -1
- package/dist/release/reconcile.d.ts.map +1 -1
- package/dist/release/reconcile.js +3 -2
- package/dist/release/reconcile.js.map +1 -1
- package/dist/release/release-manifest.d.ts +2 -1
- package/dist/release/release-manifest.d.ts.map +1 -1
- package/dist/release/release-manifest.js +6 -5
- package/dist/release/release-manifest.js.map +1 -1
- package/dist/render/nexus/graph/index.d.ts +4 -0
- package/dist/render/nexus/graph/index.d.ts.map +1 -1
- package/dist/render/nexus/graph/index.js +57 -0
- package/dist/render/nexus/graph/index.js.map +1 -1
- package/dist/resources/spawn-wrapper.d.ts +8 -1
- package/dist/resources/spawn-wrapper.d.ts.map +1 -1
- package/dist/resources/spawn-wrapper.js +42 -5
- package/dist/resources/spawn-wrapper.js.map +1 -1
- package/dist/routing/build-command-groups.d.ts.map +1 -1
- package/dist/routing/build-command-groups.js +1 -0
- package/dist/routing/build-command-groups.js.map +1 -1
- package/dist/sagas/index.d.ts +1 -1
- package/dist/sagas/index.d.ts.map +1 -1
- package/dist/sagas/index.js +1 -1
- package/dist/sagas/index.js.map +1 -1
- package/dist/sagas/reconcile.d.ts +24 -4
- package/dist/sagas/reconcile.d.ts.map +1 -1
- package/dist/sagas/reconcile.js +226 -100
- package/dist/sagas/reconcile.js.map +1 -1
- package/dist/scaffold/ensure-config.d.ts.map +1 -1
- package/dist/scaffold/ensure-config.js +31 -1
- package/dist/scaffold/ensure-config.js.map +1 -1
- package/dist/scaffold/project-identity.d.ts +2 -8
- package/dist/scaffold/project-identity.d.ts.map +1 -1
- package/dist/scaffold/project-identity.js +5 -21
- package/dist/scaffold/project-identity.js.map +1 -1
- package/dist/security/input-sanitization.d.ts +9 -1
- package/dist/security/input-sanitization.d.ts.map +1 -1
- package/dist/security/input-sanitization.js +12 -2
- package/dist/security/input-sanitization.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +4321 -1807
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sentient/propose-tick.d.ts.map +1 -1
- package/dist/sentient/propose-tick.js +4 -2
- package/dist/sentient/propose-tick.js.map +1 -1
- package/dist/sequence/index.d.ts.map +1 -1
- package/dist/sequence/index.js +6 -23
- package/dist/sequence/index.js.map +1 -1
- package/dist/session/engine-ops.d.ts +9 -0
- package/dist/session/engine-ops.d.ts.map +1 -1
- package/dist/session/engine-ops.js +110 -18
- package/dist/session/engine-ops.js.map +1 -1
- package/dist/sessions/assumptions.d.ts.map +1 -1
- package/dist/sessions/assumptions.js +7 -4
- package/dist/sessions/assumptions.js.map +1 -1
- package/dist/sessions/briefing.d.ts +6 -0
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/briefing.js +12 -1
- package/dist/sessions/briefing.js.map +1 -1
- package/dist/sessions/decisions.d.ts +38 -3
- package/dist/sessions/decisions.d.ts.map +1 -1
- package/dist/sessions/decisions.js +235 -21
- package/dist/sessions/decisions.js.map +1 -1
- package/dist/sessions/drift-watchdog.d.ts.map +1 -1
- package/dist/sessions/drift-watchdog.js +4 -2
- package/dist/sessions/drift-watchdog.js.map +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +33 -4
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/session-end-snapshot-entry.d.ts +26 -0
- package/dist/sessions/session-end-snapshot-entry.d.ts.map +1 -0
- package/dist/sessions/session-end-snapshot-entry.js +59 -0
- package/dist/sessions/session-end-snapshot-entry.js.map +1 -0
- package/dist/sessions/session-end-snapshot.d.ts +114 -0
- package/dist/sessions/session-end-snapshot.d.ts.map +1 -0
- package/dist/sessions/session-end-snapshot.js +446 -0
- package/dist/sessions/session-end-snapshot.js.map +1 -0
- package/dist/sessions/session-manifest-mirror.d.ts.map +1 -1
- package/dist/sessions/session-manifest-mirror.js +10 -17
- package/dist/sessions/session-manifest-mirror.js.map +1 -1
- package/dist/sessions/session-switch.d.ts.map +1 -1
- package/dist/sessions/session-switch.js +5 -3
- package/dist/sessions/session-switch.js.map +1 -1
- package/dist/sessions/snapshot.d.ts.map +1 -1
- package/dist/sessions/snapshot.js +5 -4
- package/dist/sessions/snapshot.js.map +1 -1
- package/dist/sessions/terminal-identity.d.ts +169 -0
- package/dist/sessions/terminal-identity.d.ts.map +1 -0
- package/dist/sessions/terminal-identity.js +273 -0
- package/dist/sessions/terminal-identity.js.map +1 -0
- package/dist/setup/sections/verification.d.ts +1 -1
- package/dist/setup/sections/verification.d.ts.map +1 -1
- package/dist/setup/sections/verification.js +11 -15
- package/dist/setup/sections/verification.js.map +1 -1
- package/dist/skills/discovery.d.ts +8 -2
- package/dist/skills/discovery.d.ts.map +1 -1
- package/dist/skills/discovery.js +15 -3
- package/dist/skills/discovery.js.map +1 -1
- package/dist/skills/dispatch.d.ts +5 -2
- package/dist/skills/dispatch.d.ts.map +1 -1
- package/dist/skills/dispatch.js +5 -2
- package/dist/skills/dispatch.js.map +1 -1
- package/dist/skills/doctor.d.ts.map +1 -1
- package/dist/skills/doctor.js +5 -1
- package/dist/skills/doctor.js.map +1 -1
- package/dist/skills/federated-search.js +2 -2
- package/dist/skills/federated-search.js.map +1 -1
- package/dist/skills/index.d.ts +3 -3
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/migration.d.ts +5 -5
- package/dist/skills/migration.d.ts.map +1 -1
- package/dist/skills/migration.js +7 -9
- package/dist/skills/migration.js.map +1 -1
- package/dist/skills/orchestrator/startup.d.ts.map +1 -1
- package/dist/skills/orchestrator/startup.js +4 -2
- package/dist/skills/orchestrator/startup.js.map +1 -1
- package/dist/skills/skill-paths.d.ts +30 -3
- package/dist/skills/skill-paths.d.ts.map +1 -1
- package/dist/skills/skill-paths.js +28 -9
- package/dist/skills/skill-paths.js.map +1 -1
- package/dist/skills/skill-root.d.ts +60 -11
- package/dist/skills/skill-root.d.ts.map +1 -1
- package/dist/skills/skill-root.js +100 -14
- package/dist/skills/skill-root.js.map +1 -1
- package/dist/skills/skills-guard.js +5 -5
- package/dist/skills/skills-guard.js.map +1 -1
- package/dist/skills/types.d.ts +23 -0
- package/dist/skills/types.d.ts.map +1 -1
- package/dist/skills/types.js.map +1 -1
- package/dist/spawn/agent-identity.d.ts +51 -2
- package/dist/spawn/agent-identity.d.ts.map +1 -1
- package/dist/spawn/agent-identity.js +89 -26
- package/dist/spawn/agent-identity.js.map +1 -1
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +56 -79
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/spawn/index.d.ts +2 -2
- package/dist/spawn/index.d.ts.map +1 -1
- package/dist/spawn/index.js +1 -1
- package/dist/spawn/index.js.map +1 -1
- package/dist/spawn/worktree-lock-holder.d.ts +37 -0
- package/dist/spawn/worktree-lock-holder.d.ts.map +1 -0
- package/dist/spawn/worktree-lock-holder.js +41 -0
- package/dist/spawn/worktree-lock-holder.js.map +1 -0
- package/dist/sticky/convert.d.ts.map +1 -1
- package/dist/sticky/convert.js +16 -4
- package/dist/sticky/convert.js.map +1 -1
- package/dist/store/attachment-store.d.ts +14 -0
- package/dist/store/attachment-store.d.ts.map +1 -1
- package/dist/store/attachment-store.js +11 -1
- package/dist/store/attachment-store.js.map +1 -1
- package/dist/store/backup-inspect.d.ts +7 -3
- package/dist/store/backup-inspect.d.ts.map +1 -1
- package/dist/store/backup-inspect.js +18 -7
- package/dist/store/backup-inspect.js.map +1 -1
- package/dist/store/backup-sidecar.d.ts +60 -0
- package/dist/store/backup-sidecar.d.ts.map +1 -0
- package/dist/store/backup-sidecar.js +96 -0
- package/dist/store/backup-sidecar.js.map +1 -0
- package/dist/store/data-accessor.d.ts +1 -1
- package/dist/store/data-accessor.d.ts.map +1 -1
- package/dist/store/data-accessor.js +6 -6
- package/dist/store/data-accessor.js.map +1 -1
- package/dist/store/data-safety-central.d.ts +13 -4
- package/dist/store/data-safety-central.d.ts.map +1 -1
- package/dist/store/data-safety-central.js +31 -4
- package/dist/store/data-safety-central.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +10 -0
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/__fixtures__/representative-fixture.d.ts.map +1 -1
- package/dist/store/exodus/__fixtures__/representative-fixture.js +4 -0
- package/dist/store/exodus/__fixtures__/representative-fixture.js.map +1 -1
- package/dist/store/exodus/migrate.js +1 -1
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/exodus/reconcile.js +1 -1
- package/dist/store/exodus/reconcile.js.map +1 -1
- package/dist/store/exodus/runtime-targets.d.ts +4 -3
- package/dist/store/exodus/runtime-targets.d.ts.map +1 -1
- package/dist/store/exodus/runtime-targets.js +4 -3
- package/dist/store/exodus/runtime-targets.js.map +1 -1
- package/dist/store/import-logging.d.ts.map +1 -1
- package/dist/store/import-logging.js +5 -4
- package/dist/store/import-logging.js.map +1 -1
- package/dist/store/legacy-tasks-lineage.d.ts.map +1 -1
- package/dist/store/legacy-tasks-lineage.js +2 -7
- package/dist/store/legacy-tasks-lineage.js.map +1 -1
- package/dist/store/lock.d.ts +1 -0
- package/dist/store/lock.d.ts.map +1 -1
- package/dist/store/lock.js +1 -0
- package/dist/store/lock.js.map +1 -1
- package/dist/store/memory-accessor.d.ts +8 -3
- package/dist/store/memory-accessor.d.ts.map +1 -1
- package/dist/store/memory-accessor.js +29 -13
- package/dist/store/memory-accessor.js.map +1 -1
- package/dist/store/memory-sqlite.d.ts.map +1 -1
- package/dist/store/memory-sqlite.js +5 -0
- package/dist/store/memory-sqlite.js.map +1 -1
- package/dist/store/open-cleo-db.d.ts.map +1 -1
- package/dist/store/open-cleo-db.js +11 -6
- package/dist/store/open-cleo-db.js.map +1 -1
- package/dist/store/portable-bundle-relocate.d.ts +13 -8
- package/dist/store/portable-bundle-relocate.d.ts.map +1 -1
- package/dist/store/portable-bundle-relocate.js +75 -31
- package/dist/store/portable-bundle-relocate.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +2 -1
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/portable-bundle.d.ts.map +1 -1
- package/dist/store/portable-bundle.js +7 -3
- package/dist/store/portable-bundle.js.map +1 -1
- package/dist/store/pre-repair-snapshot.d.ts +77 -0
- package/dist/store/pre-repair-snapshot.d.ts.map +1 -0
- package/dist/store/pre-repair-snapshot.js +122 -0
- package/dist/store/pre-repair-snapshot.js.map +1 -0
- package/dist/store/regenerators.d.ts +2 -1
- package/dist/store/regenerators.d.ts.map +1 -1
- package/dist/store/regenerators.js +17 -3
- package/dist/store/regenerators.js.map +1 -1
- package/dist/store/relocated-store-guard.d.ts +71 -0
- package/dist/store/relocated-store-guard.d.ts.map +1 -0
- package/dist/store/relocated-store-guard.js +138 -0
- package/dist/store/relocated-store-guard.js.map +1 -0
- package/dist/store/safety-data-accessor.d.ts +2 -2
- package/dist/store/safety-data-accessor.d.ts.map +1 -1
- package/dist/store/safety-data-accessor.js +2 -2
- package/dist/store/safety-data-accessor.js.map +1 -1
- package/dist/store/schema/attachments.d.ts +2 -2
- package/dist/store/schema/audit.d.ts +5 -5
- package/dist/store/schema/background-jobs.d.ts +1 -1
- package/dist/store/schema/cleo-global/models-catalog.d.ts +1 -1
- package/dist/store/schema/cleo-global/nexus.d.ts +686 -0
- package/dist/store/schema/cleo-global/nexus.d.ts.map +1 -1
- package/dist/store/schema/cleo-global/nexus.js +175 -7
- package/dist/store/schema/cleo-global/nexus.js.map +1 -1
- package/dist/store/schema/cleo-global/skills.d.ts +2 -2
- package/dist/store/schema/cleo-project/audit.d.ts +5 -5
- package/dist/store/schema/cleo-project/docs.d.ts +3 -3
- package/dist/store/schema/cleo-project/lifecycle.d.ts +3 -3
- package/dist/store/schema/cleo-project/provenance-commits.d.ts +2 -2
- package/dist/store/schema/cleo-project/provenance-rest.d.ts +3 -3
- package/dist/store/schema/cleo-project/tasks-core-batch2.d.ts +1 -1
- package/dist/store/schema/cleo-project/tasks-core.d.ts +6 -6
- package/dist/store/schema/cleo-shared/brain.d.ts +26 -26
- package/dist/store/schema/lifecycle.d.ts +3 -3
- package/dist/store/schema/manifest.d.ts +1 -1
- package/dist/store/schema/memory-schema.d.ts +26 -26
- package/dist/store/schema/nexus-schema.d.ts +696 -2
- package/dist/store/schema/nexus-schema.d.ts.map +1 -1
- package/dist/store/schema/nexus-schema.js +175 -19
- package/dist/store/schema/nexus-schema.js.map +1 -1
- package/dist/store/schema/provenance/releases.d.ts +1 -1
- package/dist/store/schema/skills-schema.d.ts +2 -2
- package/dist/store/schema/tasks.d.ts +5 -5
- package/dist/store/session-binding-schema.d.ts +132 -0
- package/dist/store/session-binding-schema.d.ts.map +1 -0
- package/dist/store/session-binding-schema.js +53 -0
- package/dist/store/session-binding-schema.js.map +1 -0
- package/dist/store/session-store.d.ts +262 -4
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +486 -26
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/snapshot-gate.d.ts +239 -0
- package/dist/store/snapshot-gate.d.ts.map +1 -0
- package/dist/store/snapshot-gate.js +398 -0
- package/dist/store/snapshot-gate.js.map +1 -0
- package/dist/store/split-brain-import.js +2 -2
- package/dist/store/split-brain-import.js.map +1 -1
- package/dist/store/sqlite-backup.d.ts +55 -13
- package/dist/store/sqlite-backup.d.ts.map +1 -1
- package/dist/store/sqlite-backup.js +414 -106
- package/dist/store/sqlite-backup.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts +15 -0
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +63 -4
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sqlite.d.ts +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +53 -5
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/store-write-guard.d.ts +28 -0
- package/dist/store/store-write-guard.d.ts.map +1 -0
- package/dist/store/store-write-guard.js +50 -0
- package/dist/store/store-write-guard.js.map +1 -0
- package/dist/store/table-classification.d.ts +143 -0
- package/dist/store/table-classification.d.ts.map +1 -0
- package/dist/store/table-classification.js +1866 -0
- package/dist/store/table-classification.js.map +1 -0
- package/dist/store/task-version.d.ts +72 -0
- package/dist/store/task-version.d.ts.map +1 -0
- package/dist/store/task-version.js +93 -0
- package/dist/store/task-version.js.map +1 -0
- package/dist/store/tasks-schema.d.ts +1 -0
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/tasks-schema.js +13 -0
- package/dist/store/tasks-schema.js.map +1 -1
- package/dist/store/tasks-sqlite.d.ts +10 -3
- package/dist/store/tasks-sqlite.d.ts.map +1 -1
- package/dist/store/tasks-sqlite.js +12 -3
- package/dist/store/tasks-sqlite.js.map +1 -1
- package/dist/store/twin-collapse.d.ts +275 -0
- package/dist/store/twin-collapse.d.ts.map +1 -0
- package/dist/store/twin-collapse.js +805 -0
- package/dist/store/twin-collapse.js.map +1 -0
- package/dist/store/umbrella-data-accessor.d.ts +2 -2
- package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
- package/dist/store/umbrella-data-accessor.js +2 -2
- package/dist/store/umbrella-data-accessor.js.map +1 -1
- package/dist/store/validation-schemas.d.ts +63 -63
- package/dist/store/worktree-isolation-guard.d.ts +46 -24
- package/dist/store/worktree-isolation-guard.d.ts.map +1 -1
- package/dist/store/worktree-isolation-guard.js +69 -50
- package/dist/store/worktree-isolation-guard.js.map +1 -1
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/backup.js +3 -67
- package/dist/system/backup.js.map +1 -1
- package/dist/system/cleo-link.d.ts +163 -0
- package/dist/system/cleo-link.d.ts.map +1 -0
- package/dist/system/cleo-link.js +246 -0
- package/dist/system/cleo-link.js.map +1 -0
- package/dist/system/dependencies.d.ts.map +1 -1
- package/dist/system/dependencies.js +3 -6
- package/dist/system/dependencies.js.map +1 -1
- package/dist/system/global-delivery.d.ts +205 -0
- package/dist/system/global-delivery.d.ts.map +1 -0
- package/dist/system/global-delivery.js +320 -0
- package/dist/system/global-delivery.js.map +1 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +29 -6
- package/dist/system/health.js.map +1 -1
- package/dist/system/project-health.d.ts.map +1 -1
- package/dist/system/project-health.js +1 -20
- package/dist/system/project-health.js.map +1 -1
- package/dist/system/runtime.d.ts.map +1 -1
- package/dist/system/runtime.js +2 -12
- package/dist/system/runtime.js.map +1 -1
- package/dist/system/safestop.d.ts.map +1 -1
- package/dist/system/safestop.js +5 -2
- package/dist/system/safestop.js.map +1 -1
- package/dist/system/skill-fixtures.d.ts +200 -0
- package/dist/system/skill-fixtures.d.ts.map +1 -0
- package/dist/system/skill-fixtures.js +331 -0
- package/dist/system/skill-fixtures.js.map +1 -0
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +4 -2
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +173 -0
- package/dist/tasks/affected-packages.d.ts.map +1 -0
- package/dist/tasks/affected-packages.js +389 -0
- package/dist/tasks/affected-packages.js.map +1 -0
- package/dist/tasks/change-set.d.ts +151 -0
- package/dist/tasks/change-set.d.ts.map +1 -0
- package/dist/tasks/change-set.js +634 -0
- package/dist/tasks/change-set.js.map +1 -0
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +10 -5
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/done-plan.d.ts +103 -0
- package/dist/tasks/done-plan.d.ts.map +1 -0
- package/dist/tasks/done-plan.js +541 -0
- package/dist/tasks/done-plan.js.map +1 -0
- package/dist/tasks/done-record.d.ts +127 -0
- package/dist/tasks/done-record.d.ts.map +1 -0
- package/dist/tasks/done-record.js +373 -0
- package/dist/tasks/done-record.js.map +1 -0
- package/dist/tasks/duplicate-detector.d.ts +68 -11
- package/dist/tasks/duplicate-detector.d.ts.map +1 -1
- package/dist/tasks/duplicate-detector.js +314 -58
- package/dist/tasks/duplicate-detector.js.map +1 -1
- package/dist/tasks/evidence-paths.d.ts +78 -0
- package/dist/tasks/evidence-paths.d.ts.map +1 -0
- package/dist/tasks/evidence-paths.js +150 -0
- package/dist/tasks/evidence-paths.js.map +1 -0
- package/dist/tasks/evidence.d.ts +55 -0
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +223 -26
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-preview.d.ts +5 -0
- package/dist/tasks/gate-preview.d.ts.map +1 -1
- package/dist/tasks/gate-preview.js +25 -21
- package/dist/tasks/gate-preview.js.map +1 -1
- package/dist/tasks/gate-result-cache.d.ts +205 -0
- package/dist/tasks/gate-result-cache.d.ts.map +1 -0
- package/dist/tasks/gate-result-cache.js +285 -0
- package/dist/tasks/gate-result-cache.js.map +1 -0
- package/dist/tasks/gate-runner.d.ts +126 -3
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +483 -28
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/session-scope.d.ts +3 -1
- package/dist/tasks/session-scope.d.ts.map +1 -1
- package/dist/tasks/session-scope.js +18 -3
- package/dist/tasks/session-scope.js.map +1 -1
- package/dist/tasks/tool-resolver.d.ts +23 -6
- package/dist/tasks/tool-resolver.d.ts.map +1 -1
- package/dist/tasks/tool-resolver.js +146 -8
- package/dist/tasks/tool-resolver.js.map +1 -1
- package/dist/tasks/tool-vacuity.d.ts +71 -0
- package/dist/tasks/tool-vacuity.d.ts.map +1 -0
- package/dist/tasks/tool-vacuity.js +334 -0
- package/dist/tasks/tool-vacuity.js.map +1 -0
- package/dist/tasks/update.d.ts +8 -0
- package/dist/tasks/update.d.ts.map +1 -1
- package/dist/tasks/update.js +159 -78
- package/dist/tasks/update.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +18 -2
- package/dist/upgrade.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +20 -2
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +64 -4
- package/dist/validation/doctor/checks.js.map +1 -1
- package/dist/validation/doctor/index.d.ts +1 -1
- package/dist/validation/doctor/index.d.ts.map +1 -1
- package/dist/validation/doctor/index.js +1 -1
- package/dist/validation/doctor/index.js.map +1 -1
- package/dist/validation/doctor/utils.d.ts.map +1 -1
- package/dist/validation/doctor/utils.js +7 -2
- package/dist/validation/doctor/utils.js.map +1 -1
- package/dist/validation/engine-ops.d.ts +15 -0
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +249 -98
- package/dist/validation/engine-ops.js.map +1 -1
- package/migrations/drizzle-cleo-global/20260927000000_t12469-project-locations/migration.sql +121 -0
- package/migrations/drizzle-cleo-global/20260927010000_t12543-user-profile-project-scope/migration.sql +23 -0
- package/migrations/drizzle-cleo-global/20260928010000_t12470-location-candidates/migration.sql +72 -0
- package/migrations/drizzle-cleo-global/20260928020000_t12510-nexus-devices/migration.sql +34 -0
- package/migrations/drizzle-cleo-global/20260928030000_t12511-project-git-state/migration.sql +62 -0
- package/migrations/drizzle-cleo-project/20260927000000_t12499-session-terminal-bindings/migration.sql +34 -0
- package/migrations/drizzle-cleo-project/20260928000000_t12500-binding-provider-flag/migration.sql +15 -0
- package/package.json +23 -13
- package/schemas/config.schema.json +54 -0
- package/schemas/project-context.schema.json +51 -3
- package/schemas/project-info.schema.json +28 -1
- package/templates/CLEO-INJECTION.md +40 -332
- package/templates/CLEO-REFERENCE.md +303 -0
- package/templates/workflows/README.md +36 -14
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cleo doctor projects` — machine-wide integrity check of the project
|
|
3
|
+
* registry (T12471 · ADR-094).
|
|
4
|
+
*
|
|
5
|
+
* The registry is keyed by project id, never by path. A path is a per-device
|
|
6
|
+
* hint that goes stale when a project moves, is deleted, or is re-initialized
|
|
7
|
+
* under a new id. This module walks every registry row, probes its path, scans
|
|
8
|
+
* the directories projects live in for `.cleo/` declarations, and classifies
|
|
9
|
+
* each row (see `NexusRegistryFindingKind`):
|
|
10
|
+
*
|
|
11
|
+
* - {@link inspectProjectRegistry} is read-only and returns every non-`ok`
|
|
12
|
+
* row with its exact remedy.
|
|
13
|
+
* - {@link applyProjectRegistryRepair} applies only the safe classes: a
|
|
14
|
+
* `moved` row is rebound to the one path that declares its id, a
|
|
15
|
+
* `missing` row's location is recorded as `missing`, and an alias row whose
|
|
16
|
+
* truncated legacy key several projects claim is removed (T12589). No
|
|
17
|
+
* registry row is deleted. The rows the changes touch are imaged before and
|
|
18
|
+
* after, and both images are written to `nexus_audit_log` in the same
|
|
19
|
+
* transaction as the changes.
|
|
20
|
+
* - {@link rollbackProjectRegistryRepair} restores the before image, guarded
|
|
21
|
+
* on the current rows still matching the after image.
|
|
22
|
+
*
|
|
23
|
+
* Split identities, ambiguous moves, temp rows and home/root rows are owner
|
|
24
|
+
* decisions: they are reported with a remedy and never changed.
|
|
25
|
+
*
|
|
26
|
+
* Probes are bounded: at most `concurrency` filesystem reads at once, each
|
|
27
|
+
* with a `timeoutMs` budget. Only ENOENT/ENOTDIR prove a path gone; EACCES,
|
|
28
|
+
* EPERM or a timeout leave the row `unreadable` and untouched (#1606).
|
|
29
|
+
*
|
|
30
|
+
* @task T12471
|
|
31
|
+
* @epic T12468
|
|
32
|
+
* @see ADR-094 — write-once portable project identity
|
|
33
|
+
*/
|
|
34
|
+
import { randomUUID } from 'node:crypto';
|
|
35
|
+
import { existsSync } from 'node:fs';
|
|
36
|
+
import { readdir } from 'node:fs/promises';
|
|
37
|
+
import { homedir } from 'node:os';
|
|
38
|
+
import { basename, dirname, join, parse, resolve, sep } from 'node:path';
|
|
39
|
+
import { legacyAliasClaimants, readDeclaredProjectIdentity } from '@cleocode/paths';
|
|
40
|
+
import { eq, inArray, or } from 'drizzle-orm';
|
|
41
|
+
import { runWithConcurrency } from '../lib/concurrency.js';
|
|
42
|
+
import { readCheckoutNonce } from '../nexus/checkout-nonce.js';
|
|
43
|
+
import { collectCheckoutEvidence } from '../nexus/identity.js';
|
|
44
|
+
import { registryAliasClaimants } from '../nexus/legacy-alias.js';
|
|
45
|
+
import { confirmProjectLocation, currentDeviceId, demoteProjectLocation, holdingKey, isSupersededRegistryPath, LOCAL_DEVICE_SENTINEL, probeProjectHolding, } from '../nexus/path-map.js';
|
|
46
|
+
import { walkForCleoBounded, withinBudget } from '../nexus/projects-scan.js';
|
|
47
|
+
import { isEphemeralPath } from '../nexus/registry-hygiene.js';
|
|
48
|
+
import { getCleoHome } from '../paths.js';
|
|
49
|
+
import { isStrictlyInsideDir, readValidProjectTombstone } from '../project-tombstone.js';
|
|
50
|
+
/** Command that reports registry integrity; the prefix of the remedies below. */
|
|
51
|
+
const DOCTOR_COMMAND = 'cleo doctor projects';
|
|
52
|
+
/** Audit-log `action` of an applied repair. */
|
|
53
|
+
const APPLY_ACTION = 'doctor.projects.apply';
|
|
54
|
+
/** Audit-log `action` of a rollback. */
|
|
55
|
+
const ROLLBACK_ACTION = 'doctor.projects.rollback';
|
|
56
|
+
/** Default filesystem reads in flight at once. */
|
|
57
|
+
const DEFAULT_CONCURRENCY = 16;
|
|
58
|
+
/** Default budget for one filesystem probe. */
|
|
59
|
+
const DEFAULT_TIMEOUT_MS = 2000;
|
|
60
|
+
/** Default scan depth below each root. */
|
|
61
|
+
const DEFAULT_MAX_DEPTH = 2;
|
|
62
|
+
/** SQLite bound-parameter headroom per `IN (...)` list. */
|
|
63
|
+
const CHUNK = 400;
|
|
64
|
+
/** Every kind, so `counts` always carries all of them. */
|
|
65
|
+
const FINDING_KINDS = [
|
|
66
|
+
'ok',
|
|
67
|
+
'moved',
|
|
68
|
+
'ambiguous',
|
|
69
|
+
'split',
|
|
70
|
+
'possible-split',
|
|
71
|
+
'missing',
|
|
72
|
+
'temp',
|
|
73
|
+
'root',
|
|
74
|
+
'unreadable',
|
|
75
|
+
'other-device',
|
|
76
|
+
];
|
|
77
|
+
/** Error with a stable code, raised when a rollback is refused. */
|
|
78
|
+
export class RegistryRepairError extends Error {
|
|
79
|
+
code;
|
|
80
|
+
/** @override */
|
|
81
|
+
name = 'RegistryRepairError';
|
|
82
|
+
/**
|
|
83
|
+
* @param code - Stable error code (`E_NOT_FOUND`, `E_ROLLBACK_CONFLICT`).
|
|
84
|
+
* @param message - Explanation with the remedy.
|
|
85
|
+
*/
|
|
86
|
+
constructor(code, message) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.code = code;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Whether a registry path is a temp row: under a temp directory while the
|
|
93
|
+
* registry itself is persistent. A sandboxed registry (temp `CLEO_HOME`) keeps
|
|
94
|
+
* its temp fixtures legitimately, as encounter registration does.
|
|
95
|
+
*
|
|
96
|
+
* @param projectPath - Registered path.
|
|
97
|
+
* @param cleoHome - Home of the registry being checked.
|
|
98
|
+
* @returns `true` for a temp row.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* isTempRegistryPath('/tmp/fixture', '/home/me/.local/share/cleo'); // true
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function isTempRegistryPath(projectPath, cleoHome) {
|
|
106
|
+
return isEphemeralPath(projectPath) && !isEphemeralPath(cleoHome);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether a registry path is the home directory or a filesystem root: a
|
|
110
|
+
* project registered there claims every directory below it.
|
|
111
|
+
*
|
|
112
|
+
* @param projectPath - Registered path.
|
|
113
|
+
* @param home - Home directory (defaults to the current user's).
|
|
114
|
+
* @returns `true` for a home or root row.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* isHomeOrRootPath('/'); // true
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function isHomeOrRootPath(projectPath, home = homedir()) {
|
|
122
|
+
const resolved = resolve(projectPath);
|
|
123
|
+
return resolved === parse(resolved).root || resolved === resolve(home);
|
|
124
|
+
}
|
|
125
|
+
/** Split `items` into slices of at most {@link CHUNK}. */
|
|
126
|
+
function chunks(items) {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (let i = 0; i < items.length; i += CHUNK)
|
|
129
|
+
out.push(items.slice(i, i + CHUNK));
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Open the global registry store and its schema. */
|
|
133
|
+
async function openRegistry(cleoHome) {
|
|
134
|
+
const { getNexusRegistryDb, getNexusRegistryDbPath } = await import('../store/nexus-sqlite.js');
|
|
135
|
+
const schema = await import('../store/schema/nexus-schema.js');
|
|
136
|
+
return {
|
|
137
|
+
db: await getNexusRegistryDb(cleoHome),
|
|
138
|
+
storePath: getNexusRegistryDbPath(cleoHome),
|
|
139
|
+
...schema,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Scan `roots` and read the id each found project declares. Every `.cleo/`
|
|
144
|
+
* is first read asynchronously within the budget; only a directory that
|
|
145
|
+
* answered in time is read synchronously, so one hung mount cannot stall the
|
|
146
|
+
* scan. Projects in a trash directory are never candidates.
|
|
147
|
+
*/
|
|
148
|
+
async function scanRoots(roots, opts) {
|
|
149
|
+
const walked = await walkForCleoBounded(roots.filter((root) => !isTrashPath(root)), opts);
|
|
150
|
+
const timedOut = [...walked.timedOut];
|
|
151
|
+
const unreadable = [...walked.unreadable];
|
|
152
|
+
const found = walked.found.filter((path) => !isTrashPath(path));
|
|
153
|
+
const answered = await runWithConcurrency(found, opts.concurrency, async (path) => {
|
|
154
|
+
const read = await withinBudget(readdir(join(path, '.cleo')).then(() => 'ok', () => 'unreadable'), opts.timeoutMs);
|
|
155
|
+
if (read === 'timeout')
|
|
156
|
+
timedOut.push(path);
|
|
157
|
+
else if (read === 'unreadable')
|
|
158
|
+
unreadable.push(path);
|
|
159
|
+
return read === 'ok';
|
|
160
|
+
});
|
|
161
|
+
const projects = [];
|
|
162
|
+
found.forEach((path, index) => {
|
|
163
|
+
if (!answered[index])
|
|
164
|
+
return;
|
|
165
|
+
const declared = readDeclaredProjectIdentity(path);
|
|
166
|
+
if (!declared)
|
|
167
|
+
return;
|
|
168
|
+
projects.push({ path, projectId: declared.projectId, nonce: readCheckoutNonce(path) });
|
|
169
|
+
});
|
|
170
|
+
return { projects, timedOut: timedOut.sort(), unreadable: unreadable.sort() };
|
|
171
|
+
}
|
|
172
|
+
/** Path segments that mark a trash directory (macOS, Windows, freedesktop). */
|
|
173
|
+
const TRASH_SEGMENTS = new Set(['.trash', '.trashes', '$recycle.bin']);
|
|
174
|
+
/**
|
|
175
|
+
* Whether `path` lies in a trash directory. A project the owner trashed is
|
|
176
|
+
* never a move target, and its registry row may be cleaned.
|
|
177
|
+
*
|
|
178
|
+
* @param path - Absolute path.
|
|
179
|
+
* @returns `true` inside `.Trash`, `.Trashes`, `$RECYCLE.BIN` or `.local/share/Trash`.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* isTrashPath('/Users/me/.Trash/app'); // true
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function isTrashPath(path) {
|
|
187
|
+
const segments = resolve(path)
|
|
188
|
+
.split(sep)
|
|
189
|
+
.map((segment) => segment.toLowerCase());
|
|
190
|
+
return segments.some((segment, i) => TRASH_SEGMENTS.has(segment) ||
|
|
191
|
+
(segment === 'trash' && segments[i - 1] === 'share' && segments[i - 2] === '.local'));
|
|
192
|
+
}
|
|
193
|
+
/** `true` when `inner` is strictly inside `outer`, compared lexically. */
|
|
194
|
+
function isLexicallyInside(outer, inner) {
|
|
195
|
+
const o = resolve(outer);
|
|
196
|
+
return resolve(inner).startsWith(o.endsWith(sep) ? o : o + sep);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Directories to scan: the parent of every registered path, existing or not
|
|
200
|
+
* (a project is most often moved or renamed next to where it was), plus the
|
|
201
|
+
* operator's `--roots`. A filesystem root is never scanned by default; a
|
|
202
|
+
* parent that no longer exists is skipped by the walker.
|
|
203
|
+
*/
|
|
204
|
+
function scanRootsFor(paths, extra) {
|
|
205
|
+
const roots = new Set(extra.map((root) => resolve(root)));
|
|
206
|
+
for (const projectPath of paths) {
|
|
207
|
+
if (isSupersededRegistryPath(projectPath))
|
|
208
|
+
continue;
|
|
209
|
+
const parent = dirname(resolve(projectPath));
|
|
210
|
+
if (parent !== parse(parent).root)
|
|
211
|
+
roots.add(parent);
|
|
212
|
+
}
|
|
213
|
+
return [...roots].sort();
|
|
214
|
+
}
|
|
215
|
+
/** The remedy for a split proven by shared repository evidence. */
|
|
216
|
+
function splitRemedy(row, peer) {
|
|
217
|
+
const keepPeer = `to keep ${peer.projectId}: review \`cleo nexus show ${row.projectId}\`, then \`cleo nexus unregister ${row.projectId}\``;
|
|
218
|
+
const keepRow = `to keep ${row.projectId}: in "${peer.projectPath}" restore .cleo/project-id to ${row.projectId} ` +
|
|
219
|
+
`(\`git log -p -- .cleo/project-id\`)${peer.registered ? `, run \`cleo nexus unregister ${peer.projectId}\`` : ''}, ` +
|
|
220
|
+
'then run `cleo doctor project-identity --resolve` there';
|
|
221
|
+
return `Owner decision — CLEO never merges ids. ${keepPeer}; or ${keepRow}; or keep both: a fork is a distinct project.`;
|
|
222
|
+
}
|
|
223
|
+
/** Why a scanned path that declares a row's id is not a rebind target, or `null`. */
|
|
224
|
+
function ineligibleTarget(target, mine, cleoHome) {
|
|
225
|
+
if (isTrashPath(target.path))
|
|
226
|
+
return 'it is in the trash';
|
|
227
|
+
if (isLexicallyInside(cleoHome, target.path))
|
|
228
|
+
return 'it is inside the CLEO home (a worktree)';
|
|
229
|
+
if (readValidProjectTombstone(target.path))
|
|
230
|
+
return 'it holds a valid reroot tombstone (the project was rerooted away from it)';
|
|
231
|
+
// #1606 reroot geometry: the location was demoted to missing while a
|
|
232
|
+
// location strictly inside it became the project's home.
|
|
233
|
+
const here = mine.find((l) => l.path === target.path);
|
|
234
|
+
if (here?.state === 'missing' &&
|
|
235
|
+
mine.some((l) => l.path !== target.path && isStrictlyInsideDir(target.path, l.path)))
|
|
236
|
+
return 'the project was rerooted away from it (its location is recorded missing)';
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Classify one row whose registered path provably no longer holds it.
|
|
241
|
+
*
|
|
242
|
+
* A row is rebound only to a path whose untracked checkout NONCE equals one
|
|
243
|
+
* recorded for the id (T12470 · #1606): the committed id alone is forgeable
|
|
244
|
+
* (a clone, a copied `.cleo/project-id`), so an id-only match is reported
|
|
245
|
+
* with the explicit `project-identity --resolve` remedy and never applied.
|
|
246
|
+
*/
|
|
247
|
+
async function classifyGone(row, ctx) {
|
|
248
|
+
const base = { projectId: row.projectId, name: row.name, projectPath: row.projectPath };
|
|
249
|
+
const recordedNonces = new Set(ctx.mine.map((l) => l.checkoutNonce).filter((n) => n !== null));
|
|
250
|
+
const elsewhere = (ctx.byId.get(row.projectId) ?? []).filter((p) => p.path !== row.projectPath);
|
|
251
|
+
const excluded = elsewhere
|
|
252
|
+
.map((p) => ({ p, reason: ineligibleTarget(p, ctx.mine, ctx.cleoHome) }))
|
|
253
|
+
.filter((x) => x.reason !== null);
|
|
254
|
+
const eligible = elsewhere.filter((p) => !excluded.some((x) => x.p === p));
|
|
255
|
+
const proven = eligible.filter((p) => p.nonce !== null && recordedNonces.has(p.nonce));
|
|
256
|
+
const foundAt = elsewhere.map((p) => p.path);
|
|
257
|
+
const target = proven.length === 1 ? proven[0] : undefined;
|
|
258
|
+
if (target) {
|
|
259
|
+
return {
|
|
260
|
+
finding: {
|
|
261
|
+
...base,
|
|
262
|
+
kind: 'moved',
|
|
263
|
+
message: `${row.projectId} is no longer at ${row.projectPath}; ${target.path} declares it and its checkout nonce matches (a real move).`,
|
|
264
|
+
remedy: `${DOCTOR_COMMAND} --apply (rebinds the row to ${target.path}; permissions stay with the row)`,
|
|
265
|
+
applicable: true,
|
|
266
|
+
foundAt,
|
|
267
|
+
proof: 'nonce',
|
|
268
|
+
},
|
|
269
|
+
rebindTo: { path: target.path, nonce: target.nonce },
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (proven.length > 1) {
|
|
273
|
+
return {
|
|
274
|
+
finding: {
|
|
275
|
+
...base,
|
|
276
|
+
kind: 'ambiguous',
|
|
277
|
+
message: `${row.projectId} is no longer at ${row.projectPath}; ${proven.length} paths carry its checkout nonce (copies of one checkout).`,
|
|
278
|
+
remedy: `cd into the checkout to keep and run \`cleo doctor project-identity --resolve\` (candidates: ${proven.map((p) => `"${p.path}"`).join(', ')})`,
|
|
279
|
+
applicable: false,
|
|
280
|
+
foundAt,
|
|
281
|
+
proof: 'nonce',
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (elsewhere.length > 0) {
|
|
286
|
+
const why = excluded.map((x) => `${x.p.path}: ${x.reason}`).join('; ');
|
|
287
|
+
const candidates = eligible.map((p) => `"${p.path}"`).join(', ');
|
|
288
|
+
return {
|
|
289
|
+
finding: {
|
|
290
|
+
...base,
|
|
291
|
+
kind: 'moved',
|
|
292
|
+
message: `${row.projectId} is no longer at ${row.projectPath}. ` +
|
|
293
|
+
(eligible.length > 0
|
|
294
|
+
? `${candidates} declare${eligible.length === 1 ? 's' : ''} the id, but no checkout nonce proves a move (a clone or a copied .cleo/project-id declares it too).`
|
|
295
|
+
: 'Every path that declares the id is excluded.') +
|
|
296
|
+
(why ? ` Not a target — ${why}.` : ''),
|
|
297
|
+
remedy: eligible.length > 0
|
|
298
|
+
? `If it IS the project, confirm explicitly: cd into it and run \`cleo doctor project-identity --resolve\` (${candidates}). Never applied automatically.`
|
|
299
|
+
: `Inspect the paths listed; if the project left this device, the row can stay (\`cleo nexus unregister ${row.projectId}\` only when it is gone for good).`,
|
|
300
|
+
applicable: false,
|
|
301
|
+
foundAt,
|
|
302
|
+
proof: 'id-only',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const split = await splitPeers(row, ctx);
|
|
307
|
+
const localState = ctx.mine.find((l) => l.path === row.projectPath)?.state ?? null;
|
|
308
|
+
const markMissing = ctx.gone !== 'sentinel' && localState !== 'missing';
|
|
309
|
+
const byEvidence = split.filter((peer) => peer.matchedBy !== 'name');
|
|
310
|
+
const peer = byEvidence[0];
|
|
311
|
+
if (peer) {
|
|
312
|
+
return {
|
|
313
|
+
finding: {
|
|
314
|
+
...base,
|
|
315
|
+
kind: 'split',
|
|
316
|
+
message: `Split identity: ${row.projectId} is gone from ${row.projectPath}, and ${peer.projectPath} holds the same repository (${peer.matchedBy === 'remote' ? 'git remote' : 'root commit'}) under ${peer.projectId} (a re-minted id).`,
|
|
317
|
+
remedy: splitRemedy(row, peer),
|
|
318
|
+
applicable: false,
|
|
319
|
+
splitWith: byEvidence,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const named = split[0];
|
|
324
|
+
if (named) {
|
|
325
|
+
return {
|
|
326
|
+
finding: {
|
|
327
|
+
...base,
|
|
328
|
+
kind: 'possible-split',
|
|
329
|
+
message: `${row.projectId} is gone from ${row.projectPath}; ${named.projectPath} has the same name under ${named.projectId}, but no repository evidence ties them — it may be an unrelated project.`,
|
|
330
|
+
remedy: `Inspect only — compare \`cleo nexus show ${row.projectId}\` with "${named.projectPath}" (its git log and .cleo/project-id). ` +
|
|
331
|
+
"Never change the other project's id or registry row on a name match alone; " +
|
|
332
|
+
'keep both if they differ: a fork is a distinct project.' +
|
|
333
|
+
(markMissing
|
|
334
|
+
? ` \`${DOCTOR_COMMAND} --apply\` records ${row.projectId}'s location as missing and keeps its row.`
|
|
335
|
+
: ''),
|
|
336
|
+
applicable: markMissing,
|
|
337
|
+
splitWith: split,
|
|
338
|
+
},
|
|
339
|
+
markMissing,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (ctx.gone === 'path' && isTempRegistryPath(row.projectPath, ctx.cleoHome)) {
|
|
343
|
+
return {
|
|
344
|
+
finding: {
|
|
345
|
+
...base,
|
|
346
|
+
kind: 'temp',
|
|
347
|
+
message: `${row.projectPath} is a temp directory that no longer exists (a test fixture or scratch project).`,
|
|
348
|
+
remedy: 'cleo nexus projects clean --include-temp --dry-run then without --dry-run',
|
|
349
|
+
applicable: false,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const where = ctx.gone === 'sentinel'
|
|
354
|
+
? `${row.projectId} has no live location on this device`
|
|
355
|
+
: ctx.gone === 'path'
|
|
356
|
+
? `${row.projectPath} no longer exists`
|
|
357
|
+
: ctx.gone === 'cleo'
|
|
358
|
+
? `${row.projectPath} exists but has no .cleo/ (the project was moved out of it)`
|
|
359
|
+
: `${row.projectPath}/.cleo now declares ${ctx.gone.declares}, not ${row.projectId}`;
|
|
360
|
+
return {
|
|
361
|
+
finding: {
|
|
362
|
+
...base,
|
|
363
|
+
kind: 'missing',
|
|
364
|
+
message: `${where}, and no scanned path declares ${row.projectId}.`,
|
|
365
|
+
remedy: `If it moved, scan its new parent: \`${DOCTOR_COMMAND} --roots <dir>\`. ` +
|
|
366
|
+
`If it is gone for good: \`cleo nexus unregister ${row.projectId}\`.` +
|
|
367
|
+
(markMissing
|
|
368
|
+
? ` \`${DOCTOR_COMMAND} --apply\` records the location as missing and keeps the row.`
|
|
369
|
+
: ''),
|
|
370
|
+
applicable: markMissing,
|
|
371
|
+
},
|
|
372
|
+
markMissing,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Scanned projects with the row's name under another id. A peer whose
|
|
377
|
+
* repository evidence (git remote or root commit, recorded for the row and
|
|
378
|
+
* read from the peer) matches is tagged by that evidence; otherwise `name`.
|
|
379
|
+
*/
|
|
380
|
+
async function splitPeers(row, ctx) {
|
|
381
|
+
const name = isSupersededRegistryPath(row.projectPath) ? row.name : basename(row.projectPath);
|
|
382
|
+
const remotes = new Set(ctx.mine.map((l) => l.gitRemote).filter((v) => !!v));
|
|
383
|
+
const commits = new Set(ctx.mine.map((l) => l.gitRootCommit).filter((v) => !!v));
|
|
384
|
+
const peers = [];
|
|
385
|
+
for (const p of ctx.scanned) {
|
|
386
|
+
if (p.projectId === row.projectId || ctx.linked(p.projectId, row.projectId))
|
|
387
|
+
continue;
|
|
388
|
+
if (basename(p.path) !== name && basename(p.path) !== row.name)
|
|
389
|
+
continue;
|
|
390
|
+
const evidence = remotes.size > 0 || commits.size > 0 ? await ctx.evidenceOf(p.path) : null;
|
|
391
|
+
// A shared root commit with a DIFFERENT remote is a fork or a second
|
|
392
|
+
// clone of one template — a distinct project, never proof of a split.
|
|
393
|
+
const remotesDiffer = !!evidence?.gitRemote && remotes.size > 0 && !remotes.has(evidence.gitRemote);
|
|
394
|
+
const matchedBy = evidence?.gitRemote && remotes.has(evidence.gitRemote)
|
|
395
|
+
? 'remote'
|
|
396
|
+
: evidence?.gitRootCommit && commits.has(evidence.gitRootCommit) && !remotesDiffer
|
|
397
|
+
? 'root-commit'
|
|
398
|
+
: 'name';
|
|
399
|
+
peers.push({
|
|
400
|
+
projectId: p.projectId,
|
|
401
|
+
projectPath: p.path,
|
|
402
|
+
registered: ctx.registered.has(p.projectId),
|
|
403
|
+
matchedBy,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return peers;
|
|
407
|
+
}
|
|
408
|
+
/** Classify one row whose registered path still holds it. */
|
|
409
|
+
function classifyHeld(row, remotePeers, cleoHome) {
|
|
410
|
+
const base = { projectId: row.projectId, name: row.name, projectPath: row.projectPath };
|
|
411
|
+
if (isHomeOrRootPath(row.projectPath)) {
|
|
412
|
+
return {
|
|
413
|
+
finding: {
|
|
414
|
+
...base,
|
|
415
|
+
kind: 'root',
|
|
416
|
+
message: `${row.projectPath} is the home directory or a filesystem root; a project there claims every directory below it.`,
|
|
417
|
+
remedy: `If a real project lives below it, run \`cleo project reroot <dir>\` from ${row.projectPath}; otherwise \`cleo nexus unregister ${row.projectId}\``,
|
|
418
|
+
applicable: false,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (isTempRegistryPath(row.projectPath, cleoHome)) {
|
|
423
|
+
return {
|
|
424
|
+
finding: {
|
|
425
|
+
...base,
|
|
426
|
+
kind: 'temp',
|
|
427
|
+
message: `${row.projectPath} is under a temp directory (a test fixture or scratch project in the persistent registry).`,
|
|
428
|
+
remedy: 'cleo nexus projects clean --include-temp --dry-run then without --dry-run',
|
|
429
|
+
applicable: false,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const peer = remotePeers[0];
|
|
434
|
+
if (peer) {
|
|
435
|
+
return {
|
|
436
|
+
finding: {
|
|
437
|
+
...base,
|
|
438
|
+
kind: 'split',
|
|
439
|
+
message: `Split identity: ${row.projectId} at ${row.projectPath} and ${peer.projectId} at ${peer.projectPath} share a git remote.`,
|
|
440
|
+
remedy: splitRemedy(row, peer),
|
|
441
|
+
applicable: false,
|
|
442
|
+
splitWith: remotePeers,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
finding: {
|
|
448
|
+
...base,
|
|
449
|
+
kind: 'ok',
|
|
450
|
+
message: 'The registered path holds the project.',
|
|
451
|
+
remedy: null,
|
|
452
|
+
applicable: false,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/** Gather registry state, probe, scan and classify. Read-only. */
|
|
457
|
+
async function inspect(opts) {
|
|
458
|
+
const cleoHome = opts.cleoHome ?? getCleoHome();
|
|
459
|
+
const budget = scanBudget(opts);
|
|
460
|
+
const { db, storePath, projectRegistry, projectLocations, projectIdAliases } = await openRegistry(cleoHome);
|
|
461
|
+
const rows = db
|
|
462
|
+
.select({
|
|
463
|
+
projectId: projectRegistry.projectId,
|
|
464
|
+
projectPath: projectRegistry.projectPath,
|
|
465
|
+
name: projectRegistry.name,
|
|
466
|
+
})
|
|
467
|
+
.from(projectRegistry)
|
|
468
|
+
.all();
|
|
469
|
+
const locations = db
|
|
470
|
+
.select({
|
|
471
|
+
projectId: projectLocations.projectId,
|
|
472
|
+
deviceId: projectLocations.deviceId,
|
|
473
|
+
path: projectLocations.path,
|
|
474
|
+
state: projectLocations.state,
|
|
475
|
+
checkoutNonce: projectLocations.checkoutNonce,
|
|
476
|
+
gitRemote: projectLocations.gitRemote,
|
|
477
|
+
gitRootCommit: projectLocations.gitRootCommit,
|
|
478
|
+
})
|
|
479
|
+
.from(projectLocations)
|
|
480
|
+
.all();
|
|
481
|
+
const aliases = db
|
|
482
|
+
.select({ legacyId: projectIdAliases.legacyId, canonicalId: projectIdAliases.canonicalId })
|
|
483
|
+
.from(projectIdAliases)
|
|
484
|
+
.all();
|
|
485
|
+
const deviceId = currentDeviceId();
|
|
486
|
+
const local = (l) => l.deviceId === deviceId || l.deviceId === LOCAL_DEVICE_SENTINEL;
|
|
487
|
+
const aliasPairs = new Set(aliases.map((a) => `${a.legacyId}\u0000${a.canonicalId}`));
|
|
488
|
+
const linked = (a, b) => aliasPairs.has(`${a}\u0000${b}`) || aliasPairs.has(`${b}\u0000${a}`);
|
|
489
|
+
const registered = new Map(rows.map((row) => [row.projectId, row]));
|
|
490
|
+
const localById = new Map();
|
|
491
|
+
for (const l of locations) {
|
|
492
|
+
if (!local(l))
|
|
493
|
+
continue;
|
|
494
|
+
const list = localById.get(l.projectId) ?? [];
|
|
495
|
+
list.push(l);
|
|
496
|
+
localById.set(l.projectId, list);
|
|
497
|
+
}
|
|
498
|
+
const roots = scanRootsFor(rows.map((row) => row.projectPath), opts.roots ?? []);
|
|
499
|
+
const scan = await scanRoots(roots, budget);
|
|
500
|
+
const byId = new Map();
|
|
501
|
+
for (const project of scan.projects) {
|
|
502
|
+
const list = byId.get(project.projectId) ?? [];
|
|
503
|
+
list.push(project);
|
|
504
|
+
byId.set(project.projectId, list);
|
|
505
|
+
}
|
|
506
|
+
// Registered rows sharing one git remote on this device are one project
|
|
507
|
+
// under two ids — unless one is nested inside another registered project
|
|
508
|
+
// (a fixture project inside a repo shares the repo's remote).
|
|
509
|
+
const realPaths = rows.filter((r) => !isSupersededRegistryPath(r.projectPath));
|
|
510
|
+
const nested = new Set(realPaths
|
|
511
|
+
.filter((r) => realPaths.some((o) => o.projectId !== r.projectId && isLexicallyInside(o.projectPath, r.projectPath)))
|
|
512
|
+
.map((r) => r.projectId));
|
|
513
|
+
const remoteOf = new Map();
|
|
514
|
+
for (const l of locations) {
|
|
515
|
+
const row = registered.get(l.projectId);
|
|
516
|
+
if (l.gitRemote && row && l.path === row.projectPath && local(l) && !nested.has(l.projectId))
|
|
517
|
+
remoteOf.set(l.projectId, l.gitRemote);
|
|
518
|
+
}
|
|
519
|
+
const holdings = await runWithConcurrency(rows, budget.concurrency, async (row) => {
|
|
520
|
+
if (isSupersededRegistryPath(row.projectPath))
|
|
521
|
+
return 'no';
|
|
522
|
+
const here = locations.filter((l) => l.projectId === row.projectId && l.path === row.projectPath);
|
|
523
|
+
if (here.length > 0 && !here.some(local))
|
|
524
|
+
return 'other-device';
|
|
525
|
+
return probeProjectHolding(row.projectPath, row.projectId, budget.timeoutMs);
|
|
526
|
+
});
|
|
527
|
+
const evidence = new Map();
|
|
528
|
+
const evidenceOf = (path) => {
|
|
529
|
+
const cached = evidence.get(path);
|
|
530
|
+
if (cached)
|
|
531
|
+
return cached;
|
|
532
|
+
const pending = withinBudget(collectCheckoutEvidence(path).catch(() => null), budget.timeoutMs).then((settled) => (settled === 'timeout' ? null : settled));
|
|
533
|
+
evidence.set(path, pending);
|
|
534
|
+
return pending;
|
|
535
|
+
};
|
|
536
|
+
const planned = await runWithConcurrency(rows, budget.concurrency, async (row, index) => {
|
|
537
|
+
const holding = holdings[index];
|
|
538
|
+
const base = { projectId: row.projectId, name: row.name, projectPath: row.projectPath };
|
|
539
|
+
if (holding === 'other-device') {
|
|
540
|
+
return {
|
|
541
|
+
finding: {
|
|
542
|
+
...base,
|
|
543
|
+
kind: 'other-device',
|
|
544
|
+
message: `${row.projectPath} was recorded on another device; it is not probed here.`,
|
|
545
|
+
remedy: null,
|
|
546
|
+
applicable: false,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
if (holding === 'unknown') {
|
|
551
|
+
return {
|
|
552
|
+
finding: {
|
|
553
|
+
...base,
|
|
554
|
+
kind: 'unreadable',
|
|
555
|
+
message: `${row.projectPath}/.cleo could not be read (permission denied or timed out). Only a missing path proves a project gone, so the row is kept as is.`,
|
|
556
|
+
remedy: `Check access to "${row.projectPath}" (\`ls -ld "${row.projectPath}/.cleo"\`), then re-run \`${DOCTOR_COMMAND}\`; or \`cleo nexus unregister ${row.projectId}\` if you no longer need it`,
|
|
557
|
+
applicable: false,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (holding === 'yes') {
|
|
562
|
+
const remote = remoteOf.get(row.projectId);
|
|
563
|
+
const peers = remote
|
|
564
|
+
? rows
|
|
565
|
+
.filter((other) => other.projectId !== row.projectId &&
|
|
566
|
+
remoteOf.get(other.projectId) === remote &&
|
|
567
|
+
!linked(other.projectId, row.projectId))
|
|
568
|
+
.map((other) => ({
|
|
569
|
+
projectId: other.projectId,
|
|
570
|
+
projectPath: other.projectPath,
|
|
571
|
+
registered: true,
|
|
572
|
+
matchedBy: 'remote',
|
|
573
|
+
}))
|
|
574
|
+
: [];
|
|
575
|
+
return classifyHeld(row, peers, cleoHome);
|
|
576
|
+
}
|
|
577
|
+
return classifyGone(row, {
|
|
578
|
+
byId,
|
|
579
|
+
scanned: scan.projects,
|
|
580
|
+
registered,
|
|
581
|
+
linked,
|
|
582
|
+
mine: localById.get(row.projectId) ?? [],
|
|
583
|
+
gone: goneShape(row),
|
|
584
|
+
cleoHome,
|
|
585
|
+
evidenceOf,
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
// T12589: a truncated legacy key several projects claim names none of them.
|
|
589
|
+
const recorded = rows.map((row) => ({ projectId: row.projectId, path: row.projectPath }));
|
|
590
|
+
const ambiguousAliases = aliases.flatMap((alias) => {
|
|
591
|
+
const claimants = legacyAliasClaimants(alias.legacyId, alias.canonicalId, recorded);
|
|
592
|
+
if (claimants.length < 2)
|
|
593
|
+
return [];
|
|
594
|
+
return [
|
|
595
|
+
{
|
|
596
|
+
legacyId: alias.legacyId,
|
|
597
|
+
canonicalId: alias.canonicalId,
|
|
598
|
+
claimants,
|
|
599
|
+
message: `Alias ${alias.legacyId} is claimed by ${claimants.length} projects (${claimants.join(', ')}); it resolves to none of them.`,
|
|
600
|
+
remedy: `${DOCTOR_COMMAND} --apply`,
|
|
601
|
+
},
|
|
602
|
+
];
|
|
603
|
+
});
|
|
604
|
+
const counts = Object.fromEntries(FINDING_KINDS.map((kind) => [kind, 0]));
|
|
605
|
+
for (const p of planned)
|
|
606
|
+
counts[p.finding.kind]++;
|
|
607
|
+
return {
|
|
608
|
+
report: {
|
|
609
|
+
dryRun: true,
|
|
610
|
+
storePath,
|
|
611
|
+
roots,
|
|
612
|
+
scan: {
|
|
613
|
+
projectsFound: scan.projects.length,
|
|
614
|
+
timedOut: scan.timedOut,
|
|
615
|
+
unreadable: scan.unreadable,
|
|
616
|
+
},
|
|
617
|
+
counts,
|
|
618
|
+
findings: planned.filter((p) => p.finding.kind !== 'ok').map((p) => p.finding),
|
|
619
|
+
ambiguousAliases,
|
|
620
|
+
},
|
|
621
|
+
planned,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* What a registered path that provably no longer holds its project holds
|
|
626
|
+
* now. Read only after the bounded probe answered `no`, so these reads hit a
|
|
627
|
+
* directory that already responded.
|
|
628
|
+
*/
|
|
629
|
+
function goneShape(row) {
|
|
630
|
+
if (isSupersededRegistryPath(row.projectPath))
|
|
631
|
+
return 'sentinel';
|
|
632
|
+
const declared = readDeclaredProjectIdentity(row.projectPath);
|
|
633
|
+
if (declared && declared.projectId !== row.projectId)
|
|
634
|
+
return { declares: declared.projectId };
|
|
635
|
+
return existsSync(join(row.projectPath, '.cleo')) || existsSync(row.projectPath)
|
|
636
|
+
? 'cleo'
|
|
637
|
+
: 'path';
|
|
638
|
+
}
|
|
639
|
+
/** Resolve the scan budget from options. */
|
|
640
|
+
function scanBudget(opts) {
|
|
641
|
+
return {
|
|
642
|
+
maxDepth: Math.max(0, Math.min(opts.maxDepth ?? DEFAULT_MAX_DEPTH, 20)),
|
|
643
|
+
concurrency: Math.max(1, opts.concurrency ?? DEFAULT_CONCURRENCY),
|
|
644
|
+
timeoutMs: Math.max(1, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Classify every registry row, read-only.
|
|
649
|
+
*
|
|
650
|
+
* @param opts - Extra scan roots, depth, concurrency, per-probe budget, registry home.
|
|
651
|
+
* @returns Counts per kind and every non-`ok` row with its exact remedy.
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```ts
|
|
655
|
+
* const report = await inspectProjectRegistry({ roots: ['/home/me/src'] });
|
|
656
|
+
* for (const f of report.findings) console.log(f.kind, f.projectPath, f.remedy);
|
|
657
|
+
* ```
|
|
658
|
+
*/
|
|
659
|
+
export async function inspectProjectRegistry(opts = {}) {
|
|
660
|
+
return (await inspect(opts)).report;
|
|
661
|
+
}
|
|
662
|
+
/** Stable text of a row, independent of key order. */
|
|
663
|
+
function rowKey(row) {
|
|
664
|
+
return JSON.stringify(Object.entries(row).sort(([a], [b]) => a.localeCompare(b)));
|
|
665
|
+
}
|
|
666
|
+
/** Sort image rows so two images compare by content. */
|
|
667
|
+
function normalizeImage(image) {
|
|
668
|
+
const sort = (rows) => [...rows].sort((a, b) => rowKey(a).localeCompare(rowKey(b)));
|
|
669
|
+
return {
|
|
670
|
+
registry: sort(image.registry),
|
|
671
|
+
locations: sort(image.locations),
|
|
672
|
+
paths: sort(image.paths),
|
|
673
|
+
...(image.aliases ? { aliases: sort(image.aliases) } : {}),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/** Read every row of the three tables that names one of `ids` or `paths`. */
|
|
677
|
+
function captureImage(db, schema, ids, paths, aliasKeys) {
|
|
678
|
+
const { projectRegistry, projectLocations, projectPaths, projectIdAliases } = schema;
|
|
679
|
+
const aliasRows = [];
|
|
680
|
+
for (const slice of chunks(aliasKeys ?? []))
|
|
681
|
+
for (const row of db
|
|
682
|
+
.select()
|
|
683
|
+
.from(projectIdAliases)
|
|
684
|
+
.where(inArray(projectIdAliases.legacyId, slice))
|
|
685
|
+
.all())
|
|
686
|
+
aliasRows.push({ ...row });
|
|
687
|
+
const registry = new Map();
|
|
688
|
+
const locations = new Map();
|
|
689
|
+
const pathRows = new Map();
|
|
690
|
+
for (const idSlice of chunks(ids.length > 0 ? ids : [''])) {
|
|
691
|
+
for (const pathSlice of chunks(paths.length > 0 ? paths : [''])) {
|
|
692
|
+
for (const row of db
|
|
693
|
+
.select()
|
|
694
|
+
.from(projectRegistry)
|
|
695
|
+
.where(or(inArray(projectRegistry.projectId, idSlice), inArray(projectRegistry.projectPath, pathSlice)))
|
|
696
|
+
.all())
|
|
697
|
+
registry.set(row.projectId, { ...row });
|
|
698
|
+
for (const row of db
|
|
699
|
+
.select()
|
|
700
|
+
.from(projectLocations)
|
|
701
|
+
.where(or(inArray(projectLocations.projectId, idSlice), inArray(projectLocations.path, pathSlice)))
|
|
702
|
+
.all())
|
|
703
|
+
locations.set(`${row.projectId}\u0000${row.deviceId}\u0000${row.path}`, { ...row });
|
|
704
|
+
for (const row of db
|
|
705
|
+
.select()
|
|
706
|
+
.from(projectPaths)
|
|
707
|
+
.where(or(inArray(projectPaths.projectId, idSlice), inArray(projectPaths.projectPath, pathSlice)))
|
|
708
|
+
.all())
|
|
709
|
+
pathRows.set(row.projectPath, { ...row });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return normalizeImage({
|
|
713
|
+
registry: [...registry.values()],
|
|
714
|
+
locations: [...locations.values()],
|
|
715
|
+
paths: [...pathRows.values()],
|
|
716
|
+
...(aliasKeys ? { aliases: aliasRows } : {}),
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
/** Delete every row of the three tables in scope. */
|
|
720
|
+
function deleteScope(db, schema, ids, paths, aliasKeys = []) {
|
|
721
|
+
const { projectRegistry, projectLocations, projectPaths, projectIdAliases } = schema;
|
|
722
|
+
for (const slice of chunks(aliasKeys))
|
|
723
|
+
db.delete(projectIdAliases).where(inArray(projectIdAliases.legacyId, slice)).run();
|
|
724
|
+
for (const idSlice of chunks(ids.length > 0 ? ids : [''])) {
|
|
725
|
+
for (const pathSlice of chunks(paths.length > 0 ? paths : [''])) {
|
|
726
|
+
db.delete(projectRegistry)
|
|
727
|
+
.where(or(inArray(projectRegistry.projectId, idSlice), inArray(projectRegistry.projectPath, pathSlice)))
|
|
728
|
+
.run();
|
|
729
|
+
db.delete(projectLocations)
|
|
730
|
+
.where(or(inArray(projectLocations.projectId, idSlice), inArray(projectLocations.path, pathSlice)))
|
|
731
|
+
.run();
|
|
732
|
+
db.delete(projectPaths)
|
|
733
|
+
.where(or(inArray(projectPaths.projectId, idSlice), inArray(projectPaths.projectPath, pathSlice)))
|
|
734
|
+
.run();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* The rows a repair can change, fixed BEFORE it runs so the before and after
|
|
740
|
+
* images cover the same rows: every touched id, every touched path, and the id
|
|
741
|
+
* of any other project registered at a touched path (a rebind re-homes that
|
|
742
|
+
* row). A rollback deletes exactly this scope and re-inserts the before image,
|
|
743
|
+
* so a row outside it is never touched and a row inside it is always restored.
|
|
744
|
+
*/
|
|
745
|
+
function repairScope(db, schema, touchedIds, touchedPaths) {
|
|
746
|
+
const ids = new Set(touchedIds);
|
|
747
|
+
for (const slice of chunks([...new Set(touchedPaths)]))
|
|
748
|
+
for (const row of db
|
|
749
|
+
.select({ projectId: schema.projectRegistry.projectId })
|
|
750
|
+
.from(schema.projectRegistry)
|
|
751
|
+
.where(inArray(schema.projectRegistry.projectPath, slice))
|
|
752
|
+
.all())
|
|
753
|
+
ids.add(row.projectId);
|
|
754
|
+
// Every registry writer re-keys migration-backfilled (`local`) location rows
|
|
755
|
+
// to this device first (adoptLocalDeviceRows). Those rows belong to ANY
|
|
756
|
+
// project, so their ids join the scope: the before image keeps them as they
|
|
757
|
+
// were and a rollback restores them exactly.
|
|
758
|
+
for (const row of db
|
|
759
|
+
.select({ projectId: schema.projectLocations.projectId })
|
|
760
|
+
.from(schema.projectLocations)
|
|
761
|
+
.where(eq(schema.projectLocations.deviceId, LOCAL_DEVICE_SENTINEL))
|
|
762
|
+
.all())
|
|
763
|
+
ids.add(row.projectId);
|
|
764
|
+
return { ids: [...ids].sort(), paths: [...new Set(touchedPaths)].sort() };
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Probe, before the write transaction, every local location the writers
|
|
768
|
+
* would otherwise probe synchronously inside it (T12471): the live locations
|
|
769
|
+
* of each id in scope. Keyed by {@link holdingKey}.
|
|
770
|
+
*/
|
|
771
|
+
async function preProbe(db, schema, ids, budget) {
|
|
772
|
+
const deviceId = currentDeviceId();
|
|
773
|
+
const targets = [];
|
|
774
|
+
for (const slice of chunks(ids))
|
|
775
|
+
for (const l of db
|
|
776
|
+
.select({
|
|
777
|
+
projectId: schema.projectLocations.projectId,
|
|
778
|
+
path: schema.projectLocations.path,
|
|
779
|
+
deviceId: schema.projectLocations.deviceId,
|
|
780
|
+
state: schema.projectLocations.state,
|
|
781
|
+
})
|
|
782
|
+
.from(schema.projectLocations)
|
|
783
|
+
.where(inArray(schema.projectLocations.projectId, slice))
|
|
784
|
+
.all())
|
|
785
|
+
if (l.state === 'live' && (l.deviceId === deviceId || l.deviceId === LOCAL_DEVICE_SENTINEL))
|
|
786
|
+
targets.push(l);
|
|
787
|
+
const holdings = await runWithConcurrency(targets, budget.concurrency, (t) => probeProjectHolding(t.path, t.projectId, budget.timeoutMs));
|
|
788
|
+
return new Map(targets.map((t, i) => [holdingKey(t.projectId, t.path), holdings[i] ?? 'unknown']));
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Inspect, then apply the safe repairs: rebind every `moved` row to the path
|
|
792
|
+
* that declares its id, record every `missing` row's location as
|
|
793
|
+
* `missing`, and remove every alias row whose key several projects claim
|
|
794
|
+
* (T12589). One immediate transaction makes the changes and writes the
|
|
795
|
+
* receipt (before and after images of every touched row, alias rows
|
|
796
|
+
* included) to `nexus_audit_log`. No registry row is deleted, and nothing
|
|
797
|
+
* outside the registry is written. A row that changed since inspection is
|
|
798
|
+
* skipped.
|
|
799
|
+
*
|
|
800
|
+
* @param opts - Same options as {@link inspectProjectRegistry}.
|
|
801
|
+
* @returns The report, with `dryRun: false` and a receipt when anything applied.
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```ts
|
|
805
|
+
* const result = await applyProjectRegistryRepair();
|
|
806
|
+
* if (result.receipt) console.log(result.receipt.rollback);
|
|
807
|
+
* ```
|
|
808
|
+
*/
|
|
809
|
+
export async function applyProjectRegistryRepair(opts = {}) {
|
|
810
|
+
const { report, planned } = await inspect(opts);
|
|
811
|
+
// A row registered at another row's rebind target moves first, so the later
|
|
812
|
+
// rebind does not displace it to the `superseded:` sentinel.
|
|
813
|
+
const targets = new Set(planned.flatMap((p) => (p.rebindTo ? [p.rebindTo.path] : [])));
|
|
814
|
+
const work = planned
|
|
815
|
+
.filter((p) => p.rebindTo || p.markMissing)
|
|
816
|
+
.sort((a, b) => Number(targets.has(b.finding.projectPath)) - Number(targets.has(a.finding.projectPath)));
|
|
817
|
+
const aliasKeys = report.ambiguousAliases.map((a) => a.legacyId);
|
|
818
|
+
if (work.length === 0 && aliasKeys.length === 0)
|
|
819
|
+
return { ...report, dryRun: false };
|
|
820
|
+
const cleoHome = opts.cleoHome ?? getCleoHome();
|
|
821
|
+
const schema = await openRegistry(cleoHome);
|
|
822
|
+
const { db, storePath, projectRegistry, projectIdAliases, nexusAuditLog } = schema;
|
|
823
|
+
const receiptId = randomUUID();
|
|
824
|
+
const now = new Date().toISOString();
|
|
825
|
+
const touchedIds = work.map((p) => p.finding.projectId);
|
|
826
|
+
const touchedPaths = work.flatMap((p) => p.rebindTo ? [p.finding.projectPath, p.rebindTo.path] : [p.finding.projectPath]);
|
|
827
|
+
// Probe first, write after: no filesystem access inside the IMMEDIATE tx.
|
|
828
|
+
const probed = await preProbe(db, schema, repairScope(db, schema, touchedIds, touchedPaths).ids, scanBudget(opts));
|
|
829
|
+
const receipt = db.transaction((tx) => {
|
|
830
|
+
const rowsBefore = tx
|
|
831
|
+
.select({ id: projectRegistry.projectId })
|
|
832
|
+
.from(projectRegistry)
|
|
833
|
+
.all().length;
|
|
834
|
+
const scope = { ...repairScope(tx, schema, touchedIds, touchedPaths), aliases: aliasKeys };
|
|
835
|
+
const before = captureImage(tx, schema, scope.ids, scope.paths, scope.aliases);
|
|
836
|
+
const actions = [];
|
|
837
|
+
for (const p of work) {
|
|
838
|
+
const { projectId, projectPath } = p.finding;
|
|
839
|
+
const current = tx
|
|
840
|
+
.select({ projectPath: projectRegistry.projectPath })
|
|
841
|
+
.from(projectRegistry)
|
|
842
|
+
.where(eq(projectRegistry.projectId, projectId))
|
|
843
|
+
.get();
|
|
844
|
+
const unchanged = current?.projectPath === projectPath;
|
|
845
|
+
if (p.rebindTo) {
|
|
846
|
+
if (unchanged)
|
|
847
|
+
confirmProjectLocation(tx, {
|
|
848
|
+
projectId,
|
|
849
|
+
projectPath: p.rebindTo.path,
|
|
850
|
+
now,
|
|
851
|
+
checkoutNonce: p.rebindTo.nonce,
|
|
852
|
+
probed,
|
|
853
|
+
});
|
|
854
|
+
actions.push({
|
|
855
|
+
action: 'rebind',
|
|
856
|
+
projectId,
|
|
857
|
+
from: projectPath,
|
|
858
|
+
to: p.rebindTo.path,
|
|
859
|
+
outcome: unchanged ? 'applied' : 'skipped',
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
if (unchanged)
|
|
864
|
+
demoteProjectLocation(tx, { projectId, projectPath, now }, 'missing');
|
|
865
|
+
actions.push({
|
|
866
|
+
action: 'mark-missing',
|
|
867
|
+
projectId,
|
|
868
|
+
from: projectPath,
|
|
869
|
+
outcome: unchanged ? 'applied' : 'skipped',
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// T12589: remove each alias row whose key several projects still claim,
|
|
874
|
+
// re-checked inside the transaction (after any rebind above).
|
|
875
|
+
for (const ambiguous of report.ambiguousAliases) {
|
|
876
|
+
const row = tx
|
|
877
|
+
.select({ canonicalId: projectIdAliases.canonicalId })
|
|
878
|
+
.from(projectIdAliases)
|
|
879
|
+
.where(eq(projectIdAliases.legacyId, ambiguous.legacyId))
|
|
880
|
+
.get();
|
|
881
|
+
const still = row?.canonicalId === ambiguous.canonicalId &&
|
|
882
|
+
registryAliasClaimants(tx, ambiguous.legacyId, row.canonicalId).length > 1;
|
|
883
|
+
if (still)
|
|
884
|
+
tx.delete(projectIdAliases)
|
|
885
|
+
.where(eq(projectIdAliases.legacyId, ambiguous.legacyId))
|
|
886
|
+
.run();
|
|
887
|
+
const owner = tx
|
|
888
|
+
.select({ projectPath: projectRegistry.projectPath })
|
|
889
|
+
.from(projectRegistry)
|
|
890
|
+
.where(eq(projectRegistry.projectId, ambiguous.canonicalId))
|
|
891
|
+
.get();
|
|
892
|
+
actions.push({
|
|
893
|
+
action: 'drop-ambiguous-alias',
|
|
894
|
+
projectId: ambiguous.canonicalId,
|
|
895
|
+
from: owner?.projectPath ?? '',
|
|
896
|
+
alias: ambiguous.legacyId,
|
|
897
|
+
outcome: still ? 'applied' : 'skipped',
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const after = captureImage(tx, schema, scope.ids, scope.paths, scope.aliases);
|
|
901
|
+
const result = {
|
|
902
|
+
receiptId,
|
|
903
|
+
storePath,
|
|
904
|
+
appliedAt: now,
|
|
905
|
+
actions,
|
|
906
|
+
registryRows: {
|
|
907
|
+
before: rowsBefore,
|
|
908
|
+
after: tx.select({ id: projectRegistry.projectId }).from(projectRegistry).all().length,
|
|
909
|
+
},
|
|
910
|
+
rollback: `${DOCTOR_COMMAND} --rollback ${receiptId}`,
|
|
911
|
+
};
|
|
912
|
+
const stored = {
|
|
913
|
+
receipt: result,
|
|
914
|
+
scope,
|
|
915
|
+
before,
|
|
916
|
+
after,
|
|
917
|
+
};
|
|
918
|
+
tx.insert(nexusAuditLog)
|
|
919
|
+
.values({
|
|
920
|
+
id: receiptId,
|
|
921
|
+
action: APPLY_ACTION,
|
|
922
|
+
domain: 'doctor',
|
|
923
|
+
operation: APPLY_ACTION,
|
|
924
|
+
success: 1,
|
|
925
|
+
detailsJson: JSON.stringify(stored),
|
|
926
|
+
})
|
|
927
|
+
.run();
|
|
928
|
+
return result;
|
|
929
|
+
}, { behavior: 'immediate' });
|
|
930
|
+
return { ...report, dryRun: false, receipt };
|
|
931
|
+
}
|
|
932
|
+
/** Read and parse a stored repair; `null` when the id is not an applied repair. */
|
|
933
|
+
function readStoredRepair(db, schema, receiptId) {
|
|
934
|
+
const row = db
|
|
935
|
+
.select({ action: schema.nexusAuditLog.action, detailsJson: schema.nexusAuditLog.detailsJson })
|
|
936
|
+
.from(schema.nexusAuditLog)
|
|
937
|
+
.where(eq(schema.nexusAuditLog.id, receiptId))
|
|
938
|
+
.get();
|
|
939
|
+
if (!row || row.action !== APPLY_ACTION || !row.detailsJson)
|
|
940
|
+
return null;
|
|
941
|
+
return JSON.parse(row.detailsJson);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Restore the rows an applied repair changed. Refused when the receipt is
|
|
945
|
+
* unknown, was already rolled back, or when any row in its scope changed
|
|
946
|
+
* since the repair (the current rows must equal the receipt's after image) —
|
|
947
|
+
* a later edit is never overwritten. The rollback writes its own receipt.
|
|
948
|
+
*
|
|
949
|
+
* @param receiptId - `receiptId` from {@link applyProjectRegistryRepair}.
|
|
950
|
+
* @param opts - Registry home.
|
|
951
|
+
* @returns Rows restored per table and the rollback's own receipt id.
|
|
952
|
+
* @throws RegistryRepairError (`E_NOT_FOUND`, `E_ROLLBACK_CONFLICT`).
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* ```ts
|
|
956
|
+
* await rollbackProjectRegistryRepair(receipt.receiptId);
|
|
957
|
+
* ```
|
|
958
|
+
*/
|
|
959
|
+
export async function rollbackProjectRegistryRepair(receiptId, opts = {}) {
|
|
960
|
+
const schema = await openRegistry(opts.cleoHome ?? getCleoHome());
|
|
961
|
+
const { db, projectRegistry, projectLocations, projectPaths, projectIdAliases, nexusAuditLog } = schema;
|
|
962
|
+
const stored = readStoredRepair(db, schema, receiptId);
|
|
963
|
+
if (!stored)
|
|
964
|
+
throw new RegistryRepairError('E_NOT_FOUND', `No applied \`${DOCTOR_COMMAND}\` repair has receipt ${receiptId}. List receipts in nexus_audit_log (action ${APPLY_ACTION}).`);
|
|
965
|
+
const rollbackReceiptId = randomUUID();
|
|
966
|
+
return db.transaction((tx) => {
|
|
967
|
+
const prior = tx
|
|
968
|
+
.select({ detailsJson: nexusAuditLog.detailsJson })
|
|
969
|
+
.from(nexusAuditLog)
|
|
970
|
+
.where(eq(nexusAuditLog.action, ROLLBACK_ACTION))
|
|
971
|
+
.all()
|
|
972
|
+
.some((row) => {
|
|
973
|
+
const details = JSON.parse(row.detailsJson ?? '{}');
|
|
974
|
+
return details.rolledBack === receiptId;
|
|
975
|
+
});
|
|
976
|
+
if (prior)
|
|
977
|
+
throw new RegistryRepairError('E_ROLLBACK_CONFLICT', `Receipt ${receiptId} was already rolled back.`);
|
|
978
|
+
const current = captureImage(tx, schema, stored.scope.ids, stored.scope.paths, stored.scope.aliases);
|
|
979
|
+
if (JSON.stringify(current) !== JSON.stringify(normalizeImage(stored.after)))
|
|
980
|
+
throw new RegistryRepairError('E_ROLLBACK_CONFLICT', `Rows in the scope of receipt ${receiptId} changed after the repair; rolling back would overwrite that change. ` +
|
|
981
|
+
`Inspect with \`${DOCTOR_COMMAND}\` and repair forward instead.`);
|
|
982
|
+
deleteScope(tx, schema, stored.scope.ids, stored.scope.paths, stored.scope.aliases);
|
|
983
|
+
for (const row of stored.before.registry)
|
|
984
|
+
tx.insert(projectRegistry).values(row).run();
|
|
985
|
+
for (const row of stored.before.locations)
|
|
986
|
+
tx.insert(projectLocations).values(row).run();
|
|
987
|
+
for (const row of stored.before.paths)
|
|
988
|
+
tx.insert(projectPaths).values(row).run();
|
|
989
|
+
for (const row of stored.before.aliases ?? [])
|
|
990
|
+
tx.insert(projectIdAliases).values(row).run();
|
|
991
|
+
const restored = {
|
|
992
|
+
registry: stored.before.registry.length,
|
|
993
|
+
locations: stored.before.locations.length,
|
|
994
|
+
paths: stored.before.paths.length,
|
|
995
|
+
...(stored.before.aliases ? { aliases: stored.before.aliases.length } : {}),
|
|
996
|
+
};
|
|
997
|
+
tx.insert(nexusAuditLog)
|
|
998
|
+
.values({
|
|
999
|
+
id: rollbackReceiptId,
|
|
1000
|
+
action: ROLLBACK_ACTION,
|
|
1001
|
+
domain: 'doctor',
|
|
1002
|
+
operation: ROLLBACK_ACTION,
|
|
1003
|
+
success: 1,
|
|
1004
|
+
detailsJson: JSON.stringify({ rolledBack: receiptId, restored, replaced: current }),
|
|
1005
|
+
})
|
|
1006
|
+
.run();
|
|
1007
|
+
return { receiptId, rollbackReceiptId, restored };
|
|
1008
|
+
}, { behavior: 'immediate' });
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* For each id, the paths on this device other than `exclude` that provably
|
|
1012
|
+
* hold it now: recorded locations of the id that still declare it, plus
|
|
1013
|
+
* directories found by scanning `roots`. Used by `nexus projects clean` to
|
|
1014
|
+
* refuse deleting a row whose project merely moved (T12471).
|
|
1015
|
+
*
|
|
1016
|
+
* @param ids - Project ids to look for, with the registered path to exclude.
|
|
1017
|
+
* @param opts - Scan roots and budgets; roots default to the parents of the given paths.
|
|
1018
|
+
* @returns Map from id to the paths that declare it (ids found nowhere are absent).
|
|
1019
|
+
*
|
|
1020
|
+
* @example
|
|
1021
|
+
* ```ts
|
|
1022
|
+
* const found = await locateProjectsElsewhere([{ projectId, projectPath }]);
|
|
1023
|
+
* ```
|
|
1024
|
+
*/
|
|
1025
|
+
export async function locateProjectsElsewhere(ids, opts = {}) {
|
|
1026
|
+
const out = new Map();
|
|
1027
|
+
if (ids.length === 0)
|
|
1028
|
+
return out;
|
|
1029
|
+
const budget = scanBudget(opts);
|
|
1030
|
+
const { db, projectRegistry, projectLocations } = await openRegistry(opts.cleoHome ?? getCleoHome());
|
|
1031
|
+
const wanted = new Map(ids.map((i) => [i.projectId, i.projectPath]));
|
|
1032
|
+
const add = (id, path) => {
|
|
1033
|
+
// A trashed checkout does not keep a row alive.
|
|
1034
|
+
if (wanted.get(id) === path || isTrashPath(path))
|
|
1035
|
+
return;
|
|
1036
|
+
const list = out.get(id) ?? [];
|
|
1037
|
+
if (!list.includes(path))
|
|
1038
|
+
list.push(path);
|
|
1039
|
+
out.set(id, list);
|
|
1040
|
+
};
|
|
1041
|
+
const deviceId = currentDeviceId();
|
|
1042
|
+
const recorded = [];
|
|
1043
|
+
for (const slice of chunks([...wanted.keys()]))
|
|
1044
|
+
for (const l of db
|
|
1045
|
+
.select({
|
|
1046
|
+
projectId: projectLocations.projectId,
|
|
1047
|
+
path: projectLocations.path,
|
|
1048
|
+
deviceId: projectLocations.deviceId,
|
|
1049
|
+
state: projectLocations.state,
|
|
1050
|
+
})
|
|
1051
|
+
.from(projectLocations)
|
|
1052
|
+
.where(inArray(projectLocations.projectId, slice))
|
|
1053
|
+
.all())
|
|
1054
|
+
if ((l.deviceId === deviceId || l.deviceId === LOCAL_DEVICE_SENTINEL) &&
|
|
1055
|
+
l.state !== 'superseded')
|
|
1056
|
+
recorded.push(l);
|
|
1057
|
+
const holds = await runWithConcurrency(recorded, budget.concurrency, (l) => wanted.get(l.projectId) === l.path
|
|
1058
|
+
? Promise.resolve('no')
|
|
1059
|
+
: probeProjectHolding(l.path, l.projectId, budget.timeoutMs));
|
|
1060
|
+
recorded.forEach((l, i) => {
|
|
1061
|
+
if (holds[i] === 'yes' && readDeclaredProjectIdentity(l.path)?.projectId === l.projectId)
|
|
1062
|
+
add(l.projectId, l.path);
|
|
1063
|
+
});
|
|
1064
|
+
const allPaths = db
|
|
1065
|
+
.select({ projectPath: projectRegistry.projectPath })
|
|
1066
|
+
.from(projectRegistry)
|
|
1067
|
+
.all();
|
|
1068
|
+
const roots = scanRootsFor(allPaths.map((r) => r.projectPath), opts.roots ?? []);
|
|
1069
|
+
const scan = await scanRoots(roots, budget);
|
|
1070
|
+
for (const project of scan.projects)
|
|
1071
|
+
if (wanted.has(project.projectId))
|
|
1072
|
+
add(project.projectId, project.path);
|
|
1073
|
+
return out;
|
|
1074
|
+
}
|
|
1075
|
+
//# sourceMappingURL=projects.js.map
|