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
@@ -133,7 +133,16 @@ function jsonlPath() {
133
133
  * Defense-in-depth byte ceiling for transcript reads (#624). The transcript path
134
134
  * is harness-trusted, so this is not an exploitable vector — but the repo follows
135
135
  * a bounded-read discipline: a pathologically large transcript is skipped (yields
136
- * the empty/zero-usage result) rather than read whole into memory.
136
+ * the null-usage result) rather than read whole into memory.
137
+ *
138
+ * CEILING: a skipped transcript yields a stop record with found=true but null
139
+ * tokens and model; the session rollup counts it as an unpriced cost candidate
140
+ * (#1474) instead of reading it. Frequency per issue #1474 (2026-09-29): 2 of
141
+ * 10,482 subagent transcripts exceeded this size. REVISIT when oversized
142
+ * transcripts exceed ~0.1% of found stop records — count them from transcript
143
+ * sizes, because the ledger writes the same token-less record for an oversized,
144
+ * a usage-free and an unreadable transcript — then read large transcripts
145
+ * streamed instead of skipping them.
137
146
  */
138
147
  const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024; // ~50 MB
139
148
 
@@ -423,13 +432,23 @@ function readStdinJson() {
423
432
  * Four buckets, not two (#1244 / schema_version 2): `input_tokens`,
424
433
  * `cache_read_input_tokens`, `cache_creation_input_tokens` and `output_tokens`
425
434
  * are accumulated SEPARATELY per deduped turn, because they are billed at three
426
- * different rates. `model` is captured from `message.model` of the last kept
427
- * block that carries one (null when absent) — it is what makes a cost estimate
428
- * possible at all downstream.
435
+ * different rates. `model` is what makes a cost estimate possible at all
436
+ * downstream, so it names only a model that actually carried tokens (#1470): a
437
+ * kept turn is token-bearing when its four clamped buckets sum to > 0. Exactly
438
+ * one distinct `message.model` across the token-bearing turns → that model;
439
+ * zero-token turns of another model (a trailing `<synthetic>` turn) are ignored.
440
+ * Two or more, or a token-bearing turn that names no model → null, an honest
441
+ * unknown, with the token sums unchanged. No token-bearing turn at all → the
442
+ * last kept block that carries one (null when absent). Two or more models with
443
+ * every token-bearing turn named → `modelsUsage` carries the four buckets per
444
+ * model, so the session rollup can price each part; otherwise it is null.
429
445
  *
430
446
  * @param {string|undefined|null} transcriptPath — absolute path from stdin
431
447
  * @returns {{ tokenInputUncached: number|null, tokenCacheRead: number|null,
432
- * tokenCacheCreation: number|null, tokenOutput: number|null, model: string|null }}
448
+ * tokenCacheCreation: number|null, tokenOutput: number|null, model: string|null,
449
+ * modelsUsage: Array<{ model: string, token_input_uncached: number,
450
+ * token_cache_read: number, token_cache_creation: number,
451
+ * token_output: number }>|null }}
433
452
  */
434
453
  function extractTranscriptUsage(transcriptPath) {
435
454
  const nullResult = {
@@ -438,6 +457,7 @@ function extractTranscriptUsage(transcriptPath) {
438
457
  tokenCacheCreation: null,
439
458
  tokenOutput: null,
440
459
  model: null,
460
+ modelsUsage: null,
441
461
  };
442
462
  try {
443
463
  if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) return nullResult;
@@ -523,22 +543,73 @@ function extractTranscriptUsage(transcriptPath) {
523
543
  let tokenCacheRead = 0;
524
544
  let tokenCacheCreation = 0;
525
545
  let tokenOutput = 0;
546
+ /** Last non-null model over ALL kept turns — the zero-token fallback. */
526
547
  let model = null;
548
+ /** Per-model bucket sums of the token-bearing kept turns (#1470). */
549
+ const billedModels = new Map();
550
+ /** A token-bearing turn that names no model: its price is unknowable. */
551
+ let unattributed = false;
552
+ // Per-turn clamp (#624): a turn's value counts ONLY when it is a non-negative
553
+ // integer. A poisoned value (negative, NaN, float like 10.5) contributes 0 so
554
+ // the good turns survive. An absent side contributes 0, not null.
555
+ const clamp = (value) => (Number.isInteger(value) && value >= 0 ? value : 0);
527
556
  for (const { usage, model: turnModel } of kept) {
528
- // Per-turn clamp (#624): add a turn's value ONLY when it is a non-negative
529
- // integer. A poisoned value (negative, NaN, float like 10.5) is skipped so
530
- // the good turns survive. An absent side contributes 0, not null.
531
- const inTok = usage.input_tokens;
532
- const cacheRead = usage.cache_read_input_tokens;
533
- const cacheCreation = usage.cache_creation_input_tokens;
534
- const outTok = usage.output_tokens;
535
- if (Number.isInteger(inTok) && inTok >= 0) tokenInputUncached += inTok;
536
- if (Number.isInteger(cacheRead) && cacheRead >= 0) tokenCacheRead += cacheRead;
537
- if (Number.isInteger(cacheCreation) && cacheCreation >= 0) tokenCacheCreation += cacheCreation;
538
- if (Number.isInteger(outTok) && outTok >= 0) tokenOutput += outTok;
539
- if (turnModel !== null) model = turnModel;
557
+ const inTok = clamp(usage.input_tokens);
558
+ const cacheRead = clamp(usage.cache_read_input_tokens);
559
+ const cacheCreation = clamp(usage.cache_creation_input_tokens);
560
+ const outTok = clamp(usage.output_tokens);
561
+ tokenInputUncached += inTok;
562
+ tokenCacheRead += cacheRead;
563
+ tokenCacheCreation += cacheCreation;
564
+ tokenOutput += outTok;
565
+ const billed = inTok + cacheRead + cacheCreation + outTok > 0;
566
+ if (turnModel === null) {
567
+ if (billed) unattributed = true;
568
+ continue;
569
+ }
570
+ model = turnModel;
571
+ if (!billed) continue;
572
+ let part = billedModels.get(turnModel);
573
+ if (part === undefined) {
574
+ part = {
575
+ model: turnModel,
576
+ token_input_uncached: 0,
577
+ token_cache_read: 0,
578
+ token_cache_creation: 0,
579
+ token_output: 0,
580
+ };
581
+ billedModels.set(turnModel, part);
582
+ }
583
+ part.token_input_uncached += inTok;
584
+ part.token_cache_read += cacheRead;
585
+ part.token_cache_creation += cacheCreation;
586
+ part.token_output += outTok;
540
587
  }
541
588
 
589
+ // Model attribution (#1470): only a token-bearing turn is an observation of
590
+ // its model; one distinct model → that model, two or more → null, none →
591
+ // the fallback above. A token-bearing turn that names no model also gives
592
+ // null: its tokens must not be priced at whichever other model was named.
593
+ // A mixed-model record carries its per-model sums as `modelsUsage`, which
594
+ // the session rollup prices part by part (#1470). Measured 2026-09-29 over
595
+ // 10,745 subagent transcripts under `~/.claude/projects`: 17 (0.16%)
596
+ // contained more than one model on token-bearing turns (0 of 917 in this
597
+ // repo), while 273 contained more than one model id only through zero-token
598
+ // `<synthetic>` turns, which this rule ignores. A model-less token-bearing
599
+ // turn gets no breakdown: its tokens belong to no part, so the record stays
600
+ // unpriced. The ledger now tells a mixed-model null (`models_usage`
601
+ // present) from a missing model (absent). The fallback also names the LAST
602
+ // model of a transcript whose turns all carry 0 tokens — measured 2026-09-29,
603
+ // 31 of 10,479 records, all `<synthetic>`; the session rollup prices a record
604
+ // whose four buckets are all 0 at $0 regardless of its model (#1474), so that
605
+ // name is harmless.
606
+ let modelsUsage = null;
607
+ if (unattributed) model = null;
608
+ else if (billedModels.size > 1) {
609
+ model = null;
610
+ modelsUsage = [...billedModels.values()];
611
+ } else if (billedModels.size === 1) [model] = billedModels.keys();
612
+
542
613
  // The aggregates are guaranteed non-negative integers by per-turn clamping
543
614
  // above (Σ of non-negative integers), so emit them directly.
544
615
  return {
@@ -547,6 +618,7 @@ function extractTranscriptUsage(transcriptPath) {
547
618
  tokenCacheCreation,
548
619
  tokenOutput,
549
620
  model,
621
+ modelsUsage,
550
622
  };
551
623
  } catch {
552
624
  return nullResult;
@@ -752,7 +824,7 @@ async function main() {
752
824
  // NO fallback to input.transcript_path: that path is the parent session
753
825
  // transcript, and reading it is the #949 defect (every stop inherited the
754
826
  // parent's running totals). A phantom stop gets null — the honest value.
755
- const { tokenInputUncached, tokenCacheRead, tokenCacheCreation, tokenOutput, model } =
827
+ const { tokenInputUncached, tokenCacheRead, tokenCacheCreation, tokenOutput, model, modelsUsage } =
756
828
  subagentTranscriptFound
757
829
  ? extractTranscriptUsage(subagentTranscriptPath)
758
830
  : {
@@ -761,6 +833,7 @@ async function main() {
761
833
  tokenCacheCreation: null,
762
834
  tokenOutput: null,
763
835
  model: null,
836
+ modelsUsage: null,
764
837
  };
765
838
 
766
839
  // schema_version 2 (#1244): `token_input` is now BILLABLE PROMPT VOLUME —
@@ -776,11 +849,15 @@ async function main() {
776
849
  }
777
850
  if (tokenOutput !== null) record.token_output = tokenOutput;
778
851
 
779
- // Model id (#1244) — null when the transcript exposes none. Cost is NOT
852
+ // Model id (#1244) — null when the transcript exposes none, or when its
853
+ // token-bearing turns are not attributable to one model (#1470). Cost is NOT
780
854
  // computed here: pricing lives in scripts/lib/telemetry/pricing.mjs and is
781
855
  // applied by the session rollup, so this hot-path hook keeps its import
782
856
  // graph unchanged.
783
857
  record.model = model;
858
+ // Per-model breakdown (#1470) — written only for a mixed-model record, so
859
+ // the rollup can price it part by part; absent on every other record.
860
+ if (modelsUsage !== null) record.models_usage = modelsUsage;
784
861
 
785
862
  // Cost is best-effort / forward-compat (#624): the native transcript does
786
863
  // NOT expose total_cost_usd today, so this is null in practice. No rate
@@ -22,26 +22,48 @@
22
22
  // agent's fence — i.e. another agent's intent to stage that path.
23
23
  //
24
24
  // Behavior summary
25
- // - No .orchestrator/wave-scope.json → exit 0 (no active wave; sub-mode B
26
- // short-circuits). Sub-mode C still runs if a fence directory exists.
27
- // - Allowed paths violation → exit 1 with restore hint.
25
+ // - No wave-scope.json → exit 0 (no active wave; sub-mode B short-circuits).
26
+ // Sub-mode C still runs if a fence directory exists.
27
+ // - Sub-mode B, in order (#1493.1): a manifest bound to ANOTHER session is
28
+ // not this commit's scope (stand down, one stderr note); `enforcement:
29
+ // "off"` skips.
30
+ // - Non-empty allowedPaths: a staged path outside it BLOCKS (exit 1) under
31
+ // every enforcement level, `warn` included. Deliberately not tied to
32
+ // `enforcement`: in a warn-mode wave enforce-scope lets out-of-scope writes
33
+ // through, so this guard is the only hard PSA-004 stop left for a
34
+ // lint-staged sweep — softening it would weaken detection for the very
35
+ // class it exists to catch.
36
+ // - EMPTY allowedPaths is no longer a silent pass: a Discovery (read-only)
37
+ // manifest means no agent is committing, i.e. a coordinator commit between
38
+ // waves → allow with a note; a manifest older than the newest session
39
+ // clock may be a leftover or a parallel session's live one → report,
40
+ // never block; any other role means the `--union` step never completed →
41
+ // `strict` (the default when the field is absent, as in enforce-scope)
42
+ // blocks, `warn` reports and lets the commit through. Only this new
43
+ // verdict follows `enforcement`: it judges the coordinator's own
44
+ // bookkeeping, not a staged foreign file.
28
45
  // - Cross-agent fence overlap → exit 1 with "staging-fence: cross-agent
29
46
  // overlap" stderr.
30
47
  // - --no-verify bypass: this hook is invoked as a git pre-commit hook;
31
48
  // `git commit --no-verify` skips it entirely (operator opts out by name).
32
49
 
33
50
  import { execSync } from 'node:child_process';
34
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
35
- import { join } from 'node:path';
51
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
52
+ import { join, relative } from 'node:path';
36
53
 
37
54
  // Re-use existing helpers from scripts/lib/hardening.mjs.
38
55
  // IMPORTANT: import is resolved relative to THIS file's location (the hook
39
56
  // ships with the session-orchestrator package), NOT relative to the git
40
57
  // repo it is protecting. The two diverge when the hook is invoked from a
41
58
  // consumer repo or a test tmp-dir.
42
- import { pathMatchesPattern, findScopeFile } from '../scripts/lib/hardening.mjs';
59
+ import { pathMatchesPattern } from '../scripts/lib/hardening.mjs';
43
60
  import { withStagingFenceLock } from '../scripts/lib/session-lock.mjs';
44
61
  import { isMainModule } from '../scripts/lib/is-main-module.mjs';
62
+ import { classifyEmptyScope, findOwnScopeFile, sessionAgeMs } from '../scripts/lib/scope-gate.mjs';
63
+ import {
64
+ classifyManifestSession,
65
+ readProcessLocalSessionIds,
66
+ } from '../scripts/lib/session-identity/own-session.mjs';
45
67
 
46
68
  // Resolve the "current" agent id from SO_WAVE_AGENT_ID (when set by the
47
69
  // caller) or fall back to a PID-derived marker. The fence files themselves
@@ -56,19 +78,6 @@ const ownAgentId = process.env.SO_WAVE_AGENT_ID ?? null;
56
78
  */
57
79
  const ALL_PATHS_MARKER = '*';
58
80
 
59
- /**
60
- * Build a regex that finds a staged path inside a `git add` command string.
61
- * Word-boundary on both sides so `src/foo.ts` does not match `src/foo.ts.bak`.
62
- * The path is escaped so glob metacharacters (`*`, `?`) and shell metas
63
- * cannot be reinterpreted.
64
- *
65
- * LEGACY PATH ONLY since #1404 — see the fallback branch in findOverlaps.
66
- */
67
- function pathRegex(p) {
68
- const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
- return new RegExp(`(?:^|[\\s'"])${escaped}(?:$|[\\s'"])`);
70
- }
71
-
72
81
  /**
73
82
  * Normalise a path token so reader and writer compare the same spelling.
74
83
  *
@@ -107,19 +116,13 @@ function pathsOverlap(fencePath, ourPath) {
107
116
  * Walk a single sibling fence file and return the staged paths a sibling
108
117
  * agent also recorded an intent to stage.
109
118
  *
110
- * Two entry shapes are accepted:
111
- * - #1404 shape `{ paths, command_hash, timestamp }` — path LISTS compared
112
- * against our staged set.
113
- * - LEGACY shape `{ command, timestamp }` — a fence file written by a
114
- * pre-#1404 hook version. Live sessions on this host may hold such files
115
- * RIGHT NOW, and dropping them would read as "no overlap" — silently
116
- * weaker than before the change. So the old raw-command regex still runs
117
- * for an entry that has `command` and no `paths`.
118
- * NAMED CEILING (BV-004): this fallback exists only to span the sessions
119
- * running across the upgrade. REVISIT TRIGGER — remove it one minor
120
- * release after 5.3.0 (i.e. in 5.4.0), by which point no process started
121
- * before the upgrade can still be writing fence files. The command text it
122
- * reads is never PRINTED (issue #1404) — only the fact of the match is.
119
+ * One entry shape is read: the #1404 `{ paths, command_hash, timestamp }`,
120
+ * whose path LIST is compared against our staged set. An entry without a
121
+ * `paths` array is skipped — that includes the pre-#1404 `{ command }` shape,
122
+ * whose raw-command regex fallback was removed in 5.4.x as its own revisit
123
+ * trigger demanded (#1487 point 16): the only fence writer,
124
+ * hooks/pre-bash-staging-fence.mjs, has written `paths` + `command_hash` and
125
+ * never `command` since 5.3.0, and no other hook or bridge writes fence files.
123
126
  */
124
127
  function findOverlaps(fenceJsonPath, ourStaged) {
125
128
  let body;
@@ -135,36 +138,126 @@ function findOverlaps(fenceJsonPath, ourStaged) {
135
138
  const siblingAgent = body.agent_id ?? '<unknown>';
136
139
  const matches = [];
137
140
  for (const entry of body.staged_paths) {
138
- if (Array.isArray(entry?.paths)) {
139
- const hash = typeof entry.command_hash === 'string' ? entry.command_hash : '<no-hash>';
140
- for (const raw of entry.paths) {
141
- if (typeof raw !== 'string') continue;
142
- const fencePath = normalizeStagedPath(raw);
143
- for (const ours of ourStaged) {
144
- if (pathsOverlap(fencePath, normalizeStagedPath(ours))) {
145
- matches.push({ ourPath: ours, siblingAgent, fencePath, hash });
146
- }
141
+ if (!Array.isArray(entry?.paths)) continue;
142
+ const hash = typeof entry.command_hash === 'string' ? entry.command_hash : '<no-hash>';
143
+ for (const raw of entry.paths) {
144
+ if (typeof raw !== 'string') continue;
145
+ const fencePath = normalizeStagedPath(raw);
146
+ for (const ours of ourStaged) {
147
+ if (pathsOverlap(fencePath, normalizeStagedPath(ours))) {
148
+ matches.push({ ourPath: ours, siblingAgent, fencePath, hash });
147
149
  }
148
150
  }
149
- continue;
150
- }
151
-
152
- const cmd = entry?.command;
153
- if (typeof cmd !== 'string') continue;
154
- for (const ours of ourStaged) {
155
- if (pathRegex(ours).test(cmd)) {
156
- matches.push({
157
- ourPath: ours,
158
- siblingAgent,
159
- fencePath: '<legacy entry, pre-#1404>',
160
- hash: '<legacy entry, pre-#1404>',
161
- });
162
- }
163
151
  }
164
152
  }
165
153
  return matches;
166
154
  }
167
155
 
156
+ /** mtime of a file in ms, or `null` when it cannot be stat'ed. */
157
+ function mtimeMsOf(file) {
158
+ try {
159
+ return statSync(file).mtimeMs;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Sub-mode B's decision, pure: which lines to print and whether to block.
167
+ *
168
+ * Until #1493.1 an empty `allowedPaths` skipped the check outright, the
169
+ * manifest's `role` and `enforcement: "off"` were never read, and a PEER
170
+ * session's manifest governed this session's commits. Each rung below closes
171
+ * one of those; the order is load-bearing — ownership first (a foreign
172
+ * manifest's enforcement level is not ours to honour), then `off`, then the
173
+ * verdict. `warn` softens ONLY the empty-union verdict; a non-empty violation
174
+ * blocks at every level (header § Behavior summary).
175
+ *
176
+ * @param {object} p
177
+ * @param {Record<string, unknown>} p.scope parsed manifest
178
+ * @param {string[]} p.stagedFiles
179
+ * @param {string} p.scopeRel manifest path for messages, repo-relative
180
+ * @param {Set<string>} p.ownIds this process's session ids
181
+ * @param {number|null} p.scopeMtimeMs
182
+ * @param {number|null} p.sessionAge ms since the newest session clock
183
+ * @returns {{action: 'pass'|'report'|'block', lines: string[]}}
184
+ */
185
+ function scopeCommitVerdict({ scope, stagedFiles, scopeRel, ownIds, scopeMtimeMs, sessionAge }) {
186
+ if (stagedFiles.length === 0) return { action: 'pass', lines: [] };
187
+
188
+ // Same classifier and same disposition as enforce-scope Gate 3b: only a
189
+ // PROVABLY foreign manifest stands down; unknown ownership enforces.
190
+ if (classifyManifestSession(scope, ownIds).verdict === 'foreign') {
191
+ return {
192
+ action: 'pass',
193
+ lines: [`wave-scope-commit-guard: ${scopeRel} belongs to another session — its allowedPaths do not apply to this commit.`],
194
+ };
195
+ }
196
+
197
+ const enforcement = scope.enforcement ?? 'strict';
198
+ if (enforcement === 'off') return { action: 'pass', lines: [] };
199
+
200
+ const allowedPaths = Array.isArray(scope.allowedPaths) ? scope.allowedPaths : [];
201
+ if (allowedPaths.length > 0) {
202
+ const violations = stagedFiles.filter(
203
+ (f) => !allowedPaths.some((pattern) => pathMatchesPattern(f, pattern)),
204
+ );
205
+ if (violations.length === 0) return { action: 'pass', lines: [] };
206
+ // Blocks under `warn` too — see the header: in a warn-mode wave this is
207
+ // the only hard stop a lint-staged sweep meets before the commit exists.
208
+ return {
209
+ action: 'block',
210
+ lines: [
211
+ '✗ wave-scope-commit-guard: staged paths outside wave-scope.allowedPaths:',
212
+ ...violations.map((v) => ` - ${v}`),
213
+ '',
214
+ 'These files were likely added by lint-staged eslint --fix / prettier --write.',
215
+ 'To proceed:',
216
+ ' 1) git restore --staged <path> # for each foreign path',
217
+ ' 2) git commit # retry',
218
+ ],
219
+ };
220
+ }
221
+
222
+ // From here on the verdict judges the coordinator's own bookkeeping (an empty
223
+ // union), not a staged foreign file — only this part follows `enforcement`.
224
+ const failAction = enforcement === 'strict' ? 'block' : 'report';
225
+
226
+ const role = typeof scope.role === 'string' ? scope.role.trim() : '';
227
+ const reason = classifyEmptyScope({
228
+ role,
229
+ parseOk: true,
230
+ scopeMtimeMs,
231
+ sessionStartMs: typeof sessionAge === 'number' ? Date.now() - sessionAge : null,
232
+ });
233
+ if (reason === 'read-only-role') {
234
+ // No wave agent commits (PSA-007), so a commit under a read-only manifest
235
+ // is the coordinator's own, between waves.
236
+ return {
237
+ action: 'pass',
238
+ lines: [`wave-scope-commit-guard: ${scopeRel} is a ${role} (read-only) manifest — treated as a coordinator commit between waves; allowedPaths not checked.`],
239
+ };
240
+ }
241
+ if (reason === 'stale-manifest') {
242
+ // Older than the newest session clock of this working copy: a leftover OR a
243
+ // parallel session's live manifest, which the clocks cannot tell apart
244
+ // (scope-gate.mjs § "…and the reader may not be the OWNER") — so it is
245
+ // never grounds to block this commit.
246
+ return {
247
+ action: 'report',
248
+ lines: [`⚠ wave-scope-commit-guard: ${scopeRel} grants no paths and predates the newest session start in this working copy — a leftover or a parallel session's live manifest; allowedPaths not checked for this commit.`],
249
+ };
250
+ }
251
+ return {
252
+ action: failAction,
253
+ lines: [
254
+ `${failAction === 'block' ? '✗' : '⚠'} wave-scope-commit-guard: ${scopeRel} grants no paths${role ? ` for role \`${role}\`` : ''} — the coordinator's \`--union\` step did not complete, so none of the ${stagedFiles.length} staged path(s) can be checked against this wave's scope.`,
255
+ 'Re-run the union step (skills/wave-executor/references/wave-loop-scope-manifest.md § 3.3), then retry the commit; do not hand-edit allowedPaths.',
256
+ ...(failAction === 'block' ? [] : ['(enforcement: warn — reported, the commit proceeds)']),
257
+ ],
258
+ };
259
+ }
260
+
168
261
  /**
169
262
  * Both sub-modes, in the order the pre-commit hook needs them. Every statement
170
263
  * here used to sit at module top level, which meant a bare `import()` of this
@@ -181,7 +274,16 @@ async function main() {
181
274
  // skills/wave-executor/wave-loop.md), so this guard never fired. findScopeFile
182
275
  // returns null when no scope file exists at any precedence dir, preserving
183
276
  // the "no active wave → exit 0" semantics below.
184
- const scopePath = findScopeFile(repoRoot);
277
+ //
278
+ // #1504 point 6: the first manifest that is not provably a peer's, so a
279
+ // peer's higher-precedence manifest no longer hides this session's own one;
280
+ // when every candidate is foreign the first comes back and
281
+ // `scopeCommitVerdict` passes it as before. The git process inherits the
282
+ // committing session's env; there is no hook payload here, so the env tier is
283
+ // the only process-local identity.
284
+ const ownIds = new Set(readProcessLocalSessionIds());
285
+ const located = findOwnScopeFile(repoRoot, ownIds, classifyManifestSession);
286
+ const scopePath = located.path ?? located.foreignPath;
185
287
  const fenceDir = join(repoRoot, '.orchestrator', 'staging-fence');
186
288
 
187
289
  // -------------------------------------------------------------------------
@@ -200,22 +302,16 @@ async function main() {
200
302
  process.exit(1);
201
303
  }
202
304
 
203
- const allowedPaths = Array.isArray(scope.allowedPaths) ? scope.allowedPaths : [];
204
- if (allowedPaths.length > 0) {
205
- const violations = stagedFiles.filter(
206
- (f) => !allowedPaths.some((pattern) => pathMatchesPattern(f, pattern)),
207
- );
208
-
209
- if (violations.length > 0) {
210
- process.stderr.write('✗ wave-scope-commit-guard: staged paths outside wave-scope.allowedPaths:\n');
211
- for (const v of violations) process.stderr.write(` - ${v}\n`);
212
- process.stderr.write('\nThese files were likely added by lint-staged eslint --fix / prettier --write.\n');
213
- process.stderr.write('To proceed:\n');
214
- process.stderr.write(' 1) git restore --staged <path> # for each foreign path\n');
215
- process.stderr.write(' 2) git commit # retry\n');
216
- process.exit(1);
217
- }
218
- }
305
+ const verdict = scopeCommitVerdict({
306
+ scope: scope !== null && typeof scope === 'object' ? scope : {},
307
+ stagedFiles,
308
+ scopeRel: relative(repoRoot, scopePath) || scopePath,
309
+ ownIds,
310
+ scopeMtimeMs: mtimeMsOf(scopePath),
311
+ sessionAge: sessionAgeMs(repoRoot),
312
+ });
313
+ for (const line of verdict.lines) process.stderr.write(`${line}\n`);
314
+ if (verdict.action === 'block') process.exit(1);
219
315
  }
220
316
 
221
317
  // -------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
5
5
  "type": "module",
6
6
  "homepage": "https://session-orchestrator.com",
@@ -54,6 +54,7 @@
54
54
  "!**/.DS_Store"
55
55
  ],
56
56
  "scripts": {
57
+ "update:local": "node scripts/self-update.mjs",
57
58
  "test": "node scripts/check-package-manager.mjs && vitest --run",
58
59
  "test:coverage": "node scripts/check-package-manager.mjs && vitest --run --coverage",
59
60
  "test:watch": "node scripts/check-package-manager.mjs && vitest",
@@ -92,11 +93,6 @@
92
93
  "prettier": "^3.0.0",
93
94
  "vitest": "^4.1.11"
94
95
  },
95
- "overrides": {
96
- "vite": "^8.0.16",
97
- "fast-uri": "^3.1.5",
98
- "brace-expansion": "^5.0.9"
99
- },
100
96
  "license": "MIT",
101
97
  "repository": {
102
98
  "type": "git",
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: "Use when writing, reviewing, or pruning tests. Three modes, one value bar: a four-question gate before any new test is written, a focused audit of one scope, and a campaign over a whole repo or subsystem that removes the least useful tests under a coverage guard. Every delete, merge, or repair carries written evidence and a caught mutation. Triggered by \"audit the tests\", \"prune the test suite\", \"remove low-value tests\", \"test diet\", \"is this test worth adding\", /test-audit."
3
+ argument-hint: "[gate|audit|campaign] [scope] [--goal \\\"<goal>\\\"]"
4
+ ---
5
+
6
+ # /test-audit
7
+
8
+ Use the Session Orchestrator skill definition at `skills/test-audit/SKILL.md`.
9
+
10
+ Arguments: $@
11
+
12
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
package/rules/README.md CHANGED
@@ -11,7 +11,12 @@ It is **not** the always-on rule set of *this* repository — those live in
11
11
 
12
12
  Every rule file here carries a
13
13
  `<!-- source: session-orchestrator plugin (canonical: rules/<path>) -->` header.
14
- On sync, plugin-sourced files are overwritten in the consumer while its
14
+ In a rule with frontmatter it stands on the first line directly after the
15
+ closing `---` — the frontmatter itself must open on line 1, because Claude Code
16
+ reads a rule's frontmatter only there. A rule without frontmatter carries the
17
+ header on line 1. A path-scoped rule declares its file patterns twice, as
18
+ `globs:` and `paths:` with identical lists: `paths:` is the only key Claude Code
19
+ reads, `globs:` is what `rule-loader.mjs` reads first (#1449). On sync, plugin-sourced files are overwritten in the consumer while its
15
20
  locally-authored rules are preserved (copy-on-write). The canonical manifest of
16
21
  what vendors where is [`_index.md`](./_index.md).
17
22
 
package/rules/_index.md CHANGED
@@ -40,7 +40,7 @@ baseline project type (`nextjs-saas`, `express-service`, `docker-service`,
40
40
  - `always-on/build-value.md` — BV-001..004: the seven-rung build ladder, the four protections never simplified away
41
41
  - `always-on/cross-session-messaging.md` — CSM-001..005: messaging is transport, not shared state; no permission laundering
42
42
  - `always-on/loop-and-monitor.md` — LM-001..008: routing between `/goal`, Workflows, Channels, Monitor, `/loop`, and Routines
43
- - `always-on/bash-harness-pitfalls.md` — six false-green shell-harness failure classes (path-scoped via its own `globs:`/`paths:` frontmatter)
43
+ - `always-on/bash-harness-pitfalls.md` — six false-green shell-harness failure classes (path-scoped via `globs:` + `paths:` frontmatter carrying identical lists — `paths:` is what Claude Code reads, `globs:` what `rule-loader.mjs` reads first; #1449)
44
44
 
45
45
  ## opt-in-stack (vendored on match)
46
46
 
@@ -52,8 +52,8 @@ baseline project type (`nextjs-saas`, `express-service`, `docker-service`,
52
52
 
53
53
  ## opt-in-domain (vendored on match)
54
54
 
55
- - `opt-in-domain/prompt-caching.md` — Anthropic prompt caching (breakpoint placement, TTL selection, pre-warming, verification) [archetypes: nextjs-saas, express-service, docker-service, monorepo-oss, cli-tool]
55
+ - `opt-in-domain/prompt-caching.md` — Anthropic prompt caching (breakpoint placement, TTL selection, pre-warming, verification) [archetypes: nextjs-saas, express-service, docker-service, monorepo-oss]
56
56
 
57
57
  ## Sync mechanism
58
58
 
59
- Consumer repos receive these files via `/bootstrap --sync-rules`. Re-running the command syncs every manifest category that has entries: universal entries always vendor, and archetype-tagged entries vendor only when the consumer repo's resolved archetype matches. Plugin-sourced files are overwritten while local rules are preserved (copy-on-write via the `<!-- source: session-orchestrator plugin ... -->` header on plugin files). Category entries must not share a basename, because all synced files flatten into `.claude/rules/`.
59
+ Consumer repos receive these files via `/bootstrap --sync-rules`. Re-running the command syncs every manifest category that has entries: universal entries always vendor, and archetype-tagged entries vendor only when the consumer repo's resolved archetype matches. Plugin-sourced files are overwritten while local rules are preserved (copy-on-write via the `<!-- source: session-orchestrator plugin ... -->` header on plugin files). That header stands on the first line directly after the closing `---` of a rule's frontmatter, because Claude Code reads frontmatter only when it opens on line 1; only a rule without frontmatter carries it on line 1 (#1449). A consumer copy written in the older format (header on line 1, frontmatter after it) still counts as plugin-owned and is rewritten into the current format on the next sync. Category entries must not share a basename, because all synced files flatten into `.claude/rules/`.
@@ -1,4 +1,3 @@
1
- <!-- source: session-orchestrator plugin (canonical: rules/always-on/bash-harness-pitfalls.md) -->
2
1
  ---
3
2
  globs:
4
3
  - "**/*.sh"
@@ -6,8 +5,15 @@ globs:
6
5
  - scripts/**
7
6
  - tests/**
8
7
  - .husky/**
8
+ paths:
9
+ - "**/*.sh"
10
+ - "**/*.bash"
11
+ - scripts/**
12
+ - tests/**
13
+ - .husky/**
9
14
  tier: wave-only
10
15
  ---
16
+ <!-- source: session-orchestrator plugin (canonical: rules/always-on/bash-harness-pitfalls.md) -->
11
17
  # Bash / Shell-Harness Pitfalls (Path-scoped)
12
18
 
13
19
  Six recurring bash-harness failure classes, each surfaced by a live run rather
@@ -1,11 +1,15 @@
1
- <!-- source: session-orchestrator plugin (canonical: rules/opt-in-domain/prompt-caching.md) -->
2
1
  ---
3
2
  globs:
4
3
  - "**/lib/ai/**"
5
4
  - "**/lib/llm/**"
6
5
  - "**/providers/**"
6
+ paths:
7
+ - "**/lib/ai/**"
8
+ - "**/lib/llm/**"
9
+ - "**/providers/**"
7
10
  tier: wave-only
8
11
  ---
12
+ <!-- source: session-orchestrator plugin (canonical: rules/opt-in-domain/prompt-caching.md) -->
9
13
  # Prompt Caching Rules (Path-scoped — repos using `@anthropic-ai/sdk` or `@ai-sdk/anthropic`)
10
14
 
11
15
  > Path-scoped — applies anywhere a project imports `@anthropic-ai/sdk` or uses the `@ai-sdk/anthropic` provider. Out of scope: `session-orchestrator` itself (no SDK use; `backend.md` § "AI Provider Abstraction" already forbids direct SDK imports in business logic, and the orchestrator runs inside Claude Code's harness which manages caching at the platform layer).
@@ -1,4 +1,3 @@
1
- <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend-data.md) -->
2
1
  ---
3
2
  globs:
4
3
  - src/lib/db/**
@@ -8,8 +7,17 @@ globs:
8
7
  - migrations/**
9
8
  - src/lib/redis/**
10
9
  - src/lib/queue/**
10
+ paths:
11
+ - src/lib/db/**
12
+ - src/lib/cache/**
13
+ - src/services/db/**
14
+ - supabase/**
15
+ - migrations/**
16
+ - src/lib/redis/**
17
+ - src/lib/queue/**
11
18
  tier: wave-only
12
19
  ---
20
+ <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend-data.md) -->
13
21
  # Backend Data Rules (Path-scoped)
14
22
 
15
23
  ## Database (Supabase)
@@ -1,4 +1,3 @@
1
- <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend.md) -->
2
1
  ---
3
2
  globs:
4
3
  - src/app/**
@@ -6,8 +5,15 @@ globs:
6
5
  - src/lib/api/**
7
6
  - src/routes/**
8
7
  - src/app/api/**
8
+ paths:
9
+ - src/app/**
10
+ - src/services/**
11
+ - src/lib/api/**
12
+ - src/routes/**
13
+ - src/app/api/**
9
14
  tier: wave-only
10
15
  ---
16
+ <!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend.md) -->
11
17
  # Backend Rules (Path-scoped)
12
18
 
13
19
  ## Server Actions (Next.js)