@lovrozagar/grunt 0.10.1 → 0.11.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 (120) hide show
  1. package/.agents/agents/orchestrator.md +1 -1
  2. package/.agents/hooks.json +2 -2
  3. package/.agents/skills/browser/SKILL.md +3 -3
  4. package/.agents/skills/clasp/SKILL.md +1 -1
  5. package/.agents/skills/google-workspace/SKILL.md +3 -3
  6. package/.agents/skills/handoff/SKILL.md +2 -2
  7. package/.agents/skills/implement-plan/SKILL.md +2 -2
  8. package/.agents/skills/jev/SKILL.md +3 -3
  9. package/.agents/skills/listen/SKILL.md +3 -3
  10. package/.agents/skills/pickup/SKILL.md +1 -1
  11. package/.agents/skills/speak/SKILL.md +3 -3
  12. package/.agents/skills/tmp/SKILL.md +2 -2
  13. package/.agents/skills/write-plan/SKILL.md +4 -2
  14. package/.claude/agents/orchestrator.md +1 -1
  15. package/.claude/settings.json +3 -3
  16. package/.claude/skills/browser/SKILL.md +3 -3
  17. package/.claude/skills/clasp/SKILL.md +1 -1
  18. package/.claude/skills/google-workspace/SKILL.md +3 -3
  19. package/.claude/skills/handoff/SKILL.md +2 -2
  20. package/.claude/skills/implement-plan/SKILL.md +2 -2
  21. package/.claude/skills/jev/SKILL.md +3 -3
  22. package/.claude/skills/listen/SKILL.md +3 -3
  23. package/.claude/skills/pickup/SKILL.md +1 -1
  24. package/.claude/skills/speak/SKILL.md +3 -3
  25. package/.claude/skills/tmp/SKILL.md +2 -2
  26. package/.claude/skills/write-plan/SKILL.md +4 -2
  27. package/.codex/agents/orchestrator.toml +1 -1
  28. package/.codex/hooks.json +3 -3
  29. package/.grok/agents/orchestrator.md +1 -1
  30. package/.grok/hooks/orchestrate-parent.js +9 -9
  31. package/.grok/skills/browser/SKILL.md +3 -3
  32. package/.grok/skills/clasp/SKILL.md +1 -1
  33. package/.grok/skills/google-workspace/SKILL.md +3 -3
  34. package/.grok/skills/handoff/SKILL.md +2 -2
  35. package/.grok/skills/implement-plan/SKILL.md +2 -2
  36. package/.grok/skills/jev/SKILL.md +3 -3
  37. package/.grok/skills/listen/SKILL.md +3 -3
  38. package/.grok/skills/pickup/SKILL.md +1 -1
  39. package/.grok/skills/speak/SKILL.md +3 -3
  40. package/.grok/skills/tmp/SKILL.md +2 -2
  41. package/.grok/skills/write-plan/SKILL.md +4 -2
  42. package/.rulesync/hooks.jsonc +3 -3
  43. package/.rulesync/reference/INDEX.md +9 -9
  44. package/.rulesync/reference/browser.md +4 -4
  45. package/.rulesync/reference/cascade.md +2 -2
  46. package/.rulesync/reference/clasp.md +1 -1
  47. package/.rulesync/reference/google-workspace.md +29 -29
  48. package/.rulesync/reference/hooks.md +3 -3
  49. package/.rulesync/reference/implementation-format.md +2 -2
  50. package/.rulesync/reference/jev.md +3 -3
  51. package/.rulesync/reference/law.md +1 -1
  52. package/.rulesync/reference/listen.md +12 -12
  53. package/.rulesync/reference/map.md +25 -25
  54. package/.rulesync/reference/refs-map.md +3 -3
  55. package/.rulesync/reference/rtk.md +1 -1
  56. package/.rulesync/reference/scope.md +12 -14
  57. package/.rulesync/reference/setup.md +7 -7
  58. package/.rulesync/reference/skills-map.md +5 -5
  59. package/.rulesync/reference/speak.md +6 -6
  60. package/.rulesync/rules/overview.md +5 -5
  61. package/{scripts → .rulesync/scripts}/doctor.mjs +2 -2
  62. package/{scripts → .rulesync/scripts}/feature-packs.mjs +1 -1
  63. package/{scripts → .rulesync/scripts}/folder-map.mjs +7 -3
  64. package/{scripts → .rulesync/scripts}/gate-fat-tools.mjs +3 -3
  65. package/{scripts → .rulesync/scripts}/google-workspace.mjs +2 -2
  66. package/{scripts → .rulesync/scripts}/grunt-job.mjs +2 -0
  67. package/{scripts → .rulesync/scripts}/guarded-md.mjs +10 -4
  68. package/{scripts → .rulesync/scripts}/hooks-union.mjs +13 -8
  69. package/{scripts → .rulesync/scripts}/jev.mjs +2 -2
  70. package/{scripts → .rulesync/scripts}/listen.mjs +6 -6
  71. package/{scripts → .rulesync/scripts}/pipeline.mjs +14 -14
  72. package/{scripts → .rulesync/scripts}/session-map.mjs +1 -1
  73. package/{scripts → .rulesync/scripts}/setup.mjs +4 -4
  74. package/{scripts → .rulesync/scripts}/speak.mjs +4 -4
  75. package/.rulesync/skills/browser/SKILL.md +3 -3
  76. package/.rulesync/skills/clasp/SKILL.md +1 -1
  77. package/.rulesync/skills/google-workspace/SKILL.md +3 -3
  78. package/.rulesync/skills/handoff/SKILL.md +2 -2
  79. package/.rulesync/skills/implement-plan/SKILL.md +2 -2
  80. package/.rulesync/skills/jev/SKILL.md +3 -3
  81. package/.rulesync/skills/listen/SKILL.md +3 -3
  82. package/.rulesync/skills/pickup/SKILL.md +1 -1
  83. package/.rulesync/skills/speak/SKILL.md +3 -3
  84. package/.rulesync/skills/tmp/SKILL.md +2 -2
  85. package/.rulesync/skills/write-plan/SKILL.md +4 -2
  86. package/.rulesync/subagents/orchestrator.md +1 -1
  87. package/AGENTS.md +7 -5
  88. package/CHANGELOG.md +21 -3
  89. package/CLAUDE.md +2 -0
  90. package/README.md +23 -23
  91. package/cli/grunt.mjs +3 -3
  92. package/cli/grunt.test.ts +6 -6
  93. package/cli/init.mjs +64 -16
  94. package/cli/init.test.ts +310 -163
  95. package/cli/prompt.mjs +1 -1
  96. package/cli/self-update.mjs +1 -1
  97. package/package.json +15 -50
  98. /package/{scripts → .rulesync/scripts}/board.mjs +0 -0
  99. /package/{scripts → .rulesync/scripts}/browser.mjs +0 -0
  100. /package/{scripts → .rulesync/scripts}/check-globals.mjs +0 -0
  101. /package/{scripts → .rulesync/scripts}/emit-agent-shell-tools.mjs +0 -0
  102. /package/{scripts → .rulesync/scripts}/emit-gemini.mjs +0 -0
  103. /package/{scripts → .rulesync/scripts}/emit-maps.mjs +0 -0
  104. /package/{scripts → .rulesync/scripts}/emit-mcp-policy.mjs +0 -0
  105. /package/{scripts → .rulesync/scripts}/guarded-roots.mjs +0 -0
  106. /package/{scripts → .rulesync/scripts}/interactive.mjs +0 -0
  107. /package/{scripts → .rulesync/scripts}/jsonc.mjs +0 -0
  108. /package/{scripts → .rulesync/scripts}/package-manager.mjs +0 -0
  109. /package/{scripts → .rulesync/scripts}/parse-need.mjs +0 -0
  110. /package/{scripts → .rulesync/scripts}/persist-handoff.mjs +0 -0
  111. /package/{scripts → .rulesync/scripts}/persist-implementation.mjs +0 -0
  112. /package/{scripts → .rulesync/scripts}/persist-plan.mjs +0 -0
  113. /package/{scripts → .rulesync/scripts}/persist-tmp.mjs +0 -0
  114. /package/{scripts → .rulesync/scripts}/prompt.mjs +0 -0
  115. /package/{scripts → .rulesync/scripts}/purge-global-mcps.mjs +0 -0
  116. /package/{scripts → .rulesync/scripts}/scrub-spawn-prompt.mjs +0 -0
  117. /package/{scripts → .rulesync/scripts}/scrub-text +0 -0
  118. /package/{scripts → .rulesync/scripts}/scrub-text-lib.mjs +0 -0
  119. /package/{scripts → .rulesync/scripts}/skill-conflicts.mjs +0 -0
  120. /package/{scripts → .rulesync/scripts}/sync-global-settings.mjs +0 -0
package/cli/init.test.ts CHANGED
@@ -8,8 +8,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
8
8
  const emitMaps = vi.hoisted(() => vi.fn());
9
9
  const actualEmitMaps = vi.hoisted(() => ({ fn: null as null | ((...args: unknown[]) => unknown) }));
10
10
 
11
- vi.mock("../scripts/emit-maps.mjs", async (importOriginal) => {
12
- const actual = await importOriginal<typeof import("../scripts/emit-maps.mjs")>();
11
+ vi.mock("../.rulesync/scripts/emit-maps.mjs", async (importOriginal) => {
12
+ const actual = await importOriginal<typeof import("../.rulesync/scripts/emit-maps.mjs")>();
13
13
  actualEmitMaps.fn = actual.emitMaps;
14
14
  emitMaps.mockImplementation((...args: unknown[]) =>
15
15
  actual.emitMaps(...(args as Parameters<typeof actual.emitMaps>)),
@@ -19,6 +19,10 @@ vi.mock("../scripts/emit-maps.mjs", async (importOriginal) => {
19
19
  import {
20
20
  MAX_GUARDED_MARKDOWN_BYTES,
21
21
  PRODUCT_SCRIPTS,
22
+ SCRIPTS_REL,
23
+ LEGACY_SCRIPTS_REL,
24
+ pruneLegacyScripts,
25
+ rewriteLegacyScriptRefs,
22
26
  composeGuardedMarkdown,
23
27
  copyTree,
24
28
  destAlreadyInited,
@@ -61,9 +65,9 @@ import {
61
65
  withGuardedCheckInteriors,
62
66
  writeMergedGuardedFile,
63
67
  } from "./init.mjs";
64
- import { UNKNOWN_PACKAGE_MANAGER } from "../scripts/package-manager.mjs";
65
- import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
66
- import { COMMANDS as PIPELINE_COMMANDS } from "../scripts/pipeline.mjs";
68
+ import { UNKNOWN_PACKAGE_MANAGER } from "../.rulesync/scripts/package-manager.mjs";
69
+ import { runGuardedRoots } from "../.rulesync/scripts/guarded-roots.mjs";
70
+ import { COMMANDS as PIPELINE_COMMANDS } from "../.rulesync/scripts/pipeline.mjs";
67
71
 
68
72
  const tmpDirs: string[] = [];
69
73
  afterEach(() => {
@@ -97,7 +101,7 @@ const SRC_CLAUDE_SETTINGS = {
97
101
  hooks: [
98
102
  {
99
103
  type: "command",
100
- command: 'node "${ROOT}/scripts/session-map.mjs"',
104
+ command: 'node "${ROOT}/.rulesync/scripts/session-map.mjs"',
101
105
  timeout: 5,
102
106
  },
103
107
  ],
@@ -109,7 +113,7 @@ const SRC_CLAUDE_SETTINGS = {
109
113
  hooks: [
110
114
  {
111
115
  type: "command",
112
- command: 'node "${ROOT}/scripts/scrub-spawn-prompt.mjs"',
116
+ command: 'node "${ROOT}/.rulesync/scripts/scrub-spawn-prompt.mjs"',
113
117
  },
114
118
  ],
115
119
  },
@@ -118,7 +122,7 @@ const SRC_CLAUDE_SETTINGS = {
118
122
  hooks: [
119
123
  {
120
124
  type: "command",
121
- command: 'node "${ROOT}/scripts/gate-fat-tools.mjs"',
125
+ command: 'node "${ROOT}/.rulesync/scripts/gate-fat-tools.mjs"',
122
126
  },
123
127
  ],
124
128
  },
@@ -166,12 +170,12 @@ function stubPkgRoot(pkg: Record<string, unknown> = {
166
170
  for (const f of GUARDED_MD_FILES) {
167
171
  fs.writeFileSync(path.join(root, f), `${f} content`);
168
172
  }
169
- fs.mkdirSync(path.join(root, "scripts"));
173
+ fs.mkdirSync(path.join(root, ".rulesync", "scripts"));
170
174
  for (const name of PRODUCT_FILES) {
171
- fs.writeFileSync(path.join(root, "scripts", name), name);
175
+ fs.writeFileSync(path.join(root, ".rulesync", "scripts", name), name);
172
176
  }
173
- fs.mkdirSync(path.join(root, "scripts", "scrub-text"));
174
- fs.writeFileSync(path.join(root, "scripts", "scrub-text", "inside"), "dir");
177
+ fs.mkdirSync(path.join(root, ".rulesync", "scripts", "scrub-text"));
178
+ fs.writeFileSync(path.join(root, ".rulesync", "scripts", "scrub-text", "inside"), "dir");
175
179
  fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
176
180
  return root;
177
181
  }
@@ -264,7 +268,7 @@ describe("mergeGuardedMarkdown", () => {
264
268
  const dest = tmp("md-absent-");
265
269
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
266
270
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
267
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
271
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n",
268
272
  );
269
273
  });
270
274
 
@@ -274,7 +278,7 @@ describe("mergeGuardedMarkdown", () => {
274
278
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
275
279
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
276
280
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
277
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
281
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nhand-written consumer doc\n",
278
282
  );
279
283
  expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
280
284
  });
@@ -285,7 +289,7 @@ describe("mergeGuardedMarkdown", () => {
285
289
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
286
290
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md", { alreadyInited: true });
287
291
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
288
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
292
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nhand-written consumer doc\n",
289
293
  );
290
294
  expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
291
295
  });
@@ -296,7 +300,7 @@ describe("mergeGuardedMarkdown", () => {
296
300
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "");
297
301
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
298
302
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
299
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
303
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n",
300
304
  );
301
305
  });
302
306
 
@@ -309,7 +313,7 @@ describe("mergeGuardedMarkdown", () => {
309
313
  );
310
314
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
311
315
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
312
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nbefore\nafter\n",
316
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nbefore\nafter\n",
313
317
  );
314
318
  expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
315
319
  });
@@ -324,7 +328,7 @@ describe("mergeGuardedMarkdown", () => {
324
328
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
325
329
  const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
326
330
  expect(out).toBe(
327
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nkeep-a\nkeep-b\n",
331
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nkeep-a\nkeep-b\n",
328
332
  );
329
333
  expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
330
334
  });
@@ -336,7 +340,7 @@ describe("mergeGuardedMarkdown", () => {
336
340
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
337
341
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
338
342
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
339
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
343
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nhand-written consumer doc\n",
340
344
  );
341
345
  });
342
346
 
@@ -349,7 +353,7 @@ describe("mergeGuardedMarkdown", () => {
349
353
  const dest = tmp("md-wrapped-src-");
350
354
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
351
355
  const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
352
- expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
356
+ expect(out).toBe("<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n");
353
357
  expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
354
358
  expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
355
359
  });
@@ -363,7 +367,7 @@ describe("mergeGuardedMarkdown", () => {
363
367
  );
364
368
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
365
369
  const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
366
- expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
370
+ expect(out).toBe("<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n");
367
371
  expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
368
372
  });
369
373
 
@@ -380,7 +384,7 @@ describe("mergeGuardedMarkdown", () => {
380
384
  );
381
385
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
382
386
  const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
383
- expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
387
+ expect(out).toBe("<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n");
384
388
  expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
385
389
  expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
386
390
  });
@@ -395,7 +399,7 @@ describe("mergeGuardedMarkdown", () => {
395
399
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
396
400
  const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
397
401
  expect(out).toBe(
398
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nprefix\nsuffix\n",
402
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nprefix\nsuffix\n",
399
403
  );
400
404
  expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
401
405
  expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
@@ -408,7 +412,7 @@ describe("mergeGuardedMarkdown", () => {
408
412
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), user);
409
413
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
410
414
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
411
- `<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n${user}`,
415
+ `<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\n${user}`,
412
416
  );
413
417
  });
414
418
 
@@ -418,7 +422,7 @@ describe("mergeGuardedMarkdown", () => {
418
422
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "user line\r\n");
419
423
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
420
424
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
421
- "<!-- grunt:begin -->\r\nCLAUDE.md content\r\n<!-- grunt:end -->\r\nuser line\r\n",
425
+ "<!-- grunt:begin -->\r\n\r\nCLAUDE.md content\r\n\r\n<!-- grunt:end -->\r\n\r\nuser line\r\n",
422
426
  );
423
427
  });
424
428
 
@@ -428,7 +432,7 @@ describe("mergeGuardedMarkdown", () => {
428
432
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "CLAUDE.md content\n");
429
433
  mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
430
434
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
431
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
435
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n",
432
436
  );
433
437
  });
434
438
 
@@ -449,7 +453,7 @@ describe("mergeGuardedMarkdown", () => {
449
453
  mergeGuardedMarkdown(dest, pkgRoot, "AGENTS.md");
450
454
  expect(fs.readFileSync(path.join(dest, "pkg", "AGENTS.md"), "utf8")).toBe("nested user\n");
451
455
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
452
- "<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\n",
456
+ "<!-- grunt:begin -->\n\nAGENTS.md content\n\n<!-- grunt:end -->\n",
453
457
  );
454
458
  });
455
459
  });
@@ -467,10 +471,10 @@ describe("snapshot/remerge guarded roots", () => {
467
471
  fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
468
472
  remergeGuardedRoots(dest, snap);
469
473
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
470
- "<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\nconsumer agents\n",
474
+ "<!-- grunt:begin -->\n\ngenerated agents\n\n<!-- grunt:end -->\n\nconsumer agents\n",
471
475
  );
472
476
  expect(fs.readFileSync(path.join(dest, "GEMINI.md"), "utf8")).toBe(
473
- "<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\nuser gemini\n",
477
+ "<!-- grunt:begin -->\n\n@AGENTS.md\n\n<!-- grunt:end -->\n\nuser gemini\n",
474
478
  );
475
479
  });
476
480
 
@@ -486,7 +490,7 @@ describe("snapshot/remerge guarded roots", () => {
486
490
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
487
491
  remergeGuardedRoots(dest, snap);
488
492
  const out = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
489
- expect(out).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
493
+ expect(out).toBe("<!-- grunt:begin -->\n\ngenerated agents\n\n<!-- grunt:end -->\n");
490
494
  expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
491
495
  expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
492
496
  });
@@ -521,12 +525,12 @@ describe("snapshot/remerge guarded roots", () => {
521
525
  "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
522
526
  "<!-- grunt:end -->\n",
523
527
  ),
524
- ).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
528
+ ).toBe("<!-- grunt:begin -->\n\nbody\n\n<!-- grunt:end -->\n");
525
529
  expect(composeGuardedMarkdown("body", null)).toBe(
526
- "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
530
+ "<!-- grunt:begin -->\n\nbody\n\n<!-- grunt:end -->\n",
527
531
  );
528
532
  expect(composeGuardedMarkdown("body", "\n\n")).toBe(
529
- "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
533
+ "<!-- grunt:begin -->\n\nbody\n\n<!-- grunt:end -->\n",
530
534
  );
531
535
  expect(extractGruntBody("no markers")).toBeNull();
532
536
  expect(
@@ -535,26 +539,26 @@ describe("snapshot/remerge guarded roots", () => {
535
539
  ),
536
540
  ).toBe("a\nb");
537
541
  expect(mergeGuardedContent(null, "g")).toBe(
538
- "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
542
+ "<!-- grunt:begin -->\n\ng\n\n<!-- grunt:end -->\n",
539
543
  );
540
544
  expect(mergeGuardedContent("", "a\r\nb")).toBe(
541
- "<!-- grunt:begin -->\r\na\r\nb\r\n<!-- grunt:end -->\r\n",
545
+ "<!-- grunt:begin -->\r\n\r\na\r\nb\r\n\r\n<!-- grunt:end -->\r\n",
542
546
  );
543
547
  expect(composeGuardedMarkdown("b", "\r\n")).toBe(
544
- "<!-- grunt:begin -->\nb\n<!-- grunt:end -->\n",
548
+ "<!-- grunt:begin -->\n\nb\n\n<!-- grunt:end -->\n",
545
549
  );
546
550
  expect(mergeGuardedContent("keep\n", "g\r\n")).toBe(
547
- "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\nkeep\n",
551
+ "<!-- grunt:begin -->\n\ng\n\n<!-- grunt:end -->\n\nkeep\n",
548
552
  );
549
553
  expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toBe(
550
- "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
554
+ "<!-- grunt:begin -->\n\ng\n\n<!-- grunt:end -->\n",
551
555
  );
552
556
  const dest = tmp("md-helpers-");
553
557
  const missing = path.join(dest, "nope.md");
554
558
  expect(guardedMarkdownDrift(missing, "g")).toBe(true);
555
559
  writeMergedGuardedFile(missing, "g");
556
560
  expect(fs.readFileSync(missing, "utf8")).toBe(
557
- "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
561
+ "<!-- grunt:begin -->\n\ng\n\n<!-- grunt:end -->\n",
558
562
  );
559
563
  expect(guardedMarkdownDrift(missing, "g")).toBe(false);
560
564
  expect(guardedMarkdownDrift(missing, "other")).toBe(true);
@@ -569,15 +573,30 @@ describe("snapshot/remerge guarded roots", () => {
569
573
  expect(writeMergedGuardedFile(huge, "g")).toBe("aborted-unsafe");
570
574
  });
571
575
 
576
+ it("formatter-padded sentinels are a fixed point and do not drift", () => {
577
+ const padded = "<!-- grunt:begin -->\n\n@AGENTS.md\n\n<!-- grunt:end -->\n";
578
+ const withUser = "<!-- grunt:begin -->\n\nbody\n\n<!-- grunt:end -->\n\n# Mine\n";
579
+ expect(extractGruntBody(padded)).toBe("@AGENTS.md");
580
+ expect(mergeGuardedContent(padded, "@AGENTS.md\n")).toBe(padded);
581
+ expect(mergeGuardedContent(withUser, "body")).toBe(withUser);
582
+ expect(extractUserMarkdown(withUser)).toBe("\n# Mine\n");
583
+ expect(
584
+ mergeGuardedContent("<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\nuser\n", "@AGENTS.md"),
585
+ ).toBe("<!-- grunt:begin -->\n\n@AGENTS.md\n\n<!-- grunt:end -->\n\nuser\n");
586
+ const file = path.join(tmp("md-padded-"), "GEMINI.md");
587
+ fs.writeFileSync(file, padded);
588
+ expect(guardedMarkdownDrift(file, "@AGENTS.md")).toBe(false);
589
+ });
590
+
572
591
  it("unclosed begin unwraps interior; inline orphan end is stripped", () => {
573
592
  expect(extractGruntBody("<!-- grunt:begin -->\nhello")).toBe("hello");
574
593
  const composed = composeGuardedMarkdown("<!-- grunt:begin -->\nhello", "");
575
- expect(composed).toBe("<!-- grunt:begin -->\nhello\n<!-- grunt:end -->\n");
594
+ expect(composed).toBe("<!-- grunt:begin -->\n\nhello\n\n<!-- grunt:end -->\n");
576
595
  expect(composed.split(SENTINEL_BEGIN)).toHaveLength(2);
577
596
  expect(composed.split(SENTINEL_END)).toHaveLength(2);
578
597
  expect(composed).not.toContain(`${SENTINEL_BEGIN}\n${SENTINEL_BEGIN}`);
579
598
  expect(mergeGuardedContent("note\n<!-- grunt:begin -->\nstale", "new")).toBe(
580
- "<!-- grunt:begin -->\nnew\n<!-- grunt:end -->\nnote\n",
599
+ "<!-- grunt:begin -->\n\nnew\n\n<!-- grunt:end -->\n\nnote\n",
581
600
  );
582
601
  expect(extractUserMarkdown("keep <!-- grunt:end --> me")).not.toContain(SENTINEL_END);
583
602
  expect(extractUserMarkdown("keep <!-- grunt:end --> me")).toBe("keep me");
@@ -594,7 +613,7 @@ describe("snapshot/remerge guarded roots", () => {
594
613
  expect(extractUserMarkdown(mixed)).toBe("user bit\n");
595
614
  expect(
596
615
  composeGuardedMarkdown("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n", ""),
597
- ).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
616
+ ).toBe("<!-- grunt:begin -->\n\nbody\n\n<!-- grunt:end -->\n");
598
617
  });
599
618
 
600
619
  it("extract/compose peel stacked dest and trailing orphan end", () => {
@@ -602,18 +621,18 @@ describe("snapshot/remerge guarded roots", () => {
602
621
  "<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\n";
603
622
  expect(extractGruntBody(stacked)).toBe("BODY");
604
623
  const stackedOut = composeGuardedMarkdown(stacked, "");
605
- expect(stackedOut).toBe("<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n");
624
+ expect(stackedOut).toBe("<!-- grunt:begin -->\n\nBODY\n\n<!-- grunt:end -->\n");
606
625
  expect(stackedOut.split(SENTINEL_BEGIN)).toHaveLength(2);
607
626
  expect(stackedOut.split(SENTINEL_END)).toHaveLength(2);
608
627
  expect(composeGuardedMarkdown("BODY\n<!-- grunt:end -->\n", "")).toBe(
609
- "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n",
628
+ "<!-- grunt:begin -->\n\nBODY\n\n<!-- grunt:end -->\n",
610
629
  );
611
630
  expect(extractGruntBody("BODY\n\n<!-- grunt:end -->")).toBe("BODY");
612
631
  expect(mergeGuardedContent(stacked, "SRC")).toBe(
613
- "<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\n",
632
+ "<!-- grunt:begin -->\n\nSRC\n\n<!-- grunt:end -->\n",
614
633
  );
615
634
  expect(mergeGuardedContent("keep\n<!-- grunt:end -->\n", "SRC")).toBe(
616
- "<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\nkeep\n",
635
+ "<!-- grunt:begin -->\n\nSRC\n\n<!-- grunt:end -->\n\nkeep\n",
617
636
  );
618
637
  expect(
619
638
  extractGruntBody("<!-- grunt:begin --> \nBODY\n<!-- grunt:end --> \n"),
@@ -632,7 +651,7 @@ describe("snapshot/remerge guarded roots", () => {
632
651
  expect(extractGruntBody(midLine)).toBe("BODY");
633
652
  expect(extractUserMarkdown(midLine)).toBe("still user\n");
634
653
  expect(composeGuardedMarkdown(extractGruntBody(midLine) ?? "", extractUserMarkdown(midLine))).toBe(
635
- "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\nstill user\n",
654
+ "<!-- grunt:begin -->\n\nBODY\n\n<!-- grunt:end -->\n\nstill user\n",
636
655
  );
637
656
  });
638
657
 
@@ -654,7 +673,7 @@ describe("snapshot/remerge guarded roots", () => {
654
673
  expect(fs.readFileSync(agents)).toEqual(rawBuf);
655
674
  expect(fs.readFileSync(claude, "utf8")).toBe("keep claude\n");
656
675
  expect(fs.readFileSync(gemini, "utf8")).toBe(
657
- "<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\n",
676
+ "<!-- grunt:begin -->\n\n@AGENTS.md\n\n<!-- grunt:end -->\n",
658
677
  );
659
678
  fs.writeFileSync(claude, Buffer.from([0x00]));
660
679
  remergeGuardedRoots(dest, snap);
@@ -681,7 +700,7 @@ describe("snapshot/remerge guarded roots", () => {
681
700
  );
682
701
  remergeGuardedRoots(dest, snap);
683
702
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
684
- "<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
703
+ "<!-- grunt:begin -->\n\nsame\n\n<!-- grunt:end -->\n",
685
704
  );
686
705
  });
687
706
 
@@ -714,14 +733,14 @@ describe("snapshot/remerge guarded roots", () => {
714
733
  expect(exec.mock.calls.map((c) => c[0])).toEqual([
715
734
  "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills",
716
735
  "rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks",
717
- "node scripts/emit-mcp-policy.mjs",
718
- "node scripts/emit-gemini.mjs",
719
- "node scripts/emit-agent-shell-tools.mjs",
720
- "node scripts/emit-maps.mjs",
721
- "node scripts/hooks-union.mjs",
736
+ "node .rulesync/scripts/emit-mcp-policy.mjs",
737
+ "node .rulesync/scripts/emit-gemini.mjs",
738
+ "node .rulesync/scripts/emit-agent-shell-tools.mjs",
739
+ "node .rulesync/scripts/emit-maps.mjs",
740
+ "node .rulesync/scripts/hooks-union.mjs",
722
741
  ]);
723
742
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
724
- "<!-- grunt:begin -->\nfresh ssot\n<!-- grunt:end -->\nkeep me\n",
743
+ "<!-- grunt:begin -->\n\nfresh ssot\n\n<!-- grunt:end -->\n\nkeep me\n",
725
744
  );
726
745
  });
727
746
 
@@ -736,7 +755,7 @@ describe("snapshot/remerge guarded roots", () => {
736
755
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch gen 1\n");
737
756
  healGuardedRootFile(dest, "AGENTS.md", snap);
738
757
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
739
- "<!-- grunt:begin -->\nwatch gen 1\n<!-- grunt:end -->\nkeep me\n",
758
+ "<!-- grunt:begin -->\n\nwatch gen 1\n\n<!-- grunt:end -->\n\nkeep me\n",
740
759
  );
741
760
  fs.writeFileSync(
742
761
  path.join(dest, "AGENTS.md"),
@@ -746,12 +765,12 @@ describe("snapshot/remerge guarded roots", () => {
746
765
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch gen 2\n");
747
766
  healGuardedRootFile(dest, "AGENTS.md", snap);
748
767
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
749
- "<!-- grunt:begin -->\nwatch gen 2\n<!-- grunt:end -->\nedited bottom\n",
768
+ "<!-- grunt:begin -->\n\nwatch gen 2\n\n<!-- grunt:end -->\n\nedited bottom\n",
750
769
  );
751
770
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "watch claude\n");
752
771
  healGuardedRootFile(dest, "CLAUDE.md", snap);
753
772
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
754
- "<!-- grunt:begin -->\nwatch claude\n<!-- grunt:end -->\nuser claude\n",
773
+ "<!-- grunt:begin -->\n\nwatch claude\n\n<!-- grunt:end -->\n\nuser claude\n",
755
774
  );
756
775
  healGuardedRootFile(dest, "README.md", snap);
757
776
  expect(GUARDED_ROOT_FILES).toEqual(["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
@@ -763,12 +782,12 @@ describe("snapshot/remerge guarded roots", () => {
763
782
  fs.writeFileSync(path.join(eq, "AGENTS.md"), "same");
764
783
  healGuardedRootFile(eq, "AGENTS.md", eqSnap);
765
784
  expect(fs.readFileSync(path.join(eq, "AGENTS.md"), "utf8")).toBe(
766
- "<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
785
+ "<!-- grunt:begin -->\n\nsame\n\n<!-- grunt:end -->\n",
767
786
  );
768
787
  fs.writeFileSync(path.join(eq, "GEMINI.md"), "@AGENTS.md\n");
769
788
  healGuardedRootFile(eq, "GEMINI.md", {});
770
789
  expect(fs.readFileSync(path.join(eq, "GEMINI.md"), "utf8")).toBe(
771
- "<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\n",
790
+ "<!-- grunt:begin -->\n\n@AGENTS.md\n\n<!-- grunt:end -->\n",
772
791
  );
773
792
  });
774
793
 
@@ -804,7 +823,7 @@ describe("snapshot/remerge guarded roots", () => {
804
823
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch1\n");
805
824
  heal("AGENTS.md");
806
825
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
807
- "<!-- grunt:begin -->\nwatch1\n<!-- grunt:end -->\nkeep me\n",
826
+ "<!-- grunt:begin -->\n\nwatch1\n\n<!-- grunt:end -->\n\nkeep me\n",
808
827
  );
809
828
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch2\n");
810
829
  heal("AGENTS.md");
@@ -819,14 +838,14 @@ describe("snapshot/remerge guarded roots", () => {
819
838
  },
820
839
  });
821
840
  expect(exec.mock.calls.map((c) => c[0])).toEqual([
822
- "node scripts/emit-mcp-policy.mjs",
823
- "node scripts/emit-gemini.mjs",
824
- "node scripts/emit-agent-shell-tools.mjs",
841
+ "node .rulesync/scripts/emit-mcp-policy.mjs",
842
+ "node .rulesync/scripts/emit-gemini.mjs",
843
+ "node .rulesync/scripts/emit-agent-shell-tools.mjs",
825
844
  "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
826
845
  ]);
827
846
  expect(stop).toHaveBeenCalledTimes(1);
828
847
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
829
- "<!-- grunt:begin -->\nwatch2\n<!-- grunt:end -->\nkeep me\n",
848
+ "<!-- grunt:begin -->\n\nwatch2\n\n<!-- grunt:end -->\n\nkeep me\n",
830
849
  );
831
850
  });
832
851
 
@@ -841,7 +860,7 @@ describe("snapshot/remerge guarded roots", () => {
841
860
  });
842
861
  runGuardedRoots("watch", { cwd: dest, exec });
843
862
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
844
- "<!-- grunt:begin -->\nfrom-watch\n<!-- grunt:end -->\nkeep me\n",
863
+ "<!-- grunt:begin -->\n\nfrom-watch\n\n<!-- grunt:end -->\n\nkeep me\n",
845
864
  );
846
865
  expect(() => runGuardedRoots("nope")).toThrow(/generate\|check\|watch/);
847
866
  });
@@ -938,7 +957,7 @@ describe("mergeClaudeSettings", () => {
938
957
  hooks: [
939
958
  {
940
959
  type: "command",
941
- command: 'node "${OLD_ROOT}/scripts/scrub-spawn-prompt.mjs"',
960
+ command: 'node "${OLD_ROOT}/.rulesync/scripts/scrub-spawn-prompt.mjs"',
942
961
  },
943
962
  ],
944
963
  },
@@ -989,7 +1008,7 @@ describe("mergeClaudeSettings", () => {
989
1008
  const mixedGroup = {
990
1009
  matcher: "mixed",
991
1010
  hooks: [
992
- { type: "command", command: 'node "${OLD_ROOT}/scripts/gate-fat-tools.mjs"' },
1011
+ { type: "command", command: 'node "${OLD_ROOT}/.rulesync/scripts/gate-fat-tools.mjs"' },
993
1012
  { type: "command", command: "node scripts/my-custom-hook.mjs" },
994
1013
  ],
995
1014
  };
@@ -1278,7 +1297,7 @@ describe("mergePackageJson", () => {
1278
1297
  const pkgRoot = stubPkgRoot({
1279
1298
  name: "fixture-pkg",
1280
1299
  scripts: {
1281
- "rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
1300
+ "rulesync:generate": "node ./.rulesync/scripts/guarded-roots.mjs generate",
1282
1301
  "rulesync:generate:raw": "echo raw",
1283
1302
  "rulesync:check:raw": "echo check-raw",
1284
1303
  "rulesync:watch:raw": "echo watch-raw",
@@ -1289,7 +1308,7 @@ describe("mergePackageJson", () => {
1289
1308
  const dest = tmp("pj-raw-src-");
1290
1309
  mergePackageJson(dest, pkgRoot);
1291
1310
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1292
- expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1311
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./.rulesync/scripts/guarded-roots.mjs generate");
1293
1312
  expect(out.scripts["rulesync:generate"]).toBeUndefined();
1294
1313
  expect(out.scripts["rulesync:generate:raw"]).toBeUndefined();
1295
1314
  expect(out.scripts["rulesync:check:raw"]).toBeUndefined();
@@ -1299,7 +1318,7 @@ describe("mergePackageJson", () => {
1299
1318
  it("leaves dest-only :raw keys", () => {
1300
1319
  const pkgRoot = stubPkgRoot({
1301
1320
  name: "fixture-pkg",
1302
- scripts: { "rulesync:generate": "node ./scripts/guarded-roots.mjs generate" },
1321
+ scripts: { "rulesync:generate": "node ./.rulesync/scripts/guarded-roots.mjs generate" },
1303
1322
  dependencies: { "smol-toml": "^1.8.0" },
1304
1323
  devDependencies: { rulesync: "latest" },
1305
1324
  });
@@ -1312,14 +1331,14 @@ describe("mergePackageJson", () => {
1312
1331
  );
1313
1332
  mergePackageJson(dest, pkgRoot);
1314
1333
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1315
- expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1334
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./.rulesync/scripts/guarded-roots.mjs generate");
1316
1335
  expect(out.scripts["rulesync:generate:raw"]).toBe("echo consumer-raw");
1317
1336
  });
1318
1337
 
1319
1338
  it("replaces a grunt-owned script with no suffix", () => {
1320
1339
  const pkgRoot = stubPkgRoot({
1321
1340
  name: "fixture-pkg",
1322
- scripts: { "rulesync:generate": "rulesync generate -t bar", "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply" },
1341
+ scripts: { "rulesync:generate": "rulesync generate -t bar", "sync:globals:apply": "node .rulesync/scripts/sync-global-settings.mjs --apply" },
1323
1342
  dependencies: { "smol-toml": "^1.8.0" },
1324
1343
  devDependencies: { rulesync: "latest" },
1325
1344
  });
@@ -1333,7 +1352,7 @@ describe("mergePackageJson", () => {
1333
1352
  mergePackageJson(dest, pkgRoot);
1334
1353
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1335
1354
  expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
1336
- expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
1355
+ expect(out.scripts["grunt:sync:globals:apply"]).toBe("node .rulesync/scripts/sync-global-settings.mjs --apply");
1337
1356
  expect(out.scripts["rulesync:generate"]).toBeUndefined();
1338
1357
  });
1339
1358
 
@@ -1369,18 +1388,18 @@ describe("mergePackageJson", () => {
1369
1388
  const pkgRoot = stubPkgRoot({
1370
1389
  name: "fixture-pkg",
1371
1390
  scripts: {
1372
- "rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
1373
- "rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
1374
- "rulesync:check": "node ./scripts/guarded-roots.mjs check",
1375
- "hooks:union": "node scripts/hooks-union.mjs",
1376
- "hooks:check": "node scripts/hooks-union.mjs --check",
1377
- doctor: "node ./scripts/doctor.mjs",
1391
+ "rulesync:generate": "node ./.rulesync/scripts/guarded-roots.mjs generate",
1392
+ "rulesync:watch": "node ./.rulesync/scripts/guarded-roots.mjs watch",
1393
+ "rulesync:check": "node ./.rulesync/scripts/guarded-roots.mjs check",
1394
+ "hooks:union": "node .rulesync/scripts/hooks-union.mjs",
1395
+ "hooks:check": "node .rulesync/scripts/hooks-union.mjs --check",
1396
+ doctor: "node ./.rulesync/scripts/doctor.mjs",
1378
1397
  "rulesync:doctor": "rulesync doctor",
1379
- "sync:globals": "node scripts/sync-global-settings.mjs",
1380
- "sync:globals:check": "node scripts/check-globals.mjs",
1381
- "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
1382
- "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
1383
- "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
1398
+ "sync:globals": "node .rulesync/scripts/sync-global-settings.mjs",
1399
+ "sync:globals:check": "node .rulesync/scripts/check-globals.mjs",
1400
+ "sync:globals:apply": "node .rulesync/scripts/sync-global-settings.mjs --apply",
1401
+ "purge:global-mcps": "node .rulesync/scripts/purge-global-mcps.mjs",
1402
+ "purge:global-mcps:apply": "node .rulesync/scripts/purge-global-mcps.mjs --apply",
1384
1403
  test: "vitest run --coverage",
1385
1404
  },
1386
1405
  dependencies: { "smol-toml": "^1.8.0" },
@@ -1397,19 +1416,19 @@ describe("mergePackageJson", () => {
1397
1416
  check:
1398
1417
  "node scripts/portability-check.mjs && npm run rulesync:check && npm run lint && npm test && node scripts/media-fixtures.mjs --check",
1399
1418
  "codex:sync": "node scripts/codex-sync.mjs",
1400
- doctor: "node ./scripts/doctor.mjs",
1401
- "hooks:check": "node scripts/hooks-union.mjs --check",
1402
- "hooks:union": "node scripts/hooks-union.mjs",
1419
+ doctor: "node ./.rulesync/scripts/doctor.mjs",
1420
+ "hooks:check": "node .rulesync/scripts/hooks-union.mjs --check",
1421
+ "hooks:union": "node .rulesync/scripts/hooks-union.mjs",
1403
1422
  lint: "eslint scripts tests eslint.config.mjs",
1404
- "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
1405
- "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
1406
- "rulesync:check": "node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1423
+ "purge:global-mcps": "node .rulesync/scripts/purge-global-mcps.mjs",
1424
+ "purge:global-mcps:apply": "node .rulesync/scripts/purge-global-mcps.mjs --apply",
1425
+ "rulesync:check": "node ./.rulesync/scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1407
1426
  "rulesync:doctor": "rulesync doctor",
1408
- "rulesync:generate": "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1409
- "rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
1410
- "sync:globals": "node scripts/sync-global-settings.mjs",
1411
- "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
1412
- "sync:globals:check": "node scripts/check-globals.mjs",
1427
+ "rulesync:generate": "node ./.rulesync/scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1428
+ "rulesync:watch": "node ./.rulesync/scripts/guarded-roots.mjs watch",
1429
+ "sync:globals": "node .rulesync/scripts/sync-global-settings.mjs",
1430
+ "sync:globals:apply": "node .rulesync/scripts/sync-global-settings.mjs --apply",
1431
+ "sync:globals:check": "node .rulesync/scripts/check-globals.mjs",
1413
1432
  test: "node scripts/run-tests.mjs",
1414
1433
  },
1415
1434
  }),
@@ -1417,21 +1436,21 @@ describe("mergePackageJson", () => {
1417
1436
  mergePackageJson(dest, pkgRoot);
1418
1437
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1419
1438
  expect(out.scripts["grunt:rulesync:generate"]).toBe(
1420
- "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1439
+ "node ./.rulesync/scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1421
1440
  );
1422
1441
  expect(out.scripts["grunt:rulesync:check"]).toBe(
1423
- "node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1442
+ "node ./.rulesync/scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1424
1443
  );
1425
- expect(out.scripts["grunt:rulesync:watch"]).toBe("node ./scripts/guarded-roots.mjs watch");
1444
+ expect(out.scripts["grunt:rulesync:watch"]).toBe("node ./.rulesync/scripts/guarded-roots.mjs watch");
1426
1445
  expect(out.scripts["grunt:rulesync:doctor"]).toBe("rulesync doctor");
1427
- expect(out.scripts["grunt:hooks:union"]).toBe("node scripts/hooks-union.mjs");
1428
- expect(out.scripts["grunt:hooks:check"]).toBe("node scripts/hooks-union.mjs --check");
1429
- expect(out.scripts["grunt:sync:globals"]).toBe("node scripts/sync-global-settings.mjs");
1430
- expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
1431
- expect(out.scripts["grunt:sync:globals:check"]).toBe("node scripts/check-globals.mjs");
1432
- expect(out.scripts["grunt:purge:global-mcps"]).toBe("node scripts/purge-global-mcps.mjs");
1433
- expect(out.scripts["grunt:purge:global-mcps:apply"]).toBe("node scripts/purge-global-mcps.mjs --apply");
1434
- expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
1446
+ expect(out.scripts["grunt:hooks:union"]).toBe("node .rulesync/scripts/hooks-union.mjs");
1447
+ expect(out.scripts["grunt:hooks:check"]).toBe("node .rulesync/scripts/hooks-union.mjs --check");
1448
+ expect(out.scripts["grunt:sync:globals"]).toBe("node .rulesync/scripts/sync-global-settings.mjs");
1449
+ expect(out.scripts["grunt:sync:globals:apply"]).toBe("node .rulesync/scripts/sync-global-settings.mjs --apply");
1450
+ expect(out.scripts["grunt:sync:globals:check"]).toBe("node .rulesync/scripts/check-globals.mjs");
1451
+ expect(out.scripts["grunt:purge:global-mcps"]).toBe("node .rulesync/scripts/purge-global-mcps.mjs");
1452
+ expect(out.scripts["grunt:purge:global-mcps:apply"]).toBe("node .rulesync/scripts/purge-global-mcps.mjs --apply");
1453
+ expect(out.scripts["grunt:doctor"]).toBe("node ./.rulesync/scripts/doctor.mjs");
1435
1454
  expect(out.scripts.doctor).toBeUndefined();
1436
1455
  expect(out.scripts["rulesync:generate"]).toBeUndefined();
1437
1456
  expect(out.scripts["rulesync:check"]).toBeUndefined();
@@ -1450,7 +1469,7 @@ describe("mergePackageJson", () => {
1450
1469
  it("leaves custom doctor and writes grunt:doctor from SoT", () => {
1451
1470
  const pkgRoot = stubPkgRoot({
1452
1471
  name: "fixture-pkg",
1453
- scripts: { doctor: "node ./scripts/doctor.mjs" },
1472
+ scripts: { doctor: "node ./.rulesync/scripts/doctor.mjs" },
1454
1473
  dependencies: { "smol-toml": "^1.8.0" },
1455
1474
  devDependencies: { rulesync: "latest" },
1456
1475
  });
@@ -1463,7 +1482,7 @@ describe("mergePackageJson", () => {
1463
1482
  mergePackageJson(dest, pkgRoot);
1464
1483
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1465
1484
  expect(out.scripts.doctor).toBe("node scripts/audit.mjs");
1466
- expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
1485
+ expect(out.scripts["grunt:doctor"]).toBe("node ./.rulesync/scripts/doctor.mjs");
1467
1486
  expect(warnSpy).toHaveBeenCalled();
1468
1487
  warnSpy.mockRestore();
1469
1488
  });
@@ -1496,7 +1515,7 @@ describe("mergePackageJson", () => {
1496
1515
  it("does not rewrite npm run rulesync:check:raw when only rulesync:check migrated", () => {
1497
1516
  const pkgRoot = stubPkgRoot({
1498
1517
  name: "fixture-pkg",
1499
- scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
1518
+ scripts: { "rulesync:check": "node ./.rulesync/scripts/guarded-roots.mjs check" },
1500
1519
  dependencies: { "smol-toml": "^1.8.0" },
1501
1520
  devDependencies: { rulesync: "latest" },
1502
1521
  });
@@ -1505,7 +1524,7 @@ describe("mergePackageJson", () => {
1505
1524
  path.join(dest, "package.json"),
1506
1525
  JSON.stringify({
1507
1526
  scripts: {
1508
- "rulesync:check": "node ./scripts/guarded-roots.mjs check",
1527
+ "rulesync:check": "node ./.rulesync/scripts/guarded-roots.mjs check",
1509
1528
  "rulesync:check:raw": "echo consumer-raw",
1510
1529
  check: "npm run rulesync:check && npm run rulesync:check:raw",
1511
1530
  },
@@ -1513,7 +1532,7 @@ describe("mergePackageJson", () => {
1513
1532
  );
1514
1533
  mergePackageJson(dest, pkgRoot);
1515
1534
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1516
- expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
1535
+ expect(out.scripts["grunt:rulesync:check"]).toBe("node ./.rulesync/scripts/guarded-roots.mjs check");
1517
1536
  expect(out.scripts["rulesync:check"]).toBeUndefined();
1518
1537
  expect(out.scripts["rulesync:check:raw"]).toBe("echo consumer-raw");
1519
1538
  expect(out.scripts.check).toBe("npm run grunt:rulesync:check && npm run rulesync:check:raw");
@@ -1522,7 +1541,7 @@ describe("mergePackageJson", () => {
1522
1541
  it("rewrites yarn/pnpm/bun run refs the same way as npm run", () => {
1523
1542
  const pkgRoot = stubPkgRoot({
1524
1543
  name: "fixture-pkg",
1525
- scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
1544
+ scripts: { "rulesync:check": "node ./.rulesync/scripts/guarded-roots.mjs check" },
1526
1545
  dependencies: { "smol-toml": "^1.8.0" },
1527
1546
  devDependencies: { rulesync: "latest" },
1528
1547
  });
@@ -1531,7 +1550,7 @@ describe("mergePackageJson", () => {
1531
1550
  path.join(dest, "package.json"),
1532
1551
  JSON.stringify({
1533
1552
  scripts: {
1534
- "rulesync:check": "node ./scripts/guarded-roots.mjs check",
1553
+ "rulesync:check": "node ./.rulesync/scripts/guarded-roots.mjs check",
1535
1554
  check: "pnpm run rulesync:check && yarn run rulesync:check && bun run rulesync:check",
1536
1555
  },
1537
1556
  }),
@@ -1634,7 +1653,7 @@ describe("mergePackageJson", () => {
1634
1653
  it("skips non-string dest scripts when rewriting npm run refs", () => {
1635
1654
  const pkgRoot = stubPkgRoot({
1636
1655
  name: "fixture-pkg",
1637
- scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
1656
+ scripts: { "rulesync:check": "node ./.rulesync/scripts/guarded-roots.mjs check" },
1638
1657
  dependencies: { "smol-toml": "^1.8.0" },
1639
1658
  devDependencies: { rulesync: "latest" },
1640
1659
  });
@@ -1652,7 +1671,7 @@ describe("mergePackageJson", () => {
1652
1671
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1653
1672
  expect(out.scripts.extra).toBe(1);
1654
1673
  expect(out.scripts.check).toBe("npm run grunt:rulesync:check");
1655
- expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
1674
+ expect(out.scripts["grunt:rulesync:check"]).toBe("node ./.rulesync/scripts/guarded-roots.mjs check");
1656
1675
  });
1657
1676
 
1658
1677
  it("emits grunt:claude into a consumer package.json", () => {
@@ -1763,7 +1782,7 @@ describe("mergePackageJson", () => {
1763
1782
  mergePackageJson(dest, PKG_ROOT);
1764
1783
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1765
1784
  expect(out.scripts["grunt:test:e2e"]).toBeUndefined();
1766
- expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
1785
+ expect(out.scripts["grunt:rulesync:check"]).toBe("node ./.rulesync/scripts/guarded-roots.mjs check");
1767
1786
  });
1768
1787
 
1769
1788
  it("adds grunt:test:e2e when dest has a vitest e2e config", () => {
@@ -1887,7 +1906,7 @@ describe("init", () => {
1887
1906
  expect(pkg.name).toBe("fixture-pkg");
1888
1907
  expect(fs.existsSync(path.join(pkgRoot, ".tmp"))).toBe(true);
1889
1908
  expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
1890
- expect(fs.readFileSync(path.join(pkgRoot, "scripts", "grunt-job.mjs"), "utf8")).toBe(
1909
+ expect(fs.readFileSync(path.join(pkgRoot, ".rulesync", "scripts", "grunt-job.mjs"), "utf8")).toBe(
1891
1910
  "grunt-job.mjs",
1892
1911
  );
1893
1912
  // self-skip: markdown/settings stay exactly as-is, no sentinel wrap
@@ -1910,7 +1929,7 @@ describe("init", () => {
1910
1929
  }
1911
1930
  expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1912
1931
  expect(PRODUCT_FILES).not.toContain("grunt-config.mjs");
1913
- expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1932
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "grunt-config.mjs"))).toBe(false);
1914
1933
  expect(fs.readFileSync(path.join(dest, ".claude", "marker"), "utf8")).toBe(".claude");
1915
1934
  const settings = JSON.parse(
1916
1935
  fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
@@ -1919,17 +1938,17 @@ describe("init", () => {
1919
1938
 
1920
1939
  for (const f of GUARDED_MD_FILES) {
1921
1940
  expect(fs.readFileSync(path.join(dest, f), "utf8")).toBe(
1922
- `<!-- grunt:begin -->\n${f} content\n<!-- grunt:end -->\n`,
1941
+ `<!-- grunt:begin -->\n\n${f} content\n\n<!-- grunt:end -->\n`,
1923
1942
  );
1924
1943
  }
1925
1944
  expect(fs.existsSync(path.join(dest, ".mcp.json"))).toBe(false);
1926
1945
 
1927
1946
  for (const name of PRODUCT_FILES) {
1928
1947
  const optional = ["speak.mjs", "listen.mjs", "google-workspace.mjs"].includes(name);
1929
- expect(fs.existsSync(path.join(dest, "scripts", name)), name).toBe(!optional);
1948
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", name)), name).toBe(!optional);
1930
1949
  }
1931
- expect(fs.statSync(path.join(dest, "scripts", "scrub-text")).isDirectory()).toBe(true);
1932
- expect(fs.readFileSync(path.join(dest, "scripts", "scrub-text", "inside"), "utf8")).toBe(
1950
+ expect(fs.statSync(path.join(dest, ".rulesync", "scripts", "scrub-text")).isDirectory()).toBe(true);
1951
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "scripts", "scrub-text", "inside"), "utf8")).toBe(
1933
1952
  "dir",
1934
1953
  );
1935
1954
 
@@ -2119,11 +2138,11 @@ describe("init", () => {
2119
2138
  expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
2120
2139
  expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
2121
2140
  expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
2122
- expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
2123
- expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
2124
- expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(false);
2125
- expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(false);
2126
- expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(false);
2141
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "scrub-text"))).toBe(true);
2142
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "grunt-config.mjs"))).toBe(false);
2143
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "speak.mjs"))).toBe(false);
2144
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "listen.mjs"))).toBe(false);
2145
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "google-workspace.mjs"))).toBe(false);
2127
2146
  expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "clasp"))).toBe(false);
2128
2147
  expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "browser"))).toBe(true);
2129
2148
  expect(fs.existsSync(path.join(dest, ".rulesync", "reference", "listen.md"))).toBe(false);
@@ -2174,8 +2193,8 @@ describe("init", () => {
2174
2193
  expect(fs.existsSync(path.join(dest, ".claude", "skills", "clasp"))).toBe(false);
2175
2194
  expect(fs.existsSync(path.join(dest, ".agents", "skills", "listen"))).toBe(false);
2176
2195
  expect(fs.existsSync(path.join(dest, ".grok", "skills", "clasp"))).toBe(false);
2177
- expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(false);
2178
- expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(false);
2196
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "listen.mjs"))).toBe(false);
2197
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "speak.mjs"))).toBe(false);
2179
2198
  expect(fs.existsSync(path.join(dest, ".rulesync", "reference", "listen.md"))).toBe(false);
2180
2199
  expect(fs.readFileSync(path.join(dest, ".rulesync", "reference", "law.md"), "utf8")).toContain("## Mine");
2181
2200
  expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.features.jsonc"))).toBe(false);
@@ -2183,10 +2202,10 @@ describe("init", () => {
2183
2202
  const picked = tmp("packs-listen-");
2184
2203
  init(picked, { pkgRoot, execFileSync: exec, packageManager: "npm", packs: ["listen"] });
2185
2204
  expect(fs.readFileSync(path.join(picked, ".rulesync", "skills", "listen", "SKILL.md"), "utf8")).toBe("listen\n");
2186
- expect(fs.readFileSync(path.join(picked, "scripts", "listen.mjs"), "utf8")).toBe("listen.mjs");
2205
+ expect(fs.readFileSync(path.join(picked, ".rulesync", "scripts", "listen.mjs"), "utf8")).toBe("listen.mjs");
2187
2206
  expect(fs.existsSync(path.join(picked, ".rulesync", "reference", "listen.md"))).toBe(true);
2188
2207
  expect(fs.existsSync(path.join(picked, ".rulesync", "skills", "speak"))).toBe(false);
2189
- expect(fs.existsSync(path.join(picked, "scripts", "speak.mjs"))).toBe(false);
2208
+ expect(fs.existsSync(path.join(picked, ".rulesync", "scripts", "speak.mjs"))).toBe(false);
2190
2209
  expect(JSON.parse(fs.readFileSync(path.join(picked, ".rulesync", "grunt.features.jsonc"), "utf8"))).toEqual({
2191
2210
  packs: ["listen"],
2192
2211
  });
@@ -2199,17 +2218,17 @@ describe("init", () => {
2199
2218
  );
2200
2219
  init(saved, { pkgRoot, execFileSync: exec, packageManager: "npm" });
2201
2220
  expect(fs.existsSync(path.join(saved, ".rulesync", "skills", "speak"))).toBe(true);
2202
- expect(fs.existsSync(path.join(saved, "scripts", "listen.mjs"))).toBe(false);
2221
+ expect(fs.existsSync(path.join(saved, ".rulesync", "scripts", "listen.mjs"))).toBe(false);
2203
2222
  expect(fs.readFileSync(path.join(saved, ".rulesync", "grunt.features.jsonc"), "utf8")).toBe(
2204
2223
  '{"packs":["speak"]}\n',
2205
2224
  );
2206
2225
 
2207
2226
  const pkgSelf = stubPkgRoot();
2208
2227
  writeSkill(pkgSelf, "clasp");
2209
- fs.writeFileSync(path.join(pkgSelf, "scripts", "speak.mjs"), "keep-speak\n");
2228
+ fs.writeFileSync(path.join(pkgSelf, ".rulesync", "scripts", "speak.mjs"), "keep-speak\n");
2210
2229
  init(pkgSelf, { pkgRoot: pkgSelf, execFileSync: exec, packs: [] });
2211
2230
  expect(fs.readFileSync(path.join(pkgSelf, ".rulesync", "skills", "clasp", "SKILL.md"), "utf8")).toBe("clasp\n");
2212
- expect(fs.readFileSync(path.join(pkgSelf, "scripts", "speak.mjs"), "utf8")).toBe("keep-speak\n");
2231
+ expect(fs.readFileSync(path.join(pkgSelf, ".rulesync", "scripts", "speak.mjs"), "utf8")).toBe("keep-speak\n");
2213
2232
  expect(fs.existsSync(path.join(pkgSelf, ".rulesync", "grunt.features.jsonc"))).toBe(false);
2214
2233
  expect(() => init(tmp("packs-bad-"), { pkgRoot, execFileSync: exec, packageManager: "npm", packs: ["nope"] })).toThrow(
2215
2234
  /unknown pack/,
@@ -2271,10 +2290,10 @@ describe("init", () => {
2271
2290
  const exec = vi.fn();
2272
2291
  init(dest, { pkgRoot, execFileSync: exec });
2273
2292
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
2274
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
2293
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nhand-written consumer doc\n",
2275
2294
  );
2276
2295
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
2277
- "<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
2296
+ "<!-- grunt:begin -->\n\nAGENTS.md content\n\n<!-- grunt:end -->\n\nhand-written consumer doc\n",
2278
2297
  );
2279
2298
  expect(fs.readFileSync(path.join(dest, "CLAUDE.grunt.md"), "utf8")).toBe("stale side");
2280
2299
  expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
@@ -2288,7 +2307,7 @@ describe("init", () => {
2288
2307
  const exec = vi.fn();
2289
2308
  init(dest, { pkgRoot, execFileSync: exec });
2290
2309
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
2291
- "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
2310
+ "<!-- grunt:begin -->\n\nCLAUDE.md content\n\n<!-- grunt:end -->\n\nhand-written consumer doc\n",
2292
2311
  );
2293
2312
  expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
2294
2313
  expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
@@ -2315,17 +2334,17 @@ describe("init", () => {
2315
2334
  });
2316
2335
  init(dest, { pkgRoot, execFileSync: exec });
2317
2336
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
2318
- "<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\nconsumer agents\n",
2337
+ "<!-- grunt:begin -->\n\ngenerated agents\n\n<!-- grunt:end -->\n\nconsumer agents\n",
2319
2338
  );
2320
2339
  expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
2321
- "<!-- grunt:begin -->\ngenerated claude\n<!-- grunt:end -->\nconsumer claude\n",
2340
+ "<!-- grunt:begin -->\n\ngenerated claude\n\n<!-- grunt:end -->\n\nconsumer claude\n",
2322
2341
  );
2323
2342
  expect(fs.readFileSync(path.join(dest, "GEMINI.md"), "utf8")).toBe(
2324
- "<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\nconsumer gemini\n",
2343
+ "<!-- grunt:begin -->\n\n@AGENTS.md\n\n<!-- grunt:end -->\n\nconsumer gemini\n",
2325
2344
  );
2326
2345
  init(dest, { pkgRoot, execFileSync: vi.fn() });
2327
2346
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
2328
- "<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\nconsumer agents\n",
2347
+ "<!-- grunt:begin -->\n\nAGENTS.md content\n\n<!-- grunt:end -->\n\nconsumer agents\n",
2329
2348
  );
2330
2349
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8").split("consumer agents").length).toBe(
2331
2350
  2,
@@ -2350,8 +2369,8 @@ describe("init", () => {
2350
2369
  init(dest, { pkgRoot, execFileSync: exec });
2351
2370
  const agents = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
2352
2371
  const claude = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
2353
- expect(agents).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
2354
- expect(claude).toBe("<!-- grunt:begin -->\ngenerated claude\n<!-- grunt:end -->\n");
2372
+ expect(agents).toBe("<!-- grunt:begin -->\n\ngenerated agents\n\n<!-- grunt:end -->\n");
2373
+ expect(claude).toBe("<!-- grunt:begin -->\n\ngenerated claude\n\n<!-- grunt:end -->\n");
2355
2374
  expect(agents.split("<!-- grunt:begin -->")).toHaveLength(2);
2356
2375
  expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
2357
2376
  expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
@@ -2483,23 +2502,26 @@ describe("init", () => {
2483
2502
  expect(index).toMatch(/push → go-live\//);
2484
2503
  });
2485
2504
 
2486
- it("seeds grunt.pipeline.jsonc from dest pipeline omissions during init", () => {
2505
+ it("seeds grunt.pipeline.jsonc from a legacy root pipeline, then moves grunt scripts out of root", () => {
2487
2506
  const dest = tmp("grunt-pipe-init-");
2488
2507
  fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
2508
+ const legacy = (c: string) => c.replace(".rulesync/scripts/", "scripts/");
2489
2509
  const skipped = PIPELINE_COMMANDS.check.find((c) => c.includes("subagents"));
2490
2510
  const src = `export const COMMANDS = {
2491
2511
  check: [
2492
- ${PIPELINE_COMMANDS.check.filter((c) => c !== skipped).map((c) => JSON.stringify(c)).join(",\n ")}
2512
+ ${PIPELINE_COMMANDS.check.filter((c) => c !== skipped).map((c) => JSON.stringify(legacy(c))).join(",\n ")}
2493
2513
  ],
2494
2514
  };
2495
2515
  `;
2496
2516
  fs.writeFileSync(path.join(dest, "scripts", "pipeline.mjs"), src);
2517
+ fs.writeFileSync(path.join(dest, "scripts", "gate-fat-tools.mjs"), "old gate");
2497
2518
  init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
2498
2519
  const cfg = JSON.parse(fs.readFileSync(path.join(dest, PIPELINE_CONFIG_REL), "utf8"));
2499
2520
  expect(cfg.check.skip).toEqual([skipped]);
2500
- expect(fs.readFileSync(path.join(dest, "scripts", "pipeline.mjs"), "utf8")).toContain(
2521
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "scripts", "pipeline.mjs"), "utf8")).toContain(
2501
2522
  "subagents --check",
2502
2523
  );
2524
+ expect(fs.existsSync(path.join(dest, "scripts"))).toBe(false);
2503
2525
  });
2504
2526
 
2505
2527
  it("does not stamp package INDEX over dest; re-emits union", () => {
@@ -2634,10 +2656,13 @@ function relativeSpecs(text: string, re: RegExp) {
2634
2656
  }
2635
2657
 
2636
2658
  describe("product scripts are consumer-safe", () => {
2637
- it("package.json files lists every PRODUCT_SCRIPTS entry", () => {
2659
+ it("package.json files ships every PRODUCT_SCRIPTS entry and no tests or fixtures", () => {
2638
2660
  const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"));
2661
+ expect(pkg.files).toContain(".rulesync");
2662
+ expect(pkg.files).toContain("!.rulesync/scripts/**/*.test.ts");
2663
+ expect(pkg.files).toContain("!.rulesync/scripts/fixtures");
2639
2664
  for (const name of PRODUCT_SCRIPTS) {
2640
- expect(pkg.files).toContain(`scripts/${name}`);
2665
+ expect(fs.existsSync(path.join(PKG_ROOT, SCRIPTS_REL, name)), name).toBe(true);
2641
2666
  }
2642
2667
  });
2643
2668
 
@@ -2660,25 +2685,25 @@ describe("product scripts are consumer-safe", () => {
2660
2685
  }
2661
2686
  };
2662
2687
  walk(path.join(PKG_ROOT, "bin", "grunt.js"));
2663
- expect(seen.has(path.join(PKG_ROOT, "scripts", "folder-map.mjs"))).toBe(true);
2688
+ expect(seen.has(path.join(PKG_ROOT, ".rulesync", "scripts", "folder-map.mjs"))).toBe(true);
2664
2689
  expect(bad).toEqual([]);
2665
2690
  });
2666
2691
 
2667
- it("do not parent-import (from or import()) — init copies scripts/ only", () => {
2692
+ it("do not parent-import (from or import()) — init copies .rulesync/scripts/ only", () => {
2668
2693
  for (const name of PRODUCT_FILES) {
2669
- const text = fs.readFileSync(path.join(PKG_ROOT, "scripts", name), "utf8");
2694
+ const text = fs.readFileSync(path.join(PKG_ROOT, ".rulesync", "scripts", name), "utf8");
2670
2695
  expect(relativeSpecs(text, PARENT_IMPORT_RE), name).toEqual([]);
2671
2696
  }
2672
2697
  });
2673
2698
 
2674
- it("repro: dest node ./scripts/guarded-roots.mjs generate misses ../cli/init.mjs", () => {
2699
+ it("repro: dest node ./.rulesync/scripts/guarded-roots.mjs generate misses ../cli/init.mjs", () => {
2675
2700
  const dest = tmp("repro-cli-miss-");
2676
- fs.mkdirSync(path.join(dest, "scripts"));
2701
+ fs.mkdirSync(path.join(dest, ".rulesync", "scripts"), { recursive: true });
2677
2702
  fs.writeFileSync(
2678
- path.join(dest, "scripts", "broken.mjs"),
2679
- 'import { snapshotGuardedRoots } from "../cli/init.mjs";\n',
2703
+ path.join(dest, ".rulesync", "scripts", "broken.mjs"),
2704
+ 'import { snapshotGuardedRoots } from "../../cli/init.mjs";\n',
2680
2705
  );
2681
- const r = spawnDest(dest, ["./scripts/broken.mjs", "generate"]);
2706
+ const r = spawnDest(dest, ["./.rulesync/scripts/broken.mjs", "generate"]);
2682
2707
  expect(r.status).not.toBe(0);
2683
2708
  expect(r.stderr).toMatch(/ERR_MODULE_NOT_FOUND/);
2684
2709
  expect(r.stderr).toMatch(/cli\/init\.mjs/);
@@ -2693,11 +2718,11 @@ describe("product scripts are consumer-safe", () => {
2693
2718
  applyGlobals: false,
2694
2719
  });
2695
2720
  expect(fs.existsSync(path.join(dest, "cli"))).toBe(false);
2696
- expect(fs.existsSync(path.join(dest, "scripts", "guarded-roots.mjs"))).toBe(true);
2721
+ expect(fs.existsSync(path.join(dest, ".rulesync", "scripts", "guarded-roots.mjs"))).toBe(true);
2697
2722
 
2698
2723
  const optionalScripts = new Set(["speak.mjs", "listen.mjs", "google-workspace.mjs"]);
2699
2724
  for (const name of PRODUCT_FILES) {
2700
- const file = path.join(dest, "scripts", name);
2725
+ const file = path.join(dest, ".rulesync", "scripts", name);
2701
2726
  if (optionalScripts.has(name)) {
2702
2727
  expect(fs.existsSync(file), name).toBe(false);
2703
2728
  continue;
@@ -2711,7 +2736,7 @@ describe("product scripts are consumer-safe", () => {
2711
2736
  }
2712
2737
  }
2713
2738
 
2714
- const setupRun = spawnDest(dest, ["./scripts/setup.mjs"], {
2739
+ const setupRun = spawnDest(dest, ["./.rulesync/scripts/setup.mjs"], {
2715
2740
  ...process.env,
2716
2741
  CI: "1",
2717
2742
  PATH: ["/usr/bin", "/bin"].join(path.delimiter),
@@ -2720,12 +2745,12 @@ describe("product scripts are consumer-safe", () => {
2720
2745
  expect(setupRun.status).toBe(1);
2721
2746
  expect(`${setupRun.stdout}${setupRun.stderr}`).toContain("usage: setup");
2722
2747
 
2723
- const loaded = spawnDest(dest, ["./scripts/guarded-roots.mjs"]);
2748
+ const loaded = spawnDest(dest, ["./.rulesync/scripts/guarded-roots.mjs"]);
2724
2749
  expect(loaded.status).toBe(1);
2725
2750
  expect(loaded.stderr).toBe("usage: guarded-roots.mjs generate|check|watch\n");
2726
2751
  expect(loaded.stderr).not.toMatch(/ERR_MODULE_NOT_FOUND/);
2727
2752
 
2728
- const gen = spawnDest(dest, ["./scripts/guarded-roots.mjs", "generate"], {
2753
+ const gen = spawnDest(dest, ["./.rulesync/scripts/guarded-roots.mjs", "generate"], {
2729
2754
  ...process.env,
2730
2755
  PATH: ["/usr/bin", "/bin"].join(path.delimiter),
2731
2756
  });
@@ -2737,7 +2762,7 @@ describe("product scripts are consumer-safe", () => {
2737
2762
  const r = spawnDest(dest, [
2738
2763
  "--input-type=module",
2739
2764
  "-e",
2740
- `import(${JSON.stringify(`./scripts/${name}`)})`,
2765
+ `import(${JSON.stringify(`./.rulesync/scripts/${name}`)})`,
2741
2766
  ]);
2742
2767
  expect(r.status, `${name} load\n${r.stderr}`).toBe(0);
2743
2768
  expect(r.stderr, name).not.toMatch(/ERR_MODULE_NOT_FOUND/);
@@ -2745,3 +2770,125 @@ describe("product scripts are consumer-safe", () => {
2745
2770
  });
2746
2771
  });
2747
2772
 
2773
+
2774
+ describe("legacy root scripts migration", () => {
2775
+ function legacyDir(names: string[]) {
2776
+ const dest = tmp("legacy-scripts-");
2777
+ fs.mkdirSync(path.join(dest, LEGACY_SCRIPTS_REL));
2778
+ for (const n of names) fs.writeFileSync(path.join(dest, LEGACY_SCRIPTS_REL, n), n);
2779
+ return dest;
2780
+ }
2781
+
2782
+ it("drops grunt names and keeps consumer files when a prior grunt install is detected", () => {
2783
+ const dest = legacyDir(["gate-fat-tools.mjs", "doctor.mjs", "mine.mjs"]);
2784
+ pruneLegacyScripts(dest);
2785
+ expect(fs.readdirSync(path.join(dest, LEGACY_SCRIPTS_REL))).toEqual(["mine.mjs"]);
2786
+ });
2787
+
2788
+ it("removes the root dir when only grunt names were in it", () => {
2789
+ const dest = legacyDir(["guarded-roots.mjs", "pipeline.mjs"]);
2790
+ pruneLegacyScripts(dest);
2791
+ expect(fs.existsSync(path.join(dest, LEGACY_SCRIPTS_REL))).toBe(false);
2792
+ });
2793
+
2794
+ it("leaves a consumer's own same-named scripts alone without a grunt marker", () => {
2795
+ const dest = legacyDir(["setup.mjs", "doctor.mjs"]);
2796
+ pruneLegacyScripts(dest);
2797
+ expect(fs.readdirSync(path.join(dest, LEGACY_SCRIPTS_REL)).sort()).toEqual(["doctor.mjs", "setup.mjs"]);
2798
+ const none = tmp("legacy-none-");
2799
+ pruneLegacyScripts(none);
2800
+ expect(fs.existsSync(path.join(none, LEGACY_SCRIPTS_REL))).toBe(false);
2801
+ });
2802
+
2803
+ it("rewrites only grunt-owned legacy refs", () => {
2804
+ expect(rewriteLegacyScriptRefs("node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs")).toBe(
2805
+ "node ./.rulesync/scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
2806
+ );
2807
+ expect(rewriteLegacyScriptRefs('node "${ROOT}/scripts/gate-fat-tools.mjs"')).toBe(
2808
+ 'node "${ROOT}/.rulesync/scripts/gate-fat-tools.mjs"',
2809
+ );
2810
+ expect(rewriteLegacyScriptRefs('["node scripts/emit-maps.mjs --check"]')).toBe(
2811
+ '["node .rulesync/scripts/emit-maps.mjs --check"]',
2812
+ );
2813
+ for (const keep of [
2814
+ "node scripts/lib/env.mjs",
2815
+ "node tools/scripts/doctor.mjs",
2816
+ "node scripts/doctor.mjsx",
2817
+ "node .rulesync/scripts/doctor.mjs",
2818
+ ]) {
2819
+ expect(rewriteLegacyScriptRefs(keep)).toBe(keep);
2820
+ }
2821
+ });
2822
+
2823
+ it("init migrates grunt:* package scripts and the pipeline overlay; consumer refs stay", () => {
2824
+ const pkgRoot = stubPkgRoot({
2825
+ name: "fixture-pkg",
2826
+ version: "1.0.0",
2827
+ scripts: { "rulesync:generate": "node ./.rulesync/scripts/guarded-roots.mjs generate" },
2828
+ dependencies: { "smol-toml": "^1.8.0" },
2829
+ devDependencies: { rulesync: "latest" },
2830
+ });
2831
+ const dest = tmp("legacy-init-");
2832
+ fs.writeFileSync(
2833
+ path.join(dest, "package.json"),
2834
+ JSON.stringify({
2835
+ scripts: {
2836
+ "grunt:rulesync:generate": "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
2837
+ lint: "node scripts/lint.mjs",
2838
+ },
2839
+ }),
2840
+ );
2841
+ fs.mkdirSync(path.join(dest, ".rulesync"), { recursive: true });
2842
+ fs.writeFileSync(
2843
+ path.join(dest, PIPELINE_CONFIG_REL),
2844
+ JSON.stringify({
2845
+ check: { skip: ["node scripts/emit-maps.mjs --check"], extra: ["node scripts/codex-sync.mjs --check"] },
2846
+ }),
2847
+ );
2848
+ init(dest, { pkgRoot, execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
2849
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
2850
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
2851
+ "node ./.rulesync/scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
2852
+ );
2853
+ expect(out.scripts.lint).toBe("node scripts/lint.mjs");
2854
+ const cfg = JSON.parse(fs.readFileSync(path.join(dest, PIPELINE_CONFIG_REL), "utf8"));
2855
+ expect(cfg.check).toEqual({
2856
+ skip: ["node .rulesync/scripts/emit-maps.mjs --check"],
2857
+ extra: ["node scripts/codex-sync.mjs --check"],
2858
+ });
2859
+ });
2860
+
2861
+ it("init copies product scripts to .rulesync/scripts and moves them out of a legacy root", () => {
2862
+ const pkgRoot = stubPkgRoot();
2863
+ fs.writeFileSync(path.join(pkgRoot, SCRIPTS_REL, "x.test.ts"), "test");
2864
+ const dest = legacyDir(["gate-fat-tools.mjs", "board.mjs", "mine.mjs"]);
2865
+ init(dest, { pkgRoot, execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
2866
+ expect(fs.readFileSync(path.join(dest, SCRIPTS_REL, "gate-fat-tools.mjs"), "utf8")).toBe("gate-fat-tools.mjs");
2867
+ expect(fs.existsSync(path.join(dest, SCRIPTS_REL, "x.test.ts"))).toBe(false);
2868
+ expect(fs.readdirSync(path.join(dest, LEGACY_SCRIPTS_REL))).toEqual(["mine.mjs"]);
2869
+ });
2870
+
2871
+ it("mergeClaudeSettings replaces legacy-path grunt hooks without duplicating; consumer hook kept", () => {
2872
+ const pkgRoot = stubPkgRoot();
2873
+ const destRoot = tmp("settings-legacy-");
2874
+ fs.mkdirSync(path.join(destRoot, ".claude"), { recursive: true });
2875
+ fs.writeFileSync(
2876
+ path.join(destRoot, ".claude", "settings.json"),
2877
+ JSON.stringify({
2878
+ hooks: {
2879
+ PreToolUse: [
2880
+ { matcher: "spawn", hooks: [{ type: "command", command: 'node "${ROOT}/scripts/scrub-spawn-prompt.mjs"' }] },
2881
+ { matcher: "Write", hooks: [{ type: "command", command: "node scripts/my-custom-hook.mjs" }] },
2882
+ ],
2883
+ },
2884
+ }),
2885
+ );
2886
+ mergeClaudeSettings(destRoot, pkgRoot);
2887
+ const out = JSON.parse(fs.readFileSync(path.join(destRoot, ".claude", "settings.json"), "utf8"));
2888
+ const cmds = out.hooks.PreToolUse.flatMap((g: { hooks: { command: string }[] }) => g.hooks.map((h) => h.command));
2889
+ expect(cmds.filter((c: string) => c.includes("scrub-spawn-prompt.mjs"))).toEqual([
2890
+ 'node "${ROOT}/.rulesync/scripts/scrub-spawn-prompt.mjs"',
2891
+ ]);
2892
+ expect(cmds).toContain("node scripts/my-custom-hook.mjs");
2893
+ });
2894
+ });