session-orchestrator 3.22.0 → 3.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) 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/repo-audit/SKILL.md +13 -0
  74. package/.cursor/skills/session-end/SKILL.md +13 -0
  75. package/.cursor/skills/session-plan/SKILL.md +13 -0
  76. package/.cursor/skills/session-start/SKILL.md +13 -0
  77. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  78. package/.cursor/skills/spinout/SKILL.md +12 -0
  79. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  80. package/.cursor/skills/test-runner/SKILL.md +13 -0
  81. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  82. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  83. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  84. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  85. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  86. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  87. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  88. package/.mcp.json +4 -1
  89. package/CHANGELOG.md +168 -0
  90. package/README.md +18 -15
  91. package/agents/AGENTS.md +23 -4
  92. package/agents/code-implementer.md +2 -1
  93. package/agents/db-specialist.md +2 -1
  94. package/agents/docs-writer.md +3 -1
  95. package/agents/eval-judge.md +1 -1
  96. package/agents/session-reviewer.md +7 -1
  97. package/agents/test-writer.md +2 -1
  98. package/agents/ui-developer.md +2 -1
  99. package/commands/bootstrap.md +2 -2
  100. package/commands/close.md +3 -1
  101. package/commands/go.md +1 -1
  102. package/commands/journey-audit.md +43 -0
  103. package/docs/USER-GUIDE.md +2 -2
  104. package/docs/ci-setup.md +14 -0
  105. package/docs/codex-setup.md +64 -0
  106. package/docs/components.md +6 -6
  107. package/docs/cursor-setup.md +26 -47
  108. package/docs/events-schema.md +76 -4
  109. package/docs/github-mirror-protection.md +197 -0
  110. package/docs/pi-setup.md +2 -0
  111. package/docs/rule-authoring.md +3 -1
  112. package/docs/scope-collision-guard.md +49 -2
  113. package/docs/session-config-reference.md +26 -4
  114. package/docs/session-config-template.md +4 -3
  115. package/docs/telemetry.md +22 -0
  116. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  117. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  118. package/hooks/enforce-scope.mjs +64 -0
  119. package/hooks/hooks-codex.json +1 -1
  120. package/hooks/hooks-cursor.json +201 -20
  121. package/hooks/hooks-pi.json +1 -1
  122. package/hooks/hooks.json +2 -2
  123. package/hooks/on-session-end.mjs +211 -10
  124. package/hooks/on-session-start.mjs +214 -11
  125. package/hooks/on-stop.mjs +48 -9
  126. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  127. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  128. package/hooks/pre-bash-issue-budget.mjs +117 -4
  129. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  130. package/hooks/pre-bash-staging-fence.mjs +4 -0
  131. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  132. package/hooks/skill-invocation-telemetry.mjs +21 -10
  133. package/monitors/monitors.json +6 -0
  134. package/package.json +1 -1
  135. package/pi/prompts/journey-audit.md +12 -0
  136. package/rules/_index.md +9 -1
  137. package/rules/always-on/ask-via-tool.md +62 -0
  138. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  139. package/rules/always-on/build-value.md +47 -0
  140. package/rules/always-on/cross-session-messaging.md +59 -0
  141. package/rules/always-on/loop-and-monitor.md +221 -0
  142. package/rules/always-on/parallel-sessions.md +142 -12
  143. package/rules/always-on/receiving-review.md +108 -0
  144. package/rules/always-on/test-value.md +40 -0
  145. package/rules/always-on/verification-before-completion.md +77 -0
  146. package/scripts/archive-closed-prds.mjs +258 -18
  147. package/scripts/autopilot.mjs +5 -0
  148. package/scripts/backfill-evidence-digest.mjs +376 -0
  149. package/scripts/cursor-install.mjs +89 -48
  150. package/scripts/export-hw-learnings.mjs +143 -2
  151. package/scripts/express-path.mjs +299 -0
  152. package/scripts/generate-cursor-adapter.mjs +253 -0
  153. package/scripts/github-protection-audit.mjs +358 -0
  154. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  155. package/scripts/lib/build-live-signals.mjs +24 -5
  156. package/scripts/lib/ci-status-banner.mjs +158 -11
  157. package/scripts/lib/command-blocker.mjs +70 -0
  158. package/scripts/lib/config/reconcile.mjs +79 -4
  159. package/scripts/lib/config/section-extractor.mjs +235 -36
  160. package/scripts/lib/config-schema.mjs +9 -1
  161. package/scripts/lib/config.mjs +57 -6
  162. package/scripts/lib/convergence-monitor.mjs +13 -2
  163. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  164. package/scripts/lib/dispatcher/cli.mjs +2 -2
  165. package/scripts/lib/express-path.mjs +327 -0
  166. package/scripts/lib/file-lock.mjs +22 -4
  167. package/scripts/lib/gates/gate-full.mjs +81 -8
  168. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  169. package/scripts/lib/git-config-drift.mjs +134 -5
  170. package/scripts/lib/host-identity.mjs +247 -2
  171. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  172. package/scripts/lib/issue-budget.mjs +229 -30
  173. package/scripts/lib/learnings/io.mjs +55 -10
  174. package/scripts/lib/learnings/schema.mjs +95 -28
  175. package/scripts/lib/lock-reaper.mjs +7 -1
  176. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  177. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  178. package/scripts/lib/memory-banner.mjs +5 -2
  179. package/scripts/lib/memory-paths.mjs +15 -6
  180. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  181. package/scripts/lib/platform.mjs +72 -9
  182. package/scripts/lib/plugin-root.mjs +143 -19
  183. package/scripts/lib/project-hygiene.mjs +43 -3
  184. package/scripts/lib/quality-gate.mjs +271 -13
  185. package/scripts/lib/reconcile/emitter.mjs +87 -19
  186. package/scripts/lib/reconcile/engine.mjs +281 -13
  187. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  188. package/scripts/lib/reconcile/renderer.mjs +148 -3
  189. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  190. package/scripts/lib/reconcile/writer.mjs +415 -84
  191. package/scripts/lib/rule-loader.mjs +37 -2
  192. package/scripts/lib/rules-sync.mjs +51 -8
  193. package/scripts/lib/scope-gate.mjs +90 -0
  194. package/scripts/lib/session-close-backfill.mjs +369 -28
  195. package/scripts/lib/session-discovery.mjs +13 -3
  196. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  197. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  198. package/scripts/lib/session-id.mjs +30 -14
  199. package/scripts/lib/session-identity/own-session.mjs +159 -0
  200. package/scripts/lib/session-lock.mjs +85 -30
  201. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  202. package/scripts/lib/session-schema/validator.mjs +40 -0
  203. package/scripts/lib/session-start-probes.mjs +608 -0
  204. package/scripts/lib/session-transition.mjs +277 -0
  205. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  206. package/scripts/lib/spiral-carryover.mjs +90 -9
  207. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  208. package/scripts/lib/state-md/mission-status.mjs +350 -52
  209. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  210. package/scripts/lib/state-md.mjs +12 -2
  211. package/scripts/lib/telemetry/sync.mjs +46 -8
  212. package/scripts/lib/validate/check-agents.mjs +66 -0
  213. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  214. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  215. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  216. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  217. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  218. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  219. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  220. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  221. package/scripts/lib/validate/repo-files.mjs +275 -0
  222. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  223. package/scripts/lib/vault-mirror/process.mjs +99 -43
  224. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  225. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  226. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  227. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  228. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  229. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  230. package/scripts/materialize-wave-scope.mjs +209 -12
  231. package/scripts/mcp-server.sh +11 -2
  232. package/scripts/parse-config.mjs +65 -0
  233. package/scripts/token-audit.sh +9 -2
  234. package/scripts/validate-plugin.mjs +3 -0
  235. package/scripts/validate-wave-scope.mjs +67 -0
  236. package/scripts/vault-mirror.mjs +203 -34
  237. package/skills/_shared/monitor-patterns.md +31 -5
  238. package/skills/_shared/parallel-aware-auq.md +1 -1
  239. package/skills/_shared/parallel-aware-preamble.md +4 -2
  240. package/skills/_shared/platform-tools.md +11 -5
  241. package/skills/_shared/state-ownership.md +29 -2
  242. package/skills/autopilot/SKILL.md +5 -1
  243. package/skills/bootstrap/SKILL.md +3 -3
  244. package/skills/bootstrap/_shared-template.md +18 -10
  245. package/skills/bootstrap/deep-template.md +10 -6
  246. package/skills/bootstrap/fast-template.md +15 -8
  247. package/skills/bootstrap/standard-template.md +10 -6
  248. package/skills/claude-md-drift-check/checker.mjs +39 -11
  249. package/skills/dispatcher/SKILL.md +1 -1
  250. package/skills/journey-audit/SKILL.md +269 -0
  251. package/skills/peekaboo-driver/SKILL.md +15 -3
  252. package/skills/persona-panel/SKILL.md +1 -1
  253. package/skills/reconcile/SKILL.md +41 -1
  254. package/skills/session-end/SKILL.md +17 -4
  255. package/skills/session-end/metrics-collection.md +7 -4
  256. package/skills/session-end/phase-3-6-tail.md +11 -3
  257. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  258. package/skills/session-plan/SKILL.md +6 -1
  259. package/skills/session-plan/wave-template.md +1 -0
  260. package/skills/session-start/SKILL.md +30 -16
  261. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  262. package/skills/session-start/phase-8-5-express-path.md +77 -12
  263. package/skills/vault-sync/validator.mjs +31 -0
  264. package/skills/wave-executor/SKILL.md +4 -2
  265. package/skills/wave-executor/circuit-breaker.md +34 -9
  266. package/skills/wave-executor/wave-loop.md +102 -19
  267. package/templates/_shared/journey-manifest.md +110 -0
  268. 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
  }
@@ -992,6 +992,90 @@ function literalScopeSuffix(entry) {
992
992
  return star === -1 ? null : entry.slice(star + 1);
993
993
  }
994
994
 
995
+ /**
996
+ * Segments of a scope entry, with the implicit recursion of a trailing `/` made
997
+ * EXPLICIT as a `**` segment (`tests/` → `['tests', '**']`).
998
+ *
999
+ * Load-bearing: {@link pathMatchesPattern} matches a `dir/` prefix with
1000
+ * `startsWith`, i.e. at ANY depth. Splitting `tests/` naively yields a trailing
1001
+ * EMPTY segment, which {@link globsDisagreeOnLiteralSegment} would then compare
1002
+ * against `lib` as if the entry ended there — turning `tests/` vs
1003
+ * `tests/lib/*.mjs` into a false "disjoint".
1004
+ *
1005
+ * @param {string} entry
1006
+ * @returns {string[]}
1007
+ */
1008
+ function scopeEntrySegments(entry) {
1009
+ const segs = entry.split('/');
1010
+ if (segs.length > 1 && segs[segs.length - 1] === '') segs[segs.length - 1] = '**';
1011
+ return segs;
1012
+ }
1013
+
1014
+ /**
1015
+ * Is this segment a PLAIN literal — one that can only match the byte-identical
1016
+ * path segment? Deliberately conservative: `*`, `**`, `?`, `{a,b}` and `[...]`
1017
+ * all count as "could match anything", and an empty segment does too.
1018
+ * @param {string} seg
1019
+ * @returns {boolean}
1020
+ */
1021
+ function isPlainLiteralSegment(seg) {
1022
+ return seg.length > 0 && !/[*?{}[\]]/.test(seg);
1023
+ }
1024
+
1025
+ /**
1026
+ * Do two glob entries carry PLAIN LITERAL segments that DISAGREE at an aligned
1027
+ * position? If so no path can satisfy both, and the pair is disjoint however
1028
+ * much literal prefix and suffix they share (#1130).
1029
+ *
1030
+ * Alignment is exact — and only within the two scans below. Every segment that
1031
+ * is not `**` consumes EXACTLY ONE path segment (`*` compiles to `[^/]*`, which
1032
+ * never crosses `/`; `?`/`{}`/`[]` are regex-ESCAPED by
1033
+ * {@link pathMatchesPattern}, so they are literal text). `**` is the only
1034
+ * variable-width segment, so each scan walks inward from one end and STOPS at
1035
+ * the first `**` on either side — past that wall, positions no longer
1036
+ * correspond. That is why the #1130 pair is decided from the TAIL:
1037
+ * `scripts/**\/reconcile/*.mjs` vs `scripts/**\/learnings/*.mjs` share the
1038
+ * `scripts` head and the `*.mjs` tail, and `reconcile` vs `learnings` is the
1039
+ * first aligned position inward from the tail.
1040
+ *
1041
+ * CEILING (BV-004) — literal-vs-literal ONLY. A `{a,b}`, `?` or `[...]` segment
1042
+ * is treated as "could match anything" and proves nothing, so
1043
+ * `scripts/**\/{reconcile,x}/*.mjs` vs `scripts/**\/learnings/*.mjs` still
1044
+ * reports a collision even though this dialect escapes braces into literal
1045
+ * text. That over-approximation is the SAFE direction for a collision gate (a
1046
+ * reported non-collision costs a coordinator one re-plan; a missed one is the
1047
+ * #1020 incident). Revisit only if {@link pathMatchesPattern} gains real brace
1048
+ * or char-class expansion — then those segments become enumerable and can be
1049
+ * compared set-wise instead of being waved through.
1050
+ *
1051
+ * @param {string} x
1052
+ * @param {string} y
1053
+ * @returns {boolean} true when the two entries provably cannot match one path
1054
+ */
1055
+ function globsDisagreeOnLiteralSegment(x, y) {
1056
+ const xs = scopeEntrySegments(x);
1057
+ const ys = scopeEntrySegments(y);
1058
+ const isWall = (seg) => seg.includes('**');
1059
+ const disagree = (a, b) =>
1060
+ isPlainLiteralSegment(a) && isPlainLiteralSegment(b) && a !== b;
1061
+
1062
+ // Scan inward from the HEAD, stopping at the first `**` wall.
1063
+ for (let k = 0; k < xs.length && k < ys.length; k++) {
1064
+ if (isWall(xs[k]) || isWall(ys[k])) break;
1065
+ if (disagree(xs[k], ys[k])) return true;
1066
+ }
1067
+ // Scan inward from the TAIL, same wall rule. Independent of the head scan:
1068
+ // either end can carry the deciding literal, and #1130's does not sit at the
1069
+ // head (both entries begin `scripts/**`).
1070
+ for (let k = 0; k < xs.length && k < ys.length; k++) {
1071
+ const xv = xs[xs.length - 1 - k];
1072
+ const yv = ys[ys.length - 1 - k];
1073
+ if (isWall(xv) || isWall(yv)) break;
1074
+ if (disagree(xv, yv)) return true;
1075
+ }
1076
+ return false;
1077
+ }
1078
+
995
1079
  /**
996
1080
  * Normalize the `agentScopes` input of {@link findScopeCollisions} into
997
1081
  * `{id, declaredId, files}` records. Never throws; malformed members are
@@ -1072,6 +1156,12 @@ function classifyEntryCollision(x, y, expand) {
1072
1156
  ) {
1073
1157
  return null;
1074
1158
  }
1159
+ // Prefix + suffix say nothing about the MIDDLE, so `scripts/**\/reconcile/
1160
+ // *.mjs` vs `scripts/**\/learnings/*.mjs` passed every check above and was
1161
+ // reported as a collision (#1130, reproduced 2026-08-24 @ f0766e1). One
1162
+ // aligned pair of DISAGREEING plain literals settles it — see the ceiling
1163
+ // note on the helper for what it deliberately does not decide.
1164
+ if (globsDisagreeOnLiteralSegment(x, y)) return null;
1075
1165
  return 'glob-prefix';
1076
1166
  }
1077
1167