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
@@ -141,11 +141,12 @@ export async function writeJson(filePath, obj) {
141
141
  * PIPE_BUF (4 KiB).
142
142
  * @param {string} filePath
143
143
  * @param {unknown} obj
144
+ * @param {{ mode?: number }} [opts] — file mode on creation (e.g. 0o600 for host-private logs, #1632)
144
145
  * @returns {Promise<void>}
145
146
  */
146
- export async function appendJsonl(filePath, obj) {
147
+ export async function appendJsonl(filePath, obj, { mode } = {}) {
147
148
  await fs.mkdir(path.dirname(filePath), { recursive: true });
148
- await fs.appendFile(filePath, JSON.stringify(obj) + '\n', 'utf8');
149
+ await fs.appendFile(filePath, JSON.stringify(obj) + '\n', mode === undefined ? 'utf8' : { encoding: 'utf8', mode });
149
150
  }
150
151
 
151
152
  // ---------------------------------------------------------------------------
@@ -16,21 +16,88 @@
16
16
  // here). We deliberately do NOT import `parseSessionConfig` — calling it from
17
17
  // `getCrossRepoProjects` would re-run `_parseCrossRepo` (double-parse). By calling
18
18
  // `_parseCrossRepo` directly on the raw content we parse exactly once. (issue #664)
19
+ import { statSync } from 'node:fs';
19
20
  import { homedir } from 'node:os';
20
- import { join } from 'node:path';
21
+ import { isAbsolute, join } from 'node:path';
22
+ import { expandTilde } from '../common.mjs';
21
23
  import { readConfigFile } from './io.mjs';
24
+ import { loadHostPaths, resolveHostPath } from './host-paths.mjs';
22
25
  import { matchBlockHeader } from './block-header.mjs';
23
26
  import { preprocessBlockLines } from './block-preprocess.mjs';
24
27
 
25
28
  /**
26
- * Resolve the cross-repo confinement root. Defaults to ~/Projects.
27
- * Override via CROSS_REPO_CONFINEMENT_ROOT env (test-only — never set in production).
29
+ * Expand a leading `~` / `~/` to the home directory and accept the result only
30
+ * when it is absolute. A relative value would otherwise resolve against the
31
+ * process cwd — a different root per caller — so it is rejected instead.
32
+ * An existing non-directory (a file) is rejected too (#1567): every sweep under
33
+ * it would find nothing. A path that does not exist yet is accepted, like the
34
+ * ~/Projects default — callers guard with existsSync, and falling back from an
35
+ * unmounted volume to ~/Projects would widen the confinement root.
28
36
  *
29
- * @returns {string} absolute path
37
+ * @param {string} raw — non-blank value from one precedence tier
38
+ * @param {string} source — tier name for the WARN (`env SO_PROJECTS_ROOT` / `owner.yaml paths.projects-root`)
39
+ * @returns {string|null} absolute path, or null when rejected
30
40
  */
31
- export function getConfinementRoot() {
32
- const v = process.env.CROSS_REPO_CONFINEMENT_ROOT;
33
- return (v && v.trim()) ? v : join(homedir(), 'Projects');
41
+ function expandProjectsRoot(raw, source) {
42
+ const expanded = expandTilde(raw.trim());
43
+ if (!isAbsolute(expanded)) {
44
+ process.stderr.write(
45
+ `cross-repo: ignoring ${source}=${JSON.stringify(raw)} — not an absolute path after ~ expansion; falling back\n`,
46
+ );
47
+ return null;
48
+ }
49
+ let stat;
50
+ try {
51
+ stat = statSync(expanded, { throwIfNoEntry: false });
52
+ } catch {
53
+ stat = undefined; // unreadable (EACCES/ENOTDIR): treated like a missing path — never throw here
54
+ }
55
+ if (stat && !stat.isDirectory()) {
56
+ process.stderr.write(
57
+ `cross-repo: ignoring ${source}=${JSON.stringify(raw)} — exists but is not a directory; falling back\n`,
58
+ );
59
+ return null;
60
+ }
61
+ return expanded;
62
+ }
63
+
64
+ /**
65
+ * Resolve the cross-repo confinement root — the directory host-wide sweeps scan
66
+ * and outside which `cross-repo.projects` entries are dropped (#1562).
67
+ *
68
+ * Precedence (highest first):
69
+ * 1. env CROSS_REPO_CONFINEMENT_ROOT — test-only, never set in production;
70
+ * returned verbatim when non-blank (no ~ expansion, no absolute check).
71
+ * 2. env SO_PROJECTS_ROOT — host-local, via resolveHostPath.
72
+ * 3. owner.yaml paths.projects-root — host-local, via resolveHostPath.
73
+ * 4. ~/Projects — default.
74
+ * Tiers 2 and 3 expand a leading `~`; a value that is still not absolute is
75
+ * ignored with one stderr WARN and the next tier is tried. owner.yaml is read
76
+ * only when tiers 1 and 2 yield nothing usable.
77
+ *
78
+ * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [ctx]
79
+ * injection seam for tests, mirroring resolveHostPath; `ownerConfig` omitted →
80
+ * owner.yaml is loaded from disk via loadHostPaths()
81
+ * @returns {string} confinement root
82
+ */
83
+ export function getConfinementRoot({ env = process.env, ownerConfig } = {}) {
84
+ const v = env.CROSS_REPO_CONFINEMENT_ROOT;
85
+ if (v && v.trim()) return v;
86
+
87
+ const fromEnv = resolveHostPath('projects-root', undefined, { env, ownerConfig: undefined });
88
+ if (typeof fromEnv === 'string') {
89
+ const root = expandProjectsRoot(fromEnv, 'env SO_PROJECTS_ROOT');
90
+ if (root) return root;
91
+ }
92
+
93
+ const owner = ownerConfig ?? loadHostPaths({ env }).ownerConfig;
94
+ const fromOwner = resolveHostPath('projects-root', undefined, { env: {}, ownerConfig: owner });
95
+ if (typeof fromOwner === 'string') {
96
+ const root = expandProjectsRoot(fromOwner, 'owner.yaml paths.projects-root');
97
+ if (root) return root;
98
+ }
99
+
100
+ return join(homedir(), 'Projects');
34
101
  }
35
102
 
36
103
  // Allowlist: only characters that are safe in shell contexts and file paths.
@@ -8,7 +8,8 @@
8
8
  * committed default act as a fallback for unconfigured hosts.
9
9
  *
10
10
  * Precedence (highest first):
11
- * 1. env-var (SO_VAULT_DIR / SO_BASELINE_PATH)
11
+ * 1. env-var (SO_VAULT_DIR / SO_BASELINE_PATH / SO_NAMESPACE_MAP /
12
+ * SO_CONFIDENTIAL_NAMES_FILE / SO_PROJECTS_ROOT — see ENV_KEYS)
12
13
  * 2. owner.yaml paths[key] (host-local, never committed — ~/.config/session-orchestrator/owner.yaml)
13
14
  * 3. committed Session Config default (the value the committed CLAUDE.md —
14
15
  * AGENTS.md on Codex CLI — produced)
@@ -40,6 +41,9 @@ const ENV_KEYS = /** @type {const} */ ({
40
41
  'namespace-map-path': 'SO_NAMESPACE_MAP',
41
42
  // #728a — host-local confidential customer/repo name list for CP11 leak scanning.
42
43
  'confidential-names-file': 'SO_CONFIDENTIAL_NAMES_FILE',
44
+ // #1562 — host-local cross-repo confinement / scan root (default ~/Projects);
45
+ // consumed by getConfinementRoot() in config/cross-repo.mjs.
46
+ 'projects-root': 'SO_PROJECTS_ROOT',
43
47
  });
44
48
 
45
49
  /**
@@ -86,7 +90,7 @@ export function loadHostPaths({ env = process.env, ownerLoader = loadOwnerConfig
86
90
  * (fall through to the next tier). When no override is set, `committedDefault`
87
91
  * passes through unchanged — including `null`/`undefined`, preserving back-compat.
88
92
  *
89
- * @param {'vault-dir'|'baseline-path'|'namespace-map-path'|'confidential-names-file'} key — logical path key
93
+ * @param {'vault-dir'|'baseline-path'|'namespace-map-path'|'confidential-names-file'|'projects-root'} key — logical path key
90
94
  * @param {string|null|undefined} committedDefault — value the committed Session Config produced
91
95
  * @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [ctx] — from loadHostPaths()
92
96
  * @returns {string|null|undefined} resolved value
@@ -18,7 +18,7 @@ import { preprocessBlockLinesNoDash } from './block-preprocess.mjs';
18
18
  *
19
19
  * Block shape:
20
20
  * remote-hosts:
21
- * - alias: m5 # required; becomes `-H <alias>` argv
21
+ * - alias: build-host # required; becomes `-H <alias>` argv
22
22
  * roles-allowed: [test, ui, perf] # subset of ALLOWED_REMOTE_ROLES
23
23
  * repo-path: ~/Projects/Alice # optional
24
24
  * claude-path: ~/.local/bin/claude # optional
@@ -54,7 +54,7 @@ export const ALLOWED_REMOTE_ROLES = Object.freeze(['test', 'ui', 'perf']);
54
54
  // The first character is anchored separately from the rest because a charset
55
55
  // cannot express a POSITION: an alias of `-H` passes any hyphen-inclusive
56
56
  // allowlist and then reaches argv as an OPTION token, where the CLI swallows the
57
- // operand behind it. Interior hyphens (`m5-box`) stay legal.
57
+ // operand behind it. Interior hyphens (`build-host`) stay legal.
58
58
  const SAFE_NAME_RE = /^[A-Za-z0-9._][A-Za-z0-9._-]*$/;
59
59
 
60
60
  // `repo-path` / `claude-path` are filesystem paths handed to a remote shell —
@@ -345,8 +345,11 @@ export function _parseKV(lines) {
345
345
  * and at any indent. Deliberately NOT imported from block-header.mjs: that
346
346
  * matcher answers "does this line open the block for THIS key" for a known key,
347
347
  * while here the key is unknown by construction.
348
+ *
349
+ * Group 1 captures the key, for `collectTopLevelKeyLines` below; `.test()`
350
+ * callers are unaffected by the group.
348
351
  */
349
- const NESTED_BLOCK_HEADER_RE = /^\s*(?:-\s+)?(?:\*\*)?[a-zA-Z][a-zA-Z0-9_.-]*:(?:\*\*)?\s*$/;
352
+ const NESTED_BLOCK_HEADER_RE = /^\s*(?:-\s+)?(?:\*\*)?([a-zA-Z][a-zA-Z0-9_.-]*):(?:\*\*)?\s*$/;
350
353
 
351
354
  /** A YAML sequence element that is not a `key: value` pair (` - some-value`). */
352
355
  const LIST_ITEM_RE = /^\s*-\s+\S/;
@@ -391,8 +394,67 @@ const COMMENT_RE = /^\s*(?:#|<!--|>)/;
391
394
  export function collectUnparsableLines(content) {
392
395
  if (typeof content !== 'string' || content === '') return [];
393
396
 
394
- const lines = content.split(/\r?\n/);
395
397
  const unparsable = [];
398
+ for (const { lineNo, line } of liveBlockLines(content)) {
399
+ if (_matchKVLine(line)) continue;
400
+ if (NESTED_BLOCK_HEADER_RE.test(line)) continue;
401
+ if (LIST_ITEM_RE.test(line)) continue;
402
+
403
+ unparsable.push({ line: lineNo, text: sanitizeReportText(line.trim()) });
404
+ }
405
+
406
+ return unparsable;
407
+ }
408
+
409
+ /**
410
+ * Collect the TOP-LEVEL keys the runtime reads from `## Session Config` — the
411
+ * local half of the unknown-key report in `scripts/parse-config.mjs` (#1562).
412
+ *
413
+ * Same block, same walk, same line acceptance as `collectUnparsableLines`
414
+ * (both iterate `liveBlockLines`), so a key inside a multi-line `<!-- -->`
415
+ * comment, a fence, or a `#` line is never collected. A line counts as a
416
+ * top-level key when it starts at column 0 and is either a key/value pair the
417
+ * reader accepts (`_matchKVLine`, both spellings) or a nested-block header
418
+ * (`vault-integration:`); indented lines are children of a nested block.
419
+ *
420
+ * Ceiling (deliberate): a compact YAML sequence written at column 0 under a
421
+ * nested header (`custom-phases:` then `- name: x` unindented) yields `name`
422
+ * here. `extractTopLevelKeys` in `template-keys.mjs` reads that line the
423
+ * same way, so drift-check Check 6 and this collector agree on it. Revisit if
424
+ * a fleet CLAUDE.md is ever measured writing that form.
425
+ *
426
+ * @param {string} content — full markdown document
427
+ * @returns {Array<{ line: number, key: string }>} 1-based document line numbers
428
+ * in document order (a repeated key appears once per line); empty when the
429
+ * block is absent or the content is not a string. Keys are passed through
430
+ * `sanitizeReportText` because the bold spelling admits any character but
431
+ * `*` and `:`, and the one consumer prints them on a terminal.
432
+ */
433
+ export function collectTopLevelKeyLines(content) {
434
+ if (typeof content !== 'string' || content === '') return [];
435
+
436
+ const keys = [];
437
+ for (const { lineNo, line } of liveBlockLines(content)) {
438
+ if (/^\s/.test(line)) continue;
439
+ const kv = _matchKVLine(line);
440
+ const key = kv ? kv.key : NESTED_BLOCK_HEADER_RE.exec(line)?.[1];
441
+ if (key) keys.push({ line: lineNo, key: sanitizeReportText(key) });
442
+ }
443
+ return keys;
444
+ }
445
+
446
+ /**
447
+ * The lines of the FIRST `## Session Config` block that can carry meaning:
448
+ * everything except blank lines, fences, `#`/`<!--`/`>` documentation lines and
449
+ * the inside of a multi-line HTML comment. The one walk both classifiers share,
450
+ * in the order `_extractConfigSection` applies the same skips — the shared
451
+ * `htmlCommentSkipper` call is what keeps the accept-sets identical.
452
+ *
453
+ * @param {string} content
454
+ * @returns {Generator<{ lineNo: number, line: string }>} 1-based line numbers
455
+ */
456
+ function* liveBlockLines(content) {
457
+ const lines = content.split(/\r?\n/);
396
458
  let inSection = false;
397
459
  const skipHtmlComment = htmlCommentSkipper();
398
460
 
@@ -406,20 +468,12 @@ export function collectUnparsableLines(content) {
406
468
  if (!inSection) continue;
407
469
  if (/^## /.test(line)) break;
408
470
 
409
- // A multi-line HTML comment: everything up to `-->` is documentation.
410
- // Same call, same order, as `_extractConfigSection` — that shared call is
411
- // what makes the two accept-sets identical rather than merely similar.
412
471
  if (skipHtmlComment(line)) continue;
413
472
 
414
473
  if (!line.trim()) continue;
415
474
  if (isCodeFence(line)) continue;
416
475
  if (COMMENT_RE.test(line)) continue;
417
- if (_matchKVLine(line)) continue;
418
- if (NESTED_BLOCK_HEADER_RE.test(line)) continue;
419
- if (LIST_ITEM_RE.test(line)) continue;
420
476
 
421
- unparsable.push({ line: i + 1, text: sanitizeReportText(line.trim()) });
477
+ yield { lineNo: i + 1, line };
422
478
  }
423
-
424
- return unparsable;
425
479
  }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * template-keys.mjs — the KNOWN top-level Session Config keys, read from
3
+ * `docs/session-config-template.md`.
4
+ *
5
+ * Two consumers read the same population and must not drift apart:
6
+ * - `skills/claude-md-drift-check/checker.mjs` Check 6 (session-config-parity)
7
+ * diffs a repo's local block against the template's blocks;
8
+ * - `scripts/parse-config.mjs` reports a local top-level key the template
9
+ * never documents — a typo'd key is otherwise absent from the KV map and
10
+ * every consumer silently applies its default (#1562).
11
+ *
12
+ * The template carries TWO literal `## Session Config` lines, both inside fenced
13
+ * ```yaml examples: the FIRST is the starter / minimal baseline (the mandatory
14
+ * keys), the LAST is the opt-in baseline (the full key catalog). Check 6
15
+ * classifies mandatory vs opt-in by exactly that order.
16
+ */
17
+
18
+ import { isSessionConfigHeading, collectTopLevelKeyLines } from './section-extractor.mjs';
19
+
20
+ /**
21
+ * Extract the YAML block under a `## Session Config` heading from a Markdown
22
+ * document. Tries fenced YAML first (```yaml ... ```) *within the matched
23
+ * heading's own body slice*, then falls back to the raw body up to the next
24
+ * `^## ` heading or EOF.
25
+ *
26
+ * `docs/session-config-template.md` carries TWO literal `## Session Config`
27
+ * lines — both live INSIDE fenced ```yaml example blocks: the starter / "Full
28
+ * minimal baseline" (7 mandatory keys) and the "Full opt-in baseline" (the
29
+ * complete key catalog, a strict superset of the minimal block). By default
30
+ * this function matches the FIRST occurrence (correct for a local CLAUDE.md /
31
+ * AGENTS.md instruction file, which has exactly one real heading). Pass
32
+ * `{ occurrence: 'last' }` to match the LAST occurrence instead — the template
33
+ * call site uses this to reach the opt-in baseline's full key set rather than
34
+ * silently stopping at the 7-key minimal block (issue #785).
35
+ *
36
+ * Returns { body: string, headingLine: number } or null when no heading found.
37
+ *
38
+ * @param {string} content
39
+ * @param {{ occurrence?: 'first'|'last' }} [options]
40
+ * @returns {{ body: string, headingLine: number } | null}
41
+ */
42
+ export function extractSessionConfigBlock(content, { occurrence = 'first' } = {}) {
43
+ const lines = content.split('\n');
44
+ const headingIdxs = [];
45
+ for (let i = 0; i < lines.length; i++) {
46
+ // SSOT predicate (#968). The previous local `/^##\s+Session Config\b/`
47
+ // was the loosest JS comparator in the repo: no end anchor and `\s+`
48
+ // instead of a single space, so it accepted `## Session Config` (two
49
+ // spaces) and `## Session Config Convention` — both of which the runtime
50
+ // parser rejects. The drift-check's whole job is to report on the block
51
+ // the runtime reads, so matching a heading the runtime cannot see made it
52
+ // audit a section that does not exist.
53
+ if (isSessionConfigHeading(lines[i])) headingIdxs.push(i);
54
+ }
55
+ if (headingIdxs.length === 0) return null;
56
+
57
+ const targetIdx = occurrence === 'last' ? headingIdxs[headingIdxs.length - 1] : headingIdxs[0];
58
+ const headingLine = targetIdx + 1; // 1-based line number for error reporting
59
+
60
+ // Raw body: from the line after the heading up to the next `## ` heading or EOF.
61
+ let endIdx = lines.length;
62
+ for (let i = targetIdx + 1; i < lines.length; i++) {
63
+ if (/^##\s+/.test(lines[i])) { endIdx = i; break; }
64
+ }
65
+ const rawBody = lines.slice(targetIdx + 1, endIdx).join('\n');
66
+
67
+ // Prefer a fenced YAML block found anywhere within the raw body slice
68
+ // (tolerates a blank line between the heading and the fence); fall back to
69
+ // the raw body itself when no fence is present.
70
+ const fenced = rawBody.match(/```ya?ml\r?\n([\s\S]*?)\r?\n```/);
71
+ const body = fenced ? fenced[1] : rawBody;
72
+
73
+ return { body, headingLine };
74
+ }
75
+
76
+ /**
77
+ * Extract top-level YAML keys from a YAML body. Only column-0 keys are
78
+ * collected (indented keys are children and ignored).
79
+ *
80
+ * Accepts four equivalent forms consumer CLAUDE.md files write a Session
81
+ * Config key in: bare `key:`, Markdown bullet `- key:`, bold `**key:**`
82
+ * (closing bold before the colon), and the bold-bullet consumer shape
83
+ * `- **key:** value` (bold wraps the key AND colon together, closing bold
84
+ * AFTER the colon). Baseline issue #60: the original bare-only regex
85
+ * extracted zero keys from bullet-form local files, so session-config-parity
86
+ * (drift-check Check 6) either false-positived on every mandatory key (local
87
+ * side reads as empty) or, when the TEMPLATE side also used bullet form,
88
+ * passed vacuously (both sides empty, so the diff was always empty). Sibling
89
+ * precedent: `scripts/lib/config/block-header.mjs` recognizes only header
90
+ * lines, not individual key-value pairs — this function is the per-key
91
+ * counterpart.
92
+ *
93
+ * @param {string} body
94
+ * @returns {string[]} keys in document order (duplicates kept)
95
+ */
96
+ export function extractTopLevelKeys(body) {
97
+ const keys = [];
98
+ const re = /^(?:-\s+)?(?:\*\*)?([A-Za-z][\w-]*)(?:\*\*)?:/gm;
99
+ let m;
100
+ while ((m = re.exec(body)) !== null) {
101
+ keys.push(m[1]);
102
+ }
103
+ return keys;
104
+ }
105
+
106
+ /**
107
+ * The known-key union of a Session Config template: every top-level key of its
108
+ * FIRST `## Session Config` block (starter / mandatory) and its LAST one
109
+ * (opt-in catalog).
110
+ *
111
+ * Returns null — "not measurable", never "no keys" — when the template does not
112
+ * carry two DISTINCT blocks. A single block is not a key catalog (the same
113
+ * guard drift-check Check 6 applies before it warns on local keys): diffing a
114
+ * repo against the seven starter keys alone would report every legitimately
115
+ * adopted opt-in key as unknown.
116
+ *
117
+ * @param {string} content - full template document
118
+ * @returns {Set<string> | null}
119
+ */
120
+ export function knownTemplateKeys(content) {
121
+ if (typeof content !== 'string') return null;
122
+ const first = extractSessionConfigBlock(content, { occurrence: 'first' });
123
+ const last = extractSessionConfigBlock(content, { occurrence: 'last' });
124
+ if (!first || !last || first.headingLine === last.headingLine) return null;
125
+ return new Set([...extractTopLevelKeys(first.body), ...extractTopLevelKeys(last.body)]);
126
+ }
127
+
128
+ /**
129
+ * The template's starter (mandatory) keys a local instruction file does NOT
130
+ * carry in its `## Session Config` block — the input of `/bootstrap
131
+ * --retroactive` step 6 (#1567).
132
+ *
133
+ * Why not the validator: `parseSessionConfig()` fills every mandatory field
134
+ * with its default, so `validate-config` never sees an ABSENT key; in a pnpm
135
+ * repo without `test-command` the run silently uses `npm test`. Presence is a
136
+ * property of the FILE, so it is read from the file: local keys come from
137
+ * `collectTopLevelKeyLines()` (the lines the runtime reads, all spellings),
138
+ * the mandatory population from the template's FIRST block — the same
139
+ * population drift-check Check 6 calls mandatory.
140
+ *
141
+ * @param {string} localContent - the repo's CLAUDE.md / AGENTS.md
142
+ * @param {string} templateContent - `docs/session-config-template.md`
143
+ * @returns {string[] | null} missing keys in template order; null when the
144
+ * template has no `## Session Config` block ("not measurable", never "none
145
+ * missing")
146
+ */
147
+ export function missingStarterKeys(localContent, templateContent) {
148
+ if (typeof templateContent !== 'string') return null;
149
+ const starter = extractSessionConfigBlock(templateContent, { occurrence: 'first' });
150
+ if (!starter) return null;
151
+ const present = new Set(collectTopLevelKeyLines(localContent).map(({ key }) => key));
152
+ return [...new Set(extractTopLevelKeys(starter.body))].filter((key) => !present.has(key));
153
+ }
@@ -347,6 +347,7 @@ export function _parseResourceThresholds(kv) {
347
347
  // Unchanged at 5 — but now compared against LIVE PEER SESSIONS rather than
348
348
  // the Claude process count. Same number, different denominator: measured
349
349
  // firing rate drops from 93.6% to 4.2% (median processes:sessions = 6.0).
350
+ // That 4.2% did not hold: 67.4% M4 / 25.7% M5 re-measured 2026-10-17 (#1650).
350
351
  'concurrent-sessions-warn': _coerceInteger(kv, 'concurrent-sessions-warn', 5),
351
352
  'ssh-no-docker': _coerceBoolean(kv, 'ssh-no-docker', true),
352
353
  };
@@ -327,7 +327,7 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
327
327
  // dispatched natively, exactly as before.
328
328
  // impl: code-implementer → native Agent dispatch (unchanged)
329
329
  // impl: cursor:composer-2.5 → foreign model over the Cursor channel
330
- // test: ssh:m5 → same Claude, another host (#1160)
330
+ // test: ssh:build-host → same Claude, another host (#1160)
331
331
  // An unknown prefix is a typo or a channel this build cannot route, and
332
332
  // must fail loudly: silently accepting it would dispatch to an agent that
333
333
  // does not exist, which surfaces only as an empty wave much later.
@@ -433,8 +433,8 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
433
433
  const eventsRotation = _parseEventsRotation(mdContent);
434
434
 
435
435
  // express-path: parsed from full content (standalone top-level block, #214/#1119).
436
- // Documented in docs/session-config-template.md:585 and
437
- // docs/session-config-reference.md:1519 since #214, but never in the key schema —
436
+ // Documented in docs/session-config-template.md § Express Path and
437
+ // docs/session-config-reference.md § Express Path (#214) since #214, but never in the key schema —
438
438
  // measured 2026-08-23, a probe carrying `express-path:\n enabled: false` produced
439
439
  // 88 keys and none of them was `express-path`. The key was dropped even when it
440
440
  // stood in the file, so the documented default was prose rather than a value.
@@ -20,7 +20,7 @@ import { promises as fs } from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import os from 'node:os';
22
22
 
23
- import { $, nothrow } from 'zx';
23
+ import { $, nothrow } from './zx.mjs';
24
24
 
25
25
  // Do not spam stdout/stderr with git command echoes.
26
26
  $.verbose = false;
@@ -19,6 +19,7 @@ import { appendFile, readFile, mkdir } from 'node:fs/promises';
19
19
  import { existsSync } from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import { findProjectRoot } from '../common.mjs';
22
+ import { resolveLedgerPath } from '../ledger-path.mjs';
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Constants
@@ -56,7 +57,7 @@ export const DEFAULT_STATE_FILE = '.orchestrator/metrics/discovery-triage.jsonl'
56
57
  function resolveStatePath(stateFilePath) {
57
58
  const p = stateFilePath ?? DEFAULT_STATE_FILE;
58
59
  if (path.isAbsolute(p)) return p;
59
- return path.join(findProjectRoot(), p);
60
+ return resolveLedgerPath(p, findProjectRoot());
60
61
  }
61
62
 
62
63
  /**
@@ -29,13 +29,10 @@
29
29
  * @typedef {{ repoRoot: string, repoName: string, free: boolean, status: 'frei'|'in-progress'|'force-closed', heartbeat: string|null, sessionId: string|null }} Candidate
30
30
  */
31
31
 
32
- import path from 'node:path';
33
-
34
32
  import { scanBacklog } from '../backlog-scan.mjs';
35
33
  import { checkCiStatus as realCheckCiStatus } from '../ci-status-banner.mjs';
36
34
  import { probe as realProbe, evaluate as realEvaluate, DEFAULT_RESOURCE_THRESHOLDS as CANONICAL_RESOURCE_THRESHOLDS } from '../resource-probe.mjs';
37
- import { readCanonicalSessions } from '../sessions-canonical.mjs';
38
- import { isRealSession } from '../session-schema/filters.mjs';
35
+ import { isMeasuredSession, readCanonicalSessions } from '../sessions-canonical.mjs';
39
36
 
40
37
  /** Staleness cap (days). Beyond this, additional age does not raise the score. */
41
38
  export const STALENESS_CAP_DAYS = 90;
@@ -184,13 +181,14 @@ async function defaultFetchPriority(repoRoot, nowMs) {
184
181
  */
185
182
  async function defaultStaleDaysFor(repoRoot, nowMs) {
186
183
  try {
187
- const file = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
188
- const records = readCanonicalSessions({ filePath: file });
184
+ const records = readCanonicalSessions({ repoRoot });
189
185
  if (records.length === 0) return STALENESS_CAP_DAYS;
190
186
 
191
187
  let latestMs = null;
192
188
  for (const rec of records) {
193
- if (!isRealSession(rec)) continue;
189
+ // Audit E1: a synthetic backfill stub (even a `status: 'completed'` one)
190
+ // must not make a neglected repo look freshly touched.
191
+ if (!isMeasuredSession(rec)) continue;
194
192
  const iso = rec.completed_at || rec.started_at || null;
195
193
  if (!iso || typeof iso !== 'string') continue;
196
194
  const t = Date.parse(iso);
@@ -38,8 +38,8 @@
38
38
  */
39
39
 
40
40
  import { existsSync, statSync, readFileSync } from 'node:fs';
41
- import { join } from 'node:path';
42
41
  import { getProjectDir } from './platform.mjs';
42
+ import { resolveLedgerPath } from './ledger-path.mjs';
43
43
 
44
44
  // Ceiling (BV-004): one process per session that invoked session-start
45
45
  // (monitors/monitors.json `when: on-skill-invoke:session-start`), alive until
@@ -132,7 +132,7 @@ function fingerprint(absPath) {
132
132
  async function watchLoop(intervalS) {
133
133
  // #1517: the PROJECT root, never CLAUDE_PLUGIN_ROOT (the installed plugin
134
134
  // directory, whose metrics are not this repo's).
135
- const absPath = join(getProjectDir(), STATE_FILE_REL);
135
+ const absPath = resolveLedgerPath(STATE_FILE_REL, getProjectDir());
136
136
 
137
137
  // Startup event — confirms the watcher is alive even when nothing has
138
138
  // happened yet. Without this, a "no events" stream is indistinguishable
@@ -931,8 +931,9 @@ export function evaluateSession(opts = {}) {
931
931
  throw new Error('evaluateSession: model.id is required');
932
932
  }
933
933
 
934
- const sessionsPath = path.join(metricsDir, 'sessions.jsonl');
935
- // #1514 point 1 — the repo's ONE ledger (main checkout for a linked worktree).
934
+ // #1514 point 1 / #1592 — the repo's ONE ledger pair (main checkout for a
935
+ // linked worktree); resolving only events left the session unresolvable.
936
+ const sessionsPath = path.join(resolveEventsLedgerRoot(metricsDir), 'sessions.jsonl');
936
937
  const eventsPath = path.join(resolveEventsLedgerRoot(metricsDir), 'events.jsonl');
937
938
  // #1209: sessions.jsonl is APPEND-ONLY (the same physical session can carry
938
939
  // more than one line — crash-recovery re-appends, #1068 stub/supersede
@@ -8,7 +8,8 @@
8
8
  * 1. WHICH session? — resolveSession() implements the operator-approved
9
9
  * cascade (Decision #2, revised #822): explicit id > newest record (source
10
10
  * order) that is either status:'completed' or non-abandoned with completed
11
- * work. Abandoned records are ALWAYS skipped.
11
+ * work. The cascade uses only measured records and requires completion
12
+ * evidence; abandoned records remain ineligible for a completed-session eval.
12
13
  *
13
14
  * 2. WHOSE events? — quality_gate events in events.jsonl do not RELIABLY
14
15
  * carry a session_id (measured 2026-10-05, this repo's events.jsonl:
@@ -26,6 +27,8 @@
26
27
  * arrays. Determinism is load-bearing for the CLI `--verify` re-evaluation path.
27
28
  */
28
29
 
30
+ import { isMeasuredSession } from '../sessions-canonical.mjs';
31
+
29
32
  /** Thrown by resolveSession when no eligible session can be selected. */
30
33
  export class SessionResolutionError extends Error {
31
34
  constructor(message) {
@@ -81,6 +84,7 @@ export function resolveSession(records, sessionId) {
81
84
  for (let i = records.length - 1; i >= 0; i--) {
82
85
  const r = records[i];
83
86
  if (!isPlainObject(r)) continue;
87
+ if (!isMeasuredSession(r)) continue;
84
88
  if (r.status === 'abandoned') continue;
85
89
  if (r.status === 'completed') {
86
90
  return { record: r, resolvedVia: 'cascade-completed' };
@@ -96,7 +100,7 @@ export function resolveSession(records, sessionId) {
96
100
  }
97
101
 
98
102
  throw new SessionResolutionError(
99
- 'no completed session found (all records abandoned or without evidence of completed work)',
103
+ 'no completed session found (all records unmeasured or without evidence of completed work)',
100
104
  );
101
105
  }
102
106
 
@@ -19,6 +19,7 @@ import { mkdirSync, appendFileSync } from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { validateEvalRecord, normalizeEvalRecord } from './schema.mjs';
21
21
  import { readJsonlFile } from '../io.mjs';
22
+ import { resolveLedgerPath } from '../ledger-path.mjs';
22
23
 
23
24
  /** Default repo-relative path for the eval journal. */
24
25
  export const DEFAULT_EVAL_JSONL_PATH = '.orchestrator/metrics/eval.jsonl';
@@ -36,11 +37,11 @@ export const DEFAULT_EVAL_JSONL_PATH = '.orchestrator/metrics/eval.jsonl';
36
37
  *
37
38
  * @param {object} record — candidate session-eval record.
38
39
  * @param {{ path?: string }} [opts] — target path (absolute preferred); defaults
39
- * to DEFAULT_EVAL_JSONL_PATH (repo-relative).
40
+ * to `resolveLedgerPath(DEFAULT_EVAL_JSONL_PATH)` (#1722).
40
41
  * @returns {{ ok: true, record: object, path: string }
41
42
  * | { ok: false, reason: 'validation'|'fs-error', error: string }}
42
43
  */
43
- export function appendEvalRecord(record, { path: filePath = DEFAULT_EVAL_JSONL_PATH } = {}) {
44
+ export function appendEvalRecord(record, { path: filePath = resolveLedgerPath(DEFAULT_EVAL_JSONL_PATH) } = {}) {
44
45
  let validated;
45
46
  try {
46
47
  validated = validateEvalRecord(record);
@@ -71,7 +72,7 @@ export function appendEvalRecord(record, { path: filePath = DEFAULT_EVAL_JSONL_P
71
72
  * @param {string} [filePath] — absolute path to the eval journal.
72
73
  * @returns {object[]} normalized session-eval records, in source order.
73
74
  */
74
- export function readEvalRecords(filePath = DEFAULT_EVAL_JSONL_PATH) {
75
+ export function readEvalRecords(filePath = resolveLedgerPath(DEFAULT_EVAL_JSONL_PATH)) {
75
76
  const rows = readJsonlFile(filePath, { skipInvalid: true });
76
77
  return rows.map((r) => normalizeEvalRecord(r));
77
78
  }
@@ -109,8 +109,8 @@ const HEAD_BYTES = 64 * 1024;
109
109
  * `rg -n "readEventsWithRotations\(|scanEventsBackwards\(|listEventSourcesNewestFirst\(" --glob '!tests/**' scripts hooks skills`
110
110
  * → 6 call sites beside the definitions in `events.mjs` (and its internal
111
111
  * `listEventSourcesNewestFirst` call); the seventh, `session-close-backfill`,
112
- * joined with #1498. A test re-runs that census, so an eighth reader cannot
113
- * join unlisted.
112
+ * joined with #1498, the eighth, `session-work-trees`, with #1659. A test
113
+ * re-runs that census, so a ninth reader cannot join unlisted.
114
114
  */
115
115
  export const EVENTS_WINDOW_READERS = Object.freeze([
116
116
  { label: 'eval/engine', spec: './eval/engine.mjs' },
@@ -120,6 +120,7 @@ export const EVENTS_WINDOW_READERS = Object.freeze([
120
120
  { label: 'instruction-budget-guard', spec: './instruction-budget-guard.mjs' },
121
121
  { label: 'backfill-abandoned-sessions', spec: '../backfill-abandoned-sessions.mjs' },
122
122
  { label: 'session-close-backfill', spec: './session-close-backfill.mjs' },
123
+ { label: 'session-work-trees', spec: './session-work-trees.mjs' },
123
124
  ]);
124
125
 
125
126
  /**
@@ -99,6 +99,16 @@ export const ARCHIVE_NAME_RE =
99
99
  /** Upper bound of the legacy `.1`..`.N` ring (`max-backups` is capped at 20). */
100
100
  export const LEGACY_RING_MAX = 20;
101
101
 
102
+ /**
103
+ * Per-session work-tree record (#1659): written by hooks/pre-bash-staging-fence.mjs
104
+ * BEFORE a `git commit` runs, payload `{ session_id, tree, start_ref }` — the raw
105
+ * session id, the realpath of the committing tree's toplevel, and that tree's
106
+ * full `HEAD` sha before the commit. Read by scripts/lib/session-work-trees.mjs
107
+ * (first record per tree wins). Lives in this zero-import module so the hook
108
+ * and the reader share the name without either importing the other.
109
+ */
110
+ export const SESSION_WORK_TREE_EVENT = 'orchestrator.session.work_tree';
111
+
102
112
  /** Prefix marking an orchestrator-owned event. */
103
113
  export const ORCHESTRATOR_PREFIX = 'orchestrator.';
104
114