session-orchestrator 3.21.0 → 3.23.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 (345) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +11 -2
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/040-discovery.mdc +6 -6
  38. package/.cursor/rules/050-plan.mdc +10 -10
  39. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  40. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  41. package/.cursor/skills/architecture/SKILL.md +13 -0
  42. package/.cursor/skills/autopilot/SKILL.md +12 -0
  43. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  44. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  45. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  46. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  47. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  48. package/.cursor/skills/daily/SKILL.md +12 -0
  49. package/.cursor/skills/debug/SKILL.md +13 -0
  50. package/.cursor/skills/discovery/SKILL.md +13 -0
  51. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  52. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  53. package/.cursor/skills/domain-model/SKILL.md +13 -0
  54. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  55. package/.cursor/skills/eli5/SKILL.md +13 -0
  56. package/.cursor/skills/eval/SKILL.md +12 -0
  57. package/.cursor/skills/evolve/SKILL.md +13 -0
  58. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  60. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  61. package/.cursor/skills/grill/SKILL.md +13 -0
  62. package/.cursor/skills/hook-development/SKILL.md +13 -0
  63. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  64. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  65. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  66. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  67. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  68. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  69. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  70. package/.cursor/skills/plan/SKILL.md +13 -0
  71. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  72. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  73. package/.cursor/skills/reconcile/SKILL.md +12 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +269 -0
  91. package/README.md +18 -15
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -1
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/memory-proposal-collector.md +6 -4
  98. package/agents/session-reviewer.md +7 -1
  99. package/agents/test-writer.md +2 -1
  100. package/agents/ui-developer.md +2 -1
  101. package/commands/bootstrap.md +2 -2
  102. package/commands/close.md +3 -1
  103. package/commands/eli5.md +33 -0
  104. package/commands/go.md +1 -1
  105. package/commands/journey-audit.md +43 -0
  106. package/commands/release.md +5 -3
  107. package/commands/test.md +2 -2
  108. package/docs/USER-GUIDE.md +2 -2
  109. package/docs/ci-setup.md +14 -0
  110. package/docs/codex-setup.md +64 -0
  111. package/docs/components.md +7 -6
  112. package/docs/cursor-setup.md +26 -47
  113. package/docs/events-schema.md +76 -4
  114. package/docs/github-mirror-protection.md +197 -0
  115. package/docs/pi-setup.md +2 -0
  116. package/docs/rule-authoring.md +3 -1
  117. package/docs/scope-collision-guard.md +52 -5
  118. package/docs/session-config-reference.md +57 -12
  119. package/docs/session-config-template.md +4 -3
  120. package/docs/telemetry.md +22 -0
  121. package/hooks/_lib/lock-bootstrap.mjs +26 -16
  122. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  123. package/hooks/enforce-scope.mjs +64 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +12 -2
  128. package/hooks/on-session-end.mjs +237 -104
  129. package/hooks/on-session-start.mjs +404 -110
  130. package/hooks/on-stop.mjs +48 -9
  131. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  132. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  133. package/hooks/pre-auq-clarity.mjs +787 -0
  134. package/hooks/pre-bash-issue-budget.mjs +133 -21
  135. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  136. package/hooks/pre-bash-staging-fence.mjs +4 -0
  137. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  138. package/hooks/skill-invocation-telemetry.mjs +21 -10
  139. package/monitors/monitors.json +6 -0
  140. package/package.json +3 -1
  141. package/pi/prompts/eli5.md +12 -0
  142. package/pi/prompts/journey-audit.md +12 -0
  143. package/rules/_index.md +9 -1
  144. package/rules/always-on/ask-via-tool.md +62 -0
  145. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  146. package/rules/always-on/build-value.md +47 -0
  147. package/rules/always-on/cross-session-messaging.md +59 -0
  148. package/rules/always-on/loop-and-monitor.md +221 -0
  149. package/rules/always-on/parallel-sessions.md +142 -12
  150. package/rules/always-on/receiving-review.md +108 -0
  151. package/rules/always-on/test-value.md +40 -0
  152. package/rules/always-on/verification-before-completion.md +77 -0
  153. package/scripts/archive-closed-prds.mjs +258 -18
  154. package/scripts/auq-audit.mjs +825 -0
  155. package/scripts/autopilot.mjs +12 -8
  156. package/scripts/backfill-evidence-digest.mjs +376 -0
  157. package/scripts/cursor-install.mjs +89 -48
  158. package/scripts/export-hw-learnings.mjs +143 -2
  159. package/scripts/express-path.mjs +299 -0
  160. package/scripts/generate-cursor-adapter.mjs +253 -0
  161. package/scripts/github-protection-audit.mjs +358 -0
  162. package/scripts/lib/auq/clarity.mjs +1314 -0
  163. package/scripts/lib/auq/parse.mjs +1006 -0
  164. package/scripts/lib/auq/schema.mjs +1457 -0
  165. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  166. package/scripts/lib/build-live-signals.mjs +24 -5
  167. package/scripts/lib/ci-status-banner.mjs +221 -68
  168. package/scripts/lib/command-blocker.mjs +70 -0
  169. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  170. package/scripts/lib/config/reconcile.mjs +79 -4
  171. package/scripts/lib/config/section-extractor.mjs +235 -36
  172. package/scripts/lib/config/vault-integration.mjs +12 -1
  173. package/scripts/lib/config-schema.mjs +9 -1
  174. package/scripts/lib/config.mjs +57 -6
  175. package/scripts/lib/convergence-monitor.mjs +13 -2
  176. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  177. package/scripts/lib/dispatcher/cli.mjs +2 -2
  178. package/scripts/lib/dispatcher/rank.mjs +4 -7
  179. package/scripts/lib/express-path.mjs +327 -0
  180. package/scripts/lib/file-lock.mjs +22 -4
  181. package/scripts/lib/gates/gate-full.mjs +84 -11
  182. package/scripts/lib/gates/gate-helpers.mjs +93 -21
  183. package/scripts/lib/git-config-drift.mjs +134 -5
  184. package/scripts/lib/host-identity.mjs +247 -2
  185. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  186. package/scripts/lib/io.mjs +239 -0
  187. package/scripts/lib/issue-budget.mjs +284 -31
  188. package/scripts/lib/learnings/io.mjs +55 -10
  189. package/scripts/lib/learnings/schema.mjs +95 -28
  190. package/scripts/lib/lock-reaper.mjs +7 -1
  191. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  192. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  193. package/scripts/lib/memory-banner.mjs +5 -2
  194. package/scripts/lib/memory-paths.mjs +15 -6
  195. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  196. package/scripts/lib/owner-interview.mjs +78 -32
  197. package/scripts/lib/peer-discovery.mjs +73 -22
  198. package/scripts/lib/platform.mjs +72 -9
  199. package/scripts/lib/plugin-root.mjs +143 -19
  200. package/scripts/lib/project-hygiene.mjs +104 -4
  201. package/scripts/lib/quality-gate.mjs +271 -13
  202. package/scripts/lib/reconcile/emitter.mjs +87 -19
  203. package/scripts/lib/reconcile/engine.mjs +281 -13
  204. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  205. package/scripts/lib/reconcile/renderer.mjs +165 -7
  206. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  207. package/scripts/lib/reconcile/writer.mjs +415 -84
  208. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  209. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  210. package/scripts/lib/resource-probe.mjs +18 -2
  211. package/scripts/lib/rule-loader.mjs +37 -2
  212. package/scripts/lib/rules-sync.mjs +51 -8
  213. package/scripts/lib/scope-gate.mjs +90 -0
  214. package/scripts/lib/session-close-backfill.mjs +369 -28
  215. package/scripts/lib/session-discovery.mjs +13 -3
  216. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  217. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  218. package/scripts/lib/session-id.mjs +30 -14
  219. package/scripts/lib/session-identity/own-session.mjs +159 -0
  220. package/scripts/lib/session-lock.mjs +85 -30
  221. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  222. package/scripts/lib/session-schema/validator.mjs +40 -0
  223. package/scripts/lib/session-start-probes.mjs +608 -0
  224. package/scripts/lib/session-transition.mjs +277 -0
  225. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  226. package/scripts/lib/spiral-carryover.mjs +111 -9
  227. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  228. package/scripts/lib/state-md/mission-status.mjs +467 -72
  229. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  230. package/scripts/lib/state-md.mjs +12 -2
  231. package/scripts/lib/telemetry/sync.mjs +46 -8
  232. package/scripts/lib/validate/check-agents.mjs +66 -0
  233. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  234. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  235. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  236. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  237. package/scripts/lib/validate/check-hooks-symmetry.mjs +59 -63
  238. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  239. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  240. package/scripts/lib/validate/check-rules.mjs +153 -9
  241. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  242. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  243. package/scripts/lib/validate/repo-files.mjs +275 -0
  244. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  245. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  246. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  247. package/scripts/lib/vault-mirror/process.mjs +99 -43
  248. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  249. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  250. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  251. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  252. package/scripts/lib/vcs-repo-spec.mjs +182 -13
  253. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  254. package/scripts/lib/wave-resource-gate.mjs +67 -73
  255. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  256. package/scripts/materialize-wave-scope.mjs +478 -0
  257. package/scripts/mcp-server.sh +11 -2
  258. package/scripts/parse-config.mjs +65 -0
  259. package/scripts/release.mjs +443 -122
  260. package/scripts/run-quality-gate.mjs +14 -0
  261. package/scripts/token-audit.sh +9 -2
  262. package/scripts/validate-plugin.mjs +6 -0
  263. package/scripts/validate-wave-scope.mjs +73 -1
  264. package/scripts/vault-backfill.mjs +32 -5
  265. package/scripts/vault-mirror.mjs +203 -34
  266. package/skills/_shared/monitor-patterns.md +31 -5
  267. package/skills/_shared/parallel-aware-auq.md +31 -25
  268. package/skills/_shared/parallel-aware-preamble.md +35 -4
  269. package/skills/_shared/platform-tools.md +11 -5
  270. package/skills/_shared/state-ownership.md +61 -8
  271. package/skills/autopilot/SKILL.md +5 -1
  272. package/skills/bootstrap/SKILL.md +5 -4
  273. package/skills/bootstrap/_shared-template.md +18 -10
  274. package/skills/bootstrap/deep-template.md +10 -6
  275. package/skills/bootstrap/fast-template.md +15 -8
  276. package/skills/bootstrap/standard-template.md +10 -6
  277. package/skills/brainstorm/SKILL.md +18 -18
  278. package/skills/brainstorm/soul.md +12 -0
  279. package/skills/claude-md-drift-check/checker.mjs +39 -11
  280. package/skills/discovery/SKILL.md +28 -24
  281. package/skills/dispatcher/SKILL.md +1 -1
  282. package/skills/eli5/SKILL.md +43 -0
  283. package/skills/evolve/SKILL.md +8 -9
  284. package/skills/gitlab-ops/SKILL.md +30 -26
  285. package/skills/grill/SKILL.md +6 -6
  286. package/skills/grill/soul.md +16 -0
  287. package/skills/journey-audit/SKILL.md +269 -0
  288. package/skills/memory-cleanup/SKILL.md +2 -2
  289. package/skills/npm-publish/SKILL.md +4 -4
  290. package/skills/peekaboo-driver/SKILL.md +18 -6
  291. package/skills/persona-panel/SKILL.md +1 -1
  292. package/skills/plan/SKILL.md +18 -16
  293. package/skills/plan/mode-feature.md +1 -1
  294. package/skills/plan/mode-new.md +35 -23
  295. package/skills/plan/soul.md +12 -0
  296. package/skills/reconcile/SKILL.md +44 -4
  297. package/skills/session-end/SKILL.md +70 -24
  298. package/skills/session-end/metrics-collection.md +7 -4
  299. package/skills/session-end/phase-3-6-tail.md +48 -5
  300. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  301. package/skills/session-plan/SKILL.md +6 -1
  302. package/skills/session-plan/wave-template.md +1 -0
  303. package/skills/session-start/SKILL.md +96 -48
  304. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  305. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  306. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  307. package/skills/session-start/phase-8-5-express-path.md +77 -12
  308. package/skills/session-start/soul.md +110 -0
  309. package/skills/test-runner/SKILL.md +2 -2
  310. package/skills/using-orchestrator/SKILL.md +1 -1
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +4 -2
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +129 -24
  315. package/skills/write-executable-plan/SKILL.md +6 -6
  316. package/templates/_shared/journey-manifest.md +110 -0
  317. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  318. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  319. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  320. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  321. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  322. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  323. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  324. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  325. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  326. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  327. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  328. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  329. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  330. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  331. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  332. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  333. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  334. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  335. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  336. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  337. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  338. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  339. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  340. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  341. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  342. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  343. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  344. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
  345. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -42,7 +42,7 @@ This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's
42
42
  **Outcome handling:**
43
43
  - `PASS_THROUGH` → continue to Phase 1
44
44
  - `EXCLUSIVE_BLOCKED` → exit Phase 0 cleanly per the AUQ outcome (`Warten` / `Andere Session beenden` / `Abbrechen` — all three return without initializing STATE.md)
45
- - `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId(), `branch` from current HEAD, `repoRoot = process.cwd()`. On success, exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit stderr WARN `parallel-aware: enterWorktree failed: <err>; falling back to Manuell` and proceed via the Manuell path.
45
+ - `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId() **for the worktree-name attribution label only**, `branch` from current HEAD, `repoRoot = process.cwd()`. It is not a lock/registry ownership key; the new worktree's Phase 1.2 obtains its own physical raw `session_id`. Because `branch` is the current HEAD it is normally checked out by `repoRoot` already, so `enterWorktree` treats it as a start point only and lands the promoted worktree on a fresh `so/<sessionId>` branch, returning `{ branch: 'so/<sessionId>', promotedFrom: '<branch>' }` (#1067) — the new worktree's STATE.md `branch` MUST record `so/<sessionId>` and note `promoted from <branch>@<repoRoot>`, never the source branch alone. On success, and BEFORE exiting Phase 0, call `leaveSourceRoot({ repoRoot, sessionId: <the raw physical session_id that owns this root's lock/registry entry>, semanticSessionId, reason: 'worktree-promotion' })` from `scripts/lib/session-transition.mjs`. **`sessionId` is the RAW physical `session_id` read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })` — never the semantic label, and never the id in `current-session.json`, which may describe a peer session (#863); a wrong id now aborts the teardown with `ok: false, reason: 'lock-session-mismatch:<owner>'` and removes nothing.** — the promotion is a PROCESS BOUNDARY, not a live migration (#1069): the old root is deregistered and its `session.lock` released BEFORE the new worktree's own Phase 1.2 acquires, so the two roots never both own a live claim at once. It never throws; report a `{ ok: false }` result as a stderr WARN (`parallel-aware: leaveSourceRoot: <reason>`) and continue — the destination worktree already exists, so aborting here would leave exactly the two-live-roots state the call prevents. Then exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit stderr WARN `parallel-aware: enterWorktree failed: <err>; falling back to Manuell` and proceed via the Manuell path.
46
46
  - `PROMOTION_OFFER` with user picking "Manuell — in-place daneben" → append Deviation, continue to Phase 1
47
47
  - `PROMOTION_OFFER` with user picking "Abbrechen" → exit cleanly
48
48
 
@@ -112,14 +112,14 @@ if (content && !isDispatcherAutonomyBlockPresent(content)) {
112
112
 
113
113
  Acquire a distributed session-lock to detect parallel sessions in the same repo before initializing STATE.md. This prevents two concurrent Claude/Codex sessions from stomping each other's wave state and metrics writes.
114
114
 
115
- **Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's `session_id` does not match the current session's id (a rare divergence — surface via AUQ before overwriting). The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
115
+ **Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's raw `session_id` does not exactly match the current session's raw id (a rare divergence — surface via AUQ before overwriting). A matching `semantic_session_id`, STATE.md `session`, or owner proof cannot repair that mismatch. The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
116
116
 
117
117
  ```javascript
118
118
  import { acquire, forceAcquire } from 'scripts/lib/session-lock.mjs';
119
119
  const result = acquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() });
120
120
  ```
121
121
 
122
- Where `sessionId` is the session identifier derived from the session type and timestamp (e.g. `main-2026-05-08-deep-1`), and `sessionType` is the session mode (`housekeeping`, `feature`, or `deep`).
122
+ Where `sessionId` is the physical raw identity for this invocation: the native harness-provided raw id, or a generated UUID when no trustworthy raw id exists. It is the only value passed to `acquire()` and the only live lock/registry ownership key. `semanticSessionId` may be recorded separately as an attribution/history label and may populate STATE.md `session`; neither label is a substitute for `sessionId`. `sessionType` is the session mode (`housekeeping`, `feature`, or `deep`).
123
123
 
124
124
  ### Decision flow
125
125
 
@@ -131,48 +131,57 @@ Where `sessionId` is the session identifier derived from the session type and ti
131
131
  ```js
132
132
  AskUserQuestion({
133
133
  questions: [{
134
- question: `Another session lock is active in this repo (started ${ageHours}h ago, mode=${existingLock.mode}, host=${existingLock.host}, pid=${existingLock.pid}). How should I proceed?`,
135
- header: "Session Lock Conflict",
134
+ question: `Another session holds the lock here started ${ageHours}h ago, mode=${existingLock.mode}, host=${existingLock.host}, pid=${existingLock.pid}. Wait, or take the lock?`,
135
+ header: "Session lock",
136
136
  multiSelect: false,
137
137
  options: [
138
- { label: "Abort (Recommended)", description: "Let the other session finish. Safe default prevents metrics and wave-state corruption." },
139
- { label: "Force-take the lock", description: "Overwrites the active lock. ONLY use if you are certain the other session is no longer running." },
138
+ { label: "Abort (Recommended)", description: "Stop here and let the other session finish, then start again. Nothing is written until it releases the lock, and two sessions sharing one wave state overwrite each other's metrics." },
139
+ { label: "Force-take the lock", description: "Overwrites the active lock and starts anyway. Only when that session is certainly gone — otherwise both keep writing the same wave state and one of them loses everything." },
140
140
  ],
141
141
  }],
142
142
  });
143
143
  ```
144
144
  - **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
145
145
  ```
146
- Session lock conflict active lock detected (started <ageHours>h ago, mode=<mode>, host=<host>, pid=<pid>).
147
- 1. Abort (Recommended) — let the other session finish.
148
- 2. Force-take the lock — ONLY if the other session is known dead.
146
+ Another session holds the lock here started <ageHours>h ago, mode=<mode>, host=<host>, pid=<pid>. Wait, or take the lock?
147
+ 1. Abort (Recommended) — stop here and let the other session finish, then start again; nothing is written until it releases the lock.
148
+ 2. Force-take the lock — overwrites the active lock. Only when that session is certainly gone, otherwise both keep writing the same wave state and one loses everything.
149
149
  Reply with the number of your choice.
150
150
  ```
151
151
  - On **Abort**: exit session-start cleanly with a brief stderr note (`session-lock: aborted — active lock held by session_id=<id>`). Do NOT initialize STATE.md.
152
152
  - On **Force-take**: call `forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() })`. After Phase 1.5 initializes STATE.md, append a deviation via `appendDeviation()`:
153
153
  `Force-took session lock from session_id=<existingLock.session_id>, age=<ageHours>h, mode=<existingLock.mode>, pid=<existingLock.pid>`. Continue.
154
154
 
155
- 3. **`result.ok === false`** with `reason === 'stale-pid-dead'` or `'stale-pid-alive'**:
156
- - A stale lock was found (TTL expired). Likely left behind by a session that crashed or was force-killed.
155
+ 3. **`result.ok === false`** with `reason === 'stale-heartbeat'`:
156
+ - A stale lock was found (its last heartbeat is older than its ttl). Likely left behind by a session that crashed or was force-killed. The lock's recorded `pid` is NOT consulted — it belongs to the ephemeral hook subprocess that wrote the lock, never to the session; measured 2026-08-23: 7 of 7 recorded pids were dead, including the live heartbeating session's own (#1137).
157
157
  - Present a choice via `AskUserQuestion`:
158
158
  ```js
159
+ // `heartbeatAgeMinutes` and `ageHours` come straight off the acquire() result (#1137);
160
+ // `sameHost` is not on the result — compute it first. Use hostnamesMatch, NEVER a raw
161
+ // `===` against os.hostname(): the hostname flips spelling on a single machine
162
+ // (measured 2026-08-24: `Mac.home` and `Ferdinands-MacBook-Pro.local` ten minutes apart),
163
+ // so a raw comparison labels this machine's OWN lock "another machine" (#1072).
164
+ // `||`, not `??` — an EMPTY-STRING host_id must fall back to `host`, or
165
+ // hostnamesMatch('', …) is false and this machine reads its own lock as
166
+ // cross-host. Production uses `lockHostCandidate()` from host-identity.mjs.
167
+ const sameHost = hostnamesMatch(existingLock.host_id || existingLock.host, os.hostname());
159
168
  AskUserQuestion({
160
169
  questions: [{
161
- question: `Stale session lock found (started ${ageHours}h ago, ttl=${existingLock.ttl_hours}h). Process pid=${existingLock.pid} on host=${existingLock.host} is ${reason === 'stale-pid-dead' ? 'confirmed dead' : 'still running or status unknown'}. Reclaim the lock?`,
162
- header: "Stale Session Lock",
170
+ question: `A stale session lock is in the way — started ${ageHours}h ago on host=${existingLock.host}${sameHost ? '' : ' (another machine)'}, its ttl=${existingLock.ttl_hours}h has expired, and its last heartbeat was ${Math.round(heartbeatAgeMinutes)} minutes ago. Reclaim it?`,
171
+ header: "Stale lock",
163
172
  multiSelect: false,
164
173
  options: [
165
- { label: "Reclaim (Recommended)", description: "Overwrite the stale lock and continue. Safe when the previous session is no longer active." },
166
- { label: "Abort — investigate manually", description: "Stop here. Inspect .orchestrator/session.lock before proceeding." },
174
+ { label: "Reclaim (Recommended)", description: "Overwrites the stale lock and continues, because its time-to-live has run out. When that process is really dead, nothing of the old session is lost." },
175
+ { label: "Abort — investigate manually", description: "Stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive." },
167
176
  ],
168
177
  }],
169
178
  });
170
179
  ```
171
180
  - **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
172
181
  ```
173
- Stale session lock found (started <ageHours>h ago, ttl=<ttlHours>h, pid=<pid> on <host>).
174
- 1. Reclaim (Recommended) — overwrite stale lock and continue.
175
- 2. Abort — investigate .orchestrator/session.lock manually.
182
+ A stale session lock is in the way — started <ageHours>h ago on <host>, ttl=<ttlHours>h expired, last heartbeat <heartbeatAgeMinutes> minutes ago. Reclaim it?
183
+ 1. Reclaim (Recommended) — overwrites the stale lock and continues, because its time-to-live has run out and that process is no longer holding anything.
184
+ 2. Abort — stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive.
176
185
  Reply with the number of your choice.
177
186
  ```
178
187
  - On **Reclaim**: call `forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() })`. After Phase 1.5 initializes STATE.md, append a deviation:
@@ -186,21 +195,24 @@ Where `sessionId` is the session identifier derived from the session type and ti
186
195
 
187
196
  ### Cross-host behaviour
188
197
 
189
- When `existingLock.host !== os.hostname()`, PID liveness cannot be checked (`pidAlive: null`). In this case:
198
+ When `hostnamesMatch(existingLock.host_id || existingLock.host, os.hostname())` is **false** — never a raw `existingLock.host !== os.hostname()`, which labels this machine's own lock "another machine" the moment the hostname flips spelling (#1072; mirror the Phase-1.2 snippet above) — the lock was written on another machine and nothing local can corroborate its heartbeat. `checkStale()` carries no `pidAlive` field at all (REMOVED in #1151; #1137 had left it as an always-`null` stub) — `heartbeatAgeMinutes` is the magnitude to reason from, and `isLive` the verdict. In this case:
190
199
  - For `reason === 'active'`: the recommendation is **Abort** — cross-host locks cannot be verified as dead.
191
200
  - For stale reasons: the recommendation is still **Reclaim** only if TTL is clearly expired (>2× ttl_hours). Otherwise default to **Abort**.
192
201
  - **Never auto-reclaim cross-host locks** under any circumstance — always present the AUQ and let the user decide.
193
- - The AUQ question text for cross-host cases should note: `"(cross-host — PID liveness cannot be verified)"`.
202
+ - The AUQ question text for cross-host cases should note: `"(cross-host — the heartbeat cannot be corroborated locally)"`. Do NOT phrase it as PID liveness: the pid on a lock belongs to the ephemeral writer subprocess, not the session, and is never probed (#1137/#1151).
194
203
 
195
204
  ## Phase 1.2.1: Peer-Guard (Epic #583 defense-in-depth)
196
205
 
197
206
  > Skip this phase if `persistence` config is `false`.
198
207
 
199
- After Phase 1.2 acquires (or confirms) the lock, call `checkPeerStateMd(repoRoot, sessionId)` from `scripts/lib/state-md-peer-guard.mjs`. This catches the rare case where lock-based detection missed an active peer (e.g., the peer's `session.lock` was force-deleted by an out-of-band sweep but STATE.md is still `status: active`, OR the peer's registry write succeeded but the lock-bootstrap hook crashed before the lock landed).
208
+ After Phase 1.2 acquires (or confirms) the lock, use `findPeers(repoRoot, { mySessionId: callerSessionHint })` for the STATE.md peer guard. `callerSessionHint` is the original semantic attribution label when one exists, otherwise the raw `sessionId`: `findPeers` may translate the semantic hint for the discovered lock/registry surface only after the exact raw binding check in `parallel-aware-preamble.md`, while keeping the original hint for STATE.md. This catches the rare case where lock-based detection missed an active peer (e.g., the peer's `session.lock` was force-deleted by an out-of-band sweep but STATE.md is still `status: active`, OR the peer's registry write succeeded but the lock-bootstrap hook crashed before the lock landed).
200
209
 
201
210
  ```javascript
202
211
  import { findPeers } from '$PLUGIN_ROOT/scripts/lib/peer-discovery.mjs';
203
- const { peers } = await findPeers(process.cwd(), { mySessionId: sessionId });
212
+ // Keep the STATE.md comparison in its original attribution-label space.
213
+ // findPeers performs the guarded semantic→raw translation only for discovered peers.
214
+ const callerSessionHint = semanticSessionId ?? sessionId;
215
+ const { peers } = await findPeers(process.cwd(), { mySessionId: callerSessionHint });
204
216
  const peer = peers.find((p) => p.source === 'state-md') ?? null;
205
217
  // Phase 1.2.1 consumes only the 'state-md' subset (STATE.md surface only).
206
218
  if (peer) {
@@ -214,7 +226,7 @@ if (peer) {
214
226
 
215
227
  1. **`peer === null`** → no active peer owns STATE.md. Continue to Phase 1.5.
216
228
  2. **`peer !== null`** → STATE.md is owned by a live peer session. **Do NOT proceed with the default Phase 1.5/1b STATE.md overwrite.** Fire the Worktree-Promotion AUQ from `skills/_shared/parallel-aware-auq.md` (same options the Phase 0.5 preamble would emit on `PROMOTION_OFFER`).
217
- - User picks "Worktree anlegen + starten" → call `enterWorktree(...)` and exit Phase 1 immediately (the new worktree's own session-start runs from scratch).
229
+ - User picks "Worktree anlegen + starten" → call `enterWorktree(...)`, then — BEFORE exiting Phase 1 — `leaveSourceRoot({ repoRoot, sessionId, semanticSessionId, reason: 'worktree-promotion' })` from `scripts/lib/session-transition.mjs` — `sessionId` is the RAW physical `session_id` read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })`, never the semantic label and never `current-session.json` (which may describe a peer, #863) — (#1069 process boundary: this site runs AFTER Phase 1.2 already acquired the lock, so the old root MUST be deregistered and its lock released here, or the new worktree's own Phase 1.2 finds a phantom owner). It never throws; on `{ ok: false }` emit a stderr WARN `parallel-aware: leaveSourceRoot: <reason>` and continue — the destination worktree already exists, so aborting here would leave exactly the two-live-roots state the call prevents. Then exit Phase 1 immediately (the new worktree's own session-start runs from scratch).
218
230
  - User picks "Manuell — in-place daneben" → append a Deviation describing the missed peer detection, continue to Phase 1.5. STATE.md WILL be overwritten — the user has explicitly accepted that risk.
219
231
  - User picks "Abbrechen" → exit cleanly.
220
232
 
@@ -367,29 +379,60 @@ If `snaps.length >= 1` → present the following choice:
367
379
 
368
380
  **Claude Code (AskUserQuestion):**
369
381
 
382
+ Before asking, read what "Recover" would actually put back — the operator decides on that diff, not on the word:
383
+
384
+ ```js
385
+ import { execFileSync } from 'node:child_process';
386
+
387
+ // Read-only: `git stash show` prints a diffstat and never touches the working tree.
388
+ // Capped at 12 lines so the preview box stays shorter than the option list beside it.
389
+ const stat = execFileSync('git', ['stash', 'show', '--stat', snaps[0].sha], { encoding: 'utf8' })
390
+ .split('\n').slice(0, 12).join('\n');
391
+ const refs = snaps.map((s) => s.ref).join('\n');
392
+ ```
393
+
370
394
  ```js
371
395
  AskUserQuestion({
372
396
  questions: [{
373
- question: `Found ${snaps.length} coordinator snapshot(s) from the resumed session (latest from ${humanAgeOf(snaps[0].createdAt)}). Recover, keep as backup, or discard?`,
397
+ question: `${snaps.length} snapshot(s) from the resumed session, newest ${humanAgeOf(snaps[0].createdAt)}. Recover, keep, discard?`,
374
398
  header: "Snapshot",
375
399
  multiSelect: false,
376
400
  options: [
377
- { label: "Recover (diff vs current tree) (Recommended)", description: "Apply the latest snapshot back onto the working tree. You will see a diff and can unstage unwanted changes before committing." },
378
- { label: "Keep as backup", description: "Leave refs/so-snapshots/* in place untouched. You can recover manually later via `git stash apply $(git rev-parse <ref>)`." },
379
- { label: "Discard all", description: "Delete all refs/so-snapshots/<sessionId>/* immediately via deleteSnapshot." },
401
+ {
402
+ label: "Recover (Recommended)",
403
+ description: "Puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.",
404
+ preview: `These files come back:\n\n\`\`\`\n${stat}\n\`\`\``,
405
+ },
406
+ {
407
+ label: "Keep as backup",
408
+ description: "Nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.",
409
+ },
410
+ {
411
+ label: "Discard all",
412
+ description: "Deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.",
413
+ preview: `Deleted for good:\n\n\`\`\`\n${refs}\n\`\`\``,
414
+ },
380
415
  ],
381
416
  }],
382
417
  });
383
418
  ```
384
419
 
420
+ `preview` renders beside the option list and only works with `multiSelect: false`. It is used here because the answer decides which literal text lands in the working tree — "Recover" is a diff, "Discard all" is a list of refs that stop existing. "Keep as backup" carries none: keeping is exactly the state the operator already sees.
421
+
385
422
  **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
386
423
 
424
+ These harnesses have no preview box, so the same diffstat is printed inline — it is the only place the operator ever sees it:
425
+
387
426
  ```markdown
388
- Snapshot recovery options:
427
+ "Recover" would put these files back:
428
+
429
+ <git stash show --stat <snaps[0].sha>, capped at 12 lines>
430
+
431
+ <N> snapshot(s) from the resumed session, newest <age>. Recover, keep, discard?
389
432
 
390
- 1. **Recover (Recommended)** — Apply the latest snapshot back onto the working tree. You will see a diff and can unstage unwanted changes before committing.
391
- 2. **Keep as backup** — Leave the refs in place untouched. You can recover manually later.
392
- 3. **Discard all** — Delete all refs/so-snapshots/<sessionId>/* immediately.
433
+ 1. **Recover (Recommended)** — puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.
434
+ 2. **Keep as backup** — nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.
435
+ 3. **Discard all** — deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.
393
436
 
394
437
  Reply with the number of your choice.
395
438
  ```
@@ -471,7 +514,7 @@ await sweepBoard({
471
514
 
472
515
  This single call does three things:
473
516
 
474
- 1. **Sets THIS repo's board row to `in-progress`** with the current semantic-session-id, branch, mode, and heartbeat (read off this repo's `session.lock` v2 lease + the host-wide registry — both already written by Phase 1.2's `acquire()`).
517
+ 1. **Sets THIS repo's board row to `in-progress`** with the current semantic-session-id **attribution label** (never a lock/registry ownership key), branch, mode, and heartbeat (read off this repo's `session.lock` v2 lease + the host-wide registry — both already written by Phase 1.2's `acquire()`).
475
518
  2. **Re-derives THIS repo's status from its live lease**, so a stale lease left by a prior crashed session in this same repo renders as `force-closed` (heartbeat older than the v2 ttl, default 4h — `DEFAULT_TTL_HOURS` in `scripts/lib/session-lock.mjs`, evaluated via `isLockLive`) and is **never silently dropped** — its fields are read straight off the dead lock.
476
519
  3. **Re-derives every OTHER busy repo's status host-wide** via `enumerateCandidates` — a dead lease in repo B renders `force-closed` on the board the next time ANY repo's session-start runs `sweepBoard`, closing the #676→#716 gap. `frei` (lock-less) repos are excluded from re-derivation to avoid board noise; their prior rows, and the prior rows of any repo `enumerateCandidates` did not surface, are preserved unchanged via the idempotent merge — never dropped.
477
520
 
@@ -676,13 +719,14 @@ Group issues by:
676
719
  - **info-only version mismatch** (patch or minor version only): `"ℹ bootstrap.lock: plugin-version=<lock-ver> (current=<plugin-ver>) — minor drift only, no action required."`
677
720
  - **legacy lock without plugin-version** (soft signal only): `"ℹ bootstrap.lock: lock predates plugin-version field; consider /bootstrap --refresh-lock to stamp a current plugin-version reference."`
678
721
 
679
- Additionally, if `.orchestrator/metrics/vault-staleness.jsonl` exists in the current repo (vault-integration enabled), read the most recent line via `scripts/lib/vault-staleness-banner.mjs` (`checkVaultStaleness({repoRoot})`). When `stale_count > 0`, render a banner alongside the bootstrap-lock warning:
722
+ Additionally, if `.orchestrator/metrics/vault-staleness.jsonl` exists in the current repo (vault-integration enabled), read the most recent line via `scripts/lib/vault-staleness-banner.mjs` (`checkVaultStaleness({repoRoot})`). The helper returns THREE shapes, not two: `null` (silent no-op) when the JSONL is absent, malformed, or `stale_count === 0`; a fresh finding when `stale_count > 0` and the record itself is recent; or `{severity: 'warn', kind: 'probe-stale', message, ageDays, timestamp}` when `stale_count > 0` but the record's own `timestamp` is older than `MAX_RECORD_AGE_DAYS` (7 days, #1159) — the probe has stopped running, so its recorded findings are NOT a current finding about the repo. `severity` is `'warn'` in this case too (not a distinct `'info'` value) so a caller reading only `severity` still renders it; `kind` is what a caller uses to tell "probe went stale" apart from "projects are actually stale" (single-vocabulary fix, #1158/#1159 review N3). Render a banner alongside the bootstrap-lock warning for every non-null result:
680
723
  - **warn** (`stale_count > 0`, max `delta_hours <= 48`): `"⚠ vault-staleness: <N> projects stale (max delta: <X>h) — last run <timestamp>."`
681
724
  - **alert** (`stale_count > 0`, max `delta_hours > 48`): `"⚠ vault-staleness: <N> projects stale (max delta: <X>h) — Clank-Vault-Sync cron likely broken, see agents/vault#70 fix pattern."`
725
+ - **probe-stale** (`kind: 'probe-stale'`): `"⚠ vault-staleness: last probe record is <N> days old (<timestamp>) — the probe has not run since; the recorded <N> stale projects are NOT a current finding."`
682
726
 
683
- The helper returns `null` (silent no-op) when the JSONL is absent, malformed, or `stale_count === 0`. Skip silently in those casesdo not block the session.
727
+ Skip silently on `null`; do not block the session in any case. This passage does not restate a severity-mapping table for the probe-stale shape the `vault-staleness` entry in the probe registry (`scripts/lib/session-start-probes.mjs`, `PROBES`) is the single declaration site for how a probe's result maps onto the rendered banner severity, and (post-N3) needs no custom mapping for this probe at all.
684
728
 
685
- Additionally, if the current repo has a configured `origin` remote and `glab` (GitLab) or `gh` (GitHub) is available, invoke the CI-status probe (`scripts/lib/ci-status-banner.mjs`) via `checkCiStatus({ repoRoot: process.cwd() })`. The helper returns `null` (silent no-op) when no VCS remote, no CLI tool, parse failure, or CLI timeout (8s default). When `result.status === 'red'`, render a banner alongside the bootstrap-lock and vault-staleness warnings:
729
+ Additionally, if the current repo has a configured `origin` remote and `glab` (GitLab) or `gh` (GitHub) is available, invoke the CI-status probe (`scripts/lib/ci-status-banner.mjs`) via `await checkCiStatus({ repoRoot: process.cwd() })` — it is `export async function` (`ci-status-banner.mjs:555`), so a caller following this line without `await` gets a Promise and renders nothing. The helper returns `null` (silent no-op) when no VCS remote, no CLI tool, parse failure, or CLI timeout (8s default). When `result.status === 'red'`, render a banner alongside the bootstrap-lock and vault-staleness warnings:
686
730
  - **Red** (`status === 'red'`): `"🚨 CI RED on HEAD (pipeline #<currentPipelineId>) — last green: #<lastGreen.pipelineId> (commit <SHA-7>, <redCount> pipelines ago). Failing job: <failingJobName>"`
687
731
  - **Green with soft failures** (`status === 'green'` AND `result.allowFailureJobs` is present): `"⚠ CI green on HEAD, but <N> allow_failure job(s) FAILED: <names>. A pipeline reports success regardless of these — a job red on every run stays invisible at the pipeline level."` Render this even though the pipeline passed: the whole point is that pipeline status cannot express it.
688
732
  - **Green** (no `allowFailureJobs`) or **unknown**: silent (no banner) — informational only.
@@ -789,7 +833,9 @@ Group issues by:
789
833
 
790
834
  The return contract has THREE states, not the usual two, and the third is the point: `null` means either "no mirror remote" or "queried successfully, zero open issues"; `{ severity, message, count, repoSpec, issues }` means N > 0; and `{ severity, message, repoSpec, degraded }` means the query did NOT succeed, where `degraded` is one of `cli-missing | timeout | parse-error | auth-error | query-failed`. Render `result.message` verbatim in either non-null case. A `degraded` result must be read as *"the mirror's state is unknown"* — never as clean. `scripts/lib/ci-status-banner.mjs` collapses all three of missing-CLI, unparseable output and absent-remote onto `null`, which in the banner contract reads as "all clear"; that collapse is why this gap survived unseen. Do not reproduce it.
791
835
 
792
- Additionally, invoke the git-config-drift probe (`scripts/lib/git-config-drift.mjs`) via `checkGitConfigDrift({ repoRoot })` (synchronous — no await; `env` defaults to `process.env`). It reads `git config --local --list` with a FILTERED environment, so an ambient `GIT_DIR` cannot redirect the probe itself at a foreign repository and let it call this one clean. **Three states, not two:** `null` = read and clean; `{ severity: 'warn', message, findings }` = at least one unexpected entry (a local identity override, a local `commit.gpgsign`, a remote on a reserved fixture host, a `core.hooksPath` not pointing at `.husky/_`, or `GIT_DIR`/`GIT_WORK_TREE` set in the environment); `{ …, degraded }` = the config could NOT be read — **never render that as clean.** Render `result.message` alongside the other banners.
836
+ Additionally, invoke the git-config-drift probe (`scripts/lib/git-config-drift.mjs`) via `checkGitConfigDrift({ repoRoot })` (synchronous — no await; `env` defaults to `process.env`). It reads `git config --local --list` with a FILTERED environment, so an ambient `GIT_DIR` cannot redirect the probe itself at a foreign repository and let it call this one clean. **Three states, not two:** `null` = read and clean; `{ severity: 'warn', message, findings }` = at least one unexpected entry (a local identity override, a local `commit.gpgsign`, a remote on a reserved fixture host, a `core.hooksPath` not pointing at `.husky/_` AND not DECLARED by the repo, or `GIT_DIR`/`GIT_WORK_TREE` set in the environment); `{ …, degraded }` = the config could NOT be read — **never render that as clean.** Render `result.message` alongside the other banners.
837
+
838
+ The `core.hooksPath` trigger is narrower than "points somewhere other than `.husky/_`" — a hooksPath the repo DECLARES is accepted without any config key. Declared means `git ls-files -- <hooksPath>` finds at least one TRACKED file DIRECTLY under it (not nested deeper) whose BASENAME is a real git hook name (`pre-commit`, `pre-push`, `commit-msg`, …) — a tracked directory of ordinary source, or an untracked file merely named like a hook, does not count. This document does not restate the hook-name list — `GIT_HOOK_NAMES` in `scripts/lib/git-config-drift.mjs` (sourced from `git help hooks`) is the single declaration site (#1158 review N1 — the first cut of this rule accepted any tracked file under the path at all, which a fixture-planted `scripts/pre-commit` left untracked, sitting beside ordinary tracked source, would have bypassed silently).
793
839
 
794
840
  This is the only probe that inspects `.git/config`, and that is the whole point: `git status` cannot see that file. On 2026-08-19 a coordinator diagnostic exported `GIT_DIR` at this repository while the suite ran; test fixtures wrote a foreign remote and their own `user.email`/`user.name` into the local config, and the identity then authored two commits that reached both remotes. A recovery pass checked HEAD, the index and all 1614 tracked files, found everything clean, and missed it — because none of those surfaces show `.git/config`. It surfaced two hours later, from an agent measuring something else.
795
841
 
@@ -1027,6 +1073,8 @@ Cross-reference: PRD F2.3 acceptance criteria (#505); `scripts/lib/memory-banner
1027
1073
 
1028
1074
  > Skip this phase silently when `persistence: false` in Session Config. Also skip silently when non-interactive (headless / CI — no TTY to prompt on), and when the consent decision has already been made (stored `granted`/`denied`, an env override, or the fleet flag). In all of these `resolveConsent().prompt` is `false` and the phase is a no-op — it must NEVER print anything or slow session-start in the common (already-decided / headless) case.
1029
1075
 
1076
+ > **The trigger is MECHANICAL since #1138.** `hooks/on-session-start.mjs` calls `resolveConsent()` itself and, when `prompt === true` and the run is not CI, injects a one-line instruction into the session via `hookSpecificOutput.additionalContext`. This phase is the WORDING and the fallback — the AUQ text below is the single source of truth for what gets asked — but it is no longer what decides *whether* to ask. Two consequences: (a) the coordinator may receive that instruction before it ever reaches this line, and should act on it then; (b) the hook gates on `isCiEnv()`, **not** `!isHeadless()` as the snippet below does — measured 2026-08-23, `isHeadless()` returns `true` in ANY non-TTY subprocess (`isHeadless()=true isCiEnv()=false stdout.isTTY=undefined`), which includes both a hook process and the `node -e` a coordinator would run this snippet in. Executed verbatim in a Bash tool call, the snippet below therefore resolves `prompt: false` every time; keep it as the semantic reference, and trust the hook for the firing decision.
1077
+
1030
1078
  Anonymous usage telemetry is **strictly opt-in** and, on a host that has never decided, is offered exactly once via a single interactive AskUserQuestion. The consent machine lives in `scripts/lib/telemetry/consent.mjs`; this phase only decides *whether* to prompt and then records the operator's answer. The `resolveConsent()` precedence machine is fail-closed — `prompt` is `true` only for a fresh, interactive, not-yet-decided, not-fleet, not-env-overridden host.
1031
1079
 
1032
1080
  ```javascript
@@ -1049,12 +1097,12 @@ if (!c.prompt) {
1049
1097
  ```js
1050
1098
  AskUserQuestion({
1051
1099
  questions: [{
1052
- question: "Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, whitelist-projiziert (keine Repo-Namen/Pfade/Prompts), jederzeit abschaltbar — Details: docs/telemetry.md",
1053
- header: "Usage Telemetry",
1100
+ question: "Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, jederzeit abschaltbar; was genau gesendet wird: docs/telemetry.md",
1101
+ header: "Telemetrie",
1054
1102
  multiSelect: false,
1055
1103
  options: [
1056
- { label: "Ja, aktivieren", description: "Anonymer Zähl-/Struktur-Datensatz (Skill-/Phasen-Nutzung, Erfolg/Abbruch) whitelist-projiziert, keine Pfade/Prompts/Repo-Namen. Details: docs/telemetry.md" },
1057
- { label: "Nein", description: "Keine Telemetrie senden. Jederzeit später aktivierbar via node scripts/telemetry.mjs." },
1104
+ { label: "Ja, aktivieren", description: "Sendet anonyme Zähl- und Strukturdaten (welche Phase lief, Erfolg oder Abbruch), whitelist-projiziert: keine Pfade, keine Prompts, keine Repo-Namen." },
1105
+ { label: "Nein", description: "Sendet nichts; die Frage kommt hier nicht wieder. Einschalten geht später mit `node scripts/telemetry.mjs` (das ist der Befehl dafür)." },
1058
1106
  ],
1059
1107
  }],
1060
1108
  });
@@ -1064,9 +1112,9 @@ AskUserQuestion({
1064
1112
 
1065
1113
  - **Codex CLI / Cursor IDE fallback (numbered Markdown list — AUQ-004 exception 1):**
1066
1114
  ```
1067
- Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, whitelist-projiziert (keine Repo-Namen/Pfade/Prompts), jederzeit abschaltbar — Details: docs/telemetry.md
1068
- 1. Ja, aktivieren — anonymer Zähl-/Struktur-Datensatz, keine Pfade/Prompts/Repo-Namen.
1069
- 2. Nein — keine Telemetrie senden.
1115
+ Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, jederzeit abschaltbar; was genau gesendet wird: docs/telemetry.md
1116
+ 1. Ja, aktivieren — sendet anonyme Zähl- und Strukturdaten (welche Phase lief, Erfolg oder Abbruch), whitelist-projiziert: keine Pfade, keine Prompts, keine Repo-Namen.
1117
+ 2. Nein — sendet nichts; die Frage kommt hier nicht wieder. Einschalten geht später mit `node scripts/telemetry.mjs` (das ist der Befehl dafür).
1070
1118
  Reply with the number of your choice. (No option is pre-recommended — the choice is yours.)
1071
1119
  ```
1072
1120
 
@@ -1146,13 +1194,13 @@ Present your findings following that structure. Key rules:
1146
1194
 
1147
1195
  ### Phase 8.5: Express Path Evaluation (#214)
1148
1196
 
1149
- After the user confirms session type and scope, evaluate whether the Express Path applies. Activation requires ALL three: `express-path.enabled: true` in Session Config (default: `true` when `express-path.enabled: false`, this evaluation is skipped entirely and the normal 5-wave session-plan flow runs), session type `housekeeping`, and scope ≤ 3 sequential issues. The 13 prior coordinator-direct sessions in `CLAUDE.md` (or `AGENTS.md` on Codex CLI; 2026-04 series) were all running this pattern implicitly — this phase codifies what was already proven to work.
1197
+ After the user confirms session type and scope, evaluate whether the Express Path applies. **Do not judge the conditions by hand — run `node scripts/express-path.mjs --repo-root "$PWD" --session-type <type> --task-count <N> --parallel-agents <true|false>`.** That CLI is the canonical caller (#1146): it makes the decision AND records it as `orchestrator.express_path.evaluated`, on refusal as well as activation. stdout is one JSON line `{"activated":<bool>,"reasons":[…]}`; exit 0 means the evaluation completed, so branch on `activated`, never on the exit code. Activation requires ALL three: `express-path.enabled: true` in Session Config (default: `true`; an explicit `false` still runs the evaluation and records `disabled-by-config`, then the normal 5-wave session-plan flow proceeds), session type `housekeeping`, and scope ≤ 3 sequential issues. The 13 prior coordinator-direct sessions in `CLAUDE.md` (or `AGENTS.md` on Codex CLI; 2026-04 series) were all running this pattern implicitly — this phase codifies what was already proven to work.
1150
1198
 
1151
- When all conditions are met, emits the banner:
1199
+ When all conditions are met, the CLI emits the banner on stderr:
1152
1200
  ```
1153
1201
  Express path activated — <N> tasks, coordinator-direct, no inter-wave checks.
1154
1202
  ```
1155
- Then executes tasks coordinator-direct (bypassing session-plan and wave-executor) and logs a Deviations entry in STATE.md. Silent no-op when any condition fails — proceeds normally to Phase 9.
1203
+ Carry that banner into Phase 9 and hand off to session-plan as usual — session-plan short-circuits to a 1-wave `coordinator-direct` plan, which is the artifact `/go` detects. Tasks are then executed coordinator-direct (bypassing wave-executor, subagent dispatch and inter-wave checkpoints) and a Deviations entry is logged in STATE.md. Silent no-op when any condition fails — proceeds normally to Phase 9.
1156
1204
 
1157
1205
  **See `phase-8-5-express-path.md` for full details.**
1158
1206
 
@@ -1173,7 +1221,7 @@ After user alignment:
1173
1221
 
1174
1222
  - **NEVER make assumptions** about code state based on memory or docs — always verify in actual files
1175
1223
  - **NEVER skip the Q&A phase** — the user MUST confirm direction before wave planning
1176
- - **ALWAYS use `run_in_background: false`** for parallel subagent work wait for completion
1224
+ - **ALWAYS verify parallel subagent work against the started set**, never against the launch ack — `run_in_background: true` is allowed and recommended for wave dispatch (`skills/wave-executor/wave-loop.md § Started-Set Verification`); skills that need every result before their next phase (persona-panel, discovery, test-runner, session-end) keep `false` and say why
1177
1225
  - **ALWAYS check `.env` or `.env.local`** for VCS host, API keys, and service URLs
1178
1226
  - **ALWAYS present options with pros/cons and a clear recommendation** — never just list facts
1179
1227
  - **ALWAYS update VCS issue status** when claiming work — use the issue update command per the "Common CLI Commands" section of the gitlab-ops skill
@@ -58,15 +58,15 @@ AskUserQuestion({
58
58
  options: [
59
59
  {
60
60
  label: "Dev (Recommended)", // add "(Recommended)" to each detected audience
61
- description: "Architektur-, Modul- oder Refactoring-Änderungen aktualisiert CLAUDE.md (oder AGENTS.md auf Codex CLI), docs/dev/**, docs/adr/**."
61
+ description: "Architektur-, Modul-, Refactoring-Änderungen. Dann ändern sich CLAUDE.md bzw. AGENTS.md und `docs/dev/**, docs/adr/**` (Handbuch und Entscheidungen)."
62
62
  },
63
63
  {
64
64
  label: "User",
65
- description: "Öffentlich sichtbare Änderungen aktualisiert README.md, docs/user/**, examples/**."
65
+ description: "Öffentlich sichtbare Änderungen. Dann werden README.md und `docs/user/**, examples/**` (was Nutzer davon lesen) nachgezogen."
66
66
  },
67
67
  {
68
68
  label: "Vault",
69
- description: "Strategische oder Status-Änderungen aktualisiert <vault>/01-projects/<slug>/context.md, decisions.md, people.md."
69
+ description: "Strategische oder Status-Änderungen. Dann wird `<vault>/01-projects/<slug>/context.md, decisions.md, people.md` (die Projektakte dazu) nachgezogen."
70
70
  }
71
71
  ]
72
72
  }]
@@ -76,13 +76,13 @@ AskUserQuestion({
76
76
  **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
77
77
 
78
78
  ```markdown
79
- Welche Audiences berührt dieser Scope? (Mehrfachauswahl möglich)
80
-
81
79
  Auto-detected: [dev] ← list detected audiences here, or "none" if empty intersection
82
80
 
83
- 1. **Dev (Recommended)** Architektur-, Modul- oder Refactoring-Änderungen. Targets: CLAUDE.md (oder AGENTS.md auf Codex CLI), docs/dev/**, docs/adr/**.
84
- 2. **User** — Öffentlich sichtbare Änderungen. Targets: README.md, docs/user/**, examples/**.
85
- 3. **Vault** — Strategische oder Status-Änderungen. Targets: <vault>/01-projects/<slug>/context.md, decisions.md, people.md.
81
+ Welche Audiences berührt dieser Scope? Mehrfachauswahl möglich.
82
+
83
+ 1. **Dev (Recommended)** — Architektur-, Modul-, Refactoring-Änderungen. Dann ändern sich CLAUDE.md bzw. AGENTS.md und `docs/dev/**, docs/adr/**` (Handbuch und Entscheidungen).
84
+ 2. **User** — öffentlich sichtbare Änderungen. Dann werden README.md und `docs/user/**, examples/**` (was Nutzer davon lesen) nachgezogen.
85
+ 3. **Vault** — strategische oder Status-Änderungen. Dann wird `<vault>/01-projects/<slug>/context.md, decisions.md, people.md` (die Projektakte dazu) nachgezogen.
86
86
 
87
87
  Enter one or more numbers (comma-separated), or press Enter to accept the recommended default.
88
88
  ```
@@ -16,31 +16,88 @@ const verdict = evaluate(snapshot, config['resource-thresholds'], {
16
16
  });
17
17
  ```
18
18
 
19
- The `evaluate()` result has three fields:
20
- - `verdict`: `green` | `warn` | `critical`
21
- - `reasons`: array of human-readable explanations
19
+ The `evaluate()` result has four fields:
20
+ - `verdict`: `green` | `warn` | `critical` (the `degraded` tier is no longer produced)
21
+ - `reasons`: array of human-readable explanations, including `info:`-prefixed
22
+ lines for signals that were seen but deliberately not acted on
22
23
  - `recommended_agents_per_wave_cap`: integer cap (0 = coordinator-direct) or null
24
+ - `signals`: `{ hard: string[], soft: string[] }` — which axes fired (#1089)
23
25
 
24
26
  The third `options` argument is optional (HR-003/HR-004, baseline #60) — when `config['heavy-repo']` is `true`, the cap is forced to at most `config['agents-per-wave']` regardless of the live verdict (static preflight ceiling; more-restrictive-wins against whatever the resource signals already computed). Omitting `options` entirely preserves pre-#60 behaviour.
25
27
 
26
- ## Adaptive Rules (default thresholds; configurable via `resource-thresholds`)
27
-
28
- | Signal | Threshold | Action |
29
- |--------|-----------|--------|
30
- | RAM free below `ram-free-min-gb` (default 4) | warn | Cap `agents-per-wave` at 2 |
31
- | RAM free below `ram-free-critical-gb` (default 2) | critical | Recommend coordinator-direct (0 agents) |
32
- | CPU load above `cpu-load-max-pct` (default 80) sustained judged on **min(1m, 5m)** load average (#943) | warn | Cap `agents-per-wave` at 2 |
33
- | Claude processes ≥ `concurrent-sessions-warn` (default 5) | warn | Warn; suggest sequencing or waiting |
34
- | SSH session detected AND `ssh-no-docker: true` | info | Append note: host is SSH-attached, Docker-dependent steps should run on a local dev host |
35
-
36
- **CPU methodology (#943):** the gate/probe runs right after the coordinator's own CPU-saturating quality-gate run by construction, so the 1-minute load average systematically carries that decaying tail (observed: 96% 75% within 36s). `probe()` therefore also emits `cpu_load_5m` / `cpu_load_5m_pct`, and `evaluate()` + `evaluateWaveResourceGate()` judge the CPU threshold on **min(1m, 5m)**: only-1m-high is reported as an informational "decaying transient" reason without capping; both-high (genuine sustained load) still caps. When `cpu_load_5m_pct` is `null` (Windows, zero-load), judging falls back to the 1m-derived `cpu_load_pct` alone.
28
+ ## Adaptive Rules (rebuilt in #1089 see `.claude/rules/host-resources.md`)
29
+
30
+ The rule set is **signal precedence + the two-signal rule**, not a list of
31
+ independent thresholds ORed together. Measured 2026-08-21 over 1477
32
+ `orchestrator.session.started` events across 18 repos, the previous OR-of-three
33
+ produced warn-or-worse on **99.0%** of session starts a warning that fires
34
+ almost always changes no decision except how fast it gets ignored.
35
+
36
+ **Memory judge on the best signal present, never on a worse one:**
37
+
38
+ | Precedence | Signal | Hard (→ critical) | Soft |
39
+ |---|---|---|---|
40
+ | 1 | `memory_pressure_pct_free` (macOS) | `< 15%` | `< 30%` |
41
+ | 2 | `ram_available_gb` (macOS, vm_stat) | `< ram-free-critical-gb` | `< ram-free-min-gb` |
42
+ | 3 | `ram_free_gb` (`os.freemem`) | same | same |
43
+
44
+ Level 3 is reached only when neither better signal exists — i.e. on
45
+ Linux/Windows, where `os.freemem()` is accurate. On Darwin it reports `Pages
46
+ free` only (median **0.4 GB** across the corpus, on 24-128 GB hosts), so gating
47
+ on it there fired the *critical* threshold on 84.0% of starts.
48
+
49
+ **Other axes:**
50
+
51
+ | Signal | Threshold | Class |
52
+ |---|---|---|
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 |
56
+ | Swap, **only while memory is unhealthy** | `> 3072 MB` hard / `> 1024 MB` soft | both |
57
+ | Zombie processes with a live peer/process context | ≥ 1 | **info** (reported, never counted — see HR-104) |
58
+ | SSH detected AND `ssh-no-docker: true` | — | info note |
59
+
60
+ **Verdict composition:**
61
+
62
+ - any **hard** signal → `critical`, recommend coordinator-direct (0 agents)
63
+ - **two or more independent soft** signals → `warn`, cap agents-per-wave at 2
64
+ - exactly **one soft** signal → `green`, reported in `reasons`, **no cap**
65
+ - none → `green`
66
+
67
+ `evaluate()` additionally returns `signals: { hard: [...], soft: [...] }` so a
68
+ caller can log which axes fired rather than re-deriving them from prose.
69
+
70
+ **Unit note (#1089):** `concurrent-sessions-warn` is denominated in SESSIONS. It
71
+ was compared against `claude_processes_count` until this rebuild — a measured 6x
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
74
+ `peer_sessions_count` from the session registry (`detectPeers()`, self excluded,
75
+ heartbeat-fresh); the rescaled process count is a fallback for hosts where the
76
+ registry is unreadable.
77
+
78
+ **CPU methodology (#943):** the gate/probe runs right after the coordinator's own
79
+ CPU-saturating quality-gate run by construction, so the 1-minute load average
80
+ systematically carries that decaying tail (observed: 96% → 75% within 36s).
81
+ `probe()` therefore also emits `cpu_load_5m` / `cpu_load_5m_pct`, and both
82
+ `evaluate()` and `evaluateWaveResourceGate()` judge CPU on **min(1m, 5m)**:
83
+ only-1m-high is reported as an informational "decaying transient" and is NOT
84
+ counted as a signal at all — so it cannot become the second signal that triggers
85
+ a cap. When `cpu_load_5m_pct` is `null` (Windows, zero-load), judging falls back
86
+ to `cpu_load_pct` alone.
37
87
 
38
88
  ## Presentation
39
89
 
40
90
  Print a one-line Resource Health verdict immediately after Phase 4's output:
41
91
 
42
92
  ```
43
- Resource Health: ⚠ warn — RAM free 3.1 GB below threshold 4 GB; capping agents-per-wave at 2.
93
+ Resource Health: ⚠ warn — two signals agree (cpu + concurrency); capping agents-per-wave at 2.
94
+ ```
95
+
96
+ On `green` with a reported signal, print the line but say plainly that nothing
97
+ was capped — a bare signal with no consequence reads as a suppressed warning:
98
+
99
+ ```
100
+ Resource Health: ✓ green — 6884 MB swap present but memory_pressure healthy (53% free); no cap.
44
101
  ```
45
102
 
46
103
  When `config['heavy-repo']` is `true` and the HR-004 preflight ceiling actually reduces `recommended_agents_per_wave_cap` below what the live verdict alone would have produced, print an additional banner line right after the verdict line:
@@ -49,10 +106,16 @@ When `config['heavy-repo']` is `true` and the HR-004 preflight ceiling actually
49
106
  ⚠ Heavy-repo mode active — agents-per-wave capped to 4 (Session Config heavy-repo: true)
50
107
  ```
51
108
 
52
- When verdict is `warn` or `critical`, use the AskUserQuestion tool to present:
53
- 1. **Proceed as recommended** (apply the cap) Recommended
54
- 2. **Proceed as originally planned** (user accepts the risk)
55
- 3. **Abort** (no wave planning runs; user closes or investigates)
109
+ **AUQ only on `critical`** (#1089). A `warn` applies its cap and reports it in
110
+ one line it does not interrupt. Under the previous rule set warn-or-worse was
111
+ the verdict on 99.0% of session starts, so an AUQ here was an operator interrupt
112
+ on essentially every session, which `.claude/rules/ask-via-tool.md` AUQ-005
113
+ names outright ("an AUQ that blocks nothing"). `critical` means coordinator-direct
114
+ — zero agents — which genuinely changes the plan, so it earns the prompt:
115
+
116
+ 1. **Proceed coordinator-direct** (0 agents) — Recommended
117
+ 2. **Proceed as originally planned** (operator accepts the risk)
118
+ 3. **Abort** (no wave planning runs; operator closes or investigates)
56
119
 
57
120
  When SSH is detected and the session type is `deep`, auto-append this note to the plan handoff to session-plan (no user prompt needed):
58
121
  > Host is SSH-attached — Docker-dependent wave steps should run on a local dev host.
@@ -22,13 +22,25 @@ import { buildLiveSignals } from '$PLUGIN_ROOT/scripts/lib/build-live-signals.mj
22
22
 
23
23
  // Pass the surfaced top-N learnings (already computed in Phase 6.6) to avoid
24
24
  // re-reading learnings.jsonl. Other paths default to canonical locations
25
- // (.claude/STATE.md, .orchestrator/metrics/sessions.jsonl, .orchestrator/bootstrap.lock).
25
+ // RELATIVE TO repoRoot (.claude/STATE.md, .orchestrator/metrics/sessions.jsonl,
26
+ // .orchestrator/bootstrap.lock).
27
+ //
28
+ // `repoRoot` is passed explicitly (#1071). It defaults to process.cwd(), but
29
+ // naming it here is the point: session-start may run from a worktree or a
30
+ // subdirectory, and an implicit default silently binds STATE.md, sessions.jsonl,
31
+ // bootstrap.lock and the backlog scan to whatever directory the process happens
32
+ // to sit in — not to the repo the recommendation is about.
26
33
  const signals = await buildLiveSignals({
34
+ repoRoot: process.cwd(), // absolute project root — bind it, never infer it
27
35
  learnings: surfacedTopLearnings, // array, may be empty
28
- backlogLimit: 50,
29
36
  });
30
37
  ```
31
38
 
39
+ Do **not** hand-write a `backlogLimit` here. The window default lives once, in
40
+ `backlog-scan.mjs` (`DEFAULT_BACKLOG_LIMIT`); the `50` that used to stand in this
41
+ example read 50 of 89 open issues and reported `critical: 0` / `stale: 0` from the
42
+ truncated window (fixed in `c20d4d2`).
43
+
32
44
  `buildLiveSignals` is the single SSOT for the Signals shape consumed by `selectMode` and by the autopilot driver protocol (see `skills/autopilot/SKILL.md § Production Wiring`). Phase 7.5 here and the autopilot in-process driver MUST go through this helper — do not inline the recipe in either call site.
33
45
 
34
46
  **Key source bindings (Phase → field):**
@@ -43,7 +55,7 @@ const signals = await buildLiveSignals({
43
55
  | `recentSessions` | tail-10 of `.orchestrator/metrics/sessions.jsonl` | Phase 6.6 |
44
56
  | `bootstrapLock` | `.orchestrator/bootstrap.lock` via `parseBootstrapLock` | Phase 4 |
45
57
  | `learnings` | surfaced top-N learnings (confidence > 0.3) | Phase 6.6 |
46
- | `backlog` | `scanBacklog({limit: 50})` from `backlog-scan.mjs` (live VCS scan) | Phase 7.5 Step 1 |
58
+ | `backlog` | `scanBacklog({limit: DEFAULT_BACKLOG_LIMIT, repoRoot})` from `backlog-scan.mjs` (live VCS scan) | Phase 7.5 Step 1 |
47
59
 
48
60
  ## Step 2: Invoke selectMode
49
61