session-orchestrator 5.12.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  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 +162 -4
  11. package/README.md +126 -127
  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 +1 -1
  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 +90 -17
  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 +49 -15
  47. package/docs/session-config-template.md +30 -22
  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 +139 -49
  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 -2
  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 +39 -27
  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/section-extractor.mjs +65 -11
  143. package/scripts/lib/config/template-keys.mjs +153 -0
  144. package/scripts/lib/config/vault-integration.mjs +1 -0
  145. package/scripts/lib/config.mjs +2 -2
  146. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  147. package/scripts/lib/discovery/triage-state.mjs +2 -1
  148. package/scripts/lib/dispatcher/rank.mjs +5 -7
  149. package/scripts/lib/ecosystem-health.mjs +2 -2
  150. package/scripts/lib/eval/engine.mjs +3 -2
  151. package/scripts/lib/eval/session-resolve.mjs +6 -2
  152. package/scripts/lib/eval/sink.mjs +4 -3
  153. package/scripts/lib/events-retention-banner.mjs +3 -2
  154. package/scripts/lib/events-schema.mjs +10 -0
  155. package/scripts/lib/events.mjs +23 -12
  156. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  157. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  158. package/scripts/lib/fleet-protocol.mjs +4 -0
  159. package/scripts/lib/gates/gate-full.mjs +172 -11
  160. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  161. package/scripts/lib/git-config-drift.mjs +141 -7
  162. package/scripts/lib/github-export.mjs +447 -0
  163. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  164. package/scripts/lib/handover-queue-marks.mjs +159 -0
  165. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  166. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  167. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  168. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  169. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  170. package/scripts/lib/host-identity.mjs +9 -5
  171. package/scripts/lib/http-request.mjs +64 -0
  172. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  173. package/scripts/lib/io.mjs +6 -3
  174. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  175. package/scripts/lib/issue-budget.mjs +36 -7
  176. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  177. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  178. package/scripts/lib/learnings/io.mjs +289 -15
  179. package/scripts/lib/learnings/select.mjs +53 -4
  180. package/scripts/lib/learnings/store-role.mjs +209 -0
  181. package/scripts/lib/learnings/surface.mjs +46 -7
  182. package/scripts/lib/ledger-path.mjs +113 -0
  183. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  184. package/scripts/lib/memory-banner.mjs +5 -6
  185. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  186. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  187. package/scripts/lib/memory-proposals/store.mjs +22 -5
  188. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  189. package/scripts/lib/no-follow-open.mjs +57 -0
  190. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  191. package/scripts/lib/own-lock-root.mjs +95 -0
  192. package/scripts/lib/owner-interview.mjs +2 -1
  193. package/scripts/lib/owner-yaml.mjs +70 -7
  194. package/scripts/lib/platform.mjs +161 -9
  195. package/scripts/lib/plugin-update-banner.mjs +15 -11
  196. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  197. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  198. package/scripts/lib/process-group.mjs +270 -34
  199. package/scripts/lib/project-hygiene.mjs +28 -0
  200. package/scripts/lib/quality-gate.mjs +33 -9
  201. package/scripts/lib/reconcile/backlog.mjs +29 -15
  202. package/scripts/lib/reconcile/engine.mjs +26 -2
  203. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  204. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  205. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  206. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  207. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  208. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  209. package/scripts/lib/rule-loader.mjs +20 -10
  210. package/scripts/lib/scope-baseline.mjs +123 -8
  211. package/scripts/lib/scope-echo.mjs +5 -2
  212. package/scripts/lib/scope-gate.mjs +55 -20
  213. package/scripts/lib/session-close-backfill.mjs +75 -7
  214. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  215. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  216. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  217. package/scripts/lib/session-id.mjs +9 -1
  218. package/scripts/lib/session-identity/own-session.mjs +63 -0
  219. package/scripts/lib/session-lock.mjs +114 -0
  220. package/scripts/lib/session-record-repair.mjs +245 -35
  221. package/scripts/lib/session-registry.mjs +72 -9
  222. package/scripts/lib/session-schema/constants.mjs +9 -0
  223. package/scripts/lib/session-schema/filters.mjs +56 -1
  224. package/scripts/lib/session-schema/validator.mjs +15 -5
  225. package/scripts/lib/session-start-probes.mjs +149 -22
  226. package/scripts/lib/session-token-rollup.mjs +2 -1
  227. package/scripts/lib/session-work-trees.mjs +166 -0
  228. package/scripts/lib/sessions-canonical.mjs +61 -3
  229. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  230. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  231. package/scripts/lib/sh-quote.mjs +1 -0
  232. package/scripts/lib/skill-evidence-window.mjs +5 -1
  233. package/scripts/lib/skill-health/join.mjs +10 -9
  234. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  235. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  236. package/scripts/lib/snapshot-probe.mjs +414 -0
  237. package/scripts/lib/so-hud/collect.mjs +49 -5
  238. package/scripts/lib/so-hud/runtime.mjs +15 -4
  239. package/scripts/lib/spiral-carryover.mjs +8 -1
  240. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  241. package/scripts/lib/state-md/idle-reset.mjs +14 -5
  242. package/scripts/lib/state-md/mission-status.mjs +5 -1
  243. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  244. package/scripts/lib/state-md.mjs +31 -8
  245. package/scripts/lib/sunset/walker.mjs +4 -3
  246. package/scripts/lib/telemetry/consent.mjs +4 -2
  247. package/scripts/lib/telemetry/queue.mjs +3 -3
  248. package/scripts/lib/telemetry/sync.mjs +45 -9
  249. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  250. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  251. package/scripts/lib/ux-grill/paths.mjs +5 -1
  252. package/scripts/lib/validate/check-agents.mjs +37 -6
  253. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  254. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  255. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  256. package/scripts/lib/validate/check-json-files.mjs +3 -1
  257. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  258. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  259. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  260. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  261. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  262. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  263. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  264. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  265. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  266. package/scripts/lib/validate/confidential-names.mjs +3 -1
  267. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  268. package/scripts/lib/vault-mirror/namespace.mjs +19 -7
  269. package/scripts/lib/vault-mirror/process.mjs +56 -27
  270. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  271. package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
  272. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  273. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  274. package/scripts/lib/vault-mirror/utils.mjs +79 -0
  275. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  276. package/scripts/lib/vault-status/board-writer.mjs +2 -1
  277. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  278. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  279. package/scripts/lib/win-process-tree.mjs +137 -0
  280. package/scripts/lib/workspace.mjs +1 -1
  281. package/scripts/lib/worktree/constants.mjs +1 -1
  282. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  283. package/scripts/lib/worktree/listing.mjs +1 -1
  284. package/scripts/lib/zx.mjs +50 -0
  285. package/scripts/mcp-server.mjs +16 -7
  286. package/scripts/mcp-server.sh +13 -83
  287. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  288. package/scripts/memory-propose.mjs +10 -3
  289. package/scripts/migrate-cold-start-seed.mjs +7 -0
  290. package/scripts/migrate-legacy-learnings.sh +22 -0
  291. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  292. package/scripts/migrate-vault-paths.mjs +29 -13
  293. package/scripts/parse-config.mjs +71 -14
  294. package/scripts/print-learnings-index.mjs +25 -2
  295. package/scripts/release-session-lock.mjs +94 -7
  296. package/scripts/release.mjs +451 -24
  297. package/scripts/relocate-vault-corpus.mjs +11 -7
  298. package/scripts/repair-invalid-sessions.mjs +68 -3
  299. package/scripts/resolve-session-invocation.mjs +10 -17
  300. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  301. package/scripts/run-quality-gate.mjs +33 -7
  302. package/scripts/self-update.mjs +56 -7
  303. package/scripts/session-commit-files.mjs +133 -0
  304. package/scripts/site-numbers.mjs +52 -9
  305. package/scripts/snapshot-probe.mjs +156 -0
  306. package/scripts/so-hud.mjs +29 -6
  307. package/scripts/state-md-idle-reset.mjs +2 -1
  308. package/scripts/sweep-expired-learnings.mjs +54 -6
  309. package/scripts/sweep-expired-rules.mjs +4 -1
  310. package/scripts/validate-plugin.mjs +22 -0
  311. package/scripts/vault-integration-watcher.mjs +54 -6
  312. package/scripts/vault-mirror.mjs +9 -0
  313. package/skills/_shared/fleet-protocol.md +142 -1
  314. package/skills/_shared/instruction-file-resolution.md +1 -1
  315. package/skills/_shared/monitor-patterns.md +44 -25
  316. package/skills/_shared/platform-tools.md +7 -0
  317. package/skills/_shared/state-ownership.md +3 -2
  318. package/skills/architecture/references/domain-model.md +1 -1
  319. package/skills/bootstrap/SKILL.md +3 -1
  320. package/skills/bootstrap/_shared-template.md +4 -4
  321. package/skills/bootstrap/fast-template.md +1 -1
  322. package/skills/bootstrap/private-contract.md +1 -1
  323. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  324. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  325. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  326. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  327. package/skills/claude-md-drift-check/SKILL.md +3 -3
  328. package/skills/claude-md-drift-check/checker.mjs +61 -101
  329. package/skills/close/SKILL.md +20 -7
  330. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  331. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  332. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  333. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  334. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  335. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  336. package/skills/discovery/probes-session.md +3 -0
  337. package/skills/discovery/probes-vault.md +2 -0
  338. package/skills/dispatcher/SKILL.md +1 -1
  339. package/skills/eval/SKILL.md +3 -1
  340. package/skills/evolve/SKILL.md +2 -0
  341. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  342. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  343. package/skills/gitlab-ops/SKILL.md +7 -6
  344. package/skills/gitlab-portfolio/SKILL.md +2 -2
  345. package/skills/go/SKILL.md +13 -1
  346. package/skills/npm-publish/SKILL.md +5 -2
  347. package/skills/plan/SKILL.md +23 -10
  348. package/skills/plan/mode-feature.md +13 -8
  349. package/skills/plan/mode-new.md +6 -14
  350. package/skills/plan/prd-feature-template.md +19 -0
  351. package/skills/plan/prd-full-template.md +12 -5
  352. package/skills/plan/prd-premortem-panel.md +48 -0
  353. package/skills/plan/prd-reviewer-prompt.md +11 -3
  354. package/skills/plan/soul.md +1 -1
  355. package/skills/portfolio/SKILL.md +5 -6
  356. package/skills/quality-gates/SKILL.md +2 -4
  357. package/skills/reconcile/SKILL.md +27 -4
  358. package/skills/release/SKILL.md +16 -8
  359. package/skills/session-end/SKILL.md +94 -11
  360. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  361. package/skills/session-end/phase-3-6-tail.md +11 -7
  362. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  363. package/skills/session-end/plan-verification.md +16 -1
  364. package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
  365. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  366. package/skills/session-end/references/session-summary-template.md +4 -0
  367. package/skills/session-end/session-metrics-write.md +7 -4
  368. package/skills/session-plan/SKILL.md +5 -3
  369. package/skills/session-start/SKILL.md +9 -3
  370. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  371. package/skills/session-start/phase-8-5-express-path.md +4 -3
  372. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  373. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
  374. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  375. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  376. package/skills/tmux-layout/SKILL.md +2 -2
  377. package/skills/vault-sync/validator.mjs +3 -2
  378. package/skills/wave-executor/SKILL.md +5 -4
  379. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  380. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  381. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  382. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  383. package/assets/og-card.png +0 -0
  384. package/assets/og-card.svg +0 -47
  385. package/assets/wave-lifecycle.svg +0 -98
@@ -40,7 +40,10 @@
40
40
  * Slice: `.slice(0, n)` after sort.
41
41
  */
42
42
 
43
+ import { existsSync } from 'node:fs';
43
44
  import { readFile } from 'node:fs/promises';
45
+ import { readLearnings } from './io.mjs';
46
+ import { inboxPathFor, isCanonicalStore } from './store-role.mjs';
44
47
 
45
48
  const MS_PER_DAY = 86_400_000;
46
49
 
@@ -147,6 +150,49 @@ export function effectiveScore(entry, nowMs, decay) {
147
150
  * then sliced to `n`. Returns [] on missing/unreadable file or all-empty input.
148
151
  */
149
152
  export async function surfaceTopN(filePath, n = 5, opts = {}) {
153
+ // Preserve the pre-read clock used by the existing file reader.
154
+ const now = opts.now instanceof Date ? opts.now.getTime()
155
+ : typeof opts.now === 'number' ? opts.now : Date.now();
156
+ let raw;
157
+ try {
158
+ raw = await readFile(filePath, 'utf8');
159
+ } catch {
160
+ return [];
161
+ }
162
+ return surfaceLearningsText(await withVisibleInboxText(filePath, raw), n, { ...opts, now });
163
+ }
164
+
165
+ /**
166
+ * `raw` (the live store's text) plus the visible inbox records of the
167
+ * canonical store, one JSON line each (#1516) — the same visibility rule
168
+ * `readLearnings()` applies. Without an inbox file beside a canonical store
169
+ * `raw` comes back unchanged, so every pre-#1516 output stays byte-identical.
170
+ *
171
+ * Ceiling (BV-004): the live file is read twice when an inbox exists (once
172
+ * raw here, once by `readLearnings`); revisit if the inbox grows past the
173
+ * 1 MiB trigger named in io.mjs.
174
+ *
175
+ * @param {string} filePath — the store `raw` was read from
176
+ * @param {string} raw
177
+ * @returns {Promise<string>}
178
+ */
179
+ export async function withVisibleInboxText(filePath, raw) {
180
+ if (!isCanonicalStore(filePath) || !existsSync(inboxPathFor(filePath))) return raw;
181
+ let read;
182
+ try {
183
+ read = await readLearnings(filePath);
184
+ } catch {
185
+ return raw; // an unreadable inbox never costs the live surface
186
+ }
187
+ const { entries, inbox } = read;
188
+ const visible = inbox ? entries.slice(entries.length - inbox.visible) : [];
189
+ if (visible.length === 0) return raw;
190
+ const sep = raw.length === 0 || raw.endsWith('\n') ? '' : '\n';
191
+ return `${raw}${sep}${visible.map((e) => JSON.stringify(e)).join('\n')}\n`;
192
+ }
193
+
194
+ /** Same surface policy over one already-read JSONL snapshot; no I/O. */
195
+ export function surfaceLearningsText(raw, n = 5, opts = {}) {
150
196
  const { now: nowOpt, confidenceFloor = 0.3, decay: decayOpt } = opts;
151
197
  const nowMs =
152
198
  nowOpt instanceof Date
@@ -163,13 +209,6 @@ export async function surfaceTopN(filePath, n = 5, opts = {}) {
163
209
  floorFactor: decayOpt?.floorFactor ?? DECAY_DEFAULTS.floorFactor,
164
210
  };
165
211
 
166
- let raw;
167
- try {
168
- raw = await readFile(filePath, 'utf8');
169
- } catch {
170
- return [];
171
- }
172
-
173
212
  const lines = raw
174
213
  .split('\n')
175
214
  .map((l) => l.trim())
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Where a ledger under `.orchestrator/` lives when its writer or reader runs
3
+ * in a linked git worktree (#1722).
4
+ *
5
+ * A linked worktree has its own working tree, so `<worktree>/.orchestrator/
6
+ * metrics/<x>.jsonl` is a different file from the main checkout's. Which one is
7
+ * right depends on whether the repo versions the ledger:
8
+ *
9
+ * - **gitignored** (this plugin repo, `.gitignore:57`): the worktree copy is
10
+ * lost when the worktree is removed, and every reader that maps to the main
11
+ * checkout never sees it. Measured 2026-10-22: `eval.jsonl` stranded in
12
+ * `_wt/session-orchestrator/so-66c`, `skill-invocations.jsonl` and
13
+ * `eval.jsonl` in `so-1e`. → the main checkout's copy.
14
+ * - **versioned** (27 fleet repos track `learnings`/`sessions`/… under
15
+ * `.orchestrator/metrics/`, counted 2026-10-21T23:06Z; the bootstrap
16
+ * template calls them durable project data): the worktree copy travels
17
+ * with the branch and reaches main through the merge. Writing the main
18
+ * checkout's copy instead would dirty another session's working tree and
19
+ * leave the branch without the record. → the worktree's own copy.
20
+ *
21
+ * `git check-ignore` (index-aware: a tracked file is never reported ignored)
22
+ * decides between the two. It is spawned ONLY inside a linked worktree — a
23
+ * plain checkout, a non-git dir and a test tmpdir resolve spawn-free to the
24
+ * local path, as before — and memoised per process.
25
+ *
26
+ * Only a worktree's TOP LEVEL maps. A root below it (a caller's cwd in
27
+ * `scripts/` or in an ignored `.orchestrator/tmp/…`) keeps the local path, as
28
+ * before #1722: `git check-ignore` would answer for the ignored subdirectory,
29
+ * not for the ledger, and the mapped path would land inside the main
30
+ * checkout's copy of that subdirectory.
31
+ *
32
+ * Not used for `events.jsonl` and `sessions.jsonl`: those map unconditionally
33
+ * through {@link resolveEventsLedgerRoot} (owner decision on #1514 point 1,
34
+ * and #1584), versioned or not.
35
+ *
36
+ * Ceiling: a failed or timed-out `git check-ignore` (exit ≥ 128, no git, 2 s)
37
+ * keeps the local path — the pre-#1722 behaviour, never a write into a foreign
38
+ * working tree on an unmeasured answer. Revisit if a `*.jsonl` is found growing
39
+ * inside a linked worktree of a repo that ignores it.
40
+ *
41
+ * @module scripts/lib/ledger-path
42
+ */
43
+
44
+ import { spawnSync } from 'node:child_process';
45
+ import path from 'node:path';
46
+ import { dotGitAncestor, getProjectDir, resolveEventsLedgerRoot } from './platform.mjs';
47
+
48
+ /** @type {Map<string, boolean>} `<root>\0<rel>` → ignored in that worktree */
49
+ const ignoredMemo = new Map();
50
+
51
+ /**
52
+ * The `git` child must answer for `root`: an ambient `GIT_DIR` outranks `-C`
53
+ * (see `scripts/lib/git-config-drift.mjs`), and a hook can run inside a git
54
+ * hook that exports it.
55
+ */
56
+ function gitEnv() {
57
+ const env = {};
58
+ for (const [key, value] of Object.entries(process.env)) {
59
+ if (!key.startsWith('GIT_')) env[key] = value;
60
+ }
61
+ return env;
62
+ }
63
+
64
+ function isIgnoredIn(root, relPath) {
65
+ const key = `${root}\0${relPath}`;
66
+ if (ignoredMemo.has(key)) return ignoredMemo.get(key);
67
+ const r = spawnSync('git', ['-C', root, 'check-ignore', '-q', '--', relPath], {
68
+ env: gitEnv(),
69
+ stdio: 'ignore',
70
+ timeout: 2000,
71
+ });
72
+ const ignored = r.status === 0;
73
+ ignoredMemo.set(key, ignored);
74
+ return ignored;
75
+ }
76
+
77
+ /**
78
+ * The repo root the ledger `relPath` lives under for a caller rooted at `root`:
79
+ * the main checkout when `root` is a linked worktree that ignores the ledger,
80
+ * else `root` itself (resolved). Path guards that require a target inside the
81
+ * project validate against THIS root, so a mapped ledger passes and a path
82
+ * outside it is still refused.
83
+ *
84
+ * @param {string} relPath ledger path relative to the repo root
85
+ * @param {string} [root] repo root the caller works in; default {@link getProjectDir}
86
+ * @returns {string} absolute root
87
+ */
88
+ export function resolveLedgerRoot(relPath, root) {
89
+ const base = path.resolve(typeof root === 'string' && root.trim() !== '' ? root : getProjectDir());
90
+ const mainRoot = resolveEventsLedgerRoot(base);
91
+ if (mainRoot === base || dotGitAncestor(base) !== base) return base;
92
+ return isIgnoredIn(base, relPath) ? mainRoot : base;
93
+ }
94
+
95
+ /**
96
+ * Absolute path of the ledger `relPath` for a writer or reader rooted at `root`.
97
+ *
98
+ * @param {string} relPath ledger path relative to the repo root, e.g.
99
+ * `.orchestrator/metrics/eval.jsonl`; an absolute path is returned unchanged
100
+ * (an explicit override stays exactly what the caller asked for)
101
+ * @param {string} [root] repo root the caller works in; default
102
+ * {@link getProjectDir} (the launch dir in a hook, the cwd's project in a CLI)
103
+ * @returns {string}
104
+ */
105
+ export function resolveLedgerPath(relPath, root) {
106
+ if (path.isAbsolute(relPath)) return relPath;
107
+ return path.join(resolveLedgerRoot(relPath, root), relPath);
108
+ }
109
+
110
+ /** Test seam: forget memoised `git check-ignore` answers. */
111
+ export function _resetLedgerPathMemo() {
112
+ ignoredMemo.clear();
113
+ }
@@ -79,12 +79,13 @@ import { sweepExpiredLearnings } from './learnings/expiry-sweep.mjs';
79
79
  import { shouldDispatchAutoDialectic } from './auto-dialectic.mjs';
80
80
  import { shouldDispatchAutoDream } from './auto-dream.mjs';
81
81
  import { resolveMemoryDir } from './memory-paths.mjs';
82
- import { readCanonicalSessions } from './sessions-canonical.mjs';
83
- import { filterRealSessions } from './session-schema.mjs';
82
+ import { resolveLedgerPath } from './ledger-path.mjs';
83
+ import { readCanonicalSessions, isMeasuredSession } from './sessions-canonical.mjs';
84
84
  import {
85
85
  daysUntilGeneratedRuleExpiry,
86
86
  listMachineGeneratedRules,
87
87
  } from './instruction-budget-guard.mjs';
88
+ import { planRuleExpirySweep } from './reconcile/rule-expiry-sweep.mjs';
88
89
 
89
90
  /**
90
91
  * How many signals this probe knows about. The banner denominator is this
@@ -233,7 +234,7 @@ function readLastEvolveRun(repoRoot) {
233
234
  */
234
235
  function readLastHousekeeping(repoRoot) {
235
236
  try {
236
- const entries = filterRealSessions(readCanonicalSessions({ repoRoot }));
237
+ const entries = readCanonicalSessions({ repoRoot }).filter(isMeasuredSession);
237
238
  let max = null;
238
239
  for (const e of entries) {
239
240
  if (e?.session_type !== 'housekeeping') continue;
@@ -275,6 +276,40 @@ function isClaudePlatform(platform) {
275
276
  * lastHousekeeping: string|null,
276
277
  * }>}
277
278
  */
279
+ /**
280
+ * Generated rule files the expiry sweep can never act on from THIS host: the
281
+ * sweep's plan at the end of the warning horizon keeps the file whole because
282
+ * not one of its entries resolves to a learning in the local store. The
283
+ * learnings store is host-local and untracked, so a host whose store lacks the
284
+ * ids (measured 2026-10-19 on the M5: 6 records, 0 of 77 rule ids) was told to
285
+ * run a command that sweeps nothing. A failing plan (unreadable store) yields
286
+ * an empty set, i.e. the pre-existing recommendation.
287
+ *
288
+ * @param {string} repoRoot
289
+ * @param {number} now epoch ms
290
+ * @returns {Promise<Set<string>>} rule file names
291
+ */
292
+ async function unsweepableRuleFiles(repoRoot, now) {
293
+ try {
294
+ const plan = await planRuleExpirySweep({
295
+ repoRoot,
296
+ now: new Date(now + GENERATED_RULE_EXPIRY_HORIZON_DAYS * 86_400_000).toISOString(),
297
+ });
298
+ return new Set(
299
+ plan.plans
300
+ .filter(
301
+ (p) =>
302
+ p.action === 'keep' &&
303
+ p.substantivePairs > 0 &&
304
+ p.unresolvedPairIds.length === p.substantivePairs,
305
+ )
306
+ .map((p) => p.file),
307
+ );
308
+ } catch {
309
+ return new Set();
310
+ }
311
+ }
312
+
278
313
  export async function computeMaintenanceDue(opts = {}) {
279
314
  const repoRoot =
280
315
  typeof opts.repoRoot === 'string' && opts.repoRoot.length > 0 ? opts.repoRoot : process.cwd();
@@ -323,7 +358,8 @@ export async function computeMaintenanceDue(opts = {}) {
323
358
  if (nudge.nudge === true) {
324
359
  // HR-106: report what the rule JUDGED. `computeReconcileNudge` nudges on
325
360
  // a BACKLOG — rule-eligible learnings minus those already materialized
326
- // in the sidecar or `.claude/rules/` (`countReconcileBacklog`, #1380;
361
+ // in the sidecar, the verdict ledger (#1710) or `.claude/rules/`
362
+ // (`countReconcileBacklog`, #1380;
327
363
  // before #1380 it counted all eligible ones) — never on a date — so
328
364
  // printing `lastRunAt` here put today's date next to the word "due" and
329
365
  // read as "last run today and already due again". Measured 2026-09-09
@@ -339,10 +375,9 @@ export async function computeMaintenanceDue(opts = {}) {
339
375
 
340
376
  // --- sweep (S2) ----------------------------------------------------------
341
377
  try {
342
- const metrics = path.join(repoRoot, '.orchestrator', 'metrics');
343
378
  const res = await sweepExpiredLearnings({
344
- filePath: path.join(metrics, 'learnings.jsonl'),
345
- archivePath: path.join(metrics, 'learnings-archive.jsonl'),
379
+ filePath: resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot),
380
+ archivePath: resolveLedgerPath('.orchestrator/metrics/learnings-archive.jsonl', repoRoot),
346
381
  now: opts.now,
347
382
  dryRun: true,
348
383
  });
@@ -426,11 +461,27 @@ export async function computeMaintenanceDue(opts = {}) {
426
461
  // finding. The sweep (#1377) is the mechanical half — prose out, the
427
462
  // provenance pair kept as `markers only` — and an operator who is told
428
463
  // only which files expired has to rediscover that the command exists.
429
- markDue(
430
- 'generated-rules-expiring',
431
- `${expiring.map((r) => `${r.file} ${r.expiresAt}`).join(', ')} → ` +
432
- 'node scripts/sweep-expired-rules.mjs (dry-run first)',
433
- );
464
+ const unsweepable = await unsweepableRuleFiles(repoRoot, now);
465
+ const sweepable = expiring.filter((r) => !unsweepable.has(r.file));
466
+ const stuck = expiring.filter((r) => unsweepable.has(r.file));
467
+ const parts = [];
468
+ if (sweepable.length > 0) {
469
+ parts.push(
470
+ `${sweepable.map((r) => `${r.file} ${r.expiresAt}`).join(', ')} → ` +
471
+ 'node scripts/sweep-expired-rules.mjs (dry-run first)',
472
+ );
473
+ }
474
+ if (stuck.length > 0) {
475
+ // The sweep resolves every entry's date through its learning in the
476
+ // HOST-LOCAL learnings store; with none of a file's ids there it keeps
477
+ // the file whole, so recommending the command would be a no-op.
478
+ parts.push(
479
+ `${stuck.map((r) => `${r.file} ${r.expiresAt}`).join(', ')} → not sweepable on this host ` +
480
+ '(none of their learning ids are in .orchestrator/metrics/learnings.jsonl), ' +
481
+ 'sweep on the host that holds them',
482
+ );
483
+ }
484
+ markDue('generated-rules-expiring', parts.join('; '));
434
485
  }
435
486
  }
436
487
  } catch {
@@ -28,6 +28,8 @@ import { readDreamSignals } from './auto-dream.mjs';
28
28
  import { readPeerCards } from './peer-cards/reader.mjs';
29
29
  import { surfaceTopN, decayOptsFromConfig } from './learnings/surface.mjs';
30
30
  import { countSessionsInJsonl } from './sessions-canonical.mjs';
31
+ import { resolveEventsLedgerRoot } from './platform.mjs';
32
+ import { resolveLedgerPath } from './ledger-path.mjs';
31
33
 
32
34
  // ---------------------------------------------------------------------------
33
35
  // Constants
@@ -346,7 +348,7 @@ export async function readBannerInputs({ repoRoot, memoryDir, learningsPath, now
346
348
  : resolveMemoryDir(repoRoot);
347
349
  const learningsFile = typeof learningsPath === 'string' && learningsPath.length > 0
348
350
  ? learningsPath
349
- : path.join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
351
+ : resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot);
350
352
  const clock = now instanceof Date ? now : new Date();
351
353
 
352
354
  // --- Top learnings ---------------------------------------------------------
@@ -385,11 +387,8 @@ export async function readBannerInputs({ repoRoot, memoryDir, learningsPath, now
385
387
  try {
386
388
  const signals = await readDreamSignals({ repoRoot, memoryDir: memDir });
387
389
  const memoryFiles = await countMemoryFiles(memDir);
388
- // We deliberately use the actual sessions.jsonl path under repoRoot for
389
- // the count of "sessions ever" — readDreamSignals exposes sessionsFilePath
390
- // computed the same way, so we re-derive locally to keep this dependency
391
- // surface lean and explicit.
392
- const sessionsPath = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
390
+ // Use the same main-checkout ledger as the cleanup signals above.
391
+ const sessionsPath = path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl');
393
392
  const sessionsEver = await countSessionIdentities(sessionsPath);
394
393
 
395
394
  let daysSinceCleanup = null;
@@ -18,6 +18,7 @@
18
18
  import { readFile, readdir } from 'node:fs/promises';
19
19
  import { existsSync } from 'node:fs';
20
20
  import path from 'node:path';
21
+ import { resolveLedgerPath } from '../ledger-path.mjs';
21
22
 
22
23
  // ---------------------------------------------------------------------------
23
24
  // Path helpers
@@ -30,7 +31,9 @@ import path from 'node:path';
30
31
  * @returns {string}
31
32
  */
32
33
  function proposalsJsonlPath(repoRoot) {
33
- return path.join(repoRoot, '.orchestrator', 'metrics', 'proposals.jsonl');
34
+ // #1722: same root as store.mjs — a linked worktree of a repo that ignores
35
+ // the queue reads the main checkout's copy.
36
+ return resolveLedgerPath('.orchestrator/metrics/proposals.jsonl', repoRoot);
34
37
  }
35
38
 
36
39
  /**
@@ -40,7 +43,7 @@ function proposalsJsonlPath(repoRoot) {
40
43
  * @returns {string}
41
44
  */
42
45
  function metricsDir(repoRoot) {
43
- return path.join(repoRoot, '.orchestrator', 'metrics');
46
+ return path.dirname(proposalsJsonlPath(repoRoot));
44
47
  }
45
48
 
46
49
  // ---------------------------------------------------------------------------
@@ -47,6 +47,7 @@ import path from 'node:path';
47
47
  import { appendLearning } from '../learnings/io.mjs';
48
48
  import { validatePathInsideProject } from '../path-utils.mjs';
49
49
  import { atomicWriteWithBackup } from '../io.mjs';
50
+ import { resolveLedgerRoot } from '../ledger-path.mjs';
50
51
 
51
52
  // ---------------------------------------------------------------------------
52
53
  // Path constants (relative to repoRoot)
@@ -219,7 +220,7 @@ export async function writeApproved({ approved, repoRoot, sessionId, ...rest })
219
220
  return { written: 0, errors: [] };
220
221
  }
221
222
 
222
- const learningsResult = validateSinkTarget(LEARNINGS_REL, repoRoot);
223
+ const learningsResult = validateSinkTarget(LEARNINGS_REL, resolveLedgerRoot(LEARNINGS_REL, repoRoot));
223
224
  if (!learningsResult.ok) {
224
225
  return { written: 0, errors: [`path-safety: ${learningsResult.reason} (${LEARNINGS_REL})`] };
225
226
  }
@@ -262,7 +263,8 @@ export async function archiveRejected({ rejected, repoRoot, reason }) {
262
263
  return { archived: 0, errors: [] };
263
264
  }
264
265
 
265
- const rejectedResult = validateSinkTarget(REJECTED_LOG_REL, repoRoot);
266
+ // The rejected log sits beside the queue it records (#1722): same mapped root.
267
+ const rejectedResult = validateSinkTarget(REJECTED_LOG_REL, resolveLedgerRoot(PROPOSALS_REL, repoRoot));
266
268
  if (!rejectedResult.ok) {
267
269
  return { archived: 0, errors: [`path-safety: ${rejectedResult.reason} (${REJECTED_LOG_REL})`] };
268
270
  }
@@ -346,7 +348,7 @@ export async function archiveRejected({ rejected, repoRoot, reason }) {
346
348
  * @returns {Promise<{ cleared: boolean, summariesCleared: number, reason?: string }>}
347
349
  */
348
350
  export async function clearProposalsJsonl({ repoRoot }) {
349
- const proposalsResult = validateSinkTarget(PROPOSALS_REL, repoRoot);
351
+ const proposalsResult = validateSinkTarget(PROPOSALS_REL, resolveLedgerRoot(PROPOSALS_REL, repoRoot));
350
352
  if (!proposalsResult.ok) {
351
353
  return { cleared: false, summariesCleared: 0, reason: `path-safety: ${proposalsResult.reason}` };
352
354
  }
@@ -360,7 +362,8 @@ export async function clearProposalsJsonl({ repoRoot }) {
360
362
  try {
361
363
  const preClearContent = readFileSync(proposalsPath, 'utf8');
362
364
  if (preClearContent.length > 0) {
363
- const archiveResult = validateSinkTarget(PROPOSALS_ARCHIVE_REL, repoRoot);
365
+ // Archive and queue never sit in different checkouts (#1722).
366
+ const archiveResult = validateSinkTarget(PROPOSALS_ARCHIVE_REL, resolveLedgerRoot(PROPOSALS_REL, repoRoot));
364
367
  if (!archiveResult.ok) {
365
368
  return { cleared: false, summariesCleared: 0, reason: `path-safety: ${archiveResult.reason}` };
366
369
  }
@@ -24,6 +24,7 @@ import { serializeProposal } from './schema.mjs';
24
24
  import { validatePathInsideProject } from '../path-utils.mjs';
25
25
  import { tryAcquireFileLock, releaseFileLock } from '../file-lock.mjs';
26
26
  import { writeJsonAtomicSync } from '../io.mjs';
27
+ import { resolveLedgerRoot } from '../ledger-path.mjs';
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Constants
@@ -40,6 +41,19 @@ const DEFAULT_CONFIDENCE_FLOOR = 0.5;
40
41
  const DEFAULT_LOCK_TIMEOUT_MS = 1000;
41
42
  const LOCK_POLL_MS = 50;
42
43
 
44
+ /**
45
+ * Root of the whole proposals store (queue, overflow, lock, summaries). One
46
+ * root for all of them, decided by the queue ledger: the lock and the summary
47
+ * files are no `*.jsonl`, so asking `git check-ignore` per file could split
48
+ * the lock from the ledger it guards.
49
+ *
50
+ * @param {string} repoRoot
51
+ * @returns {string}
52
+ */
53
+ function storeRootFor(repoRoot) {
54
+ return resolveLedgerRoot(PROPOSALS_JSONL, repoRoot);
55
+ }
56
+
43
57
  // ---------------------------------------------------------------------------
44
58
  // Internal helpers
45
59
  // ---------------------------------------------------------------------------
@@ -160,7 +174,7 @@ function ensureDir(filePath) {
160
174
  * @returns {string}
161
175
  */
162
176
  function lockPathFor(repoRoot) {
163
- return safePath(repoRoot, PROPOSALS_LOCK);
177
+ return safePath(storeRootFor(repoRoot), PROPOSALS_LOCK);
164
178
  }
165
179
 
166
180
  /**
@@ -169,7 +183,7 @@ function lockPathFor(repoRoot) {
169
183
  * @returns {string}
170
184
  */
171
185
  function jsonlPathFor(repoRoot) {
172
- return safePath(repoRoot, PROPOSALS_JSONL);
186
+ return safePath(storeRootFor(repoRoot), PROPOSALS_JSONL);
173
187
  }
174
188
 
175
189
  /**
@@ -178,11 +192,14 @@ function jsonlPathFor(repoRoot) {
178
192
  * @returns {string}
179
193
  */
180
194
  function overflowPathFor(repoRoot) {
181
- return safePath(repoRoot, PROPOSALS_OVERFLOW_JSONL);
195
+ return safePath(storeRootFor(repoRoot), PROPOSALS_OVERFLOW_JSONL);
182
196
  }
183
197
 
184
198
  /** Flush a directory entry before depending on its persistence. */
185
199
  function syncOverflowDirectory(directory) {
200
+ // Windows rejects fsync on a directory handle (EPERM); NTFS metadata
201
+ // journaling already covers the directory entry there.
202
+ if (process.platform === 'win32') return;
186
203
  const fd = fs.openSync(directory, 'r');
187
204
  try {
188
205
  fs.fsyncSync(fd);
@@ -294,7 +311,7 @@ function appendOverflow(repoRoot, overflowPath, line) {
294
311
  fs.appendFileSync(overflowPath, lineBytes);
295
312
  return;
296
313
  }
297
- const root = fs.realpathSync(repoRoot);
314
+ const root = fs.realpathSync(storeRootFor(repoRoot));
298
315
  const directory = path.dirname(overflowPath);
299
316
  const token = randomUUID();
300
317
  const archive = safePath(root, path.relative(root, `${overflowPath}.archive-${token}.jsonl`));
@@ -332,7 +349,7 @@ function summaryPathFor(repoRoot, waveId) {
332
349
  if (!/^[A-Za-z0-9_-]+$/.test(waveId)) {
333
350
  throw new TypeError(`store.mjs: waveId contains invalid characters: ${waveId}`);
334
351
  }
335
- return safePath(repoRoot, `${PROPOSALS_SUMMARY_PREFIX}${waveId}.json`);
352
+ return safePath(storeRootFor(repoRoot), `${PROPOSALS_SUMMARY_PREFIX}${waveId}.json`);
336
353
  }
337
354
 
338
355
  /**
@@ -33,6 +33,7 @@
33
33
 
34
34
  import { appendLearning } from './learnings.mjs';
35
35
  import { isValidMode } from './recommendations-v0.mjs';
36
+ import { resolveLedgerPath } from './ledger-path.mjs';
36
37
 
37
38
  export const LEARNING_TYPE = 'mode-selector-accuracy';
38
39
  export const INITIAL_CONFIDENCE = 0.5;
@@ -66,7 +67,7 @@ export async function recordAccuracy(opts) {
66
67
  const recommended = opts?.recommended;
67
68
  const chosen = opts?.chosen;
68
69
  const sessionId = opts?.sessionId;
69
- const filePath = opts?.filePath || '.orchestrator/metrics/learnings.jsonl';
70
+ const filePath = opts?.filePath || resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', process.cwd());
70
71
 
71
72
  // No-op when the selector did not render a recommendation (banner suppressed,
72
73
  // confidence 0.0, etc). Caller passes recommended=null in that case.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * no-follow-open.mjs — open a regular file for reading without following a
3
+ * symlink in the final path component, on every platform (#1744).
4
+ *
5
+ * Node leaves `fs.constants.O_NOFOLLOW` and `O_NONBLOCK` undefined on win32, so
6
+ * callers that gated their read on those flags silently skipped it there.
7
+ */
8
+
9
+ import { constants as fsConstants, promises as fsPromises } from 'node:fs';
10
+
11
+ const codedError = (message, code) => Object.assign(new Error(message), { code });
12
+
13
+ /**
14
+ * Open `target` read-only without following a final-component symlink.
15
+ *
16
+ * - POSIX (`O_NOFOLLOW` and `O_NONBLOCK` both defined): one
17
+ * `open(O_RDONLY | O_NOFOLLOW | O_NONBLOCK)`; a symlink fails with the
18
+ * kernel's `ELOOP`. Callers keep doing their own stat checks on the handle.
19
+ * - Fallback (either flag missing, i.e. win32): `lstat` first — a symlink
20
+ * (Node reports Windows junctions as symlinks too) throws `code: 'ELOOP'`, a
21
+ * non-regular file throws `code: 'ERR_NOT_REGULAR_FILE'`; then `open('r')`
22
+ * and compare the handle's identity with the lstat. A non-regular file or a
23
+ * different `dev`/`ino` closes the handle and throws `code: 'ERR_FILE_CHANGED'`
24
+ * (the path was swapped between check and open). Bigint stats, because
25
+ * Windows file IDs can exceed 2^53.
26
+ *
27
+ * BV-004 ceiling: the fallback still has a window between lstat and open. On
28
+ * win32 there are no FIFOs in the filesystem namespace, so the open cannot
29
+ * block, and the post-open identity check catches a swap. Revisit when a POSIX
30
+ * platform lacks O_NONBLOCK, or a caller must read a FIFO-capable location on
31
+ * such a platform.
32
+ *
33
+ * @param {string} target
34
+ * @param {{ constants?: Record<string, number|undefined>, fsImpl?: typeof fsPromises }} [options]
35
+ * Injection exists only so the fallback branch is testable on POSIX CI;
36
+ * production callers pass nothing.
37
+ * @returns {Promise<import('node:fs/promises').FileHandle>} caller closes it
38
+ */
39
+ export async function openNoFollow(target, { constants = fsConstants, fsImpl = fsPromises } = {}) {
40
+ if (typeof constants.O_NOFOLLOW === 'number' && typeof constants.O_NONBLOCK === 'number') {
41
+ return fsImpl.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
42
+ }
43
+ const before = await fsImpl.lstat(target, { bigint: true });
44
+ if (before.isSymbolicLink()) throw codedError(`${target} is a symbolic link`, 'ELOOP');
45
+ if (!before.isFile()) throw codedError(`${target} is not a regular file`, 'ERR_NOT_REGULAR_FILE');
46
+ const handle = await fsImpl.open(target, 'r');
47
+ try {
48
+ const after = await handle.stat({ bigint: true });
49
+ if (!after.isFile() || after.dev !== before.dev || after.ino !== before.ino) {
50
+ throw codedError(`${target} changed between check and open`, 'ERR_FILE_CHANGED');
51
+ }
52
+ } catch (err) {
53
+ await handle.close();
54
+ throw err;
55
+ }
56
+ return handle;
57
+ }
@@ -9,7 +9,8 @@ import path from 'node:path';
9
9
 
10
10
  import { replaceRegularFile } from '../process-group.mjs';
11
11
  import { readTailWindow } from '../tail-window.mjs';
12
- import { REAPER_DEFAULTS, underRepo } from './defaults.mjs';
12
+ import { REAPER_DEFAULTS } from './defaults.mjs';
13
+ import { resolveLedgerPath } from '../ledger-path.mjs';
13
14
  import { NOT_A_REGULAR_FILE, appendNoFollow } from './no-follow-append.mjs';
14
15
 
15
16
  /** Relative path of the JSONL kill audit (B5). Gitignored via `.gitignore:55`
@@ -67,7 +68,8 @@ function isRateDecision(record) {
67
68
  /** Absolute path of the kill audit for a repo. One constant per path.
68
69
  * @param {string} repoRoot @returns {string} */
69
70
  export function auditPath(repoRoot) {
70
- return underRepo(repoRoot, REAPER_AUDIT_RELPATH);
71
+ // #1722: a linked worktree of a repo that ignores the audit maps to the main checkout.
72
+ return resolveLedgerPath(REAPER_AUDIT_RELPATH, repoRoot);
71
73
  }
72
74
 
73
75
  /**