session-orchestrator 3.22.0 → 3.24.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 (316) 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 +8 -0
  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/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -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 +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -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 +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -152,9 +152,10 @@ After learnings are written (Phase 3.6), determine whether to emit a **manual-ca
152
152
  ```javascript
153
153
  import { shouldDispatchAutoDream } from '${PLUGIN_ROOT}/scripts/lib/auto-dream.mjs';
154
154
  import { resolveMemoryDir } from '${PLUGIN_ROOT}/scripts/lib/memory-paths.mjs';
155
- const memoryDir = resolveMemoryDir();
155
+ const repoRoot = process.cwd();
156
+ const memoryDir = resolveMemoryDir(repoRoot);
156
157
  const decision = await shouldDispatchAutoDream({
157
- repoRoot: process.cwd(),
158
+ repoRoot,
158
159
  memoryDir,
159
160
  threshold: config['memory-cleanup-threshold'] ?? 5,
160
161
  softLimit: config['memory-cleanup-soft-limit'] ?? 180,
@@ -245,14 +246,15 @@ After learnings are written (Phase 3.6) and the auto-dream decision is made (Pha
245
246
 
246
247
  1. Read `dialectic.cadence` (default 5), `dialectic.model` (default haiku), `dialectic.budget-tokens` (default 8000) from `$CONFIG`.
247
248
 
248
- 2. Invoke `shouldDispatchAutoDialectic` from `scripts/lib/auto-dialectic.mjs`:
249
+ 2. Invoke `decideAndRecordAutoDialectic` from `scripts/lib/auto-dialectic.mjs`:
249
250
  ```javascript
250
- import { shouldDispatchAutoDialectic } from '${PLUGIN_ROOT}/scripts/lib/auto-dialectic.mjs';
251
- const decision = await shouldDispatchAutoDialectic({
251
+ import { decideAndRecordAutoDialectic } from '${PLUGIN_ROOT}/scripts/lib/auto-dialectic.mjs';
252
+ const decision = await decideAndRecordAutoDialectic({
252
253
  repoRoot: process.cwd(),
253
254
  cadence: config.dialectic?.cadence ?? 5,
254
255
  });
255
256
  ```
257
+ Same return shape as `shouldDispatchAutoDialectic` (`{trigger, reason, signals}`) — `decideAndRecordAutoDialectic` calls it internally and additionally emits the mechanical `orchestrator.dialectic.nudge_decided` telemetry record on all four return paths (#1200 part c), so the nudge decision is observable without depending on this prose actually reaching step 5/7.
256
258
 
257
259
  3. If `decision.trigger === false`: log `auto-dialectic: not triggered (${decision.reason})` and continue. Emit no nudge. Do NOT update `.orchestrator/dialectic-last-run`.
258
260
 
@@ -290,17 +292,19 @@ After the auto-dialectic nudge decision is made (Phase 3.6.7), and when the reco
290
292
 
291
293
  1. Read Session Config: `reconcile.enabled` (default `false`), `reconcile['rule-expiry-days']` (default `null` — falls back to per-type TTL in the engine), `reconcile['confidence-floor']` (default `0.5`), `reconcile['min-rule-days']` (default `7` — floor window (days) applied to a proposed rule's `expires-at` so a near-dead or already-elapsed natural expiry never produces a born-dead rule, issue #741.1), `reconcile['min-insight-chars']` (default `24` — opt-in minimum insight length gating the eligibility placeholder-insight check, issue #741.2), `reconcile['max-proposals-per-run']` (default `10` — volume brake, issue #900 D; the engine sorts eligible learnings by confidence DESC and proposes at most this many per run). If `reconcile.enabled` is not `true`, log `reconcile: disabled (reconcile.enabled=false)` and skip all remaining steps.
292
294
 
293
- 2. Invoke `runReconcile` from `scripts/lib/reconcile/engine.mjs`:
295
+ 2. Invoke `runReconcileAtSessionEnd` from `scripts/lib/reconcile/engine.mjs`:
294
296
 
295
297
  ```javascript
296
- import { runReconcile } from '${PLUGIN_ROOT}/scripts/lib/reconcile/engine.mjs';
297
- const { proposals, rejected, summary, error } = await runReconcile({
298
+ import { runReconcileAtSessionEnd } from '${PLUGIN_ROOT}/scripts/lib/reconcile/engine.mjs';
299
+ const { proposals, rejected, summary, error } = await runReconcileAtSessionEnd({
298
300
  repoRoot: process.cwd(),
299
301
  ruleExpiryDays: config.reconcile['rule-expiry-days'] ?? undefined,
300
302
  minRuleDays: config.reconcile['min-rule-days'] ?? undefined,
301
303
  minInsightChars: config.reconcile['min-insight-chars'] ?? undefined,
302
304
  maxProposalsPerRun: config.reconcile['max-proposals-per-run'] ?? undefined,
303
305
  now: new Date(),
306
+ // trigger is pinned to 'session-end' IN CODE by runReconcileAtSessionEnd
307
+ // (#1201 Part A) — this prose block no longer sets it.
304
308
  });
305
309
  ```
306
310
 
@@ -384,12 +388,19 @@ After the auto-dialectic nudge decision is made (Phase 3.6.7), and when the reco
384
388
  approved,
385
389
  rejected: [...rejected, ...operatorRejected],
386
390
  repoRoot: process.cwd(),
391
+ // #1099 — FORWARD BOTH. `decideReconcile()` already resolved them onto its
392
+ // RUN decision (`scripts/lib/session-end/phase-skip.mjs`, `targets` +
393
+ // `baselineRoot`); dropping them here silently pins every session to
394
+ // repo-local writes no matter what `reconcile.targets` says. Absent
395
+ // `baselineRoot` is the documented no-op path, not an error.
396
+ targets: decision.targets,
397
+ baselineRoot: decision.baselineRoot,
387
398
  sessionId,
388
399
  });
389
400
  // writeResult = { written: number, archived: number, errors: string[] }
390
401
  ```
391
402
 
392
- `writeApprovedRules` is lock-serialised (via `withFileLock` on `.orchestrator/rules.lock`) and writes each approved proposal to `.claude/rules/<slug>.md`. Rejected proposals (engine-rejected + operator-rejected) are archived to `.orchestrator/reconcile.rejected.log` with reason `user-declined` for operator-rejected and the engine's own audit reason for engine-rejected.
403
+ `writeApprovedRules` is lock-serialised (via `withFileLock` on `.orchestrator/rules.lock`) and writes each approved proposal to the directory its target names — `.claude/rules/<slug>.md` for `repo-local`, `<baselineRoot>/proposals/<slug>.md` for `baseline`. Each target's write root is confined separately; the leaf comes from the renderer-minted `slug`, never from a caller-supplied path. Rejected proposals (engine-rejected + operator-rejected) are archived to `.orchestrator/reconcile.rejected.log` with reason `user-declined` for operator-rejected and the engine's own audit reason for engine-rejected.
393
404
 
394
405
  8. Log outcome for Phase 6 Final Report: `reconcile: ${surfaced.length} surfaced → ${approved.length} approved (written: ${writeResult.written}), ${operatorRejected.length} operator-declined${writeResult.errors.length > 0 ? `, ${writeResult.errors.length} write-errors (see sweep.log)` : ''}`.
395
406
 
@@ -11,12 +11,20 @@ Compute the v0 recommendation from in-memory session metrics and additively writ
11
11
  ```bash
12
12
  node --input-type=module -e "
13
13
  import {appendFileSync, mkdirSync} from 'node:fs';
14
+ import {execSync} from 'node:child_process';
14
15
  import {updateFrontmatterFieldsOnDisk} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
15
16
  import {computeV0Recommendation} from '${PLUGIN_ROOT}/scripts/lib/recommendations-v0.mjs';
16
17
 
17
18
  const SWEEP_LOG = '.orchestrator/metrics/sweep.log';
18
19
 
19
20
  try {
21
+ // repoRoot is REQUIRED — updateFrontmatterFieldsOnDisk calls requireRepoRoot()
22
+ // and THROWS on undefined (scripts/lib/state-md/frontmatter-mutators.mjs).
23
+ // Resolve it explicitly; there is no implicit cwd fallback by design (PSA
24
+ // parallel-session CWD drift). Passing an undefined repoRoot here sends every close down
25
+ // the fail-open catch below and silently omits all 5 fields (GitLab #1036).
26
+ const repoRoot = execSync('git rev-parse --show-toplevel', {encoding: 'utf8'}).trim();
27
+
20
28
  // In-memory session metrics — pulled from the session's running state,
21
29
  // NOT re-read from sessions.jsonl (which was just-written in Phase 3.7).
22
30
  const completionRate = <number from session metrics: completed_issues / planned_issues>;
@@ -33,7 +41,7 @@ try {
33
41
  'rationale': rec.rationale,
34
42
  };
35
43
 
36
- await updateFrontmatterFieldsOnDisk(undefined, fields);
44
+ await updateFrontmatterFieldsOnDisk(repoRoot, fields);
37
45
  console.log('Recommendations written: ' + rec.mode + ' (' + rec.rationale + ')');
38
46
  } catch (err) {
39
47
  // AC3: defensive — exception must NOT block Phase 3.4 status: completed.
@@ -44,11 +52,17 @@ try {
44
52
  error: String(err && err.message ? err.message : err),
45
53
  };
46
54
  appendFileSync(SWEEP_LOG, JSON.stringify(evt) + '\n');
47
- console.error('⚠ Phase 3.7a: recommendation compute failed fields omitted, sweep.log entry written. Continuing.');
55
+ // Name the CAUSE on stderr, not just the consequence: the fail-open path is
56
+ // correct policy, but a WARN that says only 'fields omitted' is what let
57
+ // #1036 survive 5 weeks across two closes (sweep.log had the message, nobody
58
+ // read sweep.log). The operator sees this line in the close transcript.
59
+ console.error('⚠ Phase 3.7a: recommendation compute failed — ' + evt.error + ' — fields omitted, sweep.log entry written. Continuing.');
48
60
  }
49
61
  "
50
62
  ```
51
63
 
64
+ **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
+
52
66
  **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.
53
67
 
54
68
  **Field precision:**
@@ -52,6 +52,8 @@ These are passed via the conversation context (not a file). Parse the preceding
52
52
 
53
53
  > Check this **before Step 0**. If the express path is active, this skill emits a minimal 1-wave plan and exits — no role decomposition, no wave splitting, no agent count computation.
54
54
 
55
+ > Phase 8.5 of session-start hands off here NORMALLY when the express path activates — it does not skip session-plan (#1146). The banner below is printed by `node scripts/express-path.mjs`, and the 1-wave plan this section emits is the artifact `/go` detects.
56
+
55
57
  **Detect express-path activation:** Search the conversation context for the banner line:
56
58
 
57
59
  ```
@@ -136,11 +138,14 @@ Before assigning tasks to waves, discover available agents for this session:
136
138
  - Role keys: `impl`, `test`, `db`, `ui`, `security`, `compliance`, `docs`, `perf`
137
139
  - Example: `agent-mapping: { impl: code-editor, test: test-specialist, db: database-architect }`
138
140
  - If present, these explicit mappings take priority over auto-matching
141
+ - A value MAY carry a channel prefix: `session-orchestrator:<plugin-agent>` or `cursor:<model>` (foreign model, #1150). An unknown prefix is rejected fail-loud by `scripts/lib/config.mjs` at parse time — see `docs/session-config-reference.md` § `agent-mapping` values.
139
142
 
140
143
  **Validation:** If `agent-mapping` specifies an agent name, verify the agent exists:
141
144
  - For project agents: check `<state-dir>/agents/<name>.md` exists
142
145
  - For plugin agents: check the agent is registered (contains `:` separator)
143
- - If the agent doesn't exist: warn the user and fall back to auto-discovery for that role
146
+ - For `cursor:<model>` (foreign channel): the existence check is on the CHANNEL, not the model — `cursor-agent` on `PATH` and logged in (`cursor-agent status`). The model string is free-form and is validated only at dispatch time, because the model catalogue lives outside this repo.
147
+ - If the agent doesn't exist — or the cursor channel is unavailable (binary missing / not logged in) — warn the user and fall back to auto-discovery for that role (same fallback shape in both cases; never hard-fail the plan)
148
+ - **Two constraints the plan must carry into the wave, both owned by `skills/wave-executor/wave-loop.md` § Third branch: foreign-model dispatch** (one place owns the contract — do not restate it here): a `cursor:<model>` mapping is INERT for any `never_foreign` role (impl-core, security-review, migration, release, secrets, incident, refactor-crosscut — the adapter refuses it), and every foreign run requires a MANDATORY Claude semantic diff-review before merge-back. Plan the review as work, not as a formality.
144
149
 
145
150
  3. **Build Agent Registry** (resolution priority):
146
151
  - **Priority 1**: Project agents (from `<state-dir>/agents/` — see Platform Note) — matched by name
@@ -19,6 +19,7 @@ For each wave, define agents with:
19
19
  - `Isolation: worktree` means the wave-executor will pass `isolation: "worktree"` to the Agent tool, giving each agent its own git worktree copy
20
20
  - `MaxTurns` is enforced via the agent prompt — wave-executor includes a turn limit instruction in each agent's prompt
21
21
  - `status` is the mission-status enum value for this wave-plan item (#340). Always `brainstormed` in the initial plan. Wave-executor updates it at gate transitions (validated → in-dev → testing → completed). Rollback to `brainstormed` is allowed from any state. The five values are listed in `SKILL.md` § Mission-Status Enum; nothing validates them mechanically — `setMissionStatus` writes the string it is given to both STATE.md surfaces on purpose, so keeping the value in-enum is the coordinator's job.
22
+ - The wave-plan item's `id` becomes the `taskId` every `setMissionStatus` call for this item uses, and `setMissionStatus` REFUSES ids outside `[a-z][a-z0-9]*(?:-[a-z0-9]+)*-\d+` (lowercase segments, single hyphens, trailing bare digits) with `refused: 'id-grammar'` and a stderr WARN — nothing is written. Mint ids matching it: `m-1`, `docs-2`, `w2-1`, `w2-a-10` all accepted; `w2-a10`, `w3-p2`, `W3-I1`, `Docs_2` all refused.
22
23
 
23
24
  > **Deconfliction rule:** Before finalizing agent specs for a wave, verify that no two agents in the same wave list overlapping `Files:` paths. If overlap is found, either merge the agents into one or move one task to a later wave. Two agents editing the same file in parallel causes merge conflicts that require manual resolution.
24
25
 
@@ -41,8 +41,9 @@ This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's
41
41
 
42
42
  **Outcome handling:**
43
43
  - `PASS_THROUGH` → continue to Phase 1
44
+ - `PASS_THROUGH` with a non-empty `advisory` array (GH#67) → a `discovered` peer with `lockSuperseded: true` never fires the Promotion AUQ (it stays visible, per the #1085 advisory-lock contract — it is not filtered). Print ONE advisory line per entry: `parallel-aware: registry entry <sessionId> (last heartbeat <N> min ago) is superseded by this root's live lock <lockOwnerId> — likely a finished task on a platform without SessionEnd (GH#67); still counted for PSA-001 awareness`, then continue to Phase 1. It remains PSA-002-relevant if the same id also shows up in STATE.md (`source: 'state-md'` is handled by Phase 1.2.1 unchanged).
44
45
  - `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() **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`. 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.
46
+ - `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot, rawSessionId, reason: 'worktree-promotion' })` from `scripts/lib/autopilot/worktree-pipeline.mjs` — since #1170 this ONE call does both jobs: it creates the destination worktree AND, because `rawSessionId` is supplied, releases the source root internally (see below), so no separate `leaveSourceRoot` call is made at this site. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId() **for the worktree-name attribution label only**, `branch` from current HEAD, `repoRoot = process.cwd()`, `rawSessionId` from `readLock({ repoRoot }).session_id`. `sessionId` (the semantic label) 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. **`rawSessionId` 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 aborts the internal `leaveSourceRoot()` teardown with `left.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. `enterWorktree()`'s return value carries the outcome as `left: { ok, steps, reason? }`; `leaveSourceRoot()` never throws, so on `left.ok !== true` `enterWorktree` itself emits the stderr WARN `enterWorktree: leaveSourceRoot: <reason>` and the promotion continues regardless — the destination worktree already exists by the time this runs, 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
47
  - `PROMOTION_OFFER` with user picking "Manuell — in-place daneben" → append Deviation, continue to Phase 1
47
48
  - `PROMOTION_OFFER` with user picking "Abbrechen" → exit cleanly
48
49
 
@@ -53,6 +54,25 @@ This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's
53
54
 
54
55
  Read and parse Session Config per `skills/_shared/config-reading.md`. Store result as `$CONFIG`.
55
56
 
57
+ ## Phase 1.05: Skill-Invocation Self-Report (#1199)
58
+
59
+ > Emit an L1 skill-invocation record for `session-start` itself. The PreToolUse `Skill`-matcher hook only captures skills dispatched via the `Skill` tool — a **prose-invoked** skill like this one is invisible to it (verified gap: external users show 0/20 sessions with a `session-start` row in `skill-invocations.jsonl`, vs. 93/93 for the operator). This self-report closes that gap so L2/L3 skill-health has a `session-start` selection signal. Best-effort, try/catch-silent — it never blocks Phase 1.1.
60
+
61
+ ```javascript
62
+ try {
63
+ const { appendSkillInvocation, DEFAULT_SKILL_INVOCATIONS_PATH } =
64
+ await import('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs');
65
+ const nodePath = await import('node:path');
66
+ await appendSkillInvocation(nodePath.join(process.cwd(), DEFAULT_SKILL_INVOCATIONS_PATH), {
67
+ timestamp: new Date().toISOString(),
68
+ event: 'selected',
69
+ skill: 'session-orchestrator:session-start',
70
+ session_id: null, // no session.lock is bound yet at Phase 1 — the raw id is acquired at Phase 1.2 (#1199)
71
+ phase: 'session-start',
72
+ });
73
+ } catch { /* self-report is advisory — never blocks Phase 1.1 */ }
74
+ ```
75
+
56
76
  ## Phase 1.1: Dispatcher-Autonomy Migration Capture (one-time, per-repo)
57
77
 
58
78
  > Closes session-orchestrator issue #681 (Epic #673 P3 — one-time per-repo dispatcher-autonomy capture). Migration trigger: the first session-start after this feature ships on a repo whose committed `dispatcher-autonomy:` block is still absent. Cross-reference `.claude/rules/ask-via-tool.md` (AUQ via tool, not prose).
@@ -152,13 +172,22 @@ Where `sessionId` is the physical raw identity for this invocation: the native h
152
172
  - 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
173
  `Force-took session lock from session_id=<existingLock.session_id>, age=<ageHours>h, mode=<existingLock.mode>, pid=<existingLock.pid>`. Continue.
154
174
 
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.
175
+ 3. **`result.ok === false`** with `reason === 'stale-heartbeat'`:
176
+ - 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
177
  - Present a choice via `AskUserQuestion`:
158
178
  ```js
179
+ // `heartbeatAgeMinutes` and `ageHours` come straight off the acquire() result (#1137);
180
+ // `sameHost` is not on the result — compute it first. Use hostnamesMatch, NEVER a raw
181
+ // `===` against os.hostname(): the hostname flips spelling on a single machine
182
+ // (measured 2026-08-24: `Mac.home` and `Ferdinands-MacBook-Pro.local` ten minutes apart),
183
+ // so a raw comparison labels this machine's OWN lock "another machine" (#1072).
184
+ // `||`, not `??` — an EMPTY-STRING host_id must fall back to `host`, or
185
+ // hostnamesMatch('', …) is false and this machine reads its own lock as
186
+ // cross-host. Production uses `lockHostCandidate()` from host-identity.mjs.
187
+ const sameHost = hostnamesMatch(existingLock.host_id || existingLock.host, os.hostname());
159
188
  AskUserQuestion({
160
189
  questions: [{
161
- question: `A stale session lock is in the way — started ${ageHours}h ago, its ttl=${existingLock.ttl_hours}h has expired, and pid=${existingLock.pid} on host=${existingLock.host} is ${reason === 'stale-pid-dead' ? 'confirmed dead' : 'still running or status unknown'}. Reclaim it?`,
190
+ 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?`,
162
191
  header: "Stale lock",
163
192
  multiSelect: false,
164
193
  options: [
@@ -170,7 +199,7 @@ Where `sessionId` is the physical raw identity for this invocation: the native h
170
199
  ```
171
200
  - **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
172
201
  ```
173
- A stale session lock is in the way — started <ageHours>h ago, ttl=<ttlHours>h expired, pid=<pid> on <host>. Reclaim it?
202
+ 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?
174
203
  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.
175
204
  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
205
  Reply with the number of your choice.
@@ -186,11 +215,11 @@ Where `sessionId` is the physical raw identity for this invocation: the native h
186
215
 
187
216
  ### Cross-host behaviour
188
217
 
189
- When `existingLock.host !== os.hostname()`, PID liveness cannot be checked (`pidAlive: null`). In this case:
218
+ 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
219
  - For `reason === 'active'`: the recommendation is **Abort** — cross-host locks cannot be verified as dead.
191
220
  - For stale reasons: the recommendation is still **Reclaim** only if TTL is clearly expired (>2× ttl_hours). Otherwise default to **Abort**.
192
221
  - **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)"`.
222
+ - 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
223
 
195
224
  ## Phase 1.2.1: Peer-Guard (Epic #583 defense-in-depth)
196
225
 
@@ -213,11 +242,13 @@ if (peer) {
213
242
  }
214
243
  ```
215
244
 
245
+ GH#67 note: the `lockSuperseded` advisory-downgrade described in Phase 0.5's outcome handling applies only to the `discovered` peer subset — this phase's `peer` is always `source: 'state-md'`, so a `discovered`-side `lockSuperseded: true` never suppresses this guard; the Worktree-Promotion AUQ still fires exactly as below whenever a live STATE.md peer is found.
246
+
216
247
  ### Decision flow
217
248
 
218
249
  1. **`peer === null`** → no active peer owns STATE.md. Continue to Phase 1.5.
219
250
  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`).
220
- - User picks "Worktree anlegen + starten" → call `enterWorktree(...)` and exit Phase 1 immediately (the new worktree's own session-start runs from scratch).
251
+ - User picks "Worktree anlegen + starten" → call `enterWorktree({ ..., rawSessionId, reason: 'worktree-promotion' })` from `scripts/lib/autopilot/worktree-pipeline.mjs` — since #1170 this ONE call also releases the source root: it calls `leaveSourceRoot({ repoRoot, sessionId: rawSessionId, semanticSessionId, reason })` from `scripts/lib/session-transition.mjs` internally, on BOTH success exits, so no separate `leaveSourceRoot` call is made at this site. `rawSessionId` 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). The return value's `left` field carries `leaveSourceRoot()`'s result; it never throws, so on `left.ok !== true` `enterWorktree` itself emits the stderr WARN `enterWorktree: leaveSourceRoot: <reason>` and the promotion continues regardless — 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).
221
252
  - 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.
222
253
  - User picks "Abbrechen" → exit cleanly.
223
254
 
@@ -515,7 +546,7 @@ This single call does three things:
515
546
 
516
547
  - **Generator-marked + idempotent.** The board carries the `_generator: session-orchestrator-active-sessions@1` frontmatter sentinel; repeated writes that produce identical content are no-ops, so re-running this phase never churns the file.
517
548
  - **Host-local + git-ignorable.** The board lives under the operator's vault tree (under `$HOME`), never inside any repo — it is never committed.
518
- - **NEVER touches the sven-owned `_overview.md`.** The writer hard-refuses any path whose basename is `_overview.md` (returns `{ action: 'skipped-handwritten' }`), and only ever overwrites files it owns (frontmatter `_generator` matches the marker). The handwritten overview is structurally safe.
549
+ - **NEVER touches the human-authored `_overview.md`.** The writer hard-refuses any path whose basename is `_overview.md` (returns `{ action: 'skipped-handwritten' }`), and only ever overwrites files it owns (frontmatter `_generator` matches the marker). The handwritten overview is structurally safe.
519
550
 
520
551
  ### Non-blocking behavior
521
552
 
@@ -710,13 +741,14 @@ Group issues by:
710
741
  - **info-only version mismatch** (patch or minor version only): `"ℹ bootstrap.lock: plugin-version=<lock-ver> (current=<plugin-ver>) — minor drift only, no action required."`
711
742
  - **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."`
712
743
 
713
- 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:
744
+ 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:
714
745
  - **warn** (`stale_count > 0`, max `delta_hours <= 48`): `"⚠ vault-staleness: <N> projects stale (max delta: <X>h) — last run <timestamp>."`
715
746
  - **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."`
747
+ - **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."`
716
748
 
717
- 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.
749
+ 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.
718
750
 
719
- 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:
751
+ 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:
720
752
  - **Red** (`status === 'red'`): `"🚨 CI RED on HEAD (pipeline #<currentPipelineId>) — last green: #<lastGreen.pipelineId> (commit <SHA-7>, <redCount> pipelines ago). Failing job: <failingJobName>"`
721
753
  - **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.
722
754
  - **Green** (no `allowFailureJobs`) or **unknown**: silent (no banner) — informational only.
@@ -823,7 +855,9 @@ Group issues by:
823
855
 
824
856
  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.
825
857
 
826
- 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.
858
+ 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.
859
+
860
+ 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).
827
861
 
828
862
  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.
829
863
 
@@ -1061,6 +1095,8 @@ Cross-reference: PRD F2.3 acceptance criteria (#505); `scripts/lib/memory-banner
1061
1095
 
1062
1096
  > 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.
1063
1097
 
1098
+ > **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.
1099
+
1064
1100
  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.
1065
1101
 
1066
1102
  ```javascript
@@ -1180,13 +1216,13 @@ Present your findings following that structure. Key rules:
1180
1216
 
1181
1217
  ### Phase 8.5: Express Path Evaluation (#214)
1182
1218
 
1183
- 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.
1219
+ 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.
1184
1220
 
1185
- When all conditions are met, emits the banner:
1221
+ When all conditions are met, the CLI emits the banner on stderr:
1186
1222
  ```
1187
1223
  Express path activated — <N> tasks, coordinator-direct, no inter-wave checks.
1188
1224
  ```
1189
- 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.
1225
+ 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.
1190
1226
 
1191
1227
  **See `phase-8-5-express-path.md` for full details.**
1192
1228
 
@@ -1207,7 +1243,7 @@ After user alignment:
1207
1243
 
1208
1244
  - **NEVER make assumptions** about code state based on memory or docs — always verify in actual files
1209
1245
  - **NEVER skip the Q&A phase** — the user MUST confirm direction before wave planning
1210
- - **ALWAYS use `run_in_background: false`** for parallel subagent work wait for completion
1246
+ - **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
1211
1247
  - **ALWAYS check `.env` or `.env.local`** for VCS host, API keys, and service URLs
1212
1248
  - **ALWAYS present options with pros/cons and a clear recommendation** — never just list facts
1213
1249
  - **ALWAYS update VCS issue status** when claiming work — use the issue update command per the "Common CLI Commands" section of the gitlab-ops skill
@@ -1219,6 +1255,7 @@ After user alignment:
1219
1255
  | File | Purpose |
1220
1256
  |------|---------|
1221
1257
  | `soul.md` | Identity and communication principles |
1258
+ | (inline) Phase 1.05 | Skill-Invocation Self-Report (#1199) — mirrors session-end Phase 0.6 |
1222
1259
  | (inline) Phase 1.2 | Session Lock Acquire — `acquire()` call, active/stale/cross-host AUQ flows, `forceAcquire()` on user consent, deviation note wiring |
1223
1260
  | (inline) Phase 1.7 | Vault Live-Status Board (#674/#716) — `sweepBoard()` from `scripts/lib/vault-status/board-writer.mjs`; gated on `vault-integration.enabled: true`; marks this repo `in-progress` + host-wide staleness sweep via `enumerateCandidates()` (`scripts/lib/dispatcher/enumerate.mjs`), so a crashed session in ANY repo renders `force-closed` from any repo's session-start; generator-marked + idempotent; never touches `_overview.md`; non-blocking (falls back to single-repo `mirrorBoard()` on enumeration failure) |
1224
1261
  | `presentation-format.md` | Phase 8 output templates and AskUserQuestion examples |
@@ -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
 
@@ -4,13 +4,57 @@
4
4
 
5
5
  After the user confirms the session type and scope via the Q&A above, evaluate whether the **Express Path** applies before handing off to session-plan. The express path collapses the full 5-wave plan into a single coordinator-direct phase for lightweight sessions.
6
6
 
7
- **Activation conditions (ALL three must be true):**
7
+ **Do not evaluate these conditions by hand run the CLI (#1119, #1146).**
8
+
9
+ ```bash
10
+ node scripts/express-path.mjs \
11
+ --repo-root "$PWD" \
12
+ --session-type <housekeeping|feature|deep> \
13
+ --task-count <N> \
14
+ --parallel-agents <true|false>
15
+ ```
16
+
17
+ stdout is one JSON line — `{"activated":<bool>,"reasons":[…]}`. The activation banner and every
18
+ diagnostic go to stderr. Exit `0` means the evaluation COMPLETED, activation and refusal alike:
19
+ branch on the `activated` field, never on the exit code. Exit `1` is an input error (a missing or
20
+ invalid `--session-type` / `--task-count` / `--repo-root`), exit `2` an unreadable config file.
21
+ `--parallel-agents` is optional and tri-state — omitting it asserts nothing and leaves the field
22
+ out of the record, which is NOT the same claim as `false`.
23
+
24
+ `--repo-root` is required and is never defaulted from `SO_PROJECT_DIR` (#941): an ambient
25
+ destination once put a synthetic record into the operator's real fleet ledger.
26
+
27
+ Until #1146 this block was a fenced `js` snippet with an `import` — prose no process runs. Measured
28
+ at HEAD `01eb35d`: `rg -n "evaluateExpressPath" scripts hooks` returned the definition and nothing
29
+ else, so the module had ZERO production callers. A hook cannot close that gap either — `sessionType`
30
+ and `taskCount` exist only AFTER the Phase 8 Q&A, and no hook event fires there. The caller has to
31
+ be something the coordinator RUNS at this point in the flow, which is what `scripts/express-path.mjs`
32
+ is.
33
+
34
+ `evaluateExpressPath` (behind the CLI) makes the decision AND records it as `orchestrator.express_path.evaluated`
35
+ — on **every** evaluation, activation and refusal alike. That is the whole point: until #1119 the
36
+ conditions below were prose only, `scripts/lib/config.mjs` discarded `express-path` **even when the
37
+ block was present** (measured: 88 keys emitted, none of them this one), and the ledger held **0**
38
+ express-path events across its entire history. Whether the path ever fired was unanswerable.
39
+ Re-deriving the conditions in a coordinator turn re-opens exactly that hole; the conditions below
40
+ are the specification the module implements, not a second implementation.
41
+
42
+ `reasons` carries the blocking codes when `activated: false` and the satisfied ones when `true`.
43
+ Nothing short-circuits, so a refusal names **every** blocker — a reader can see whether trimming
44
+ the issue list alone would have helped. Unmeasured inputs are omitted from the payload, never
45
+ written as `0`/`false`, and an unmeasured `sessionType`/`taskCount` fails CLOSED.
46
+
47
+ **Activation conditions (the module's specification):**
8
48
 
9
49
  1. `express-path.enabled` is `true` in Session Config (default: `true` — opt-in by default, opt-out via `express-path.enabled: false`).
10
50
  2. Session type is `housekeeping` (the user confirmed `housekeeping` in Phase 8).
11
51
  3. Agreed issue scope is ≤ 3 issues AND no parallel agents are required (i.e., tasks are sequential, no wave decomposition needed).
12
52
 
13
- **Backward compat:** when `express-path.enabled: false`, this evaluation is skipped entirely and the normal 5-wave session-plan flow runs as before.
53
+ > Condition 3 carries **two** clauses, so the module takes **four** inputs, not three. The
54
+ > condition matrix below and `docs/session-config-reference.md` both list a `housekeeping` / 1–3 /
55
+ > `enabled: true` row that still does NOT activate, because parallel agents are required.
56
+
57
+ **Backward compat:** when `express-path.enabled: false`, the normal 5-wave session-plan flow runs as before. Note that the EVALUATION itself is not skipped — run the CLI regardless, and it returns `{"activated":false,"reasons":["disabled-by-config"]}` and records that refusal. An opt-out that leaves no record is indistinguishable in the ledger from an evaluation that never happened, which is the #1119 hole.
14
58
 
15
59
  **Historical context:** The 13 prior coordinator-direct sessions documented in `CLAUDE.md` (or `AGENTS.md` on Codex CLI; 2026-04 series — vault-mirror GH#31, phased-rollout #307, v3.2.0 release, etc.) were all running this pattern implicitly: no wave decomposition, coordinator executes tasks directly in sequence. This phase codifies what was already proven to work.
16
60
 
@@ -22,23 +66,42 @@ Emit the following banner immediately after the Phase 8 Q&A resolves:
22
66
  Express path activated — <N> tasks, coordinator-direct, no inter-wave checks.
23
67
  ```
24
68
 
25
- Then **skip the handoff to session-plan entirely**. Instead, execute the agreed tasks directly as the coordinator:
26
-
27
- 1. For each agreed task (in dependency order): execute as a direct coordinator action — read files, make changes, run quality checks inline.
28
- 2. After all tasks complete, invoke `skills/session-end/SKILL.md` directly (bypass session-plan and wave-executor).
29
- 3. 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)`.
69
+ > **RESOLVED (#1146, operator decision) session-plan RUNS, in shortened form.** Five documents
70
+ > described the post-activation routing and two of them said session-plan was skipped entirely.
71
+ > That reading cannot work: `commands/go.md` gates on a 1-wave Express Path plan, which under a
72
+ > skipped session-plan would never have been produced — `/go` would look for a plan that does not
73
+ > exist. The routing is now one sentence everywhere:
74
+ >
75
+ > **Phase 8.5 evaluates via `scripts/express-path.mjs`, prints the banner, then hands off NORMALLY
76
+ > to Phase 9 → session-plan.** session-plan detects the banner and its
77
+ > `## Express Path Short-Circuit (#214)` section emits a minimal 1-wave `coordinator-direct` plan
78
+ > (0 agents dispatched, no role decomposition, no wave splitting). `/go` detects that plan per
79
+ > `commands/go.md` § Express Path Detection and routes to coord-direct execution plus
80
+ > session-end auto-invocation — never to wave-executor.
81
+ >
82
+ > What activation skips is the WAVE MACHINERY (subagent dispatch, role decomposition, inter-wave
83
+ > checkpoints), not the planning handoff. The two sites that said otherwise —
84
+ > this file and `skills/session-start/SKILL.md` — were corrected in the same pass;
85
+ > `docs/session-config-reference.md`, `skills/session-plan/SKILL.md` and `commands/go.md`
86
+ > already carried the surviving reading.
87
+
88
+ Hand off to Phase 9 as usual. The coordinator then executes the 1-wave plan session-plan emits directly, without dispatching subagents:
89
+
90
+ 1. Proceed to Phase 9 (session-plan handoff) carrying the banner. session-plan short-circuits to the 1-wave `coordinator-direct` plan; `/go` detects it and does NOT invoke wave-executor.
91
+ 2. 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.
92
+ 3. 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.
30
93
  4. After session-end completes successfully: verify STATE.md `status` is `completed` and `## Deviations` contains the express-path entry from step 3. 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.
31
94
 
32
95
  **Persistence contract:**
33
96
 
34
- The four steps above MUST all happen within a single coordinator turn. Specifically:
97
+ Step 1 is the Phase 9 handoff and ends the session-start turn — the operator types `/go` next, exactly as on the normal path. Steps 2–4 then MUST all happen within a SINGLE coordinator turn, the one `/go` opens. Specifically:
35
98
 
36
- - Step 1 (execute tasks) happens first in the coordinator's main flow.
37
- - Step 2 (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`.
38
- - Step 3 (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.
99
+ - Step 2 (execute tasks) happens first in that turn's main flow.
100
+ - Step 3a (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`.
101
+ - Step 3b (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.
39
102
  - Step 4 (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:`.
40
103
 
41
- If `/go` is invoked but the session-plan emitted a 1-wave Express Path plan (per `skills/session-plan/SKILL.md` § "Express Path Short-Circuit"), the `/go` command MUST detect this and route to coord-direct execution + session-end auto-invocation, NOT to wave-executor. See `commands/go.md` for the detection branch.
104
+ When `/go` is invoked and session-plan emitted a 1-wave Express Path plan (per `skills/session-plan/SKILL.md` § "Express Path Short-Circuit"), the `/go` command MUST detect this and route to coord-direct execution + session-end auto-invocation, NOT to wave-executor. See `commands/go.md` for the detection branch — that plan is the artifact `/go` keys on, which is why Phase 8.5 hands off to session-plan rather than skipping it.
42
105
 
43
106
  **When Express Path does NOT activate** (conditions not met):
44
107
 
@@ -56,6 +119,8 @@ Proceed normally to Phase 9 (session-plan handoff). The express-path evaluation
56
119
 
57
120
  ## See Also
58
121
 
122
+ - `scripts/express-path.mjs` — the CLI this phase runs; `scripts/lib/express-path.mjs` holds the decision + its `orchestrator.express_path.evaluated` record
123
+ - `skills/session-plan/SKILL.md` § "Express Path Short-Circuit (#214)" — the 1-wave plan Phase 9 emits when the banner is present
59
124
  - `commands/go.md` — Express Path detection and auto-invocation of session-end after coord-direct tasks
60
125
  - `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
61
126
  - `commands/close.md` — Rule 2 wording the user sees if express-path persistence breaks
@@ -92,6 +92,26 @@ const vaultNoteTypeSchema = z.enum([
92
92
  'board',
93
93
  ]);
94
94
 
95
+ // VENDORED COPY. SSOT: projects-baseline/packages/zod-schemas/src/vault-frontmatter.ts
96
+ // (`vaultNoteStatusSchema`). This file is a standalone CLI with no exports, so the
97
+ // values cannot be imported — they are hand-kept in sync, and that is the known cost.
98
+ //
99
+ // Measured 2026-08-23: FOUR hand-maintained copies of this enum exist —
100
+ // 1. the SSOT above
101
+ // 2. this file
102
+ // 3. sven-infra `02-cron/vault-overview-sync.sh:216`
103
+ // 4. `tests/lib/vault-mirror/render-sessions.test.mjs:490`, whose own comment names
104
+ // this file as its source and then transcribes it
105
+ // Copy 4 is deliberately NOT widened: it asserts membership for a mapper that emits
106
+ // only `verified`/`draft`, so adding values it cannot produce would weaken it.
107
+ //
108
+ // A drift test reading the SSOT directly was considered and REJECTED: the SSOT lives
109
+ // in a sibling repo resolved host-locally, so such a test passes on this machine and
110
+ // fails in CI, where projects-baseline is not checked out. The durable fix is
111
+ // generation from the SSOT at build time, not a test that reads across a repo
112
+ // boundary. Revisit trigger: a fifth copy, or the first CI-visible drift.
113
+ //
114
+ // Order below mirrors the SSOT exactly, so a diff of the two lists is readable.
95
115
  const vaultNoteStatusSchema = z.enum([
96
116
  'draft',
97
117
  'active',
@@ -100,6 +120,17 @@ const vaultNoteStatusSchema = z.enum([
100
120
  'production',
101
121
  'mvp',
102
122
  'idea',
123
+ // Added 2026-08-23 (baseline MR !27, merge 6f38aeb). sven renders these into
124
+ // `01-projects/*/_overview.md`; measured the same day, SIX of them already carried
125
+ // one of these values (4x dead, 1x paused, 1x maintenance). Falsified rather than
126
+ // assumed: removing the four again produced exactly 6 `status` errors, re-adding
127
+ // them produced 0. Without them the strict gate blocks every vault session-close.
128
+ // Slugs deliberately not listed here — the scanner treats them as private
129
+ // (`check-owner-leakage` CP6), and the count is the load-bearing part anyway.
130
+ 'maintenance',
131
+ 'planned',
132
+ 'paused',
133
+ 'dead',
103
134
  ]);
104
135
 
105
136
  const vaultFrontmatterSchema = z