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
@@ -22,15 +22,18 @@ Fires when an `exclusive`-class session (`bootstrap`, `housekeeping`, `memory-cl
22
22
  ### Claude Code (AskUserQuestion)
23
23
 
24
24
  ```js
25
+ // Unpacked once so the question reads as one sentence instead of five key=value pairs.
26
+ const { mode, host, pid, worktreePath } = blockingSession;
27
+
25
28
  AskUserQuestion({
26
29
  questions: [{
27
- question: `An exclusive session is active in this repository (mode=${blockingSession.mode}, started ${ageHours}h ago, host=${blockingSession.host}, pid=${blockingSession.pid}, worktree=${blockingSession.worktreePath}). This blocks all other modes. How should I proceed?`,
28
- header: "Parallel-Exclusive",
30
+ question: `A ${mode} session (process ${pid} on ${host}) started ${ageHours}h ago in ${worktreePath}. What now?`,
31
+ header: "Repo belegt",
29
32
  multiSelect: false,
30
33
  options: [
31
- { label: "Warten (Recommended)", description: "Wait for the exclusive session to finish. The preamble will not retry automatically re-run the command after the other session closes." },
32
- { label: "Andere Session beenden", description: "I will close the other session myself, then re-run this command. The preamble surfaces but does NOT terminate the other session." },
33
- { label: "Abbrechen", description: "Exit cleanly. No STATE.md initialization, no lock acquired." },
34
+ { label: "Warten (Recommended)", description: "Nothing else can start here until that session closes. This command does not retry — run it again afterwards." },
35
+ { label: "Andere Session beenden", description: "You close it yourself, then run this command again. Nothing here stops the other session for you." },
36
+ { label: "Abbrechen", description: "Exit now. Nothing is written: no STATE.md, no lock." },
34
37
  ],
35
38
  }],
36
39
  });
@@ -39,19 +42,17 @@ AskUserQuestion({
39
42
  ### Codex CLI / Cursor IDE / Pi fallback (numbered Markdown list)
40
43
 
41
44
  ```
42
- Parallel-Exclusive conflict an exclusive session is active in this repository.
43
- Mode: <blockingSession.mode>
44
- Started: <ageHours>h ago (host=<host>, pid=<pid>)
45
- Worktree: <blockingSession.worktreePath>
46
- This blocks all other modes.
45
+ A <mode> session (process <pid> on <host>) started <ageHours>h ago in <worktreePath>. What now?
47
46
 
48
- 1. Warten (Recommended) — wait for the exclusive session to finish; re-run after it closes.
49
- 2. Andere Session beenden — I will close the other session myself.
50
- 3. Abbrechen — exit cleanly without initializing STATE.md.
47
+ 1. Warten (Recommended) — nothing else can start here until that session closes; this command does not retry, so run it again afterwards.
48
+ 2. Andere Session beenden — you close it yourself, then run this command again. Nothing here stops the other session for you.
49
+ 3. Abbrechen — exit now. Nothing is written: no STATE.md, no lock.
51
50
 
52
51
  Reply with the number of your choice.
53
52
  ```
54
53
 
54
+ The four slots are `blockingSession.mode`, `blockingSession.host`, `blockingSession.pid` and `blockingSession.worktreePath`; `<ageHours>` is the age of that session in hours.
55
+
55
56
  ### Outcome handling
56
57
 
57
58
  - **Warten** → exit Phase-0 cleanly with stderr note `parallel-aware: waiting on exclusive session_id=<id>`. No retry loop.
@@ -65,37 +66,42 @@ Fires when the caller is `parallel-ok`-class AND another `parallel-ok` session i
65
66
  ### Claude Code (AskUserQuestion)
66
67
 
67
68
  ```js
69
+ // Unpacked once so the question reads as one sentence instead of three key=value pairs.
70
+ const { mode, pid } = parallelPeer;
71
+
68
72
  AskUserQuestion({
69
73
  questions: [{
70
- question: `A compatible parallel session is active in this worktree (mode=${parallelPeer.mode}, started ${ageHours}h ago, pid=${parallelPeer.pid}). You can either: (a) auto-promote to a sibling worktree to run isolated, or (b) run in-place alongside the existing session (file conflicts likely). How should I proceed?`,
71
- header: "Worktree-Promo",
74
+ question: `A ${mode} session (process ${pid}) started ${ageHours}h ago in this same folder. Run separately or alongside?`,
75
+ header: "Wo starten?",
72
76
  multiSelect: false,
73
77
  options: [
74
- { label: "Worktree anlegen + starten (Recommended)", description: "Create a sibling git worktree at ../<repo-name>-<semantic-session-id>/ and start the new session there. Isolates file edits; recommended for parallel deep/feature sessions. Calls enterWorktree() from scripts/lib/autopilot/worktree-pipeline.mjs." },
75
- { label: "Manuell — in-place daneben", description: "Run in the current worktree alongside the existing session. File conflicts possible; PSA-001/002/004 discipline required. A Deviation is logged." },
76
- { label: "Abbrechen", description: "Exit cleanly. No STATE.md initialization." },
78
+ { label: "Worktree anlegen + starten (Recommended)", description: "Creates a second working folder beside this one and starts there isolates your edits, so nothing collides." },
79
+ { label: "Manuell — in-place daneben", description: "Both sessions write in this same folder conflicts are likely and you resolve them yourself. A Deviation is logged." },
80
+ { label: "Abbrechen", description: "Exit now. Nothing is written: no STATE.md, no lock." },
77
81
  ],
78
82
  }],
79
83
  });
80
84
  ```
81
85
 
86
+ The second working folder is a git worktree at `<basePath>/<repo-name>-<sessionId>/`; `enterWorktree()` from `scripts/lib/autopilot/worktree-pipeline.mjs` creates it (see Outcome handling below). Running in-place puts PSA-001/PSA-002/PSA-004 discipline on the operator.
87
+
82
88
  ### Codex CLI / Cursor IDE / Pi fallback (numbered Markdown list)
83
89
 
84
90
  ```
85
- Worktree-Promotion offer a compatible parallel session is active in this worktree.
86
- Peer mode: <parallelPeer.mode>
87
- Started: <ageHours>h ago (pid=<pid>)
91
+ A <mode> session (process <pid>) started <ageHours>h ago in this same folder. Run separately or alongside?
88
92
 
89
- 1. Worktree anlegen + starten (Recommended) — create sibling worktree and start isolated session.
90
- 2. Manuell — in-place daneben — run alongside (file conflicts possible, Deviation logged).
91
- 3. Abbrechen — exit cleanly.
93
+ 1. Worktree anlegen + starten (Recommended) — creates a second working folder beside this one and starts there; isolates your edits, so nothing collides.
94
+ 2. Manuell — in-place daneben — both sessions write in this folder; conflicts are likely and you resolve them. A Deviation is logged.
95
+ 3. Abbrechen — exit now. Nothing is written: no STATE.md, no lock.
92
96
 
93
97
  Reply with the number of your choice.
94
98
  ```
95
99
 
100
+ The two slots are `parallelPeer.mode` and `parallelPeer.pid`; `<ageHours>` is the age of that session in hours.
101
+
96
102
  ### Outcome handling
97
103
 
98
- - **Worktree anlegen + starten** → invoke `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. The helper creates a sibling worktree at `<basePath>/<repo-name>-<sessionId>/`, runs idempotency + boundary checks, and logs a WARN line to stderr on fresh creation. Once the worktree exists, exit the current preamble flow — the new worktree's own session-start runs from scratch (Phase 1 onwards). On failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit a stderr warning `parallel-aware: enterWorktree failed: <error>; falling back to Manuell` and proceed via the Manuell path.
104
+ - **Worktree anlegen + starten** → invoke `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. The helper creates a sibling worktree at `<basePath>/<repo-name>-<sessionId>/`, runs idempotency + boundary checks, and logs a WARN line to stderr on fresh creation. When `<branch>` is already checked out by another worktree — the normal case, since Phase 0.5 passes the current HEAD — the worktree lands on a fresh `so/<sessionId>` branch created at `<branch>` and the helper returns `{ branch: 'so/<sessionId>', promotedFrom: '<branch>' }` (#1067); the new worktree's STATE.md `branch` MUST record `so/<sessionId>` and note `promoted from <branch>@<repoRoot>`. Once the worktree exists, and BEFORE exiting the preamble flow, call `leaveSourceRoot({ repoRoot, sessionId: <the raw physical session_id owning this root's lock/registry entry>, semanticSessionId, reason: 'worktree-promotion' })` from `scripts/lib/session-transition.mjs`. **Read that `sessionId` from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })` — never the semantic label, and never `current-session.json`, which may describe a peer session (#863); a wrong id aborts the teardown with `ok: false, reason: 'lock-session-mismatch:<owner>'` and removes nothing.** In detail: 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. The call never throws; on `{ ok: false }` emit a stderr WARN `parallel-aware: leaveSourceRoot: <reason>` and continue (the destination worktree already exists — aborting here would leave the two-live-roots state the call prevents). Then exit the current preamble flow — the new worktree's own session-start runs from scratch (Phase 1 onwards). On failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit a stderr warning `parallel-aware: enterWorktree failed: <error>; falling back to Manuell` and proceed via the Manuell path.
99
105
  - **Manuell** → append a Deviation via `appendDeviationOnDisk()`:
100
106
  `Worktree-Auto-Promotion declined; running in-place alongside session_id=<peer.sessionId>, mode=<peer.mode>, pid=<peer.pid>. PSA-001/PSA-002/PSA-004 discipline applies.`
101
107
  Continue Phase-0.
@@ -29,6 +29,33 @@ If an `exclusive`-class session is also active, the Exclusive-Conflict AUQ takes
29
29
  **Always-OK class** (`discovery`, `evolve`, `plan`, `repo-audit`, `portfolio`):
30
30
  The preamble passes through with zero AUQ regardless of other active sessions. Read-only modes never conflict.
31
31
 
32
+ ## Identity Binding for `findPeers` (#1085)
33
+
34
+ `mySessionId` / `callerSessionId` is a **hint for the caller's original
35
+ surface**, not a license to turn an attribution label into ownership. A native
36
+ raw id self-excludes on the discovered lock/registry surface directly. Given a
37
+ semantic hint, `findPeers` may translate it to a concrete local raw id only when
38
+ both proofs hold: `current-session.json` has the same semantic label **and** its
39
+ raw `session_id` exactly equals the readable local lock's raw `session_id`.
40
+
41
+ On a missing, malformed, or mismatched binding, `findPeers` must map nothing and
42
+ leave the discovered lock visible. The STATE.md surface always receives the
43
+ original hint and therefore compares STATE.md `session` as the attribution label
44
+ it is; it is never rewritten to a raw id. This guarded translation is only
45
+ self-exclusion for discovery, not lock/registry ownership and not a continuity
46
+ bridge across a host rotation that changes both values.
47
+
48
+ **What the binding does not prove.** Both files it reads are repo-global, so the
49
+ check establishes that they are mutually CONSISTENT — not that they describe
50
+ *this* process. Semantic labels are routinely shared between simultaneously live
51
+ sessions, and when a foreign live session wrote both files last under a label
52
+ equal to this hint, its raw id is filtered out and the peer disappears from the
53
+ result. Measured 2026-08-21: with a null hint the foreign peer is returned, with
54
+ the colliding semantic hint `peers` is empty. Treat a quiet `findPeers` result as
55
+ weaker evidence than a git or filesystem signal, and prefer passing the native
56
+ raw id whenever the caller has one. Closing this needs a per-process ownership
57
+ proof; see #1091.
58
+
32
59
  ## Preamble Algorithm
33
60
 
34
61
  Execute these steps in order. Any classification determines outcome.
@@ -126,7 +153,7 @@ The skill consuming the preamble translates the outcome:
126
153
  |---------|--------|
127
154
  | `PASS_THROUGH` | Continue immediately. No AUQ. Pre-P1.3 behavior. |
128
155
  | `EXCLUSIVE_BLOCKED` | Fire Exclusive-Conflict AUQ from `parallel-aware-auq.md`. Block until user response. On "Abbrechen": exit cleanly. On "Andere Session beenden": surface to user (preamble does NOT kill other session). On "Warten": pause Phase 0; re-run preamble on user retry. |
129
- | `PROMOTION_OFFER` | Fire Promotion AUQ from `parallel-aware-auq.md`. On "Worktree anlegen": call enterWorktree() from worktree-pipeline.mjs (see parallel-aware-auq.md outcome-handling). On "Manuell": append Deviation (`Worktree-Auto-Promotion declined; running in-place alongside session_id=<peer.sessionId>`) and continue. On "Abbrechen": exit. |
156
+ | `PROMOTION_OFFER` | Fire Promotion AUQ from `parallel-aware-auq.md`. On "Worktree anlegen": call enterWorktree() from worktree-pipeline.mjs, then — BEFORE exiting Phase 0 — `leaveSourceRoot({ repoRoot, sessionId, semanticSessionId, reason: 'worktree-promotion' })` from `session-transition.mjs` (see parallel-aware-auq.md outcome-handling). `sessionId` is the RAW physical `session_id` from this root's `.orchestrator/session.lock` (`readLock({ repoRoot })`), never the semantic label and never `current-session.json` (may describe a peer, #863). The promotion is a process boundary, not a live migration (#1069): the old root is deregistered and its lock released BEFORE the new worktree's own Phase 1.2 acquires — never both roots owning at once. It never throws; on `{ ok: false }` emit a stderr WARN `parallel-aware: leaveSourceRoot: <reason>` and continue. On "Manuell": append Deviation (`Worktree-Auto-Promotion declined; running in-place alongside session_id=<peer.sessionId>`) and continue. On "Abbrechen": exit. |
130
157
 
131
158
  ## Phase 1b Peer-Guard (defense-in-depth)
132
159
 
@@ -142,7 +169,9 @@ The guard is a SOFT-GATE — operator can override, but the warning is mandatory
142
169
  findPeers(repoRoot, { mySessionId }) → peer = peers.find((p) => p.source === 'state-md') →
143
170
  peer === null → safe to write STATE.md; continue Phase 1b normally.
144
171
  peer !== null → fire Promotion AUQ (parallel-aware-auq.md "Promotion" block).
145
- On "Worktree anlegen": enterWorktree() → continue in sibling.
172
+ On "Worktree anlegen": enterWorktree() → leaveSourceRoot(this root)
173
+ → continue in sibling (process boundary,
174
+ old root released before the new acquire).
146
175
  On "Manuell": appendDeviationOnDisk() + continue in-place.
147
176
  On "Abbrechen": exit cleanly.
148
177
  ```
@@ -152,8 +181,10 @@ findPeers(repoRoot, { mySessionId }) → peer = peers.find((p) => p.source === '
152
181
  ```js
153
182
  import { findPeers } from '../../scripts/lib/peer-discovery.mjs';
154
183
 
155
- // Inside Phase 1b, before writing STATE.md:
156
- const { peers } = await findPeers(repoRoot, { mySessionId: sessionId });
184
+ // Inside Phase 1b, before writing STATE.md. Preserve the original
185
+ // attribution-label hint for the STATE.md surface; findPeers guards any
186
+ // semantic→raw translation for discovered peers internally.
187
+ const { peers } = await findPeers(repoRoot, { mySessionId: callerSessionHint });
157
188
  const peer = peers.find((p) => p.source === 'state-md') ?? null;
158
189
  // Phase 1.2.1 consumes only the 'state-md' subset (STATE.md surface only).
159
190
  if (peer !== null) {
@@ -20,10 +20,12 @@ These tools have the same name and behavior on all platforms. Cursor IDE uses eq
20
20
 
21
21
  ## Platform-Specific Tool Mapping
22
22
 
23
+ > **Every statement about a foreign platform carries a measurement date and the tool version it was measured on** (PSA-006). An undated capability claim about Codex CLI, Cursor IDE, or Pi is unverified — re-measure before relying on it, and stamp what you find.
24
+
23
25
  | Function | Claude Code | Codex CLI | Cursor IDE | Pi |
24
26
  |----------|------------|-----------|------------|----|
25
27
  | Present choices to user | `AskUserQuestion` tool with structured options | Numbered Markdown list as plain text, wait for user reply | Numbered Markdown list (same as Codex) | Numbered Markdown list v1; native UI adapter planned |
26
- | Dispatch subagent | `Agent({ description, prompt, subagent_type })` | Delegate via Codex subagents / typed roles (`explorer`, `worker`) when available; otherwise execute sequentially in the main session | Sequential execution no parallel subagents. Execute tasks one by one within a single session. | Sequential execution v1. Do not assume native subagents until the Pi SDK dispatcher exists. |
28
+ | Dispatch subagent | `Agent({ description, prompt, subagent_type })` | Native multi-agent collaboration namespace: `spawn_agent`, `list_agents`, `wait_agent`, `send_message`, `followup_task`, `interrupt_agent`, `close_agent`. Measured 2026-08-25 on `codex-cli 0.141.0` `codex features list` → `multi_agent stable true`. | No native in-session Agent tool. Coordinator-side foreign dispatch via the headless `cursor-agent` CLI exists (`scripts/lib/wave-executor/foreign-dispatch.mjs`, #1150) — measured 2026-08-25 on `cursor-agent 2026.08.11-e8db854`. | Sequential execution v1. Do not assume native subagents until the Pi SDK dispatcher exists. |
27
29
  | Track tasks | `TaskCreate` / `TaskUpdate` / `TaskList` | Plain-text checklist in response context | Plain-text checklist (same as Codex) | Plain-text checklist (same as Codex) |
28
30
  | Enter plan mode | `EnterPlanMode` / `ExitPlanMode` tools | `/plan` slash command (prompt-level, not tool-based) | Instruction-based: "Focus on analysis and planning. Do not modify files until the user approves." | `/plan` prompt template; use instruction-based planning when tool mode is unavailable |
29
31
  | Web search | `WebSearch` tool | Built-in web search (invoke via instruction) | `@web` in Cursor chat | Use Pi's available web/search tools if configured; otherwise Bash curl or browser handoff |
@@ -53,18 +55,22 @@ Agent({
53
55
  description: "3-5 word summary",
54
56
  prompt: "full task context...",
55
57
  subagent_type: "general-purpose",
56
- run_in_background: false
58
+ run_in_background: true // RECOMMENDED for wave dispatch since 2026-08-25 (FA-6);
59
+ // verify the started set via meta.json sidecars, never via the
60
+ // launch ack — see skills/wave-executor/wave-loop.md § Started-Set Verification
57
61
  })
58
62
  ```
59
63
 
60
64
  **On Codex CLI / Codex Desktop:**
61
- Delegate the task in detail using the available Codex subagent mechanism when it exists. Map work to these roles:
65
+ Codex has a native multi-agent collaboration namespace — use it; do not fall back to sequential in-session execution. Measured 2026-08-25 on `codex-cli 0.141.0`: `codex features list` reports `multi_agent stable true`, and a tool-call census over `~/.codex/sessions` (`grep -rhoE '"(spawn_agent|send_message|wait_agent|list_agents|followup_task|interrupt_agent|close_agent)"' . | sort | uniq -c`) returns wait_agent 6041 · send_message 2233 · spawn_agent 1748 · list_agents 836 · close_agent 781 · followup_task 656 · interrupt_agent 109 — i.e. the toolset is in routine production use, not a preview.
66
+
67
+ Lifecycle: `spawn_agent` (launch, one per wave task) → `list_agents` (enumerate the started set) → `wait_agent` (block on completion) → `send_message` / `followup_task` (steer a running agent) → `interrupt_agent` / `close_agent` (abort / reap). Verify the started set via `list_agents`, never via the `spawn_agent` return alone. Map wave work to these prompt-level roles:
62
68
  - **explorer** — read-only evidence gathering (maps to Claude Code's `Explore` subagent)
63
69
  - **worker** — implementation tasks (maps to Claude Code's `general-purpose` subagent)
64
- - **session-reviewer** — quality review when a dedicated review role is available; otherwise perform the review in the main session
70
+ - **session-reviewer** — quality review; spawn as a separate agent rather than reviewing in the main session
65
71
 
66
72
  **On Cursor IDE:**
67
- No Agent() tool or typed agent roles. Execute wave tasks sequentially within the active Composer session. After completing each task, report status and move to the next. Parallel execution is not possible — `agents-per-wave` config is ignored on Cursor.
73
+ No native Agent() tool or typed agent roles inside the Composer session — wave tasks run sequentially there, and `agents-per-wave` is ignored. This is a statement about the IDE session only: coordinator-side foreign dispatch to Cursor models via the headless `cursor-agent` CLI does exist (`scripts/lib/wave-executor/foreign-dispatch.mjs`, #1150 detached worktree, filesystem-measured result, `orchestrator.foreign_dispatch.completed` telemetry, mandatory Claude review, `NEVER_FOREIGN_ROLES` lock). Measured 2026-08-25 on `cursor-agent 2026.08.11-e8db854`. Slash commands live in `.cursor/commands/`. There is no Skill tool — Read `skills/<name>/SKILL.md` instead.
68
74
 
69
75
  **On Pi:**
70
76
  No Session Orchestrator Pi subagent dispatcher exists in v1. Execute wave tasks sequentially in the active Pi session. Treat `agents-per-wave` as advisory until the SDK-based dispatcher lands.
@@ -17,7 +17,7 @@ current-wave: <N>
17
17
  total-waves: <N>
18
18
  # Optional fields (schema-version 1, additive for backward-compat):
19
19
  updated: <ISO 8601 UTC> # last write timestamp, touched by any writer
20
- session: <session-id> # <branch>-<YYYY-MM-DD>-<mode>-<n> (semantic, since #573); legacy UUID-v4 also accepted by parseSessionId
20
+ session: <session-label> # attribution/history label; normally semantic since #573, legacy UUID-v4 remains readable; never a lock/registry ownership key
21
21
  session-start-ref: <sha> # git ref at session start
22
22
  ---
23
23
  ```
@@ -25,7 +25,7 @@ session-start-ref: <sha> # git ref at session start
25
25
  ### Required vs. optional fields
26
26
 
27
27
  - `schema-version`, `session-type`, `branch`, `issues`, `started_at`, `status`, `current-wave`, `total-waves` — **required** in every session-owned STATE.md.
28
- - `updated`, `session`, `session-start-ref` — **optional**. Added by #184. STATE.md files without these fields remain valid and should be treated as `updated: null` / `session: null`. Writers SHOULD populate these fields but readers MUST tolerate their absence. The `session` field's value format is `<branch>-<YYYY-MM-DD>-<mode>-<n>` since #573 (Epic #568 Parallel-Aware Sessions P2.2); pre-#573 files may contain a UUID-v4 — both formats are read via `parseSessionId()` from `scripts/lib/session-id.mjs` per PRD §3 P2 row 3 (backward-compat).
28
+ - `updated`, `session`, `session-start-ref` — **optional**. Added by #184. STATE.md files without these fields remain valid and should be treated as `updated: null` / `session: null`. Writers SHOULD populate these fields but readers MUST tolerate their absence. `session` is an attribution/history label, normally `<branch>-<YYYY-MM-DD>-<mode>-<n>` since #573 (Epic #568 Parallel-Aware Sessions P2.2); pre-#573 files may contain a UUID-v4 — both formats are read via `parseSessionId()` from `scripts/lib/session-id.mjs` per PRD §3 P2 row 3 (backward-compat). Neither form grants lock or registry ownership.
29
29
 
30
30
  The `session-type: none` + `status: idle` combination is used only for bootstrap-scaffolded placeholder files (no active session).
31
31
 
@@ -79,6 +79,32 @@ A log of unresolved, user-facing questions surfaced by wave agents during a sess
79
79
 
80
80
  Helpers: `readOpenQuestions` (pure), `appendOpenQuestion` (pure), `markOpenQuestionAnswered` (pure), `appendOpenQuestionOnDisk` (lock-guarded write), `markOpenQuestionAnsweredOnDisk` (lock-guarded write) — all exported from `scripts/lib/state-md.mjs`.
81
81
 
82
+ ## Session Identity and Lock Ownership (#1085)
83
+
84
+ This contract distinguishes a physical live-session key from labels that make a
85
+ session intelligible to people and history readers. It does not add an identity
86
+ layer.
87
+
88
+ - **`session_id` is the only live ownership key.** It is the native raw identity
89
+ supplied by the active harness, or a generated UUID when no trustworthy raw
90
+ identity is available. Lock acquisition, registry membership, self-exclusion,
91
+ proof checks, and lock release use this physical key.
92
+ - **`semantic_session_id` and STATE.md `session` are attribution/history
93
+ labels, never ownership.** They may describe the same work to a human, but
94
+ equality of either label cannot acquire, refresh, release, or reclaim a lock.
95
+ A legacy UUID in STATE.md remains readable only as historical data.
96
+ - **Never bridge a raw mismatch with a label or a proof.** If the current raw
97
+ id and a live lock's raw id differ, ownership is ambiguous. Leave the live
98
+ lock visible and let its TTL/Reaper lifecycle resolve it; do not substitute a
99
+ semantic match, STATE.md `session` match, or owner-proof match.
100
+ - **There is no `logical_session_id`.** A true cross-harness restart-continuity
101
+ contract requires a trusted native resume identifier and remains a follow-up.
102
+ In particular, a host rotation that changes both raw and semantic values has
103
+ no guaranteed continuity.
104
+
105
+ The peer-discovery and issue-budget procedures below apply these rules at their
106
+ narrow surfaces; neither creates a second ownership model.
107
+
82
108
  ## CCU-009 — Status = Index, Never History (#730/H6)
83
109
 
84
110
  > Adopted from an external-repo fleet-mining finding (2026-07-02): narrative
@@ -117,6 +143,20 @@ The Ownership Model above resolves *STATE.md* specifically, but the same discipl
117
143
 
118
144
  This is the wave-plan-time analog of PSA-007 (subagents never race the shared git index) applied one layer up, to shared *files* rather than the git index — see [`../../.claude/rules/parallel-sessions.md`](../../.claude/rules/parallel-sessions.md) § PSA-007.
119
145
 
146
+ ### `wave-scope.json` Session Binding (#1123)
147
+
148
+ The rule above deconflicts writers *inside one wave*. The same working copy is also shared across SESSIONS, and `<state-dir>/wave-scope.json` is the one control artefact that constrains writes rather than describing them. It lives in the working copy, not in the session — so before #1123 a manifest written by session A governed session B's every Edit. Measured 2026-08-22 (#1082): a Discovery wave's `allowedPaths: []` — prescribed for every Discovery wave — denied all writes of an unrelated parallel session, with a deny reason that could only tell it to fix a wave plan it does not own.
149
+
150
+ **The manifest is SESSION-BOUND since #1123.** The coordinator that writes it names itself in two optional fields, `session` (raw `session_id`) and `semantic_session`, both from ONE `sessionAttribution(repoRoot)` call (`scripts/lib/events.mjs`) — see `skills/wave-executor/wave-loop.md` § Scope Manifest 1. `hooks/enforce-scope.mjs` Gate 3b classifies the manifest with `readOwnSessionIds()` + `classifyManifestSession()` (`scripts/lib/session-identity/own-session.mjs`):
151
+
152
+ - **`foreign`** (ids present, none of them ours) → the gate ALLOWS the write and emits one `orchestrator.scope.foreign_session_ignored` event. A foreign manifest is somebody else's wave plan; it never had authority here, and the event keeps the skip counted rather than silent.
153
+ - **`own`** → enforce, unchanged.
154
+ - **`unknown`** — no id in the manifest (legacy, pre-#1123) or our own identity unresolvable → enforce, unchanged. Only what is PROVABLY foreign is treated as foreign; a guess would turn "cannot tell" into a silent enforcement-off.
155
+
156
+ Two consequences for anyone touching this artefact. A stale manifest left by a crashed or finished PEER session no longer scopes this session out of its own writes — but a stale manifest of THIS session still does, so the § Scope Manifest lifecycle (delete `wave-scope.json` with `filescopes/` at session end) remains the operator's job. And an empty id is never an honest "unbound": `scripts/validate-wave-scope.mjs` (`validateSession()`) rejects `"session": ""` as an ERROR while an ABSENT key is only a warning, because an empty id matches nobody and would make every reader treat the manifest as foreign where the writer meant "binds everyone".
157
+
158
+ Mechanism, disposition table and named limits: [`../../docs/scope-collision-guard.md`](../../docs/scope-collision-guard.md) § 2.3.
159
+
120
160
  ## Guards
121
161
 
122
162
  ### Branch Validation
@@ -174,8 +214,8 @@ The `.orchestrator/session.lock` file is written mechanically by `hooks/_lib/loc
174
214
 
175
215
  ```json
176
216
  {
177
- "session_id": "<UUID-v4 OR semantic-id>",
178
- "semantic_session_id": "<branch>-<YYYY-MM-DD>-<mode>-<n>",
217
+ "session_id": "<native-raw-id OR generated-UUID>",
218
+ "semantic_session_id": "<attribution-label>",
179
219
  "started_at": "<ISO-8601 UTC>",
180
220
  "last_heartbeat": "<ISO-8601 UTC>",
181
221
  "mode": "deep|feature|housekeeping|session|...",
@@ -189,8 +229,8 @@ The `.orchestrator/session.lock` file is written mechanically by `hooks/_lib/loc
189
229
 
190
230
  | Field | Required since | Description |
191
231
  |---|---|---|
192
- | `session_id` | v1 | The session identifier (UUID-v4 on Claude Code, semantic on Codex/Cursor). |
193
- | `semantic_session_id` | v2 (Epic #583) | The semantic form (`<branch>-<YYYY-MM-DD>-<mode>-<n>`) **always present**, even when `session_id` is a UUID. Closes D4 gap: semantic-id branch was previously dead code on Claude Code (stdin always provides UUID). |
232
+ | `session_id` | v1 | The physical live lock/registry ownership key: a native raw harness identity, or a generated UUID when no trustworthy raw identity exists. Never use a semantic label here. |
233
+ | `semantic_session_id` | v2 (Epic #583) | An attribution/history label, normally `<branch>-<YYYY-MM-DD>-<mode>-<n>`, surfaced alongside the raw key. It never establishes lock or registry ownership, including when it equals STATE.md `session`. |
194
234
  | `started_at` | v1 | ISO-8601 timestamp when the lock was written. |
195
235
  | `last_heartbeat` | v2 (Epic #583) | ISO-8601 timestamp updated by the `SessionStart` hook and by `PostToolBatch`/`Stop` hooks. **Basis for liveness determination** — replaces PID-liveness (see below). |
196
236
  | `mode` | v1 | Session mode consulted by exclusivity-matrix. May be `"unknown"` in the provisional lock written by the hook before Session Config + AUQ have settled. |
@@ -211,11 +251,24 @@ This replaces the v1 PID-liveness check (`process.kill(pid, 0)`) which was funda
211
251
  Despite the v2 liveness rule above existing since Epic #583, `acquire()`'s conflict classifier (`scripts/lib/session-lock.mjs`, the `classifyExisting()` closure) and `checkStale()` still let `pidAlive`/TTL-age act as an independent veto — which let an external `/close` observe the lock's recorded `pid` (the ephemeral hook subprocess / `node -e acquire()` PID, routinely dead within <1s) as dead and misclassify a live, actively-heartbeating session as `stale-pid-dead`, hijacking it mid-wave. Fixed in #744:
212
252
 
213
253
  - `classifyExisting()` now checks `isLockLive(existing)` **first** and unconditionally returns `{ reason: 'active' }` when true — a dead recorded `pid` can never veto a fresh `last_heartbeat`.
214
- - Only once `isLockLive()` is false does `pidAlive` pick the stale variant: `stale-pid-dead` when `pidAlive === false` (same-host, confirmed dead), else `stale-pid-alive` which also covers `pidAlive === null` (cross-host locks, `host !== os.hostname()`, never a confirmable dead PID, so cross-host locks can never land on `stale-pid-dead`).
215
- - `checkStale()` surfaces the same `isLockLive()` result as an additive `isLive` field alongside the legacy `ttlExpired`/`pidAlive` signals, so recovery-flow diagnostics can observe when the two diverge.
254
+ - Only once `isLockLive()` is false is the lock classified stale — see the #1137 follow-up below for the single reason it now returns.
255
+ - `checkStale()` surfaces the same `isLockLive()` result as an additive `isLive` field alongside the legacy `ttlExpired` signal, so recovery-flow diagnostics can observe when the two diverge.
216
256
 
217
257
  Net: `pid` (field notes above) stays forensic-only; `last_heartbeat` freshness is the sole determinant of "is this session still active" everywhere in `session-lock.mjs`.
218
258
 
259
+ ### #1137 — one stale reason, `stale-heartbeat`
260
+
261
+ #744 left the *stale* half still keyed on `pidAlive`: `stale-pid-dead` when the recorded pid was confirmed dead, `stale-pid-alive` otherwise. Measured 2026-08-23 across the fleet's live locks: **7 of 7 recorded pids were dead**, including the lock of the session that was heartbeating at that very moment. The pid on a lock is the `node -e` / hook subprocess that wrote it, and it exits within about a second of genesis. Two consequences, both live defects:
262
+
263
+ - `stale-pid-alive` was **structurally unreachable** same-host — nothing could produce it except a pid-number collision.
264
+ - The Phase-1.2 recovery AUQ rendered "pid=… is confirmed dead" for **every** same-host stale lock, presenting a measurement it had not made as the operator's reason to reclaim.
265
+
266
+ The fix removes the question rather than re-answering it. `classifyExisting()` returns exactly one stale reason, `stale-heartbeat`, carrying `ageHours` (age from `started_at`, unchanged) and `heartbeatAgeMinutes` (age from `last_heartbeat`) — the quantity the liveness rule actually thresholds against, so a recovery prompt states the measured heartbeat age instead of a liveness verdict. `checkStale()` gains the same `heartbeatAgeMinutes` field.
267
+
268
+ **#1151 follow-up — the `pidAlive` stub is GONE.** #1137 left `checkStale()` returning `pidAlive: null` as a shape-compatible placeholder. It was removed outright: measured @ `f0766e1`, zero production readers repo-wide, so the field only invited a reader to treat `null` as "unknown liveness" — a question this code no longer asks. `checkStale()` now returns `isLive` (the verdict) and `heartbeatAgeMinutes` (the magnitude behind it); anything reasoning about a lock's liveness reads those two. `isPidAliveOnHost` stays exported for `file-lock.mjs` and `lock-reaper.mjs`, where the pid IS the process being asked about.
269
+
270
+ `isPidAliveOnHost` remains exported from `session-lock.mjs` and is unaffected — `file-lock.mjs` and `lock-reaper.mjs` are legitimate callers, because there the pid IS the process being asked about.
271
+
219
272
  ### Schema v1 → v2 backward-compat
220
273
 
221
274
  Readers (e.g., `readLock()` in `session-lock.mjs`, `discoverActiveSessions()`) MUST tolerate absent `last_heartbeat` and `semantic_session_id` fields (v1 locks written before Epic #583). When `last_heartbeat` is absent, fall back to TTL-based expiry from `started_at`. When `semantic_session_id` is absent, treat as unknown.
@@ -206,7 +206,11 @@ const modeSelector = async () => {
206
206
  // Each iteration rebuilds signals from current disk state. STATE.md will be
207
207
  // freshly idle-reset by the previous /close, sessions.jsonl will have the
208
208
  // new tail entry, etc. This is the contract: live signals every iteration.
209
- const signals = await buildLiveSignals({ backlogLimit: 50 });
209
+ // `repoRoot` is passed explicitly (#1071) rather than left to the helper's
210
+ // cwd default. Do NOT hand-write a `backlogLimit` here — the window default
211
+ // lives once, in backlog-scan.mjs (`DEFAULT_BACKLOG_LIMIT`); see
212
+ // skills/session-start/phase-7-5-mode-selector.md for the same contract.
213
+ const signals = await buildLiveSignals({ repoRoot: process.cwd() });
210
214
  return selectMode(signals);
211
215
  };
212
216
 
@@ -19,7 +19,7 @@ description: >
19
19
 
20
20
  This skill runs when the Bootstrap Gate is closed (missing CLAUDE.md, Session Config, or `.orchestrator/bootstrap.lock`) or when the user invokes `/bootstrap` directly. It scaffolds the minimum structure required by all session-orchestrator skills, commits it, and writes the lock file that opens the gate for all future invocations.
21
21
 
22
- **Anti-bureaucracy contract:** At most ONE `AskUserQuestion` call in the normal case (tier confirmation). A second question is only asked when the archetype is truly ambiguous on the Public Path for Standard/Deep tiers. No wizard, no multi-step flow.
22
+ **Anti-bureaucracy contract:** On a first-time full bootstrap (no tier flags, no `--no-interview`), expect **7–9** `AskUserQuestion` prompts in three fixed blocks — not an open-ended wizard. (1) **Tier/stack** (Phase 2): one tier-confirmation question, plus an optional second archetype question when `PATH_TYPE = public` and archetype confidence is low (Standard/Deep only). (2) **Owner persona** (Phase 3.5): five questions from `scripts/lib/owner-interview.mjs` (first-run only). (3) **Dispatcher autonomy** (Phase 3.5.1): one question from `scripts/lib/config/dispatcher-autonomy-capture.mjs`. Flagged flows (`--upgrade`, `--retroactive`, `--sync-rules`, `--ecosystem-health`) and `--no-interview` skip some or all of these blocks.
23
23
 
24
24
  ## Invocation Context
25
25
 
@@ -79,7 +79,6 @@ AskUserQuestion({
79
79
  question: "Leeres Repo erkannt. Basierend auf '<HEURISTIC_REASON>' empfehle ich **<RECOMMENDED_TIER>**. Passt das?",
80
80
  header: "Bootstrap",
81
81
  options: [
82
- { label: "<RECOMMENDED_TIER> (Empfohlen)", description: "<one-line description of what this tier scaffolds>" },
83
82
  { label: "fast", description: "Nur CLAUDE.md + .gitignore + README. Für Demos, Spikes, Playgrounds." },
84
83
  { label: "standard", description: "Fast + package.json/Manifest + TypeScript + Linting + Tests. Für MVPs und echte Produkte." },
85
84
  { label: "deep", description: "Standard + CI + CODEOWNERS + CHANGELOG. Für Production, Team, Langlebige Repos." },
@@ -90,6 +89,8 @@ AskUserQuestion({
90
89
  })
91
90
  ```
92
91
 
92
+ Before rendering: append ` (Empfohlen)` to whichever of the three tier labels equals `<RECOMMENDED_TIER>`, and move that option to position 1. The recommended tier is one of the three — listing it a fourth time as its own option made five options, one more than `AskUserQuestion` accepts, and repeated the same choice twice.
93
+
93
94
  If user selects "Abbrechen": stop. Report "Bootstrap abgebrochen. Kein Kommando wird ausgeführt." Do not continue.
94
95
 
95
96
  Store confirmed tier as `CONFIRMED_TIER`.
@@ -119,7 +120,7 @@ AskUserQuestion({
119
120
  })
120
121
  ```
121
122
 
122
- Store as `CONFIRMED_ARCHETYPE`. Maximum interactions in bootstrap flow: **2 questions total**.
123
+ Store as `CONFIRMED_ARCHETYPE`. The tier/stack block contributes **1–2** questions; a first-run full bootstrap adds **6 more** from the owner interview (Phase 3.5, five questions) and dispatcher-autonomy capture (Phase 3.5.1, one question) — **7–9 total**.
123
124
 
124
125
  ## Upgrade Flow (`--upgrade <tier>`)
125
126
 
@@ -585,7 +586,7 @@ If invoked directly via `/bootstrap`: report the created files list and stop.
585
586
 
586
587
  - **NEVER create application code during bootstrap** — only structural files (CLAUDE.md, .gitignore, README.md, manifests, CI). The feature that follows brings its own implementation.
587
588
  - **NEVER skip the lock file write** — `.orchestrator/bootstrap.lock` is the gate's mechanical truth. Bootstrap without a lock file is incomplete.
588
- - **NEVER ask more than 2 questions** — even if the user's intent is unclear, make a best-effort recommendation and let the user correct via `/bootstrap --upgrade` later.
589
+ - **Fixed question budget, no ad-hoc prompts** — tier/stack (1–2), owner interview (5, Phase 3.5), dispatcher-autonomy capture (1, Phase 3.5.1) sum to **7–9** on a first-run full bootstrap; `--no-interview` and flag short-circuits reduce this. Make a best-effort tier recommendation and let the user correct via `/bootstrap --upgrade` later — do not add prompts beyond these blocks.
589
590
  - **ALWAYS commit** — bootstrap ends with a git commit. The lock file is part of that commit.
590
591
  - **ALWAYS check for retroactive flag** — if `--retroactive` is in `$ARGUMENTS`, skip all scaffolding and jump directly to writing `bootstrap.lock` (tier inferred from existing file inventory, fallback: `fast`).
591
592
  - **NEVER abort bootstrap on rules-fetch failure** — rules-fetch is opt-in and best-effort. The legacy Clank sync path is the safety net.
@@ -11,25 +11,34 @@
11
11
 
12
12
  ---
13
13
 
14
- ## #parallel-sessions-rule — Step 3a: Install Parallel-Sessions Rule
14
+ ## #parallel-sessions-rule — Step 3a: Install Canonical Rules
15
15
 
16
- Write the vendored rule from `$PLUGIN_ROOT/templates/_shared/rules/parallel-sessions.md` to `$REPO_ROOT/.claude/rules/parallel-sessions.md`.
16
+ Vendor the canonical always-on rules from the plugin's `rules/` library into `$REPO_ROOT/.claude/rules/`. `rules/` is the single source of truth for every distributable rule — never `cp` a rule file from anywhere else (see "Why one writer" below).
17
17
 
18
- Idempotency:
18
+ Idempotency is handled by the writer itself:
19
19
  - Missing → create
20
- - Exists and byte-identical → skip silently
21
- - Exists and differs → overwrite (vendored is canonical)
20
+ - Exists, plugin-owned (first line is the `<!-- source: session-orchestrator plugin ... -->` header) and byte-identical → skip silently
21
+ - Exists, plugin-owned and stale → overwrite (the plugin copy is canonical)
22
+ - Exists WITHOUT that header → preserved untouched (a repo-private rule the operator authored)
22
23
 
23
24
  Shell:
24
25
  ```bash
25
- mkdir -p "$REPO_ROOT/.claude/rules"
26
- cp "$PLUGIN_ROOT/templates/_shared/rules/parallel-sessions.md" "$REPO_ROOT/.claude/rules/parallel-sessions.md"
26
+ mkdir -p "$REPO_ROOT/.claude"
27
+ node "$PLUGIN_ROOT/scripts/lib/rules-sync.mjs" --repo-root "$REPO_ROOT"
27
28
  cp "$PLUGIN_ROOT/templates/_shared/loop.md" "$REPO_ROOT/.claude/loop.md"
28
29
  ```
29
30
 
30
- Why: PSA-003 destructive-command safeguards require every consumer repo to carry the rule. See issue #155. The `loop.md` vendor gives bare `/loop` a repo-aware maintenance prompt (issue #633 Hebel 3).
31
+ The command prints a JSON report (`written` / `skipped` / `preserved` / `errors` / `warnings` / `sanitizer`) and exits non-zero on any error. Surface `errors[]` to the operator; a non-empty `preserved[]` is normal and means a repo-private rule was left alone.
31
32
 
32
- Note: This step runs before the baseline-fetch step (S99/D99). If that step executes and fetches a newer version of `parallel-sessions.md` from the baseline, the baseline version wins (S99 overwrites by designacceptable).
33
+ Also surface `sanitizer[]` (issue #1098) — `{file, line, kind, text}` records for citations that read fine inside the plugin repo and dangle once vendored (`repo-local-path`, `unresolvable-see-also`). The CLI additionally prints each one to stderr as `rules-sync: sanitizer <kind> <file>:<line> — <text>`. **Report it to the operator; do not act on it automatically**the sanitizer never rewrites content and never changes the exit code, because silently stripping a citation would change a rule's meaning at vendoring time. A human decides whether the citation is a leak.
34
+
35
+ Archetype-scoped entries in `rules/_index.md` resolve from `.orchestrator/bootstrap.lock`, which does not exist yet at this step — they report `archetype-unknown` and are skipped. The always-on rules (including `parallel-sessions.md`) are universal and vendor regardless. Re-run `/bootstrap --sync-rules` after the lock is written to pick up the archetype-scoped ones.
36
+
37
+ Why: PSA-003 destructive-command safeguards require every consumer repo to carry the parallel-sessions rule. See issue #155. The `loop.md` vendor gives bare `/loop` a repo-aware maintenance prompt (issue #633 Hebel 3).
38
+
39
+ Why one writer (issue #1060): a literal `cp` from a second source directory bypasses the pre-write validator AND lands a file carrying no provenance header. On the next `--sync-rules` a headerless file is classified as a repo-private override and preserved forever — so the plugin can never update it again, and whichever rival copy is smaller silently wins. `rules/` is the only source with a manifest, archetype scoping, a basename-collision guard and a pre-write validator, so it is the only sanctioned writer to `.claude/rules/`.
40
+
41
+ Note: This step runs before the baseline-fetch step (S99/D99), and S99 must NOT overwrite a rule that `rules/` owns. The baseline's copy carries no provenance header, so letting it win would permanently mark the target as a repo-private override — the exact failure described above. `.claude/rules/parallel-sessions.md` has therefore been removed from the S99 manifest. Any other basename present in BOTH `rules/_index.md` and the S99 manifest has the same defect and needs the same treatment.
33
42
 
34
43
  ---
35
44
 
@@ -133,7 +142,6 @@ if [[ -n "$BASELINE_REF" && -n "${GITLAB_TOKEN:-}" && -n "${GITLAB_HOST:-}" && -
133
142
  .claude/rules/swift.md
134
143
  .claude/rules/mvp-scope.md
135
144
  .claude/rules/cli-design.md
136
- .claude/rules/parallel-sessions.md
137
145
  .claude/rules/ai-agent.md
138
146
  .claude/rules/claude-code-usage.md
139
147
  MANIFEST
@@ -492,14 +492,18 @@ If the call fails, log the structured message above and continue. Raw API respon
492
492
 
493
493
  Canonical implementation in [`_shared-template.md#parallel-sessions-rule`](_shared-template.md).
494
494
 
495
- Write the vendored rule from `$PLUGIN_ROOT/templates/_shared/rules/parallel-sessions.md` to
496
- `$REPO_ROOT/.claude/rules/parallel-sessions.md` (idempotent: missing→create, identical→skip,
497
- differs→overwrite). The shared partial also runs
495
+ Run `node "$PLUGIN_ROOT/scripts/lib/rules-sync.mjs" --repo-root "$REPO_ROOT"` — it vendors every
496
+ rule registered in `rules/_index.md` (idempotent: missing→create, identical→skip, differs→overwrite),
497
+ and it is the only writer that applies the pre-write validator, the basename-collision guard and the
498
+ copy-on-write branch that preserves repo-private rules. The shared partial also runs
498
499
  `cp "$PLUGIN_ROOT/templates/_shared/loop.md" "$REPO_ROOT/.claude/loop.md"` so bare `/loop` gets a
499
- repo-aware maintenance prompt. See shared partial for full shell command. Issues #155, #633.
500
+ repo-aware maintenance prompt. See shared partial for full shell command. Issues #155, #633, #1060.
500
501
 
501
- Note: Runs before D99. If D99 (via inherited S99) fetches a newer `parallel-sessions.md` from
502
- the baseline, the baseline version wins (acceptable S99 is canonical).
502
+ Note: Runs before D99. D99 (via inherited S99) no longer fetches `parallel-sessions.md` from the
503
+ baseline that entry was removed from the S99 manifest in #1060. The baseline copy carries no
504
+ provenance header, so letting it win would make the next `--sync-rules` classify the target as a
505
+ repo-private override and preserve it permanently, i.e. the plugin could never update that rule
506
+ again. `rules/` is the single source for it.
503
507
 
504
508
  ## Step 3b: Initialize .orchestrator/metrics/ (Deep) (#185)
505
509
 
@@ -162,23 +162,30 @@ build/
162
162
 
163
163
  Note: `.orchestrator/` is NOT gitignored — `bootstrap.lock` must be committed. Only the platform state dirs (`.claude/`, `.codex/`, `.cursor/`) are excluded.
164
164
 
165
- ## Step 3a: Install Parallel-Sessions Rule
165
+ ## Step 3a: Install Canonical Rules
166
166
 
167
- Write the vendored rule from `$PLUGIN_ROOT/templates/_shared/rules/parallel-sessions.md` to `$REPO_ROOT/.claude/rules/parallel-sessions.md`.
167
+ Vendor the canonical always-on rules from the plugin's `rules/` library into `$REPO_ROOT/.claude/rules/`. `rules/` is the single source of truth for every distributable rule — never `cp` a rule file from anywhere else.
168
168
 
169
- Idempotency:
169
+ Idempotency is handled by the writer itself:
170
170
  - Missing → create
171
- - Exists and byte-identical → skip silently
172
- - Exists and differs → overwrite (vendored is canonical)
171
+ - Exists, plugin-owned (first line is the `<!-- source: session-orchestrator plugin ... -->` header) and byte-identical → skip silently
172
+ - Exists, plugin-owned and stale → overwrite (the plugin copy is canonical)
173
+ - Exists WITHOUT that header → preserved untouched (a repo-private rule the operator authored)
173
174
 
174
175
  Shell:
175
176
  ```bash
176
- mkdir -p "$REPO_ROOT/.claude/rules"
177
- cp "$PLUGIN_ROOT/templates/_shared/rules/parallel-sessions.md" "$REPO_ROOT/.claude/rules/parallel-sessions.md"
177
+ mkdir -p "$REPO_ROOT/.claude"
178
+ node "$PLUGIN_ROOT/scripts/lib/rules-sync.mjs" --repo-root "$REPO_ROOT"
178
179
  cp "$PLUGIN_ROOT/templates/_shared/loop.md" "$REPO_ROOT/.claude/loop.md"
179
180
  ```
180
181
 
181
- Why: PSA-003 destructive-command safeguards require every consumer repo to carry the rule. See issue #155. The `loop.md` vendor gives bare `/loop` a repo-aware maintenance prompt (issue #633 Hebel 3).
182
+ The command prints a JSON report (`written` / `skipped` / `preserved` / `errors` / `warnings` / `sanitizer`) and exits non-zero on any error. At fast tier `.orchestrator/bootstrap.lock` does not exist yet (Step 5 writes it), so archetype-scoped entries report `archetype-unknown` and are skipped — the always-on rules vendor regardless.
183
+
184
+ Surface `errors[]` and `sanitizer[]` to the operator. `sanitizer[]` (issue #1098) carries `{file, line, kind, text}` records for citations that read fine inside the plugin repo and dangle once vendored (`repo-local-path`, `unresolvable-see-also`); the CLI also prints each to stderr as `rules-sync: sanitizer <kind> <file>:<line> — <text>`. **Report it, do not act on it automatically** — it never rewrites content and never changes the exit code, so a human decides whether the citation is a leak.
185
+
186
+ Why: PSA-003 destructive-command safeguards require every consumer repo to carry the parallel-sessions rule. See issue #155. The `loop.md` vendor gives bare `/loop` a repo-aware maintenance prompt (issue #633 Hebel 3).
187
+
188
+ Why one writer (issue #1060): a literal `cp` from a second source directory bypasses the pre-write validator AND lands a file carrying no provenance header. On the next `--sync-rules` a headerless file is classified as a repo-private override and preserved forever, so the plugin can never update it again — and whichever rival copy is smaller silently wins.
182
189
 
183
190
  ## Step 4: Generate README.md
184
191
 
@@ -592,14 +592,18 @@ indent_size = 2
592
592
 
593
593
  Canonical implementation in [`_shared-template.md#parallel-sessions-rule`](_shared-template.md).
594
594
 
595
- Write the vendored rule from `$PLUGIN_ROOT/templates/_shared/rules/parallel-sessions.md` to
596
- `$REPO_ROOT/.claude/rules/parallel-sessions.md` (idempotent: missing→create, identical→skip,
597
- differs→overwrite). The shared partial also runs
595
+ Run `node "$PLUGIN_ROOT/scripts/lib/rules-sync.mjs" --repo-root "$REPO_ROOT"` — it vendors every
596
+ rule registered in `rules/_index.md` (idempotent: missing→create, identical→skip, differs→overwrite),
597
+ and it is the only writer that applies the pre-write validator, the basename-collision guard and the
598
+ copy-on-write branch that preserves repo-private rules. The shared partial also runs
598
599
  `cp "$PLUGIN_ROOT/templates/_shared/loop.md" "$REPO_ROOT/.claude/loop.md"` so bare `/loop` gets a
599
- repo-aware maintenance prompt. See shared partial for full shell command. Issues #155, #633.
600
+ repo-aware maintenance prompt. See shared partial for full shell command. Issues #155, #633, #1060.
600
601
 
601
- Note: Runs before S99. If S99 fetches a newer `parallel-sessions.md` from the baseline, the
602
- baseline version wins (acceptable S99 is canonical).
602
+ Note: Runs before S99. S99 no longer fetches `parallel-sessions.md` from the baseline — that entry
603
+ was removed from the S99 manifest in #1060. The baseline copy carries no provenance header, so
604
+ letting it win would make the next `--sync-rules` classify the target as a repo-private override and
605
+ preserve it permanently, i.e. the plugin could never update that rule again. `rules/` is the single
606
+ source for it.
603
607
 
604
608
  ## Step 3b: Initialize .orchestrator/metrics/ (#185)
605
609