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
@@ -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.
@@ -143,6 +143,20 @@ The Ownership Model above resolves *STATE.md* specifically, but the same discipl
143
143
 
144
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.
145
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 `readProcessLocalSessionIds()` + `classifyManifestSession()` (`scripts/lib/session-identity/own-session.mjs`) — process-local tiers only (hook payload, `CLAUDE_CODE_SESSION_ID`), never the repo-global `session.lock`, which is shared by every session in the checkout and made a peer's manifest read `own` (#1194):
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
+
146
160
  ## Guards
147
161
 
148
162
  ### Branch Validation
@@ -237,11 +251,24 @@ This replaces the v1 PID-liveness check (`process.kill(pid, 0)`) which was funda
237
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:
238
252
 
239
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`.
240
- - 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`).
241
- - `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.
242
256
 
243
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`.
244
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
+
245
272
  ### Schema v1 → v2 backward-compat
246
273
 
247
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
 
@@ -120,7 +120,7 @@ AskUserQuestion({
120
120
  })
121
121
  ```
122
122
 
123
- 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**.
124
124
 
125
125
  ## Upgrade Flow (`--upgrade <tier>`)
126
126
 
@@ -586,7 +586,7 @@ If invoked directly via `/bootstrap`: report the created files list and stop.
586
586
 
587
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.
588
588
  - **NEVER skip the lock file write** — `.orchestrator/bootstrap.lock` is the gate's mechanical truth. Bootstrap without a lock file is incomplete.
589
- - **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.
590
590
  - **ALWAYS commit** — bootstrap ends with a git commit. The lock file is part of that commit.
591
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`).
592
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
 
@@ -910,11 +910,18 @@ function main() {
910
910
  // kebab(s) = s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
911
911
  //
912
912
  // WARN (never error) when:
913
- // - learnings.jsonl is present AND no entry's derived key matches the rule's
914
- // `learning-key` (absent learning), OR
913
+ // - NEITHER a learnings.jsonl entry NOR a valid `evidence-digest` frontmatter
914
+ // scalar is present (the rule's provenance is unresolvable), OR
915
915
  // - the matching entry's `expires_at` < now (expired learning).
916
- // When learnings.jsonl is absent, every key counts as absent — warn on each
917
- // generated rule's key.
916
+ //
917
+ // #1101 the digest branch. `.orchestrator/metrics/learnings.jsonl` is
918
+ // GITIGNORED, so in a fresh clone it does not exist and every generated rule
919
+ // used to warn (measured 2026-08-26: 23 of 23). A rule carrying a valid
920
+ // `evidence-digest: sha256-v1:<64 hex>` is SELF-CONTAINED — its `## Evidence`
921
+ // block plus its own `## Provenance` header fields re-derive the seal with no
922
+ // jsonl lookup — so its provenance is resolvable offline and it must not warn.
923
+ // The digest is EVIDENCE, not a second pointer; see
924
+ // `scripts/lib/reconcile/renderer.mjs` § computeEvidenceDigest.
918
925
  // The check is silently skipped (no id pushed) when .claude/rules/ is absent
919
926
  // or contains no .md files with auto-generated: true.
920
927
  if (!args.skipGeneratedRuleStaleness) {
@@ -926,15 +933,17 @@ function main() {
926
933
  // Reads the opening --- ... --- block from a markdown file.
927
934
  function extractFrontmatterFields(mdContent) {
928
935
  const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(stripLeadingRuleHeaderLines(mdContent));
929
- if (!m) return { autoGenerated: false, learningKey: null, expiresAt: null };
936
+ if (!m) return { autoGenerated: false, learningKey: null, expiresAt: null, evidenceDigest: null };
930
937
  const block = m[1];
931
938
  const autoGenM = /^auto-generated:\s*(.+)$/m.exec(block);
932
939
  const learningKeyM = /^learning-key:\s*(.+)$/m.exec(block);
933
940
  const expiresAtM = /^expires-at:\s*(.+)$/m.exec(block);
941
+ const evidenceDigestM = /^evidence-digest:\s*(.+)$/m.exec(block);
934
942
  return {
935
943
  autoGenerated: autoGenM ? autoGenM[1].trim() === 'true' : false,
936
944
  learningKey: learningKeyM ? learningKeyM[1].trim() : null,
937
945
  expiresAt: expiresAtM ? expiresAtM[1].trim() : null,
946
+ evidenceDigest: evidenceDigestM ? evidenceDigestM[1].trim() : null,
938
947
  };
939
948
  }
940
949
 
@@ -955,12 +964,20 @@ function main() {
955
964
  relPath: relative(vaultDir, absPath),
956
965
  learningKey: fields.learningKey,
957
966
  expiresAt: fields.expiresAt,
967
+ evidenceDigest: fields.evidenceDigest,
958
968
  });
959
969
  }
960
970
 
961
971
  // No generated rules found → silently skip (don't push the check id).
962
972
  if (generatedRules.length === 0) return;
963
973
 
974
+ // #1101 — a LITERAL COPY of `EVIDENCE_DIGEST_RE` from
975
+ // `scripts/lib/reconcile/renderer.mjs`. Copied, not imported: this checker
976
+ // is a standalone skill script, and importing across the skill/script
977
+ // boundary would add real coupling for one 30-character literal. The two
978
+ // copies are pinned equal by `tests/lib/reconcile/renderer.test.mjs`.
979
+ const EVIDENCE_DIGEST_RE = /^sha256-v1:[0-9a-f]{64}$/;
980
+
964
981
  // Slugify function mirroring emitter.mjs `kebab()`.
965
982
  const kebab = (s) =>
966
983
  String(s)
@@ -1001,26 +1018,37 @@ function main() {
1001
1018
  const nowMs = Date.now();
1002
1019
 
1003
1020
  for (const rule of generatedRules) {
1004
- const { relPath, learningKey, expiresAt } = rule;
1021
+ const { relPath, learningKey, expiresAt, evidenceDigest } = rule;
1005
1022
 
1006
1023
  // If we cannot evaluate the key (no learning-key frontmatter), skip
1007
1024
  // silently — avoid false positives on malformed rules.
1008
1025
  if (!learningKey) continue;
1009
1026
 
1010
- if (!learningsPresent || !knownKeys.has(learningKey)) {
1011
- // Learning is absent from learnings.jsonl (or the file is missing entirely).
1027
+ const hasJsonlEntry = learningsPresent && knownKeys.has(learningKey);
1028
+ const hasValidDigest =
1029
+ typeof evidenceDigest === 'string' && EVIDENCE_DIGEST_RE.test(evidenceDigest);
1030
+
1031
+ if (!hasJsonlEntry && !hasValidDigest) {
1032
+ // NEITHER resolution path is available: the learning is absent from
1033
+ // learnings.jsonl (or the file is missing entirely) AND the rule carries
1034
+ // no self-contained evidence digest, so nothing can vouch for it.
1012
1035
  warnings.push({
1013
1036
  check: 'generated-rule-staleness',
1014
1037
  file: relPath,
1015
1038
  line: 1,
1016
- message: `Auto-generated rule references learning-key '${learningKey}' which is absent from .orchestrator/metrics/learnings.jsonl`,
1039
+ message: `Auto-generated rule references learning-key '${learningKey}' which is absent from .orchestrator/metrics/learnings.jsonl, and the rule carries no valid evidence-digest to verify it offline`,
1017
1040
  extracted: learningKey,
1018
1041
  });
1019
1042
  continue;
1020
1043
  }
1021
1044
 
1022
- // Learning exists — check expiry.
1023
- const storedExpiresAt = knownKeys.get(learningKey);
1045
+ // Provenance resolves — check expiry.
1046
+ // `storedExpiresAt` is only meaningful when the jsonl entry actually
1047
+ // exists. A digest-only rule (fresh clone, or a learning aged out of the
1048
+ // store) is AUTHORITATIVE FOR ITS OWN EXPIRY via frontmatter; reading a
1049
+ // stored value that is not there would make `expiryStr` null and silently
1050
+ // skip the expiry gate for a rule that has a perfectly good `expires-at`.
1051
+ const storedExpiresAt = hasJsonlEntry ? knownKeys.get(learningKey) : null;
1024
1052
  // Prefer the frontmatter expires-at on the rule file; fall back to the
1025
1053
  // stored expires_at from the learning entry (both should agree, but the
1026
1054
  // rule file is authoritative for its own expiry).
@@ -131,7 +131,7 @@ schema, so nobody double-checks the actual runtime behavior, and invalid data pa
131
131
  undetected.
132
132
 
133
133
  Reference case: `maxLength: 63` was the natural way to express S3 bucket-name limits. The
134
- downstream validator (`aiat-poc-infra/scripts/estate/validate-estate-yaml.sh`) is a hand-written
134
+ downstream validator (`aiat-poc-infra/scripts/estate/validate-estate-yaml.sh`) <!-- path-check: historical --> is a hand-written
135
135
  JSON-Schema mini-interpreter that does not implement `maxLength` — it would have parsed the
136
136
  schema, not recognized the keyword, and silently done nothing, so a 200-character bucket name
137
137
  would still validate green. Decision: extend the consumer (option 1) rather than relying only on
@@ -113,7 +113,7 @@ const res = claimRepo({ repoRoot: R, sessionId, mode, ttlHours, semanticSessionI
113
113
  Or reuse the primitive directly: `acquire({ sessionId, mode, ttlHours, repoRoot, semanticSessionId })` from `scripts/lib/session-lock.mjs`. The claim is a `linkSync` create-or-fail = **atomic**.
114
114
 
115
115
  - **`ok: true`** → the claim is held. Proceed to Phase 4.
116
- - **`ok: false`** (race lost / busy — reasons: `active`, `stale-pid-alive`, `stale-pid-dead`, `fs-error`, …) → **exclude R**, re-rank the remaining free candidates (drop R from `free`, re-run Phase 1's rank step), and re-present Phase 2. Loop until a claim succeeds or no free candidate remains (then Phase 5).
116
+ - **`ok: false`** (race lost / busy — reasons: `active`, `stale-heartbeat`, `fs-error`, …) → **exclude R**, re-rank the remaining free candidates (drop R from `free`, re-run Phase 1's rank step), and re-present Phase 2. Loop until a claim succeeds or no free candidate remains (then Phase 5).
117
117
 
118
118
  Do NOT reinvent the claim — always go through `claimRepo`/`acquire`. The `ok:false` path is the load-bearing concurrency guard: two parallel dispatchers can both recommend R, but only one wins the `linkSync`; the loser must re-rank, never force.
119
119
 
@@ -24,7 +24,10 @@ For harness < 2.1.105 (no monitor support), the skill's manual probes documented
24
24
 
25
25
  This skill reads from the project's `## Session Config` section in the platform instruction file:
26
26
 
27
- - **`health-endpoints`** — list of `{name, url}` objects for service health checks
27
+ - **`health-endpoints`** — list of `{name, url}` objects for service health checks. Parsed
28
+ content-scoped by `scripts/lib/config/health-endpoints.mjs` (#1174), which accepts the nested
29
+ block form this wizard writes (below), an inline object array, and a bare list of URLs — see
30
+ `docs/session-config-reference.md` § VCS & Infrastructure for the full form table.
28
31
  - **`cross-repos`** — list of related repositories for critical issue scanning
29
32
 
30
33
  Both fields are optional. The skill degrades gracefully when either is missing. On Codex this means `AGENTS.md`; on Claude/Cursor it means `CLAUDE.md`.
@@ -128,6 +128,11 @@ ecosystem-health:
128
128
  the block is NOT overwritten. The wizard prints "Skipped (already present)" and
129
129
  exits 0. Re-run to edit: remove the existing block first, then re-run.
130
130
 
131
+ This nested `health-endpoints:` block (an indented list under a valueless header) is now parsed
132
+ content-scoped by `scripts/lib/config/health-endpoints.mjs` (#1174) — before that fix the flat
133
+ key/value reader bailed to `null` the moment it saw the `{`/nested-list shape this wizard writes,
134
+ so the block above wrote successfully but the ecosystem-health skill silently never saw it.
135
+
131
136
  ### 4b — `.orchestrator/policy/ecosystem.json`
132
137
 
133
138
  ```json
@@ -34,6 +34,8 @@ Do NOT proceed past Phase 0 if GATE_CLOSED. There is no bypass. Refer to `skills
34
34
 
35
35
  ## Phase 1: Config & Data Loading
36
36
 
37
+ **Telemetry start marker (#1200):** note the current wall-clock time before Step 1.1 runs (e.g. `date +%s%3N`, or the coordinator's own turn-start instant). Every `orchestrator.evolve.completed` emit in Phase 1 / Phase 3 below reports `duration_ms` (placeholder `DURATION_MS`) as the elapsed milliseconds since this marker — same in-memory-value convention as `CT`/`AC`/`ASK`/`DROP` in `skills/session-end/SKILL.md`'s `orchestrator.handover.gated` emits.
38
+
37
39
  ### 1.1 Read Session Config
38
40
 
39
41
  Read and parse Session Config per `skills/_shared/config-reading.md`. Store result as `$CONFIG`.
@@ -44,6 +46,13 @@ Extract `persistence` from `$CONFIG`. If `persistence` is `false`, abort with me
44
46
 
45
47
  > "Learnings require persistence to be enabled in Session Config. Add `persistence: true` to your Session Config block (CLAUDE.md for Claude Code, AGENTS.md for Codex CLI)."
46
48
 
49
+ **Telemetry on abort (#1200):** before stopping, emit the abort form of the run-completion event:
50
+
51
+ ```bash
52
+ node scripts/emit-event.mjs --type orchestrator.evolve.completed --payload \
53
+ "$(node -e "process.stdout.write(JSON.stringify({aborted: 'persistence-disabled', reason: 'Learnings require persistence to be enabled in Session Config.'.slice(0,300), duration_ms: DURATION_MS}))")"
54
+ ```
55
+
47
56
  ### 1.3 Determine Mode
48
57
 
49
58
  Read mode from `$ARGUMENTS`:
@@ -91,7 +100,12 @@ Extract learnings from session history.
91
100
  - Read all entries from `.orchestrator/metrics/sessions.jsonl` (or `<state-dir>/metrics/sessions.jsonl` if the v2 path does not exist — see Phase 1.4 fallback)
92
101
  - Parse each JSONL line as JSON
93
102
  - Sort by `completed_at` descending (most recent first)
94
- - If no sessions found, abort: "No session data available. Complete at least one session before running evolve."
103
+ - If no sessions found, abort: "No session data available. Complete at least one session before running evolve." **Telemetry on abort (#1200):** before stopping, emit:
104
+
105
+ ```bash
106
+ node scripts/emit-event.mjs --type orchestrator.evolve.completed --payload \
107
+ "$(node -e "process.stdout.write(JSON.stringify({aborted: 'no-session-data', reason: 'No session data available. Complete at least one session before running evolve.'.slice(0,300), duration_ms: DURATION_MS}))")"
108
+ ```
95
109
 
96
110
  ### Step 3.1b: Read Extra Sources (#638)
97
111
 
@@ -380,6 +394,13 @@ For confirmed learnings, use atomic rewrite strategy:
380
394
 
381
395
  Report: "Saved N new learnings, updated M existing. Total active: K."
382
396
 
397
+ **Telemetry (#1200):** emit the run-completion event as the last action of this step, using the counts already computed above — `N` (Step 3.5(4) new-learnings count) → `appended`, `M` (Step 3.5(2) reinforced-existing count) → `boosted`, `$PRUNE.archived` (the sweep CLI's returned total, Step 3.5(5)) → `pruned`. `promoted` is always `0` from THIS call site: promotion to `public` scope is the separate `npm run share:hw-learnings -- --promote` CLI, never invoked by `/evolve analyze` itself — see `docs/events-schema.md`. All four counters are ALWAYS present, including as `0`:
398
+
399
+ ```bash
400
+ node scripts/emit-event.mjs --type orchestrator.evolve.completed --payload \
401
+ "$(node -e "process.stdout.write(JSON.stringify({appended: N, boosted: M, pruned: PRUNED, promoted: 0, duration_ms: DURATION_MS}))")"
402
+ ```
403
+
383
404
  ### Step 3.6: C2 Auto-Repair Feeder (opt-in — #647)
384
405
 
385
406
  > **Default OFF (advisory-only).** With no `skill-evolution:` block in Session Config, this step surfaces repair candidates as ADVICE only — it applies nothing and opens no MR. This mirrors the opt-in precedent of `slopcheck` (#520) and `verification-auto-fix` (#521): the engine is dark unless explicitly enabled.
@@ -542,6 +563,8 @@ N active learnings (M high confidence, K expiring soon)
542
563
 
543
564
  Single-pass LLM derivation of USER.md + AGENT.md (peer cards from #503) updates from current learnings + sessions + steering files. Dry-run-default per #506 EARS contract.
544
565
 
566
+ **Telemetry start marker (#1200):** note the current wall-clock time at Phase 6 entry (`DURATION_MS` in the Step 6.4/6.5 emits below is the elapsed milliseconds since this marker) — same placeholder convention as `skills/session-end/SKILL.md`'s `orchestrator.handover.gated` emits.
567
+
545
568
  ### Step 6.0: Argument Parsing
546
569
 
547
570
  Parse `$ARGUMENTS` for trailing flags after the `dialectic` keyword:
@@ -603,6 +626,13 @@ const result = await runDialecticDeriver({
603
626
  - If `--apply`: call `mergePeerCard(existingBody, managedUpdates)` from `scripts/lib/peer-cards/merger.mjs` for each card target, then `writePeerCard(repoRoot, 'user', mergedUserCard)` and `writePeerCard(repoRoot, 'agent', mergedAgentCard)` from `scripts/lib/peer-cards/writer.mjs`. Update the `updated:` frontmatter.
604
627
  - Report: `Dialectic-derived: M deltas to USER.md, N deltas to AGENT.md. Dry-run | Applied. Tokens: in=<X> out=<Y>.`
605
628
 
629
+ **Telemetry (#1200):** immediately after the report line above, emit the success form (`mode` mirrors which branch ran):
630
+
631
+ ```bash
632
+ node scripts/emit-event.mjs --type orchestrator.dialectic.completed --payload \
633
+ "$(node -e "process.stdout.write(JSON.stringify({mode: 'MODE', user_deltas: M, agent_deltas: N, tokens_in: X, tokens_out: Y, duration_ms: DURATION_MS}))")"
634
+ ```
635
+
606
636
  ### Step 6.5: Error Handling
607
637
  - `status: 'unknown-model'` → fail with clear error (already thrown by validateModel)
608
638
  - `status: 'budget-exceeded'` → emit `{status:'budget-exceeded', used:N, budget:M}`, do NOT truncate
@@ -610,6 +640,13 @@ const result = await runDialecticDeriver({
610
640
  - `status: 'empty-input'` → exit clean with message "dialectic: skipped (no input)"
611
641
  - subagent crash → log ⚠, exit cleanly (do NOT write to `.orchestrator/dialectic-pending.md`)
612
642
 
643
+ **Telemetry (#1200):** for EACH outcome above, before exiting, emit `orchestrator.dialectic.completed` in its abort form — `SLUG` is `unknown-model` \| `budget-exceeded` \| `would-empty-card` \| `empty-input` \| `subagent-crash` respectively (the subagent-crash case has no `runDialecticDeriver` status of its own; use the literal slug `subagent-crash`):
644
+
645
+ ```bash
646
+ node scripts/emit-event.mjs --type orchestrator.dialectic.completed --payload \
647
+ "$(node -e "process.stdout.write(JSON.stringify({aborted: 'SLUG', duration_ms: DURATION_MS}))")"
648
+ ```
649
+
613
650
  Cross-reference: PRD #506 AC1-AC4 + EARS gates. Vault Integration: dialectic does NOT mirror to vault (#506 scope — peer cards are repo-local by design; vault mirror is for cross-repo sessions/learnings).
614
651
 
615
652
  ---