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
@@ -48,10 +48,14 @@ For session-end specifically: the preamble is DETECTION-ONLY. The lock-release p
48
48
 
49
49
  ```javascript
50
50
  try {
51
+ const { pathToFileURL } = await import('node:url');
51
52
  const { appendSkillInvocation, DEFAULT_SKILL_INVOCATIONS_PATH } =
52
- await import('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs');
53
- const nodePath = await import('node:path');
54
- await appendSkillInvocation(nodePath.join(process.cwd(), DEFAULT_SKILL_INVOCATIONS_PATH), {
53
+ await import(pathToFileURL('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs').href);
54
+ const { resolveLedgerPath } =
55
+ await import(pathToFileURL('${PLUGIN_ROOT}/scripts/lib/ledger-path.mjs').href);
56
+ // In a linked worktree the main checkout's ledger when it is gitignored there,
57
+ // so the record survives the worktree (#1722).
58
+ await appendSkillInvocation(resolveLedgerPath(DEFAULT_SKILL_INVOCATIONS_PATH, process.cwd()), {
55
59
  timestamp: new Date().toISOString(),
56
60
  event: 'selected',
57
61
  skill: 'session-orchestrator:session-end',
@@ -63,7 +67,7 @@ try {
63
67
 
64
68
  ## Phase 1: Plan Verification
65
69
 
66
- > Always runs, first. Reads back the agreed plan and dispositions every item: 1.1 Done (verify with evidence) + 1.1a File-Level Grounding, 1.2 / 1.3 / 1.4 which COLLECT carryover candidates and file nothing (#769), 1.5 Discovery Scan, 1.6 Safety Review + 1.6.6 "What Not To Retry", then **1.65 Handover Alignment Gate** — the only place `[Carryover]` filing is authorized to originate, fail-open to "everything carries" — followed by 1.7 Metrics Collection, 1.8 Session Review, and 1.9 / 1.10 Mission-Status classification + breakdown. Full procedure: [`plan-verification.md`](plan-verification.md).
70
+ > Always runs, first. Reads back the agreed plan and dispositions every item: 1.1 Done (verify with evidence) + 1.1a File-Level Grounding + 1.1b External Acceptance (open `[Abnahme]` issues, closed only on oracle evidence), 1.2 / 1.3 / 1.4 which COLLECT carryover candidates and file nothing (#769), 1.5 Discovery Scan, 1.6 Safety Review + 1.6.6 "What Not To Retry", then **1.65 Handover Alignment Gate** — the only place `[Carryover]` filing is authorized to originate, fail-open to "everything carries" — followed by 1.7 Metrics Collection, 1.8 Session Review, and 1.9 / 1.10 Mission-Status classification + breakdown. Full procedure: [`plan-verification.md`](plan-verification.md).
67
71
 
68
72
  ## Phase 2: Quality Gate
69
73
 
@@ -159,7 +163,9 @@ node scripts/release-session-lock.mjs --session-id "<raw session_id>" --json
159
163
 
160
164
  `--session-id` is the PHYSICAL raw value established by session-start Phase 1.2 and stored in `.orchestrator/session.lock` `session_id` — never STATE.md `session:` and never `semantic_session_id`, both of which are attribution labels.
161
165
 
162
- The command releases the lock only when that raw id owns it, emits the terminal `orchestrator.session.lock.released` breadcrumb, and then RE-READS the lock path: **exit 0 means the lock is provably gone.** It replaces the hand-executed `release()` this phase used to prescribe, which deleted the lock silently — the SessionEnd hook then found `status: 'absent'` and emitted nothing either, so a lock lifecycle ended with no terminal event at all (#1395: 5 `lock.acquired` against 0 `lock.released` in this repo). `outcome: "absent"` is a normal exit-0 result (idempotent re-run; nothing was released, so no event is written).
166
+ No `--repo-root`: the command looks for the checkout whose lock carries that id itself (#1730) — the registry's `session_root`, the cwd's checkout, its main checkout, a registered linked worktree — and names it in the JSON (`repo_root`, `root_source`). Before, it read only the cwd, answered `absent` with exit 0 when the lock sat in the other checkout, and the lock stayed.
167
+
168
+ The command releases the lock only when that raw id owns it, emits the terminal `orchestrator.session.lock.released` breadcrumb, and then RE-READS the lock path: **exit 0 means the lock is provably gone.** It replaces the hand-executed `release()` this phase used to prescribe, which deleted the lock silently — the SessionEnd hook then found `status: 'absent'` and emitted nothing either, so a lock lifecycle ended with no terminal event at all (#1395: 5 `lock.acquired` against 0 `lock.released` in this repo). `outcome: "absent"` is a normal exit-0 result (idempotent re-run; nothing was released, so no event is written). A verified release also leaves `.orchestrator/runtime/lock-released/<raw session_id>.json` (`release_marker: true` in the JSON), so a later `compact` or `resume` of the same raw session does not re-create the lock (#1651); `startup` and `clear` take a lock as before, and the session's next `acquire()` removes the marker.
163
169
 
164
170
  A non-zero exit is a WARN, never a blocker — log `⚠ session-lock: <outcome>` and continue; the TTL provides automatic expiry for the next session:
165
171
 
@@ -187,7 +193,7 @@ Use Conventional Commits format:
187
193
  type(scope): description
188
194
 
189
195
  - [bullet points of what changed]
190
- - Closes #IID1, #IID2 (if applicable)
196
+ - Closes #IID1, #IID2 (if applicable — never an `[Abnahme]` IID, plan-verification 1.1b)
191
197
 
192
198
  Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
193
199
  ```
@@ -218,7 +224,37 @@ between the markers and executes it, so no second copy of this command may exist
218
224
  # so a consumer repo (even one without a Session Config) skips with exit 0.
219
225
  # With a remote, Session Config `mirror` decides (#1034), read through parse-config.mjs:
220
226
  # `mirror: none` → never push, even with a 'github' remote (explicit opt-out)
221
- # `mirror: github` or key absent (`mirror: null` counts as absent) → push
227
+ # `mirror: github` or key absent (`mirror: null` counts as absent) → push HEAD,
228
+ # but only after `github-export.mjs --plain-guard` (#1645):
229
+ # when HEAD or origin's main (ls-remote, fetched if not
230
+ # local) carries .orchestrator/policy/public-export.json,
231
+ # the repo publishes curated only — a checkout whose
232
+ # CLAUDE.md predates `mirror: curated` must not push its
233
+ # full private history → WARN, exit 1, nothing pushed.
234
+ # origin unreadable → same refusal (fail closed); no
235
+ # 'origin' remote → refused unless `mirror: github` is
236
+ # set explicitly (nothing to check the policy against).
237
+ # `mirror: curated` → publish ONE squash snapshot (scripts/github-export.mjs,
238
+ # #1631) on top of the verified public github main,
239
+ # never HEAD. Source anchor: only when HEAD is on branch
240
+ # `main` AND equals origin's main (`git ls-remote`, after
241
+ # the 4.3 push) — GitHub main deploys production, so a
242
+ # feature branch or unpushed/unreviewed local commits
243
+ # skip (exit 0, nothing pushed); origin unreadable →
244
+ # WARN, exit 1. Export exit 3 = nothing to publish (no
245
+ # push), other non-zero → WARN, exit 1. Bound to the
246
+ # scanned sha (read from the export's stdout only):
247
+ # push URL must equal fetch URL, then `--verify <sha>`
248
+ # re-checks it right before the push (one commit on the
249
+ # ls-remote-verified github main, trailer
250
+ # `Source-Commit: <origin main>`, no excluded path in its
251
+ # tree), else WARN, exit 1; the push sends that sha, not
252
+ # the export ref. Plain push, no lease: the commit's
253
+ # parent IS the verified sha, and a plain push is
254
+ # fast-forward-only, so GitHub itself rejects it if its
255
+ # main moved to anything that is not an ancestor of that
256
+ # sha — the same anchor a lease would enforce, without a
257
+ # force flag (blocked by the destructive-command guard).
222
258
  # any other value / config unreadable → loud WARN, no push, exit 1
223
259
  # The plugin root is resolved, not assumed: a fresh /close shell often lacks
224
260
  # PLUGIN_ROOT. Order per skills/_shared/config-reading.md (native PLUGIN_ROOT,
@@ -260,8 +296,49 @@ elif ! mirror_cfg=$(printf '%s' "$cfg_json" | node -e 'let s="";process.stdin.on
260
296
  exit 1
261
297
  elif [ "$mirror_cfg" = "none" ]; then
262
298
  echo "GitHub mirror: 'mirror: none' in Session Config — skipping (not an error)."
263
- elif [ -n "$mirror_cfg" ] && [ "$mirror_cfg" != "github" ]; then
264
- echo "WARN GitHub mirror: unknown 'mirror: ${mirror_cfg}' (expected none | github) — nothing pushed." >&2
299
+ elif [ -n "$mirror_cfg" ] && [ "$mirror_cfg" != "github" ] && [ "$mirror_cfg" != "curated" ]; then
300
+ echo "WARN GitHub mirror: unknown 'mirror: ${mirror_cfg}' (expected none | github | curated) — nothing pushed." >&2
301
+ exit 1
302
+ elif [ "$mirror_cfg" = "curated" ]; then
303
+ if ! origin_ls=$(git ls-remote origin refs/heads/main 2>&1); then
304
+ echo "WARN GitHub mirror: cannot read origin's main — curated source not verifiable, nothing pushed." >&2
305
+ echo " git said: ${origin_ls}" >&2
306
+ exit 1
307
+ fi
308
+ origin_main=$(printf '%s\n' "$origin_ls" | awk '$2 == "refs/heads/main" { print $1 }')
309
+ if [ "$(git symbolic-ref --short -q HEAD)" != "main" ] || [ -z "$origin_main" ] || [ "$(git rev-parse HEAD)" != "$origin_main" ]; then
310
+ echo "GitHub mirror: 'mirror: curated' publishes only a HEAD on main that equals origin's main (HEAD $(git symbolic-ref --short -q HEAD || echo detached) $(git rev-parse --short HEAD), origin main ${origin_main:-none}) — skipping (not an error)."
311
+ else
312
+ # stdout carries ONLY the sha (stderr goes straight to the operator), so the
313
+ # sha pushed below is the one the export scanned — never a ref name that could
314
+ # be moved in between.
315
+ export_sha=$(GIT_NO_REPLACE_OBJECTS=1 node "$so_root/scripts/github-export.mjs" --source "$origin_main")
316
+ export_rc=$?
317
+ if [ "$export_rc" -eq 3 ]; then
318
+ echo "GitHub mirror: curated snapshot of ${origin_main} unchanged — nothing to publish."
319
+ elif [ "$export_rc" -ne 0 ]; then
320
+ echo "WARN GitHub mirror: curated export REFUSED (exit ${export_rc}, reason above) — nothing pushed." >&2
321
+ exit 1
322
+ elif ! printf '%s' "$export_sha" | grep -Eqx '[0-9a-f]{40}([0-9a-f]{24})?'; then
323
+ echo "WARN GitHub mirror: the export printed no commit sha — nothing pushed." >&2
324
+ exit 1
325
+ elif [ "$(git remote get-url github)" != "$(git remote get-url --push github)" ]; then
326
+ echo "WARN GitHub mirror: the push URL of 'github' differs from its fetch URL (pushurl or pushInsteadOf) — the verified main would not describe the push target; nothing pushed." >&2
327
+ exit 1
328
+ elif ! GIT_NO_REPLACE_OBJECTS=1 node "$so_root/scripts/github-export.mjs" --verify "$export_sha" --source "$origin_main"; then
329
+ echo "WARN GitHub mirror: ${export_sha} is not one clean snapshot of ${origin_main} on the verified github main — nothing pushed." >&2
330
+ exit 1
331
+ elif push_out=$(git push github "${export_sha}:refs/heads/main" 2>&1); then
332
+ echo "GitHub mirror: pushed curated snapshot ${export_sha} (source ${origin_main}) -> ${mirror_url}"
333
+ else
334
+ echo "WARN GitHub mirror PUSH FAILED: curated snapshot ${export_sha} is NOT on ${mirror_url}" >&2
335
+ echo "${push_out}" >&2
336
+ echo "WARN Mirror is stale — anything wired to it (site deploy) will not update." >&2
337
+ exit 1
338
+ fi
339
+ fi
340
+ elif ! GIT_NO_REPLACE_OBJECTS=1 node "$so_root/scripts/github-export.mjs" --plain-guard --mirror-key "${mirror_cfg:-unset}"; then
341
+ echo "WARN GitHub mirror: plain push of HEAD REFUSED (reason above) — this repo publishes curated only; nothing pushed." >&2
265
342
  exit 1
266
343
  elif push_out=$(git push github HEAD 2>&1); then
267
344
  echo "GitHub mirror: pushed $(git rev-parse HEAD) -> ${mirror_url}"
@@ -278,6 +355,12 @@ State 3 exits non-zero on purpose: it is the only machine-readable signal that t
278
355
  behind. Report it to the operator in the session summary; do not retry silently and do not
279
356
  swallow it with `|| true`.
280
357
 
358
+ ### 4.5 Only a Pipeline or Release Train Left
359
+
360
+ Do not wait for a pipeline or release train inside session-end. If the session has no other work left, apply `skills/_shared/fleet-protocol.md` § Rücklauf an den Navigator (Pflicht) with the wait case `zug-<name>`: exactly one report in this `/close` turn, only with fleet integration (`flotte: true`). That the waiting answer before `/close` already reported the train is intended — every waiting turn triggered by an input (`/close` is one) reports once (#1611); turns woken by `/loop`, a Monitor event, an own background agent or shell, or ScheduleWakeup do not. Write NO handover or train manifest here — a coordinator only acts on one written with the waiting answer before `/close` (same section, Übergabe und Zug-Manifest) — and never invoke `/close` or another close yourself. Then continue with Phase 4a; never gate the close on a reply (CSM-004).
361
+
362
+ **Queue marks on the own handover (#1639).** With fleet integration — whether or not a train is left — re-check the newest own handover or train manifest in the fleet folder (fleet-protocol § Flotten-Ordner und Session-Name gives `<name>`, `<cwd>` and `<startedAt>`): `node "$PLUGIN_ROOT/scripts/handover-queue-check.mjs" --name <name> --fleet-dir <cwd>/.orchestrator/runtime/fleet --since <startedAt> --apply --repo <repo> [--navigator-cli <koordinator-cli>]`, same `<repo>` and CLI as fleet-protocol Schritt 1a. It files every open point that still lacks a `queue:#`/`issue:#` mark and writes the id into the file; this edits the existing file and is not the forbidden new handover. `--since` keeps a predecessor's file of the same session name out (names repeat across days). Exit 3 (points left unmarked, e.g. the CLI is unreachable): name `Übergabe ohne Queue-IDs: <n> Punkte` with the listed points in the session summary. It never blocks the close; without fleet integration or without an own file it is a silent no-op (exit 0).
363
+
281
364
  ## Phase 4a: Auto-Promoted Worktree Cleanup (#575 P3.2)
282
365
 
283
366
  > Skip when `persistence: false`; skip silently unless the CURRENT worktree is an auto-promoted sibling — `detectAutoPromotedWorktree()` from `scripts/lib/session-end/worktree-cleanup.mjs` (marker `.orchestrator/promoted-from.json` first, legacy basename match as fallback, #1069). Runs AFTER the Phase 4 commit+push, never before (#490 durableCommit ordering). Clean worktree → auto-remove with WARN; dirty → the 3-option AUQ (`Behalten` / `Löschen` / `Manuell`) before any destructive action, per PSA-003. Full procedure: [`references/phase-4a-worktree-cleanup.md`](references/phase-4a-worktree-cleanup.md).
@@ -292,7 +375,7 @@ swallow it with `|| true`.
292
375
 
293
376
  ## Phase 6: Final Report
294
377
 
295
- Present to the user the **Session Summary**: Completed / Carried Over / Dropped at Handover Gate (#769) / New Issues Created / Unresolved Review Findings (MED-LOW, #617) / Metrics — including the Docs Health line rendered from the Phase 2.3 result and the Custom Phases line from Phase 2.5 — / Next Session Recommendations. Full template plus the Test-delta and Documentation-Coverage anchors: [`references/session-summary-template.md`](references/session-summary-template.md).
378
+ Present to the user the **Session Summary**: Completed / Abnahme offen (open `[Abnahme]` issues from Phase 1.1b — never counted as Completed, #1678) / Carried Over / Dropped at Handover Gate (#769) / New Issues Created / Unresolved Review Findings (MED-LOW, #617) / Metrics — including the Docs Health line rendered from the Phase 2.3 result and the Custom Phases line from Phase 2.5 — / Next Session Recommendations. Full template plus the Test-delta and Documentation-Coverage anchors: [`references/session-summary-template.md`](references/session-summary-template.md).
296
379
 
297
380
  ## Sub-File Reference
298
381
 
@@ -318,7 +401,7 @@ Present to the user the **Session Summary**: Completed / Carried Over / Dropped
318
401
  | `references/phase-3-documentation-updates.md` § 3.7d | Session-Eval (opt-in — #803) — `node scripts/eval-session.mjs --json` scores the just-closed session; gated on `eval.enabled` + `eval.mode != off` (parsed by `scripts/lib/config/eval.mjs`), optional `eval-judge` dispatch + `writeEvalReport`, advisory/never-blocks-close, ordered after 3.7 (record must exist) and before 3.4/Phase 4 (record committed with the session). Full flow in `skills/eval/SKILL.md` |
319
402
  | `references/phase-3-7e-vault-orphans.md` | Phase 3.7e Vault Orphans (#1563) — `node scripts/vault-orphans.mjs --since <started_at> --json` (`findVaultOrphans` in `scripts/lib/session-end/vault-orphans.mjs`, read-only `git ls-files --others`) names untracked vault files modified since the session start, minus `_generator: session-orchestrator-` files; candidates only (mtime is no proof), one Open Question via `appendOpenQuestionOnDisk`, never commits/moves/deletes in the vault; `unavailable` = "could not measure", never "no orphans"; gated on `vault-integration.enabled`, ordered after 3.7d and before 3.4 |
320
403
  | (inline) Phase 3.8 | Session Lock Release — `release()` uses the physical raw `session_id`; raw mismatch/absent is non-fatal but never repaired with semantic labels or proof (live ambiguity remains for TTL/Reaper); fs-errors are non-fatal; runs after STATE.md writes and before Phase 4 commit staging |
321
- | (inline) Phase 4 | Commit & Push — stage individually (PSA-004), commit, push to origin, then the `github-mirror-push` block (4 states: not-a-repo / no `github` remote / pushed / push failed) |
404
+ | (inline) Phase 4 | Commit & Push — stage individually (PSA-004), commit, push to origin, then the `github-mirror-push` block (4 states: not-a-repo / no `github` remote / pushed / push failed), then 4.5 (only a pipeline or train left → one fleet Rücklauf `zug-<name>` in this turn, fleet integration only; no manifest, no self-close) |
322
405
  | `references/phase-4a-worktree-cleanup.md` | Phase 4a full procedural body — auto-promoted-worktree detection (`detectAutoPromotedWorktree`, marker-keyed since #1069), clean-check, clean auto-remove path, dirty 3-option AUQ (`Behalten`/`Löschen`/`Manuell`), PSA-003 + #490 ordering rationale |
323
406
  | `references/phase-4b-worktree-orphan-sweep.md` | Phase 4b full procedural body — `checkWorktreeOrphans()` read-only proposal set, the coordinator-rendered AUQ, opt-in `worktree-orphans.enabled` gate |
324
407
  | `references/phase-5-issue-cleanup.md` | Phase 5 full procedural body — close resolved issues via `node "$PLUGIN_ROOT/scripts/lib/issue-close-strip-labels.mjs" --close` (strip `status:*` → close → re-read; `closed: false` is a finding, not done, #308), Step 3 filing of the Phase 1.65 carry-list incl. the deferred `createSpiralCarryoverIssue` and `markOpenQuestionAnsweredOnDisk`, Step 3b `[Backlog-Sammel]` overflow, discovery-issue creation |
@@ -36,6 +36,7 @@ process.stdout.write(JSON.stringify(tasks));
36
36
  Run the accessor in `plan-verification.md § SESSION_START_REF accessor`; it exports `$SESSION_RANGE`, one complete diff range. Summary:
37
37
  - Primary: the pinned `session-start-ref` from STATE.md frontmatter → `<sha>..HEAD`.
38
38
  - Field absent: the baseline `resolveBaselineRange()` resolves → `<base>...HEAD` (never a hard-coded `origin/main`).
39
+ - Multi-tree (#1659): also run `node scripts/session-commit-files.mjs` and treat its union as additional changed files (commits in the session's other own worktrees). The own-tree range above stays authoritative; no diff across trees is computed; skipped trees (stderr) are unmeasured, not unchanged.
39
40
  - Nothing resolves: `$SESSION_RANGE` is empty and the accessor names `no-baseline-ref` or `unresolvable-ref` — log `⚠ Phase 3.2: no session range (<reason>). Docs verification skipped.` and skip Phase 3.2. Never treat the empty range as "no docs changed".
40
41
 
41
42
  If `git diff` itself fails (network issue, corrupt repo), log:
@@ -147,12 +147,12 @@ import { runTailPhases } from '${PLUGIN_ROOT}/scripts/lib/session-end/tail-runne
147
147
 
148
148
  const tail = await runTailPhases({ repoRoot: process.cwd(), plan });
149
149
  const sweep = tail['3.6.4'];
150
- // { ran: true, scanned, archived, archivePath } | { ran: false, reason: 'plan-skip' | 'no-plan' | 'error', error? }
150
+ // { ran: true, scanned, archived, archivePath } | { ran: false, reason: 'plan-skip' | 'no-plan' | 'store-role-inbox' | 'error', error? }
151
151
  ```
152
152
 
153
153
  - `runTailPhases` delegates to `runExpiredSweep({ repoRoot, plan, now })` — the same module's single-phase entry point — and returns a KEYED shape so a caller keeps working when a second phase becomes mechanical. Today exactly one phase is: 3.6.3, 3.6.5–3.6.8 stay coordinator-executed because they are AUQ-gated or need a subagent dispatch a library function cannot make.
154
154
  - **Never throws, fails CLOSED.** Any error yields `{ ran: false, reason: 'error' }` and the close proceeds. Stale-past-grace entries move into `.orchestrator/metrics/learnings-archive.jsonl` (append-only, never deleted).
155
- - **Report** `sweep.ran`, `sweep.scanned` and `sweep.archived` in the Phase 6 Final Report, e.g. `expired-sweep: 12 archived of 640 scanned`. When `ran: false`, report the `reason` instead — a skipped sweep is a stated outcome, never silence.
155
+ - **Report** `sweep.ran`, `sweep.scanned` and `sweep.archived` in the Phase 6 Final Report, e.g. `expired-sweep: 12 archived of 640 scanned`. When `ran: false`, report the `reason` instead — a skipped sweep is a stated outcome, never silence. `store-role-inbox` means this host only appends to `learnings-inbox.jsonl` (owner.yaml `learnings.store-role: inbox`, #1516); the writer host sweeps.
156
156
  - **The proof it ran is the event `orchestrator.learnings.sweep_applied`** in `.orchestrator/metrics/events.jsonl` (payload source `session-end-3.6.4`, which separates it from the standalone CLI). A close claiming a sweep with no such event did not sweep.
157
157
 
158
158
  The standalone `node scripts/sweep-expired-learnings.mjs --apply --json` CLI remains available for manual/out-of-session use; it is no longer the session-end path.
@@ -180,17 +180,19 @@ After learnings are written (Phase 3.6), and when the judge is enabled, run a **
180
180
 
181
181
  2. Determine the **judged set** — only THIS session's selected skills.
182
182
 
183
- > **Join on the RAW UUID, never on the semantic session id.** The writer is the `PreToolUse` hook `hooks/skill-invocation-telemetry.mjs`, which stamps `session_id` straight from the hook payload (`:190`) — that is the harness UUID. Measured 2026-09-19 over `.orchestrator/metrics/skill-invocations.jsonl` (841 lines): **767 raw UUIDs, 55 semantic ids, 17 null**. Joining on `main-<date>-session-N` therefore matches ~0 rows for a current session, the judged set comes back empty, and the phase reports `empty-input` — a silent no-op indistinguishable from "this session used no skills". The raw id is `process.env.CLAUDE_CODE_SESSION_ID` (or `session_id` of a live `.orchestrator/session.lock`); it is also the id that names the transcript file in step 3.
183
+ > **Join on the RAW UUID, never on the semantic session id.** The writer is the `PreToolUse` hook `hooks/skill-invocation-telemetry.mjs`, which stamps `session_id` straight from the hook payload (`:190`) — that is the harness UUID. Measured 2026-09-19 over `.orchestrator/metrics/skill-invocations.jsonl` (841 lines): **767 raw UUIDs, 55 semantic ids, 17 null**. Joining on `main-<date>-session-N` therefore matches ~0 rows for a current session, the judged set comes back empty, and the phase reports `empty-input` — a silent no-op indistinguishable from "this session used no skills". The raw id is `process.env.CLAUDE_CODE_SESSION_ID`; it is also the id that names the transcript file in step 3. The repo-global `.orchestrator/session.lock` may name a PEER session (#1664, same class as #1663), so its `session_id` counts only via `resolveSessionIds()`, which returns it only when it is among this process's own ids (`readProcessLocalSessionIds()`) and `null` otherwise. No provable id → empty judged set → `empty-input`, never a judgment of the peer's transcript.
184
184
 
185
185
  ```javascript
186
186
  import { readFileSync } from 'node:fs';
187
- import { readLock } from '${PLUGIN_ROOT}/scripts/lib/session-lock.mjs';
187
+ import { resolveSessionIds } from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
188
188
 
189
+ // Lock id only when provably OWN — never a peer's (#1664).
189
190
  const rawSessionId =
190
191
  (process.env.CLAUDE_CODE_SESSION_ID || '').trim() ||
191
- (readLock({ repoRoot: process.cwd() })?.session_id || '').trim();
192
+ resolveSessionIds({ repoRoot: process.cwd() }).session_id;
192
193
 
193
- const selectedSkills = [...new Set(
194
+ // No id → no join: `null` would otherwise match the null-session rows.
195
+ const selectedSkills = !rawSessionId ? [] : [...new Set(
194
196
  readFileSync('.orchestrator/metrics/skill-invocations.jsonl', 'utf8')
195
197
  .split('\n').filter(Boolean)
196
198
  .flatMap((l) => { try { return [JSON.parse(l)]; } catch { return []; } })
@@ -207,6 +209,7 @@ After learnings are written (Phase 3.6), and when the judge is enabled, run a **
207
209
  import { buildSkillEvidence } from '${PLUGIN_ROOT}/scripts/lib/skill-evidence-window.mjs';
208
210
  import { runSkillJudge, evidenceBudgetChars } from '${PLUGIN_ROOT}/scripts/lib/skill-judge.mjs';
209
211
  import { appendSkillJudgment } from '${PLUGIN_ROOT}/scripts/lib/skill-judgments-schema.mjs';
212
+ import { resolveLedgerPath } from '${PLUGIN_ROOT}/scripts/lib/ledger-path.mjs';
210
213
  import path from 'node:path';
211
214
 
212
215
  const budgetTokens = config['skill-evolution']['judge-budget-tokens'] ?? 8000;
@@ -253,7 +256,8 @@ After learnings are written (Phase 3.6), and when the judge is enabled, run a **
253
256
  4. On `result.status === 'ok'`: the **COORDINATOR** writes each returned judgment to the sidecar. Stamp the per-record metadata (`timestamp`, `event: 'judged'`, `session_id`, `advisory: true`, `model`, `schema_version: 1`) and append:
254
257
 
255
258
  ```javascript
256
- const judgmentsPath = path.join(process.cwd(), '.orchestrator/metrics/skill-judgments.jsonl');
259
+ // In a linked worktree the main checkout's ledger when it is gitignored there (#1722).
260
+ const judgmentsPath = resolveLedgerPath('.orchestrator/metrics/skill-judgments.jsonl', process.cwd());
257
261
  const nowIso = new Date().toISOString();
258
262
  for (const j of result.judgments) {
259
263
  await appendSkillJudgment(
@@ -24,6 +24,10 @@ try {
24
24
  // parallel-session CWD drift). Passing an undefined repoRoot here sends every close down
25
25
  // the fail-open catch below and silently omits all 5 fields (GitLab #1036).
26
26
  const repoRoot = execSync('git rev-parse --show-toplevel', {encoding: 'utf8'}).trim();
27
+ // SAME path Phase 3.4 writes `status: completed` into (#1640): the STATE.md
28
+ // this session's state-init wrote. Pass it explicitly — never let the
29
+ // resolver re-guess, or a foreign .claude/STATE.md can take the fields.
30
+ const statePath = '<state-dir>/STATE.md';
27
31
 
28
32
  // In-memory session metrics — pulled from the session's running state,
29
33
  // NOT re-read from sessions.jsonl (which was just-written in Phase 3.7).
@@ -41,8 +45,9 @@ try {
41
45
  'rationale': rec.rationale,
42
46
  };
43
47
 
44
- await updateFrontmatterFieldsOnDisk(repoRoot, fields);
45
- console.log('Recommendations written: ' + rec.mode + ' (' + rec.rationale + ')');
48
+ const res = await updateFrontmatterFieldsOnDisk(repoRoot, fields, {statePath});
49
+ if (res.reason) throw new Error('STATE.md not written (' + res.reason + '): ' + res.path);
50
+ console.log('Recommendations written to ' + res.path + ': ' + rec.mode + ' (' + rec.rationale + ')');
46
51
  } catch (err) {
47
52
  // AC3: defensive — exception must NOT block Phase 3.4 status: completed.
48
53
  mkdirSync('.orchestrator/metrics', {recursive: true});
@@ -63,6 +68,8 @@ try {
63
68
 
64
69
  **Repo-root contract (#1036 / Kanevry#65):** `updateFrontmatterFieldsOnDisk(repoRoot, fields)` takes the repo root as its FIRST argument and rejects a missing one via `requireRepoRoot()` — there is no implicit `process.cwd()` fallback, deliberately (parallel-session CWD drift, PSA rules). Run the snippet verbatim, including the `repoRoot` binding: an `undefined` first argument throws *before* STATE.md is touched, and the AC3 catch in the snippet turns that throw into a green close with all 5 fields missing. That failure is invisible in STATE.md — it looks exactly like "this session produced no recommendation".
65
70
 
71
+ **Shared path source (#1640):** `statePath` is the one `<state-dir>/STATE.md` value this close uses for BOTH Phase 3.7a and Phase 3.4 — substitute the identical path in both snippets. The writer still refuses a target that is another session's COMPLETED record (`reason: 'foreign-state-md'`, stderr names the file); it never overwrites such a record and never redirects the write. Phase 3.4 re-stamps `session-id` to the closing process's own id, so a re-run of this phase after 3.4 still finds its own file.
72
+
66
73
  **Data source guarantee:** The three inputs (`completionRate`, `carryoverRatio`, `carryoverIssues`) MUST come from the in-memory session metrics object built in Phase 1.7, NOT from a re-read of `.orchestrator/metrics/sessions.jsonl`. Reading the just-written JSONL would introduce a circular dependency and risk reading a truncated line if Phase 3.7's `appendJsonl` was mid-flush.
67
74
 
68
75
  **Field precision:**
@@ -43,6 +43,7 @@ Read back the session plan that was agreed at the start. For EACH planned item:
43
43
  - **Verify with evidence**: read the changed files, check git diff, run relevant test
44
44
  - Confirm acceptance criteria are met
45
45
  - Mark as completed
46
+ - An `[Abnahme]` issue is never a Done item, even when its implementation landed this session — it goes to 1.1b (#1678)
46
47
 
47
48
  ### 1.1a File-Level Grounding
48
49
 
@@ -51,7 +52,7 @@ Read back the session plan that was agreed at the start. For EACH planned item:
51
52
  Compare the files the plan said would be touched against the files actually changed in the session. Catches both **scope creep** (files changed that were not in any agent's prompt scope) and **incomplete coverage** (files in the plan that were never edited).
52
53
 
53
54
  1. **Planned files** = union of all file paths from agent prompt scopes across all waves. Source: STATE.md Wave History, falling back to the original session plan's per-agent "Files:" specs. Glob patterns are expanded against the working tree at session-start time.
54
- 2. **Actual files** = `git diff --name-only "$SESSION_RANGE"` (accessor above: the pinned `session-start-ref`, else the resolved baseline range). Empty `$SESSION_RANGE` → skip 1.1a with the accessor's reason.
55
+ 2. **Actual files** = `git diff --name-only "$SESSION_RANGE"` (accessor above: the pinned `session-start-ref`, else the resolved baseline range). Empty `$SESSION_RANGE` → skip 1.1a with the accessor's reason. **Multi-tree (#1659):** also run `node scripts/session-commit-files.mjs` (session id defaults to `$CLAUDE_CODE_SESSION_ID`; `--json` prints `{ trees, perTree, skipped, union, malformed }`) and add its union — files committed in the session's OTHER worktrees, recorded by `hooks/pre-bash-staging-fence.mjs` as `orchestrator.session.work_tree` events — to Actual. The own-tree `session-start-ref` range stays authoritative for the own tree; the CLI diffs each recorded tree only inside itself, never across trees. Trees it lists as skipped (stderr) were not measured: report them, do not read them as "no changes".
55
56
  3. **Compute discrepancies:**
56
57
  - **Touched** = files in both Planned and Actual
57
58
  - **Unplanned (scope creep)** = files in Actual but not in Planned
@@ -83,6 +84,20 @@ Compare the files the plan said would be touched against the files actually chan
83
84
  ```
84
85
  The metrics field is conditional on `grounding-check: true` — when the gate is off, omit the field entirely (do not write `null`).
85
86
 
87
+ ### 1.1b External Acceptance (`[Abnahme]` issues, #1678)
88
+
89
+ `/plan` files every PRD § External Acceptance row as its own issue `[Abnahme] <Kriterium>` — real-world acceptance by the owner or an external oracle, which the implementation issue does not close. List the OPEN ones, separate from 1.1:
90
+
91
+ ```bash
92
+ glab issue list --search "[Abnahme]" --in title --per-page 100 # GitLab (open is the default)
93
+ gh issue list --search "[Abnahme] in:title" --state open --limit 100 # GitHub
94
+ ```
95
+
96
+ - **Title filter:** keep only titles that START with `[Abnahme]` — GitHub search ignores punctuation and also matches e.g. "Abnahme-Checkliste".
97
+ - **Scope:** keep only issues linked to this session's planned issues or their epic (issue links, `Epic`/`Parent` reference in the body, shared epic label). If the link is not determinable, list all open `[Abnahme]` issues and say so in the report (`scope: all open — link not determinable`).
98
+ - **Per issue:** `#<IID> <Kriterium> — owner: <from the body, else assignee, else "Owner"> — not-before: <date from the body or a label, else —>`. `/plan` writes owner, not-before, pass condition, fallback and oracle command into the body (`skills/plan/SKILL.md` § 6.1), so read the body first.
99
+ - **Closing rule:** an `[Abnahme]` issue is closed ONLY with oracle evidence in the closing comment — the command plus its result, or the owner's observation, quoted and dated. A green test alone is not enough. session-end never closes one on its own because the implementation landed, and files no carryover candidate for it (it stays open by design); it reports it in Phase 6 under **Abnahme offen**.
100
+
86
101
  ### 1.2 Partially Done Items
87
102
  - Document what was completed and what remains
88
103
  - **Do NOT file the carryover issue here (#769).** Collect a carryover **candidate** instead — append it to the in-memory candidate list that the Phase 1.65 Handover Alignment Gate consumes. The issue is filed (only if the gate confirms it) in Phase 5 Step 3. Candidate record (JS keys as `routeCandidates` / `normalizeCandidate` read them — `source-phase`→`sourcePhase`, `origin-issue`→`originIssue`; see `plan-verification.md § Candidate Record Format`):
@@ -11,7 +11,9 @@
11
11
  > // so a long-idle deep session's 4h-TTL lock does not lapse mid-close.
12
12
  > // BEFORE Phase 3.8 lock-release (which deletes the lock).
13
13
  > import { updateHeartbeat } from 'scripts/lib/session-lock.mjs';
14
- > updateHeartbeat({ sessionId, repoRoot: process.cwd() });
14
+ > import { resolveOwnLockRootFor } from 'scripts/lib/own-lock-root.mjs';
15
+ > // The checkout holding this session's lock, not the cwd's (#1730) — at the wrong root nothing is written.
16
+ > updateHeartbeat({ sessionId, repoRoot: resolveOwnLockRootFor({ sessionId }).root });
15
17
  > ```
16
18
  >
17
19
  > Skip silently if `persistence: false` in Session Config (no session.lock exists in that mode).
@@ -63,24 +65,24 @@ Review `<state-dir>/rules/` files that are relevant to this session's work:
63
65
 
64
66
  ### 3.4 Update STATE.md
65
67
 
66
- > **Ownership Reference:** See `skills/_shared/state-ownership.md`. session-end is authorized to set `status: completed` plus the optional `updated` timestamp (#184), and — as of Phase A of Epic #271 — the 5 Recommendation fields written by Phase 3.7a. No other fields.
68
+ > **Ownership Reference:** See `skills/_shared/state-ownership.md`. session-end is authorized to set `status: completed` plus the optional `updated` timestamp (#184), the final `current-wave`, the re-stamped `session-id` (#1640, see step 1), and — as of Phase A of Epic #271 — the 5 Recommendation fields written by Phase 3.7a. No other fields.
67
69
 
68
70
  > **Runtime Ordering Note (Epic #271 Phase A):** Phase 3.4's `status: completed` write executes LAST in Phase 3, AFTER Phase 3.7 (sessions.jsonl) and Phase 3.7a (Compute and Write Recommendations). The ordinal position here (3.4) is kept for historical compatibility; the canonical runtime order is `3.1 → 3.2 → 3.3 → 3.4a → 3.5 → 3.5a → 3.6 → 3.6.3 → 3.6.4 → 3.6.6 → 3.6.8 → 3.7 → 3.45 → 3.7a → 3.7b → 3.7c → 3.7d → 3.7e → 3.4` (3.6.3/3.6.4/3.6.6 were missing from this note pre-#724; 3.6.5 Auto-Dream and 3.6.7 Auto-Dialectic were RETIRED 2026-09-09 — see `phase-3-6-tail.md` — leaving four live tail phases, not six; the Tail-Diät skip-plan dispatcher dispatches exactly those four mechanically). Rationale: Phase 3.7a reads in-memory session metrics and writes the 5 Recommendation fields via `updateFrontmatterFields`; that write must complete BEFORE the STATE.md frontmatter is finalized with `status: completed` so the Recommendation fields are visible to the next session-start while STATE.md is still `status: active`. Crash-resilience: if `/close` aborts between 3.7a and 3.4, STATE.md carries `status: active` + Recommendations; session-start Phase 1.5 offers resume (and the banner renders). If the reverse ordering were used (status: completed first), a crash would leave `status: completed` without Recommendations — the Reader would silently no-op the banner, losing the handoff. Phase 3.45 (Telemetry Flush, #844) sits AFTER Phase 3.7 because it drains the send-queue with the just-written `sessions.jsonl` record already included, and BEFORE Phase 3.7a because it is a fire-and-forget side-effect with no dependency on the Recommendation-write ordering below it. Phase 3.7d (Session-Eval, #803) sits AFTER Phase 3.7 because it scores the `sessions.jsonl` record that phase just wrote — the record must exist first — and BEFORE Phase 3.4 because its `eval.jsonl` output is advisory and must never block the close.
69
71
 
70
72
  > Gate: Only run if `persistence` is enabled in Session Config and `<state-dir>/STATE.md` exists.
71
- 1. Set frontmatter `status: completed`
72
- 2. Record final wave count and completion time in the frontmatter
73
- 3. Touch `updated: <ISO 8601 UTC>` in the frontmatter (issue #184). Use `scripts/lib/state-md.mjs` → `touchUpdatedField` for safety:
73
+ 1. Finalize the frontmatter in ONE lock-guarded write, into the SAME file Phase 3.7a wrote (#1640 — identical `statePath`, passed explicitly): `status: completed`, `updated: <ISO 8601 UTC>` (#184), the final wave count as `current-wave`, and `session-id` re-stamped to this process's own raw id when one is measurable (after a mid-session `/clear` the record still names the pre-`/clear` id; the re-stamp keeps it this session's). Do not hand-edit any of these fields:
74
74
  ```bash
75
75
  node --input-type=module -e "
76
- import {readFileSync, writeFileSync} from 'node:fs';
77
- import {touchUpdatedField} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
78
- const p = '<state-dir>/STATE.md';
79
- writeFileSync(p, touchUpdatedField(readFileSync(p, 'utf8'), new Date().toISOString()));
76
+ import {execSync} from 'node:child_process';
77
+ import {markStateMdCompletedOnDisk} from '${PLUGIN_ROOT}/scripts/lib/state-md/frontmatter-mutators.mjs';
78
+ const repoRoot = execSync('git rev-parse --show-toplevel', {encoding: 'utf8'}).trim();
79
+ const statePath = '<state-dir>/STATE.md'; // same value as Phase 3.7a
80
+ const res = await markStateMdCompletedOnDisk(repoRoot, {statePath, currentWave: <final wave number>});
81
+ console.log(res.written ? 'STATE.md completed: ' + res.path : 'STATE.md NOT written (' + (res.reason ?? 'no change') + '): ' + res.path);
80
82
  "
81
83
  ```
82
- Silent no-op if the file has no frontmatter.
83
- 4. Keep the file as a record — do NOT delete it (next session-start reads it)
84
+ No-op if the file has no frontmatter. `reason: 'foreign-state-md'` means the path holds another session's completed record — report it as a Deviation, never hand-edit it.
85
+ 2. Keep the file as a record — do NOT delete it (next session-start reads it)
84
86
 
85
87
  If STATE.md doesn't exist, skip this subsection.
86
88
 
@@ -233,7 +235,7 @@ const boardResult = await mirrorBoard({
233
235
 
234
236
  Evaluate the just-closed session deterministically and, when configured, with an advisory LLM judge. This phase is a thin hook — the full evaluation flow lives in `skills/eval/SKILL.md`; only the close-out integration is described here.
235
237
 
236
- 1. **Deterministic run.** Invoke `node scripts/eval-session.mjs --json` — with no `--session`, the cascade (`resolveSession`, revised #822) walks records newest-to-oldest (source order) and evaluates the first one that is either `status:'completed'` or non-abandoned with evidence of completed work (typically the record Phase 3.7 just appended). Model capture: the coordinator passes `--model-id <id> --model-source self-report`; the `$ANTHROPIC_MODEL` env var wins automatically when set. Pass the configured pseudonym through with `--handle <eval.handle>` (omit when `null`). The CLI appends the eval record to `.orchestrator/metrics/eval.jsonl` (`appendEvalRecord` is never-throw).
238
+ 1. **Deterministic run.** Invoke `node scripts/eval-session.mjs --json` — with no `--session`, the cascade (`resolveSession`, revised #822) walks records newest-to-oldest (source order) and evaluates the first one that is either `status:'completed'` or non-abandoned with evidence of completed work (typically the record Phase 3.7 just appended). Model capture: the coordinator passes `--model-id <id> --model-source self-report`; the `$ANTHROPIC_MODEL` env var wins automatically when set. Pass the configured pseudonym through with `--handle <eval.handle>` (omit when `null`). The CLI appends the eval record to `.orchestrator/metrics/eval.jsonl` (`appendEvalRecord` is never-throw); run from a linked worktree, that is the main checkout's journal when the worktree ignores it (`resolveLedgerPath`, #1722), so the record survives the worktree.
237
239
  2. **Advisory judge (opt-in).** When `eval.judge` is not `off`, run the judge flow per `skills/eval/SKILL.md` § Phase 3: the coordinator dispatches the read-only `eval-judge` agent (DI'd dispatch, untrusted-data nonce fence), merges the advisory judge dimensions (`method: "judge"`, `advisory: true`, `calibration_status: "uncalibrated"`) into the record, and appends the merged record. When `eval.judge: off`, no agent is dispatched and no judge dimensions are produced.
238
240
  3. **HTML report (opt-in).** When `eval.report: html` (the default), call `writeEvalReport(record, …)` from `scripts/lib/eval/report.mjs` to emit the self-contained run report under `.orchestrator/eval/reports/<run-id>.html` (gitignored, regenerable from the record). When `eval.report: none`, skip the report.
239
241
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **VCS Reference:** Use CLI commands per the "Common CLI Commands" section of the gitlab-ops skill.
8
8
 
9
- 1. **Close resolved issues — one command strips, closes and verifies (#308):** run the CLI ONCE with every resolved issue ID, `--vcs` taken from Session Config:
9
+ 1. **Close resolved issues — one command strips, closes and verifies (#308):** run the CLI ONCE with every resolved issue ID, `--vcs` taken from Session Config. An `[Abnahme]` issue is never in that list: it closes only on oracle evidence (`plan-verification.md` § 1.1b, #1678).
10
10
 
11
11
  ```bash
12
12
  node "$PLUGIN_ROOT/scripts/lib/issue-close-strip-labels.mjs" --close --vcs <gitlab|github> <iid> [<iid> ...]
@@ -11,6 +11,10 @@ Present to the user:
11
11
  - [x] Issue #N: [description] — [evidence: tests passing, files changed]
12
12
  - [x] Issue #M: [description]
13
13
 
14
+ ### Abnahme offen (implemented ≠ accepted — never counted as Completed) [#1678]
15
+ - [ ] #<IID> [Abnahme] <Kriterium> — owner: <body | assignee | Owner> — not-before: <date | —>
16
+ - scope: [linked to this session's plan/epic | all open — link not determinable]
17
+
14
18
  ### Carried Over
15
19
  - [ ] Issue #P: [what's left] — new issue #Q created
16
20
  - [ ] [description] — blocked by [reason]
@@ -13,9 +13,10 @@
13
13
  >
14
14
  > Minimal invocation:
15
15
  > ```bash
16
- > printf '%s' "$METRICS_ENTRY" | node "$PLUGIN_ROOT/scripts/emit-session.mjs" --file .orchestrator/metrics/sessions.jsonl
16
+ > printf '%s' "$METRICS_ENTRY" | node "$PLUGIN_ROOT/scripts/emit-session.mjs"
17
17
  > ```
18
18
  > See step 2 below for the full invocation including exit-code handling.
19
+ > Pass no `--file`: the default is the main checkout's `sessions.jsonl` (#1584). A relative `--file` is taken verbatim and, run from a linked worktree, writes the worktree's own copy, which is gitignored here and lost with the worktree (#1722).
19
20
 
20
21
  1. Ensure `.orchestrator/metrics/` directory exists: `mkdir -p .orchestrator/metrics`
21
22
 
@@ -29,7 +30,7 @@
29
30
 
30
31
  > **#701.2 DOC NOTE — `completed_at >= started_at` guard:** This invariant is enforced mechanically by `scripts/emit-session.mjs`. The writer applies `clampTimestampsMonotonic()` (from `scripts/lib/session-schema/timestamps.mjs`) before `validateSession()`, clamping any inversion of `completed_at < started_at` to `started_at` and recording forensics in `_clamped: true` / `_original_completed_at`. Previously-inverted entries (e.g. `main-2026-06-21-session-4`) are already corrected. **No per-session coordinator action is needed** — the writer enforces the invariant at write time. Do not add defensive clamping logic here; the canonical guard lives in `emit-session.mjs`.
31
32
 
32
- 1a. **Token Rollup (#644, #1244 — mechanical since #1436)** — no coordinator action. `scripts/emit-session.mjs` (step 2) runs `rollupSessionTokens()` itself whenever the incoming record carries no `total_tokens`: it resolves the session's raw UUID from `.orchestrator/current-session.json` (adopted only when its `semantic_session_id` equals the record's `session_id` and no process-local session id contradicts it), joins the sibling `subagents.jsonl` on `parent_session_id`, and fills `total_tokens`, `total_token_input`, `total_token_output`, the three cache buckets, `total_cost_usd`, `subagents_with_tokens`, `matched_records`, `cost_records_priced`, `cost_records_total` and `_token_schema` for every key the record does not already carry — plus `raw_session_id`. No matching records, an unreadable ledger or a foreign marker omits the fields with a WARN on stderr, never a fabricated `0`. An explicit key on `$METRICS_ENTRY` still wins. The same UUID also backs `session_start_ref` when STATE.md has none (the `head_sha` of the session's own `orchestrator.session.started` event).
33
+ 1a. **Token Rollup (#644, #1244 — mechanical since #1436)** — no coordinator action. `scripts/emit-session.mjs` (step 2) runs `rollupSessionTokens()` itself whenever the incoming record carries no `total_tokens`: it resolves the session's raw UUID from `.orchestrator/current-session.json` (adopted only when its `semantic_session_id` equals the record's `session_id` and no process-local session id contradicts it), joins the sibling `subagents.jsonl` on `parent_session_id`, and fills `total_tokens`, `total_token_input`, `total_token_output`, the three cache buckets, `total_cost_usd`, `subagents_with_tokens`, `matched_records`, `cost_records_priced`, `cost_records_total` and `_token_schema` for every key the record does not already carry — plus `raw_session_id`. No matching records, an unreadable ledger or a foreign marker omits the fields with a WARN on stderr, never a fabricated `0`. An explicit key on `$METRICS_ENTRY` still wins. The same UUID also backs `session_start_ref` when STATE.md has none (the `head_sha` of the session's own `orchestrator.session.started` event) — except when the session's own registry entry shows `work_root` ≠ `session_root` (#1652): that sha was measured in the SessionStart root, not the tree the session works in, so the field is omitted with a WARN.
33
34
 
34
35
  **Semantics:** `null` totals mean "no token data was captured for this session" — this is NOT the same as zero cost. Do NOT coerce null to 0 when displaying or summing across sessions. The same holds for `total_cost_usd`: it is omitted whenever `cost_records_priced < cost_records_total` (#1475) — at least one cost-relevant subagent record could not be priced (a model the price table does not know, a token-bearing turn naming no model, a transcript that yielded no tokens, e.g. one over the hook's size cap, or an agent with a start record whose stop found no transcript of its own, mostly `workflow-subagent`, or an agent known only from `schema_version` 1 records — #1487 Pkt 1) — and also when `cost_records_total` is 0, because there was nothing to price. It is never "$0" for an unknown cost and never a partial sum over the v2 agents the ledger can see. Phantom stops (#939, no start record) are no agents and never count. The two counters are persisted beside it, so a missing cost says whether part of the session was unpriceable or nothing was priceable. Blind spots remain: an agent whose start record fell outside the hook's tail window looks like a phantom, an agent with neither start nor stop record is invisible. A session spanning the 2026-09-09 schema boundary therefore reports no cost (its v1 agents are unpriced candidates since 2026-10-02; before, it was priced over its v2 records only — 4 distinct fleet sessions carried such a partial sum). A subagent whose turns span several models carries a per-model `models_usage` breakdown in `subagents.jsonl` (#1470) and is priced part by part. A written `total_cost_usd` is the **API list-price equivalent** of the metered tokens, never a payment: subscription quota, purchased credits and the amount actually billed are invisible to the rollup, and no record states any of them. Since #1470 emit-session marks this with `cost_basis: "api-list-equivalent"` beside every `total_cost_usd` it writes, and writes `pricing_table_date` (the `PRICING_TABLE_DATE` of `scripts/lib/telemetry/pricing.mjs` that priced the session) beside the cost counters. A record without these fields predates #1470; its cost is a list-price estimate too, priced by an older table, not a verified bill. The emit-session warning names the reason via `match_status` (ledger absent, ledger with no readable records, or records present but none for the own raw UUID); none of these means zero cost.
35
36
 
@@ -39,7 +40,7 @@
39
40
 
40
41
  2. Append the prepared JSONL entry (from Phase 1.7; the writer itself adds the token fields — step 1a) via the validating writer `scripts/emit-session.mjs` (issue #249):
41
42
  ```bash
42
- printf '%s' "$METRICS_ENTRY" | node "$PLUGIN_ROOT/scripts/emit-session.mjs" --file .orchestrator/metrics/sessions.jsonl
43
+ printf '%s' "$METRICS_ENTRY" | node "$PLUGIN_ROOT/scripts/emit-session.mjs"
43
44
  EMIT_EXIT=$?
44
45
  if [[ $EMIT_EXIT -eq 1 ]]; then
45
46
  echo "ERROR: session-end validation failed — entry rejected by scripts/emit-session.mjs. See stderr above. Session metrics NOT written." >&2
@@ -199,7 +200,9 @@
199
200
  # gate. Non-blocking: a failure surfaces a warning and never blocks close,
200
201
  # using the same strict/warn degradation idiom as the vault-mirror step above.
201
202
  NM_OUTPUT=$(node -e "
202
- import('$PLUGIN_ROOT/scripts/lib/vault-status/narrative-mirror.mjs').then(async (m) => {
203
+ import('node:url').then(({ pathToFileURL }) =>
204
+ import(pathToFileURL('$PLUGIN_ROOT/scripts/lib/vault-status/narrative-mirror.mjs').href)
205
+ ).then(async (m) => {
203
206
  const r = await m.mirrorNarrative({ repoRoot: process.cwd() });
204
207
  process.stdout.write(JSON.stringify(r));
205
208
  }).catch((e) => { process.stderr.write(String(e && e.message || e)); process.exit(3); });
@@ -231,7 +231,7 @@ It prints one JSON line carrying:
231
231
  - `wavesConfigHonored` — whether the Session Config `waves` value was used
232
232
  - `notes` — human-readable reasons for any of the above
233
233
 
234
- **By default, the plan's wave list IS that output.** The coordinator fills tasks into the returned waves without adding, removing, or renumbering them. Exceptions are the empty-role rule below (and its coordinator-direct carve-out) and the user-authorized housekeeping execution deviation above. `--known-scope true` is what drops the Discovery wave on a deep session; `--profile ultradeep` is what selects the ultradeep shape, and it applies ONLY when STATE.md frontmatter carries `session-profile: ultradeep` (written by the `/session ultradeep` argument alias — see `commands/session.md`). `session-type` stays `deep`; the profile changes the wave SHAPE, nothing else, and it ignores the Session Config `waves` value (the shape says so in `wavesConfigHonored` / `notes`). Spec: `docs/prd/2026-09-06-ultradeep-session-profile.md` § 5.
234
+ **By default, the plan's wave list IS that output.** The coordinator fills tasks into the returned waves without adding, removing, or renumbering them. Exceptions are the empty-role rule below (and its coordinator-direct carve-out) and the user-authorized housekeeping execution deviation above. `--known-scope true` is what drops the Discovery wave on a deep session; `--profile ultradeep` is what selects the ultradeep shape, and it applies ONLY when STATE.md frontmatter carries `session-profile: ultradeep` (written by the `/session ultradeep` argument alias — see `commands/session.md`). `session-type` stays `deep`; the profile changes the wave SHAPE, nothing else, and it ignores the Session Config `waves` value (the shape says so in `wavesConfigHonored` / `notes`). Spec: the internal ultradeep session-profile PRD (2026-09-06, not in the public mirror) § 5.
235
235
 
236
236
  **Ultradeep agent counts per wave:** take each wave's cap from that wave's `agentCap` in the shape — there is no second table here to disagree with it. The caps are ceilings, not targets, and the Quality wave's cap is still EARNED per the Step 3 rule (the shape marks it `qualityEarned: true`); Research and Code-Discovery share wave 1's cap across their two separately-scoped groups; the Synthesis-Gate wave carries `agentCap: 0` with `coordinatorDirect: true` and writes only the coordinator's own artifacts (audit report, STATE.md, plan).
237
237
 
@@ -268,7 +268,7 @@ When `docs-orchestrator.enabled: true`, apply the following concrete dispatch ru
268
268
 
269
269
  - `agents: 0` on such an item is a DECLARATION, never a defect — do not "fix" it upward, and do not let the Step 3.5 constraint check or the Step 3 tier table raise it.
270
270
  - The wave still counts toward `total-waves` and still occupies its wave number; the renumbering above skips over it, it does not absorb it.
271
- - The ultradeep Synthesis-Gate (wave 2) is the only such wave today. Without this exception the empty-role rule deletes it — and it is the one wave whose entire job is to stop and ask before any code is written (`docs/prd/2026-09-06-ultradeep-session-profile.md` AC-4).
271
+ - The ultradeep Synthesis-Gate (wave 2) is the only such wave today. Without this exception the empty-role rule deletes it — and it is the one wave whose entire job is to stop and ask before any code is written (the internal ultradeep session-profile PRD (2026-09-06, not in the public mirror) AC-4).
272
272
  - The exception is scoped to the MARKER, not to the profile: any future coordinator-direct wave inherits it without another edit here.
273
273
 
274
274
  ### Role Details
@@ -446,7 +446,9 @@ The wait between presenting this plan and the operator's answer is the LONGEST u
446
446
  // #1229 — the plan-approval wait is the longest unheartbeated stretch in the flow.
447
447
  // Best-effort: a failure must NOT block the handoff to wave-executor.
448
448
  import { updateHeartbeat } from 'scripts/lib/session-lock.mjs';
449
- updateHeartbeat({ sessionId, repoRoot: process.cwd() });
449
+ import { resolveOwnLockRootFor } from 'scripts/lib/own-lock-root.mjs';
450
+ // The checkout holding this session's lock, not the cwd's (#1730) — at the wrong root nothing is written.
451
+ updateHeartbeat({ sessionId, repoRoot: resolveOwnLockRootFor({ sessionId }).root });
450
452
  ```
451
453
 
452
454
  - **Ownership-guarded — this cannot steal a peer's lock.** `updateHeartbeat` (`scripts/lib/session-lock.mjs`) reads the lock first and returns `false` WITHOUT writing when `session_id` differs from the one passed, when no lock exists, or when `sessionId` is empty. The write is reached only for a lock this session already owns.
@@ -77,10 +77,14 @@ Read and parse Session Config per `skills/_shared/config-reading.md`. Store resu
77
77
 
78
78
  ```javascript
79
79
  try {
80
+ const { pathToFileURL } = await import('node:url');
80
81
  const { appendSkillInvocation, DEFAULT_SKILL_INVOCATIONS_PATH } =
81
- await import('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs');
82
- const nodePath = await import('node:path');
83
- await appendSkillInvocation(nodePath.join(process.cwd(), DEFAULT_SKILL_INVOCATIONS_PATH), {
82
+ await import(pathToFileURL('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs').href);
83
+ const { resolveLedgerPath } =
84
+ await import(pathToFileURL('${PLUGIN_ROOT}/scripts/lib/ledger-path.mjs').href);
85
+ // In a linked worktree the main checkout's ledger when it is gitignored there,
86
+ // so the record survives the worktree (#1722).
87
+ await appendSkillInvocation(resolveLedgerPath(DEFAULT_SKILL_INVOCATIONS_PATH, process.cwd()), {
84
88
  timestamp: new Date().toISOString(),
85
89
  event: 'selected',
86
90
  skill: 'session-orchestrator:session-start',
@@ -350,6 +354,8 @@ Invokes `buildLiveSignals` (single SSOT for the signals shape) then `selectMode(
350
354
 
351
355
  **See `phase-7-5-mode-selector.md` for full details.**
352
356
 
357
+ If session-start aborts without work scope (no candidates, nothing to do), send the one report required by `skills/_shared/fleet-protocol.md` § Rücklauf an den Navigator (Pflicht) in this turn (wait case `keine-arbeit`) before returning, instead of waiting silently, and end the answer with its wait sentence.
358
+
353
359
  ## Phase 7.6: Fleet Check-in (#1462)
354
360
 
355
361
  > Skip silently when `persistence: false` (no ledger for the event). Runs BEFORE the first writing action; everything not stated here is in `skills/_shared/fleet-protocol.md`. Without fleet integration the CLI itself skips (stdout `skipped: "no-fleet"`, no file, no event, #1559): then this phase ends there — no hint, no `auflagen/` read, and no Standard-Auflagen, except for a headless run (`plattform: kopflos`), which applies them at once.
@@ -51,8 +51,8 @@ on it there fired the *critical* threshold on 84.0% of starts.
51
51
  | Signal | Threshold | Class |
52
52
  |---|---|---|
53
53
  | CPU, judged on **min(1m, 5m)** (#943) | above `cpu-load-max-pct` (default 90) | soft |
54
- | Live peer **sessions** from the registry | ≥ `concurrent-sessions-warn` (default 5) | soft |
55
- | Claude **processes** (fallback only, registry unreadable) | ≥ threshold × 6 | soft |
54
+ | Live peer **sessions** from the registry | ≥ `concurrent-sessions-warn` (default 5) | **info** (reported, never counted — standing condition, #1654) |
55
+ | Claude **processes** (fallback only, registry unreadable) | ≥ threshold × 6 | **info** (same state, same rule — #1654) |
56
56
  | Swap, **only while memory is unhealthy** | `> 3072 MB` hard / `> 1024 MB` soft | both |
57
57
  | Zombie processes with a live peer/process context | ≥ 1 | **info** (reported, never counted — see HR-104) |
58
58
  | SSH detected AND `ssh-no-docker: true` | — | info note |
@@ -70,10 +70,16 @@ caller can log which axes fired rather than re-deriving them from prose.
70
70
  **Unit note (#1089):** `concurrent-sessions-warn` is denominated in SESSIONS. It
71
71
  was compared against `claude_processes_count` until this rebuild — a measured 6x
72
72
  unit error (median processes:sessions = 6.0 over 1461 paired samples) that made
73
- the threshold fire on 93.6% of starts instead of 4.2%. `probe()` now supplies
73
+ the threshold fire on 93.6% of starts instead of 4.2%. The 4.2% did not hold
74
+ (re-measured 2026-10-17, #1650: 67.4% M4 / 25.7% M5 since 2026-08-24). `probe()` now supplies
74
75
  `peer_sessions_count` from the session registry (`detectPeers()`, self excluded,
75
76
  heartbeat-fresh); the rescaled process count is a fallback for hosts where the
76
- registry is unreadable.
77
+ registry is unreadable. **Report-only since #1654:** a state present on two thirds
78
+ of M4 starts is a standing condition, and HR-104 forbids counting one as a second
79
+ opinion — so the peer count (and its process fallback) appears as an `info:`
80
+ reason but never enters `signals.soft`, neither here nor in the inter-wave gate
81
+ (`evaluateWaveResourceGate()` delegates to `evaluate()`). The rare concurrency
82
+ signal is the session-start banner's start wave (`recent_starts_count`, #1650).
77
83
 
78
84
  **CPU methodology (#943):** the gate/probe runs right after the coordinator's own
79
85
  CPU-saturating quality-gate run by construction, so the 1-minute load average
@@ -90,7 +96,7 @@ to `cpu_load_pct` alone.
90
96
  Print a one-line Resource Health verdict immediately after Phase 4's output:
91
97
 
92
98
  ```
93
- Resource Health: ⚠ warn — two signals agree (cpu + concurrency); capping agents-per-wave at 2.
99
+ Resource Health: ⚠ warn — two signals agree (memory + cpu); capping agents-per-wave at 2.
94
100
  ```
95
101
 
96
102
  On `green` with a reported signal, print the line but say plainly that nothing
@@ -92,7 +92,7 @@ Hand off to Phase 9 as usual. The coordinator then executes the 1-wave plan sess
92
92
  1. Proceed to Phase 9 (session-plan handoff) carrying the banner. session-plan short-circuits to the 1-wave `coordinator-direct` plan — the same shape `scripts/session-shape.mjs` resolves for `housekeeping`; `/go` detects it and does NOT invoke wave-executor.
93
93
  2. **Run the maintenance loop FIRST** (`skills/session-start/SKILL.md` Phase 7, "For housekeeping sessions"): drift-check work-list, expired sweep, `/evolve analyze`, `/reconcile`, `/evolve dialectic` (dry-run then apply), `/memory-cleanup` — each AUQ-gated. `coordinator-direct` means no wave-executor, NOT zero subagents: the dialectic step dispatches the read-only `dialectic-deriver`. The operator-selected tasks run after the loop, because the loop's outputs (new learnings, new rules) are inputs the task work should already see.
94
94
  3. For each agreed task (in dependency order): execute as a direct coordinator action — read files, make changes, run quality checks inline. No subagents, no inter-wave checkpoints.
95
- 4. Log the express-path activation in STATE.md `## Deviations` section: `Express path: N tasks executed coord-direct (express-path.enabled: true, session-type: housekeeping, scope: N issues)` — written BEFORE session-end is invoked. Then invoke `skills/session-end/SKILL.md` directly.
95
+ 4. Log the express-path activation in STATE.md `## Deviations` section: `Express path: N tasks executed coord-direct (express-path.enabled: true, session-type: housekeeping, scope: N issues)` — written BEFORE session-end is invoked. Then invoke `skills/session-end/SKILL.md` directly — **unless the session is lead-guided** (fleet integration, `flotte: true` per `skills/_shared/fleet-protocol.md` § Rücklauf an den Navigator): then it ends with summary, report, hand-over and `Bereit zum Schließen.` as the last sentence instead, and the coordinator triggers `/close` (fleet-protocol § Übergabe Punkt 4, #1620). Step 5 does not run on this path: the regular session-end flow of that `/close` finalizes STATE.md.
96
96
  5. After session-end completes successfully: verify STATE.md `status` is `completed` and `## Deviations` contains the express-path entry from step 4. If either is missing, warn the user with a one-line note and instructions to re-run `/close` manually. Then return the final session summary to the user.
97
97
 
98
98
  **Persistence contract:**
@@ -101,6 +101,7 @@ Step 1 is the Phase 9 handoff and ends the session-start turn — the operator t
101
101
 
102
102
  - Step 2 (maintenance loop) and step 3 (execute tasks) happen first in that turn's main flow, in that order.
103
103
  - Step 4a (deviations log) is written BEFORE session-end is invoked. The coordinator calls `appendDeviation()` from `scripts/lib/state-md.mjs` to append the `Express path:` bullet to the `## Deviations` section while STATE.md is still `status: active`.
104
+ - Lead-guided exception (#1620): with fleet integration, steps 4b and 5 do not run — the turn ends with the hand-over after step 4a, and session-end runs when the coordinator triggers `/close`.
104
105
  - Step 4b (invoke session-end) flips `status` to `completed`, writes the metrics record to `.orchestrator/metrics/sessions.jsonl`, and runs the standard close flow. Session-end has no Express Path-specific logic — it treats this run identically to any other completed session.
105
106
  - Step 5 (verification) is the coordinator's final action before returning control. The verification check uses `parseStateMd()` from `scripts/lib/state-md.mjs` to read the file and check `frontmatter.status === 'completed'` and that the body contains the literal string `Express path:`.
106
107
 
@@ -125,5 +126,5 @@ Proceed normally to Phase 9 (session-plan handoff). The express-path evaluation
125
126
  - `scripts/express-path.mjs` — the CLI this phase runs; `scripts/lib/express-path.mjs` holds the decision + its `orchestrator.express_path.evaluated` record
126
127
  - `skills/session-plan/SKILL.md` § "Express Path Short-Circuit (#214)" — the 1-wave plan Phase 9 emits when the banner is present
127
128
  - `skills/go/SKILL.md` — Express Path detection and auto-invocation of session-end after coord-direct tasks
128
- - `skills/session-end/SKILL.md` — Phase 1 pre-check (Rule 2) blocks `/close` when STATE.md `status: completed`; auto-invocation from express-path bypasses this
129
- - `skills/close/SKILL.md` — Rule 2 wording the user sees if express-path persistence breaks
129
+ - `skills/session-end/SKILL.md` — the session-end workflow express-path auto-invokes directly, bypassing the `/close` pre-check
130
+ - `skills/close/SKILL.md` — its pre-check (`scripts/close-precheck.mjs`) stops `/close` with verdict `already-finalized` when STATE.md is `completed` and recorded; that wording is what the user sees if express-path persistence breaks