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,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lock-reaper.mjs — reconciliation for orphaned session.lock leases (Epic #724 C7).
|
|
3
|
+
*
|
|
4
|
+
* The complement to the deterministic release wired into hooks/on-session-end.mjs
|
|
5
|
+
* (W2). That release covers the clean-exit path (readLock → ownership-match →
|
|
6
|
+
* release). The reaper is the RECONCILIATION for leases that were left behind
|
|
7
|
+
* anyway — a crash before SessionEnd, an old fleet-wide backlog, a lock a
|
|
8
|
+
* different repo's process never cleaned up. Fleet evidence: 10 orphans (dead
|
|
9
|
+
* PIDs) across ~/Projects, one of which blocked this repo's own session-start
|
|
10
|
+
* this morning until it was force-taken-over by hand.
|
|
11
|
+
*
|
|
12
|
+
* Discovery reuses dispatcher/enumerate.mjs (host-wide candidate scan classifying
|
|
13
|
+
* each repo's lease free | in-progress | force-closed). `force-closed` — a lock
|
|
14
|
+
* present but with a heartbeat older than its TTL — is exactly the reap set.
|
|
15
|
+
*
|
|
16
|
+
* HARD SAFETY INVARIANTS (see reapStaleLocks / evaluateRepo):
|
|
17
|
+
* (a) NEVER reap a live lock (isLockLive true) — heartbeat-fresh means an
|
|
18
|
+
* active owner, regardless of anything else.
|
|
19
|
+
* (b) Defense-in-depth: own-host lock whose recorded PID is still alive is
|
|
20
|
+
* NEVER reaped, even past TTL (PID-recycle / genuinely-alive edge).
|
|
21
|
+
* (c) Cross-host locks are NEVER auto-reaped — we cannot signal a remote
|
|
22
|
+
* process, so they are only LISTED for an operator (reason
|
|
23
|
+
* 'cross-host-requires-operator'), independent of any flag.
|
|
24
|
+
* (d) A reap is an ARCHIVE-MOVE (write a copy to
|
|
25
|
+
* <repo>/.orchestrator/tmp/reaped-locks/<sessionId>-<ts>.json, then unlink
|
|
26
|
+
* the original) — never an unlink-only. The archive is the audit trail.
|
|
27
|
+
* (e) TOCTOU defense: immediately before the destructive unlink, the lease is
|
|
28
|
+
* RE-READ and re-verified (still dead + same session_id as the lock this
|
|
29
|
+
* evaluation started from). A session that bootstraps and writes a FRESH
|
|
30
|
+
* lock in the window between the initial read and the unlink must not
|
|
31
|
+
* have its lease destroyed. A changed/live lease aborts the reap with
|
|
32
|
+
* reason 'lock-changed-during-reap' and best-effort undoes the archive
|
|
33
|
+
* write that was already made.
|
|
34
|
+
*
|
|
35
|
+
* CURRENT-SESSION.JSON ORPHAN SWEEP (Epic #724 follow-up): `evaluateRepo` also
|
|
36
|
+
* sweeps an orphaned `.orchestrator/current-session.json` as part of the SAME
|
|
37
|
+
* lease decision — never as an independent check. The file's fate is folded
|
|
38
|
+
* into the per-repo result's `currentSession` field:
|
|
39
|
+
* { archived: true, archivePath } | { archived: false, reason }
|
|
40
|
+
* The sweep only ever runs once the LOCK side has already been confirmed
|
|
41
|
+
* dead/absent/reap-eligible (mirrors invariants a-c by construction — a live,
|
|
42
|
+
* re-entrant, cross-host, or pid-alive lock short-circuits with the SAME
|
|
43
|
+
* reason before the file is even read). Once eligible, the file itself is
|
|
44
|
+
* still guarded by:
|
|
45
|
+
* (b) re-entrancy — never touch a file that references `currentSessionId`.
|
|
46
|
+
* (c) fresh-preservation — a file whose own age (its `timestamp` field,
|
|
47
|
+
* falling back to mtime) is within `DEFAULT_TTL_HOURS` is preserved,
|
|
48
|
+
* protecting a repo mid-bootstrap that wrote current-session.json
|
|
49
|
+
* before its lock (mirrors the mtime fallback in session-registry.mjs
|
|
50
|
+
* sweepZombies).
|
|
51
|
+
* (d) archive-move (same reaped-locks directory, `current-session-` prefix).
|
|
52
|
+
* (e) TOCTOU re-check immediately before the destructive unlink — aborts
|
|
53
|
+
* with reason 'current-session-changed-during-reap' if the file changed.
|
|
54
|
+
*
|
|
55
|
+
* No-throw contract: every path catches and degrades. Per-repo failures land in
|
|
56
|
+
* `skipped` with an `error: …` reason; the sweep continues.
|
|
57
|
+
*
|
|
58
|
+
* Dependency-injected (the `deps` arg) exactly like enumerate.mjs so tests drive
|
|
59
|
+
* tmp fixtures + spy on emitEvent without touching the real ~/Projects fleet.
|
|
60
|
+
*
|
|
61
|
+
* Plain Node ESM. Named exports. No external deps.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from 'node:fs';
|
|
65
|
+
import os from 'node:os';
|
|
66
|
+
import path from 'node:path';
|
|
67
|
+
|
|
68
|
+
import { enumerateCandidates } from './dispatcher/enumerate.mjs';
|
|
69
|
+
import { readLock, isLockLive, isPidAliveOnHost, LOCK_PATH, DEFAULT_TTL_HOURS } from './session-lock.mjs';
|
|
70
|
+
import { emitEvent } from './events.mjs';
|
|
71
|
+
|
|
72
|
+
const REAPED_ARCHIVE_SUBDIR = '.orchestrator/tmp/reaped-locks';
|
|
73
|
+
const EVENTS_RELPATH = '.orchestrator/metrics/events.jsonl';
|
|
74
|
+
const REAPED_EVENT = 'orchestrator.session.lock.reaped';
|
|
75
|
+
const CURRENT_SESSION_RELPATH = '.orchestrator/current-session.json';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the dependency bundle, defaulting every seam to the real
|
|
79
|
+
* implementation. Tests override the ones they need (hostname, isPidAliveOnHost,
|
|
80
|
+
* emitEvent, fs, and enumerateDeps.getCrossRepoProjects for a []-stubbed scan).
|
|
81
|
+
*
|
|
82
|
+
* @param {object} [deps]
|
|
83
|
+
* @returns {object}
|
|
84
|
+
*/
|
|
85
|
+
function resolveDeps(deps = {}) {
|
|
86
|
+
const d = deps ?? {};
|
|
87
|
+
return {
|
|
88
|
+
enumerateCandidates: d.enumerateCandidates ?? enumerateCandidates,
|
|
89
|
+
readLock: d.readLock ?? readLock,
|
|
90
|
+
isLockLive: d.isLockLive ?? isLockLive,
|
|
91
|
+
isPidAliveOnHost: d.isPidAliveOnHost ?? isPidAliveOnHost,
|
|
92
|
+
hostname: d.hostname ?? (() => os.hostname()),
|
|
93
|
+
emitEvent: d.emitEvent ?? emitEvent,
|
|
94
|
+
fs: d.fs ?? { readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync },
|
|
95
|
+
// Passed straight through to enumerateCandidates for its own fs/config DI.
|
|
96
|
+
// Tests set { getCrossRepoProjects: async () => [] } so the scan stays
|
|
97
|
+
// confined to the tmp fixture and never unions in real ~/Projects paths.
|
|
98
|
+
enumerateDeps: d.enumerateDeps,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Absolute path to a repo's session.lock.
|
|
104
|
+
* @param {string} repoRoot
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function lockPathFor(repoRoot) {
|
|
108
|
+
return path.join(repoRoot, LOCK_PATH);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fractional hours since a lock's last heartbeat (the liveness basis). Falls
|
|
113
|
+
* back to started_at when last_heartbeat is absent (v1 lock). Returns null when
|
|
114
|
+
* neither parses.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} lock
|
|
117
|
+
* @param {number} nowMs
|
|
118
|
+
* @returns {number|null}
|
|
119
|
+
*/
|
|
120
|
+
function ageHoursOf(lock, nowMs) {
|
|
121
|
+
const hb = (typeof lock.last_heartbeat === 'string' && lock.last_heartbeat.length > 0)
|
|
122
|
+
? lock.last_heartbeat
|
|
123
|
+
: lock.started_at;
|
|
124
|
+
const ms = Date.parse(hb);
|
|
125
|
+
if (Number.isNaN(ms)) return null;
|
|
126
|
+
return Number(((nowMs - ms) / (3600 * 1000)).toFixed(2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The stable session identifier for archive/event purposes: prefer the semantic
|
|
131
|
+
* id, fall back to the UUID session_id, then a literal.
|
|
132
|
+
* @param {object} lock
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
function lockSessionId(lock) {
|
|
136
|
+
return lock.semantic_session_id ?? lock.session_id ?? 'unknown-session';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Archive-move a lock file (invariant d): copy the original bytes to the
|
|
141
|
+
* per-repo reaped-locks archive, THEN unlink the original. Never unlink-only —
|
|
142
|
+
* if the archive write fails, the original stays on disk and the caller reports
|
|
143
|
+
* an error instead of destroying the lease.
|
|
144
|
+
*
|
|
145
|
+
* TOCTOU re-check (invariant e): immediately before the destructive unlink, the
|
|
146
|
+
* lease is re-read via `D.readLock` and re-verified — still dead (per
|
|
147
|
+
* `D.isLockLive`) AND the same `session_id` as the `lock` this call started
|
|
148
|
+
* from. A session that bootstraps and writes a fresh lock in the window
|
|
149
|
+
* between evaluateRepo's initial read and this point must not be destroyed.
|
|
150
|
+
* On a changed/live lease, the archive write already made is best-effort
|
|
151
|
+
* undone and the caller reports `{ ok: false, reason: 'lock-changed-during-reap' }`
|
|
152
|
+
* instead of unlinking.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} repoRoot
|
|
155
|
+
* @param {string} lockFile
|
|
156
|
+
* @param {object} lock — the parsed lock (fallback content if the raw re-read fails).
|
|
157
|
+
* @param {number} nowMs
|
|
158
|
+
* @param {object} fsDep — { readFileSync, writeFileSync, unlinkSync, mkdirSync }.
|
|
159
|
+
* @param {object} D — resolved deps (needs `readLock` + `isLockLive` for the re-check).
|
|
160
|
+
* @returns {{ ok: true, archivePath: string } | { ok: false, reason: string }}
|
|
161
|
+
*/
|
|
162
|
+
function archiveLock(repoRoot, lockFile, lock, nowMs, fsDep, D) {
|
|
163
|
+
const safeId = String(lockSessionId(lock)).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
164
|
+
const ts = new Date(nowMs).toISOString().replace(/[:.]/g, '-');
|
|
165
|
+
const archiveDir = path.join(repoRoot, REAPED_ARCHIVE_SUBDIR);
|
|
166
|
+
fsDep.mkdirSync(archiveDir, { recursive: true });
|
|
167
|
+
const archivePath = path.join(archiveDir, `${safeId}-${ts}.json`);
|
|
168
|
+
|
|
169
|
+
// Preserve the exact original bytes; fall back to the serialised parsed lock
|
|
170
|
+
// if the re-read fails (e.g. it vanished between readLock and here).
|
|
171
|
+
let raw;
|
|
172
|
+
try {
|
|
173
|
+
raw = fsDep.readFileSync(lockFile, 'utf8');
|
|
174
|
+
} catch {
|
|
175
|
+
raw = JSON.stringify(lock, null, 2) + '\n';
|
|
176
|
+
}
|
|
177
|
+
fsDep.writeFileSync(archivePath, raw, 'utf8');
|
|
178
|
+
|
|
179
|
+
// Invariant (e) — TOCTOU re-check, immediately before the destructive unlink.
|
|
180
|
+
let recheck;
|
|
181
|
+
try {
|
|
182
|
+
recheck = D.readLock({ repoRoot });
|
|
183
|
+
} catch {
|
|
184
|
+
recheck = null;
|
|
185
|
+
}
|
|
186
|
+
const sameLease = recheck !== null && recheck.session_id === lock.session_id;
|
|
187
|
+
const stillDead = recheck !== null && !D.isLockLive(recheck, nowMs);
|
|
188
|
+
if (!sameLease || !stillDead) {
|
|
189
|
+
// Best-effort undo of the archive write — the original lease is untouched
|
|
190
|
+
// either way, but leaving a stray archive copy around is misleading.
|
|
191
|
+
try { fsDep.unlinkSync(archivePath); } catch { /* best-effort */ }
|
|
192
|
+
return { ok: false, reason: 'lock-changed-during-reap' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Only after the archive is durably written AND the re-check confirms the
|
|
196
|
+
// lease is unchanged do we remove the original.
|
|
197
|
+
fsDep.unlinkSync(lockFile);
|
|
198
|
+
return { ok: true, archivePath };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Absolute path to a repo's current-session.json.
|
|
203
|
+
* @param {string} repoRoot
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
function currentSessionPathFor(repoRoot) {
|
|
207
|
+
return path.join(repoRoot, CURRENT_SESSION_RELPATH);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fractional hours since a current-session.json's own `timestamp` field.
|
|
212
|
+
* Falls back to the file's mtime when `timestamp` is absent/unparseable —
|
|
213
|
+
* mirrors the malformed-entry mtime fallback in session-registry.mjs
|
|
214
|
+
* sweepZombies (fresh-preservation protects an in-flight write from being
|
|
215
|
+
* swept before its author finishes bootstrapping).
|
|
216
|
+
*
|
|
217
|
+
* @param {object} parsed — the parsed current-session.json content.
|
|
218
|
+
* @param {string} filePath
|
|
219
|
+
* @param {number} nowMs
|
|
220
|
+
* @param {object} fsDep — { statSync, … }.
|
|
221
|
+
* @returns {number|null} null when neither the timestamp nor the mtime resolve.
|
|
222
|
+
*/
|
|
223
|
+
function currentSessionAgeHours(parsed, filePath, nowMs, fsDep) {
|
|
224
|
+
const ts = typeof parsed?.timestamp === 'string' ? parsed.timestamp : null;
|
|
225
|
+
const ms = ts ? Date.parse(ts) : NaN;
|
|
226
|
+
if (!Number.isNaN(ms)) {
|
|
227
|
+
return (nowMs - ms) / (3600 * 1000);
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const info = fsDep.statSync(filePath);
|
|
231
|
+
return (nowMs - info.mtimeMs) / (3600 * 1000);
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Whether a parsed current-session.json references the same session as
|
|
239
|
+
* `lock` — either the UUID `session_id` or the `semantic_session_id`
|
|
240
|
+
* matches. Both sides must be non-empty strings to count as a match; two
|
|
241
|
+
* absent/undefined fields are never treated as "matching".
|
|
242
|
+
*
|
|
243
|
+
* @param {object} parsed
|
|
244
|
+
* @param {object} lock
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*/
|
|
247
|
+
function currentSessionReferencesLock(parsed, lock) {
|
|
248
|
+
const sameSessionId =
|
|
249
|
+
typeof parsed.session_id === 'string' && parsed.session_id.length > 0 &&
|
|
250
|
+
parsed.session_id === lock.session_id;
|
|
251
|
+
const sameSemanticId =
|
|
252
|
+
typeof parsed.semantic_session_id === 'string' && parsed.semantic_session_id.length > 0 &&
|
|
253
|
+
parsed.semantic_session_id === lock.semantic_session_id;
|
|
254
|
+
return sameSessionId || sameSemanticId;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Archive-move current-session.json (mirrors archiveLock's invariants d/e for
|
|
259
|
+
* the file sweep). Copies the original bytes into the SAME per-repo
|
|
260
|
+
* reaped-locks archive directory (prefixed `current-session-` to
|
|
261
|
+
* disambiguate from lock archives), then re-reads the live file immediately
|
|
262
|
+
* before the destructive unlink and aborts if it changed to reference a
|
|
263
|
+
* different session or the caller's own (fresh) session.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} repoRoot
|
|
266
|
+
* @param {string} filePath
|
|
267
|
+
* @param {object} parsed — the content read at evaluation time.
|
|
268
|
+
* @param {number} nowMs
|
|
269
|
+
* @param {object} fsDep
|
|
270
|
+
* @param {string} [currentSessionId] — abort if the file now belongs to the caller.
|
|
271
|
+
* @returns {{ archived: true, archivePath: string } | { archived: false, reason: string }}
|
|
272
|
+
*/
|
|
273
|
+
function archiveCurrentSessionFile(repoRoot, filePath, parsed, nowMs, fsDep, currentSessionId) {
|
|
274
|
+
const safeId = String(parsed.semantic_session_id ?? parsed.session_id ?? 'unknown-session')
|
|
275
|
+
.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
276
|
+
const ts = new Date(nowMs).toISOString().replace(/[:.]/g, '-');
|
|
277
|
+
const archiveDir = path.join(repoRoot, REAPED_ARCHIVE_SUBDIR);
|
|
278
|
+
fsDep.mkdirSync(archiveDir, { recursive: true });
|
|
279
|
+
const archivePath = path.join(archiveDir, `current-session-${safeId}-${ts}.json`);
|
|
280
|
+
|
|
281
|
+
// Preserve the exact original bytes; fall back to the serialised parsed
|
|
282
|
+
// content if the re-read fails (e.g. it vanished between read and here).
|
|
283
|
+
let raw;
|
|
284
|
+
try {
|
|
285
|
+
raw = fsDep.readFileSync(filePath, 'utf8');
|
|
286
|
+
} catch {
|
|
287
|
+
raw = JSON.stringify(parsed, null, 2) + '\n';
|
|
288
|
+
}
|
|
289
|
+
fsDep.writeFileSync(archivePath, raw, 'utf8');
|
|
290
|
+
|
|
291
|
+
// TOCTOU re-check (invariant e) — re-read immediately before the unlink.
|
|
292
|
+
let recheck;
|
|
293
|
+
try {
|
|
294
|
+
recheck = JSON.parse(fsDep.readFileSync(filePath, 'utf8'));
|
|
295
|
+
} catch {
|
|
296
|
+
recheck = null;
|
|
297
|
+
}
|
|
298
|
+
const stillSame =
|
|
299
|
+
recheck !== null &&
|
|
300
|
+
recheck.session_id === parsed.session_id &&
|
|
301
|
+
recheck.semantic_session_id === parsed.semantic_session_id;
|
|
302
|
+
const nowReferencesCaller =
|
|
303
|
+
recheck !== null &&
|
|
304
|
+
typeof currentSessionId === 'string' && currentSessionId.length > 0 &&
|
|
305
|
+
(recheck.session_id === currentSessionId || recheck.semantic_session_id === currentSessionId);
|
|
306
|
+
|
|
307
|
+
if (!stillSame || nowReferencesCaller) {
|
|
308
|
+
// Best-effort undo of the archive write — the original file is untouched
|
|
309
|
+
// either way, but leaving a stray archive copy around is misleading.
|
|
310
|
+
try { fsDep.unlinkSync(archivePath); } catch { /* best-effort */ }
|
|
311
|
+
return { archived: false, reason: 'current-session-changed-during-reap' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fsDep.unlinkSync(filePath);
|
|
315
|
+
return { archived: true, archivePath };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Evaluate + (under !dryRun) sweep an orphaned current-session.json for one
|
|
320
|
+
* repo. The caller only invokes this once the LOCK side of the SAME lease
|
|
321
|
+
* decision has already been confirmed dead/absent/reap-eligible — invariants
|
|
322
|
+
* (a) live-lease, cross-host, and own-host-pid-alive are therefore enforced
|
|
323
|
+
* by the CALLER (evaluateRepo) short-circuiting with a matching reason before
|
|
324
|
+
* this function is ever reached, not by a redundant check here.
|
|
325
|
+
*
|
|
326
|
+
* Orphan predicate: the file is orphaned when `lock` is null (no lease exists
|
|
327
|
+
* at all for the repo — subject only to the freshness guard below) OR the
|
|
328
|
+
* file references `lock` (its `session_id`/`semantic_session_id` matches the
|
|
329
|
+
* already-confirmed-dead lock this evaluation is running against).
|
|
330
|
+
*
|
|
331
|
+
* Guards:
|
|
332
|
+
* (b) re-entrancy — never touch a file that references `currentSessionId`
|
|
333
|
+
* (the caller's own fresh write).
|
|
334
|
+
* (c) fresh-preservation — a file whose own age (timestamp, falling back to
|
|
335
|
+
* mtime) is within `DEFAULT_TTL_HOURS` is preserved.
|
|
336
|
+
* (d) archive-move, never unlink-only.
|
|
337
|
+
* (e) TOCTOU re-check immediately before the destructive unlink.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} repoRoot
|
|
340
|
+
* @param {object} args
|
|
341
|
+
* @param {number} args.nowMs
|
|
342
|
+
* @param {boolean} args.dryRun
|
|
343
|
+
* @param {string} [args.currentSessionId]
|
|
344
|
+
* @param {object|null} args.lock — the confirmed-dead lock this file is being
|
|
345
|
+
* evaluated against, or null when no lock exists at all for the repo.
|
|
346
|
+
* @param {object} args.fs — resolved fs dep bundle (readFileSync, statSync, …).
|
|
347
|
+
* @returns {{ archived: boolean, archivePath?: string, reason?: string }}
|
|
348
|
+
*/
|
|
349
|
+
function evaluateCurrentSessionFile(repoRoot, { nowMs, dryRun, currentSessionId, lock, fs: fsDep }) {
|
|
350
|
+
const filePath = currentSessionPathFor(repoRoot);
|
|
351
|
+
let raw;
|
|
352
|
+
try {
|
|
353
|
+
raw = fsDep.readFileSync(filePath, 'utf8');
|
|
354
|
+
} catch {
|
|
355
|
+
return { archived: false, reason: 'no-file' };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let parsed;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(raw);
|
|
361
|
+
} catch {
|
|
362
|
+
return { archived: false, reason: 'malformed' };
|
|
363
|
+
}
|
|
364
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
365
|
+
return { archived: false, reason: 'malformed' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Guard (b): re-entrancy — the file itself belongs to the caller's own session.
|
|
369
|
+
if (
|
|
370
|
+
typeof currentSessionId === 'string' && currentSessionId.length > 0 &&
|
|
371
|
+
(parsed.session_id === currentSessionId || parsed.semantic_session_id === currentSessionId)
|
|
372
|
+
) {
|
|
373
|
+
return { archived: false, reason: 'current-session' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Orphan predicate: when a (confirmed-dead) lock exists, the file must
|
|
377
|
+
// reference IT to count as orphaned. Unrelated content is left alone.
|
|
378
|
+
if (lock && !currentSessionReferencesLock(parsed, lock)) {
|
|
379
|
+
return { archived: false, reason: 'not-orphaned' };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Guard (c): fresh-preservation — protects a repo mid-bootstrap that wrote
|
|
383
|
+
// current-session.json before its lock.
|
|
384
|
+
const ageHours = currentSessionAgeHours(parsed, filePath, nowMs, fsDep);
|
|
385
|
+
if (ageHours === null || ageHours < DEFAULT_TTL_HOURS) {
|
|
386
|
+
return { archived: false, reason: 'fresh' };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (dryRun) {
|
|
390
|
+
return { archived: false, reason: 'candidate' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
return archiveCurrentSessionFile(repoRoot, filePath, parsed, nowMs, fsDep, currentSessionId);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
return { archived: false, reason: `error: ${err?.message ?? String(err)}` };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Evaluate a single repo's lease and — under !dryRun — reap it if eligible.
|
|
402
|
+
* As part of the SAME lease decision, also evaluates + (under !dryRun) sweeps
|
|
403
|
+
* an orphaned `.orchestrator/current-session.json` (see evaluateCurrentSessionFile).
|
|
404
|
+
* Every branch below attaches a `currentSession` field to its result; a
|
|
405
|
+
* lock-side guard (live / lock-level re-entrancy / cross-host / pid-alive /
|
|
406
|
+
* lock-changed-during-reap) short-circuits the file sweep with the SAME
|
|
407
|
+
* reason, without even reading the file — the file is only actually
|
|
408
|
+
* evaluated once the lock side is dead/absent/reap-eligible.
|
|
409
|
+
*
|
|
410
|
+
* Returns exactly one of:
|
|
411
|
+
* { action: 'none', repo, reason, currentSession } — no lock present.
|
|
412
|
+
* { action: 'skipped', repo, reason, …, currentSession } — protected by an invariant.
|
|
413
|
+
* { action: 'candidate', repo, …, currentSession } — reap-eligible, dry-run (not touched).
|
|
414
|
+
* { action: 'reaped', repo, …, archivePath, currentSession } — reap-eligible, archived + emitted.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} repoRoot
|
|
417
|
+
* @param {object} args
|
|
418
|
+
* @param {number} args.nowMs
|
|
419
|
+
* @param {boolean} args.dryRun
|
|
420
|
+
* @param {string} [args.currentSessionId] — never reap the caller's own session.
|
|
421
|
+
* @param {string} args.reapMode — event tag: 'cli' | 'auto-own-repo'.
|
|
422
|
+
* @param {object} args.D — resolved deps.
|
|
423
|
+
* @returns {Promise<object>}
|
|
424
|
+
*/
|
|
425
|
+
async function evaluateRepo(repoRoot, { nowMs, dryRun, currentSessionId, reapMode, D }) {
|
|
426
|
+
const csArgs = { nowMs, dryRun, currentSessionId, fs: D.fs };
|
|
427
|
+
|
|
428
|
+
let lock;
|
|
429
|
+
try {
|
|
430
|
+
lock = D.readLock({ repoRoot });
|
|
431
|
+
} catch {
|
|
432
|
+
lock = null;
|
|
433
|
+
}
|
|
434
|
+
if (!lock || typeof lock !== 'object') {
|
|
435
|
+
// No lease at all — the current-session.json sweep is still eligible
|
|
436
|
+
// (predicate branch 2: no lock exists, subject only to the freshness guard).
|
|
437
|
+
const currentSession = evaluateCurrentSessionFile(repoRoot, { ...csArgs, lock: null });
|
|
438
|
+
return { action: 'none', repo: repoRoot, reason: 'no-lock', currentSession };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Invariant (a): a live lock (fresh heartbeat) is NEVER reaped — and the
|
|
442
|
+
// current-session.json sweep is the SAME lease decision, so it is left
|
|
443
|
+
// entirely untouched too (never even read).
|
|
444
|
+
if (D.isLockLive(lock, nowMs)) {
|
|
445
|
+
return {
|
|
446
|
+
action: 'skipped',
|
|
447
|
+
repo: repoRoot,
|
|
448
|
+
reason: 'live',
|
|
449
|
+
currentSession: { archived: false, reason: 'live' },
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Re-entrancy guard: never reap the current session's own lease. Protects the
|
|
454
|
+
// hook splice, where lock-bootstrap just wrote our fresh lock in the same run.
|
|
455
|
+
if (
|
|
456
|
+
currentSessionId &&
|
|
457
|
+
(lock.session_id === currentSessionId || lock.semantic_session_id === currentSessionId)
|
|
458
|
+
) {
|
|
459
|
+
return {
|
|
460
|
+
action: 'skipped',
|
|
461
|
+
repo: repoRoot,
|
|
462
|
+
reason: 'current-session',
|
|
463
|
+
currentSession: { archived: false, reason: 'current-session' },
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const ownHost = lock.host === D.hostname();
|
|
468
|
+
|
|
469
|
+
// Invariant (c): cross-host leases are NEVER auto-reaped — only listed.
|
|
470
|
+
if (!ownHost) {
|
|
471
|
+
return {
|
|
472
|
+
action: 'skipped',
|
|
473
|
+
repo: repoRoot,
|
|
474
|
+
reason: 'cross-host-requires-operator',
|
|
475
|
+
host: lock.host ?? null,
|
|
476
|
+
currentSession: { archived: false, reason: 'cross-host-requires-operator' },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Invariant (b): own-host + live PID → NEVER reap, even past TTL. The recorded
|
|
481
|
+
// pid is the ephemeral hook subprocess (dead in the common orphan case); a
|
|
482
|
+
// still-alive pid means either a genuinely-live owner or a recycled pid — the
|
|
483
|
+
// conservative choice is to defer to the operator rather than risk a false reap.
|
|
484
|
+
let pidAlive;
|
|
485
|
+
try {
|
|
486
|
+
pidAlive = D.isPidAliveOnHost(lock.pid) === true;
|
|
487
|
+
} catch {
|
|
488
|
+
pidAlive = false;
|
|
489
|
+
}
|
|
490
|
+
if (pidAlive) {
|
|
491
|
+
return {
|
|
492
|
+
action: 'skipped',
|
|
493
|
+
repo: repoRoot,
|
|
494
|
+
reason: 'own-host-pid-alive',
|
|
495
|
+
pid: lock.pid ?? null,
|
|
496
|
+
currentSession: { archived: false, reason: 'own-host-pid-alive' },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Reap-eligible: own-host, dead lease (heartbeat past TTL), dead PID. The
|
|
501
|
+
// current-session.json sweep is now eligible too — same lease decision.
|
|
502
|
+
const ageHours = ageHoursOf(lock, nowMs);
|
|
503
|
+
const base = {
|
|
504
|
+
repo: repoRoot,
|
|
505
|
+
repoName: path.basename(repoRoot),
|
|
506
|
+
sessionId: lockSessionId(lock),
|
|
507
|
+
host: lock.host ?? null,
|
|
508
|
+
pid: lock.pid ?? null,
|
|
509
|
+
ageHours,
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
if (dryRun) {
|
|
513
|
+
const currentSession = evaluateCurrentSessionFile(repoRoot, { ...csArgs, lock });
|
|
514
|
+
return { action: 'candidate', ...base, currentSession };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Apply: archive-move (invariant d) then best-effort event.
|
|
518
|
+
let archiveResult;
|
|
519
|
+
try {
|
|
520
|
+
archiveResult = archiveLock(repoRoot, lockPathFor(repoRoot), lock, nowMs, D.fs, D);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
// The lock predicate was already confirmed dead/eligible above — the file
|
|
523
|
+
// sweep still runs; only the LOCK's own archive mechanics failed.
|
|
524
|
+
const currentSession = evaluateCurrentSessionFile(repoRoot, { ...csArgs, lock });
|
|
525
|
+
return { action: 'skipped', repo: repoRoot, reason: `error: ${err?.message ?? String(err)}`, currentSession };
|
|
526
|
+
}
|
|
527
|
+
if (!archiveResult.ok) {
|
|
528
|
+
// Invariant (e) — the re-check found a changed/live lease. Never unlink.
|
|
529
|
+
// A 'lock-changed-during-reap' means the TRUE state is no longer
|
|
530
|
+
// dead/absent, so the file sweep must NOT proceed either — a fresh
|
|
531
|
+
// session may already own both files.
|
|
532
|
+
const currentSession = archiveResult.reason === 'lock-changed-during-reap'
|
|
533
|
+
? { archived: false, reason: 'lock-changed-during-reap' }
|
|
534
|
+
: evaluateCurrentSessionFile(repoRoot, { ...csArgs, lock });
|
|
535
|
+
return { action: 'skipped', repo: repoRoot, reason: archiveResult.reason, currentSession };
|
|
536
|
+
}
|
|
537
|
+
const archivePath = archiveResult.archivePath;
|
|
538
|
+
|
|
539
|
+
const currentSession = evaluateCurrentSessionFile(repoRoot, { ...csArgs, lock });
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
await D.emitEvent(
|
|
543
|
+
REAPED_EVENT,
|
|
544
|
+
{
|
|
545
|
+
session_id: lock.session_id ?? null,
|
|
546
|
+
semantic_session_id: lock.semantic_session_id ?? null,
|
|
547
|
+
host: lock.host ?? null,
|
|
548
|
+
pid: lock.pid ?? null,
|
|
549
|
+
age_hours: ageHours,
|
|
550
|
+
reap_mode: reapMode,
|
|
551
|
+
current_session: currentSession,
|
|
552
|
+
},
|
|
553
|
+
{ filePath: path.join(repoRoot, EVENTS_RELPATH) },
|
|
554
|
+
);
|
|
555
|
+
} catch {
|
|
556
|
+
// Observability is best-effort — the archive-move already succeeded.
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return { action: 'reaped', ...base, archivePath, currentSession };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Host-wide reconciliation: enumerate candidate repos under `startDir`, and for
|
|
564
|
+
* each `force-closed` (dead-lease) repo evaluate + (under !dryRun) reap it.
|
|
565
|
+
*
|
|
566
|
+
* @param {object} [opts]
|
|
567
|
+
* @param {string} [opts.startDir] — scan root; defaults to the confinement root
|
|
568
|
+
* (via enumerateCandidates). Tests pass a tmp fixture dir.
|
|
569
|
+
* @param {number} [opts.now] — clock seam (ms).
|
|
570
|
+
* @param {boolean} [opts.dryRun=true] — SAFE DEFAULT: classify without mutating.
|
|
571
|
+
* @param {boolean} [opts.ownHostOnly=true] — reaping is restricted to own-host
|
|
572
|
+
* leases. Cross-host leases are NEVER reaped regardless (invariant c); this
|
|
573
|
+
* flag is echoed in the result for auditability and cannot loosen invariant c.
|
|
574
|
+
* @param {object} [opts.deps] — DI seam (see resolveDeps).
|
|
575
|
+
* @returns {Promise<{ scanned:number, candidates:object[], reaped:object[], skipped:object[], dryRun:boolean, ownHostOnly:boolean }>}
|
|
576
|
+
*/
|
|
577
|
+
export async function reapStaleLocks({ startDir, now, dryRun = true, ownHostOnly = true, deps } = {}) {
|
|
578
|
+
const D = resolveDeps(deps);
|
|
579
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
580
|
+
|
|
581
|
+
let enumerated;
|
|
582
|
+
try {
|
|
583
|
+
enumerated = await D.enumerateCandidates({ startDir, now: nowMs, deps: D.enumerateDeps });
|
|
584
|
+
} catch {
|
|
585
|
+
enumerated = [];
|
|
586
|
+
}
|
|
587
|
+
if (!Array.isArray(enumerated)) enumerated = [];
|
|
588
|
+
|
|
589
|
+
const scanned = enumerated.length;
|
|
590
|
+
const candidates = [];
|
|
591
|
+
const reaped = [];
|
|
592
|
+
const skipped = [];
|
|
593
|
+
|
|
594
|
+
const forceClosed = enumerated.filter((c) => c && c.status === 'force-closed' && typeof c.repoRoot === 'string');
|
|
595
|
+
|
|
596
|
+
for (const cand of forceClosed) {
|
|
597
|
+
let result;
|
|
598
|
+
try {
|
|
599
|
+
result = await evaluateRepo(cand.repoRoot, { nowMs, dryRun, reapMode: 'cli', D });
|
|
600
|
+
} catch (err) {
|
|
601
|
+
skipped.push({ repo: cand.repoRoot, reason: `error: ${err?.message ?? String(err)}` });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
switch (result.action) {
|
|
605
|
+
case 'reaped':
|
|
606
|
+
reaped.push(result);
|
|
607
|
+
candidates.push(result);
|
|
608
|
+
break;
|
|
609
|
+
case 'candidate':
|
|
610
|
+
candidates.push(result);
|
|
611
|
+
break;
|
|
612
|
+
case 'skipped':
|
|
613
|
+
skipped.push({ repo: result.repo, reason: result.reason });
|
|
614
|
+
break;
|
|
615
|
+
// 'none' (no-lock) cannot occur for a force-closed candidate; ignore.
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { scanned, candidates, reaped, skipped, dryRun, ownHostOnly };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Single-repo reconciliation — the own-repo path used by the SessionStart hook.
|
|
624
|
+
* Reads ONE repo's lease directly (no host-wide scan → hook stays cheap) and
|
|
625
|
+
* reaps it only when eligible. The live-lock + current-session guards mean the
|
|
626
|
+
* caller's own freshly-bootstrapped lock is always left untouched.
|
|
627
|
+
*
|
|
628
|
+
* @param {object} opts
|
|
629
|
+
* @param {string} opts.repoRoot
|
|
630
|
+
* @param {number} [opts.now]
|
|
631
|
+
* @param {boolean} [opts.dryRun=true]
|
|
632
|
+
* @param {string} [opts.currentSessionId] — the caller's session id (guard).
|
|
633
|
+
* @param {string} [opts.reapMode='auto-own-repo']
|
|
634
|
+
* @param {object} [opts.deps]
|
|
635
|
+
* @returns {Promise<object>} a single evaluateRepo result.
|
|
636
|
+
*/
|
|
637
|
+
export async function reapRepoLock({ repoRoot, now, dryRun = true, currentSessionId, reapMode = 'auto-own-repo', deps } = {}) {
|
|
638
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
639
|
+
return { action: 'none', repo: repoRoot ?? null, reason: 'no-repo-root' };
|
|
640
|
+
}
|
|
641
|
+
const D = resolveDeps(deps);
|
|
642
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
643
|
+
try {
|
|
644
|
+
return await evaluateRepo(repoRoot, { nowMs, dryRun, currentSessionId, reapMode, D });
|
|
645
|
+
} catch (err) {
|
|
646
|
+
return { action: 'skipped', repo: repoRoot, reason: `error: ${err?.message ?? String(err)}` };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locks/index.mjs — convenience barrel for the short-lived lock protocols split
|
|
3
|
+
* out of session-lock.mjs in #630 (A1 barrel-preserving split).
|
|
4
|
+
*
|
|
5
|
+
* Re-exports both protocol modules so a consumer can `import { withStateMdLock,
|
|
6
|
+
* withStagingFenceLock } from '@lib/locks/index.mjs'` in one statement. This is
|
|
7
|
+
* a nice-to-have: session-lock.mjs remains the canonical barrel that preserves
|
|
8
|
+
* the original 22-symbol import surface, and the two protocol modules are also
|
|
9
|
+
* importable directly.
|
|
10
|
+
*
|
|
11
|
+
* Pure re-export leaf — imports only the two sibling modules, never
|
|
12
|
+
* session-lock.mjs, so there is no import cycle.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
STATE_LOCK_PATH,
|
|
17
|
+
DEFAULT_STATE_LOCK_TIMEOUT_MS,
|
|
18
|
+
STATE_LOCK_POLL_MS,
|
|
19
|
+
acquireStateLock,
|
|
20
|
+
releaseStateLock,
|
|
21
|
+
withStateMdLock,
|
|
22
|
+
} from './state-md-lock.mjs';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
STAGING_FENCE_LOCK_PATH,
|
|
26
|
+
DEFAULT_STAGING_FENCE_LOCK_TIMEOUT_MS,
|
|
27
|
+
STAGING_FENCE_LOCK_POLL_MS,
|
|
28
|
+
acquireStagingFenceLock,
|
|
29
|
+
releaseStagingFenceLock,
|
|
30
|
+
withStagingFenceLock,
|
|
31
|
+
} from './staging-fence-lock.mjs';
|