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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* own-lock-root.mjs — the checkout whose `.orchestrator/session.lock` carries a
|
|
3
|
+
* given RAW session id, for scripts that run outside a hook (#1730).
|
|
4
|
+
*
|
|
5
|
+
* A hook knows its session from the payload and asks `resolveOwnLockRoot`
|
|
6
|
+
* (`hooks/_lib/hook-root.mjs`). A script run by the coordinator does not: it
|
|
7
|
+
* gets a `cwd`, and the cwd is often the wrong tree. A session that works in a
|
|
8
|
+
* linked worktree by `cd` keeps its lock in the main checkout; one that took its
|
|
9
|
+
* lock in a worktree runs Bash from the main checkout, because the shell cwd
|
|
10
|
+
* resets there. Reading at `process.cwd()` then answers "no lock" (#1730 Pkt 1:
|
|
11
|
+
* `/close` released nothing and exited 0) or "no agents" (Pkt 2: the HUD).
|
|
12
|
+
*
|
|
13
|
+
* Candidates, in order; the first whose lock carries the id wins:
|
|
14
|
+
* 1. `registry` — the host registry entry's `session_root`, via the
|
|
15
|
+
* hook readers' own `resolveOwnLockRoot`;
|
|
16
|
+
* 2. `cwd` — the checkout holding `cwd`;
|
|
17
|
+
* 3. `main-checkout` — the main checkout of that repository;
|
|
18
|
+
* 4. `worktree` — a registered linked worktree of it (`registeredWorktreeRoots`).
|
|
19
|
+
* No candidate → `{ root: <checkout of cwd>, source: 'none' }`: the pre-#1730
|
|
20
|
+
* answer, so a caller that found nothing behaves as it did before.
|
|
21
|
+
*
|
|
22
|
+
* Only a lock that carries the id counts. A registry pointer, a STATE.md or a
|
|
23
|
+
* `current-session.json` alone proves nothing: they can name a peer.
|
|
24
|
+
*
|
|
25
|
+
* Spawn-free (fs reads only), never throws.
|
|
26
|
+
*
|
|
27
|
+
* Exports: resolveOwnLockRootFor, registrySessionRoot.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { resolveOwnLockRoot } from '../../hooks/_lib/hook-root.mjs';
|
|
34
|
+
import { dotGitAncestor, registeredWorktreeRoots, resolveEventsLedgerRoot } from './platform.mjs';
|
|
35
|
+
import { activeDir } from './session-registry.mjs';
|
|
36
|
+
import { readLockDetailed } from './session-lock.mjs';
|
|
37
|
+
import { parseSessionId } from './session-id.mjs';
|
|
38
|
+
|
|
39
|
+
/** @param {string} root @param {string} id */
|
|
40
|
+
function lockCarries(root, id) {
|
|
41
|
+
const detail = readLockDetailed({ repoRoot: root });
|
|
42
|
+
return detail.status === 'ok' && detail.lock.session_id === id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {string} p */
|
|
46
|
+
function canonical(p) {
|
|
47
|
+
try { return realpathSync.native(p); } catch { return path.resolve(p); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `session_root` of the host registry entry of `id`, or `null` when there is
|
|
52
|
+
* no entry, it cannot be read, or the value is no absolute path. A pointer
|
|
53
|
+
* only — the caller decides whether a lock there belongs to `id`.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} id raw session id (UUID)
|
|
56
|
+
* @returns {string|null}
|
|
57
|
+
*/
|
|
58
|
+
export function registrySessionRoot(id) {
|
|
59
|
+
if (parseSessionId(id)?.format !== 'uuid') return null;
|
|
60
|
+
try {
|
|
61
|
+
const entry = JSON.parse(readFileSync(path.join(activeDir(), `${id}.json`), 'utf8'));
|
|
62
|
+
const root = entry?.session_root;
|
|
63
|
+
return typeof root === 'string' && path.isAbsolute(root) ? root : null;
|
|
64
|
+
} catch { return null; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Where THIS session's lock is (see the module doc for the candidate order).
|
|
69
|
+
*
|
|
70
|
+
* @param {{ sessionId?: string|null, cwd?: string }} [opts]
|
|
71
|
+
* @returns {{ root: string, source: 'registry'|'cwd'|'main-checkout'|'worktree'|'none' }}
|
|
72
|
+
*/
|
|
73
|
+
export function resolveOwnLockRootFor({ sessionId = null, cwd = process.cwd() } = {}) {
|
|
74
|
+
const dir = path.resolve(typeof cwd === 'string' && cwd !== '' ? cwd : process.cwd());
|
|
75
|
+
const top = dotGitAncestor(dir) || dir;
|
|
76
|
+
if (parseSessionId(sessionId)?.format !== 'uuid') return { root: top, source: 'none' };
|
|
77
|
+
|
|
78
|
+
const viaHook = resolveOwnLockRoot({ session_id: sessionId, cwd: dir }, top);
|
|
79
|
+
if (lockCarries(viaHook, sessionId)) {
|
|
80
|
+
return { root: viaHook, source: canonical(viaHook) === canonical(top) ? 'cwd' : 'registry' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const main = resolveEventsLedgerRoot(top);
|
|
84
|
+
if (canonical(main) !== canonical(top) && lockCarries(main, sessionId)) {
|
|
85
|
+
return { root: main, source: 'main-checkout' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Sorted, so two linked worktrees that both carry the id (an anomaly: one
|
|
89
|
+
// raw session, two locks) always resolve to the same one.
|
|
90
|
+
const { roots } = registeredWorktreeRoots(canonical(path.join(main, '.git')));
|
|
91
|
+
for (const root of roots.sort()) {
|
|
92
|
+
if (canonical(root) !== canonical(top) && lockCarries(root, sessionId)) return { root, source: 'worktree' };
|
|
93
|
+
}
|
|
94
|
+
return { root: top, source: 'none' };
|
|
95
|
+
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { randomBytes } from 'node:crypto';
|
|
38
|
-
import { existsSync, copyFileSync } from 'node:fs';
|
|
38
|
+
import { existsSync, copyFileSync, chmodSync } from 'node:fs';
|
|
39
39
|
import {
|
|
40
40
|
resolveOwnerYamlPath,
|
|
41
41
|
validateOwnerConfig,
|
|
@@ -274,6 +274,7 @@ export function runOwnerInterview(opts = {}) {
|
|
|
274
274
|
const backupPath = `${filePath}.bak-${timestamp}`;
|
|
275
275
|
try {
|
|
276
276
|
copyFileSync(filePath, backupPath);
|
|
277
|
+
try { chmodSync(backupPath, 0o600); } catch { /* best effort */ } // copy keeps a loose source mode (#1632)
|
|
277
278
|
} catch {
|
|
278
279
|
// Non-fatal: continue even if backup fails (file might be unreadable)
|
|
279
280
|
}
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
* owner-leakage scanner's CP11 rule matches against tracked files.
|
|
34
34
|
* The names live in the referenced file ONLY — NEVER inline in
|
|
35
35
|
* owner.yaml. Never committed; env SO_CONFIDENTIAL_NAMES_FILE overrides.)
|
|
36
|
+
* projects-root: string ('' = no override; #1562 — host-local cross-repo confinement / scan
|
|
37
|
+
* root, default ~/Projects. A leading ~ expands to the home
|
|
38
|
+
* directory; a value that is not absolute after expansion is
|
|
39
|
+
* ignored with a WARN by getConfinementRoot (config/cross-repo.mjs).
|
|
40
|
+
* Never committed; env SO_PROJECTS_ROOT overrides.)
|
|
36
41
|
* dispatcher: (optional; host-local cross-repo dispatcher autonomy override — #679)
|
|
37
42
|
* autonomy: string ('' = no override; resolver enum off | advisory | autonomous-gated;
|
|
38
43
|
* precedence env SO_DISPATCHER_AUTONOMY > this > committed > off)
|
|
@@ -41,6 +46,14 @@
|
|
|
41
46
|
* mode: 'off' | 'warn' | 'strict' (applied only when weaker than the committed mode;
|
|
42
47
|
* precedence env SO_VAULT_INTEGRATION > this > committed.
|
|
43
48
|
* An invalid value drops ONLY this section, see loadOwnerConfig)
|
|
49
|
+
* learnings: (optional; host-local learnings-store role — #1516)
|
|
50
|
+
* store-role: 'writer' | 'inbox' (absent = writer; inbox = this host only appends
|
|
51
|
+
* to learnings-inbox.jsonl and never rewrites learnings.jsonl.
|
|
52
|
+
* precedence env SO_LEARNINGS_STORE_ROLE > this > writer.
|
|
53
|
+
* An empty `learnings:` is absent. An invalid value or any
|
|
54
|
+
* other key drops ONLY this section, see loadOwnerConfig;
|
|
55
|
+
* the store-role resolver then reads the role as invalid and
|
|
56
|
+
* canonical-store writes refuse — it never falls back to writer)
|
|
44
57
|
* drift-check: (optional; host-local path exceptions — #937)
|
|
45
58
|
* foreign-host-prefixes: string[] (narrow absolute directory prefixes;
|
|
46
59
|
* missing matching paths warn, unmatched paths error)
|
|
@@ -57,7 +70,7 @@
|
|
|
57
70
|
* validateOwnerSections(obj) — pure validation, no I/O; bucketed per section (#820)
|
|
58
71
|
* validateOwnerConfig(obj) — pure validation, no I/O; thin wrapper over validateOwnerSections
|
|
59
72
|
* loadOwnerConfig({path?}) — reads file; per-section tolerance for OPTIONAL
|
|
60
|
-
* sections (paths/dispatcher/vault-integration/drift-check/vaults/
|
|
73
|
+
* sections (paths/dispatcher/vault-integration/drift-check/learnings/vaults/
|
|
61
74
|
* baselines/vault-dirs) — #820
|
|
62
75
|
* writeOwnerConfig(config, {path?}) — validates, writes YAML, creates dir
|
|
63
76
|
* getDefaults() — returns sensible default config object
|
|
@@ -80,7 +93,7 @@
|
|
|
80
93
|
* WARN naming `npm install` as the fix.
|
|
81
94
|
*/
|
|
82
95
|
|
|
83
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
96
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
|
|
84
97
|
import { join, dirname } from 'node:path';
|
|
85
98
|
import { createRequire } from 'node:module';
|
|
86
99
|
import { resolvePrivateConfigDir } from './config/private-config-dir.mjs';
|
|
@@ -219,9 +232,10 @@ const REQUIRED_SECTIONS = /** @type {const} */ (['owner', 'tone', 'efficiency',
|
|
|
219
232
|
*
|
|
220
233
|
* `vault-integration` has no `getDefaults()` entry: absent means "no host
|
|
221
234
|
* override", so a dropped section simply leaves the committed gate in force.
|
|
235
|
+
* `learnings` (#1516) likewise has none: absent means the `writer` role.
|
|
222
236
|
*/
|
|
223
237
|
export const OPTIONAL_OBJECT_SECTIONS = /** @type {const} */ (
|
|
224
|
-
Object.freeze(['paths', 'dispatcher', 'vault-integration', 'drift-check'])
|
|
238
|
+
Object.freeze(['paths', 'dispatcher', 'vault-integration', 'drift-check', 'learnings'])
|
|
225
239
|
);
|
|
226
240
|
|
|
227
241
|
/**
|
|
@@ -235,6 +249,13 @@ const OPTIONAL_LIST_SECTIONS = /** @type {const} */ (['vaults', 'baselines', 'va
|
|
|
235
249
|
/** Host-settable vault-integration modes (mirrors VAULT_MODE_VALUES in config-schema.mjs). */
|
|
236
250
|
const VALID_VAULT_INTEGRATION_MODES = ['off', 'warn', 'strict'];
|
|
237
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Host-settable learnings-store roles (#1516). Mirrors STORE_ROLES in
|
|
254
|
+
* learnings/store-role.mjs — duplicated, not imported, because this module is
|
|
255
|
+
* on the hook import graph and must stay import-light.
|
|
256
|
+
*/
|
|
257
|
+
const VALID_LEARNINGS_STORE_ROLES = ['writer', 'inbox'];
|
|
258
|
+
|
|
238
259
|
/** Canonical validation-order — MUST match the pre-#820 inline validation order. */
|
|
239
260
|
const SECTION_ORDER = /** @type {const} */ ([
|
|
240
261
|
...REQUIRED_SECTIONS,
|
|
@@ -283,6 +304,7 @@ export function getDefaults() {
|
|
|
283
304
|
'baseline-path': '',
|
|
284
305
|
'namespace-map-path': '',
|
|
285
306
|
'confidential-names-file': '',
|
|
307
|
+
'projects-root': '',
|
|
286
308
|
},
|
|
287
309
|
dispatcher: {
|
|
288
310
|
autonomy: '',
|
|
@@ -435,7 +457,7 @@ export function validateOwnerSections(obj) {
|
|
|
435
457
|
if (!isPlainObject(paths)) {
|
|
436
458
|
errors.push('paths must be an object when present');
|
|
437
459
|
} else {
|
|
438
|
-
for (const key of ['vault-dir', 'baseline-path', 'namespace-map-path', 'confidential-names-file']) {
|
|
460
|
+
for (const key of ['vault-dir', 'baseline-path', 'namespace-map-path', 'confidential-names-file', 'projects-root']) {
|
|
439
461
|
const v = paths[key];
|
|
440
462
|
if (v !== undefined && v !== null && typeof v !== 'string') {
|
|
441
463
|
errors.push(`paths.${key} must be a string`);
|
|
@@ -497,6 +519,41 @@ export function validateOwnerSections(obj) {
|
|
|
497
519
|
sections['vault-integration'] = { valid: errors.length === 0, errors };
|
|
498
520
|
}
|
|
499
521
|
|
|
522
|
+
// ── learnings (optional; host-local store role — #1516) ─────────────────────
|
|
523
|
+
// STRICT enum like vault-integration: an invalid `learnings` section (typo
|
|
524
|
+
// `store-role: inbx`) is dropped here like any optional section. The store-
|
|
525
|
+
// role resolver (learnings/store-role.mjs) sees the key in the raw file and
|
|
526
|
+
// treats present-but-dropped as `invalid`, so canonical-store writes refuse
|
|
527
|
+
// instead of the host silently staying a writer that rewrites a synced store.
|
|
528
|
+
{
|
|
529
|
+
const errors = [];
|
|
530
|
+
const lr = obj.learnings;
|
|
531
|
+
if (lr !== undefined && lr !== null) {
|
|
532
|
+
if (!isPlainObject(lr)) {
|
|
533
|
+
errors.push('learnings must be an object when present');
|
|
534
|
+
} else {
|
|
535
|
+
// A misspelt key (`store_role: inbox`) must not read as "store-role
|
|
536
|
+
// absent" and fall back to writer (#1686): any key but `store-role`
|
|
537
|
+
// invalidates the section, so the resolver reports the role invalid.
|
|
538
|
+
for (const key of Object.keys(lr)) {
|
|
539
|
+
if (key !== 'store-role') {
|
|
540
|
+
errors.push(`learnings has unknown key ${JSON.stringify(key)} (only store-role is allowed)`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (
|
|
544
|
+
lr['store-role'] !== undefined &&
|
|
545
|
+
lr['store-role'] !== null &&
|
|
546
|
+
!VALID_LEARNINGS_STORE_ROLES.includes(lr['store-role'])
|
|
547
|
+
) {
|
|
548
|
+
errors.push(
|
|
549
|
+
`learnings.store-role must be one of ${VALID_LEARNINGS_STORE_ROLES.join(', ')}, got: ${JSON.stringify(lr['store-role'])}`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
sections.learnings = { valid: errors.length === 0, errors };
|
|
555
|
+
}
|
|
556
|
+
|
|
500
557
|
// ── vaults (optional; N named vaults for walk-up resolution — #700) ──────────
|
|
501
558
|
// Drop-and-WARN on malformed entries is handled by parseNamedVaults() in
|
|
502
559
|
// named-vault-resolver.mjs. Here we only validate the container shape to
|
|
@@ -657,7 +714,7 @@ export function validateOwnerConfig(obj) {
|
|
|
657
714
|
* Per-section tolerance (#820): an invalid REQUIRED section (owner, tone,
|
|
658
715
|
* efficiency, hardware-sharing) still discards the whole file (legacy
|
|
659
716
|
* behaviour, unchanged). An invalid OPTIONAL object section (paths,
|
|
660
|
-
* dispatcher, vault-integration, drift-check) is instead replaced by its default value — the rest of the
|
|
717
|
+
* dispatcher, vault-integration, drift-check, learnings) is instead replaced by its default value — the rest of the
|
|
661
718
|
* file survives, `source` becomes `'partial'`, and the drop is reported via
|
|
662
719
|
* `droppedSections` + a stderr WARN. OPTIONAL list sections (vaults,
|
|
663
720
|
* baselines, vault-dirs) are passed through UNTOUCHED even when strict-invalid — their
|
|
@@ -863,7 +920,10 @@ export function writeOwnerConfig(config, opts = {}) {
|
|
|
863
920
|
|
|
864
921
|
const dir = dirname(filePath);
|
|
865
922
|
try {
|
|
866
|
-
|
|
923
|
+
// 0o700 on every level this call creates: under umask 000 (WSL default) a
|
|
924
|
+
// bare mkdir left the private config dir 0777, and check-owner-leakage then
|
|
925
|
+
// refuses every owner policy beside it (#1629). An EXISTING dir keeps its mode.
|
|
926
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
867
927
|
} catch (err) {
|
|
868
928
|
return {
|
|
869
929
|
written: false,
|
|
@@ -891,7 +951,10 @@ export function writeOwnerConfig(config, opts = {}) {
|
|
|
891
951
|
}
|
|
892
952
|
|
|
893
953
|
try {
|
|
894
|
-
writeFileSync(filePath, yamlStr, 'utf8');
|
|
954
|
+
writeFileSync(filePath, yamlStr, { encoding: 'utf8', mode: 0o600 });
|
|
955
|
+
// mode only applies on creation; an owner.yaml left 0666 by an older
|
|
956
|
+
// writer would stay rewritable by other users (paths.*, salt) (#1632).
|
|
957
|
+
try { chmodSync(filePath, 0o600); } catch { /* best effort */ }
|
|
895
958
|
} catch (err) {
|
|
896
959
|
return { written: false, errors: [`failed to write owner.yaml: ${err.message}`] };
|
|
897
960
|
}
|
package/scripts/lib/platform.mjs
CHANGED
|
@@ -148,10 +148,47 @@ function walkUpFor(startDir, marker, kind) {
|
|
|
148
148
|
* 1. Trimmed allowlisted SO_PLATFORM explicit override
|
|
149
149
|
* 2. Compatibility env vars: CLAUDE_PLUGIN_ROOT → "claude", CODEX_PLUGIN_ROOT → "codex",
|
|
150
150
|
* CURSOR_RULES_DIR → "cursor", PI_PLUGIN_ROOT → "pi"
|
|
151
|
-
* 3.
|
|
151
|
+
* 3. Harness shell marker: CODEX_THREAD_ID → "codex", even when Claude Code
|
|
152
|
+
* markers are present too
|
|
153
|
+
* 4. Filesystem walk from CWD looking for marker dirs:
|
|
152
154
|
* .claude-plugin → "claude", .codex-plugin → "codex", .cursor/rules → "cursor",
|
|
153
155
|
* .pi → "pi"
|
|
154
|
-
*
|
|
156
|
+
* 5. Default: "claude"
|
|
157
|
+
*
|
|
158
|
+
* Step 3 exists because step 4 describes the REPO, not the harness: an agent
|
|
159
|
+
* Bash shell usually lacks the step-2 plugin-root vars, and a repo that ships
|
|
160
|
+
* both .claude-plugin and .codex-plugin resolved a Codex shell as "claude"
|
|
161
|
+
* (#1636). CODEX_THREAD_ID is set by Codex CLI for its exec_command shells
|
|
162
|
+
* (0.160.0 binary strings; ~/.codex/sessions transcripts of 2026-10 read it as
|
|
163
|
+
* their own thread id).
|
|
164
|
+
*
|
|
165
|
+
* Only Codex's marker decides, by design:
|
|
166
|
+
* - Claude Code's shell markers (CLAUDECODE, CLAUDE_CODE_ENTRYPOINT) do NOT:
|
|
167
|
+
* a harness started by hand from a Claude Code shell (e.g. cursor-agent or pi)
|
|
168
|
+
* inherits them and has no evidenced marker of its own, so it would read
|
|
169
|
+
* "claude" where the walk answers "cursor"/"pi". Foreign dispatch is immune:
|
|
170
|
+
* it passes only FOREIGN_ENV_ALLOWLIST (wave-executor/foreign-dispatch.mjs:119).
|
|
171
|
+
* Claude hooks still decide at step 2 via CLAUDE_PLUGIN_ROOT.
|
|
172
|
+
* - CLAUDE_CODE_SESSION_ID never decides: every nested harness inherits it (the
|
|
173
|
+
* #1636 trigger).
|
|
174
|
+
* - Cursor and pi have no evidenced shell marker (CURSOR_PLUGIN_ROOT is set by
|
|
175
|
+
* our own hook bridge, not by Cursor) and stay on steps 2/4.
|
|
176
|
+
*
|
|
177
|
+
* Named ceiling (BV-004): the dominant nesting here is Codex launched FROM a
|
|
178
|
+
* Claude Code shell (lead/headless runs), so CODEX_THREAD_ID wins over Claude
|
|
179
|
+
* markers. The reverse nesting — Claude, Cursor or pi launched from a Codex
|
|
180
|
+
* shell — inherits CODEX_THREAD_ID and reads "codex". For Claude that means
|
|
181
|
+
* agent-shell scripts cannot close its .claude/STATE.md via
|
|
182
|
+
* markStateMdCompletedOnDisk (WARN "session-id is not this process's",
|
|
183
|
+
* state-md/frontmatter-mutators.mjs:249), the /close pre-check
|
|
184
|
+
* (scripts/close-precheck.mjs run without --state-dir, resolving via
|
|
185
|
+
* resolveStateDir in scripts/lib/session-end/close-precheck.mjs) reads
|
|
186
|
+
* .codex/STATE.md, answers `no-state` and stops with "No active session
|
|
187
|
+
* found" instead of running session-end, and scripts/tmux-layout.mjs watches
|
|
188
|
+
* the wrong state dir. Its hooks are
|
|
189
|
+
* unaffected (step 2, CLAUDE_PLUGIN_ROOT). Revisit on a measured
|
|
190
|
+
* mis-detection of that kind, or when a harness exports a marker naming itself
|
|
191
|
+
* as the innermost one.
|
|
155
192
|
*
|
|
156
193
|
* @returns {"claude"|"codex"|"cursor"|"pi"}
|
|
157
194
|
*/
|
|
@@ -164,6 +201,10 @@ export function detectPlatform() {
|
|
|
164
201
|
if (_hasEnvValue('CURSOR_RULES_DIR')) return 'cursor';
|
|
165
202
|
if (_hasEnvValue('PI_PLUGIN_ROOT')) return 'pi';
|
|
166
203
|
|
|
204
|
+
// Step 3 — the one harness shell marker that may decide. See the JSDoc above
|
|
205
|
+
// for why Claude's own markers deliberately do not.
|
|
206
|
+
if (_hasEnvValue('CODEX_THREAD_ID')) return 'codex';
|
|
207
|
+
|
|
167
208
|
const cwd = process.cwd();
|
|
168
209
|
|
|
169
210
|
if (walkUpFor(cwd, '.claude-plugin', 'dir')) return 'claude';
|
|
@@ -421,14 +462,14 @@ function _isStrictlyInside(child, parent) {
|
|
|
421
462
|
* would let any directory that lacks one become the deciding root.
|
|
422
463
|
*
|
|
423
464
|
* Ceilings (BV-004):
|
|
424
|
-
* - An agent of a session that ENTERED a worktree
|
|
425
|
-
*
|
|
465
|
+
* - An agent of a session that ENTERED a worktree lands, via this lift, on the
|
|
466
|
+
* launch root, where that session has no manifest (measured: session
|
|
426
467
|
* 0d5e4fc3, 2026-09-25 — cwd `<main>/.claude/worktrees/landing-produktschau`,
|
|
427
|
-
* agent worktreePath `<main>/.claude/worktrees/agent-acd5bfd0aa590e2e8`)
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
468
|
+
* agent worktreePath `<main>/.claude/worktrees/agent-acd5bfd0aa590e2e8`).
|
|
469
|
+
* This function alone does not close that; `resolveScopeContext()` in
|
|
470
|
+
* `scope-gate.mjs` does (#1504, d99e6be1): for an isolated agent without a
|
|
471
|
+
* local manifest it searches the repo's registered worktrees
|
|
472
|
+
* (`registeredWorktreeRoots`) for the manifest bound to the own session id.
|
|
432
473
|
* - Rung 2 is `cd`-relocatable: a session whose `cwd` sits in ANOTHER repo (a
|
|
433
474
|
* submodule, a nested or sibling checkout) is checked against that repo's
|
|
434
475
|
* manifest — usually none, so its gates allow — where the launch dir's
|
|
@@ -527,6 +568,51 @@ export function registeredWorktreeRoots(commonDir) {
|
|
|
527
568
|
finally { if (dir) dir.closeSync(); }
|
|
528
569
|
}
|
|
529
570
|
|
|
571
|
+
/**
|
|
572
|
+
* The spot in ANOTHER working copy of the launch root's repository that matches
|
|
573
|
+
* the launch root — `<cwd's linked worktree>/<launchRoot relative to its own
|
|
574
|
+
* checkout>` — or `''` (#1605). The one candidate a lifecycle hook may move a
|
|
575
|
+
* session's own state (`session.lock`, `current-session.json`) to after
|
|
576
|
+
* `EnterWorktree`, where `$CLAUDE_PROJECT_DIR` still names the launch checkout.
|
|
577
|
+
*
|
|
578
|
+
* Non-empty ONLY when ALL hold:
|
|
579
|
+
* - `cwd` is absolute and sits in a LINKED worktree whose `.git` pointer is
|
|
580
|
+
* confirmed by git's back-pointer (`worktreeBinding`), so a copied or forged
|
|
581
|
+
* `.git` file (`gitdir: <main>/.git/worktrees/<name>`) lends no authority;
|
|
582
|
+
* - that worktree is not a harness subagent worktree (`agent-<hex>`, the shape
|
|
583
|
+
* `resolveSessionRoot` lifts) and not the launch root's own checkout;
|
|
584
|
+
* - its common dir is the launch checkout's (its own `.git` directory for a
|
|
585
|
+
* main checkout, the verified binding for a linked one) — same repository;
|
|
586
|
+
* - the mapped spot exists as a directory.
|
|
587
|
+
* A main checkout is never an answer: its `.git` is a directory, which
|
|
588
|
+
* `worktreeBinding` rejects. Spawn-free; never throws.
|
|
589
|
+
*
|
|
590
|
+
* Ceiling (BV-004): a launch in a subdirectory the worktree lacks (untracked in
|
|
591
|
+
* the launch checkout) answers `''`, so that session keeps the launch root.
|
|
592
|
+
* Revisit when a session launched in such a subdirectory reports #1605.
|
|
593
|
+
*
|
|
594
|
+
* @param {string|undefined} cwd the hook payload's `cwd`
|
|
595
|
+
* @param {string} launchRoot the launch root (`resolveProjectDir()`)
|
|
596
|
+
* @returns {string} Absolute path, or `''`
|
|
597
|
+
*/
|
|
598
|
+
export function linkedWorktreeSpot(cwd, launchRoot) {
|
|
599
|
+
if (typeof cwd !== 'string' || cwd.trim() === '' || !path.isAbsolute(cwd)) return '';
|
|
600
|
+
if (typeof launchRoot !== 'string' || launchRoot === '') return '';
|
|
601
|
+
const cwdCopy = dotGitAncestor(cwd);
|
|
602
|
+
const launchCopy = dotGitAncestor(launchRoot);
|
|
603
|
+
if (!cwdCopy || !launchCopy || _liftHarnessAgentWorktree(cwdCopy) !== cwdCopy) return '';
|
|
604
|
+
try {
|
|
605
|
+
const target = worktreeBinding(cwdCopy);
|
|
606
|
+
if (_canonical(cwdCopy) === _canonical(launchCopy)) return '';
|
|
607
|
+
const launchDotGit = path.join(launchCopy, '.git');
|
|
608
|
+
const launchCommon = lstatSync(launchDotGit).isDirectory()
|
|
609
|
+
? _canonical(launchDotGit) : worktreeBinding(launchCopy).commonDir;
|
|
610
|
+
if (target.commonDir !== launchCommon) return '';
|
|
611
|
+
const spot = path.join(cwdCopy, path.relative(launchCopy, launchRoot));
|
|
612
|
+
return statSync(spot).isDirectory() ? spot : '';
|
|
613
|
+
} catch { return ''; }
|
|
614
|
+
}
|
|
615
|
+
|
|
530
616
|
// ---------------------------------------------------------------------------
|
|
531
617
|
// resolveEventsLedgerRoot (#1514 point 1)
|
|
532
618
|
// ---------------------------------------------------------------------------
|
|
@@ -611,6 +697,18 @@ function _inMainCheckout(dir) {
|
|
|
611
697
|
* `repoRoot` given: the same mapping applied to that root — for a caller that
|
|
612
698
|
* holds a repo root and wants THE ledger of that repo.
|
|
613
699
|
*
|
|
700
|
+
* Fallback to the unmapped root (decided on #1592, behaviour unchanged): it is
|
|
701
|
+
* the right answer for a plain checkout (it IS the main one), a non-git
|
|
702
|
+
* directory, a submodule and a bare repo's worktree (no main checkout exists).
|
|
703
|
+
* The one lossy case is a linked worktree whose git metadata is unreadable or
|
|
704
|
+
* stale (main checkout moved/deleted before `git worktree repair`): its ledger
|
|
705
|
+
* then stays local. Not fail-closed on purpose: this runs on every hook event,
|
|
706
|
+
* and every writer and reader resolves through this same function, so both
|
|
707
|
+
* sides still agree on one file — no split. Ceiling: such a worktree's records
|
|
708
|
+
* stay out of the main ledger until merged by hand. Revisit when a
|
|
709
|
+
* `.orchestrator/metrics/sessions.jsonl` inside a linked worktree is found
|
|
710
|
+
* growing after #1584.
|
|
711
|
+
*
|
|
614
712
|
* @param {string} [repoRoot] an explicit root to map (skips the env/cwd rungs)
|
|
615
713
|
* @param {string} [cwd=process.cwd()]
|
|
616
714
|
* @returns {string} Absolute path
|
|
@@ -630,6 +728,60 @@ export function resolveEventsLedgerRoot(repoRoot, cwd = process.cwd()) {
|
|
|
630
728
|
return main || getProjectDir();
|
|
631
729
|
}
|
|
632
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Why the linked worktree rooted at `root` has NO main checkout to map to, or
|
|
733
|
+
* `''` when it is no linked worktree or maps fine (#1666). A linked worktree
|
|
734
|
+
* is recognised by its `.git` FILE pointing at a gitdir that carries a
|
|
735
|
+
* `commondir` file — a submodule's gitdir has none, so a submodule stays `''`
|
|
736
|
+
* (it is its own repo, its root IS the right ledger root). Spawn-free, never
|
|
737
|
+
* throws.
|
|
738
|
+
*
|
|
739
|
+
* @param {string} root a directory holding a `.git` entry
|
|
740
|
+
* @returns {string}
|
|
741
|
+
*/
|
|
742
|
+
function _unmappedLinkedWorktreeReason(root) {
|
|
743
|
+
if (_mainCheckoutOfLinkedWorktree(root)) return '';
|
|
744
|
+
try {
|
|
745
|
+
const dotGit = path.join(root, '.git');
|
|
746
|
+
if (!statSync(dotGit).isFile()) return '';
|
|
747
|
+
const match = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(dotGit, 'utf8'));
|
|
748
|
+
if (!match) return '';
|
|
749
|
+
const gitDir = path.resolve(root, match[1]);
|
|
750
|
+
if (!existsSync(gitDir)) return `linked worktree git metadata unreadable (${gitDir} missing)`;
|
|
751
|
+
if (!existsSync(path.join(gitDir, 'commondir'))) return ''; // a submodule
|
|
752
|
+
return 'linked worktree whose common git dir is not named .git (separate-git-dir or bare repo): no main checkout';
|
|
753
|
+
} catch {
|
|
754
|
+
return '';
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* {@link resolveEventsLedgerRoot} plus whether it reached a MAIN checkout (#1666).
|
|
760
|
+
*
|
|
761
|
+
* `resolved: false` only when the directory the resolver mapped sits in a
|
|
762
|
+
* LINKED git worktree that has no main checkout to map to (`git init
|
|
763
|
+
* --separate-git-dir`, a bare repo's worktree, stale worktree metadata) — the
|
|
764
|
+
* resolver then falls back to the worktree itself. A plain checkout, a
|
|
765
|
+
* submodule and a non-git directory report `resolved: true`: their root is the
|
|
766
|
+
* right ledger root. Readers keep using {@link resolveEventsLedgerRoot}; a
|
|
767
|
+
* WRITER of a tracked-by-accident ledger (the session-close backfill, #1660)
|
|
768
|
+
* consults this and refuses to write into the worktree.
|
|
769
|
+
*
|
|
770
|
+
* @param {string} [repoRoot] same as {@link resolveEventsLedgerRoot}
|
|
771
|
+
* @param {string} [cwd=process.cwd()]
|
|
772
|
+
* @returns {{ root: string, resolved: boolean, reason: string }}
|
|
773
|
+
*/
|
|
774
|
+
export function explainEventsLedgerRoot(repoRoot, cwd = process.cwd()) {
|
|
775
|
+
const root = resolveEventsLedgerRoot(repoRoot, cwd);
|
|
776
|
+
let mapped;
|
|
777
|
+
if (typeof repoRoot === 'string' && repoRoot.trim() !== '') mapped = path.resolve(repoRoot);
|
|
778
|
+
else if (launchDirFromEnv() !== '') mapped = path.resolve(launchDirFromEnv());
|
|
779
|
+
else mapped = path.resolve(cwd);
|
|
780
|
+
const ancestor = dotGitAncestor(mapped);
|
|
781
|
+
const reason = ancestor ? _unmappedLinkedWorktreeReason(ancestor) : '';
|
|
782
|
+
return { root, resolved: reason === '', reason };
|
|
783
|
+
}
|
|
784
|
+
|
|
633
785
|
// ---------------------------------------------------------------------------
|
|
634
786
|
// resolveStateDir
|
|
635
787
|
// ---------------------------------------------------------------------------
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* @module scripts/lib/plugin-update-banner
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
+
import { httpRequest } from './http-request.mjs';
|
|
42
43
|
import { readFileSync } from 'node:fs';
|
|
43
44
|
import { join, resolve } from 'node:path';
|
|
44
45
|
|
|
@@ -207,7 +208,7 @@ async function resolveLatestVersion({ cacheDir, now, fetchImpl }) {
|
|
|
207
208
|
// either returns or assigns, so a `= null` seed would be dead (no-useless-assignment).
|
|
208
209
|
let version;
|
|
209
210
|
try {
|
|
210
|
-
const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
211
|
+
const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), timeoutMs: FETCH_TIMEOUT_MS });
|
|
211
212
|
if (!res || res.ok !== true) return null;
|
|
212
213
|
const body = await res.json();
|
|
213
214
|
if (typeof body?.version !== 'string' || body.version.length === 0) return null;
|
|
@@ -235,13 +236,13 @@ async function resolveLatestVersion({ cacheDir, now, fetchImpl }) {
|
|
|
235
236
|
* The four platforms this probe has a SOURCED update recipe for. Each string
|
|
236
237
|
* is quoted from the doc that actually documents it, never invented:
|
|
237
238
|
*
|
|
238
|
-
* - claude:
|
|
239
|
+
* - claude: docs/install.md §"Upgrade" (`/plugin update ...`).
|
|
239
240
|
* - codex: docs/codex-setup.md §"Refresh and Explicit Cache Invalidation" —
|
|
240
241
|
* the short-form marketplace install path, labelled Recommended
|
|
241
242
|
* there (`codex plugin marketplace upgrade` + `codex plugin add`).
|
|
242
243
|
* - cursor: README.md's per-platform install row + docs/cursor-setup.md —
|
|
243
244
|
* re-running the same install script is the documented recipe
|
|
244
|
-
* (
|
|
245
|
+
* (docs/install.md §"Upgrade": "...followed by the same install
|
|
245
246
|
* script you originally ran").
|
|
246
247
|
* - pi: the PRIMARY install path is `pi install npm:session-orchestrator`
|
|
247
248
|
* (docs/pi-setup.md §"Option 1", `docs/pi-setup.md:16`), not the
|
|
@@ -262,7 +263,7 @@ async function resolveLatestVersion({ cacheDir, now, fetchImpl }) {
|
|
|
262
263
|
const PLATFORM_UPDATE_INSTRUCTIONS = {
|
|
263
264
|
claude:
|
|
264
265
|
'/plugin update session-orchestrator@kanevry, then restart Claude Code ' +
|
|
265
|
-
'(
|
|
266
|
+
'(also installed in Codex? run `npm run update:local` from a clone — the Codex copy stays old otherwise, and it installs the deps Claude Code refuses — docs/install.md § Upgrade)',
|
|
266
267
|
codex:
|
|
267
268
|
'run `codex plugin marketplace upgrade kanevry && codex plugin add session-orchestrator@kanevry` ' +
|
|
268
269
|
'(docs/codex-setup.md), then restart Codex',
|
|
@@ -298,11 +299,14 @@ const GENERIC_UPDATE_INSTRUCTION = 'run `npm update -g session-orchestrator`';
|
|
|
298
299
|
* tracks its closure — adding platform.mjs as a dependency here is a
|
|
299
300
|
* wave-scope change, not a two-line addition, so the fallback stays local
|
|
300
301
|
* exactly like `isTruthyFlag` above (same rationale, same precedent).
|
|
301
|
-
* `detectPlatform()`'s
|
|
302
|
-
* directories
|
|
303
|
-
* traversal this probe has no other reason to
|
|
304
|
-
* harness already sets its own compat env var, so
|
|
305
|
-
* re-confirm the
|
|
302
|
+
* `detectPlatform()`'s step 3 (`CODEX_THREAD_ID`) and step 4 (a filesystem
|
|
303
|
+
* walk from `cwd` for marker directories) are left out on purpose. The walk
|
|
304
|
+
* needs `node:fs`/`node:path` traversal this probe has no other reason to
|
|
305
|
+
* carry, and every non-Claude harness already sets its own compat env var, so
|
|
306
|
+
* it would only re-confirm the `'claude'` default this function falls back to.
|
|
307
|
+
* Skipping step 3 is harmless: the only caller, `hooks/on-session-start.mjs`
|
|
308
|
+
* (`checkPluginUpdate` call ~:1232), passes the platform its own full
|
|
309
|
+
* `detectPlatform()` run resolved, so this fallback does not run there.
|
|
306
310
|
*
|
|
307
311
|
* @param {Record<string,string|undefined>} env
|
|
308
312
|
* @returns {"claude"|"codex"|"cursor"|"pi"}
|
|
@@ -344,7 +348,7 @@ function resolvePlatformFromEnv(env) {
|
|
|
344
348
|
* without a cache this probe would issue one npm request per session start.
|
|
345
349
|
* @param {Record<string,string|undefined>} [opts.env] Defaults to `process.env`.
|
|
346
350
|
* @param {number} [opts.now] Injected clock (ms). Defaults to `Date.now()`.
|
|
347
|
-
* @param {Function} [opts.fetchImpl] Injected fetch. Defaults to
|
|
351
|
+
* @param {Function} [opts.fetchImpl] Injected fetch. Defaults to the Stdlib HTTP helper.
|
|
348
352
|
* @param {"claude"|"codex"|"cursor"|"pi"} [opts.platform] The harness driving
|
|
349
353
|
* this session, when the caller already knows it (the SessionStart hook
|
|
350
354
|
* computes this once and could pass it straight through). Falls back to
|
|
@@ -370,7 +374,7 @@ export async function checkPluginUpdate({
|
|
|
370
374
|
const installedParts = parseSemver(installed);
|
|
371
375
|
if (installedParts === null) return null;
|
|
372
376
|
|
|
373
|
-
const fetcher = typeof fetchImpl === 'function' ? fetchImpl :
|
|
377
|
+
const fetcher = typeof fetchImpl === 'function' ? fetchImpl : httpRequest;
|
|
374
378
|
if (typeof fetcher !== 'function') return null;
|
|
375
379
|
|
|
376
380
|
const latest = await resolveLatestVersion({ cacheDir, now, fetchImpl: fetcher });
|