session-orchestrator 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/agents/explorer.toml +14 -0
- package/.codex-plugin/agents/session-reviewer.toml +23 -0
- package/.codex-plugin/agents/wave-worker.toml +15 -0
- package/.codex-plugin/config.toml +20 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.cursor/rules/000-session-orchestrator.mdc +73 -0
- package/.cursor/rules/010-session-workflow.mdc +170 -0
- package/.cursor/rules/020-quality-gates.mdc +128 -0
- package/.cursor/rules/030-wave-execution.mdc +216 -0
- package/.cursor/rules/040-discovery.mdc +242 -0
- package/.cursor/rules/050-plan.mdc +235 -0
- package/.cursor/rules/060-evolve.mdc +232 -0
- package/.cursor/rules/070-gitlab-ops.mdc +246 -0
- package/.cursor/rules/080-ecosystem-health.mdc +145 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +1544 -0
- package/LICENSE +21 -0
- package/NOTICE +64 -0
- package/README.md +242 -0
- package/SECURITY.md +90 -0
- package/agents/AGENTS.md +136 -0
- package/agents/analyst.md +99 -0
- package/agents/architect-reviewer.md +93 -0
- package/agents/code-implementer.md +106 -0
- package/agents/db-specialist.md +104 -0
- package/agents/dialectic-deriver.md +139 -0
- package/agents/docs-writer.md +113 -0
- package/agents/eval-judge.md +146 -0
- package/agents/memory-proposal-collector.md +297 -0
- package/agents/qa-strategist.md +102 -0
- package/agents/schemas/analyst.schema.json +46 -0
- package/agents/schemas/architect-reviewer.schema.json +50 -0
- package/agents/schemas/code-implementer.schema.json +61 -0
- package/agents/schemas/db-specialist.schema.json +80 -0
- package/agents/schemas/docs-writer.schema.json +56 -0
- package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
- package/agents/schemas/qa-strategist.schema.json +46 -0
- package/agents/schemas/security-reviewer.schema.json +86 -0
- package/agents/schemas/session-reviewer.schema.json +69 -0
- package/agents/schemas/test-writer.schema.json +69 -0
- package/agents/schemas/ui-developer.schema.json +90 -0
- package/agents/schemas/ux-evaluator.schema.json +51 -0
- package/agents/security-reviewer.md +236 -0
- package/agents/session-reviewer.md +201 -0
- package/agents/skill-applied-judge.md +122 -0
- package/agents/test-writer.md +123 -0
- package/agents/ui-developer.md +109 -0
- package/agents/ux-evaluator.md +161 -0
- package/assets/icon.svg +11 -0
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +47 -0
- package/commands/autopilot-multi.md +74 -0
- package/commands/autopilot.md +80 -0
- package/commands/bootstrap.md +56 -0
- package/commands/brainstorm.md +48 -0
- package/commands/close.md +24 -0
- package/commands/debug.md +36 -0
- package/commands/discovery.md +32 -0
- package/commands/dispatcher.md +59 -0
- package/commands/eval.md +28 -0
- package/commands/evolve.md +10 -0
- package/commands/go.md +41 -0
- package/commands/grill.md +45 -0
- package/commands/harness-audit.md +26 -0
- package/commands/memory-cleanup.md +25 -0
- package/commands/persona-panel.md +121 -0
- package/commands/plan.md +15 -0
- package/commands/portfolio.md +97 -0
- package/commands/reconcile.md +23 -0
- package/commands/repo-audit.md +24 -0
- package/commands/session.md +30 -0
- package/commands/spinout.md +15 -0
- package/commands/sunset-review.md +27 -0
- package/commands/templates-ack.md +96 -0
- package/commands/test.md +97 -0
- package/docs/README.md +105 -0
- package/docs/USER-GUIDE.md +1403 -0
- package/docs/ci-setup.md +81 -0
- package/docs/codex-setup.md +142 -0
- package/docs/components.md +74 -0
- package/docs/cursor-setup.md +104 -0
- package/docs/events-schema.md +81 -0
- package/docs/migration-v3.md +148 -0
- package/docs/owner-config-schema.md +154 -0
- package/docs/persona-panel.md +433 -0
- package/docs/pi-setup.md +115 -0
- package/docs/plugin-architecture-v3.md +296 -0
- package/docs/pm-skills-marketplace.md +114 -0
- package/docs/policy-cache-validation-2026-04-28.md +118 -0
- package/docs/rule-authoring.md +316 -0
- package/docs/session-config-reference.md +1439 -0
- package/docs/session-config-template.md +961 -0
- package/docs/vault-docs-architecture.md +297 -0
- package/hooks/_lib/lock-bootstrap.mjs +272 -0
- package/hooks/_lib/lock-reconcile.mjs +93 -0
- package/hooks/_lib/profile-gate.mjs +95 -0
- package/hooks/_lib/transcript-history.mjs +211 -0
- package/hooks/agent-teams-h3-test.sh +362 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/cwd-change-restore.mjs +131 -0
- package/hooks/enforce-commands.mjs +179 -0
- package/hooks/enforce-scope.mjs +273 -0
- package/hooks/hooks-codex.json +60 -0
- package/hooks/hooks-cursor.json +15 -0
- package/hooks/hooks-pi.json +115 -0
- package/hooks/hooks.json +215 -0
- package/hooks/loop-guard.mjs +260 -0
- package/hooks/on-session-end.mjs +217 -0
- package/hooks/on-session-start.mjs +660 -0
- package/hooks/on-stop.mjs +294 -0
- package/hooks/operator-steer.mjs +64 -0
- package/hooks/post-edit-validate.mjs +225 -0
- package/hooks/post-subagent-discovery-validator.mjs +398 -0
- package/hooks/post-tool-batch-wave-signal.mjs +328 -0
- package/hooks/post-tool-failure-corrective-context.mjs +248 -0
- package/hooks/post-tooluse-frontend-slop.mjs +184 -0
- package/hooks/pre-bash-destructive-guard.mjs +515 -0
- package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
- package/hooks/pre-bash-staging-fence.mjs +223 -0
- package/hooks/pre-bash-templates-first.mjs +404 -0
- package/hooks/run-node.sh +72 -0
- package/hooks/skill-invocation-telemetry.mjs +99 -0
- package/hooks/subagent-telemetry.mjs +249 -0
- package/hooks/wave-scope-commit-guard.mjs +191 -0
- package/monitors/monitors.json +14 -0
- package/output-styles/finding-report.md +48 -0
- package/output-styles/session-report.md +53 -0
- package/output-styles/wave-summary.md +38 -0
- package/package.json +94 -0
- package/pi/extensions/session-orchestrator.ts +25 -0
- package/pi/prompts/autopilot-multi.md +12 -0
- package/pi/prompts/autopilot.md +12 -0
- package/pi/prompts/bootstrap.md +12 -0
- package/pi/prompts/brainstorm.md +12 -0
- package/pi/prompts/close.md +11 -0
- package/pi/prompts/debug.md +12 -0
- package/pi/prompts/discovery.md +12 -0
- package/pi/prompts/dispatcher.md +12 -0
- package/pi/prompts/eval.md +12 -0
- package/pi/prompts/evolve.md +12 -0
- package/pi/prompts/go.md +12 -0
- package/pi/prompts/grill.md +12 -0
- package/pi/prompts/harness-audit.md +12 -0
- package/pi/prompts/memory-cleanup.md +12 -0
- package/pi/prompts/persona-panel.md +12 -0
- package/pi/prompts/plan.md +12 -0
- package/pi/prompts/portfolio.md +12 -0
- package/pi/prompts/reconcile.md +12 -0
- package/pi/prompts/repo-audit.md +12 -0
- package/pi/prompts/session.md +12 -0
- package/pi/prompts/spinout.md +12 -0
- package/pi/prompts/sunset-review.md +12 -0
- package/pi/prompts/templates-ack.md +12 -0
- package/pi/prompts/test.md +12 -0
- package/rules/_index.md +51 -0
- package/rules/always-on/commit-discipline.md +26 -0
- package/rules/always-on/npm-quality-gates.md +26 -0
- package/rules/always-on/parallel-sessions.md +43 -0
- package/rules/opt-in-domain/prompt-caching.md +270 -0
- package/rules/opt-in-stack/backend-data.md +188 -0
- package/rules/opt-in-stack/backend.md +390 -0
- package/rules/opt-in-stack/frontend.md +98 -0
- package/rules/opt-in-stack/security-web.md +194 -0
- package/rules/opt-in-stack/swift.md +65 -0
- package/scripts/archive-closed-prds.mjs +416 -0
- package/scripts/autopilot-multi.mjs +802 -0
- package/scripts/autopilot.mjs +383 -0
- package/scripts/backfill-abandoned-sessions.mjs +265 -0
- package/scripts/backfill-learnings-expires.mjs +196 -0
- package/scripts/backfill-learnings.mjs +203 -0
- package/scripts/backfill-sessions.mjs +282 -0
- package/scripts/check-doc-consistency.sh +279 -0
- package/scripts/check-package-manager.mjs +445 -0
- package/scripts/ci/assert-vitest-green.mjs +267 -0
- package/scripts/codex-install.mjs +435 -0
- package/scripts/compute-grounding-injection.sh +186 -0
- package/scripts/cursor-install.mjs +113 -0
- package/scripts/dialectic-deriver.mjs +573 -0
- package/scripts/emit-event.mjs +160 -0
- package/scripts/emit-session.mjs +212 -0
- package/scripts/eval-session.mjs +262 -0
- package/scripts/export-hw-learnings.mjs +437 -0
- package/scripts/gc-stale-worktrees.mjs +666 -0
- package/scripts/generate-pi-prompts.mjs +127 -0
- package/scripts/harness-audit.mjs +287 -0
- package/scripts/lib/agent-frontmatter.mjs +266 -0
- package/scripts/lib/agent-output-schema.mjs +166 -0
- package/scripts/lib/agent-status.mjs +303 -0
- package/scripts/lib/ajv-loader.mjs +34 -0
- package/scripts/lib/auto-dialectic.mjs +382 -0
- package/scripts/lib/auto-dream.mjs +471 -0
- package/scripts/lib/autonomy/suitability.mjs +212 -0
- package/scripts/lib/autopilot/dep-graph.mjs +417 -0
- package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
- package/scripts/lib/autopilot/flags.mjs +104 -0
- package/scripts/lib/autopilot/kill-switches.mjs +174 -0
- package/scripts/lib/autopilot/loop.mjs +320 -0
- package/scripts/lib/autopilot/mr-draft.mjs +520 -0
- package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
- package/scripts/lib/autopilot/recent-runs.mjs +106 -0
- package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
- package/scripts/lib/autopilot/telemetry.mjs +224 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
- package/scripts/lib/autopilot-telemetry.mjs +11 -0
- package/scripts/lib/autopilot.mjs +39 -0
- package/scripts/lib/backlog-scan.mjs +179 -0
- package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
- package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
- package/scripts/lib/build-live-signals.mjs +150 -0
- package/scripts/lib/ci-status-banner.mjs +425 -0
- package/scripts/lib/claude-md-budget-lint.mjs +246 -0
- package/scripts/lib/cli-flags.mjs +158 -0
- package/scripts/lib/codex/plugin-contract.mjs +610 -0
- package/scripts/lib/cold-start-detector.mjs +240 -0
- package/scripts/lib/command-blocker.mjs +458 -0
- package/scripts/lib/common.mjs +333 -0
- package/scripts/lib/config/auto-dream.mjs +77 -0
- package/scripts/lib/config/block-header.mjs +94 -0
- package/scripts/lib/config/broken-window.mjs +114 -0
- package/scripts/lib/config/coercers.mjs +248 -0
- package/scripts/lib/config/cold-start.mjs +92 -0
- package/scripts/lib/config/config-protection.mjs +120 -0
- package/scripts/lib/config/cross-repo.mjs +104 -0
- package/scripts/lib/config/custom-phases.mjs +213 -0
- package/scripts/lib/config/dialectic.mjs +92 -0
- package/scripts/lib/config/discovery-validator.mjs +75 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
- package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
- package/scripts/lib/config/docs-orchestrator.mjs +90 -0
- package/scripts/lib/config/docs-staleness.mjs +96 -0
- package/scripts/lib/config/drift-check.mjs +155 -0
- package/scripts/lib/config/eval.mjs +130 -0
- package/scripts/lib/config/events-rotation.mjs +74 -0
- package/scripts/lib/config/evolve.mjs +308 -0
- package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
- package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
- package/scripts/lib/config/handover-gate.mjs +106 -0
- package/scripts/lib/config/host-paths.mjs +76 -0
- package/scripts/lib/config/io.mjs +54 -0
- package/scripts/lib/config/loop-guard.mjs +117 -0
- package/scripts/lib/config/memory.mjs +150 -0
- package/scripts/lib/config/persona-gate-wave.mjs +258 -0
- package/scripts/lib/config/reconcile.mjs +205 -0
- package/scripts/lib/config/section-extractor.mjs +100 -0
- package/scripts/lib/config/skill-evolution.mjs +112 -0
- package/scripts/lib/config/slopcheck.mjs +99 -0
- package/scripts/lib/config/state-md-lock.mjs +83 -0
- package/scripts/lib/config/templates-first.mjs +94 -0
- package/scripts/lib/config/test.mjs +113 -0
- package/scripts/lib/config/vault-integration.mjs +201 -0
- package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
- package/scripts/lib/config/vault-staleness.mjs +84 -0
- package/scripts/lib/config/vault-sync.mjs +96 -0
- package/scripts/lib/config/verification-auto-fix.mjs +84 -0
- package/scripts/lib/config/wave-reviewers.mjs +133 -0
- package/scripts/lib/config-schema.mjs +345 -0
- package/scripts/lib/config.mjs +474 -0
- package/scripts/lib/convergence-monitor.mjs +389 -0
- package/scripts/lib/coordinator-snapshot.mjs +371 -0
- package/scripts/lib/crypto-digest-utils.mjs +91 -0
- package/scripts/lib/discovery/helpers.mjs +127 -0
- package/scripts/lib/discovery/triage-state.mjs +279 -0
- package/scripts/lib/dispatcher/cli.mjs +257 -0
- package/scripts/lib/dispatcher/enumerate.mjs +243 -0
- package/scripts/lib/dispatcher/rank.mjs +363 -0
- package/scripts/lib/ecosystem-health.mjs +224 -0
- package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
- package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
- package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
- package/scripts/lib/ecosystem-wizard.mjs +48 -0
- package/scripts/lib/env-check.mjs +89 -0
- package/scripts/lib/eval/engine.mjs +605 -0
- package/scripts/lib/eval/judge.mjs +433 -0
- package/scripts/lib/eval/report.mjs +367 -0
- package/scripts/lib/eval/schema.mjs +618 -0
- package/scripts/lib/eval/session-resolve.mjs +137 -0
- package/scripts/lib/eval/sink.mjs +77 -0
- package/scripts/lib/events-rotation.mjs +86 -0
- package/scripts/lib/events-schema.mjs +81 -0
- package/scripts/lib/events.mjs +80 -0
- package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
- package/scripts/lib/exclusivity-matrix.mjs +68 -0
- package/scripts/lib/fetch-baseline.mjs +311 -0
- package/scripts/lib/file-lock.mjs +512 -0
- package/scripts/lib/frontend-detect/detect.mjs +138 -0
- package/scripts/lib/frontend-detect/rules.mjs +295 -0
- package/scripts/lib/frontmatter-guard.mjs +241 -0
- package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
- package/scripts/lib/gates/gate-baseline.mjs +42 -0
- package/scripts/lib/gates/gate-full.mjs +85 -0
- package/scripts/lib/gates/gate-helpers.mjs +231 -0
- package/scripts/lib/gates/gate-incremental.mjs +76 -0
- package/scripts/lib/gates/gate-per-file.mjs +55 -0
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
- package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
- package/scripts/lib/handover-gate.mjs +222 -0
- package/scripts/lib/hardening.mjs +43 -0
- package/scripts/lib/hardware-pattern-detector.mjs +238 -0
- package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
- package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
- package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
- package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
- package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
- package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
- package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
- package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
- package/scripts/lib/harness-audit/categories.mjs +19 -0
- package/scripts/lib/historical-guard.mjs +15 -0
- package/scripts/lib/host-identity.mjs +262 -0
- package/scripts/lib/instruction-budget-guard.mjs +332 -0
- package/scripts/lib/io.mjs +304 -0
- package/scripts/lib/issue-close-strip-labels.mjs +161 -0
- package/scripts/lib/language-mappers/README.md +57 -0
- package/scripts/lib/language-mappers/index.mjs +165 -0
- package/scripts/lib/language-mappers/markdown.mjs +149 -0
- package/scripts/lib/language-mappers/python.mjs +249 -0
- package/scripts/lib/language-mappers/swift.mjs +201 -0
- package/scripts/lib/language-mappers/typescript.mjs +433 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
- package/scripts/lib/learnings/filters.mjs +43 -0
- package/scripts/lib/learnings/io.mjs +255 -0
- package/scripts/lib/learnings/schema.mjs +518 -0
- package/scripts/lib/learnings/surface.mjs +207 -0
- package/scripts/lib/learnings.mjs +42 -0
- package/scripts/lib/lock-reaper.mjs +648 -0
- package/scripts/lib/locks/index.mjs +31 -0
- package/scripts/lib/locks/lock-body.mjs +62 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
- package/scripts/lib/locks/state-md-lock.mjs +351 -0
- package/scripts/lib/loop-readiness-banner.mjs +144 -0
- package/scripts/lib/memory-banner.mjs +478 -0
- package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
- package/scripts/lib/memory-paths.mjs +31 -0
- package/scripts/lib/memory-proposals/collector.mjs +334 -0
- package/scripts/lib/memory-proposals/schema.mjs +289 -0
- package/scripts/lib/memory-proposals/sink.mjs +507 -0
- package/scripts/lib/memory-proposals/store.mjs +441 -0
- package/scripts/lib/mission-status-schema.mjs +114 -0
- package/scripts/lib/mode-selector/alternatives.mjs +64 -0
- package/scripts/lib/mode-selector/constants.mjs +29 -0
- package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
- package/scripts/lib/mode-selector/rationale.mjs +55 -0
- package/scripts/lib/mode-selector/scoring.mjs +221 -0
- package/scripts/lib/mode-selector-accuracy.mjs +121 -0
- package/scripts/lib/mode-selector.mjs +160 -0
- package/scripts/lib/multi-provider-build/providers.mjs +64 -0
- package/scripts/lib/multi-provider-build/templating.mjs +130 -0
- package/scripts/lib/named-baseline-resolver.mjs +233 -0
- package/scripts/lib/named-vault-resolver.mjs +433 -0
- package/scripts/lib/owner-config/coerce.mjs +29 -0
- package/scripts/lib/owner-config/constants.mjs +21 -0
- package/scripts/lib/owner-config/defaults.mjs +50 -0
- package/scripts/lib/owner-config/error.mjs +19 -0
- package/scripts/lib/owner-config/index.mjs +13 -0
- package/scripts/lib/owner-config/merge.mjs +52 -0
- package/scripts/lib/owner-config/validate.mjs +259 -0
- package/scripts/lib/owner-config-banner.mjs +126 -0
- package/scripts/lib/owner-config-loader.mjs +159 -0
- package/scripts/lib/owner-config.example.yaml +72 -0
- package/scripts/lib/owner-config.mjs +28 -0
- package/scripts/lib/owner-interview.mjs +243 -0
- package/scripts/lib/owner-yaml.mjs +571 -0
- package/scripts/lib/package-manager.mjs +160 -0
- package/scripts/lib/path-utils.mjs +217 -0
- package/scripts/lib/peer-cards/merger.mjs +310 -0
- package/scripts/lib/peer-cards/reader.mjs +125 -0
- package/scripts/lib/peer-cards/schema.mjs +230 -0
- package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
- package/scripts/lib/peer-cards/writer.mjs +138 -0
- package/scripts/lib/peer-discovery.mjs +200 -0
- package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
- package/scripts/lib/persona-panel/consolidator.mjs +370 -0
- package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
- package/scripts/lib/persona-panel/threshold.mjs +130 -0
- package/scripts/lib/pi-hook-bridge.mjs +328 -0
- package/scripts/lib/platform.mjs +266 -0
- package/scripts/lib/playwright-driver/runner.mjs +297 -0
- package/scripts/lib/plugin-root.mjs +210 -0
- package/scripts/lib/pre-dispatch-check.mjs +126 -0
- package/scripts/lib/product-repo-detect.mjs +121 -0
- package/scripts/lib/profiles/registry.mjs +176 -0
- package/scripts/lib/profiles/schema.mjs +209 -0
- package/scripts/lib/qg-command-drift-banner.mjs +88 -0
- package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
- package/scripts/lib/quality-gate.mjs +536 -0
- package/scripts/lib/quality-gates-cache.mjs +228 -0
- package/scripts/lib/quality-gates-policy.mjs +95 -0
- package/scripts/lib/recommendations-v0.mjs +156 -0
- package/scripts/lib/reconcile/eligibility.mjs +203 -0
- package/scripts/lib/reconcile/emitter.mjs +244 -0
- package/scripts/lib/reconcile/engine.mjs +412 -0
- package/scripts/lib/reconcile/idempotency.mjs +239 -0
- package/scripts/lib/reconcile/renderer.mjs +211 -0
- package/scripts/lib/reconcile/writer.mjs +293 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
- package/scripts/lib/resource-probe/evaluate.mjs +190 -0
- package/scripts/lib/resource-probe/parsers.mjs +181 -0
- package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
- package/scripts/lib/resource-probe.mjs +95 -0
- package/scripts/lib/rule-loader.mjs +552 -0
- package/scripts/lib/rules-sync.mjs +439 -0
- package/scripts/lib/scope-gate.mjs +496 -0
- package/scripts/lib/session-close-backfill.mjs +539 -0
- package/scripts/lib/session-discovery.mjs +256 -0
- package/scripts/lib/session-end/phase-skip.mjs +357 -0
- package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
- package/scripts/lib/session-id.mjs +362 -0
- package/scripts/lib/session-lock.mjs +703 -0
- package/scripts/lib/session-registry.mjs +355 -0
- package/scripts/lib/session-schema/aliases.mjs +71 -0
- package/scripts/lib/session-schema/constants.mjs +115 -0
- package/scripts/lib/session-schema/normalizer.mjs +66 -0
- package/scripts/lib/session-schema/timestamps.mjs +64 -0
- package/scripts/lib/session-schema/validator.mjs +453 -0
- package/scripts/lib/session-schema.mjs +71 -0
- package/scripts/lib/session-token-rollup.mjs +137 -0
- package/scripts/lib/sessions-staleness-banner.mjs +247 -0
- package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
- package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
- package/scripts/lib/skill-evolution/engine.mjs +719 -0
- package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
- package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
- package/scripts/lib/skill-health/join.mjs +181 -0
- package/scripts/lib/skill-health/score.mjs +123 -0
- package/scripts/lib/skill-invocations-schema.mjs +214 -0
- package/scripts/lib/skill-judge.mjs +348 -0
- package/scripts/lib/skill-judgments-schema.mjs +264 -0
- package/scripts/lib/slopcheck.mjs +501 -0
- package/scripts/lib/soul-resolve.mjs +118 -0
- package/scripts/lib/spiral-carryover.mjs +495 -0
- package/scripts/lib/state-md/body-sections.mjs +851 -0
- package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
- package/scripts/lib/state-md/mission-status.mjs +247 -0
- package/scripts/lib/state-md/recommendations.mjs +57 -0
- package/scripts/lib/state-md/yaml-parser.mjs +234 -0
- package/scripts/lib/state-md-peer-guard.mjs +232 -0
- package/scripts/lib/state-md.mjs +53 -0
- package/scripts/lib/subagents-schema.mjs +309 -0
- package/scripts/lib/sunset/walker.mjs +1192 -0
- package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
- package/scripts/lib/test-runner/fingerprint.mjs +33 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
- package/scripts/lib/tmux-layout/layouts.mjs +224 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
- package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
- package/scripts/lib/validate/check-agents.mjs +457 -0
- package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
- package/scripts/lib/validate/check-commands.mjs +148 -0
- package/scripts/lib/validate/check-component-paths.mjs +112 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
- package/scripts/lib/validate/check-json-files.mjs +116 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
- package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
- package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
- package/scripts/lib/validate/check-pi-package.mjs +110 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
- package/scripts/lib/validate/check-plugin-json.mjs +96 -0
- package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
- package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
- package/scripts/lib/validate/check-rules.mjs +143 -0
- package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
- package/scripts/lib/validate/confidential-names.mjs +169 -0
- package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
- package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
- package/scripts/lib/validate/tier-inference.mjs +100 -0
- package/scripts/lib/validate-vendored-rules.mjs +519 -0
- package/scripts/lib/vault-archive.mjs +404 -0
- package/scripts/lib/vault-backfill/glab.mjs +164 -0
- package/scripts/lib/vault-backfill/manifest.mjs +75 -0
- package/scripts/lib/vault-backfill/template.mjs +130 -0
- package/scripts/lib/vault-consolidate-fs.mjs +331 -0
- package/scripts/lib/vault-migration-rules.mjs +155 -0
- package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
- package/scripts/lib/vault-mirror/namespace.mjs +152 -0
- package/scripts/lib/vault-mirror/process.mjs +567 -0
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
- package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
- package/scripts/lib/vault-mirror/render.mjs +8 -0
- package/scripts/lib/vault-mirror/utils.mjs +217 -0
- package/scripts/lib/vault-relocation-rules.mjs +555 -0
- package/scripts/lib/vault-repo-backfill.mjs +235 -0
- package/scripts/lib/vault-staleness-banner.mjs +142 -0
- package/scripts/lib/vault-status/board-writer.mjs +769 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
- package/scripts/lib/vault-sync-baseline.mjs +152 -0
- package/scripts/lib/wave-context.mjs +29 -0
- package/scripts/lib/wave-executor/pool.mjs +248 -0
- package/scripts/lib/wave-resource-gate.mjs +204 -0
- package/scripts/lib/wave-sizing.mjs +75 -0
- package/scripts/lib/webhook-url.mjs +105 -0
- package/scripts/lib/workspace.mjs +198 -0
- package/scripts/lib/worktree/constants.mjs +35 -0
- package/scripts/lib/worktree/index.mjs +17 -0
- package/scripts/lib/worktree/lifecycle.mjs +287 -0
- package/scripts/lib/worktree/listing.mjs +118 -0
- package/scripts/lib/worktree/meta.mjs +64 -0
- package/scripts/lib/worktree-freshness.mjs +313 -0
- package/scripts/lib/worktree.mjs +15 -0
- package/scripts/lifecycle-sim-v6.mjs +347 -0
- package/scripts/lock-reaper.mjs +185 -0
- package/scripts/mcp-server.sh +241 -0
- package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
- package/scripts/memory-propose.mjs +464 -0
- package/scripts/migrate-cold-start-seed.mjs +404 -0
- package/scripts/migrate-learnings-jsonl.mjs +189 -0
- package/scripts/migrate-legacy-learnings.sh +61 -0
- package/scripts/migrate-sessions-jsonl.mjs +448 -0
- package/scripts/migrate-subagents-jsonl.mjs +196 -0
- package/scripts/migrate-vault-paths.mjs +796 -0
- package/scripts/parse-config.mjs +149 -0
- package/scripts/pi-install.mjs +117 -0
- package/scripts/print-applicable-rules.mjs +247 -0
- package/scripts/promote-vault-strict.mjs +496 -0
- package/scripts/relocate-vault-corpus.mjs +1178 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
- package/scripts/run-quality-gate.mjs +216 -0
- package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
- package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
- package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
- package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
- package/scripts/sweep-expired-learnings.mjs +135 -0
- package/scripts/sync-vault-schema.mjs +376 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
- package/scripts/tmux-layout.mjs +245 -0
- package/scripts/token-audit.sh +191 -0
- package/scripts/typecheck.mjs +42 -0
- package/scripts/upload-social-preview.mjs +316 -0
- package/scripts/validate-config.mjs +46 -0
- package/scripts/validate-plugin-manifests.mjs +163 -0
- package/scripts/validate-plugin.mjs +264 -0
- package/scripts/validate-wave-scope.mjs +289 -0
- package/scripts/vault-backfill.mjs +404 -0
- package/scripts/vault-consolidate.mjs +596 -0
- package/scripts/vault-integration-watcher.mjs +394 -0
- package/scripts/vault-mirror.mjs +430 -0
- package/skills/_shared/bootstrap-gate.md +111 -0
- package/skills/_shared/config-reading.md +226 -0
- package/skills/_shared/instruction-file-resolution.md +79 -0
- package/skills/_shared/model-selection.md +64 -0
- package/skills/_shared/monitor-patterns.md +300 -0
- package/skills/_shared/parallel-aware-auq.md +121 -0
- package/skills/_shared/parallel-aware-preamble.md +185 -0
- package/skills/_shared/platform-tools.md +96 -0
- package/skills/_shared/state-ownership.md +221 -0
- package/skills/architecture/DEEPENING.md +37 -0
- package/skills/architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/architecture/LANGUAGE.md +53 -0
- package/skills/architecture/SKILL.md +92 -0
- package/skills/autopilot/SKILL.md +419 -0
- package/skills/bootstrap/SKILL.md +592 -0
- package/skills/bootstrap/STATE.md.template +24 -0
- package/skills/bootstrap/_shared-template.md +243 -0
- package/skills/bootstrap/deep-template.md +659 -0
- package/skills/bootstrap/fast-template.md +251 -0
- package/skills/bootstrap/intensity-heuristic.md +80 -0
- package/skills/bootstrap/public-fallback.md +342 -0
- package/skills/bootstrap/standard-template.md +736 -0
- package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
- package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
- package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
- package/skills/brainstorm/SKILL.md +268 -0
- package/skills/brainstorm/soul.md +49 -0
- package/skills/claude-md-drift-check/SKILL.md +186 -0
- package/skills/claude-md-drift-check/checker.mjs +1380 -0
- package/skills/claude-md-drift-check/checker.sh +37 -0
- package/skills/claude-md-drift-check/package.json +16 -0
- package/skills/convergence-monitoring/README.md +39 -0
- package/skills/convergence-monitoring/SIGNALS.md +246 -0
- package/skills/convergence-monitoring/SKILL.md +285 -0
- package/skills/daily/SKILL.md +222 -0
- package/skills/daily/generate.sh +92 -0
- package/skills/daily/templates/daily.md.tpl +36 -0
- package/skills/debug/SKILL.md +188 -0
- package/skills/debug/soul.md +35 -0
- package/skills/discovery/SKILL.md +567 -0
- package/skills/discovery/issue-templates.md +237 -0
- package/skills/discovery/probes/docs-staleness.mjs +195 -0
- package/skills/discovery/probes/frontend-slop.mjs +186 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
- package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
- package/skills/discovery/probes/vault-staleness.mjs +272 -0
- package/skills/discovery/probes-arch.md +252 -0
- package/skills/discovery/probes-audit.md +95 -0
- package/skills/discovery/probes-code.md +329 -0
- package/skills/discovery/probes-docs.md +76 -0
- package/skills/discovery/probes-feature.md +150 -0
- package/skills/discovery/probes-infra.md +138 -0
- package/skills/discovery/probes-intro.md +25 -0
- package/skills/discovery/probes-session.md +495 -0
- package/skills/discovery/probes-supply-chain.md +94 -0
- package/skills/discovery/probes-ui.md +147 -0
- package/skills/discovery/probes-vault.md +64 -0
- package/skills/discovery/slop-patterns.md +115 -0
- package/skills/dispatcher/SKILL.md +173 -0
- package/skills/docs-orchestrator/SKILL.md +362 -0
- package/skills/docs-orchestrator/audience-mapping.md +140 -0
- package/skills/domain-model/ADR-FORMAT.md +47 -0
- package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
- package/skills/domain-model/SKILL.md +85 -0
- package/skills/ecosystem-health/SKILL.md +119 -0
- package/skills/ecosystem-health/wizard.md +193 -0
- package/skills/eval/SKILL.md +293 -0
- package/skills/eval/rubric-v1.md +218 -0
- package/skills/evolve/SKILL.md +546 -0
- package/skills/frontmatter-guard/SKILL.md +126 -0
- package/skills/gitlab-ops/SKILL.md +368 -0
- package/skills/gitlab-portfolio/SKILL.md +196 -0
- package/skills/grill/SKILL.md +185 -0
- package/skills/grill/soul.md +55 -0
- package/skills/hook-development/SKILL.md +413 -0
- package/skills/mcp-builder/SKILL.md +260 -0
- package/skills/memory-cleanup/SKILL.md +310 -0
- package/skills/mode-selector/SKILL.md +226 -0
- package/skills/peekaboo-driver/SKILL.md +237 -0
- package/skills/peekaboo-driver/soul.md +32 -0
- package/skills/persona-panel/SKILL.md +365 -0
- package/skills/persona-panel/persona-format.md +205 -0
- package/skills/persona-panel/presets/designer-lens.md +87 -0
- package/skills/persona-panel/presets/engineer-lens.md +88 -0
- package/skills/persona-panel/presets/pm-lens.md +86 -0
- package/skills/plan/SKILL.md +496 -0
- package/skills/plan/mode-feature.md +141 -0
- package/skills/plan/mode-new.md +297 -0
- package/skills/plan/mode-retro.md +271 -0
- package/skills/plan/prd-feature-template.md +132 -0
- package/skills/plan/prd-full-template.md +151 -0
- package/skills/plan/prd-reviewer-prompt.md +103 -0
- package/skills/plan/retro-template.md +75 -0
- package/skills/plan/soul.md +62 -0
- package/skills/playwright-driver/SKILL.md +226 -0
- package/skills/playwright-driver/soul.md +30 -0
- package/skills/quality-gates/SKILL.md +212 -0
- package/skills/reconcile/SKILL.md +324 -0
- package/skills/repo-audit/SKILL.md +272 -0
- package/skills/session-end/SKILL.md +1044 -0
- package/skills/session-end/discovery-scan.md +37 -0
- package/skills/session-end/drift-operations.md +97 -0
- package/skills/session-end/learning-patterns.md +78 -0
- package/skills/session-end/metrics-collection.md +175 -0
- package/skills/session-end/phase-3-2-docs-verification.md +148 -0
- package/skills/session-end/phase-3-6-tail.md +344 -0
- package/skills/session-end/phase-3-7a-recommendations.md +86 -0
- package/skills/session-end/plan-verification.md +288 -0
- package/skills/session-end/session-metrics-write.md +223 -0
- package/skills/session-end/vault-operations.md +50 -0
- package/skills/session-end/verification-checklist.md +20 -0
- package/skills/session-plan/SKILL.md +554 -0
- package/skills/session-plan/wave-template.md +37 -0
- package/skills/session-start/SKILL.md +1043 -0
- package/skills/session-start/phase-2-5-docs-planning.md +119 -0
- package/skills/session-start/phase-4-5-resource-health.md +49 -0
- package/skills/session-start/phase-7-1-premise-check.md +47 -0
- package/skills/session-start/phase-7-5-mode-selector.md +237 -0
- package/skills/session-start/phase-8-5-express-path.md +61 -0
- package/skills/session-start/presentation-format.md +81 -0
- package/skills/session-start/soul.md +57 -0
- package/skills/skill-creator/SKILL.md +168 -0
- package/skills/spinout/SKILL.md +76 -0
- package/skills/sunset-review/SKILL.md +96 -0
- package/skills/test-runner/SKILL.md +362 -0
- package/skills/test-runner/rubric-v1.md +388 -0
- package/skills/test-runner/soul.md +46 -0
- package/skills/tmux-layout/SKILL.md +104 -0
- package/skills/ubiquitous-language/SKILL.md +97 -0
- package/skills/using-orchestrator/SKILL.md +144 -0
- package/skills/vault-mirror/SKILL.md +234 -0
- package/skills/vault-sync/SKILL.md +319 -0
- package/skills/vault-sync/package-lock.json +40 -0
- package/skills/vault-sync/package.json +11 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
- package/skills/vault-sync/validator.mjs +658 -0
- package/skills/vault-sync/validator.sh +55 -0
- package/skills/wave-executor/SKILL.md +496 -0
- package/skills/wave-executor/circuit-breaker.md +169 -0
- package/skills/wave-executor/wave-loop.md +1043 -0
- package/skills/write-executable-plan/SKILL.md +237 -0
- package/skills/write-executable-plan/plan-template.md +154 -0
- package/templates/_minimal/CLAUDE.md.tmpl +41 -0
- package/templates/_minimal/README.md.tmpl +15 -0
- package/templates/_minimal/gitignore.tmpl +47 -0
- package/templates/_shared/harte-regeln.md +16 -0
- package/templates/_shared/loop.md +90 -0
- package/templates/_shared/rules/parallel-sessions.md +77 -0
- package/templates/nextjs-minimal/README.md +30 -0
- package/templates/nextjs-minimal/app/layout.tsx +18 -0
- package/templates/nextjs-minimal/app/page.tsx +7 -0
- package/templates/nextjs-minimal/eslint.config.mjs +16 -0
- package/templates/nextjs-minimal/next.config.mjs +4 -0
- package/templates/nextjs-minimal/package.json +27 -0
- package/templates/nextjs-minimal/tsconfig.json +23 -0
- package/templates/node-minimal/README.md +33 -0
- package/templates/node-minimal/eslint.config.mjs +10 -0
- package/templates/node-minimal/package.json +21 -0
- package/templates/node-minimal/src/index.ts +1 -0
- package/templates/node-minimal/tests/sanity.test.ts +5 -0
- package/templates/node-minimal/tsconfig.json +17 -0
- package/templates/personas/README.md +150 -0
- package/templates/personas/accounting-compliance.v1.md +120 -0
- package/templates/personas/accounting-tax-advisor.v1.md +116 -0
- package/templates/personas/buyer-p1-cto.v1.md +125 -0
- package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
- package/templates/personas/buyer-p3-build.v1.md +130 -0
- package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
- package/templates/personas/buyer-p5-solo.v1.md +132 -0
- package/templates/personas/buyer-p6-ld.v1.md +130 -0
- package/templates/personas/klima-ai-expert.v1.md +114 -0
- package/templates/personas/klima-physicist.v1.md +117 -0
- package/templates/python-uv/README.md +28 -0
- package/templates/python-uv/pyproject.toml +32 -0
- package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
- package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
- package/templates/python-uv/tests/test_sanity.py +2 -0
- package/templates/static-html/README.md +19 -0
- package/templates/static-html/index.html +15 -0
- package/templates/static-html/script.js +1 -0
- package/templates/static-html/styles.css +26 -0
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* relocate-vault-corpus.mjs — IO/CLI layer for flat-corpus vault relocation.
|
|
4
|
+
*
|
|
5
|
+
* Moves flat markdown files under 40-learnings/ and 50-sessions/ in a vault
|
|
6
|
+
* into per-repo namespace subdirectories using the classification logic from
|
|
7
|
+
* scripts/lib/vault-relocation-rules.mjs (C1).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/relocate-vault-corpus.mjs --vault-dir <path> [--dry-run|--apply]
|
|
11
|
+
* [--derivable-only] [--with-backfill] [--repos-root <dir>]
|
|
12
|
+
* [--rollback <manifest>] [--learnings-only|--sessions-only] [--json] [--help]
|
|
13
|
+
*
|
|
14
|
+
* Flags:
|
|
15
|
+
* --vault-dir DIR Path to the vault git repo root (REQUIRED — no default for safety)
|
|
16
|
+
* --dry-run Preview plan, write nothing (DEFAULT; mutex with --apply)
|
|
17
|
+
* --apply Move files via git mv and write reverse-manifest (mutex with --dry-run)
|
|
18
|
+
* --derivable-only Only move files where confident===true; skip _unsorted/redacted-repo/unknown-repo
|
|
19
|
+
* --with-backfill Cross-repo session→repo backfill (Issue #700): read each repo's
|
|
20
|
+
* sessions.jsonl under --repos-root to attribute historical sessions
|
|
21
|
+
* (and, transitively, their learnings) that carry no in-file repo signal.
|
|
22
|
+
* --repos-root DIR Parent dir holding sibling repos to scan for sessions.jsonl
|
|
23
|
+
* (default: parent of --vault-dir). Only used with --with-backfill.
|
|
24
|
+
* --rollback MANIFEST Reverse a previous --apply run given its manifest JSON path
|
|
25
|
+
* --learnings-only Scope to 40-learnings/ only
|
|
26
|
+
* --sessions-only Scope to 50-sessions/ only
|
|
27
|
+
* --json Emit JSONL records on stdout (one per file)
|
|
28
|
+
* --help, -h Show this help
|
|
29
|
+
*
|
|
30
|
+
* Exit codes:
|
|
31
|
+
* 0 Success (including no-op / dry-run)
|
|
32
|
+
* 1 Input/arg error
|
|
33
|
+
* 2 IO error
|
|
34
|
+
*
|
|
35
|
+
* Data → stdout. Diagnostics/summary → stderr.
|
|
36
|
+
*
|
|
37
|
+
* Structural idempotency: flat files at maxdepth 1 are enumerated; files already
|
|
38
|
+
* at depth ≥ 2 (already namespaced) are never enumerated nor moved.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { promises as fs, existsSync } from 'node:fs';
|
|
42
|
+
import { spawnSync } from 'node:child_process';
|
|
43
|
+
import { pathToFileURL } from 'node:url';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import {
|
|
46
|
+
loadVaultRelocationRules,
|
|
47
|
+
VAULT_RELOCATION_RULES_PATH,
|
|
48
|
+
parseRelocationFrontmatter,
|
|
49
|
+
namespaceForSession,
|
|
50
|
+
classifyOwner,
|
|
51
|
+
isConfident,
|
|
52
|
+
computeDest,
|
|
53
|
+
isAlreadyNamespaced,
|
|
54
|
+
_setResolverForTest,
|
|
55
|
+
} from './lib/vault-relocation-rules.mjs';
|
|
56
|
+
import { buildBackfillIndex, parseSessionId } from './lib/vault-repo-backfill.mjs';
|
|
57
|
+
import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Constants
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const SCRIPT_NAME = 'relocate-vault-corpus';
|
|
64
|
+
const LEARNINGS_SUBDIR = '40-learnings';
|
|
65
|
+
const SESSIONS_SUBDIR = '50-sessions';
|
|
66
|
+
const MANIFEST_DIR = '.orchestrator';
|
|
67
|
+
const SCHEMA_VERSION = 1;
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Arg parsing
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function parseArgs(argv) {
|
|
74
|
+
let parsedFlags;
|
|
75
|
+
try {
|
|
76
|
+
parsedFlags = parseColumnFlags({
|
|
77
|
+
argv: argv.slice(2),
|
|
78
|
+
knownBool: {
|
|
79
|
+
help: { short: 'h', default: false },
|
|
80
|
+
apply: false,
|
|
81
|
+
'dry-run': false,
|
|
82
|
+
json: false,
|
|
83
|
+
'derivable-only': false,
|
|
84
|
+
'with-backfill': false,
|
|
85
|
+
'learnings-only': false,
|
|
86
|
+
'sessions-only': false,
|
|
87
|
+
},
|
|
88
|
+
knownString: {
|
|
89
|
+
'vault-dir': null,
|
|
90
|
+
'repos-root': null,
|
|
91
|
+
rollback: null,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err instanceof CliFlagError) {
|
|
96
|
+
process.stderr.write(`${SCRIPT_NAME}: ${err.message}\n`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const values = parsedFlags.values;
|
|
103
|
+
|
|
104
|
+
// Mutex check: --dry-run and --apply on the same invocation
|
|
105
|
+
const applyFlag = values.apply === true;
|
|
106
|
+
const dryRunFlag = values['dry-run'] === true;
|
|
107
|
+
if (applyFlag && dryRunFlag) {
|
|
108
|
+
process.stderr.write(`${SCRIPT_NAME}: --dry-run and --apply are mutually exclusive\n`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --learnings-only and --sessions-only are mutually exclusive
|
|
113
|
+
if (values['learnings-only'] && values['sessions-only']) {
|
|
114
|
+
process.stderr.write(`${SCRIPT_NAME}: --learnings-only and --sessions-only are mutually exclusive\n`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
help: values.help === true,
|
|
120
|
+
apply: applyFlag,
|
|
121
|
+
json: values.json === true,
|
|
122
|
+
derivableOnly: values['derivable-only'] === true,
|
|
123
|
+
withBackfill: values['with-backfill'] === true,
|
|
124
|
+
reposRoot: values['repos-root'] ?? null,
|
|
125
|
+
rollback: values.rollback ?? null,
|
|
126
|
+
vaultDir: values['vault-dir'] ?? null,
|
|
127
|
+
learningsOnly: values['learnings-only'] === true,
|
|
128
|
+
sessionsOnly: values['sessions-only'] === true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printHelp() {
|
|
133
|
+
process.stdout.write(
|
|
134
|
+
`Usage: ${SCRIPT_NAME}.mjs --vault-dir <path> [--dry-run|--apply]
|
|
135
|
+
[--derivable-only] [--with-backfill] [--repos-root <dir>]
|
|
136
|
+
[--rollback <manifest>] [--learnings-only|--sessions-only] [--json] [--help]
|
|
137
|
+
|
|
138
|
+
Moves flat markdown files from 40-learnings/ and 50-sessions/ roots in a vault
|
|
139
|
+
git repo into per-repo namespace subdirectories.
|
|
140
|
+
|
|
141
|
+
Flat files at depth 1 are enumerated and classified; files already in a
|
|
142
|
+
subdirectory (depth >= 2, already namespaced) are never touched.
|
|
143
|
+
|
|
144
|
+
REQUIRED:
|
|
145
|
+
--vault-dir DIR Path to the vault git repo root
|
|
146
|
+
|
|
147
|
+
MODES (default: --dry-run):
|
|
148
|
+
--dry-run Preview plan, write nothing (DEFAULT)
|
|
149
|
+
--apply Move files via git mv, write reverse-manifest (STAGE ONLY — no commit)
|
|
150
|
+
--rollback MANIFEST Reverse a previous --apply run given its manifest JSON path
|
|
151
|
+
|
|
152
|
+
FILTERS:
|
|
153
|
+
--derivable-only Only move files where confident===true; skip _unsorted/redacted-repo/unknown-repo
|
|
154
|
+
--learnings-only Scope to 40-learnings/ only
|
|
155
|
+
--sessions-only Scope to 50-sessions/ only
|
|
156
|
+
|
|
157
|
+
BACKFILL (Issue #700 — cross-repo session→repo attribution):
|
|
158
|
+
--with-backfill Read each sibling repo's sessions.jsonl to attribute
|
|
159
|
+
historical sessions (and transitively their learnings)
|
|
160
|
+
that carry NO in-file repo signal. Default OFF — when
|
|
161
|
+
absent, no sessions.jsonl is read and output is
|
|
162
|
+
byte-identical to the non-backfill run.
|
|
163
|
+
--repos-root DIR Parent dir holding sibling repos to scan
|
|
164
|
+
(default: parent of --vault-dir). Only used with --with-backfill.
|
|
165
|
+
|
|
166
|
+
OUTPUT:
|
|
167
|
+
--json Emit JSONL records on stdout (one per file)
|
|
168
|
+
--help, -h Show this help
|
|
169
|
+
|
|
170
|
+
Exit codes:
|
|
171
|
+
0 Success (including no-op / dry-run)
|
|
172
|
+
1 Input/arg error
|
|
173
|
+
2 IO error
|
|
174
|
+
|
|
175
|
+
Data goes to stdout; diagnostics and summary go to stderr.
|
|
176
|
+
|
|
177
|
+
WARNING: This tool operates on a git repo (--vault-dir). Run with --dry-run
|
|
178
|
+
first to verify the plan. The --apply mode stages moves (git mv) but does NOT
|
|
179
|
+
commit — the operator must review and commit separately.
|
|
180
|
+
|
|
181
|
+
The reverse-manifest is written to <vault-dir>/.orchestrator/relocation-manifest-<ISO>.json
|
|
182
|
+
on a successful --apply run and can be used with --rollback to undo.
|
|
183
|
+
|
|
184
|
+
Config: ${VAULT_RELOCATION_RULES_PATH}
|
|
185
|
+
`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Flat-file discovery (maxdepth 1 only — structural idempotency)
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Enumerate flat markdown files directly under <dir> (maxdepth 1, files only).
|
|
195
|
+
* Files at depth >= 2 (already in a subdirectory) are never returned.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} dir - absolute path to the corpus subdir (e.g. vault/40-learnings)
|
|
198
|
+
* @returns {string[]} absolute file paths of flat .md files
|
|
199
|
+
*/
|
|
200
|
+
function enumerateFlatFiles(dir) {
|
|
201
|
+
if (!existsSync(dir)) return [];
|
|
202
|
+
|
|
203
|
+
const result = spawnSync(
|
|
204
|
+
'find',
|
|
205
|
+
[dir, '-maxdepth', '1', '-type', 'f', '-name', '*.md'],
|
|
206
|
+
{ encoding: 'utf8' },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (result.error) {
|
|
210
|
+
process.stderr.write(
|
|
211
|
+
`${SCRIPT_NAME}: find not found on PATH (${result.error.code ?? result.error.message})\n`,
|
|
212
|
+
);
|
|
213
|
+
process.exit(2);
|
|
214
|
+
}
|
|
215
|
+
if (result.status !== 0) {
|
|
216
|
+
process.stderr.write(`${SCRIPT_NAME}: find failed: ${result.stderr}\n`);
|
|
217
|
+
process.exit(2);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result.stdout
|
|
221
|
+
.split('\n')
|
|
222
|
+
.map((l) => l.trim())
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Cross-repo sessions.jsonl indices (CLI-side IO — only built under --with-backfill)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Build the authoritative cross-repo backfill indices by scanning each sibling
|
|
232
|
+
* repo's `.orchestrator/metrics/sessions.jsonl` under reposRoot.
|
|
233
|
+
*
|
|
234
|
+
* sidIndex: Map<session_id, Set<repoSlug>>
|
|
235
|
+
* bdIndex: Map<`${branch}|${date}`, Set<repoSlug>>
|
|
236
|
+
*
|
|
237
|
+
* Immediate subdirectories of reposRoot are enumerated; `Archiv` and any
|
|
238
|
+
* dot-prefixed name are excluded. Malformed JSONL lines are skipped (never throw
|
|
239
|
+
* the whole run). The repo identity stored in the indices is the repo's CANONICAL
|
|
240
|
+
* slug derived from its git origin remote (`repoCanonicalSlug`), NOT the directory
|
|
241
|
+
* basename — so a backfilled session lands in the SAME `<repo>/` namespace as that
|
|
242
|
+
* repo's own native vault-mirror writes (which use `deriveRepo()` off the same
|
|
243
|
+
* remote). Using the basename would split e.g. `AcmeWidgetV2/` → `acmewidgetv2`
|
|
244
|
+
* while source-repo:/repo:-carrying notes use the canonical `acme-widget-v2`. Leak-guarding
|
|
245
|
+
* still happens later in the pure backfill module.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} reposRoot - absolute path to the parent dir of sibling repos
|
|
248
|
+
* @returns {{ sidIndex: Map<string, Set<string>>, bdIndex: Map<string, Set<string>>, reposScanned: number }}
|
|
249
|
+
*/
|
|
250
|
+
function repoCanonicalSlug(reposRoot, name) {
|
|
251
|
+
// Derive the repo's canonical namespace identity from its git origin remote so
|
|
252
|
+
// it matches the slug `deriveRepo()` would produce for that repo's own writes.
|
|
253
|
+
// Falls back to the directory basename when there is no readable remote.
|
|
254
|
+
try {
|
|
255
|
+
const res = spawnSync('git', ['-C', path.join(reposRoot, name), 'config', '--get', 'remote.origin.url'], {
|
|
256
|
+
encoding: 'utf8',
|
|
257
|
+
});
|
|
258
|
+
const url = (res.stdout || '').trim();
|
|
259
|
+
if (res.status === 0 && url) {
|
|
260
|
+
const seg = url.replace(/\.git$/, '').split(/[/:]/).filter(Boolean).pop();
|
|
261
|
+
if (seg) return seg;
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// fall through to basename
|
|
265
|
+
}
|
|
266
|
+
return name;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function buildCrossRepoIndices(reposRoot) {
|
|
270
|
+
const sidIndex = new Map();
|
|
271
|
+
const bdIndex = new Map();
|
|
272
|
+
let reposScanned = 0;
|
|
273
|
+
|
|
274
|
+
if (!existsSync(reposRoot)) {
|
|
275
|
+
process.stderr.write(`${SCRIPT_NAME}: WARN: --repos-root not found: ${reposRoot} — backfill indices empty\n`);
|
|
276
|
+
return { sidIndex, bdIndex, reposScanned };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let entries;
|
|
280
|
+
try {
|
|
281
|
+
entries = await fs.readdir(reposRoot, { withFileTypes: true });
|
|
282
|
+
} catch (err) {
|
|
283
|
+
process.stderr.write(`${SCRIPT_NAME}: WARN: could not read --repos-root ${reposRoot}: ${err.message} — backfill indices empty\n`);
|
|
284
|
+
return { sidIndex, bdIndex, reposScanned };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const dirent of entries) {
|
|
288
|
+
if (!dirent.isDirectory()) continue;
|
|
289
|
+
const name = dirent.name;
|
|
290
|
+
if (name === 'Archiv' || name.startsWith('.')) continue;
|
|
291
|
+
|
|
292
|
+
const jsonlPath = path.join(reposRoot, name, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
293
|
+
if (!existsSync(jsonlPath)) continue;
|
|
294
|
+
|
|
295
|
+
let content;
|
|
296
|
+
try {
|
|
297
|
+
content = await fs.readFile(jsonlPath, 'utf8');
|
|
298
|
+
} catch (err) {
|
|
299
|
+
process.stderr.write(`${SCRIPT_NAME}: WARN: skipping ${name}/sessions.jsonl (read error: ${err.message})\n`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
reposScanned++;
|
|
304
|
+
|
|
305
|
+
// Canonical namespace identity from the git remote (NOT the basename) so a
|
|
306
|
+
// backfilled session shares the folder with that repo's own native writes.
|
|
307
|
+
const repoId = repoCanonicalSlug(reposRoot, name);
|
|
308
|
+
|
|
309
|
+
for (const rawLine of content.split('\n')) {
|
|
310
|
+
const line = rawLine.trim();
|
|
311
|
+
if (!line) continue;
|
|
312
|
+
|
|
313
|
+
let record;
|
|
314
|
+
try {
|
|
315
|
+
record = JSON.parse(line);
|
|
316
|
+
} catch {
|
|
317
|
+
// Malformed JSONL line — skip and continue (never abort the whole run).
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!record || typeof record !== 'object') continue;
|
|
321
|
+
|
|
322
|
+
const sessionId = typeof record.session_id === 'string' ? record.session_id.trim() : '';
|
|
323
|
+
if (!sessionId) continue;
|
|
324
|
+
|
|
325
|
+
// sid index — the authoritative join (HIGH tier in the backfill module)
|
|
326
|
+
addToSetIndex(sidIndex, sessionId, repoId);
|
|
327
|
+
|
|
328
|
+
// branch/date index — the MEDIUM-tier fallback
|
|
329
|
+
const parsed = parseSessionId(sessionId);
|
|
330
|
+
const branch =
|
|
331
|
+
(typeof record.branch === 'string' && record.branch.trim()) || (parsed ? parsed.branch : null);
|
|
332
|
+
const date =
|
|
333
|
+
(parsed ? parsed.date : null) ||
|
|
334
|
+
(typeof record.started_at === 'string' ? record.started_at.slice(0, 10) : null);
|
|
335
|
+
|
|
336
|
+
// Skip records where no (branch,date) signal can be derived — the bdIndex
|
|
337
|
+
// key requires both. A bare date with no branch is not a usable key.
|
|
338
|
+
if (branch && date && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
339
|
+
addToSetIndex(bdIndex, `${branch}|${date}`, repoId);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { sidIndex, bdIndex, reposScanned };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Learn an authoritative `repoIdentity → canonicalNamespace` map from the vault's
|
|
349
|
+
* OWN `source-repo:`/`repo:`-carrying session notes. A repo dir without a readable git remote
|
|
350
|
+
* (so `repoCanonicalSlug` fell back to its CamelCase basename, e.g. `AcmeWidgetV2`
|
|
351
|
+
* → `acmewidgetv2`) cannot derive the hyphenated canonical slug (`acme-widget-v2`)
|
|
352
|
+
* from its name alone. But if that dir's `sessions.jsonl` contains the id of a vault
|
|
353
|
+
* session that DOES carry a `source-repo:` or `repo:` field, that session's resolved namespace is the
|
|
354
|
+
* ground-truth canonical for the dir — so backfilled repo-less sessions in the same
|
|
355
|
+
* dir land in the SAME `<repo>/` folder instead of a split CamelCase variant.
|
|
356
|
+
*
|
|
357
|
+
* Only UNANIMOUS mappings are learned; an identity whose source-repo:/repo:
|
|
358
|
+
* sessions disagree on the namespace is left unmapped (ambiguous → no remap).
|
|
359
|
+
*
|
|
360
|
+
* @param {{ sidIndex: Map<string, Set<string>>, parsedVaultSessions: Array<{id:string, frontmatter:object}> }} args
|
|
361
|
+
* @returns {Map<string, string>} identity → canonical namespace
|
|
362
|
+
*/
|
|
363
|
+
function learnCanonicalMap({ sidIndex, parsedVaultSessions }) {
|
|
364
|
+
// Strip case + every separator so two slug FORMS of the SAME repo compare equal:
|
|
365
|
+
// 'AcmeWidgetV2' → 'acmewidgetv2' === 'acme-widget-v2' → 'acmewidgetv2'.
|
|
366
|
+
const normalize = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
367
|
+
const votes = new Map(); // identity → Map<canonicalNs, count>
|
|
368
|
+
for (const { id, frontmatter } of parsedVaultSessions) {
|
|
369
|
+
if (!id || !frontmatter || !hasSessionRepoSignal(frontmatter)) continue;
|
|
370
|
+
const { namespace } = namespaceForSession(frontmatter); // source-repo:/repo: → canonical
|
|
371
|
+
if (!isConfident(namespace)) continue; // skip _unsorted/redacted-repo/unknown-repo
|
|
372
|
+
const identities = sidIndex.get(id);
|
|
373
|
+
if (!identities) continue;
|
|
374
|
+
// Only an UNAMBIGUOUS source-repo:/repo: note teaches: if its id appears in >1 repo's jsonl,
|
|
375
|
+
// it would mis-teach every other identity its own namespace (collision pollution).
|
|
376
|
+
if (identities.size !== 1) continue;
|
|
377
|
+
for (const identity of identities) {
|
|
378
|
+
if (identity === namespace) continue; // already canonical — nothing to learn
|
|
379
|
+
// GUARD against coincidental session-id collisions: session_ids are NOT globally
|
|
380
|
+
// unique, so a size-1 match can still be the WRONG repo (a different repo that
|
|
381
|
+
// happens to share the id string). Only learn when identity and namespace are
|
|
382
|
+
// the same repo in a different slug FORM (CamelCase basename vs hyphenated slug).
|
|
383
|
+
if (normalize(identity) !== normalize(namespace)) continue;
|
|
384
|
+
let m = votes.get(identity);
|
|
385
|
+
if (!m) {
|
|
386
|
+
m = new Map();
|
|
387
|
+
votes.set(identity, m);
|
|
388
|
+
}
|
|
389
|
+
m.set(namespace, (m.get(namespace) || 0) + 1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const learned = new Map();
|
|
393
|
+
for (const [identity, m] of votes) {
|
|
394
|
+
if (m.size === 1) learned.set(identity, [...m.keys()][0]); // unanimous only
|
|
395
|
+
}
|
|
396
|
+
return learned;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function hasSessionRepoSignal(frontmatter) {
|
|
400
|
+
return hasStringSignal(frontmatter['source-repo']) || hasStringSignal(frontmatter.repo);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function hasStringSignal(value) {
|
|
404
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Rewrite the cross-repo index identities through the learned canonical map (and
|
|
409
|
+
* dedup the resulting sets — two raw dirs that canonicalise to the same repo are
|
|
410
|
+
* one repo). No-op when `learned` is empty (e.g. no `--with-backfill`, or a fixture
|
|
411
|
+
* with no source-repo:/repo:-carrying sessions), preserving byte-identical behaviour.
|
|
412
|
+
*/
|
|
413
|
+
function remapIndices({ sidIndex, bdIndex }, learned) {
|
|
414
|
+
if (learned.size === 0) return { sidIndex, bdIndex };
|
|
415
|
+
const remapSet = (set) => {
|
|
416
|
+
const out = new Set();
|
|
417
|
+
for (const member of set) out.add(learned.get(member) ?? member);
|
|
418
|
+
return out;
|
|
419
|
+
};
|
|
420
|
+
const sid2 = new Map();
|
|
421
|
+
for (const [k, set] of sidIndex) sid2.set(k, remapSet(set));
|
|
422
|
+
const bd2 = new Map();
|
|
423
|
+
for (const [k, set] of bdIndex) bd2.set(k, remapSet(set));
|
|
424
|
+
return { sidIndex: sid2, bdIndex: bd2 };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Append a value to a Set under a key in a Map<string, Set<string>>, creating the
|
|
429
|
+
* Set on first insert.
|
|
430
|
+
*
|
|
431
|
+
* @param {Map<string, Set<string>>} map
|
|
432
|
+
* @param {string} key
|
|
433
|
+
* @param {string} value
|
|
434
|
+
*/
|
|
435
|
+
function addToSetIndex(map, key, value) {
|
|
436
|
+
let set = map.get(key);
|
|
437
|
+
if (!set) {
|
|
438
|
+
set = new Set();
|
|
439
|
+
map.set(key, set);
|
|
440
|
+
}
|
|
441
|
+
set.add(value);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Parse every vault session note (flat + already-namespaced) into the
|
|
446
|
+
* `{ id, frontmatter }` shape that buildBackfillIndex consumes. Reuses the same
|
|
447
|
+
* recursive find pass that buildSessionRepoIndex uses, so session files are read
|
|
448
|
+
* once per concern (this pass is only run under --with-backfill).
|
|
449
|
+
*
|
|
450
|
+
* @param {string} sessionsDir - absolute path to 50-sessions/
|
|
451
|
+
* @returns {Promise<Array<{ id: string, frontmatter: object }>>}
|
|
452
|
+
*/
|
|
453
|
+
async function collectParsedVaultSessions(sessionsDir) {
|
|
454
|
+
const out = [];
|
|
455
|
+
if (!existsSync(sessionsDir)) return out;
|
|
456
|
+
|
|
457
|
+
const result = spawnSync(
|
|
458
|
+
'find',
|
|
459
|
+
[sessionsDir, '-type', 'f', '-name', '*.md'],
|
|
460
|
+
{ encoding: 'utf8' },
|
|
461
|
+
);
|
|
462
|
+
if (result.error || result.status !== 0) {
|
|
463
|
+
process.stderr.write(`${SCRIPT_NAME}: WARN: could not scan sessions for backfill: ${result.stderr || result.error?.message || 'unknown'}\n`);
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const files = result.stdout
|
|
468
|
+
.split('\n')
|
|
469
|
+
.map((l) => l.trim())
|
|
470
|
+
.filter(Boolean);
|
|
471
|
+
|
|
472
|
+
for (const filePath of files) {
|
|
473
|
+
let content;
|
|
474
|
+
try {
|
|
475
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
476
|
+
} catch {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const frontmatter = parseRelocationFrontmatter(content);
|
|
480
|
+
// Prefer the frontmatter id; fall back to the basename (the session id by convention).
|
|
481
|
+
const id =
|
|
482
|
+
(typeof frontmatter.id === 'string' && frontmatter.id.trim()) ||
|
|
483
|
+
path.basename(filePath).replace(/\.md$/, '');
|
|
484
|
+
if (id) out.push({ id, frontmatter });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// Session→repo index builder
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Build a Map<sessionId, namespace> by scanning all session files — both flat
|
|
496
|
+
* root AND existing <repo>/ subdirs (which carry frontmatter source-repo:/repo:).
|
|
497
|
+
*
|
|
498
|
+
* The index is passed to namespaceForLearning so it can resolve
|
|
499
|
+
* source_session links to their owning namespace.
|
|
500
|
+
*
|
|
501
|
+
* When a backfillIndex is supplied (--with-backfill, Issue #700), it is forwarded
|
|
502
|
+
* to namespaceForSession so backfilled sessions (no in-file repo signal) resolve
|
|
503
|
+
* to a confident namespace and land in THIS index — which transitively lifts their
|
|
504
|
+
* learnings via namespaceForLearning's source_session resolution.
|
|
505
|
+
*
|
|
506
|
+
* @param {string} sessionsDir - absolute path to 50-sessions/
|
|
507
|
+
* @param {{ backfillIndex?: Map<string, { repo: string, confidence: string, source: string }> }} [opts]
|
|
508
|
+
* @returns {Map<string, string>} sessionId → namespace
|
|
509
|
+
*/
|
|
510
|
+
async function buildSessionRepoIndex(sessionsDir, opts = {}) {
|
|
511
|
+
const { backfillIndex } = opts;
|
|
512
|
+
const index = new Map();
|
|
513
|
+
if (!existsSync(sessionsDir)) return index;
|
|
514
|
+
|
|
515
|
+
// All session .md files: flat + in subdirs (already-namespaced)
|
|
516
|
+
const result = spawnSync(
|
|
517
|
+
'find',
|
|
518
|
+
[sessionsDir, '-type', 'f', '-name', '*.md'],
|
|
519
|
+
{ encoding: 'utf8' },
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
if (result.error || result.status !== 0) {
|
|
523
|
+
// Non-fatal: return empty index, but warn so the operator knows source_session resolution is disabled
|
|
524
|
+
process.stderr.write(`${SCRIPT_NAME}: WARN: could not scan sessions for index — transitive source_session resolution disabled: ${result.stderr || result.error?.message || 'unknown'}\n`);
|
|
525
|
+
return index;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const files = result.stdout
|
|
529
|
+
.split('\n')
|
|
530
|
+
.map((l) => l.trim())
|
|
531
|
+
.filter(Boolean);
|
|
532
|
+
|
|
533
|
+
for (const filePath of files) {
|
|
534
|
+
let content;
|
|
535
|
+
try {
|
|
536
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
537
|
+
} catch (e) {
|
|
538
|
+
process.stderr.write(`${SCRIPT_NAME}: WARN: skipping session file for index (${path.basename(filePath)}): ${e.message}\n`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const frontmatter = parseRelocationFrontmatter(content);
|
|
543
|
+
const basename = path.basename(filePath);
|
|
544
|
+
// namespaceForSession returns { namespace, source }. Forwarding backfillIndex
|
|
545
|
+
// (undefined when --with-backfill is absent → inert) lets backfilled sessions
|
|
546
|
+
// resolve to a confident namespace and land in this index, which transitively
|
|
547
|
+
// lifts their learnings via namespaceForLearning's source_session resolution.
|
|
548
|
+
const { namespace } = namespaceForSession(frontmatter, { backfillIndex });
|
|
549
|
+
if (namespace && namespace !== '_unsorted' && namespace !== 'redacted-repo' && namespace !== 'unknown-repo') {
|
|
550
|
+
// Derive sessionId from basename (strip .md)
|
|
551
|
+
const sessionId = basename.replace(/\.md$/, '');
|
|
552
|
+
index.set(sessionId, namespace);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return index;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// Classification helpers
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Classify a single file, returning the plan entry.
|
|
565
|
+
*
|
|
566
|
+
* @param {object} opts
|
|
567
|
+
* @param {string} opts.filePath - absolute path
|
|
568
|
+
* @param {string} opts.corpusRoot - absolute path to 40-learnings/ or 50-sessions/
|
|
569
|
+
* @param {Map<string, string>} opts.sessionRepoIndex
|
|
570
|
+
* @param {Map<string, { repo: string, confidence: string, source: string }>} [opts.backfillIndex]
|
|
571
|
+
* @returns {{ from: string, to: string, namespace: string, source: string, confident: boolean }}
|
|
572
|
+
*/
|
|
573
|
+
async function classifyFile({ filePath, corpusRoot, sessionRepoIndex, backfillIndex }) {
|
|
574
|
+
const basename = path.basename(filePath);
|
|
575
|
+
|
|
576
|
+
let content;
|
|
577
|
+
try {
|
|
578
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
579
|
+
} catch (err) {
|
|
580
|
+
return {
|
|
581
|
+
from: filePath,
|
|
582
|
+
to: null,
|
|
583
|
+
namespace: '_unsorted',
|
|
584
|
+
source: 'error',
|
|
585
|
+
confident: false,
|
|
586
|
+
error: err.message,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const frontmatter = parseRelocationFrontmatter(content);
|
|
591
|
+
|
|
592
|
+
// classifyOwner dispatches on frontmatter.type — handles both sessions and learnings.
|
|
593
|
+
// backfillIndex is undefined unless --with-backfill is set → inert in the default path.
|
|
594
|
+
const classifyResult = classifyOwner({ frontmatter, sessionRepoIndex, backfillIndex });
|
|
595
|
+
|
|
596
|
+
const { namespace, source, confident } = classifyResult;
|
|
597
|
+
|
|
598
|
+
const dest = computeDest({ basename, root: corpusRoot, namespace });
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
from: filePath,
|
|
602
|
+
to: dest,
|
|
603
|
+
namespace,
|
|
604
|
+
source,
|
|
605
|
+
confident,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
// Emit (stdout record per file)
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Emit a single JSONL record or human-readable line per file.
|
|
615
|
+
*
|
|
616
|
+
* @param {boolean} jsonMode
|
|
617
|
+
* @param {object} rec
|
|
618
|
+
*/
|
|
619
|
+
function emit(jsonMode, rec) {
|
|
620
|
+
if (jsonMode) {
|
|
621
|
+
process.stdout.write(JSON.stringify(rec) + '\n');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// Human-readable: action from → to [namespace] (source)
|
|
625
|
+
const fromRel = rec.from ? path.basename(rec.from) : '?';
|
|
626
|
+
const toRel = rec.to ? path.relative(path.dirname(rec.from), rec.to) : '?';
|
|
627
|
+
const reasonStr = rec.reason ? ` [${rec.reason}]` : '';
|
|
628
|
+
process.stdout.write(
|
|
629
|
+
`${SCRIPT_NAME}: ${rec.action}: ${fromRel} → ${toRel} (${rec.namespace ?? rec.reason ?? '?'})${reasonStr}\n`,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// git mv wrapper
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Ensure a directory exists inside the vault (creates via mkdir -p, not git).
|
|
639
|
+
* Vault directories can be created outside git since they'll be picked up when
|
|
640
|
+
* files are moved in.
|
|
641
|
+
*
|
|
642
|
+
* @param {string} dir - absolute path
|
|
643
|
+
*/
|
|
644
|
+
async function ensureDir(dir) {
|
|
645
|
+
await fs.mkdir(dir, { recursive: true });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Move a file using `git mv` (stages the rename, does NOT commit).
|
|
650
|
+
*
|
|
651
|
+
* @param {string} vaultDir - absolute path to vault git root
|
|
652
|
+
* @param {string} from - absolute path (source)
|
|
653
|
+
* @param {string} to - absolute path (destination)
|
|
654
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
655
|
+
*/
|
|
656
|
+
function gitMv(vaultDir, from, to) {
|
|
657
|
+
// git mv wants relative paths from the repo root
|
|
658
|
+
const fromRel = path.relative(vaultDir, from);
|
|
659
|
+
const toRel = path.relative(vaultDir, to);
|
|
660
|
+
|
|
661
|
+
const result = spawnSync(
|
|
662
|
+
'git',
|
|
663
|
+
['-C', vaultDir, 'mv', fromRel, toRel],
|
|
664
|
+
{ encoding: 'utf8' },
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
if (result.error) {
|
|
668
|
+
return { ok: false, error: result.error.message };
|
|
669
|
+
}
|
|
670
|
+
if (result.status !== 0) {
|
|
671
|
+
return { ok: false, error: (result.stderr || result.stdout || 'git mv failed').trim() };
|
|
672
|
+
}
|
|
673
|
+
return { ok: true };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
// Manifest writer (atomic tmp + rename)
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Write the reverse-manifest atomically under <vaultDir>/.orchestrator/.
|
|
682
|
+
*
|
|
683
|
+
* @param {object} opts
|
|
684
|
+
* @param {string} opts.vaultDir
|
|
685
|
+
* @param {boolean} opts.derivableOnly
|
|
686
|
+
* @param {object} opts.summary
|
|
687
|
+
* @param {Array<{from:string,to:string,namespace:string,source:string}>} opts.moves
|
|
688
|
+
* @returns {string} absolute path to the written manifest
|
|
689
|
+
*/
|
|
690
|
+
async function writeManifest({ vaultDir, derivableOnly, summary, moves }) {
|
|
691
|
+
const manifestDir = path.join(vaultDir, MANIFEST_DIR);
|
|
692
|
+
await ensureDir(manifestDir);
|
|
693
|
+
|
|
694
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
695
|
+
const manifestPath = path.join(manifestDir, `relocation-manifest-${ts}.json`);
|
|
696
|
+
const tmpPath = `${manifestPath}.tmp-${process.pid}`;
|
|
697
|
+
|
|
698
|
+
// Store vault-relative paths in the manifest
|
|
699
|
+
const movesRel = moves.map((m) => ({
|
|
700
|
+
from: path.relative(vaultDir, m.from),
|
|
701
|
+
to: path.relative(vaultDir, m.to),
|
|
702
|
+
namespace: m.namespace,
|
|
703
|
+
source: m.source,
|
|
704
|
+
}));
|
|
705
|
+
|
|
706
|
+
const manifest = {
|
|
707
|
+
schemaVersion: SCHEMA_VERSION,
|
|
708
|
+
tool: SCRIPT_NAME,
|
|
709
|
+
createdAt: new Date().toISOString(),
|
|
710
|
+
vaultDir,
|
|
711
|
+
mode: derivableOnly ? 'derivable-only' : 'full',
|
|
712
|
+
mechanism: 'git-mv',
|
|
713
|
+
summary,
|
|
714
|
+
moves: movesRel,
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
await fs.writeFile(tmpPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
718
|
+
await fs.rename(tmpPath, manifestPath);
|
|
719
|
+
|
|
720
|
+
return manifestPath;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ---------------------------------------------------------------------------
|
|
724
|
+
// Rollback
|
|
725
|
+
// ---------------------------------------------------------------------------
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Reverse a previous --apply run by reversing each move in the manifest.
|
|
729
|
+
*
|
|
730
|
+
* @param {string} manifestPath
|
|
731
|
+
* @param {boolean} jsonMode
|
|
732
|
+
*/
|
|
733
|
+
async function runRollback(manifestPath, jsonMode) {
|
|
734
|
+
let manifestContent;
|
|
735
|
+
try {
|
|
736
|
+
manifestContent = await fs.readFile(manifestPath, 'utf8');
|
|
737
|
+
} catch (err) {
|
|
738
|
+
process.stderr.write(`${SCRIPT_NAME}: failed to read manifest: ${err.message}\n`);
|
|
739
|
+
process.exit(2);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let manifest;
|
|
743
|
+
try {
|
|
744
|
+
manifest = JSON.parse(manifestContent);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
process.stderr.write(`${SCRIPT_NAME}: failed to parse manifest: ${err.message}\n`);
|
|
747
|
+
process.exit(2);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const { vaultDir, moves } = manifest;
|
|
751
|
+
if (!vaultDir || !Array.isArray(moves)) {
|
|
752
|
+
process.stderr.write(`${SCRIPT_NAME}: invalid manifest schema\n`);
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
let reversed = 0;
|
|
757
|
+
let skipped = 0;
|
|
758
|
+
let errors = 0;
|
|
759
|
+
|
|
760
|
+
// Reverse in reverse order (mirrors file-by-file; order matters if subdirs)
|
|
761
|
+
for (let i = moves.length - 1; i >= 0; i--) {
|
|
762
|
+
const m = moves[i];
|
|
763
|
+
const absTo = path.join(vaultDir, m.to); // current location
|
|
764
|
+
const absFrom = path.join(vaultDir, m.from); // destination (original)
|
|
765
|
+
|
|
766
|
+
// Assert: to exists, from absent
|
|
767
|
+
if (!existsSync(absTo)) {
|
|
768
|
+
emit(jsonMode, {
|
|
769
|
+
action: 'skipped',
|
|
770
|
+
from: absTo,
|
|
771
|
+
to: absFrom,
|
|
772
|
+
namespace: m.namespace,
|
|
773
|
+
source: m.source,
|
|
774
|
+
confident: null,
|
|
775
|
+
reason: 'source-missing-for-rollback',
|
|
776
|
+
});
|
|
777
|
+
skipped++;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (existsSync(absFrom)) {
|
|
781
|
+
emit(jsonMode, {
|
|
782
|
+
action: 'skipped',
|
|
783
|
+
from: absTo,
|
|
784
|
+
to: absFrom,
|
|
785
|
+
namespace: m.namespace,
|
|
786
|
+
source: m.source,
|
|
787
|
+
confident: null,
|
|
788
|
+
reason: 'dest-exists-for-rollback',
|
|
789
|
+
});
|
|
790
|
+
skipped++;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const mvResult = gitMv(vaultDir, absTo, absFrom);
|
|
795
|
+
if (!mvResult.ok) {
|
|
796
|
+
process.stderr.write(`${SCRIPT_NAME}: ERROR git mv failed: ${mvResult.error}\n`);
|
|
797
|
+
errors++;
|
|
798
|
+
emit(jsonMode, {
|
|
799
|
+
action: 'error',
|
|
800
|
+
from: absTo,
|
|
801
|
+
to: absFrom,
|
|
802
|
+
namespace: m.namespace,
|
|
803
|
+
source: m.source,
|
|
804
|
+
confident: null,
|
|
805
|
+
reason: mvResult.error,
|
|
806
|
+
});
|
|
807
|
+
} else {
|
|
808
|
+
emit(jsonMode, {
|
|
809
|
+
action: 'rolled-back',
|
|
810
|
+
from: absTo,
|
|
811
|
+
to: absFrom,
|
|
812
|
+
namespace: m.namespace,
|
|
813
|
+
source: m.source,
|
|
814
|
+
confident: null,
|
|
815
|
+
});
|
|
816
|
+
reversed++;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
process.stderr.write(
|
|
821
|
+
`${SCRIPT_NAME}: rollback complete — ${reversed} reversed, ${skipped} skipped, ${errors} errors\n`,
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
process.exit(errors > 0 ? 2 : 0);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// ---------------------------------------------------------------------------
|
|
828
|
+
// Main
|
|
829
|
+
// ---------------------------------------------------------------------------
|
|
830
|
+
|
|
831
|
+
async function main() {
|
|
832
|
+
const opts = parseArgs(process.argv);
|
|
833
|
+
|
|
834
|
+
if (opts.help) {
|
|
835
|
+
printHelp();
|
|
836
|
+
process.exit(0);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Handle --rollback before requiring --vault-dir (manifest carries vaultDir)
|
|
840
|
+
if (opts.rollback !== null) {
|
|
841
|
+
await runRollback(opts.rollback, opts.json);
|
|
842
|
+
return; // runRollback calls process.exit
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// --vault-dir is required for all other modes
|
|
846
|
+
if (!opts.vaultDir) {
|
|
847
|
+
process.stderr.write(
|
|
848
|
+
`${SCRIPT_NAME}: --vault-dir is required (no default to prevent accidental writes)\n`,
|
|
849
|
+
);
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const vaultDir = path.resolve(opts.vaultDir);
|
|
854
|
+
if (!existsSync(vaultDir)) {
|
|
855
|
+
process.stderr.write(`${SCRIPT_NAME}: vault-dir not found: ${vaultDir}\n`);
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Load relocation rules (errors are warnings, not fatal)
|
|
860
|
+
const ruleset = loadVaultRelocationRules();
|
|
861
|
+
for (const e of ruleset.errors ?? []) {
|
|
862
|
+
process.stderr.write(`${SCRIPT_NAME}: config: ${e}\n`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── Corpus paths ─────────────────────────────────────────────────────────
|
|
866
|
+
const learningsDir = path.join(vaultDir, LEARNINGS_SUBDIR);
|
|
867
|
+
const sessionsDir = path.join(vaultDir, SESSIONS_SUBDIR);
|
|
868
|
+
|
|
869
|
+
// ── Step 0: Build the cross-repo backfill index (--with-backfill only) ─────
|
|
870
|
+
// When --with-backfill is ABSENT, NO sessions.jsonl is read and backfillIndex
|
|
871
|
+
// stays undefined → every downstream consumer is inert → byte-identical default.
|
|
872
|
+
let backfillIndex; // undefined unless --with-backfill
|
|
873
|
+
if (opts.withBackfill) {
|
|
874
|
+
const reposRoot = opts.reposRoot
|
|
875
|
+
? path.resolve(opts.reposRoot)
|
|
876
|
+
: path.dirname(vaultDir);
|
|
877
|
+
const rawIndices = await buildCrossRepoIndices(reposRoot);
|
|
878
|
+
const parsedVaultSessions = await collectParsedVaultSessions(sessionsDir);
|
|
879
|
+
// Canonicalise repo identities against the vault's own source-repo:/repo:
|
|
880
|
+
// ground truth so a remote-less dir shares the folder with its repo-signaled notes.
|
|
881
|
+
const learned = learnCanonicalMap({ sidIndex: rawIndices.sidIndex, parsedVaultSessions });
|
|
882
|
+
const { sidIndex, bdIndex } = remapIndices(rawIndices, learned);
|
|
883
|
+
backfillIndex = buildBackfillIndex(parsedVaultSessions, { sidIndex, bdIndex });
|
|
884
|
+
process.stderr.write(
|
|
885
|
+
`${SCRIPT_NAME}: backfill: scanned ${rawIndices.reposScanned} repos under ${reposRoot} ` +
|
|
886
|
+
`(${sidIndex.size} sids, ${bdIndex.size} branch|date keys, ${learned.size} canonical-learned) → ` +
|
|
887
|
+
`${backfillIndex.size} sessions attributed from ${parsedVaultSessions.length} vault sessions\n`,
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ── Step 1: Build session→repo index (single pass over ALL session files) ──
|
|
892
|
+
// Forwarding backfillIndex lets backfilled sessions land in this index, which
|
|
893
|
+
// transitively lifts their learnings via source_session resolution.
|
|
894
|
+
const sessionRepoIndex = await buildSessionRepoIndex(sessionsDir, { backfillIndex });
|
|
895
|
+
|
|
896
|
+
// ── Step 2: Enumerate flat files (maxdepth 1 only) ───────────────────────
|
|
897
|
+
const scopeSessions = !opts.learningsOnly;
|
|
898
|
+
const scopeLearnings = !opts.sessionsOnly;
|
|
899
|
+
|
|
900
|
+
const sessionFiles = scopeSessions ? enumerateFlatFiles(sessionsDir) : [];
|
|
901
|
+
const learningFiles = scopeLearnings ? enumerateFlatFiles(learningsDir) : [];
|
|
902
|
+
|
|
903
|
+
// ── Step 3: Classify each flat file ──────────────────────────────────────
|
|
904
|
+
/** @type {Array<{from:string,to:string,namespace:string,source:string,confident:boolean,corpusRoot:string}>} */
|
|
905
|
+
const plan = [];
|
|
906
|
+
|
|
907
|
+
for (const filePath of sessionFiles) {
|
|
908
|
+
const entry = await classifyFile({
|
|
909
|
+
filePath,
|
|
910
|
+
corpusRoot: sessionsDir,
|
|
911
|
+
sessionRepoIndex,
|
|
912
|
+
backfillIndex,
|
|
913
|
+
});
|
|
914
|
+
plan.push({ ...entry, corpusRoot: sessionsDir });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for (const filePath of learningFiles) {
|
|
918
|
+
const entry = await classifyFile({
|
|
919
|
+
filePath,
|
|
920
|
+
corpusRoot: learningsDir,
|
|
921
|
+
sessionRepoIndex,
|
|
922
|
+
backfillIndex,
|
|
923
|
+
});
|
|
924
|
+
plan.push({ ...entry, corpusRoot: learningsDir });
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// ── Step 3.5: Pre-flight intra-batch dest-uniqueness detection ────────────
|
|
928
|
+
// W1-D3 safety gap: computeDest is purely structural, so two distinct source
|
|
929
|
+
// files with the same basename + namespace map to the SAME `to`. The runtime
|
|
930
|
+
// existsSync guard only catches collisions against files that ALREADY exist —
|
|
931
|
+
// it cannot see same-batch collisions in --dry-run (nothing has moved yet), so
|
|
932
|
+
// dry-run under-counts. Group move-ELIGIBLE entries by `to`; any `to` reached
|
|
933
|
+
// by ≥2 distinct `from` is an intra-batch collision. This surfaces them up-front.
|
|
934
|
+
const destGroups = new Map(); // to → Set<from>
|
|
935
|
+
for (const entry of plan) {
|
|
936
|
+
if (entry.error || !entry.to) continue;
|
|
937
|
+
const relToCorpus = path.relative(entry.corpusRoot, entry.from);
|
|
938
|
+
if (isAlreadyNamespaced(relToCorpus)) continue;
|
|
939
|
+
// Mirror the --derivable-only skip: non-confident entries don't reach the move
|
|
940
|
+
// stage in that mode, so they cannot collide there either.
|
|
941
|
+
if (opts.derivableOnly && !isConfident(entry.namespace)) continue;
|
|
942
|
+
let set = destGroups.get(entry.to);
|
|
943
|
+
if (!set) {
|
|
944
|
+
set = new Set();
|
|
945
|
+
destGroups.set(entry.to, set);
|
|
946
|
+
}
|
|
947
|
+
set.add(entry.from);
|
|
948
|
+
}
|
|
949
|
+
/** @type {Set<string>} colliding `to` paths (≥2 distinct sources) */
|
|
950
|
+
const collidingDests = new Set();
|
|
951
|
+
for (const [to, froms] of destGroups) {
|
|
952
|
+
if (froms.size >= 2) collidingDests.add(to);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ── Step 4: Apply mode decision, emit records ─────────────────────────────
|
|
956
|
+
const summary = {
|
|
957
|
+
moved: 0,
|
|
958
|
+
skippedAlreadyNamespaced: 0,
|
|
959
|
+
fallbackBucket: 0,
|
|
960
|
+
skippedNonConfident: 0,
|
|
961
|
+
destCollisions: 0,
|
|
962
|
+
intraBatchCollisions: collidingDests.size,
|
|
963
|
+
ioErrors: 0,
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
// Surface each intra-batch-colliding dest as an emit record (one per colliding
|
|
967
|
+
// `to`, listing the count of competing sources) so --dry-run is honest about them.
|
|
968
|
+
for (const to of collidingDests) {
|
|
969
|
+
emit(opts.json, {
|
|
970
|
+
action: 'intra-batch-collision',
|
|
971
|
+
from: null,
|
|
972
|
+
to,
|
|
973
|
+
namespace: null,
|
|
974
|
+
source: 'pre-flight',
|
|
975
|
+
confident: null,
|
|
976
|
+
reason: `intra-batch-collision: ${destGroups.get(to).size} sources map to this dest`,
|
|
977
|
+
collidingSources: [...destGroups.get(to)],
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** @type {Array<{from:string,to:string,namespace:string,source:string}>} */
|
|
982
|
+
const appliedMoves = [];
|
|
983
|
+
|
|
984
|
+
for (const entry of plan) {
|
|
985
|
+
// isAlreadyNamespaced is a structural guard — should never be hit (we enumerate maxdepth 1)
|
|
986
|
+
// but defensive check in case find returned a deeper path
|
|
987
|
+
const relToCorpus = path.relative(entry.corpusRoot, entry.from);
|
|
988
|
+
if (isAlreadyNamespaced(relToCorpus)) {
|
|
989
|
+
summary.skippedAlreadyNamespaced++;
|
|
990
|
+
emit(opts.json, {
|
|
991
|
+
action: 'skipped',
|
|
992
|
+
from: entry.from,
|
|
993
|
+
to: entry.to,
|
|
994
|
+
namespace: entry.namespace,
|
|
995
|
+
source: entry.source,
|
|
996
|
+
confident: entry.confident,
|
|
997
|
+
reason: 'already-namespaced',
|
|
998
|
+
});
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (entry.error) {
|
|
1003
|
+
summary.ioErrors++;
|
|
1004
|
+
emit(opts.json, {
|
|
1005
|
+
action: 'skipped',
|
|
1006
|
+
from: entry.from,
|
|
1007
|
+
to: null,
|
|
1008
|
+
namespace: '_unsorted',
|
|
1009
|
+
source: 'error',
|
|
1010
|
+
confident: false,
|
|
1011
|
+
reason: `read-error: ${entry.error}`,
|
|
1012
|
+
});
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// --derivable-only: skip non-confident entries
|
|
1017
|
+
if (opts.derivableOnly && !isConfident(entry.namespace)) {
|
|
1018
|
+
summary.skippedNonConfident++;
|
|
1019
|
+
emit(opts.json, {
|
|
1020
|
+
action: 'skipped',
|
|
1021
|
+
from: entry.from,
|
|
1022
|
+
to: entry.to,
|
|
1023
|
+
namespace: entry.namespace,
|
|
1024
|
+
source: entry.source,
|
|
1025
|
+
confident: entry.confident,
|
|
1026
|
+
reason: 'non-confident',
|
|
1027
|
+
});
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Track fallback bucket
|
|
1032
|
+
if (!isConfident(entry.namespace)) {
|
|
1033
|
+
summary.fallbackBucket++;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Dest-collision guard
|
|
1037
|
+
if (existsSync(entry.to)) {
|
|
1038
|
+
summary.destCollisions++;
|
|
1039
|
+
emit(opts.json, {
|
|
1040
|
+
action: 'skipped',
|
|
1041
|
+
from: entry.from,
|
|
1042
|
+
to: entry.to,
|
|
1043
|
+
namespace: entry.namespace,
|
|
1044
|
+
source: entry.source,
|
|
1045
|
+
confident: entry.confident,
|
|
1046
|
+
reason: 'dest-exists',
|
|
1047
|
+
});
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!opts.apply) {
|
|
1052
|
+
// Dry-run: report what would happen
|
|
1053
|
+
emit(opts.json, {
|
|
1054
|
+
action: 'would-move',
|
|
1055
|
+
from: entry.from,
|
|
1056
|
+
to: entry.to,
|
|
1057
|
+
namespace: entry.namespace,
|
|
1058
|
+
source: entry.source,
|
|
1059
|
+
confident: entry.confident,
|
|
1060
|
+
});
|
|
1061
|
+
summary.moved++;
|
|
1062
|
+
} else {
|
|
1063
|
+
// Apply: mkdir, git mv, record
|
|
1064
|
+
try {
|
|
1065
|
+
await ensureDir(path.dirname(entry.to));
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
summary.ioErrors++;
|
|
1068
|
+
process.stderr.write(
|
|
1069
|
+
`${SCRIPT_NAME}: ERROR creating dir ${path.dirname(entry.to)}: ${err.message}\n`,
|
|
1070
|
+
);
|
|
1071
|
+
emit(opts.json, {
|
|
1072
|
+
action: 'skipped',
|
|
1073
|
+
from: entry.from,
|
|
1074
|
+
to: entry.to,
|
|
1075
|
+
namespace: entry.namespace,
|
|
1076
|
+
source: entry.source,
|
|
1077
|
+
confident: entry.confident,
|
|
1078
|
+
reason: `mkdir-error: ${err.message}`,
|
|
1079
|
+
});
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const mvResult = gitMv(vaultDir, entry.from, entry.to);
|
|
1084
|
+
if (!mvResult.ok) {
|
|
1085
|
+
summary.ioErrors++;
|
|
1086
|
+
process.stderr.write(`${SCRIPT_NAME}: ERROR git mv failed: ${mvResult.error}\n`);
|
|
1087
|
+
emit(opts.json, {
|
|
1088
|
+
action: 'skipped',
|
|
1089
|
+
from: entry.from,
|
|
1090
|
+
to: entry.to,
|
|
1091
|
+
namespace: entry.namespace,
|
|
1092
|
+
source: entry.source,
|
|
1093
|
+
confident: entry.confident,
|
|
1094
|
+
reason: `git-mv-error: ${mvResult.error}`,
|
|
1095
|
+
});
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
emit(opts.json, {
|
|
1100
|
+
action: 'moved',
|
|
1101
|
+
from: entry.from,
|
|
1102
|
+
to: entry.to,
|
|
1103
|
+
namespace: entry.namespace,
|
|
1104
|
+
source: entry.source,
|
|
1105
|
+
confident: entry.confident,
|
|
1106
|
+
});
|
|
1107
|
+
summary.moved++;
|
|
1108
|
+
appliedMoves.push({
|
|
1109
|
+
from: entry.from,
|
|
1110
|
+
to: entry.to,
|
|
1111
|
+
namespace: entry.namespace,
|
|
1112
|
+
source: entry.source,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// ── Step 5: Write manifest on --apply ─────────────────────────────────────
|
|
1118
|
+
if (opts.apply && appliedMoves.length > 0) {
|
|
1119
|
+
try {
|
|
1120
|
+
const writtenPath = await writeManifest({
|
|
1121
|
+
vaultDir,
|
|
1122
|
+
derivableOnly: opts.derivableOnly,
|
|
1123
|
+
summary,
|
|
1124
|
+
moves: appliedMoves,
|
|
1125
|
+
});
|
|
1126
|
+
process.stderr.write(`${SCRIPT_NAME}: manifest written: ${writtenPath}\n`);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
summary.ioErrors++;
|
|
1129
|
+
process.stderr.write(`${SCRIPT_NAME}: WARN: manifest write failed — moves were staged via git mv but --rollback via manifest is unavailable; reverse manually via 'git -C <vault> reset' or 'git mv' back. Error: ${err.message}\n`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
1134
|
+
const modeLabel = opts.apply ? 'applied' : 'dry-run';
|
|
1135
|
+
const derivableLabel = opts.derivableOnly ? ' [derivable-only]' : '';
|
|
1136
|
+
// The intra-batch segment is appended only when collisions exist OR backfill is
|
|
1137
|
+
// active. This preserves byte-identical summary output for the legacy default
|
|
1138
|
+
// path (no --with-backfill, no intra-batch collisions), while making the new
|
|
1139
|
+
// signal visible exactly when it carries information.
|
|
1140
|
+
const intraBatchSegment =
|
|
1141
|
+
collidingDests.size > 0 || opts.withBackfill
|
|
1142
|
+
? `${summary.intraBatchCollisions} intra-batch-collisions, `
|
|
1143
|
+
: '';
|
|
1144
|
+
process.stderr.write(
|
|
1145
|
+
`${SCRIPT_NAME}: ${summary.moved} ${opts.apply ? 'moved' : 'would-move'}, ` +
|
|
1146
|
+
`${summary.skippedAlreadyNamespaced} already-namespaced, ` +
|
|
1147
|
+
`${summary.skippedNonConfident} non-confident skipped, ` +
|
|
1148
|
+
`${summary.destCollisions} dest-collisions, ` +
|
|
1149
|
+
intraBatchSegment +
|
|
1150
|
+
`${summary.ioErrors} I/O errors` +
|
|
1151
|
+
` [${modeLabel}${derivableLabel}]\n`,
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
process.exit(summary.ioErrors > 0 ? 2 : 0);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// ---------------------------------------------------------------------------
|
|
1158
|
+
// Entry guard (run only when invoked directly)
|
|
1159
|
+
// ---------------------------------------------------------------------------
|
|
1160
|
+
|
|
1161
|
+
const invokedDirectly =
|
|
1162
|
+
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
1163
|
+
|
|
1164
|
+
if (invokedDirectly) {
|
|
1165
|
+
main().catch((err) => {
|
|
1166
|
+
process.stderr.write(`${SCRIPT_NAME}: FATAL: ${err.message}\n`);
|
|
1167
|
+
process.exit(2);
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
export {
|
|
1172
|
+
enumerateFlatFiles,
|
|
1173
|
+
buildSessionRepoIndex,
|
|
1174
|
+
classifyFile,
|
|
1175
|
+
writeManifest,
|
|
1176
|
+
runRollback,
|
|
1177
|
+
_setResolverForTest,
|
|
1178
|
+
};
|