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
@@ -85,7 +85,7 @@
85
85
  * a size-ceiling check and a frontmatter-round-trip-safety check (both
86
86
  * documented at frontmatter-mutators.mjs:307-328) — a breach of either
87
87
  * REFUSES the write (leaves STATE.md untouched) and surfaces as
88
- * `{ written: false, reason: 'size-ceiling' | 'frontmatter-unsafe' }`
88
+ * `{ written: false, reason: 'size-ceiling' | 'frontmatter-unsafe' | 'foreign-state-md' }`
89
89
  * instead of silently bypassing the guard the way the old private
90
90
  * `writeStateMdAtomic()` helper did.
91
91
  *
@@ -119,12 +119,16 @@
119
119
  * No I/O at import time. Node 20+ stdlib only.
120
120
  */
121
121
 
122
- import { readFileSync } from 'node:fs';
122
+ import { readFileSync, realpathSync } from 'node:fs';
123
123
  import { execFileSync } from 'node:child_process';
124
+ import path from 'node:path';
124
125
 
125
- import { parseStateMd, serializeStateMd, resolveStateMdPath, writeStateMd } from './state-md.mjs';
126
+ import { parseStateMd, serializeStateMd, writeStateMd } from './state-md.mjs';
127
+ import { resolveStateMdWriteTarget } from './state-md/frontmatter-mutators.mjs';
126
128
  import { pathMatchesPattern } from './scope-gate.mjs';
127
129
  import { resolveBaselineRange, isQueryFailure } from './vcs-repo-spec.mjs';
130
+ import { readProcessLocalSessionIds } from './session-identity/own-session.mjs';
131
+ import { readSessionWorkTrees, sessionCommitFiles } from './session-work-trees.mjs';
128
132
 
129
133
  // ---------------------------------------------------------------------------
130
134
  // DRIFT_EXCLUDE_PATTERNS — the SINGLE filter source for both the denominator
@@ -271,6 +275,78 @@ function outputPaths(stdout) {
271
275
  // throws like any other failure and lands in the same skip.
272
276
  const DRIFT_GIT_TIMEOUT_MS = 10_000;
273
277
 
278
+ /** realpath, or the resolved spelling when the path cannot be resolved. */
279
+ function realOrResolved(p) {
280
+ try { return realpathSync(p); } catch { return path.resolve(p); }
281
+ }
282
+
283
+ /**
284
+ * Committed files from the session's OTHER own work trees (#1659).
285
+ *
286
+ * `session-start-ref` names one sha in the coordinator's own checkout; commits
287
+ * the same session made in its other worktrees are recorded as
288
+ * `orchestrator.session.work_tree` events (hooks/pre-bash-staging-fence.mjs).
289
+ * This reads them for the PROCESS-LOCAL raw session id
290
+ * (`readProcessLocalSessionIds`, never a repo-global file) and diffs each tree
291
+ * inside itself (`sessionCommitFiles`) — never across trees.
292
+ *
293
+ * Excluded before diffing: the own tree (realpath-equal to `ownTop`; the
294
+ * STATE.md ref stays authoritative there) and trees of a DIFFERENT repository
295
+ * (other `--git-common-dir`; their paths live in another path space). A tree
296
+ * whose common dir cannot be queried is passed through, so
297
+ * `sessionCommitFiles` names why it is unusable (`tree-missing`, …).
298
+ *
299
+ * Never throws: any failure yields `null` (no other-tree data), so the
300
+ * existing own-tree measurement is never turned into a skip by this source.
301
+ * That includes the own-tree identity queries (`--show-toplevel`,
302
+ * `--path-format=absolute` needs git >= 2.31): they run HERE, only once a
303
+ * session id exists, never inside computeDrift()'s numerator `try`.
304
+ *
305
+ * @param {{ repoRoot: string|undefined, gitOpts: object, env: object,
306
+ * eventsFile?: string }} args
307
+ * @returns {{ files: string[], counted: number, skipped: number, otherRepo: number,
308
+ * malformed: number, invalid: number } | null}
309
+ * `null` when no other own tree is recorded. `malformed` / `invalid` carry
310
+ * the reader's unreadable lines and own events without a usable tree/ref.
311
+ */
312
+ function otherWorkTreeFiles({ repoRoot, gitOpts, env, eventsFile }) {
313
+ try {
314
+ const [sessionId] = readProcessLocalSessionIds({ env });
315
+ if (!sessionId) return null;
316
+ const read = readSessionWorkTrees({ sessionId, repoRoot, eventsFile });
317
+ if (read.trees.length === 0) return null;
318
+ const ownTop = realOrResolved(execFileSync('git', ['rev-parse', '--show-toplevel'], gitOpts).trim());
319
+ const ownCommonDir = realOrResolved(
320
+ execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], gitOpts).trim(),
321
+ );
322
+ const others = read.trees.filter((t) => realOrResolved(t.tree) !== ownTop);
323
+ if (others.length === 0) return null;
324
+ const sameRepo = [];
325
+ let otherRepo = 0;
326
+ for (const t of others) {
327
+ let common = null;
328
+ try {
329
+ common = execFileSync('git', ['-C', t.tree, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
330
+ encoding: 'utf8', timeout: DRIFT_GIT_TIMEOUT_MS, stdio: ['ignore', 'pipe', 'ignore'],
331
+ }).trim();
332
+ } catch { /* unusable tree: sessionCommitFiles reports the reason */ }
333
+ if (common !== null && realOrResolved(common) !== ownCommonDir) { otherRepo += 1; continue; }
334
+ sameRepo.push(t);
335
+ }
336
+ const res = sessionCommitFiles({ trees: sameRepo });
337
+ return {
338
+ files: res.union,
339
+ counted: res.perTree.length,
340
+ skipped: res.skipped.length,
341
+ otherRepo,
342
+ malformed: read.malformed,
343
+ invalid: read.invalid,
344
+ };
345
+ } catch {
346
+ return null;
347
+ }
348
+ }
349
+
274
350
  /**
275
351
  * Internal: read + parse STATE.md's frontmatter exactly once. Shared by
276
352
  * `readBaseline()` and `computeDrift()` so a single `computeDrift()` call
@@ -280,7 +356,12 @@ const DRIFT_GIT_TIMEOUT_MS = 10_000;
280
356
  * @returns {{ ok: true, fm: object } | { ok: false, reason: 'no-state-md'|'unreadable-state-md' }}
281
357
  */
282
358
  function readFrontmatterOrReason(repoRoot) {
283
- const statePath = resolveStateMdPath(repoRoot);
359
+ // #1636: the write side's ownership rule — another platform's STATE.md not
360
+ // proven to be this process's would lend its baseline and session-start-ref
361
+ // to THIS session's drift tripwire. It counts as absent (`no-state-md`,
362
+ // silent like every skip here; writeBaseline() already refuses it).
363
+ const { path: statePath, rejected } = resolveStateMdWriteTarget(repoRoot);
364
+ if (rejected) return { ok: false, reason: 'no-state-md' };
284
365
  let raw;
285
366
  try {
286
367
  raw = readFileSync(statePath, 'utf8');
@@ -418,7 +499,7 @@ export function readBaseline(repoRoot) {
418
499
  * `filterExcluded()` pass at all). Anything that is not an array is
419
500
  * rejected UP FRONT — before STATE.md is read or the lock is taken —
420
501
  * as `{ written: false, reason: 'invalid-planned-files' }`.
421
- * @returns {Promise<{ written: boolean, reason?: 'invalid-planned-files'|'already-frozen'|'no-state-md'|'unreadable-state-md'|'lock-timeout'|'lock-fs-error'|'unexpected-error'|'size-ceiling'|'frontmatter-unsafe' }>}
502
+ * @returns {Promise<{ written: boolean, reason?: 'invalid-planned-files'|'already-frozen'|'no-state-md'|'unreadable-state-md'|'lock-timeout'|'lock-fs-error'|'unexpected-error'|'size-ceiling'|'frontmatter-unsafe'|'foreign-state-md' }>}
422
503
  */
423
504
  export async function writeBaseline({ repoRoot, intent, ownerBoundary, plannedFiles } = {}) {
424
505
  if (!Array.isArray(plannedFiles)) {
@@ -488,7 +569,8 @@ export async function writeBaseline({ repoRoot, intent, ownerBoundary, plannedFi
488
569
  // frontmatter-unsafe breach) — surface that reason rather than falling
489
570
  // back to the transformer's closure-captured reason, which was never
490
571
  // set on this path.
491
- if (result.reason === 'size-ceiling' || result.reason === 'frontmatter-unsafe') {
572
+ // `foreign-state-md` (#1640): the target belongs to another session.
573
+ if (result.reason === 'size-ceiling' || result.reason === 'frontmatter-unsafe' || result.reason === 'foreign-state-md') {
492
574
  return { written: false, reason: result.reason };
493
575
  }
494
576
  return { written: false, reason: skipReason ?? 'unreadable-state-md' };
@@ -600,14 +682,29 @@ export async function writeBaseline({ repoRoot, intent, ownerBoundary, plannedFi
600
682
  * @param {string|undefined} args.repoRoot
601
683
  * @param {number} [args.threshold] — breach threshold, `>=` counts as
602
684
  * breached (default `2.0`).
685
+ * @param {object} [args.env=process.env] — source of the PROCESS-LOCAL raw
686
+ * session id whose other work trees are added (#1659); injectable for tests.
687
+ * @param {string} [args.eventsFile] — explicit events ledger for the
688
+ * work-tree records (default: the repoRoot's ledger).
603
689
  * @returns {{ ok: true, skipped: true, reason: 'no-state-md'|'unreadable-state-md'|'no-baseline'|'stale-baseline'|'no-baseline-ref'|'unresolvable-ref' }
604
690
  * | { ok: true, skipped: false, filesRatio: number, plannedFiles: number,
605
- * actualFiles: number, breached: boolean, threshold: number, refUsed: string }}
691
+ * actualFiles: number, breached: boolean, threshold: number, refUsed: string,
692
+ * otherWorkTrees?: { counted: number, skipped: number, otherRepo: number,
693
+ * malformed: number, invalid: number } }}
606
694
  * Additive since #1039: `no-baseline-ref` is a NEW member of the skip-reason
607
695
  * union. No existing member was renamed or removed, and the measured-result
608
696
  * shape (all eight keys) is byte-identical to the pre-#1039 contract.
697
+ * Additive since #1659: the committed source also unions the files the
698
+ * session (process-local raw id) committed in its OTHER own work trees of
699
+ * the same repository, each diffed inside itself against its own recorded
700
+ * start ref (`orchestrator.session.work_tree`). `otherWorkTrees` appears
701
+ * only when such a tree is recorded: `counted` trees measured, `skipped`
702
+ * trees unusable (gone, ref unresolvable — they never fail the drift),
703
+ * `otherRepo` trees of a different repository (not comparable, ignored),
704
+ * `malformed` unreadable ledger lines and `invalid` own events without a
705
+ * usable tree/start_ref (a partial read, never a clean verdict).
609
706
  */
610
- export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
707
+ export function computeDrift({ repoRoot, threshold = 2.0, env = process.env, eventsFile } = {}) {
611
708
  const parsedFm = readFrontmatterOrReason(repoRoot);
612
709
  if (!parsedFm.ok) {
613
710
  return { ok: true, skipped: true, reason: parsedFm.reason };
@@ -683,6 +780,11 @@ export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
683
780
  return { ok: true, skipped: true, reason: 'unresolvable-ref' };
684
781
  }
685
782
 
783
+ // #1659: add what the session committed in its OTHER own work trees. The
784
+ // own tree is excluded inside (its STATE.md ref above stays authoritative).
785
+ const otherTrees = otherWorkTreeFiles({ repoRoot, gitOpts, env, eventsFile });
786
+ if (otherTrees !== null) for (const file of otherTrees.files) changedFiles.add(file);
787
+
686
788
  // Same `filterExcluded()` primitive `writeBaseline()`'s denominator
687
789
  // (`countPlannedFiles()`) calls (#894 review finding F1), so the pattern
688
790
  // filter on both sides of the ratio is one function. The untracked
@@ -710,5 +812,18 @@ export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
710
812
  breached,
711
813
  threshold,
712
814
  refUsed,
815
+ // Additive (#1659), present ONLY when another own work tree is recorded,
816
+ // so the eight-key shape above is unchanged for single-tree sessions.
817
+ ...(otherTrees !== null
818
+ ? {
819
+ otherWorkTrees: {
820
+ counted: otherTrees.counted,
821
+ skipped: otherTrees.skipped,
822
+ otherRepo: otherTrees.otherRepo,
823
+ malformed: otherTrees.malformed,
824
+ invalid: otherTrees.invalid,
825
+ },
826
+ }
827
+ : {}),
713
828
  };
714
829
  }
@@ -52,6 +52,7 @@ import { join, resolve } from 'node:path';
52
52
  import { digestSha256Short } from './crypto-digest-utils.mjs';
53
53
  import { isMainModule } from './is-main-module.mjs';
54
54
  import { isCoordinatorRecordId } from './scope-gate.mjs';
55
+ import { resolveEventsLedgerRoot } from './platform.mjs';
55
56
 
56
57
  /** Marker the agent must emit. Case-sensitive by design — a lowercase lookalike is not an echo. */
57
58
  export const SCOPE_ECHO_MARKER = 'SCOPE-DIGEST:';
@@ -420,7 +421,8 @@ export function digestScopeFiles(stateDir, wave, { readDir = readdirSync, readFi
420
421
  * @param {number} params.wave
421
422
  * @param {string} params.stateDir
422
423
  * @param {string} [params.session] semantic (or raw) session id to filter on
423
- * @param {string} [params.eventsPath] defaults to the cwd-relative `.orchestrator/metrics/events.jsonl`;
424
+ * @param {string} [params.eventsPath] defaults to `.orchestrator/metrics/events.jsonl` under
425
+ * `resolveEventsLedgerRoot(cwd)`;
424
426
  * the CLI passes `eventsFilePath(<state-dir>/..)` (the repo's one ledger, #1514)
425
427
  * @param {typeof readFileSync} [params.readFile]
426
428
  * @param {typeof readdirSync} [params.readDir]
@@ -437,7 +439,8 @@ export function verifyWaveScope({
437
439
  readFile = readFileSync,
438
440
  readDir = readdirSync,
439
441
  }) {
440
- const ledgerPath = eventsPath || join('.orchestrator', 'metrics', 'events.jsonl');
442
+ // #1514/#1722: the default maps a linked worktree to the main checkout's one ledger.
443
+ const ledgerPath = eventsPath || join(resolveEventsLedgerRoot(process.cwd()), '.orchestrator', 'metrics', 'events.jsonl');
441
444
  let records = [];
442
445
  let malformedLines = 0;
443
446
  try {
@@ -704,10 +704,12 @@ export function assertTestSiblingCoverage(fileScope, allowedPaths, opts = {}) {
704
704
  * module importing the barrel WOULD cycle).
705
705
  *
706
706
  * Two deltas the shared token stream does not carry, handled locally below:
707
- * - `(`/`)`: `tokenizeCommand` keeps them inside word text (they are not
708
- * control operators for its purpose). {@link peelSubshellParens} splits them
709
- * back out as separators so `(echo x > y)` still yields `y` and
710
- * `> >(cat)` still yields nothing.
707
+ * - `(`/`)`: since #1667 `tokenizeCommand` already emits a subshell's parens
708
+ * as `;` separators and hoists `$( … )` / backtick bodies as their own
709
+ * commands. What still reaches this pass glued is word text the lexer keeps
710
+ * on purpose — a process substitution word (`>(cat)`), `f()`, `arr=(…)`,
711
+ * case patterns. {@link peelSubshellParens} splits those so `> >(cat)`
712
+ * still yields nothing (the peeled `cat` lands in command-head position).
711
713
  * - here-doc BODY tokens arrive as ordinary quoted words. They are DATA, and a
712
714
  * body is multi-line, so the whitespace skip rule in
713
715
  * {@link shouldSkipWriteTarget} drops them. (The delimiter word itself now
@@ -906,13 +908,13 @@ const INPUT_REDIRECT_OP_RE = /^\d*(?:<<<|<)$/;
906
908
  * Split leading `(` / trailing `)` off an UNQUOTED word token into standalone
907
909
  * separator tokens.
908
910
  *
909
- * `tokenizeCommand` does not treat parentheses as control operators (it does not
910
- * need to), so `(echo x > y)` arrives as words `(echo` … `y)`. Without this the
911
- * subshell's redirect target would read as the literal `y)` and be dropped by the
912
- * paren skip rule in {@link shouldSkipWriteTarget} — a silent DETECTION LOSS
913
- * versus the pre-#970 lexer. Peeling restores it, and process substitution
914
- * `> >(cat)` still yields nothing because the peeled `cat` lands in command-head
915
- * position rather than redirect-target position.
911
+ * Since #1667 `tokenizeCommand` splits a word-start subshell `(` / `)` itself,
912
+ * so `(echo x > y)` arrives as `;` `echo` `x` `>` `y` `;`. What still arrives
913
+ * glued is paren text the lexer deliberately keeps inside a word — a process
914
+ * substitution word (`>(cat)`), `f()`, `arr=(…)`, case patterns, and openers
915
+ * past its nesting cap. Peeling keeps those from reading as a literal `y)`
916
+ * target, and `> >(cat)` still yields nothing because the peeled `cat` lands in
917
+ * command-head position rather than redirect-target position.
916
918
  *
917
919
  * Quoted tokens are never peeled: `echo '(' x` must keep its literal paren.
918
920
  *
@@ -2021,6 +2023,11 @@ const DENIED_ABSOLUTE_TOP_SEGMENTS = Object.freeze([
2021
2023
  'Library',
2022
2024
  'Applications',
2023
2025
  'Windows',
2026
+ // Windows machine-wide program and data roots (#1612, Opus review M3): the
2027
+ // drive-less spelling of C:/ProgramData/** and C:/Program Files/** lands here.
2028
+ 'ProgramData',
2029
+ 'Program Files',
2030
+ 'Program Files (x86)',
2024
2031
  ]);
2025
2032
 
2026
2033
  /**
@@ -2151,7 +2158,10 @@ const CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS = 4;
2151
2158
  * @returns {boolean}
2152
2159
  */
2153
2160
  function isSensitiveHomeSegment(segment) {
2154
- return segment.startsWith('.') || foldPath(segment) === 'library';
2161
+ // AppData is Windows' counterpart of ~/Library: gh/glab tokens, browser
2162
+ // profiles, credential caches (#1612, Opus review M3).
2163
+ const folded = foldPath(segment);
2164
+ return segment.startsWith('.') || folded === 'library' || folded === 'appdata';
2155
2165
  }
2156
2166
 
2157
2167
  /**
@@ -2267,14 +2277,24 @@ function classifyHomeGrant(normalizedEntry) {
2267
2277
  const cacheCarveout =
2268
2278
  foldPath(firstBelowHome) === CACHE_CARVEOUT_SEGMENT &&
2269
2279
  literal.length >= CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS;
2270
- if (!cacheCarveout && isSensitiveHomeSegment(firstBelowHome)) {
2280
+ // #1612 — Windows' os.tmpdir() is ~/AppData/Local/Temp, where worktrees and
2281
+ // fixtures live. Same DEPTH rule as ~/.cache: the Temp root stays refused,
2282
+ // a named directory below it is honoured.
2283
+ const below = HOME_MIN_LITERAL_SEGMENTS - 1;
2284
+ const windowsTempCarveout =
2285
+ foldPath(firstBelowHome) === 'appdata' &&
2286
+ literal.length >= below + 4 &&
2287
+ foldPath(literal[below + 1]) === 'local' &&
2288
+ foldPath(literal[below + 2]) === 'temp';
2289
+ if (!cacheCarveout && !windowsTempCarveout && isSensitiveHomeSegment(firstBelowHome)) {
2271
2290
  return {
2272
2291
  verdict: 'error',
2273
2292
  reason:
2274
- `grants a sensitive home subdirectory ("${firstBelowHome}") — dot-directories and Library ` +
2293
+ `grants a sensitive home subdirectory ("${firstBelowHome}") — dot-directories, Library and AppData ` +
2275
2294
  `hold credentials, tokens, owner.yaml and the agent guards themselves; scope a project ` +
2276
- `directory instead (the sole exception is ~/.cache/<project>/…, which must name at least ` +
2277
- `${CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS} literal segments — #1406)`,
2295
+ `directory instead (the exceptions are ~/.cache/<project>/…, which must name at least ` +
2296
+ `${CACHE_CARVEOUT_MIN_LITERAL_SEGMENTS} literal segments — #1406, and a named directory ` +
2297
+ `below Windows' temp root ~/AppData/Local/Temp — #1612)`,
2278
2298
  };
2279
2299
  }
2280
2300
 
@@ -2396,6 +2416,13 @@ export function canonicalizeGrantPrefix(absPath) {
2396
2416
  */
2397
2417
  export function gradeScopeEntry(entry, opts = {}) {
2398
2418
  if (typeof entry !== 'string' || entry.length === 0) return null;
2419
+ // Windows drive-absolute grants (#1612): every check below speaks POSIX
2420
+ // separators and Gate 5b matches the realpath candidate in its forward-slash
2421
+ // form ("C:/Users/..."). Graded verbatim, `C:\a\b\**` had no "/" prefix at
2422
+ // all ("/" resolves to "C:/") and a clean grant was reported non-canonical,
2423
+ // so scripts/validate-wave-scope.mjs refused it (measured on Windows 11).
2424
+ const windowsDrive = /^[A-Za-z]:[\\/]/.test(entry);
2425
+ if (windowsDrive) entry = entry.replace(/\\/g, '/');
2399
2426
  const bag = opts !== null && typeof opts === 'object' ? opts : {};
2400
2427
  const resolver = typeof bag.resolve === 'function' ? bag.resolve : null;
2401
2428
  /** @param {string} p @returns {string} */
@@ -2431,7 +2458,7 @@ export function gradeScopeEntry(entry, opts = {}) {
2431
2458
  };
2432
2459
  }
2433
2460
 
2434
- if (!path.isAbsolute(entry)) return null;
2461
+ if (!windowsDrive && !path.isAbsolute(entry)) return null;
2435
2462
 
2436
2463
  // `.` segments are collapsed BEFORE any classification (#1398): without it,
2437
2464
  // `/Users/./alice/.ssh/**` has a literal prefix whose third element is
@@ -2440,8 +2467,12 @@ export function gradeScopeEntry(entry, opts = {}) {
2440
2467
  // any entry containing `../`, whatever else it is.
2441
2468
  const normalized = path.posix.normalize(entry);
2442
2469
  const literalPrefix = literalGrantPrefix(normalized);
2470
+ // The system-directory and home-root rules are written for POSIX roots; a
2471
+ // drive-less spelling makes `C:/Users/<name>/**` as strict as `/Users/<name>/**`
2472
+ // (before #1612 a Windows home grant was never classified at all).
2473
+ const rootless = windowsDrive ? normalized.replace(/^[A-Za-z]:/, '') : normalized;
2443
2474
 
2444
- const deniedLiteral = deniedRootFor(literalPrefix, (p) => p);
2475
+ const deniedLiteral = deniedRootFor(literalGrantPrefix(rootless), (p) => p);
2445
2476
  if (deniedLiteral !== null) {
2446
2477
  return {
2447
2478
  verdict: 'error',
@@ -2452,7 +2483,7 @@ export function gradeScopeEntry(entry, opts = {}) {
2452
2483
  };
2453
2484
  }
2454
2485
 
2455
- const home = classifyHomeGrant(normalized);
2486
+ const home = classifyHomeGrant(rootless);
2456
2487
  if (home !== null) {
2457
2488
  return home.verdict === 'error'
2458
2489
  ? { verdict: 'error', code: 'home-grant', message: `${home.reason}: ${entry}` }
@@ -2481,7 +2512,11 @@ export function gradeScopeEntry(entry, opts = {}) {
2481
2512
  };
2482
2513
  }
2483
2514
 
2484
- if (canonicalPrefix !== literalPrefix) {
2515
+ // Windows: realpath answers in forward slashes with an upper-case drive letter.
2516
+ const comparable = (p) => (windowsDrive
2517
+ ? p.replace(/\\/g, '/').replace(/^([a-z]):/, (_, d) => `${d.toUpperCase()}:`)
2518
+ : p);
2519
+ if (comparable(canonicalPrefix) !== comparable(literalPrefix)) {
2485
2520
  return {
2486
2521
  verdict: 'error',
2487
2522
  code: 'non-canonical',
@@ -64,6 +64,7 @@ import { serializeSessionLineChecked as defaultSerialize } from './session-schem
64
64
  import { resolveStateMdPath as defaultResolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
65
65
  import { parseStateMd as defaultParseStateMd } from './state-md/yaml-parser.mjs';
66
66
  import { canonicalizeSessions } from './sessions-canonical.mjs';
67
+ import { explainEventsLedgerRoot, resolveEventsLedgerRoot } from './platform.mjs';
67
68
  // Already in the hook import set (`emitEvent` lives there): the rotation-aware
68
69
  // reader adds no new file to the SessionStart/SessionEnd hook graph.
69
70
  import { eventsFilePath, scanEventsBackwards } from './events.mjs'; // eventsFilePath: #1514, the repo's one ledger
@@ -137,6 +138,25 @@ export function isLoggedBackfillAction(action) {
137
138
  // Helpers
138
139
  // ---------------------------------------------------------------------------
139
140
 
141
+ /**
142
+ * A skip outcome when `repoRoot` sits in a LINKED worktree with no main
143
+ * checkout to map the ledger to (`git init --separate-git-dir`, a bare repo's
144
+ * worktree, stale worktree metadata): the resolver falls back to the worktree
145
+ * itself, and this writer would append into ITS `sessions.jsonl` again — the
146
+ * #1660 incident class. Says so on stderr in one line (#1666). `undefined`
147
+ * otherwise: write as before. The skip is a `skipped-*` action, so the
148
+ * breadcrumb log stays out of the worktree too.
149
+ *
150
+ * @param {string} repoRoot
151
+ * @returns {{ action: string, reason: string } | undefined}
152
+ */
153
+ function unresolvedLedgerSkip(repoRoot) {
154
+ const { resolved, reason } = explainEventsLedgerRoot(repoRoot);
155
+ if (resolved) return undefined;
156
+ process.stderr.write(`session-close-backfill: skipped, sessions ledger not written into the worktree: ${reason}\n`);
157
+ return { action: 'skipped-ledger-root-unresolved', reason };
158
+ }
159
+
140
160
  /**
141
161
  * True when `s` is an RFC 9562 UUID session id (any version 1-8, variant `10xx`).
142
162
  *
@@ -161,6 +181,19 @@ function markerName(id) {
161
181
  return `.backfilled-${cleaned}.marker`;
162
182
  }
163
183
 
184
+ /**
185
+ * Give a TOCTOU marker back after the ledger append failed (#1609): a marker
186
+ * without its record would keep the candidate from ever being backfilled.
187
+ * Best-effort — a failed unlink leaves the old behaviour, never throws.
188
+ */
189
+ function releaseMarker(markerPath) {
190
+ try {
191
+ fs.unlinkSync(markerPath);
192
+ } catch {
193
+ /* best-effort */
194
+ }
195
+ }
196
+
164
197
  /**
165
198
  * Read a JSONL file into an array of parsed objects. Missing file (ENOENT) →
166
199
  * [] silently; an unreadable one (EACCES/EISDIR/…) → [] with a stderr WARN
@@ -971,9 +1004,11 @@ export async function backfillAbandonedSession({
971
1004
  if (!sessionId && !semanticSessionId) {
972
1005
  return { action: 'skipped-no-identifier' };
973
1006
  }
1007
+ const unresolved = unresolvedLedgerSkip(repoRoot);
1008
+ if (unresolved) return unresolved;
974
1009
 
975
1010
  const eventsPath = eventsFilePath(repoRoot);
976
- const sessionsPath = path.join(repoRoot, ...SESSIONS_REL);
1011
+ const sessionsPath = path.join(resolveEventsLedgerRoot(repoRoot), ...SESSIONS_REL);
977
1012
 
978
1013
  // -- Resolve the SEMANTIC record id (sessions.jsonl key) ----------------
979
1014
  // Two-phase so the common SessionEnd path stays cheap: when the id is
@@ -1145,8 +1180,29 @@ export async function backfillAbandonedSession({
1145
1180
  };
1146
1181
  }
1147
1182
 
1148
- // -- TOCTOU marker — atomic create-or-fail keyed by the final id --------
1149
- const markerPath = path.join(repoRoot, '.orchestrator', 'metrics', markerName(recordId));
1183
+ // -- TOCTOU marker — atomic create-or-fail keyed by the CANDIDATE -------
1184
+ // #1592: the marker guards the ledger it sits beside — the MAIN
1185
+ // checkout's, shared by every worktree. A linked worktree has no
1186
+ // `.orchestrator/metrics/` since #1584, hence the mkdir.
1187
+ // #1609: keyed by the raw UUID when known, never by `recordId`: which id a
1188
+ // run records under (semantic, or the UUID when the semantic key is
1189
+ // occupied) depends on the ledger it read, so two racing runs of one
1190
+ // candidate claimed different markers and both appended. A legacy marker
1191
+ // (keyed by the record id) still counts as a lost claim, but only without a
1192
+ // key switch: once the semantic key is occupied and we record under our own
1193
+ // UUID, the semantic marker belongs to the session occupying that key.
1194
+ const markerDir = path.dirname(sessionsPath);
1195
+ const markerKey = isUuid(sessionId) ? sessionId : semanticRecordId;
1196
+ const markerPath = path.join(markerDir, markerName(markerKey));
1197
+ try {
1198
+ fs.mkdirSync(markerDir, { recursive: true });
1199
+ } catch {
1200
+ /* the openSync below reports the real failure */
1201
+ }
1202
+ if (recordId === semanticRecordId && recordId !== markerKey
1203
+ && fs.existsSync(path.join(markerDir, markerName(recordId)))) {
1204
+ return { action: 'skipped-marker-exists', sessionId: recordId };
1205
+ }
1150
1206
  try {
1151
1207
  const fd = openSync(markerPath, 'wx');
1152
1208
  closeSync(fd);
@@ -1162,6 +1218,7 @@ export async function backfillAbandonedSession({
1162
1218
  try {
1163
1219
  await appendJsonl(sessionsPath, validated);
1164
1220
  } catch (err) {
1221
+ releaseMarker(markerPath);
1165
1222
  return { action: 'error', error: `append: ${err?.message ?? String(err)}`, sessionId: recordId };
1166
1223
  }
1167
1224
  return {
@@ -1185,7 +1242,8 @@ export async function backfillAbandonedSession({
1185
1242
  }
1186
1243
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
1187
1244
  if (!isLoggedBackfillAction(res.action)) return;
1188
- const logPath = path.join(repoRoot, ...BACKFILL_LOG_REL);
1245
+ // #1592: one breadcrumb log beside the one ledger, not one per worktree.
1246
+ const logPath = path.join(resolveEventsLedgerRoot(repoRoot), ...BACKFILL_LOG_REL);
1189
1247
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
1190
1248
  fs.appendFileSync(
1191
1249
  logPath,
@@ -1298,6 +1356,8 @@ export async function backfillCompletedFromStateMd({
1298
1356
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
1299
1357
  return { action: 'error', error: 'repoRoot must be a non-empty string' };
1300
1358
  }
1359
+ const unresolved = unresolvedLedgerSkip(repoRoot);
1360
+ if (unresolved) return unresolved;
1301
1361
 
1302
1362
  // -- Read + parse STATE.md ------------------------------------------------
1303
1363
  const stateMdPath = resolveStateMdPath(repoRoot);
@@ -1334,7 +1394,7 @@ export async function backfillCompletedFromStateMd({
1334
1394
  // record (carrying `supersedes: <stub id>`) instead of skipping. An
1335
1395
  // authoritative record already on file still short-circuits exactly as
1336
1396
  // before.
1337
- const sessionsPath = path.join(repoRoot, ...SESSIONS_REL);
1397
+ const sessionsPath = path.join(resolveEventsLedgerRoot(repoRoot), ...SESSIONS_REL);
1338
1398
  const existing = classifyExisting(readFileSync, sessionsPath, {
1339
1399
  recordId,
1340
1400
  sessionId: nativeId,
@@ -1407,7 +1467,13 @@ export async function backfillCompletedFromStateMd({
1407
1467
 
1408
1468
  // -- TOCTOU marker — atomic create-or-fail, own namespace so a concurrent
1409
1469
  // abandoned-path claim for the same id can never collide with this one. --
1410
- const markerPath = path.join(repoRoot, '.orchestrator', 'metrics', markerName(`completed-${recordId}`));
1470
+ // #1592: beside the shared ledger it guards (see backfillAbandonedSession).
1471
+ const markerPath = path.join(path.dirname(sessionsPath), markerName(`completed-${recordId}`));
1472
+ try {
1473
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
1474
+ } catch {
1475
+ /* the openSync below reports the real failure */
1476
+ }
1411
1477
  try {
1412
1478
  const fd = openSync(markerPath, 'wx');
1413
1479
  closeSync(fd);
@@ -1422,6 +1488,7 @@ export async function backfillCompletedFromStateMd({
1422
1488
  try {
1423
1489
  await appendJsonl(sessionsPath, validated);
1424
1490
  } catch (err) {
1491
+ releaseMarker(markerPath);
1425
1492
  return { action: 'error', error: `append: ${err?.message ?? String(err)}`, sessionId: recordId };
1426
1493
  }
1427
1494
  return {
@@ -1449,7 +1516,8 @@ export async function backfillCompletedFromStateMd({
1449
1516
  }
1450
1517
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
1451
1518
  if (!isLoggedBackfillAction(res.action)) return;
1452
- const logPath = path.join(repoRoot, ...BACKFILL_LOG_REL);
1519
+ // #1592: one breadcrumb log beside the one ledger, not one per worktree.
1520
+ const logPath = path.join(resolveEventsLedgerRoot(repoRoot), ...BACKFILL_LOG_REL);
1453
1521
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
1454
1522
  fs.appendFileSync(
1455
1523
  logPath,