session-orchestrator 3.22.0 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -220,6 +220,28 @@ const SCALAR_META_KEYS = new Set([
220
220
  * @param {string} contents - raw file contents
221
221
  * @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
222
222
  */
223
+ /**
224
+ * Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
225
+ *
226
+ * `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
227
+ * js-yaml, which picks SINGLE-quote style whenever the plain form would be
228
+ * ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
229
+ * leading/trailing-quote strip then returns `it''s` for `it's` — measured
230
+ * 2026-08-23 on 40 of 146 live learning insights carrying both characters.
231
+ * Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
232
+ * `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
233
+ */
234
+ function unquoteYamlScalar(valuePart) {
235
+ const v = valuePart.trim();
236
+ if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
237
+ return v.slice(1, -1).replace(/''/g, "'");
238
+ }
239
+ if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
240
+ return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
241
+ }
242
+ return v.replace(/^["']|["']$/g, '');
243
+ }
244
+
223
245
  export function parseGlobsFrontmatter(contents) {
224
246
  const match = FRONTMATTER_RE.exec(stripLeadingProvenanceHeader(contents));
225
247
  if (!match) return { globs: null, meta: {} };
@@ -299,7 +321,7 @@ export function parseGlobsFrontmatter(contents) {
299
321
  } else if (SCALAR_META_KEYS.has(key)) {
300
322
  // Scalar activation key (issue #694). Strip surrounding quotes, then
301
323
  // apply per-key coercion. Empty values are skipped (key not present).
302
- const stripped = valuePart.replace(/^["']|["']$/g, '');
324
+ const stripped = unquoteYamlScalar(valuePart);
303
325
  if (stripped === '') continue;
304
326
  if (key === 'alwaysApply' || key === 'auto-generated') {
305
327
  if (stripped === 'true') meta[key] = true;
@@ -468,7 +490,20 @@ export function loadApplicableRules({
468
490
  try {
469
491
  entries = readdirSync(rulesDir);
470
492
  } catch (err) {
471
- process.stderr.write(`[rule-loader] Cannot read rulesDir ${rulesDir}: ${err.message}\n`);
493
+ // Category separation, not channel muting (`.claude/rules/development.md`
494
+ // § Guard & Threshold Design). An absent `.claude/rules/` (ENOENT), or a
495
+ // plain file sitting where that directory would be (ENOTDIR), is the
496
+ // NORMAL state of every repo that has not adopted the rules layer — a
497
+ // legitimate empty corpus, not a diagnosable fault. Diagnosing it made
498
+ // every stderr guard around the callers unusable: `instruction-budget-
499
+ // guard.mjs` calls this function three times unconditionally, so ONE probe
500
+ // run emitted the same non-finding three times (#1132).
501
+ // EACCES/EPERM — and anything else — stay genuinely diagnosable and keep
502
+ // the line. Both branches still `return []`, so no caller's control flow
503
+ // changes; only the side effect is gated.
504
+ if (err?.code !== 'ENOENT' && err?.code !== 'ENOTDIR') {
505
+ process.stderr.write(`[rule-loader] Cannot read rulesDir ${rulesDir}: ${err.message}\n`);
506
+ }
472
507
  return [];
473
508
  }
474
509
 
@@ -9,7 +9,13 @@
9
9
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
10
  import { join, basename, dirname, resolve } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { validateRuleContent } from './validate-vendored-rules.mjs';
12
+ import { validateRuleContent, scanVendoringLeaks } from './validate-vendored-rules.mjs';
13
+
14
+ // The vendoring sanitizer (issue #1098) lives in validate-vendored-rules.mjs —
15
+ // it has that module's shape ("judge one rule file → findings") and its
16
+ // standalone CLI needs it too. Re-exported here so importers that predate the
17
+ // move (and this file's own tests) keep resolving it from rules-sync.mjs.
18
+ export { scanVendoringLeaks };
13
19
 
14
20
  // Exported (issue #722 Epic A Wave 2) for external consumers (e.g. tests).
15
21
  // NOT imported by validate-vendored-rules.mjs — that module imports
@@ -188,6 +194,19 @@ function escapeRegex(s) {
188
194
  * at least one concrete bullet entry. This keeps `/bootstrap --sync-rules`
189
195
  * ready for future opt-in categories without requiring CLI changes.
190
196
  *
197
+ * Vendoring sanitizer (issue #1098): every source file that reaches the write
198
+ * decision is additionally scanned by `scanVendoringLeaks()`, and its findings
199
+ * are collected into the additive `sanitizer[]` array. This runs in
200
+ * **report-only mode, always** — it never rewrites content, never skips a
201
+ * write, and never contributes to `errors[]`. That default is deliberate and
202
+ * matches the existing CLI contract: `rules-sync.mjs`'s exit code is driven by
203
+ * `errors[]` alone, so adding `sanitizer[]` extends the JSON envelope without
204
+ * changing any exit code or write decision an existing caller depends on.
205
+ * Silent stripping was rejected outright — it would change a rule's meaning at
206
+ * vendoring time, invisibly to author and consumer alike. There is no opt-out
207
+ * flag because the scan is read-only and free relative to the file read it
208
+ * piggybacks on.
209
+ *
191
210
  * @param {{
192
211
  * pluginRoot: string,
193
212
  * repoRoot: string,
@@ -202,7 +221,8 @@ function escapeRegex(s) {
202
221
  * skipped: Array<string|{file: string, reason: string}>,
203
222
  * preserved: string[],
204
223
  * errors: Array<{file: string, reason: string}>,
205
- * warnings: Array<{file: string, reason: string}>
224
+ * warnings: Array<{file: string, reason: string}>,
225
+ * sanitizer: Array<{file: string, line: number, kind: string, text: string}>
206
226
  * }}
207
227
  */
208
228
  export function syncRules({
@@ -219,20 +239,21 @@ export function syncRules({
219
239
  const preserved = [];
220
240
  const errors = [];
221
241
  const warnings = [];
242
+ const sanitizer = [];
222
243
 
223
244
  if (!pluginRoot || typeof pluginRoot !== 'string') {
224
245
  errors.push({ file: '_index.md', reason: 'pluginRoot not provided' });
225
- return { written, skipped, preserved, errors, warnings };
246
+ return { written, skipped, preserved, errors, warnings, sanitizer };
226
247
  }
227
248
  if (!repoRoot || typeof repoRoot !== 'string') {
228
249
  errors.push({ file: '_index.md', reason: 'repoRoot not provided' });
229
- return { written, skipped, preserved, errors, warnings };
250
+ return { written, skipped, preserved, errors, warnings, sanitizer };
230
251
  }
231
252
 
232
253
  const indexPath = join(pluginRoot, 'rules', '_index.md');
233
254
  if (!existsSync(indexPath)) {
234
255
  errors.push({ file: '_index.md', reason: `_index.md not found at ${indexPath}` });
235
- return { written, skipped, preserved, errors, warnings };
256
+ return { written, skipped, preserved, errors, warnings, sanitizer };
236
257
  }
237
258
 
238
259
  let indexContent;
@@ -240,7 +261,7 @@ export function syncRules({
240
261
  indexContent = readFileSync(indexPath, 'utf8');
241
262
  } catch (err) {
242
263
  errors.push({ file: '_index.md', reason: `failed to read _index.md: ${err.message}` });
243
- return { written, skipped, preserved, errors, warnings };
264
+ return { written, skipped, preserved, errors, warnings, sanitizer };
244
265
  }
245
266
 
246
267
  const selectedCategories = Array.isArray(categories)
@@ -248,6 +269,14 @@ export function syncRules({
248
269
  : listManifestCategories(indexContent);
249
270
  const entries = parseIndex(indexContent, selectedCategories);
250
271
 
272
+ // Resolvability for the See-Also sanitizer is judged against the FULL
273
+ // manifest, not `selectedCategories`: an archetype-scoped rule is a
274
+ // legitimate citation target in every repo whose archetype matches, so
275
+ // narrowing this to the current selection would report false leaks.
276
+ const manifestBasenames = new Set(
277
+ parseIndex(indexContent, listManifestCategories(indexContent)).map((e) => basename(e.relPath)),
278
+ );
279
+
251
280
  if (entries.length === 0) {
252
281
  // No sources resolved — could be empty categories or malformed index
253
282
  for (const cat of selectedCategories.length > 0 ? selectedCategories : ['<none>']) {
@@ -256,7 +285,7 @@ export function syncRules({
256
285
  reason: `no sources resolved for category '${cat}' in _index.md`,
257
286
  });
258
287
  }
259
- return { written, skipped, preserved, errors, warnings };
288
+ return { written, skipped, preserved, errors, warnings, sanitizer };
260
289
  }
261
290
 
262
291
  const targetDir = join(repoRoot, '.claude', 'rules');
@@ -304,6 +333,13 @@ export function syncRules({
304
333
  continue;
305
334
  }
306
335
 
336
+ // Vendoring sanitizer (issue #1098) — report-only, never mutates
337
+ // srcContent. Deliberately placed BEFORE the validation gate so a file
338
+ // that fails validation still reports its leaks in one pass.
339
+ sanitizer.push(
340
+ ...scanVendoringLeaks({ content: srcContent, relPath, pluginRoot, manifestBasenames }),
341
+ );
342
+
307
343
  // Pre-write validation gate (issue #722 Epic A Wave 2). Runs BEFORE the
308
344
  // existsSync(targetPath) write-decision branch below, so a validation
309
345
  // failure skips the write regardless of whether the target already
@@ -376,7 +412,7 @@ export function syncRules({
376
412
  }
377
413
  }
378
414
 
379
- return { written, skipped, preserved, errors, warnings };
415
+ return { written, skipped, preserved, errors, warnings, sanitizer };
380
416
  }
381
417
 
382
418
  // ── CLI ───────────────────────────────────────────────────────────────────────
@@ -432,6 +468,13 @@ if (isMain) {
432
468
 
433
469
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
434
470
 
471
+ // Vendoring sanitizer (issue #1098) — surfaced on stderr so the operator sees
472
+ // it without parsing the JSON envelope. Report-only by construction: it never
473
+ // enters errors[] and never changes the exit code below.
474
+ for (const f of result.sanitizer) {
475
+ process.stderr.write(`rules-sync: sanitizer ${f.kind} ${f.file}:${f.line} — ${f.text}\n`);
476
+ }
477
+
435
478
  if (result.errors.length > 0) {
436
479
  process.exit(1);
437
480
  }
@@ -918,6 +918,28 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
918
918
  );
919
919
  }
920
920
 
921
+ /**
922
+ * Prefix that marks an aggregate-sidecar record as a PEER SESSION's declared
923
+ * scope rather than one of this wave's own agents (#1195).
924
+ *
925
+ * SSOT: this constant lives here — in the module both the `--union` helper and
926
+ * `hooks/post-bash-write-verify.mjs` already import — so the union exclusion
927
+ * below and the hook's peer-write notice can never disagree about what a peer
928
+ * record IS.
929
+ */
930
+ export const PEER_RECORD_PREFIX = 'peer-session-';
931
+
932
+ /**
933
+ * Is this record id a PEER SESSION's record (see {@link PEER_RECORD_PREFIX})?
934
+ * Fail-closed: a non-string id is not a peer record.
935
+ *
936
+ * @param {unknown} id
937
+ * @returns {boolean}
938
+ */
939
+ export function isPeerRecordId(id) {
940
+ return typeof id === 'string' && id.startsWith(PEER_RECORD_PREFIX);
941
+ }
942
+
921
943
  /**
922
944
  * Merge many agents' declared file scopes into ONE deduplicated, order-stable
923
945
  * list — the mechanical form of "allowedPaths is the UNION of all agent file
@@ -943,6 +965,19 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
943
965
  * non-array / non-object members and non-string, empty entries are skipped.
944
966
  * Pure, sync, no I/O — hook-safe per the module header.
945
967
  *
968
+ * PEER RECORDS ARE EXCLUDED (#1195 follow-through). A record whose id starts
969
+ * with `peer-session-` declares a territory NO agent of this wave may write —
970
+ * it exists so a peer's paths take part in the DISJOINTNESS check and so
971
+ * `hooks/post-bash-write-verify.mjs` can name a peer write instead of alarming
972
+ * about it. Unioning it into `allowedPaths` would do the exact inverse: Gate 7
973
+ * (`hooks/enforce-scope.mjs`) would GRANT every agent of the wave write access
974
+ * to the peer's files, and the hook's peer branch would become dead code (a
975
+ * peer path can only reach it while it is OUTSIDE `allowedPaths`). The
976
+ * exclusion lives HERE — the one helper `--union` runs — rather than in the
977
+ * CLI, so every consumer of the union inherits it.
978
+ * `findScopeCollisions` deliberately does NOT filter: a peer/agent path
979
+ * collision is a real collision and must surface.
980
+ *
946
981
  * @param {Array<string[]|{id?: string, files?: string[]}>} scopes
947
982
  * @returns {string[]} deduplicated union in first-seen order
948
983
  */
@@ -954,6 +989,7 @@ export function unionFileScopes(scopes) {
954
989
  let files = null;
955
990
  if (Array.isArray(scope)) files = scope;
956
991
  else if (scope !== null && typeof scope === 'object' && Array.isArray(scope.files)) {
992
+ if (isPeerRecordId(scope.id)) continue;
957
993
  files = scope.files;
958
994
  }
959
995
  if (files === null) continue;
@@ -992,6 +1028,90 @@ function literalScopeSuffix(entry) {
992
1028
  return star === -1 ? null : entry.slice(star + 1);
993
1029
  }
994
1030
 
1031
+ /**
1032
+ * Segments of a scope entry, with the implicit recursion of a trailing `/` made
1033
+ * EXPLICIT as a `**` segment (`tests/` → `['tests', '**']`).
1034
+ *
1035
+ * Load-bearing: {@link pathMatchesPattern} matches a `dir/` prefix with
1036
+ * `startsWith`, i.e. at ANY depth. Splitting `tests/` naively yields a trailing
1037
+ * EMPTY segment, which {@link globsDisagreeOnLiteralSegment} would then compare
1038
+ * against `lib` as if the entry ended there — turning `tests/` vs
1039
+ * `tests/lib/*.mjs` into a false "disjoint".
1040
+ *
1041
+ * @param {string} entry
1042
+ * @returns {string[]}
1043
+ */
1044
+ function scopeEntrySegments(entry) {
1045
+ const segs = entry.split('/');
1046
+ if (segs.length > 1 && segs[segs.length - 1] === '') segs[segs.length - 1] = '**';
1047
+ return segs;
1048
+ }
1049
+
1050
+ /**
1051
+ * Is this segment a PLAIN literal — one that can only match the byte-identical
1052
+ * path segment? Deliberately conservative: `*`, `**`, `?`, `{a,b}` and `[...]`
1053
+ * all count as "could match anything", and an empty segment does too.
1054
+ * @param {string} seg
1055
+ * @returns {boolean}
1056
+ */
1057
+ function isPlainLiteralSegment(seg) {
1058
+ return seg.length > 0 && !/[*?{}[\]]/.test(seg);
1059
+ }
1060
+
1061
+ /**
1062
+ * Do two glob entries carry PLAIN LITERAL segments that DISAGREE at an aligned
1063
+ * position? If so no path can satisfy both, and the pair is disjoint however
1064
+ * much literal prefix and suffix they share (#1130).
1065
+ *
1066
+ * Alignment is exact — and only within the two scans below. Every segment that
1067
+ * is not `**` consumes EXACTLY ONE path segment (`*` compiles to `[^/]*`, which
1068
+ * never crosses `/`; `?`/`{}`/`[]` are regex-ESCAPED by
1069
+ * {@link pathMatchesPattern}, so they are literal text). `**` is the only
1070
+ * variable-width segment, so each scan walks inward from one end and STOPS at
1071
+ * the first `**` on either side — past that wall, positions no longer
1072
+ * correspond. That is why the #1130 pair is decided from the TAIL:
1073
+ * `scripts/**\/reconcile/*.mjs` vs `scripts/**\/learnings/*.mjs` share the
1074
+ * `scripts` head and the `*.mjs` tail, and `reconcile` vs `learnings` is the
1075
+ * first aligned position inward from the tail.
1076
+ *
1077
+ * CEILING (BV-004) — literal-vs-literal ONLY. A `{a,b}`, `?` or `[...]` segment
1078
+ * is treated as "could match anything" and proves nothing, so
1079
+ * `scripts/**\/{reconcile,x}/*.mjs` vs `scripts/**\/learnings/*.mjs` still
1080
+ * reports a collision even though this dialect escapes braces into literal
1081
+ * text. That over-approximation is the SAFE direction for a collision gate (a
1082
+ * reported non-collision costs a coordinator one re-plan; a missed one is the
1083
+ * #1020 incident). Revisit only if {@link pathMatchesPattern} gains real brace
1084
+ * or char-class expansion — then those segments become enumerable and can be
1085
+ * compared set-wise instead of being waved through.
1086
+ *
1087
+ * @param {string} x
1088
+ * @param {string} y
1089
+ * @returns {boolean} true when the two entries provably cannot match one path
1090
+ */
1091
+ function globsDisagreeOnLiteralSegment(x, y) {
1092
+ const xs = scopeEntrySegments(x);
1093
+ const ys = scopeEntrySegments(y);
1094
+ const isWall = (seg) => seg.includes('**');
1095
+ const disagree = (a, b) =>
1096
+ isPlainLiteralSegment(a) && isPlainLiteralSegment(b) && a !== b;
1097
+
1098
+ // Scan inward from the HEAD, stopping at the first `**` wall.
1099
+ for (let k = 0; k < xs.length && k < ys.length; k++) {
1100
+ if (isWall(xs[k]) || isWall(ys[k])) break;
1101
+ if (disagree(xs[k], ys[k])) return true;
1102
+ }
1103
+ // Scan inward from the TAIL, same wall rule. Independent of the head scan:
1104
+ // either end can carry the deciding literal, and #1130's does not sit at the
1105
+ // head (both entries begin `scripts/**`).
1106
+ for (let k = 0; k < xs.length && k < ys.length; k++) {
1107
+ const xv = xs[xs.length - 1 - k];
1108
+ const yv = ys[ys.length - 1 - k];
1109
+ if (isWall(xv) || isWall(yv)) break;
1110
+ if (disagree(xv, yv)) return true;
1111
+ }
1112
+ return false;
1113
+ }
1114
+
995
1115
  /**
996
1116
  * Normalize the `agentScopes` input of {@link findScopeCollisions} into
997
1117
  * `{id, declaredId, files}` records. Never throws; malformed members are
@@ -1072,6 +1192,12 @@ function classifyEntryCollision(x, y, expand) {
1072
1192
  ) {
1073
1193
  return null;
1074
1194
  }
1195
+ // Prefix + suffix say nothing about the MIDDLE, so `scripts/**\/reconcile/
1196
+ // *.mjs` vs `scripts/**\/learnings/*.mjs` passed every check above and was
1197
+ // reported as a collision (#1130, reproduced 2026-08-24 @ f0766e1). One
1198
+ // aligned pair of DISAGREEING plain literals settles it — see the ceiling
1199
+ // note on the helper for what it deliberately does not decide.
1200
+ if (globsDisagreeOnLiteralSegment(x, y)) return null;
1075
1201
  return 'glob-prefix';
1076
1202
  }
1077
1203