session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
package/docs/install.md CHANGED
@@ -75,8 +75,20 @@ Setup guides: [Codex](codex-setup.md) · [Cursor IDE](cursor-setup.md) ·
75
75
  /plugin update session-orchestrator@kanevry # Claude Code
76
76
  ```
77
77
 
78
- Restart the harness afterwards, and re-run `npm install` in the plugin directory
79
- when the release adds dependencies. On Cursor and the Pi clone fallback, upgrade
78
+ Restart the harness afterwards. Then check that the plugin directory (the
79
+ `installPath` from `claude plugin list --json`) has a `node_modules/`: since
80
+ Claude Code 2.1.287 the harness refuses to install dependencies for a package
81
+ whose `package.json` sets `overrides` — releases up to and including 5.4.0 do —
82
+ and reports it only as `noteDetails: [{"type": "dependencies-refused"}]` in that
83
+ JSON (measured 2026-10-02). Later releases dropped the `overrides` block. If
84
+ `node_modules/` is missing, run `npm ci --omit=dev` in that directory.
85
+
86
+ **From a clone, one command does all of it:** `npm run update:local`
87
+ (`scripts/self-update.mjs`) fast-forwards the clone, updates the Claude Code
88
+ marketplace and plugin, installs missing runtime dependencies in the plugin
89
+ directory, and re-runs the Codex installer if the plugin is installed in Codex.
90
+ `--dry-run` shows the commands first; old cache folders are never deleted,
91
+ because the running session still uses them until the restart. On Cursor and the Pi clone fallback, upgrade
80
92
  with `git pull` in your clone followed by the same install script you originally
81
93
  ran. Manage npm-installed Pi packages through Pi's package manager. For Codex,
82
94
  follow the [refresh instructions](codex-setup.md#refresh-and-explicit-cache-invalidation)
@@ -146,15 +146,18 @@ node scripts/cursor-install.mjs /path/to/your-project # links NEW commands/ru
146
146
  # Restart Cursor
147
147
  ```
148
148
 
149
- **The installer adds new files; it never overwrites or removes an existing one.** `linkPath()`
150
- skips whenever the destination already exists as a symlink or a file
151
- (`scripts/cursor-install.mjs:69-73`), and the `hooks.json` writer skips outright when one is
152
- already there (`:139-140`). Two consequences on this upgrade: the malformed `argument-hint` fix
153
- (the 3.x generator wrote it into 24 of 28 command files, GH#54) reaches you for free through
154
- your existing symlinks the moment `git pull` updates this checkout — no re-run needed for that.
155
- But the three retired commands do **not** disappear from `.cursor/commands/` on their own, and a
156
- `hooks.json` written before 4.0.0 is never synchronised with a new hook event automatically.
157
- Both need the manual step in § 5 ("Cursor still shows the removed commands") below.
149
+ **The installer adds new files; it never overwrites an existing one.** `linkPath()` in
150
+ `scripts/cursor-install.mjs` skips whenever the destination already exists as a symlink or a
151
+ file, and the `hooks.json` writer below it skips outright when one is already there. The only
152
+ thing it removes is its own dead link: `pruneDeadLinks()` deletes a symlink that dangles into
153
+ this checkout's `.cursor/` (printed as `PRUNE:`) and leaves a regular file, a live link and a
154
+ link into another checkout alone (#1501). Two consequences on this upgrade: the malformed
155
+ `argument-hint` fix (the 3.x generator wrote it into 24 of 28 command files, GH#54) reaches you
156
+ for free through your existing symlinks the moment `git pull` updates this checkout — no re-run
157
+ needed for that. But the three retired commands do **not** disappear from `.cursor/commands/`
158
+ until you re-run the installer from this checkout, and a `hooks.json` written before 4.0.0 is
159
+ never synchronised with a new hook event automatically. § 5 ("Cursor still shows the removed
160
+ commands") below covers both.
158
161
 
159
162
  Cursor's plugin metadata now lives at `.cursor-plugin/plugin.json`, replacing the initial
160
163
  4.0.0 standard root `plugin.json`. The native manifest retains the canonical skills and
@@ -261,9 +264,11 @@ inside Claude Code with `!node --version`. This is unchanged from v3.
261
264
 
262
265
  ### Cursor still shows the removed commands
263
266
 
264
- Re-running the installer does **not** fix this: `linkPath()` skips any destination that already
265
- exists (`scripts/cursor-install.mjs:69-73`), including a stale symlink pointing at a command
266
- that no longer exists in this repo. Remove only the three dead **symlinks** from your project —
267
+ Re-running the installer from the checkout that created the links fixes this: `pruneDeadLinks()`
268
+ in `scripts/cursor-install.mjs` removes a symlink that dangles into that checkout's
269
+ `.cursor/commands/` (#1501). A link into another checkout is left alone and reported as `WARN:`;
270
+ `linkPath()` never replaces an existing destination. For those, remove only the three dead
271
+ **symlinks** from your project —
267
272
  never an unconditional `rm -f`, which would just as happily delete a regular file, including a
268
273
  command you wrote yourself under one of these three names (the installer itself never overwrites
269
274
  or deletes a regular file; this recipe must not either):
@@ -281,9 +286,10 @@ names, this recipe deletes that symlink too; a regular (non-symlink) file is the
281
286
  leaves untouched.
282
287
 
283
288
  Then restart Cursor. Same story for `.cursor/hooks.json`: the installer skips it outright once
284
- it exists (`:139-140`), so a `hooks.json` written before 4.0.0 is never re-synced with a new hook
285
- event automatically — re-check it by hand (diff it against a fresh `node scripts/cursor-install.mjs`
286
- run in an empty scratch directory if you suspect drift).
289
+ it exists (the `hooks.json` writer in `scripts/cursor-install.mjs`), so a `hooks.json` written
290
+ before 4.0.0 is never re-synced with a new hook event automatically — re-check it by hand (diff
291
+ it against a fresh `node scripts/cursor-install.mjs` run in an empty scratch directory if you
292
+ suspect drift).
287
293
 
288
294
  ## 6. Rollback
289
295
 
@@ -318,7 +324,8 @@ node scripts/pi-install.mjs /path/to/your-project --settings-only # Pi — sam
318
324
  ```
319
325
 
320
326
  Run only the line for your platform. `cursor-install.mjs` and `pi-install.mjs` both default their
321
- target to `process.cwd()` when no argument is given (`scripts/cursor-install.mjs:24-51`) — omit
327
+ target to `process.cwd()` when no argument is given (the `TARGET` constant in
328
+ `scripts/cursor-install.mjs`) — omit
322
329
  the project path here and the installer links into `/path/to/session-orchestrator` itself, not
323
330
  into your project.
324
331
 
@@ -339,7 +346,7 @@ What you get back, and what you do not:
339
346
  - **What does NOT roll back automatically** is anything an installer wrote into YOUR project:
340
347
  `.cursor/commands/`, `.cursor/hooks.json`, Pi settings. The two installers behave
341
348
  differently: `cursor-install.mjs` skips any destination that already exists — a symlink or a
342
- file (`scripts/cursor-install.mjs:69-73`; the `hooks.json` writer at `:139-140`) — and
349
+ file (`linkPath()` in `scripts/cursor-install.mjs`, and the `hooks.json` writer below it) — and
343
350
  re-creates only the ones that are MISSING, so re-running it from the 3.24.0 checkout only
344
351
  **adds** command links missing from your project; it does not restore a link you removed
345
352
  yourself and does not resync an existing `hooks.json`.
@@ -110,8 +110,8 @@ Sidecar: .orchestrator/persona-panel/2026-05-20T14-30-00Z-a1b2c3d4.json
110
110
  "run_id": "a1b2c3d4",
111
111
  "target": "/abs/path/docs/research/wfk-2.1.5.md",
112
112
  "personas_invoked": [
113
- { "name": "klima-physicist", "version": "2", "model": "claude-opus-4-7", ... },
114
- { "name": "klima-ai-expert", "version": "1", "model": "claude-opus-4-7", ... }
113
+ { "name": "klima-physicist", "version": "2", "model": "opus", ... },
114
+ { "name": "klima-ai-expert", "version": "1", "model": "opus", ... }
115
115
  ],
116
116
  "consolidation": {
117
117
  "mode": "voting-quorum",
@@ -239,7 +239,7 @@ name: my-reviewer
239
239
  schema_version: 1
240
240
  version: "1"
241
241
  role: "Domain expert in [field] — evaluates [aspect] of the target"
242
- model: claude-opus-4-7
242
+ model: opus
243
243
  tier: domain-expert
244
244
  output_contract:
245
245
  type: object
@@ -293,8 +293,9 @@ None.
293
293
  | `compliance` | DSGVO, SEC, HIPAA, ISO reviewers |
294
294
  | `custom` | Any other structured reviewer |
295
295
 
296
- `domain-expert` and `compliance` tiers default to `claude-opus-4-7` for model selection
296
+ `domain-expert` and `compliance` tiers default to the Opus tier via alias `opus` for model selection
297
297
  (Opus finds real issues Sonnet misses — see vault learning `[[persona-opus-finds-real-failing-cibadge]]`).
298
+ The alias tracks the current Opus; pin a full model ID only for a reproducibility need, and name the reason.
298
299
 
299
300
  ### output_contract security rules
300
301
 
@@ -120,7 +120,7 @@ Under `scripts/lib/`. Each module is a focused concern and exports only what cal
120
120
  | **`config-schema.mjs`** | Plain-JS validator; validates the 7 mandatory Session Config fields (3 required strings + 4 typed fields) | `validateSessionConfig` (internal `REQUIRED_STRING_FIELDS` constant covers the 3 required strings — not exported) |
121
121
  | **`events.mjs`** | Append to `.orchestrator/metrics/events.jsonl` + optional webhook POST | `emitEvent`, `appendEvent` |
122
122
  | **`worktree.mjs`** | zx-based git worktree helpers with cross-platform paths | `createWorktree`, `removeWorktree`, `listWorktrees`, `cleanupAllWorktrees` |
123
- | **`hardening.mjs`** | Scope + command enforcement primitives | `findScopeFile`, `getEnforcementLevel`, `pathMatchesPattern`, `commandMatchesBlocked` |
123
+ | **`hardening.mjs`** | Scope + command enforcement primitives | `findScopeFile` (deprecated: ignores manifest ownership; hooks use `findOwnScopeFile` from `scope-gate.mjs`), `getEnforcementLevel`, `pathMatchesPattern`, `commandMatchesBlocked` |
124
124
  | **`common.mjs`** | Grab-bag utilities | `makeTmpPath`, `utcTimestamp`, `readJson`, `writeJson`, `appendJsonl` |
125
125
  | **`state-md.mjs`** | Hand-rolled YAML-subset STATE.md parser (never throws) | `parseStateMd`, `serializeStateMd`, `touchUpdatedField` |
126
126
  | **`host-identity.mjs`** | Device fingerprint + SSH detection (v3.1 resource-awareness) | `getHostIdentity`, `isSshSession` |
@@ -23,6 +23,7 @@ All keys are optional. Unknown keys are **ignored without error** — adding a n
23
23
  | Field | Type | Required | Meaning | Example |
24
24
  |-------|------|----------|---------|---------|
25
25
  | `globs` | `string[]` (block or flow style) | no | Glob patterns relative to repo root. Rule loads only when a `scopePath` matches at least one. Absent = always-on; `[]` = matches nothing (disabled). | `globs:` then ` - src/**/*.tsx` |
26
+ | `paths` | `string[]` (block or flow style) | no — **required alongside `globs` with the identical list in a path-scoped VENDORED rule** (`rules/` library, #1449) | Alias for `globs` in `rule-loader.mjs` (#795; `globs` wins when both are present). It is the ONLY key Claude Code's native loader reads, and only from a frontmatter that opens on line 1 — a rule without it loads always-on there. A vendored rule therefore carries both keys: `paths` for Claude Code, `globs` for `rule-loader.mjs`, which reads it first. A repo's own consolidated `.claude/rules/` files are `paths`-only (see [Consolidated rules](#consolidated-rules-n-provenance-pairs-in-one-file-the-merge-contract) point 3). | `paths:` then ` - src/**/*.tsx` |
26
27
  | `description` | `string` | no | Human-readable summary of what the rule covers. Surfaced on the rule entry; used by FA2/FA4 tooling and authors. | `description: Tailwind + a11y conventions` |
27
28
  | `mode` | `string` (`housekeeping` \| `feature` \| `deep`) | no | Session-mode gate. Rule loads only in the named session mode. Absent = passes every mode. | `mode: deep` |
28
29
  | `host-class` | `string` | no | Host-class gate. Matched against `host_class` in `.orchestrator/host.json`. Rule loads only on matching hosts. Absent = passes every host. | `host-class: macos-arm64-m4pro` |
@@ -93,29 +94,42 @@ Since issue #743, `rules/opt-in-stack/{backend,backend-data,frontend,swift,secur
93
94
 
94
95
  ### Provenance header + frontmatter coexistence (issue #722)
95
96
 
96
- Vendored rule sources carry a mandatory single-line provenance header **before** any frontmatter block — `rules-sync.mjs` uses that header (`PLUGIN_HEADER_PREFIX = '<!-- source: session-orchestrator plugin ...'`) to tell "plugin-owned, safe to overwrite on re-sync" apart from "local override, preserve". The recommended shape for a vendored rule with `globs:` frontmatter:
97
+ Vendored rule sources carry a mandatory single-line provenance header — `rules-sync.mjs` uses that header (`PLUGIN_HEADER_PREFIX = '<!-- source: session-orchestrator plugin ...'`) to tell "plugin-owned, safe to overwrite on re-sync" apart from "local override, preserve". Where it goes depends on whether the rule has frontmatter (#1449):
98
+
99
+ - **Rule with frontmatter** — the frontmatter opens on **line 1** (the file's first bytes are `---`), and the header is the first line directly after the closing `---`. Claude Code reads a rule's frontmatter only when it opens on line 1, so a header above the opener makes the whole block body text and the rule loads always-on in every consumer.
100
+ - **Rule without frontmatter** — the header stays on line 1.
101
+
102
+ The shape for a path-scoped vendored rule:
97
103
 
98
104
  ```markdown
99
- <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/foo.md) -->
100
105
  ---
101
106
  globs:
102
107
  - src/**/*.tsx
108
+ paths:
109
+ - src/**/*.tsx
110
+ tier: wave-only
103
111
  ---
112
+ <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/foo.md) -->
104
113
  # Foo Rules (Path-scoped)
105
114
  ```
106
115
 
107
- `rule-loader.mjs`'s frontmatter parser (`parseGlobsFrontmatter`) tolerates a leading run of blank lines and/or single-line HTML comments before the opening `---`, so a vendored rule's provenance header does not defeat its `globs:` scoping — the header line is skipped, then frontmatter parses exactly as it would without the header. This tolerance is header-agnostic (it accepts any single-line HTML comment, not only the plugin's own), so a hand-authored rule that happens to start with a one-line comment is unaffected.
116
+ A path-scoped vendored rule declares its patterns under **both** `globs:` and `paths:`, with identical lists. `paths:` is the only key Claude Code reads from a rule; `globs:` is what `rule-loader.mjs` reads first (#742/#795 — when both are present `globs:` wins silently), so one key alone, or two different lists, would scope the rule differently per loader.
117
+
118
+ `rule-loader.mjs`'s frontmatter parser (`parseGlobsFrontmatter`) counts a frontmatter block only when its opening `---` is line 1 — the same rule Claude Code applies. Until #1449 it skipped a leading run of blank lines and single-line HTML comments, which classified a header-first rule as path-scoped while Claude Code loaded it always-on. Now a block with anything before its opener (a comment, a blank line, any text) yields no globs and no meta: the rule is always-on, and its entry carries `defect: 'frontmatter-not-at-top'` so callers such as the instruction-budget guard can report the file instead of loading it always-on without a trace. The header after the closing `---` is ordinary body text and has no effect on loading.
119
+
120
+ Consumer copies written before #1449 (header on line 1, frontmatter after it) still count as plugin-owned: `isPluginOwnedContent()` (`validate-vendored-rules.mjs`, re-exported by `rules-sync.mjs`) accepts the header at either position, so the next `/bootstrap --sync-rules` rewrites such a copy into the current format rather than preserving it as a local override.
108
121
 
109
122
  This convention binds only files that are actual sync SOURCES — the entries `syncRules()` resolves from `rules/_index.md` (`join(pluginRoot, 'rules', '_index.md')`, the manifest it reads before writing anything into a consumer's `.claude/rules/`). `rules/README.md` and `rules/_index.md` itself are never entries in that manifest, so they are never sync targets and carry no provenance header by construction — not an oversight to fix.
110
123
 
111
124
  ### Vendoring validation (issue #722)
112
125
 
113
- Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/`, it runs a pre-write gate via `validateRuleContent()` (`scripts/lib/validate-vendored-rules.mjs`). Five probes:
126
+ Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/`, it runs a pre-write gate via `validateRuleContent()` (`scripts/lib/validate-vendored-rules.mjs`). Six probes:
114
127
 
115
128
  | Probe | Severity | Rejects / flags |
116
129
  |-------|----------|------------------|
117
- | `paths-frontmatter` | error | A top-level `paths:` frontmatter key **in a `rules/` library source**. Since #795 `rule-loader.mjs` accepts `paths:` as an alias for `globs:`, so such a rule IS glob-scoped — this is a vendoring-CONVENTION gate (`globs:` is the canonical form for vendored rules, #742), not a loader-compatibility gate. Its population is what `syncRules()` reads, i.e. `<pluginRoot>/rules/**` as listed by `rules/_index.md`; the consolidated files under `.claude/rules/` are `paths:`-canonical (see § Consolidated rules point 3) and are never its input. |
118
- | `provenance-header` | error (opt-in via `requireProvenance`, default `true` in `syncRules()`) | Missing provenance header on a library source — without it, `rules-sync.mjs` mis-detects the file as a local override on the next re-sync and can never update it again. |
130
+ | `frontmatter-not-at-top` | error | A `---` frontmatter block preceded by anything — the provenance comment, a blank line, any text (#1449). Claude Code reads frontmatter only when it opens on line 1, so such a rule loads ALWAYS-ON in every consumer repo, whatever its scope keys say. Fix: move the block to line 1 and put the provenance header on the first line after the closing `---`. |
131
+ | `globs-paths-mismatch` | error | A library source whose line-1 frontmatter carries exactly one of `globs:` / `paths:`, or both with different lists (compared order-insensitively) (#1449). A path-scoped vendored rule carries both with the identical list: `paths:` is the only key Claude Code reads, `globs:` is what `rule-loader.mjs` reads first (#742/#795). Replaces the pre-#1449 `paths-frontmatter` probe, which rejected `paths:` in a library source outright. Its population is what `syncRules()` reads, i.e. `<pluginRoot>/rules/**` as listed by `rules/_index.md`; the consolidated files under `.claude/rules/` are `paths:`-only by design (see § Consolidated rules point 3) and are never its input. |
132
+ | `provenance-header` | error (opt-in via `requireProvenance`, default `true` in `syncRules()`) | Missing provenance header on a library source — without it, `rules-sync.mjs` mis-detects the file as a local override on the next re-sync and can never update it again. Two positions are accepted: line 1 (a rule without frontmatter) or the first non-blank line after the closing `---` of a frontmatter that opens on line 1. A header on line 1 ABOVE a frontmatter (the pre-#1449 format) still satisfies this probe — `frontmatter-not-at-top` reports that shape, once — and keeps an old consumer copy plugin-owned, so the next sync upgrades it to the current format instead of freezing it as a local override. |
119
133
  | `placeholder` | error | Unfilled placeholder tokens: `{{PROJECT_NAME}}`-style handlebars, a `## TODO: Customize` heading, or a `<!-- TODO:` comment — skeleton content, not a finished rule. |
120
134
  | `zero-match-globs` | warn | A `globs:` pattern matching 0 files in the target repo's tracked file list (`git ls-files`, falling back to a directory walk). Legitimately possible in a freshly-scaffolded repo. |
121
135
  | `foreign-glob` | warn | A glob segment carrying a PascalCase, product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope. |
@@ -342,10 +356,11 @@ them silently loses a learning or regenerates it:
342
356
  10 path-scoped rule files, ALL 10 `paths:`-only — 0 carry `globs:`, 0 carry
343
357
  both (`grep -rn '^globs:' .claude/rules/` → no match, exit 1). Until this
344
358
  session `cli-design.md` carried both; the duplicate `globs:` was removed
345
- here. `globs:` is canonical only for rules VENDORED OUT through the `rules/`
346
- fleet library, where `validate-vendored-rules.mjs`'s `paths-frontmatter`
347
- probe enforces it (issue #742); that probe judges `rules/` sources only and
348
- never sees a consolidated file under `.claude/rules/`. Carrying both keys is
359
+ here. Only rules VENDORED OUT through the `rules/` fleet library carry
360
+ both keys, with identical lists — `paths:` for Claude Code, `globs:` for
361
+ `rule-loader.mjs` (issues #742, #1449) — and `validate-vendored-rules.mjs`'s
362
+ `globs-paths-mismatch` probe enforces it; that probe judges `rules/`
363
+ sources only and never sees a consolidated file under `.claude/rules/`. Carrying both keys is
349
364
  allowed, but NEVER with different values: `globs:` wins SILENTLY when both
350
365
  are present (#795), and `check-rules.mjs` fails a divergent pair outright.
351
366
 
@@ -468,7 +483,7 @@ Core security principles that apply to ALL code.
468
483
 
469
484
  ### (b) Glob-scoped rule (block-style `globs:`)
470
485
 
471
- Loads only on waves whose `allowedPaths` intersect the patterns. Mirrors the real `frontend.md`:
486
+ Loads only on waves whose `allowedPaths` intersect the patterns. Mirrors the real `frontend.md` (the vendored `rules/opt-in-stack/frontend.md` additionally carries `paths:` with the same list, `tier: wave-only`, and its provenance header after the closing `---` — see [Vendored Rules](#vendored-rules-issue-722-epic-a)):
472
487
 
473
488
  ```markdown
474
489
  ---
@@ -558,4 +573,4 @@ timeout masks real perf regressions.
558
573
  - [`scripts/lib/reconcile/eligibility.mjs`](../scripts/lib/reconcile/eligibility.mjs) — `CONVERT_TYPES` (derived from `LEARNING_TYPE_REGISTRY`), rule-conversion eligibility gates
559
574
  - [`scripts/lib/memory-proposals/schema.mjs`](../scripts/lib/memory-proposals/schema.mjs) — `PROPOSAL_TYPES` (derived from `LEARNING_TYPE_REGISTRY`)
560
575
  - [`docs/session-config-reference.md`](session-config-reference.md#reconcile-693--696--697) § Reconcile — `reconcile.rule-expiry-days` / `min-rule-days` / `min-insight-chars` config keys that tune the emitted `expires-at` and eligibility gates
561
- - Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard), #880 (FA5 — handwritten-rule symmetric check, warn mode)
576
+ - Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard), #880 (FA5 — handwritten-rule symmetric check, warn mode), #1449 (frontmatter on line 1, `globs:` + `paths:` pair and provenance-header position for vendored rules)
@@ -21,9 +21,9 @@ Two things follow, and both are the point of #1020:
21
21
  | 2 | Assert disjointness | the materialized sidecar array `[{id, files}, …]` | `validate-wave-scope.mjs --assert-disjoint` → `findScopeCollisions()` |
22
22
  | 3 | Compute the union | stdout of `--union` → `allowedPaths` | `expandTestSiblings(unionFileScopes(scopes), { role })` |
23
23
  | 4 | Inject | `FILE-SCOPE — exactly these:` + a fenced block in each agent prompt | the per-agent file from step 1 |
24
- | 5 | Dispatch | `.orchestrator/wave-dispatch-scopes.json` (ledger) | `hooks/pre-task-scope-disjoint.mjs`, `PreToolUse` matcher `Agent` |
24
+ | 5 | Dispatch | `.orchestrator/wave-dispatch-scopes.<session>.json` (ledger, one per session) | `hooks/pre-task-scope-disjoint.mjs`, `PreToolUse` matcher `Agent` |
25
25
 
26
- `<state-dir>` is the first of `.pi` / `.cursor` / `.codex` / `.claude` that carries a `wave-scope.json` — the same precedence `findScopeFile()` and the hook's `waveKeyOf()` use.
26
+ `<state-dir>` is the first of `.pi` / `.cursor` / `.codex` / `.claude` that carries a `wave-scope.json` not provably owned by a peer session — `findOwnScopeFile()` resolves it using process-local session ids, including for the hook's `waveKeyOf()`. Unbound or corrupt manifests still decide under the ENFORCE contract; when only peer manifests exist, the wave key is `<session>|w?|?`.
27
27
 
28
28
  Step 1 also offers an opt-in path diagnostic: run `materialize-wave-scope.mjs` from the project root with `--warn-missing` to name absent concrete paths and their agents on stderr (#1235). Relative paths resolve from the working directory, not `<state-dir>`. A warning does not fail the command or change its stdout or either declaration shape. For a file the wave deliberately creates, repeat `--new-file PATH` as needed; every exception must exactly match a declared path and pass scope validation before any writes begin. Grants containing `*` or ending in `/` are skipped under the existing glob/prefix grammar; `?` and braces are literal. The canonical invocation and handling steps are in `skills/wave-executor/references/wave-loop-scope-manifest.md` § 3.1.
29
29
 
@@ -35,7 +35,7 @@ The same ordering argument applies one level up: step 2 runs on the **declared**
35
35
 
36
36
  ### 2.2 Why the scope files are not temp files
37
37
 
38
- Steps 1, 2, 3, 4 and the `--assert-subset` assertion all read the *same* file, addressed by wave and agent id. A `$TMPDIR` copy is the one failure in this chain that **costs no error**: the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt()` returns `[]`, and the hook allows the dispatch exactly as it did before #1020 — signal-free (matrix rows 5/6 below). The scope files are control state like `wave-scope.json` itself, never a wave territory; writing them legitimately trips `bash-write-verify` once per wave rollover, and widening `allowedPaths` to silence that would grant agents write access to the deconfliction record.
38
+ Steps 1, 2, 3, 4 and the `--assert-subset` assertion all read the *same* file, addressed by wave and agent id. A `$TMPDIR` copy is the one failure in this chain that **costs no error**: the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt()` returns `[]`, and the hook allows the dispatch exactly as it did before #1020 — signal-free (matrix rows 5/6 below). The scope files are control state like `wave-scope.json` itself, never a wave territory, so they never enter `allowedPaths` — that would grant agents write access to the deconfliction record. Until #1027 Pkt 5 writing them tripped `bash-write-verify` once per wave rollover; its `computeReport` now drops `<state-dir>/filescopes/**` from the violation set beside the manifest itself (`isScopeDeclarationPath()` in `scope-gate.mjs`, keyed on the ACTIVE manifest's state dir, so another harness dir's `filescopes/` still reports). The cost, named: a Bash rewrite of a scope file after dispatch is no longer reported there. For the per-agent files `filescopes/wave-<N>/<agent-id>.json` what it can change is bounded — the dispatch already read the file, `wave-scope.json` (and with it `allowedPaths`) keeps its content-hash control notice, and `scope-echo --verify` turns a rewritten file into `digest-unknown`. The aggregate sidecar `filescopes/wave-<N>.scopes.json` is NOT bounded that way: `post-bash-write-verify` re-reads it on every call (`readPeerScopeRecords()`), and a `peer-session-*` record added there downgrades the matching violations to a "peer write" notice — the control hash covers only `wave-scope.json`, and `scope-echo` digests only the `wave-<N>/` directory. Open follow-up: hash the sidecar into the control state (or report its rewrite) so a peer record added after dispatch is visible.
39
39
 
40
40
  The coordinator's **own** planned direct edits belong in `coordinator.json` in the identical form. They are not dispatches, so the hook can never see them (§ 6); the CLI check is the only gate that covers them.
41
41
 
@@ -54,6 +54,8 @@ The reader is `hooks/enforce-scope.mjs` **Gate 3b**, between the manifest parse
54
54
  | ids present, none matching, own identity resolvable | `foreign` | **ALLOW** + one `orchestrator.scope.foreign_session_ignored` event |
55
55
  | ids present, own identity unresolvable (empty id set) | `unknown` | ENFORCE |
56
56
 
57
+ **Which manifest Gate 3 reads (#1504 point 6).** Up to four candidates can exist (`.pi` > `.cursor` > `.codex` > `.claude`). Gate 3 takes the first one whose verdict is NOT `foreign` (`findOwnScopeFile()`, `scripts/lib/scope-gate.mjs`; identity checked per candidate inside the loop) — `unknown` included, so an unbound, legacy or corrupt manifest still decides exactly as before. Only when EVERY candidate is foreign does the first of them reach Gate 3b and its ALLOW + event above. Before, the first EXISTING file decided: a peer's `.pi`/`.cursor` manifest made Gate 3b stand down and this session's own `.claude/wave-scope.json` was never read. `enforce-commands`, `post-bash-write-verify` and `wave-scope-commit-guard` resolve the same way.
58
+
57
59
  Five properties are choices, not omissions — and every one of them points the fail-**closed** way, the deliberate inverse of § 4.1's posture for the dispatch hook:
58
60
 
59
61
  - **Only what is PROVABLY foreign is foreign.** `readProcessLocalSessionIds()` returns the ids that are PROCESS-LOCAL — hook input (`session_id`/`sessionId`/`parent_session_id`) and `CLAUDE_CODE_SESSION_ID` — and an EMPTY set when neither yields an id, which can only produce `unknown`. The repo-global `session.lock` is deliberately NOT a tier here (#1194): it is ONE file shared by every session in the checkout, so unioning it let a peer's manifest match a peer-written lock id, classify `own`, and have Gate 7 deny the second session's legitimate writes — the exact lockout G3b exists to end. A better signal REPLACES a worse one (`.claude/rules/host-resources.md` § HR-102). A gate that guessed would turn "cannot tell" into a silent enforcement-off on every harness exporting no session id. Every value is trimmed on the way in, so a whitespace-only env var cannot enter as a phantom id that matches nothing (`.claude/rules/development.md` § env-var whitespace trap).
@@ -107,10 +109,10 @@ Both globs match `scripts/lib/x.mjs`, yet the direct comparison says `false`. Fo
107
109
 
108
110
  It cannot compare a batch of siblings directly — the header records why, measured against 12 archived transcripts of this repo (147 dispatch `tool_use` blocks, 51 batches, measured 2026-08-14): the dispatch tool is named `Agent` and not `Task`; the payload carries **no** structured file scope (`files` / `file_scope` / `scope` → 441 probes, zero hits), so the scope exists only as prose inside `prompt`; and the not-yet-dispatched siblings of a batch are not visible in the transcript at dispatch time. What remains is a **ledger**: each dispatch records its scope under a wave key, and the next dispatch is checked against everything already recorded.
109
111
 
110
- - **Ledger:** `.orchestrator/wave-dispatch-scopes.json` (gitignored), keyed `<session-id>|w<wave>|<role>` from `<state-dir>/wave-scope.json`.
111
- - **Lock:** `.orchestrator/wave-dispatch-scopes.lock` — the read-modify-write cycle runs under `withFileLock()` (the primitive behind the PSA-005 STATE.md lock). `writeJsonAtomicSync` makes the write atomic, never the *cycle*.
112
+ - **Ledger:** `.orchestrator/wave-dispatch-scopes.<session>.json` — ONE FILE PER SESSION since #1493, `<session>` being the payload's `session_id` reduced to `[A-Za-z0-9._-]` (`ledgerPathsFor()`). Before, every session of the working copy shared one file holding one `waveKey`: claims of two sessions were never compared (the key's first segment is the session id), but a peer's dispatch reset the file and erased this session's live claims. The pre-split `.orchestrator/wave-dispatch-scopes.json` is read once for migration — adopted only while this session has no file yet and its `waveKey` names this session — and never written or deleted, since it may be a peer's live state. (Gitignored in this repo; consumer repos get the ignore line from bootstrap's `store-lock-ignore` step since #1495 — a repo bootstrapped earlier gets it on its next `--upgrade` or `--refresh-lock`), keyed `<session-id>|w<wave>|<role>` from `<state-dir>/wave-scope.json`.
113
+ - **Lock:** `.orchestrator/wave-dispatch-scopes.<session>.lock`, one per ledger file — the read-modify-write cycle runs under `withFileLock()` (the primitive behind the PSA-005 STATE.md lock). `writeJsonAtomicSync` makes the write atomic, never the *cycle*.
112
114
  - **Scope extraction:** `extractScopeFromPrompt()` finds a scope marker line and takes the FIRST fenced block after it, accepting only lines that survive a deliberately strict `looksLikeRepoPath()`. `normalizeScopeEntry()` then folds `./`, `//` and `/./` spellings together, and `promoteDirEntries()` rewrites `scripts/lib` → `scripts/lib/` **on evidence** (not a tracked file itself, at least one tracked file beneath it) — both because two spellings of one path previously compared as disjoint.
113
- - **DENY** fires on exactly one condition: a collision involving THIS dispatch with a prior agent of the same wave that is **still in flight** (§ 5). The reason names the agent pair, the collision kind and the evidence paths; the suggestion is "give the file exactly ONE owner, or wait for the named agent(s) and re-dispatch", plus the ledger path to delete if the state is stale. The ledger is deliberately **not** persisted on a deny — the dispatch did not happen, so recording it would make the retry-after-fix look like a duplicate.
115
+ - **DENY** fires on exactly one condition: a collision involving THIS dispatch with a prior agent of the same wave that is **still in flight** (§ 5). The reason names the agent pair, the collision kind and the evidence paths; the suggestion is "give the file exactly ONE owner, or wait for the named agent(s) and re-dispatch", plus this session's ledger path to delete if the state is stale (never a peer's). The ledger is deliberately **not** persisted on a deny — the dispatch did not happen, so recording it would make the retry-after-fix look like a duplicate.
114
116
 
115
117
  ### 4.1 Error-class matrix — deliberately fail-**open**
116
118
 
@@ -127,11 +129,11 @@ The blast radius is asymmetric. A false positive on the dispatch path blocks eve
127
129
  | 7 | ledger unreadable / corrupt | WARN + ALLOW + **self-heal** (the verdict carries a fresh ledger) |
128
130
  | 8 | `git ls-files` failed | ALLOW, degraded (stage 3a loses witnesses; concrete collisions still found) |
129
131
  | 9 | `findScopeCollisions` not evaluable | WARN + ALLOW |
130
- | 10 | same agent id re-dispatched | ALLOW, ledger record replaced (a retry must not self-lock) |
132
+ | 10 | same agent id re-dispatched | ALLOW; the earlier record is replaced only once the probe shows it FINISHED (a retry after a failure must not self-lock). A still-running predecessor with a distinct `tool_use_id` stays a collision partner (overlap → row 11); without ids on both sides it stays in the ledger as a claim, with no self-collision (#1480 B4). Replaced without a probe only where no claim is lost: the same `tool_use_id`, or (no ids on either side) a predecessor wholly covered by the retry's scope |
131
133
  | 10a | collision, but every colliding prior agent has FINISHED | ALLOW + prune those records |
132
134
  | 11 | collision with a prior agent still IN FLIGHT | **DENY** |
133
135
  | 12 | unexpected throw in `main()` | ALLOW + stderr |
134
- | 13 | liveness probe throws / no evidence at all | treated as IN FLIGHT (bounded by the TTL, § 5) |
136
+ | 13 | liveness probe throws / no evidence at all / only a result form that proves nothing | treated as IN FLIGHT (bounded by the TTL, § 5) |
135
137
  | 14 | ledger lock not acquirable within its budget | run the cycle UNLOCKED (degraded), never deny |
136
138
 
137
139
  Two structural rules keep this matrix honest, both recorded in the hook header:
@@ -157,7 +159,7 @@ Why it exists: the in-ledger `scopeSignals` counter (§ 4.1, rows 5/6) is a **wa
157
159
 
158
160
  A cryptographic proof still needs that platform boundary: no mechanism inside this repo can distinguish a copied digest from a read one.
159
161
 
160
- **The digest now JOINS the two halves (#1092, 2026-09-16).** Everything above stays true — what changed is that the halves became joinable at all. `scope_checked` carries `scope_digest` (`scopeDigest()` over the paths extracted from the prompt, omitted for an empty scope rather than defaulted to the empty-string digest), `echo_instruction_present`, `instructed_digest` and `digest_consistent`; `scope_echo_checked` already carried `expected_digest` / `actual_digest`; and `materialize-wave-scope` writes one `scope_materialized` record per wave carrying `transport_observable`. `node scripts/lib/scope-echo.mjs --verify --wave <N> --state-dir <dir>` joins all three **on the digest, never on `agent_id`** — measured 2026-09-16 over this host's ledger, 609 send-side against 51 receive-side records whose agent-id sets overlap in ZERO elements (send records the dispatch `description` + `subagent_type`, receive the coordinator's short handle), so before the digest the two halves could not be paired even in principle. Its closed verdict enum — `matched` · `injected-not-echoed` · `echoed-not-injected` · `injection-missing` · `digest-unknown` · `duplicate-claim`, all degrading to `echo-only` when the transport is unobservable — is what makes the issue's AC-2 (an omitted injection turning an end-to-end probe red) a measurable state rather than an unfalsifiable absence. `digest_consistent: false` additionally catches agent A's fenced block beside agent B's echo line **at dispatch time**, from the prompt alone. None of this raises the ceiling above: a joined digest still proves transport of a LINE, never obedience.
162
+ **The digest now JOINS the two halves (#1092, 2026-09-16).** Everything above stays true — what changed is that the halves became joinable at all. `scope_checked` carries `scope_digest` (`scopeDigest()` over the paths extracted from the prompt, omitted for an empty scope rather than defaulted to the empty-string digest), `echo_instruction_present`, `instructed_digest` and `digest_consistent`; `scope_echo_checked` already carried `expected_digest` / `actual_digest`; and `materialize-wave-scope` writes one `scope_materialized` record per wave carrying `transport_observable`. `node scripts/lib/scope-echo.mjs --verify --wave <N> --state-dir <dir>` joins all three **on the digest, never on `agent_id`** — measured 2026-09-16 over this host's ledger, 609 send-side against 51 receive-side records whose agent-id sets overlap in ZERO elements (send records the dispatch `description` + `subagent_type`, receive the coordinator's short handle), so before the digest the two halves could not be paired even in principle. Its closed verdict enum — `matched` · `injected-not-echoed` · `echoed-not-injected` · `injection-missing` · `digest-unknown` · `duplicate-claim`, all degrading to `echo-only` when the transport is unobservable — is what makes the issue's AC-2 (an omitted injection turning an end-to-end probe red) a measurable state rather than an unfalsifiable absence. `digest_consistent: false` additionally catches agent A's fenced block beside agent B's echo line **at dispatch time**, from the prompt alone. The coordinator's own `coordinator.json` takes no part in the join, and `scope_materialized` counts no agent for it (`isCoordinatorRecordId()`, #1493.2): it is a declaration, never a dispatch, so nothing can claim its digest and it used to read `injection-missing` on every wave where the coordinator planned an edit. None of this raises the ceiling above: a joined digest still proves transport of a LINE, never obedience.
161
163
 
162
164
  **Revisit-Trigger** (verbatim from issue #1092, for the transport half this section deliberately does NOT close):
163
165
 
@@ -174,7 +176,11 @@ The discriminator is therefore neither time nor the agent's name, but whether th
174
176
  - **Synchronous dispatch** — the `tool_result` for the dispatch's `tool_use` id arrives when the agent is done (measured: five `Agent` rows within 0.44 s, their results 5–11 minutes later). At the fifth agent's `PreToolUse` none of the first four has a result → all in flight → a real same-batch overlap still denies.
175
177
  - **Asynchronous dispatch** — the `tool_result` arrives in ~0.2 s and reads `Async agent launched successfully`. **That text is a launch receipt, not a completion.** Treating it as one would let every real background-batch collision through. The completion arrives later as a `<task-notification>` record carrying `<tool-use-id>` and `<status>completed</status>` (measured: launch 14:14:26.768 → notification 14:24:39.360).
176
178
 
177
- `buildTranscriptIndex()` reads all three record shapes and counts a description as finished only when **every** one of its dispatch ids is finished. Cost containment: the transcript is read **only once a collision has already been found** — i.e. only on the path that is about to deny; the no-collision path pays nothing. Transcripts above 256 MiB are treated as *no evidence*, never as a completion.
179
+ **A completion needs positive evidence (#1480 B2).** Before 2026-09-30 every `tool_result` that did not contain the launch receipt counted as a completion, so any drift in the receipt's wording turned a launch into a finish. That was measured, not hypothetical: a forked dispatch answers `Fork started — processing in background`, which was read as done. A `tool_result` now finishes its dispatch only in a form the census found on real completions (`RESULT_COMPLETION_FORMS`): `is_error`, the sync report's `<usage>`/`total_tokens` trailer (690 of 690), or a text beginning `This agent's report was delivered` (35 of 35, none followed by any notification). Any other form is *no evidence* and falls to the TTL (row 13), never to "finished".
180
+
181
+ **Resume re-opens a dispatch (#1459 item 1).** A failed or finished agent can be resumed with `SendMessage` addressed to its task id (the `agentId` in its own result, or the `<task-id>` of its notification). The resumed run finishes under the SendMessage's `tool_use` id, not the original one (census 2026-09-30: 455 of 455 resumes with a later completion). The probe therefore treats every `SendMessage` to the task id as an activation, unless its result says `"success":false` or is an error, and counts the dispatch as finished only when a completion follows the LAST activation. The completion can be a terminal notification under the original id, a SendMessage id or the task id. Closing still comes only from harness-written notification records or the dispatch's own `tool_result`. A message cannot close anything; it can only re-open.
182
+
183
+ **Exact id first (#1480 B3).** Each ledger record carries the dispatch's own `useId` (the payload's `tool_use_id`) and is resolved by that id. The description is a fallback for records without one, used only when it is non-empty, and it excludes the current dispatch's own id. A record is never looked up by its ledger id, which for an empty description is just the agent type and matched any namesake. `buildTranscriptIndex()` stays the description view: a description counts as finished only when **every** one of its dispatch ids is. Cost containment: the transcript is read **only once a collision has already been found** — i.e. only on the path that is about to deny; the no-collision path pays nothing. Transcripts above 256 MiB are treated as *no evidence*, never as a completion.
178
184
 
179
185
  **Blind fallback and its named ceiling (BV-004).** With no transcript, or none carrying a record of that agent, liveness falls back to the ledger entry's own age with `IN_FLIGHT_TTL_MS = 30 min`. The ceiling is derived: the largest **measured** same-batch dispatch spread is 95.7 s, so 30 min is ~19× headroom against the false-ALLOW direction, while both measured sequential repair gaps (36 min, 49 min) sit above it. **Revisit trigger:** a same-batch spread above ~5 min appearing in `.orchestrator/metrics/`, or a harness change that stops writing `transcript_path` — either invalidates the headroom the number rests on.
180
186
 
@@ -182,7 +188,7 @@ The discriminator is therefore neither time nor the agent's name, but whether th
182
188
 
183
189
  Complete list of what this guard does **not** see, or sees only approximately:
184
190
 
185
- 1. **The blind TTL window.** Without transcript evidence the only liveness signal is the 30-minute TTL above. Inside that window a finished agent still blocks (false deny, recoverable by deleting the ledger); outside it a running agent no longer blocks (false allow). Revisit trigger as stated in § 5.
191
+ 1. **The blind TTL window.** Without transcript evidence the only liveness signal is the 30-minute TTL above. Inside that window a finished agent still blocks (false deny, recoverable by deleting this session's ledger); outside it a running agent no longer blocks (false allow). Revisit trigger as stated in § 5.
186
192
  2. **Test-sibling collisions.** Disjointness is asserted on the **declared** scopes (step 2), before `expandTestSiblings()` runs (step 3). Two agents whose production files share a basename receive the *same* emitted sibling glob (`tests/**/{basename}*.test.mjs`), which a declared-scope check cannot see. Revisit if a wave is ever scoped by basename family instead of by directory.
187
193
  3. **Prose extraction fails only toward ALLOW.** The hook's only channel is the `FILE-SCOPE` prose block. A missing marker, a missing fence, a decorated path that fails `looksLikeRepoPath()` — all resolve to allow (rows 5/6). Measured: 42 of 147 archived prompts (28.6 %) carried a scope marker at all, so denying the non-extractable case would have denied ~7 dispatches in 10. The CLI check (step 2) is the gate that does not depend on prose.
188
194
  4. **Coordinator-direct edits are invisible to the hook.** They are not dispatches, so no ledger entry exists for them. They participate in the CLI check via `coordinator.json` only — and 2 of the 5 divergences that motivated #1020 were coordinator-direct edits.
@@ -191,16 +197,16 @@ Complete list of what this guard does **not** see, or sees only approximately:
191
197
  7. **Only collisions involving the current dispatch are actionable.** A pair among already-dispatched agents was either denied at its own dispatch or predates the guard; re-denying it would block an innocent third agent.
192
198
  8. **Lock loss reopens the race.** On lock timeout the cycle runs unlocked (row 14) — two dispatches starting together can then read the same ledger state and one record is lost. That is the pre-lock behaviour, chosen over denying on a lock-file problem.
193
199
  9. **The session binding is self-declared** (§ 2.3). `session` is a plain field in a file any process in this working copy can write, so writing a foreign id into it turns the write gate off for that manifest. Named rather than hidden: it is the SAME power `enforcement: "off"` already grants in the same file, so Gate 3b adds no new authority — the manifest is the coordinator's own artefact either way.
194
- 10. **Only the WRITE gate is session-bound.** The dispatch ledger of § 4 takes its session component from the harness's own `input.session_id` (`waveKeyOf(projectDir, sessionId, …)`), and reads only `wave` and `role` out of `wave-scope.json` — the `session` field is not consulted there at all. So a peer session's manifest cannot bind this session's writes since #1123, but the two hooks reach that property by different routes, and a change to one does not carry to the other.
200
+ 10. **The manifest binding is read by the WRITE gates, not by the dispatch hook.** `enforce-scope`, `enforce-commands`, `post-bash-write-verify` and — since #1493.1 — the git pre-commit `wave-scope-commit-guard` (env-tier identity only, it has no hook payload) classify the manifest via `classifyManifestSession()` and stand down on a foreign one. The dispatch ledger of § 4 takes its session from the harness's own `input.session_id` instead — in its file name since #1493 and in `waveKeyOf(projectDir, sessionId, …)` — and reads only `wave` and `role` out of `wave-scope.json`; the `session` field is not consulted there at all. So a peer session's manifest cannot bind this session's writes or commits, but the hooks reach that property by different routes, and a change to one does not carry to the other.
195
201
  11. **A session that published a peer's id reads its OWN manifest as `foreign` (#1194) — the writer's defense is now mechanical (#1207).** Dropping the lock tier (§ 2.3) moved the cost to the other side of the trade: with a raw `sessionAttribution()` read, a session that lost the `bootstrapLock()` race could get the peer's id and write it into its own manifest, and Gate 3b would then classify the manifest `foreign`, standing its own write guard down. Since #1207 the writer calls `attributionForRecord(repoRoot)` instead — `skills/wave-executor/wave-loop.md` § Scope Manifest 1 — which confirms the lock's raw `session_id` against `readProcessLocalSessionIds()` before returning anything and yields `{}` on any mismatch, so the coordinator performs no manual comparison and STATE.md plays no part in it (a shared working-copy artefact written by whichever session holds the lock, not a process-local witness). A filled binding is therefore provably this session's own; unbound (`{}`) = ENFORCE. CEILING (BV-004): on a harness that exports no session env var and puts no `session_id` in the hook payload (Codex CLI, Cursor today) both tiers are empty, so G3b is permanently `unknown` = enforce = pre-#1123 behaviour there. Revisit when Codex/Cursor hook payloads carry a session id.
196
202
 
197
203
  ## 7. Debugging
198
204
 
199
- **A dispatch was denied and you do not believe it.** Read `.orchestrator/wave-dispatch-scopes.json`: it carries `waveKey`, `updated`, and one `{id, desc, files, at}` record per already-dispatched agent. The deny reason names the other agent — find its record and compare its `files` to the ones in your prompt's `FILE-SCOPE` block. Three outcomes:
205
+ **A dispatch was denied and you do not believe it.** Read this session's `.orchestrator/wave-dispatch-scopes.<session>.json` (the deny suggestion names the exact file): it carries `waveKey`, `updated`, and one `{id, desc, useId, files, at}` record per already-dispatched agent (`useId` absent when the harness sent no `tool_use_id`). The deny reason names the other agent — find its record and compare its `files` to the ones in your prompt's `FILE-SCOPE` block. Three outcomes:
200
206
 
201
207
  - The other agent is genuinely running and the overlap is real → fix the ownership in the session plan (one file, one agent), rewrite the affected `filescopes/wave-<N>/*.json`, re-assert, re-dispatch.
202
- - The other agent has finished, but the ledger still binds it → the transcript carried no evidence (§ 5) and you are inside the TTL window. Delete `.orchestrator/wave-dispatch-scopes.json`; the next dispatch rebuilds it.
203
- - The `waveKey` names an older wave → the `<session>|w?|?` fallback (limit 5). Check that `<state-dir>/wave-scope.json` exists and is readable, then delete the ledger.
208
+ - The other agent has finished, but the ledger still binds it → the transcript carried no evidence (§ 5) and you are inside the TTL window. Delete this session's `.orchestrator/wave-dispatch-scopes.<session>.json`; the next dispatch rebuilds it. Never delete another session's file beside it — that erases its live claims.
209
+ - The `waveKey` names an older wave → the `<session>|w?|?` fallback (limit 5). Check that `<state-dir>/wave-scope.json` exists and is readable, then delete this session's ledger file.
204
210
 
205
211
  **A dispatch was NOT denied and should have been.** Start at the ledger line, which now says which row fired without re-running anything: `jq -c 'select(.event=="orchestrator.wave_dispatch.scope_checked")' .orchestrator/metrics/events.jsonl | tail` — `injected:false` with `signal:"marker-absent"` is row 5 (no declaration in the prompt at all, i.e. the injection step, not the guard), `signal:"unparseable"` is row 6 (a declaration the parser could not use), `ledger_result:"allow-finished"` is row 10a (the collision was real but its partner had finished), and NO record at all for a dispatch you watched happen means the hook never ran or crashed before deciding (§ 4.2). Then work down the allow rows: is `FILE-SCOPE` present in the prompt with a fenced block right after it (rows 5/6)? Is the hook armed at all (`GUARD INACTIVE` on stderr = row 2)? Did a `systemMessage` warning appear (rows 7/9)? Cross-check the same scopes through the CLI, which does not depend on prose:
206
212
 
@@ -210,7 +216,7 @@ node scripts/validate-wave-scope.mjs --assert-disjoint "$WAVE_SCOPES_SIDECAR" <
210
216
 
211
217
  Exit 1 prints one `ERROR:` line per collision (`agents "A" and "B" both claim [...]`) and one per duplicate id. Exit 0 — nothing on stderr, the manifest echoed back on stdout — means the declared scopes really are disjoint and the hook was right to allow; the divergence is then in the prompt, not in the plan.
212
218
 
213
- **Reset.** Delete `.orchestrator/wave-dispatch-scopes.json` (and `.orchestrator/wave-dispatch-scopes.lock` if a dead holder is suspected). Both are gitignored, both are rebuilt on the next dispatch, and neither is shared with any other mechanism. After the final wave, `<state-dir>/filescopes/` is deleted along with `wave-scope.json` — a stale `wave-<N>/` directory left behind is a scope claim nobody re-verified.
219
+ **Reset.** Delete this session's `.orchestrator/wave-dispatch-scopes.<session>.json` (and its `.lock` if a dead holder is suspected) — never a peer's. Both are gitignored here (in a consumer repo only once bootstrap's `store-lock-ignore` step has run, #1495), both are rebuilt on the next dispatch, and neither is shared with any other mechanism or session. After the final wave, `<state-dir>/filescopes/` is deleted along with `wave-scope.json` and this session's ledger file (`skills/wave-executor/references/wave-loop-scope-manifest.md` step 7, session-end 3.0); a crashed session's file stays behind, inert — no other session reads it — a stale `wave-<N>/` directory left behind is a scope claim nobody re-verified.
214
220
 
215
221
  **Disable.** The hook honours the repo's profile gate (`shouldRunHook('pre-task-scope-disjoint')`, row 1) — a silent exit 0, no decision at all.
216
222
 
@@ -223,6 +229,24 @@ Every figure above is quoted from a measurement recorded next to the code that c
223
229
 
224
230
  Re-measure before citing any of these downstream. A count re-briefed later is a claim about the past (`.claude/rules/parallel-sessions.md` § PSA-006).
225
231
 
232
+ ## The two-shape trap (#1020)
233
+
234
+ > Moved from CLAUDE.md Critical Gotchas, 2026-09-25.
235
+
236
+ **`allowedPaths` is COMPUTED, and the scope declaration has TWO shapes — writing only one is the silent failure (#1020).**
237
+
238
+ (a) **Per agent**, `<state-dir>/filescopes/wave-<N>/<agent-id>.json`, a JSON array of path STRINGS. This is `$AGENT_FILESCOPE_JSON` — what the `FILE-SCOPE` prompt injection, the Learnings-Index and `--assert-subset` read.
239
+
240
+ (b) **Per wave**, ONE aggregate sidecar holding an array of `{id, files}` RECORDS, built from every file in (a) plus the coordinator's own. This is the only thing `--assert-disjoint` and `--union` accept: pass them the wave directory and you get `Cannot read --union file`; pass them a single (a)-file and you get `must be a JSON array of {id, files} records`.
241
+
242
+ `node scripts/materialize-wave-scope.mjs --state-dir <state-dir> --wave <N>` is the canonical writer: it receives that one record array on stdin, writes every (a) file first, then (b) last. Neither is a `$TMPDIR` copy, and neither substitutes for the other.
243
+
244
+ Skipping (a) degrades the chain to signal-free ALLOW: no `FILE-SCOPE` block reaches the prompt, so `hooks/pre-task-scope-disjoint.mjs` extracts nothing, permits the dispatch, and writes no ledger entry — the absence looks identical to a clean run.
245
+
246
+ **Measured 2026-08-19:** a coordinator that wrote only (b) ran SIX waves and ~27 dispatches with zero injection; `.orchestrator/wave-dispatch-scopes.json` still carried a `|w2|` entry from two days earlier. Only `wave-scope.json` `allowedPaths` (global Gate 7) was live, which is why blocks still fired and the gap stayed invisible.
247
+
248
+ The coordinator's OWN direct edits go in a `coordinator.json` under (a) **and must be folded into (b)** — a `coordinator.json` that is never folded in is read by nothing. 2 of the 5 recorded divergences were coordinator-direct edits, which no agent scope file covers by construction.
249
+
226
250
  ## See Also
227
251
 
228
252
  - `skills/wave-executor/wave-loop.md` § Scope Manifest — the coordinator runbook (steps 3.1–3.3) and § Pre-Dispatch: File-Scope Injection (the prompt block shape).