session-orchestrator 5.11.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.
Files changed (394) hide show
  1. package/.claude-plugin/marketplace.json +12 -1
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +180 -3
  11. package/README.md +126 -117
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +4 -2
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +102 -25
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +55 -21
  47. package/docs/session-config-template.md +32 -24
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +158 -59
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -3
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +73 -3
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/remote-hosts.mjs +2 -2
  143. package/scripts/lib/config/section-extractor.mjs +65 -11
  144. package/scripts/lib/config/template-keys.mjs +153 -0
  145. package/scripts/lib/config/vault-integration.mjs +1 -0
  146. package/scripts/lib/config.mjs +3 -3
  147. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  148. package/scripts/lib/discovery/triage-state.mjs +2 -1
  149. package/scripts/lib/dispatcher/rank.mjs +5 -7
  150. package/scripts/lib/ecosystem-health.mjs +2 -2
  151. package/scripts/lib/eval/engine.mjs +3 -2
  152. package/scripts/lib/eval/session-resolve.mjs +6 -2
  153. package/scripts/lib/eval/sink.mjs +4 -3
  154. package/scripts/lib/events-retention-banner.mjs +3 -2
  155. package/scripts/lib/events-schema.mjs +10 -0
  156. package/scripts/lib/events.mjs +23 -12
  157. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  158. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  159. package/scripts/lib/fleet-protocol.mjs +21 -0
  160. package/scripts/lib/gates/gate-full.mjs +172 -11
  161. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  162. package/scripts/lib/git-config-drift.mjs +141 -7
  163. package/scripts/lib/github-export.mjs +447 -0
  164. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  165. package/scripts/lib/handover-queue-marks.mjs +159 -0
  166. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  167. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  168. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  169. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  170. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  171. package/scripts/lib/host-identity.mjs +9 -5
  172. package/scripts/lib/http-request.mjs +64 -0
  173. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  174. package/scripts/lib/io.mjs +6 -3
  175. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  176. package/scripts/lib/issue-budget.mjs +36 -7
  177. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  178. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  179. package/scripts/lib/learnings/io.mjs +289 -15
  180. package/scripts/lib/learnings/select.mjs +53 -4
  181. package/scripts/lib/learnings/store-role.mjs +209 -0
  182. package/scripts/lib/learnings/surface.mjs +46 -7
  183. package/scripts/lib/ledger-path.mjs +113 -0
  184. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  185. package/scripts/lib/memory-banner.mjs +5 -6
  186. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  187. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  188. package/scripts/lib/memory-proposals/store.mjs +22 -5
  189. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  190. package/scripts/lib/no-follow-open.mjs +57 -0
  191. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  192. package/scripts/lib/own-lock-root.mjs +95 -0
  193. package/scripts/lib/owner-interview.mjs +2 -1
  194. package/scripts/lib/owner-yaml.mjs +70 -7
  195. package/scripts/lib/platform.mjs +161 -9
  196. package/scripts/lib/plugin-update-banner.mjs +15 -11
  197. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  198. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  199. package/scripts/lib/process-group.mjs +270 -34
  200. package/scripts/lib/project-hygiene.mjs +28 -0
  201. package/scripts/lib/quality-gate.mjs +33 -9
  202. package/scripts/lib/reconcile/backlog.mjs +29 -15
  203. package/scripts/lib/reconcile/engine.mjs +26 -2
  204. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  205. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  206. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  207. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  208. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  209. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  210. package/scripts/lib/rule-loader.mjs +20 -10
  211. package/scripts/lib/scope-baseline.mjs +123 -8
  212. package/scripts/lib/scope-echo.mjs +5 -2
  213. package/scripts/lib/scope-gate.mjs +55 -20
  214. package/scripts/lib/session-close-backfill.mjs +75 -7
  215. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  216. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  217. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  218. package/scripts/lib/session-end/vault-orphans.mjs +205 -0
  219. package/scripts/lib/session-id.mjs +9 -1
  220. package/scripts/lib/session-identity/own-session.mjs +63 -0
  221. package/scripts/lib/session-invocation.mjs +14 -3
  222. package/scripts/lib/session-lock.mjs +114 -0
  223. package/scripts/lib/session-record-repair.mjs +245 -35
  224. package/scripts/lib/session-registry.mjs +85 -12
  225. package/scripts/lib/session-schema/constants.mjs +9 -0
  226. package/scripts/lib/session-schema/filters.mjs +56 -1
  227. package/scripts/lib/session-schema/validator.mjs +15 -5
  228. package/scripts/lib/session-start-probes.mjs +149 -22
  229. package/scripts/lib/session-token-rollup.mjs +2 -1
  230. package/scripts/lib/session-work-trees.mjs +166 -0
  231. package/scripts/lib/sessions-canonical.mjs +61 -3
  232. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  233. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  234. package/scripts/lib/sh-quote.mjs +1 -0
  235. package/scripts/lib/skill-evidence-window.mjs +5 -1
  236. package/scripts/lib/skill-health/join.mjs +10 -9
  237. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  238. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  239. package/scripts/lib/snapshot-probe.mjs +414 -0
  240. package/scripts/lib/so-hud/collect.mjs +174 -0
  241. package/scripts/lib/so-hud/runtime.mjs +87 -0
  242. package/scripts/lib/spiral-carryover.mjs +8 -1
  243. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  244. package/scripts/lib/state-md/idle-reset.mjs +248 -0
  245. package/scripts/lib/state-md/mission-status.mjs +5 -1
  246. package/scripts/lib/state-md/recommendations.mjs +26 -4
  247. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  248. package/scripts/lib/state-md.mjs +31 -8
  249. package/scripts/lib/sunset/walker.mjs +4 -3
  250. package/scripts/lib/telemetry/consent.mjs +4 -2
  251. package/scripts/lib/telemetry/queue.mjs +3 -3
  252. package/scripts/lib/telemetry/sync.mjs +45 -9
  253. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  254. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  255. package/scripts/lib/ux-grill/paths.mjs +5 -1
  256. package/scripts/lib/validate/check-agents.mjs +37 -6
  257. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  258. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  259. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  260. package/scripts/lib/validate/check-json-files.mjs +3 -1
  261. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  262. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  263. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  264. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  265. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  266. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  267. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  268. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  269. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  270. package/scripts/lib/validate/confidential-names.mjs +3 -1
  271. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  272. package/scripts/lib/vault-mirror/namespace.mjs +22 -9
  273. package/scripts/lib/vault-mirror/process.mjs +197 -65
  274. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  275. package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
  276. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  277. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  278. package/scripts/lib/vault-mirror/utils.mjs +106 -13
  279. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  280. package/scripts/lib/vault-status/board-writer.mjs +34 -5
  281. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  282. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  283. package/scripts/lib/win-process-tree.mjs +137 -0
  284. package/scripts/lib/workspace.mjs +1 -1
  285. package/scripts/lib/worktree/constants.mjs +1 -1
  286. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  287. package/scripts/lib/worktree/listing.mjs +1 -1
  288. package/scripts/lib/zx.mjs +50 -0
  289. package/scripts/mcp-server.mjs +16 -7
  290. package/scripts/mcp-server.sh +13 -83
  291. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  292. package/scripts/memory-propose.mjs +10 -3
  293. package/scripts/migrate-cold-start-seed.mjs +7 -0
  294. package/scripts/migrate-legacy-learnings.sh +22 -0
  295. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  296. package/scripts/migrate-vault-paths.mjs +29 -13
  297. package/scripts/parse-config.mjs +71 -14
  298. package/scripts/print-learnings-index.mjs +25 -2
  299. package/scripts/release-session-lock.mjs +94 -7
  300. package/scripts/release.mjs +451 -24
  301. package/scripts/relocate-vault-corpus.mjs +11 -7
  302. package/scripts/repair-invalid-sessions.mjs +68 -3
  303. package/scripts/resolve-session-invocation.mjs +57 -8
  304. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  305. package/scripts/run-quality-gate.mjs +33 -7
  306. package/scripts/self-update.mjs +56 -7
  307. package/scripts/session-commit-files.mjs +133 -0
  308. package/scripts/site-numbers.mjs +52 -9
  309. package/scripts/snapshot-probe.mjs +156 -0
  310. package/scripts/so-hud.mjs +68 -0
  311. package/scripts/state-md-idle-reset.mjs +82 -0
  312. package/scripts/sweep-expired-learnings.mjs +54 -6
  313. package/scripts/sweep-expired-rules.mjs +4 -1
  314. package/scripts/validate-plugin.mjs +22 -0
  315. package/scripts/vault-integration-watcher.mjs +54 -6
  316. package/scripts/vault-mirror.mjs +9 -0
  317. package/scripts/vault-orphans.mjs +171 -0
  318. package/skills/_shared/fleet-protocol.md +166 -3
  319. package/skills/_shared/instruction-file-resolution.md +1 -1
  320. package/skills/_shared/monitor-patterns.md +44 -25
  321. package/skills/_shared/platform-tools.md +7 -0
  322. package/skills/_shared/state-ownership.md +5 -4
  323. package/skills/architecture/references/domain-model.md +1 -1
  324. package/skills/autopilot/SKILL.md +3 -2
  325. package/skills/bootstrap/SKILL.md +3 -1
  326. package/skills/bootstrap/_shared-template.md +4 -4
  327. package/skills/bootstrap/fast-template.md +1 -1
  328. package/skills/bootstrap/private-contract.md +1 -1
  329. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  330. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  331. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  332. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  333. package/skills/claude-md-drift-check/SKILL.md +3 -3
  334. package/skills/claude-md-drift-check/checker.mjs +61 -101
  335. package/skills/close/SKILL.md +20 -7
  336. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  337. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  338. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  339. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  340. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  341. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  342. package/skills/discovery/probes-session.md +3 -0
  343. package/skills/discovery/probes-vault.md +2 -0
  344. package/skills/dispatcher/SKILL.md +1 -1
  345. package/skills/eval/SKILL.md +3 -1
  346. package/skills/evolve/SKILL.md +2 -0
  347. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  348. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  349. package/skills/gitlab-ops/SKILL.md +7 -6
  350. package/skills/gitlab-portfolio/SKILL.md +2 -2
  351. package/skills/go/SKILL.md +13 -1
  352. package/skills/npm-publish/SKILL.md +5 -2
  353. package/skills/plan/SKILL.md +23 -10
  354. package/skills/plan/mode-feature.md +13 -8
  355. package/skills/plan/mode-new.md +6 -14
  356. package/skills/plan/prd-feature-template.md +19 -0
  357. package/skills/plan/prd-full-template.md +12 -5
  358. package/skills/plan/prd-premortem-panel.md +48 -0
  359. package/skills/plan/prd-reviewer-prompt.md +11 -3
  360. package/skills/plan/soul.md +1 -1
  361. package/skills/portfolio/SKILL.md +5 -6
  362. package/skills/quality-gates/SKILL.md +2 -4
  363. package/skills/reconcile/SKILL.md +27 -4
  364. package/skills/release/SKILL.md +16 -8
  365. package/skills/session-end/SKILL.md +97 -13
  366. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  367. package/skills/session-end/phase-3-6-tail.md +11 -7
  368. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  369. package/skills/session-end/plan-verification.md +16 -1
  370. package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
  371. package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
  372. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  373. package/skills/session-end/references/session-summary-template.md +8 -0
  374. package/skills/session-end/session-metrics-write.md +7 -4
  375. package/skills/session-plan/SKILL.md +5 -3
  376. package/skills/session-start/SKILL.md +10 -4
  377. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  378. package/skills/session-start/phase-7-5-mode-selector.md +3 -1
  379. package/skills/session-start/phase-8-5-express-path.md +4 -3
  380. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  381. package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
  382. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  383. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  384. package/skills/tmux-layout/SKILL.md +2 -2
  385. package/skills/vault-sync/validator.mjs +3 -2
  386. package/skills/wave-executor/SKILL.md +5 -4
  387. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  388. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  389. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  390. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  391. package/AGENTS.md +0 -207
  392. package/assets/og-card.png +0 -0
  393. package/assets/og-card.svg +0 -47
  394. 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
- mkdirSync(dir, { recursive: true });
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
  }
@@ -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. Filesystem walk from CWD looking for marker dirs:
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
- * 4. Default: "claude"
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 is still unenforced, as before
425
- * #1492: its worktree sits under the launch checkout (measured: session
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`), so
428
- * the lift lands on the launch root, where that session has no manifest.
429
- * Fixing it needs a design (e.g. find the manifest bound to the payload
430
- * `session_id` among the repo's worktrees). Revisit when an entered-worktree
431
- * session dispatches `isolation: "worktree"` agents under a manifest.
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: README.md §"Upgrade" (`/plugin update ...`).
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
- * (README §"Upgrade": "...followed by the same install
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
- '(from a clone: `npm run update:local` also installs the deps Claude Code refuses — docs/install.md § Upgrade)',
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 third step — a filesystem walk from `cwd` for marker
302
- * directories — is left out on purpose: it needs `node:fs`/`node:path`
303
- * traversal this probe has no other reason to carry, and every non-Claude
304
- * harness already sets its own compat env var, so the walk would only ever
305
- * re-confirm the same `'claude'` default this function already falls back to.
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 `globalThis.fetch`.
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 : globalThis.fetch;
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 });