session-orchestrator 5.12.0 → 5.13.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 +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook-root.mjs — which root holds THIS session's own state (#1605).
|
|
3
|
+
*
|
|
4
|
+
* `$CLAUDE_PROJECT_DIR` stays on the LAUNCH checkout after `EnterWorktree`. A
|
|
5
|
+
* /clear, compact or resume there re-fires SessionStart with the payload `cwd`
|
|
6
|
+
* inside the entered worktree; writing the new session's `session.lock` and
|
|
7
|
+
* `current-session.json` at the launch root blocked whoever works there for the
|
|
8
|
+
* lock's 4 h TTL. Two functions, one per side of the lifecycle:
|
|
9
|
+
*
|
|
10
|
+
* - {@link resolveSessionStartRoot} — the WRITER (SessionStart) decides by rule
|
|
11
|
+
* whether the session's state moves into the entered worktree, and records
|
|
12
|
+
* its answer as `session_root` in the host registry entry of its raw id;
|
|
13
|
+
* - {@link resolveOwnLockRoot} — the READERS (Stop, PostToolBatch, SessionEnd,
|
|
14
|
+
* the current-session.json side writers) never re-derive that rule: they
|
|
15
|
+
* follow the `session_root` pointer first — whatever the current `cwd` — and
|
|
16
|
+
* accept it only when the lock there carries their own raw `session_id`.
|
|
17
|
+
*
|
|
18
|
+
* Both answer the launch root unchanged whenever anything is in doubt.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { dotGitAncestor, linkedWorktreeSpot } from '../../scripts/lib/platform.mjs';
|
|
24
|
+
import { activeDir, isRegistryEntryFresh } from '../../scripts/lib/session-registry.mjs';
|
|
25
|
+
import { isLockLive, readLockDetailed } from '../../scripts/lib/session-lock.mjs';
|
|
26
|
+
import { parseSessionId } from '../../scripts/lib/session-id.mjs';
|
|
27
|
+
|
|
28
|
+
/** SessionStart sources that continue in the tree the session already works in. */
|
|
29
|
+
const RELOCATING_SOURCES = new Set(['clear', 'compact', 'resume']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The payload's own `session_id`, UUID-only, exactly as the lifecycle hooks
|
|
33
|
+
* derive their raw id — never a value read back from `current-session.json`,
|
|
34
|
+
* which would make every ownership compare below self-fulfilling.
|
|
35
|
+
*
|
|
36
|
+
* @param {object|null} input
|
|
37
|
+
* @returns {string|null}
|
|
38
|
+
*/
|
|
39
|
+
export function rawPayloadSessionId(input) {
|
|
40
|
+
const id = input?.session_id ?? input?.sessionId ?? null;
|
|
41
|
+
return parseSessionId(id)?.format === 'uuid' ? id : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `session_id` of the lock at `root`, `null` when absent; `undefined` when unreadable/corrupt. */
|
|
45
|
+
function lockIdAt(root) {
|
|
46
|
+
const detail = readLockDetailed({ repoRoot: root });
|
|
47
|
+
if (detail.status === 'absent') return null;
|
|
48
|
+
return detail.status === 'ok' ? detail.lock.session_id : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* JSON object at `file`: `{ status: 'absent' }` on ENOENT, `{ status: 'ok', value }`
|
|
53
|
+
* for an object, `{ status: 'unreadable' }` for every other read or parse failure.
|
|
54
|
+
* Callers that judge liveness treat 'unreadable' as LIVE (fail closed, #1605 B6).
|
|
55
|
+
*
|
|
56
|
+
* @param {string} file
|
|
57
|
+
* @returns {{status: 'absent'|'ok'|'unreadable', value?: object}}
|
|
58
|
+
*/
|
|
59
|
+
function readJsonState(file) {
|
|
60
|
+
let text;
|
|
61
|
+
try { text = readFileSync(file, 'utf8'); } catch (err) {
|
|
62
|
+
return { status: err?.code === 'ENOENT' ? 'absent' : 'unreadable' };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const value = JSON.parse(text);
|
|
66
|
+
return value !== null && typeof value === 'object' ? { status: 'ok', value } : { status: 'unreadable' };
|
|
67
|
+
} catch { return { status: 'unreadable' }; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The root the host registry entry of `ownId` points at (`session_root`), when
|
|
72
|
+
* the lock there carries `ownId` — else `null`. Independent of the current
|
|
73
|
+
* `cwd`: after `ExitWorktree` the payload `cwd` no longer names the worktree
|
|
74
|
+
* that holds the lock (#1605 B1), and a compact after a `cd` must not decide
|
|
75
|
+
* anew (B2/B3).
|
|
76
|
+
*
|
|
77
|
+
* With `liveAtMs`, the lock there must also be live by its own heartbeat and
|
|
78
|
+
* TTL (`isLockLive`) — the WRITER's test (#1630): a session whose process died
|
|
79
|
+
* without SessionEnd leaves entry and lock behind, and a `resume` hours later
|
|
80
|
+
* from another checkout was pulled back into that worktree past the target
|
|
81
|
+
* checks of {@link resolveSessionStartRoot}. A lock past its TTL is no claim
|
|
82
|
+
* to continue there; the caller then decides by rule. Only a `resume` passes
|
|
83
|
+
* `liveAtMs` — it is the one source that can come from a new process. A
|
|
84
|
+
* `compact` always comes from the running session, whose own stale lock after
|
|
85
|
+
* a long idle is a refresh, not a takeover (`lock-bootstrap.mjs`); dropping the
|
|
86
|
+
* pointer there orphaned the worktree lock (#1630 review). The READERS pass no
|
|
87
|
+
* `liveAtMs` either: a running session must still find its own stale lock to
|
|
88
|
+
* heartbeat and release it.
|
|
89
|
+
*
|
|
90
|
+
* Ceiling (BV-004): a process that dies without SessionEnd and is resumed from
|
|
91
|
+
* the main checkout WITHIN the lock's TTL still follows the pointer back into
|
|
92
|
+
* the worktree — the lock there is live and indistinguishable from a running
|
|
93
|
+
* session's. Revisit when such a case is reported.
|
|
94
|
+
*
|
|
95
|
+
* @param {string|null} ownId
|
|
96
|
+
* @param {number|null} [liveAtMs]
|
|
97
|
+
* @returns {string|null}
|
|
98
|
+
*/
|
|
99
|
+
function pointedOwnRoot(ownId, liveAtMs = null) {
|
|
100
|
+
if (ownId === null) return null;
|
|
101
|
+
const entry = readJsonState(path.join(activeDir(), `${ownId}.json`));
|
|
102
|
+
const root = entry.status === 'ok' ? entry.value.session_root : null;
|
|
103
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) return null;
|
|
104
|
+
if (liveAtMs === null) return lockIdAt(root) === ownId ? root : null;
|
|
105
|
+
const detail = readLockDetailed({ repoRoot: root });
|
|
106
|
+
return detail.status === 'ok' && detail.lock.session_id === ownId && isLockLive(detail.lock, liveAtMs)
|
|
107
|
+
? root : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Is `child` equal to or inside `parent`? Plain path compare — both sides are realpaths. */
|
|
111
|
+
function isSameOrInside(child, parent) {
|
|
112
|
+
const rel = path.relative(parent, child);
|
|
113
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Does a LIVE host registry entry other than `ownId` hold state in, or work in,
|
|
118
|
+
* the checkout `targetTop`? Reads `session_root` (where its lock lives) AND
|
|
119
|
+
* `work_root` (where it works) of every fresh entry — the second catches a
|
|
120
|
+
* session that entered `targetTop` by EnterWorktree while its lock stayed at its
|
|
121
|
+
* own launch root (#1605 B4). A root that merely CONTAINS `targetTop` (the main
|
|
122
|
+
* checkout around `.claude/worktrees/<name>`) is a different working copy and
|
|
123
|
+
* does not count. Fail closed: an unreadable registry directory or entry counts
|
|
124
|
+
* as a live claim (#1605 B6); only ENOENT means "nobody".
|
|
125
|
+
*
|
|
126
|
+
* @param {string} targetTop realpath of the target checkout's top
|
|
127
|
+
* @param {string|null} ownId
|
|
128
|
+
* @param {number} nowMs
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
function foreignRegistryClaim(targetTop, ownId, nowMs) {
|
|
132
|
+
let names;
|
|
133
|
+
try { names = readdirSync(activeDir()); } catch (err) { return err?.code !== 'ENOENT'; }
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
if (!name.endsWith('.json') || name === `${ownId}.json`) continue;
|
|
136
|
+
const entry = readJsonState(path.join(activeDir(), name));
|
|
137
|
+
if (entry.status === 'absent') continue;
|
|
138
|
+
if (entry.status === 'unreadable') return true;
|
|
139
|
+
const e = entry.value;
|
|
140
|
+
if (e.session_id === ownId || !isRegistryEntryFresh(e, { now: nowMs })) continue;
|
|
141
|
+
for (const root of [e.session_root, e.work_root]) {
|
|
142
|
+
if (typeof root === 'string' && path.isAbsolute(root) && isSameOrInside(root, targetTop)) return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** realpath of the checkout top holding `dir`, or '' when it has none or cannot be resolved. */
|
|
149
|
+
function checkoutTopOf(dir) {
|
|
150
|
+
if (typeof dir !== 'string' || !path.isAbsolute(dir)) return '';
|
|
151
|
+
const top = dotGitAncestor(dir);
|
|
152
|
+
if (top === '') return '';
|
|
153
|
+
try { return realpathSync.native(top); } catch { return ''; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* #1636 Pkt 2 / #1634 Pkt 1 — a MEASUREMENT, not a guard: does a fresh host
|
|
158
|
+
* registry entry other than `ownId` WORK in the checkout `repoRoot` (its
|
|
159
|
+
* `work_root` lies in it) while its lock lives in a DIFFERENT checkout (its
|
|
160
|
+
* `session_root` does not)? That is the session that left its worktree by
|
|
161
|
+
* ExitWorktree and works in the main checkout — `acquire()` cannot see it, so a
|
|
162
|
+
* new session there gets the lock with no conflict. Recorded on
|
|
163
|
+
* `orchestrator.session.lock.acquired` so the rate is countable before any
|
|
164
|
+
* banner is built on it (HR-101/HR-105).
|
|
165
|
+
*
|
|
166
|
+
* Three states, never two (an unreadable registry must not read as "nobody"):
|
|
167
|
+
* `true`, `false`, or `null` when the registry directory or an entry cannot be
|
|
168
|
+
* read (ENOENT of the directory is `false` — no registry, no claim).
|
|
169
|
+
* Spawn-free: `fs` reads only.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} repoRoot
|
|
172
|
+
* @param {string|null} ownId — this session's raw id (its own entry is skipped).
|
|
173
|
+
* @param {number} [nowMs]
|
|
174
|
+
* @returns {boolean|null}
|
|
175
|
+
*/
|
|
176
|
+
export function foreignWorkRootIn(repoRoot, ownId, nowMs = Date.now()) {
|
|
177
|
+
const targetTop = checkoutTopOf(repoRoot);
|
|
178
|
+
if (targetTop === '') return null;
|
|
179
|
+
let names;
|
|
180
|
+
try { names = readdirSync(activeDir()); } catch (err) { return err?.code === 'ENOENT' ? false : null; }
|
|
181
|
+
let unreadable = false;
|
|
182
|
+
for (const name of names) {
|
|
183
|
+
if (!name.endsWith('.json') || name === `${ownId}.json`) continue;
|
|
184
|
+
const entry = readJsonState(path.join(activeDir(), name));
|
|
185
|
+
if (entry.status === 'absent') continue;
|
|
186
|
+
if (entry.status === 'unreadable') { unreadable = true; continue; }
|
|
187
|
+
const e = entry.value;
|
|
188
|
+
if (e.session_id === ownId || !isRegistryEntryFresh(e, { now: nowMs })) continue;
|
|
189
|
+
if (checkoutTopOf(e.work_root) === targetTop && checkoutTopOf(e.session_root) !== targetTop) return true;
|
|
190
|
+
}
|
|
191
|
+
return unreadable ? null : false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Is session `id` live by the host registry? A non-UUID id cannot be looked up
|
|
196
|
+
* safely (it would become a path segment), so it counts as live — fail closed,
|
|
197
|
+
* like an entry that exists but cannot be read (#1605 B6).
|
|
198
|
+
*
|
|
199
|
+
* @param {string} id
|
|
200
|
+
* @param {number} nowMs
|
|
201
|
+
* @returns {boolean}
|
|
202
|
+
*/
|
|
203
|
+
function isRegisteredLive(id, nowMs) {
|
|
204
|
+
if (parseSessionId(id)?.format !== 'uuid') return true;
|
|
205
|
+
const entry = readJsonState(path.join(activeDir(), `${id}.json`));
|
|
206
|
+
if (entry.status === 'absent') return false;
|
|
207
|
+
return entry.status === 'unreadable' || isRegistryEntryFresh(entry.value, { now: nowMs });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The root SessionStart writes this session's own state into (#1605).
|
|
212
|
+
*
|
|
213
|
+
* First: when the registry entry of the raw payload id already points at a root
|
|
214
|
+
* whose LIVE lock carries that id, that root — a compact or resume continues
|
|
215
|
+
* where the session's lock is, whatever the current `cwd` (#1605 B2/B3). On a
|
|
216
|
+
* `resume` a pointed lock past its TTL is ignored (#1630) and the rule below
|
|
217
|
+
* decides; a `compact` follows its own pointer even to a stale lock. Ceiling:
|
|
218
|
+
* see {@link pointedOwnRoot} (resume within the TTL after a crash).
|
|
219
|
+
*
|
|
220
|
+
* Otherwise the entered worktree wins ONLY when ALL hold, else `launchRoot`:
|
|
221
|
+
* (a) the payload `source` continues a session (`clear`, `compact`, `resume`)
|
|
222
|
+
* — a `startup` belongs to the launch root;
|
|
223
|
+
* (b) the payload `cwd` sits in a back-pointer-confirmed linked worktree of the
|
|
224
|
+
* launch root's repository, other than the launch checkout
|
|
225
|
+
* (`linkedWorktreeSpot`);
|
|
226
|
+
* (c) the session does not already hold its lock at the launch root;
|
|
227
|
+
* (d) the target holds no live lock of ANOTHER session, no unreadable lock, no
|
|
228
|
+
* `current-session.json` naming another session that is live by the host
|
|
229
|
+
* registry, and no live registry entry of another session whose
|
|
230
|
+
* `session_root` or `work_root` lies in the target checkout (B4).
|
|
231
|
+
* Registry read errors other than ENOENT count as live (B6).
|
|
232
|
+
* A /clear's predecessor is released and deregistered by its SessionEnd(clear)
|
|
233
|
+
* first, so it does not count; when that SessionEnd failed, the predecessor
|
|
234
|
+
* still looks live and the session keeps the launch root — the pre-#1605
|
|
235
|
+
* answer (ceiling, BV-004; revisit when a /clear in a worktree reports a lock
|
|
236
|
+
* written to the launch root although its SessionEnd ran).
|
|
237
|
+
*
|
|
238
|
+
* Spawn-free: `fs` reads only.
|
|
239
|
+
*
|
|
240
|
+
* @param {object|null} input the parsed SessionStart payload
|
|
241
|
+
* @param {string} launchRoot `resolveProjectDir()`
|
|
242
|
+
* @param {number} [nowMs]
|
|
243
|
+
* @returns {Promise<string>}
|
|
244
|
+
*/
|
|
245
|
+
export async function resolveSessionStartRoot(input, launchRoot, nowMs = Date.now()) {
|
|
246
|
+
const ownId = rawPayloadSessionId(input);
|
|
247
|
+
const pointed = pointedOwnRoot(ownId, input?.source === 'resume' ? nowMs : null);
|
|
248
|
+
if (pointed !== null) return pointed;
|
|
249
|
+
if (!RELOCATING_SOURCES.has(input?.source)) return launchRoot;
|
|
250
|
+
// Without a raw payload id no reader (resolveOwnLockRoot) can ever find a
|
|
251
|
+
// relocated lock again — it would sit orphaned in the worktree for its TTL.
|
|
252
|
+
if (ownId === null) return launchRoot;
|
|
253
|
+
const spot = linkedWorktreeSpot(input?.cwd, launchRoot);
|
|
254
|
+
if (spot === '') return launchRoot;
|
|
255
|
+
if (ownId !== null && lockIdAt(launchRoot) === ownId) return launchRoot;
|
|
256
|
+
|
|
257
|
+
const detail = readLockDetailed({ repoRoot: spot });
|
|
258
|
+
if (detail.status === 'unreadable' || detail.status === 'corrupt') return launchRoot;
|
|
259
|
+
if (detail.status === 'ok' && detail.lock.session_id !== ownId && isLockLive(detail.lock, nowMs)) {
|
|
260
|
+
return launchRoot;
|
|
261
|
+
}
|
|
262
|
+
const current = readJsonState(path.join(spot, '.orchestrator', 'current-session.json'));
|
|
263
|
+
if (current.status === 'unreadable') return launchRoot;
|
|
264
|
+
const named = current.status === 'ok' ? current.value.session_id : null;
|
|
265
|
+
if (typeof named === 'string' && named !== '' && named !== ownId && isRegisteredLive(named, nowMs)) {
|
|
266
|
+
return launchRoot;
|
|
267
|
+
}
|
|
268
|
+
const top = dotGitAncestor(spot);
|
|
269
|
+
if (top === '') return launchRoot;
|
|
270
|
+
let targetTop;
|
|
271
|
+
try { targetTop = realpathSync.native(top); } catch { return launchRoot; }
|
|
272
|
+
if (foreignRegistryClaim(targetTop, ownId, nowMs)) return launchRoot;
|
|
273
|
+
return spot;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The root whose `.orchestrator/session.lock` carries THIS session's raw
|
|
278
|
+
* `session_id`: the registry pointer (`session_root`) first, then the launch
|
|
279
|
+
* root, then the entered worktree's spot (`linkedWorktreeSpot` of the payload
|
|
280
|
+
* `cwd`) — else `launchRoot` unchanged. For Stop, PostToolBatch, SessionEnd and
|
|
281
|
+
* the hooks that annotate `current-session.json`: they act where SessionStart
|
|
282
|
+
* actually put the lock, and never re-derive SessionStart's rule. Without a raw
|
|
283
|
+
* payload id ownership cannot be established, so the answer is the launch root.
|
|
284
|
+
*
|
|
285
|
+
* Ceiling (BV-004): a session that never got a lock at a relocated root (lost
|
|
286
|
+
* the acquire race) is read at the launch root. Revisit when a relocated
|
|
287
|
+
* `current-session.json` is found without a lock beside it.
|
|
288
|
+
*
|
|
289
|
+
* @param {object|null} input the parsed hook payload
|
|
290
|
+
* @param {string} launchRoot `getProjectDir()`
|
|
291
|
+
* @returns {string}
|
|
292
|
+
*/
|
|
293
|
+
export function resolveOwnLockRoot(input, launchRoot) {
|
|
294
|
+
const ownId = rawPayloadSessionId(input);
|
|
295
|
+
if (ownId === null) return launchRoot;
|
|
296
|
+
const pointed = pointedOwnRoot(ownId);
|
|
297
|
+
if (pointed !== null) return pointed;
|
|
298
|
+
if (lockIdAt(launchRoot) === ownId) return launchRoot;
|
|
299
|
+
const spot = linkedWorktreeSpot(input?.cwd, launchRoot);
|
|
300
|
+
return spot !== '' && lockIdAt(spot) === ownId ? spot : launchRoot;
|
|
301
|
+
}
|