@lovrozagar/grunt 0.5.0 → 0.6.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 (153) hide show
  1. package/.agents/agents/grunt.md +2 -23
  2. package/.agents/agents/orchestrator.md +5 -45
  3. package/.agents/skills/ask/SKILL.md +11 -0
  4. package/.agents/skills/auto/SKILL.md +13 -0
  5. package/.agents/skills/browser/SKILL.md +4 -7
  6. package/.agents/skills/clasp/SKILL.md +11 -0
  7. package/.agents/skills/commit/SKILL.md +3 -1
  8. package/.agents/skills/commit-push/SKILL.md +2 -0
  9. package/.agents/skills/commit-push-deploy/SKILL.md +2 -0
  10. package/.agents/skills/commit-push-release/SKILL.md +2 -0
  11. package/.agents/skills/explain/SKILL.md +7 -13
  12. package/.agents/skills/google-workspace/SKILL.md +13 -0
  13. package/.agents/skills/handoff/SKILL.md +7 -11
  14. package/.agents/skills/implement-plan/SKILL.md +5 -108
  15. package/.agents/skills/listen/SKILL.md +15 -0
  16. package/.agents/skills/pickup/SKILL.md +6 -12
  17. package/.agents/skills/speak/SKILL.md +13 -0
  18. package/.agents/skills/tmp/SKILL.md +54 -0
  19. package/.agents/skills/write-plan/SKILL.md +5 -74
  20. package/.claude/agents/grunt.md +2 -25
  21. package/.claude/agents/orchestrator.md +12 -45
  22. package/.claude/skills/ask/SKILL.md +11 -0
  23. package/.claude/skills/auto/SKILL.md +13 -0
  24. package/.claude/skills/browser/SKILL.md +4 -7
  25. package/.claude/skills/clasp/SKILL.md +11 -0
  26. package/.claude/skills/commit/SKILL.md +3 -1
  27. package/.claude/skills/commit-push/SKILL.md +2 -0
  28. package/.claude/skills/commit-push-deploy/SKILL.md +2 -0
  29. package/.claude/skills/commit-push-release/SKILL.md +2 -0
  30. package/.claude/skills/explain/SKILL.md +7 -13
  31. package/.claude/skills/google-workspace/SKILL.md +13 -0
  32. package/.claude/skills/handoff/SKILL.md +7 -11
  33. package/.claude/skills/implement-plan/SKILL.md +5 -108
  34. package/.claude/skills/listen/SKILL.md +15 -0
  35. package/.claude/skills/pickup/SKILL.md +6 -12
  36. package/.claude/skills/speak/SKILL.md +13 -0
  37. package/.claude/skills/tmp/SKILL.md +54 -0
  38. package/.claude/skills/write-plan/SKILL.md +5 -74
  39. package/.codex/agents/grunt.toml +2 -25
  40. package/.codex/agents/orchestrator.toml +4 -45
  41. package/.grok/agents/grunt.md +2 -23
  42. package/.grok/agents/orchestrator.md +4 -43
  43. package/.grok/global-settings.toml +1 -3
  44. package/.grok/hooks/orchestrate-parent.js +278 -228
  45. package/.grok/skills/ask/SKILL.md +11 -0
  46. package/.grok/skills/auto/SKILL.md +13 -0
  47. package/.grok/skills/browser/SKILL.md +4 -7
  48. package/.grok/skills/clasp/SKILL.md +11 -0
  49. package/.grok/skills/commit/SKILL.md +3 -1
  50. package/.grok/skills/commit-push/SKILL.md +2 -0
  51. package/.grok/skills/commit-push-deploy/SKILL.md +2 -0
  52. package/.grok/skills/commit-push-release/SKILL.md +2 -0
  53. package/.grok/skills/explain/SKILL.md +7 -13
  54. package/.grok/skills/google-workspace/SKILL.md +13 -0
  55. package/.grok/skills/handoff/SKILL.md +7 -11
  56. package/.grok/skills/implement-plan/SKILL.md +5 -108
  57. package/.grok/skills/listen/SKILL.md +15 -0
  58. package/.grok/skills/pickup/SKILL.md +6 -12
  59. package/.grok/skills/speak/SKILL.md +13 -0
  60. package/.grok/skills/tmp/SKILL.md +54 -0
  61. package/.grok/skills/write-plan/SKILL.md +5 -74
  62. package/.rulesync/hooks.jsonc +1 -1
  63. package/.rulesync/mcp-policy.jsonc +0 -2
  64. package/.rulesync/reference/INDEX.md +30 -17
  65. package/.rulesync/reference/browser.md +13 -6
  66. package/.rulesync/reference/cascade.md +6 -84
  67. package/.rulesync/reference/clasp.md +21 -0
  68. package/.rulesync/reference/google-workspace.md +79 -0
  69. package/.rulesync/reference/hooks.md +7 -9
  70. package/.rulesync/reference/implementation-format.md +113 -0
  71. package/.rulesync/reference/law.md +5 -3
  72. package/.rulesync/reference/listen.md +80 -0
  73. package/.rulesync/reference/map.md +39 -31
  74. package/.rulesync/reference/output.md +3 -41
  75. package/.rulesync/reference/plan-format.md +14 -12
  76. package/.rulesync/reference/refs-map.md +12 -5
  77. package/.rulesync/reference/rtk.md +3 -2
  78. package/.rulesync/reference/scope.md +37 -0
  79. package/.rulesync/reference/setup.md +27 -0
  80. package/.rulesync/reference/skills-map.md +13 -9
  81. package/.rulesync/reference/speak.md +59 -0
  82. package/.rulesync/rules/CLAUDE.md +3 -44
  83. package/.rulesync/rules/overview.md +9 -42
  84. package/.rulesync/skills/ask/SKILL.md +11 -0
  85. package/.rulesync/skills/auto/SKILL.md +11 -0
  86. package/.rulesync/skills/browser/SKILL.md +4 -8
  87. package/.rulesync/skills/clasp/SKILL.md +9 -0
  88. package/.rulesync/skills/commit/SKILL.md +3 -1
  89. package/.rulesync/skills/commit-push/SKILL.md +2 -0
  90. package/.rulesync/skills/commit-push-deploy/SKILL.md +2 -0
  91. package/.rulesync/skills/commit-push-release/SKILL.md +2 -0
  92. package/.rulesync/skills/explain/SKILL.md +7 -14
  93. package/.rulesync/skills/google-workspace/SKILL.md +11 -0
  94. package/.rulesync/skills/handoff/SKILL.md +7 -11
  95. package/.rulesync/skills/implement-plan/SKILL.md +5 -116
  96. package/.rulesync/skills/listen/SKILL.md +13 -0
  97. package/.rulesync/skills/pickup/SKILL.md +6 -12
  98. package/.rulesync/skills/speak/SKILL.md +13 -0
  99. package/.rulesync/skills/tmp/SKILL.md +54 -0
  100. package/.rulesync/skills/write-plan/SKILL.md +5 -80
  101. package/.rulesync/subagents/grunt.md +2 -23
  102. package/.rulesync/subagents/orchestrator.md +14 -54
  103. package/AGENTS.md +9 -42
  104. package/CHANGELOG.md +89 -0
  105. package/CLAUDE.md +1 -44
  106. package/README.md +97 -88
  107. package/cli/grunt.mjs +29 -13
  108. package/cli/grunt.test.ts +62 -20
  109. package/cli/init.mjs +218 -31
  110. package/cli/init.test.ts +669 -46
  111. package/cli/prompt.mjs +8 -0
  112. package/cli/prompt.test.ts +14 -0
  113. package/package.json +13 -1
  114. package/scripts/browser.mjs +98 -5
  115. package/scripts/doctor.mjs +82 -1
  116. package/scripts/emit-gemini.mjs +1 -1
  117. package/scripts/emit-mcp-policy.mjs +1 -1
  118. package/scripts/gate-fat-tools.mjs +223 -173
  119. package/scripts/google-workspace.mjs +713 -0
  120. package/scripts/grunt-job.mjs +453 -69
  121. package/scripts/listen.mjs +829 -0
  122. package/scripts/parse-need.mjs +21 -2
  123. package/scripts/persist-implementation.mjs +278 -0
  124. package/scripts/persist-plan.mjs +17 -2
  125. package/scripts/persist-tmp.mjs +123 -0
  126. package/scripts/scrub-spawn-prompt.mjs +4 -51
  127. package/scripts/setup.mjs +746 -0
  128. package/scripts/speak.mjs +291 -0
  129. package/.agents/agents/implementer.md +0 -32
  130. package/.agents/agents/thinker.md +0 -28
  131. package/.agents/skills/cascade/SKILL.md +0 -26
  132. package/.agents/skills/parent/SKILL.md +0 -6
  133. package/.agents/skills/solo/SKILL.md +0 -23
  134. package/.claude/agents/implementer.md +0 -37
  135. package/.claude/agents/thinker.md +0 -35
  136. package/.claude/skills/cascade/SKILL.md +0 -26
  137. package/.claude/skills/parent/SKILL.md +0 -6
  138. package/.claude/skills/solo/SKILL.md +0 -23
  139. package/.codex/agents/implementer.toml +0 -26
  140. package/.codex/agents/thinker.toml +0 -25
  141. package/.grok/agents/implementer.md +0 -27
  142. package/.grok/agents/thinker.md +0 -26
  143. package/.grok/parent.md +0 -1
  144. package/.grok/roles/implementer.toml +0 -5
  145. package/.grok/roles/thinker.toml +0 -5
  146. package/.grok/skills/cascade/SKILL.md +0 -26
  147. package/.grok/skills/parent/SKILL.md +0 -6
  148. package/.grok/skills/solo/SKILL.md +0 -23
  149. package/.rulesync/skills/cascade/SKILL.md +0 -26
  150. package/.rulesync/skills/parent/SKILL.md +0 -6
  151. package/.rulesync/skills/solo/SKILL.md +0 -23
  152. package/.rulesync/subagents/implementer.md +0 -49
  153. package/.rulesync/subagents/thinker.md +0 -48
package/cli/init.test.ts CHANGED
@@ -1,16 +1,27 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { afterEach, describe, expect, it, vi } from "vitest";
5
6
  import {
6
7
  MAX_GUARDED_MARKDOWN_BYTES,
7
8
  composeGuardedMarkdown,
8
9
  destAlreadyInited,
9
10
  extractGruntBody,
11
+ extractUserMarkdown,
10
12
  GUARDED_ROOT_FILES,
13
+ SENTINEL_BEGIN,
14
+ SENTINEL_END,
11
15
  guardedMarkdownDrift,
12
16
  healGuardedRootFile,
13
17
  init,
18
+ pruneRetired,
19
+ RETIRED_SKILLS,
20
+ RETIRED_AGENTS,
21
+ RETIRED_SCRIPTS,
22
+ RETIRED_PATHS,
23
+ RESERVED_SKILLS,
24
+ LAUNCH_SCRIPTS,
14
25
  mergeClaudeSettings,
15
26
  mergeGitignore,
16
27
  mergeGuardedContent,
@@ -20,6 +31,7 @@ import {
20
31
  samePath,
21
32
  shouldAutoSkipGlobals,
22
33
  snapshotGuardedRoots,
34
+ toGruntScriptName,
23
35
  withGuardedCheckInteriors,
24
36
  writeMergedGuardedFile,
25
37
  } from "./init.mjs";
@@ -53,12 +65,18 @@ const PRODUCT_FILES = [
53
65
  "grunt-job.mjs",
54
66
  "parse-need.mjs",
55
67
  "persist-handoff.mjs",
68
+ "persist-implementation.mjs",
69
+ "persist-tmp.mjs",
56
70
  "persist-plan.mjs",
57
71
  "purge-global-mcps.mjs",
58
72
  "scrub-spawn-prompt.mjs",
59
73
  "scrub-text-lib.mjs",
60
74
  "sync-global-settings.mjs",
61
75
  "browser.mjs",
76
+ "speak.mjs",
77
+ "listen.mjs",
78
+ "google-workspace.mjs",
79
+ "setup.mjs",
62
80
  "doctor.mjs",
63
81
  "skill-conflicts.mjs",
64
82
  ];
@@ -154,18 +172,22 @@ describe("samePath", () => {
154
172
  });
155
173
  });
156
174
 
175
+ const GI_TMP = ".tmp/\n";
176
+
157
177
  describe("mergeGitignore", () => {
158
178
  it("creates .tmp/ when missing", () => {
159
179
  const dest = tmp("gi-miss-");
160
180
  mergeGitignore(dest);
161
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
181
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(GI_TMP);
162
182
  });
163
183
 
164
- it("noop when .tmp/ present", () => {
184
+ it("noop when .tmp/ already present", () => {
165
185
  const dest = tmp("gi-slash-");
166
186
  fs.writeFileSync(path.join(dest, ".gitignore"), "foo\n.tmp/\nbar\n");
167
187
  mergeGitignore(dest);
168
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe("foo\n.tmp/\nbar\n");
188
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
189
+ "foo\n.tmp/\nbar\n",
190
+ );
169
191
  });
170
192
 
171
193
  it("noop when .tmp present", () => {
@@ -175,12 +197,21 @@ describe("mergeGitignore", () => {
175
197
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp\n");
176
198
  });
177
199
 
200
+ it("noop when .tmp/ present in mixed file", () => {
201
+ const dest = tmp("gi-both-");
202
+ fs.writeFileSync(path.join(dest, ".gitignore"), `foo\n${GI_TMP}bar\n`);
203
+ mergeGitignore(dest);
204
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
205
+ `foo\n${GI_TMP}bar\n`,
206
+ );
207
+ });
208
+
178
209
  it("appends after trailing newline", () => {
179
210
  const dest = tmp("gi-nl-");
180
211
  fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/\n");
181
212
  mergeGitignore(dest);
182
213
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
183
- "node_modules/\n.tmp/\n",
214
+ `node_modules/\n${GI_TMP}`,
184
215
  );
185
216
  });
186
217
 
@@ -189,7 +220,7 @@ describe("mergeGitignore", () => {
189
220
  fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/");
190
221
  mergeGitignore(dest);
191
222
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
192
- "node_modules/\n.tmp/\n",
223
+ `node_modules/\n${GI_TMP}`,
193
224
  );
194
225
  });
195
226
 
@@ -197,7 +228,7 @@ describe("mergeGitignore", () => {
197
228
  const dest = tmp("gi-empty-");
198
229
  fs.writeFileSync(path.join(dest, ".gitignore"), "");
199
230
  mergeGitignore(dest);
200
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
231
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(GI_TMP);
201
232
  });
202
233
  });
203
234
 
@@ -290,6 +321,67 @@ describe("mergeGuardedMarkdown", () => {
290
321
  );
291
322
  });
292
323
 
324
+ it("already-guarded package src stays begin=1 end=1", () => {
325
+ const pkgRoot = stubPkgRoot();
326
+ fs.writeFileSync(
327
+ path.join(pkgRoot, "CLAUDE.md"),
328
+ "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
329
+ );
330
+ const dest = tmp("md-wrapped-src-");
331
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
332
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
333
+ expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
334
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
335
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
336
+ });
337
+
338
+ it("merge heals dest with trailing orphan grunt:end", () => {
339
+ const pkgRoot = stubPkgRoot();
340
+ const dest = tmp("md-orphan-end-");
341
+ fs.writeFileSync(
342
+ path.join(dest, "CLAUDE.md"),
343
+ "<!-- grunt:begin -->\nold\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
344
+ );
345
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
346
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
347
+ expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
348
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
349
+ });
350
+
351
+ it("dest stacked researcher shape heals to exactly one pair", () => {
352
+ const pkgRoot = stubPkgRoot();
353
+ fs.writeFileSync(
354
+ path.join(pkgRoot, "CLAUDE.md"),
355
+ "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
356
+ );
357
+ const dest = tmp("md-stacked-");
358
+ fs.writeFileSync(
359
+ path.join(dest, "CLAUDE.md"),
360
+ "<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
361
+ );
362
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
363
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
364
+ expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
365
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
366
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
367
+ });
368
+
369
+ it("dest stacked keeps user prefix/suffix outside the pair", () => {
370
+ const pkgRoot = stubPkgRoot();
371
+ const dest = tmp("md-stacked-user-");
372
+ fs.writeFileSync(
373
+ path.join(dest, "CLAUDE.md"),
374
+ "prefix\n<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\nsuffix\n",
375
+ );
376
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
377
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
378
+ expect(out).toBe(
379
+ "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nprefix\nsuffix\n",
380
+ );
381
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
382
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
383
+ });
384
+
293
385
  it("does not fuzzy-dedupe unmarked protocol-looking user text", () => {
294
386
  const pkgRoot = stubPkgRoot();
295
387
  const dest = tmp("md-proto-");
@@ -363,6 +455,23 @@ describe("snapshot/remerge guarded roots", () => {
363
455
  );
364
456
  });
365
457
 
458
+ it("remerge after wrapped-src merge+generate does not reattach orphan end", () => {
459
+ const pkgRoot = stubPkgRoot();
460
+ fs.writeFileSync(
461
+ path.join(pkgRoot, "AGENTS.md"),
462
+ "<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\n",
463
+ );
464
+ const dest = tmp("snap-wrap-src-");
465
+ mergeGuardedMarkdown(dest, pkgRoot, "AGENTS.md");
466
+ const snap = snapshotGuardedRoots(dest);
467
+ fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
468
+ remergeGuardedRoots(dest, snap);
469
+ const out = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
470
+ expect(out).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
471
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
472
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
473
+ });
474
+
366
475
  it("withGuardedCheckInteriors is restored even if check throws", () => {
367
476
  const dest = tmp("check-int-");
368
477
  const live =
@@ -378,6 +487,22 @@ describe("snapshot/remerge guarded roots", () => {
378
487
  });
379
488
 
380
489
  it("compose/extract/drift/write helpers cover remaining branches", () => {
490
+ expect(
491
+ extractUserMarkdown(
492
+ "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
493
+ ),
494
+ ).toBe("");
495
+ expect(
496
+ extractUserMarkdown(
497
+ "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\nkeep\n<!-- grunt:end -->\n",
498
+ ),
499
+ ).toBe("keep\n");
500
+ expect(
501
+ composeGuardedMarkdown(
502
+ "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
503
+ "<!-- grunt:end -->\n",
504
+ ),
505
+ ).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
381
506
  expect(composeGuardedMarkdown("body", null)).toBe(
382
507
  "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
383
508
  );
@@ -402,7 +527,9 @@ describe("snapshot/remerge guarded roots", () => {
402
527
  expect(mergeGuardedContent("keep\n", "g\r\n")).toBe(
403
528
  "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\nkeep\n",
404
529
  );
405
- expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toContain("only");
530
+ expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toBe(
531
+ "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
532
+ );
406
533
  const dest = tmp("md-helpers-");
407
534
  const missing = path.join(dest, "nope.md");
408
535
  expect(guardedMarkdownDrift(missing, "g")).toBe(true);
@@ -423,6 +550,73 @@ describe("snapshot/remerge guarded roots", () => {
423
550
  expect(writeMergedGuardedFile(huge, "g")).toBe("aborted-unsafe");
424
551
  });
425
552
 
553
+ it("unclosed begin unwraps interior; inline orphan end is stripped", () => {
554
+ expect(extractGruntBody("<!-- grunt:begin -->\nhello")).toBe("hello");
555
+ const composed = composeGuardedMarkdown("<!-- grunt:begin -->\nhello", "");
556
+ expect(composed).toBe("<!-- grunt:begin -->\nhello\n<!-- grunt:end -->\n");
557
+ expect(composed.split(SENTINEL_BEGIN)).toHaveLength(2);
558
+ expect(composed.split(SENTINEL_END)).toHaveLength(2);
559
+ expect(composed).not.toContain(`${SENTINEL_BEGIN}\n${SENTINEL_BEGIN}`);
560
+ expect(mergeGuardedContent("note\n<!-- grunt:begin -->\nstale", "new")).toBe(
561
+ "<!-- grunt:begin -->\nnew\n<!-- grunt:end -->\nnote\n",
562
+ );
563
+ expect(extractUserMarkdown("keep <!-- grunt:end --> me")).not.toContain(SENTINEL_END);
564
+ expect(extractUserMarkdown("keep <!-- grunt:end --> me")).toBe("keep me");
565
+ expect(extractUserMarkdown("keep\n<!-- grunt:end -->\nme")).toBe("keep\nme");
566
+ expect(extractUserMarkdown("keep\n <!-- grunt:end --> \nme")).toBe("keep\nme");
567
+ expect(extractGruntBody("<!-- grunt:begin -->\r\nhello")).toBe("hello");
568
+ expect(extractGruntBody("<!-- grunt:begin -->")).toBe("");
569
+ const emptyWrap = composeGuardedMarkdown("<!-- grunt:begin -->", "");
570
+ expect(emptyWrap.split(SENTINEL_BEGIN)).toHaveLength(2);
571
+ expect(emptyWrap).toBe("<!-- grunt:begin -->\n\n<!-- grunt:end -->\n");
572
+ const mixed =
573
+ "<!-- grunt:begin -->\nok\n<!-- grunt:end -->\nuser bit\n<!-- grunt:begin -->\nstale";
574
+ expect(extractGruntBody(mixed)).toBe("ok");
575
+ expect(extractUserMarkdown(mixed)).toBe("user bit\n");
576
+ expect(
577
+ composeGuardedMarkdown("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n", ""),
578
+ ).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
579
+ });
580
+
581
+ it("extract/compose peel stacked dest and trailing orphan end", () => {
582
+ const stacked =
583
+ "<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\n";
584
+ expect(extractGruntBody(stacked)).toBe("BODY");
585
+ const stackedOut = composeGuardedMarkdown(stacked, "");
586
+ expect(stackedOut).toBe("<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n");
587
+ expect(stackedOut.split(SENTINEL_BEGIN)).toHaveLength(2);
588
+ expect(stackedOut.split(SENTINEL_END)).toHaveLength(2);
589
+ expect(composeGuardedMarkdown("BODY\n<!-- grunt:end -->\n", "")).toBe(
590
+ "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n",
591
+ );
592
+ expect(extractGruntBody("BODY\n\n<!-- grunt:end -->")).toBe("BODY");
593
+ expect(mergeGuardedContent(stacked, "SRC")).toBe(
594
+ "<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\n",
595
+ );
596
+ expect(mergeGuardedContent("keep\n<!-- grunt:end -->\n", "SRC")).toBe(
597
+ "<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\nkeep\n",
598
+ );
599
+ expect(
600
+ extractGruntBody("<!-- grunt:begin --> \nBODY\n<!-- grunt:end --> \n"),
601
+ ).toBe("BODY");
602
+ expect(
603
+ extractGruntBody(" <!-- grunt:begin -->\nBODY\n <!-- grunt:end -->\n"),
604
+ ).toBe("BODY");
605
+ expect(extractGruntBody("<!-- grunt:begin -->\n<!-- grunt:end -->")).toBe("");
606
+ expect(
607
+ extractGruntBody(
608
+ "<!-- grunt:begin -->\nsee <!-- grunt:end --> here\n<!-- grunt:end -->\n",
609
+ ),
610
+ ).toBe("see <!-- grunt:end --> here");
611
+ const midLine =
612
+ "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\nstill user\n<!-- grunt:end -->\n";
613
+ expect(extractGruntBody(midLine)).toBe("BODY");
614
+ expect(extractUserMarkdown(midLine)).toBe("still user\n");
615
+ expect(composeGuardedMarkdown(extractGruntBody(midLine) ?? "", extractUserMarkdown(midLine))).toBe(
616
+ "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\nstill user\n",
617
+ );
618
+ });
619
+
426
620
  it("remerge restores unsafe, missing, binary current, and created files", () => {
427
621
  const dest = tmp("remerge-edges-");
428
622
  const agents = path.join(dest, "AGENTS.md");
@@ -786,6 +980,13 @@ describe("mergeClaudeSettings", () => {
786
980
  });
787
981
  });
788
982
 
983
+ describe("toGruntScriptName", () => {
984
+ it("prefixes unprefixed keys and does not double-prefix", () => {
985
+ expect(toGruntScriptName("rulesync:generate")).toBe("grunt:rulesync:generate");
986
+ expect(toGruntScriptName("grunt:doctor")).toBe("grunt:doctor");
987
+ });
988
+ });
989
+
789
990
  describe("mergePackageJson", () => {
790
991
  it("creates {} dest and merges scripts skipping test, injects deps, sorts keys", () => {
791
992
  const pkgRoot = stubPkgRoot();
@@ -793,11 +994,25 @@ describe("mergePackageJson", () => {
793
994
  mergePackageJson(dest, pkgRoot);
794
995
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
795
996
  expect(out.scripts).toEqual({
796
- alpha: "a",
797
- "rulesync:generate": "gen",
798
- zeta: "z",
997
+ "grunt:alpha": "a",
998
+ "grunt:antigravity": LAUNCH_SCRIPTS.antigravity,
999
+ "grunt:claude": LAUNCH_SCRIPTS.claude,
1000
+ "grunt:codex": LAUNCH_SCRIPTS.codex,
1001
+ "grunt:gemini": LAUNCH_SCRIPTS.gemini,
1002
+ "grunt:grok": LAUNCH_SCRIPTS.grok,
1003
+ "grunt:rulesync:generate": "gen",
1004
+ "grunt:zeta": "z",
799
1005
  });
800
- expect(Object.keys(out.scripts)).toEqual(["alpha", "rulesync:generate", "zeta"]);
1006
+ expect(Object.keys(out.scripts)).toEqual([
1007
+ "grunt:alpha",
1008
+ "grunt:antigravity",
1009
+ "grunt:claude",
1010
+ "grunt:codex",
1011
+ "grunt:gemini",
1012
+ "grunt:grok",
1013
+ "grunt:rulesync:generate",
1014
+ "grunt:zeta",
1015
+ ]);
801
1016
  expect(out.devDependencies).toEqual({
802
1017
  rulesync: "latest",
803
1018
  "smol-toml": "^1.8.0",
@@ -817,14 +1032,21 @@ describe("mergePackageJson", () => {
817
1032
  devDependencies: { lodash: "4" },
818
1033
  }),
819
1034
  );
1035
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
820
1036
  mergePackageJson(dest, pkgRoot);
821
1037
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
822
1038
  expect(out.scripts.test).toBe("jest");
823
1039
  expect(out.scripts.alpha).toBe("old");
1040
+ expect(out.scripts["grunt:alpha"]).toBe("a");
1041
+ expect(out.scripts["grunt:zeta"]).toBe("z");
824
1042
  expect(out.scripts.foo).toBe("bar");
825
1043
  expect(out.devDependencies.lodash).toBe("4");
826
1044
  expect(out.devDependencies["smol-toml"]).toBe("^1.8.0");
827
1045
  expect(out.devDependencies.rulesync).toBe("latest");
1046
+ expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
1047
+ true,
1048
+ );
1049
+ warnSpy.mockRestore();
828
1050
  });
829
1051
 
830
1052
  it("preserves dest suffix after an upgraded grunt script prefix", () => {
@@ -844,9 +1066,10 @@ describe("mergePackageJson", () => {
844
1066
  );
845
1067
  mergePackageJson(dest, pkgRoot);
846
1068
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
847
- expect(out.scripts["rulesync:generate"]).toBe(
1069
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
848
1070
  "rulesync generate -t bar && node scripts/codex-sync.mjs",
849
1071
  );
1072
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
850
1073
  });
851
1074
 
852
1075
  it("preserves suffix after a multi-command grunt script upgrade", () => {
@@ -869,7 +1092,7 @@ describe("mergePackageJson", () => {
869
1092
  );
870
1093
  mergePackageJson(dest, pkgRoot);
871
1094
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
872
- expect(out.scripts["rulesync:generate"]).toBe(
1095
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
873
1096
  "rulesync generate -t bar && node scripts/emit.mjs && node scripts/codex-sync.mjs",
874
1097
  );
875
1098
  });
@@ -889,6 +1112,7 @@ describe("mergePackageJson", () => {
889
1112
  mergePackageJson(dest, pkgRoot);
890
1113
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
891
1114
  expect(out.scripts.alpha).toBe("echo custom && extra");
1115
+ expect(out.scripts["grunt:alpha"]).toBe("a");
892
1116
  expect(warnSpy).toHaveBeenCalled();
893
1117
  warnSpy.mockRestore();
894
1118
  });
@@ -910,6 +1134,7 @@ describe("mergePackageJson", () => {
910
1134
  mergePackageJson(dest, pkgRoot);
911
1135
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
912
1136
  expect(out.scripts["rulesync:generate"]).toBe("echo custom;echo extra");
1137
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
913
1138
  expect(warnSpy).toHaveBeenCalled();
914
1139
  warnSpy.mockRestore();
915
1140
  });
@@ -927,7 +1152,8 @@ describe("mergePackageJson", () => {
927
1152
  );
928
1153
  mergePackageJson(dest, pkgRoot);
929
1154
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
930
- expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
1155
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
1156
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
931
1157
  });
932
1158
 
933
1159
  it("keeps dest when it already starts with the new src value plus suffix", () => {
@@ -941,7 +1167,8 @@ describe("mergePackageJson", () => {
941
1167
  fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ scripts: { "rulesync:generate": cur } }));
942
1168
  mergePackageJson(dest, pkgRoot);
943
1169
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
944
- expect(out.scripts["rulesync:generate"]).toBe(cur);
1170
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(cur);
1171
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
945
1172
  });
946
1173
 
947
1174
  it("keeps unrelated custom script values and dest-only keys, warns on skip", () => {
@@ -957,9 +1184,11 @@ describe("mergePackageJson", () => {
957
1184
  mergePackageJson(dest, pkgRoot);
958
1185
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
959
1186
  expect(out.scripts.alpha).toBe("echo custom");
1187
+ expect(out.scripts["grunt:alpha"]).toBe("a");
960
1188
  expect(out.scripts.foo).toBe("bar");
961
1189
  expect(out.scripts.test).toBe("jest");
962
- expect(out.scripts.zeta).toBe("z");
1190
+ expect(out.scripts["grunt:zeta"]).toBe("z");
1191
+ expect(out.scripts.zeta).toBeUndefined();
963
1192
  expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
964
1193
  true,
965
1194
  );
@@ -980,7 +1209,8 @@ describe("mergePackageJson", () => {
980
1209
  const dest = tmp("pj-raw-src-");
981
1210
  mergePackageJson(dest, pkgRoot);
982
1211
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
983
- expect(out.scripts["rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1212
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1213
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
984
1214
  expect(out.scripts["rulesync:generate:raw"]).toBeUndefined();
985
1215
  expect(out.scripts["rulesync:check:raw"]).toBeUndefined();
986
1216
  expect(out.scripts["rulesync:watch:raw"]).toBeUndefined();
@@ -1001,7 +1231,7 @@ describe("mergePackageJson", () => {
1001
1231
  );
1002
1232
  mergePackageJson(dest, pkgRoot);
1003
1233
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1004
- expect(out.scripts["rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1234
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1005
1235
  expect(out.scripts["rulesync:generate:raw"]).toBe("echo consumer-raw");
1006
1236
  });
1007
1237
 
@@ -1020,7 +1250,9 @@ describe("mergePackageJson", () => {
1020
1250
  );
1021
1251
  mergePackageJson(dest, pkgRoot);
1022
1252
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1023
- expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
1253
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
1254
+ expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
1255
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
1024
1256
  });
1025
1257
 
1026
1258
  it("early return when name is @lovrozagar/grunt", () => {
@@ -1040,9 +1272,257 @@ describe("mergePackageJson", () => {
1040
1272
  const dest = tmp("pj-noscripts-");
1041
1273
  mergePackageJson(dest, pkgRoot);
1042
1274
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1043
- expect(out.scripts).toEqual({});
1275
+ expect(out.scripts).toEqual({
1276
+ "grunt:antigravity": LAUNCH_SCRIPTS.antigravity,
1277
+ "grunt:claude": LAUNCH_SCRIPTS.claude,
1278
+ "grunt:codex": LAUNCH_SCRIPTS.codex,
1279
+ "grunt:gemini": LAUNCH_SCRIPTS.gemini,
1280
+ "grunt:grok": LAUNCH_SCRIPTS.grok,
1281
+ });
1044
1282
  expect(out.devDependencies).toEqual({ rulesync: "2", "smol-toml": "1" });
1045
1283
  });
1284
+
1285
+ it("golden autorun-apps: suffix keep, doctor move, check npm run rewrite, old keys gone", () => {
1286
+ const pkgRoot = stubPkgRoot({
1287
+ name: "fixture-pkg",
1288
+ scripts: {
1289
+ "rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
1290
+ "rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
1291
+ "rulesync:check": "node ./scripts/guarded-roots.mjs check",
1292
+ "hooks:union": "node scripts/hooks-union.mjs",
1293
+ "hooks:check": "node scripts/hooks-union.mjs --check",
1294
+ doctor: "node ./scripts/doctor.mjs",
1295
+ "rulesync:doctor": "rulesync doctor",
1296
+ "sync:globals": "node scripts/sync-global-settings.mjs",
1297
+ "sync:globals:check": "node scripts/check-globals.mjs",
1298
+ "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
1299
+ "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
1300
+ "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
1301
+ test: "vitest run --coverage",
1302
+ },
1303
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1304
+ });
1305
+ const dest = tmp("pj-autorun-");
1306
+ fs.writeFileSync(
1307
+ path.join(dest, "package.json"),
1308
+ JSON.stringify({
1309
+ scripts: {
1310
+ "apps:doctor": "node scripts/audit.mjs",
1311
+ "apps:mental-model:viewer:build":
1312
+ "npm --prefix scripts/excalidraw/viewer install && npm --prefix scripts/excalidraw/viewer run build",
1313
+ check:
1314
+ "node scripts/portability-check.mjs && npm run rulesync:check && npm run lint && npm test && node scripts/media-fixtures.mjs --check",
1315
+ "codex:sync": "node scripts/codex-sync.mjs",
1316
+ doctor: "node ./scripts/doctor.mjs",
1317
+ "hooks:check": "node scripts/hooks-union.mjs --check",
1318
+ "hooks:union": "node scripts/hooks-union.mjs",
1319
+ lint: "eslint scripts tests eslint.config.mjs",
1320
+ "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
1321
+ "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
1322
+ "rulesync:check": "node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1323
+ "rulesync:doctor": "rulesync doctor",
1324
+ "rulesync:generate": "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1325
+ "rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
1326
+ "sync:globals": "node scripts/sync-global-settings.mjs",
1327
+ "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
1328
+ "sync:globals:check": "node scripts/check-globals.mjs",
1329
+ test: "node scripts/run-tests.mjs",
1330
+ },
1331
+ }),
1332
+ );
1333
+ mergePackageJson(dest, pkgRoot);
1334
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1335
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
1336
+ "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1337
+ );
1338
+ expect(out.scripts["grunt:rulesync:check"]).toBe(
1339
+ "node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1340
+ );
1341
+ expect(out.scripts["grunt:rulesync:watch"]).toBe("node ./scripts/guarded-roots.mjs watch");
1342
+ expect(out.scripts["grunt:rulesync:doctor"]).toBe("rulesync doctor");
1343
+ expect(out.scripts["grunt:hooks:union"]).toBe("node scripts/hooks-union.mjs");
1344
+ expect(out.scripts["grunt:hooks:check"]).toBe("node scripts/hooks-union.mjs --check");
1345
+ expect(out.scripts["grunt:sync:globals"]).toBe("node scripts/sync-global-settings.mjs");
1346
+ expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
1347
+ expect(out.scripts["grunt:sync:globals:check"]).toBe("node scripts/check-globals.mjs");
1348
+ expect(out.scripts["grunt:purge:global-mcps"]).toBe("node scripts/purge-global-mcps.mjs");
1349
+ expect(out.scripts["grunt:purge:global-mcps:apply"]).toBe("node scripts/purge-global-mcps.mjs --apply");
1350
+ expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
1351
+ expect(out.scripts.doctor).toBeUndefined();
1352
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
1353
+ expect(out.scripts["rulesync:check"]).toBeUndefined();
1354
+ expect(out.scripts["rulesync:watch"]).toBeUndefined();
1355
+ expect(out.scripts["hooks:union"]).toBeUndefined();
1356
+ expect(out.scripts["sync:globals"]).toBeUndefined();
1357
+ expect(out.scripts.check).toBe(
1358
+ "node scripts/portability-check.mjs && npm run grunt:rulesync:check && npm run lint && npm test && node scripts/media-fixtures.mjs --check",
1359
+ );
1360
+ expect(out.scripts["apps:doctor"]).toBe("node scripts/audit.mjs");
1361
+ expect(out.scripts["codex:sync"]).toBe("node scripts/codex-sync.mjs");
1362
+ expect(out.scripts.lint).toBe("eslint scripts tests eslint.config.mjs");
1363
+ expect(out.scripts.test).toBe("node scripts/run-tests.mjs");
1364
+ });
1365
+
1366
+ it("leaves custom doctor and writes grunt:doctor from SoT", () => {
1367
+ const pkgRoot = stubPkgRoot({
1368
+ name: "fixture-pkg",
1369
+ scripts: { doctor: "node ./scripts/doctor.mjs" },
1370
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1371
+ });
1372
+ const dest = tmp("pj-custom-doctor-");
1373
+ fs.writeFileSync(
1374
+ path.join(dest, "package.json"),
1375
+ JSON.stringify({ scripts: { doctor: "node scripts/audit.mjs" } }),
1376
+ );
1377
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1378
+ mergePackageJson(dest, pkgRoot);
1379
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1380
+ expect(out.scripts.doctor).toBe("node scripts/audit.mjs");
1381
+ expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
1382
+ expect(warnSpy).toHaveBeenCalled();
1383
+ warnSpy.mockRestore();
1384
+ });
1385
+
1386
+ it("merges destKey when both prefixed and owned legacy exist, deletes legacy", () => {
1387
+ const pkgRoot = stubPkgRoot({
1388
+ name: "fixture-pkg",
1389
+ scripts: { "rulesync:generate": "rulesync generate -t bar" },
1390
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1391
+ });
1392
+ const dest = tmp("pj-both-");
1393
+ fs.writeFileSync(
1394
+ path.join(dest, "package.json"),
1395
+ JSON.stringify({
1396
+ scripts: {
1397
+ "rulesync:generate": "rulesync generate -t foo && node scripts/codex-sync.mjs",
1398
+ "grunt:rulesync:generate": "rulesync generate -t old && node scripts/other.mjs",
1399
+ },
1400
+ }),
1401
+ );
1402
+ mergePackageJson(dest, pkgRoot);
1403
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1404
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
1405
+ "rulesync generate -t bar && node scripts/other.mjs",
1406
+ );
1407
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
1408
+ });
1409
+
1410
+ it("does not rewrite npm run rulesync:check:raw when only rulesync:check migrated", () => {
1411
+ const pkgRoot = stubPkgRoot({
1412
+ name: "fixture-pkg",
1413
+ scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
1414
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1415
+ });
1416
+ const dest = tmp("pj-raw-rewrite-");
1417
+ fs.writeFileSync(
1418
+ path.join(dest, "package.json"),
1419
+ JSON.stringify({
1420
+ scripts: {
1421
+ "rulesync:check": "node ./scripts/guarded-roots.mjs check",
1422
+ "rulesync:check:raw": "echo consumer-raw",
1423
+ check: "npm run rulesync:check && npm run rulesync:check:raw",
1424
+ },
1425
+ }),
1426
+ );
1427
+ mergePackageJson(dest, pkgRoot);
1428
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1429
+ expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
1430
+ expect(out.scripts["rulesync:check"]).toBeUndefined();
1431
+ expect(out.scripts["rulesync:check:raw"]).toBe("echo consumer-raw");
1432
+ expect(out.scripts.check).toBe("npm run grunt:rulesync:check && npm run rulesync:check:raw");
1433
+ });
1434
+
1435
+ it("skips non-string dest scripts when rewriting npm run refs", () => {
1436
+ const pkgRoot = stubPkgRoot({
1437
+ name: "fixture-pkg",
1438
+ scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
1439
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1440
+ });
1441
+ const dest = tmp("pj-nonstr-");
1442
+ fs.writeFileSync(
1443
+ path.join(dest, "package.json"),
1444
+ JSON.stringify({
1445
+ scripts: {
1446
+ extra: 1,
1447
+ check: "npm run rulesync:check",
1448
+ },
1449
+ }),
1450
+ );
1451
+ mergePackageJson(dest, pkgRoot);
1452
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1453
+ expect(out.scripts.extra).toBe(1);
1454
+ expect(out.scripts.check).toBe("npm run grunt:rulesync:check");
1455
+ expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
1456
+ });
1457
+
1458
+ it("emits grunt:claude into a consumer package.json", () => {
1459
+ const pkgRoot = stubPkgRoot();
1460
+ const dest = tmp("pj-launch-emit-");
1461
+ mergePackageJson(dest, pkgRoot);
1462
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1463
+ expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
1464
+ });
1465
+
1466
+ it("leaves a consumer's bare claude launcher untouched, no warn emitted", () => {
1467
+ const pkgRoot = stubPkgRoot();
1468
+ const dest = tmp("pj-consumer-claude-");
1469
+ fs.writeFileSync(
1470
+ path.join(dest, "package.json"),
1471
+ JSON.stringify({ scripts: { claude: "npm run build && claude" } }),
1472
+ );
1473
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1474
+ mergePackageJson(dest, pkgRoot);
1475
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1476
+ expect(out.scripts.claude).toBe("npm run build && claude");
1477
+ expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
1478
+ expect(warn).not.toHaveBeenCalled();
1479
+ warn.mockRestore();
1480
+ });
1481
+
1482
+ it("does not rewrite npm run claude inside a consumer script", () => {
1483
+ const pkgRoot = stubPkgRoot();
1484
+ const dest = tmp("pj-npmrun-claude-");
1485
+ fs.writeFileSync(
1486
+ path.join(dest, "package.json"),
1487
+ JSON.stringify({
1488
+ scripts: { claude: "npm run build && claude", start: "npm run claude" },
1489
+ }),
1490
+ );
1491
+ mergePackageJson(dest, pkgRoot);
1492
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1493
+ expect(out.scripts.start).toBe("npm run claude");
1494
+ });
1495
+
1496
+ it("purges a stale grunt:yolo:claude with the exact old value; an edited variant survives", () => {
1497
+ const pkgRoot = stubPkgRoot();
1498
+ const dest = tmp("pj-purge-stale-");
1499
+ fs.writeFileSync(
1500
+ path.join(dest, "package.json"),
1501
+ JSON.stringify({
1502
+ scripts: {
1503
+ "grunt:yolo:claude": LAUNCH_SCRIPTS.claude,
1504
+ "grunt:yolo:codex": `${LAUNCH_SCRIPTS.codex} --extra`,
1505
+ },
1506
+ }),
1507
+ );
1508
+ mergePackageJson(dest, pkgRoot);
1509
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1510
+ expect(out.scripts["grunt:yolo:claude"]).toBeUndefined();
1511
+ expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
1512
+ expect(out.scripts["grunt:yolo:codex"]).toBe(`${LAUNCH_SCRIPTS.codex} --extra`);
1513
+ });
1514
+
1515
+ it("drift: every bare launcher key in grunt's package.json equals LAUNCH_SCRIPTS[key]", () => {
1516
+ const pkg = JSON.parse(
1517
+ fs.readFileSync(
1518
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
1519
+ "utf8",
1520
+ ),
1521
+ );
1522
+ for (const [k, v] of Object.entries(LAUNCH_SCRIPTS)) {
1523
+ expect(pkg.scripts[k]).toBe(v);
1524
+ }
1525
+ });
1046
1526
  });
1047
1527
 
1048
1528
  describe("init", () => {
@@ -1076,6 +1556,9 @@ describe("init", () => {
1076
1556
  if (d === ".claude") continue;
1077
1557
  expect(fs.readFileSync(path.join(dest, d, "marker"), "utf8")).toBe(d);
1078
1558
  }
1559
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1560
+ expect(PRODUCT_FILES).not.toContain("grunt-config.mjs");
1561
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1079
1562
  expect(fs.readFileSync(path.join(dest, ".claude", "marker"), "utf8")).toBe(".claude");
1080
1563
  const settings = JSON.parse(
1081
1564
  fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
@@ -1103,9 +1586,9 @@ describe("init", () => {
1103
1586
 
1104
1587
  expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
1105
1588
  ["npm", ["install"]],
1106
- ["npm", ["run", "rulesync:generate"]],
1107
- ["npm", ["run", "sync:globals:apply"]],
1108
- ["npm", ["run", "rulesync:check"]],
1589
+ ["npm", ["run", "grunt:rulesync:generate"]],
1590
+ ["npm", ["run", "grunt:sync:globals:apply"]],
1591
+ ["npm", ["run", "grunt:rulesync:check"]],
1109
1592
  ]);
1110
1593
  for (const call of exec.mock.calls) {
1111
1594
  expect(call[2]).toEqual({ cwd: dest, stdio: "inherit" });
@@ -1149,18 +1632,132 @@ describe("init", () => {
1149
1632
  expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
1150
1633
  });
1151
1634
 
1635
+ it("prunes retired 0.5 skills and agents on dest", () => {
1636
+ const pkgRoot = stubPkgRoot();
1637
+ const dest = tmp("grunt-prune-");
1638
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "solo"), { recursive: true });
1639
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "solo", "SKILL.md"), "old");
1640
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "parent"), { recursive: true });
1641
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "cascade"), { recursive: true });
1642
+ fs.mkdirSync(path.join(dest, ".claude", "agents"), { recursive: true });
1643
+ fs.writeFileSync(path.join(dest, ".claude", "agents", "implementer.md"), "old");
1644
+ fs.writeFileSync(path.join(dest, ".claude", "agents", "thinker.md"), "old");
1645
+ expect(RETIRED_SKILLS).toEqual(["parent", "solo", "cascade"]);
1646
+ expect(RETIRED_AGENTS).toEqual(["implementer", "thinker"]);
1647
+ expect(RETIRED_SCRIPTS).toEqual(["telemetry.mjs", "grunt-config.mjs"]);
1648
+ expect(RETIRED_PATHS).toEqual([
1649
+ ".grok/parent.md",
1650
+ ".grok/skills/shared",
1651
+ ".rulesync/grunt.config.jsonc",
1652
+ ".rulesync/grunt.config.local.jsonc",
1653
+ ".rulesync/grunt.config.local.jsonc.example",
1654
+ ]);
1655
+ expect(RESERVED_SKILLS).toEqual([
1656
+ "ask",
1657
+ "auto",
1658
+ "browser",
1659
+ "clasp",
1660
+ "commit",
1661
+ "commit-and-push",
1662
+ "commit-push",
1663
+ "commit-push-deploy",
1664
+ "commit-push-release",
1665
+ "explain",
1666
+ "google-workspace",
1667
+ "handoff",
1668
+ "implement-plan",
1669
+ "listen",
1670
+ "pickup",
1671
+ "speak",
1672
+ "tmp",
1673
+ "write-plan",
1674
+ ]);
1675
+ expect(RESERVED_SKILLS).not.toEqual(expect.arrayContaining(RETIRED_SKILLS));
1676
+ init(dest, { pkgRoot, execFileSync: vi.fn() });
1677
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "solo"))).toBe(false);
1678
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "parent"))).toBe(false);
1679
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "cascade"))).toBe(false);
1680
+ expect(fs.existsSync(path.join(dest, ".claude", "agents", "implementer.md"))).toBe(
1681
+ false,
1682
+ );
1683
+ expect(fs.existsSync(path.join(dest, ".claude", "agents", "thinker.md"))).toBe(false);
1684
+ pruneRetired(dest, { pkgRoot });
1685
+ });
1686
+
1687
+ it("prunes retired scripts/paths, reserved-not-packaged skills, and grok roles; keeps consumer extras", () => {
1688
+ const pkgRoot = stubPkgRoot();
1689
+ fs.mkdirSync(path.join(pkgRoot, ".rulesync", "skills", "tmp"), { recursive: true });
1690
+ fs.writeFileSync(path.join(pkgRoot, ".rulesync", "skills", "tmp", "SKILL.md"), "pkg tmp");
1691
+ const dest = tmp("grunt-prune-extra-");
1692
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "tmp"), { recursive: true });
1693
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "tmp", "SKILL.md"), "old tmp");
1694
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "browser"), { recursive: true });
1695
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "browser", "SKILL.md"), "stale reserved");
1696
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "acme"), { recursive: true });
1697
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "acme", "SKILL.md"), "consumer");
1698
+ fs.mkdirSync(path.join(dest, ".grok", "roles"), { recursive: true });
1699
+ fs.writeFileSync(path.join(dest, ".grok", "roles", "implementer.toml"), "old");
1700
+ fs.writeFileSync(path.join(dest, ".grok", "roles", "thinker.toml"), "old");
1701
+ fs.writeFileSync(path.join(dest, ".grok", "parent.md"), "pointer");
1702
+ fs.mkdirSync(path.join(dest, ".grok", "skills", "shared"), { recursive: true });
1703
+ fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1704
+ fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "old telemetry");
1705
+ fs.writeFileSync(path.join(dest, "scripts", "grunt-config.mjs"), "old config");
1706
+ fs.writeFileSync(path.join(dest, "scripts", "mine.mjs"), "keep");
1707
+ fs.writeFileSync(path.join(dest, ".rulesync", "grunt.config.jsonc"), '{"version":1}\n');
1708
+ fs.writeFileSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc"), '{"sessionGate":"ask"}\n');
1709
+ fs.writeFileSync(
1710
+ path.join(dest, ".rulesync", "grunt.config.local.jsonc.example"),
1711
+ '{"sessionGate":"ask"}\n',
1712
+ );
1713
+ fs.mkdirSync(path.join(dest, ".gemini", "agents", "implementer"), { recursive: true });
1714
+ fs.writeFileSync(path.join(dest, ".gemini", "agents", "implementer", "agent.md"), "old");
1715
+ init(dest, { pkgRoot, execFileSync: vi.fn() });
1716
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "tmp", "SKILL.md"), "utf8")).toBe(
1717
+ "pkg tmp",
1718
+ );
1719
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "browser"))).toBe(false);
1720
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "acme", "SKILL.md"), "utf8")).toBe(
1721
+ "consumer",
1722
+ );
1723
+ expect(fs.existsSync(path.join(dest, ".grok", "roles", "implementer.toml"))).toBe(false);
1724
+ expect(fs.existsSync(path.join(dest, ".grok", "roles", "thinker.toml"))).toBe(false);
1725
+ expect(fs.existsSync(path.join(dest, ".grok", "parent.md"))).toBe(false);
1726
+ expect(fs.existsSync(path.join(dest, ".grok", "skills", "shared"))).toBe(false);
1727
+ expect(fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))).toBe(false);
1728
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1729
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1730
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc"))).toBe(false);
1731
+ expect(
1732
+ fs.existsSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc.example")),
1733
+ ).toBe(false);
1734
+ expect(fs.readFileSync(path.join(dest, "scripts", "mine.mjs"), "utf8")).toBe("keep");
1735
+ expect(fs.existsSync(path.join(dest, ".gemini", "agents", "implementer"))).toBe(false);
1736
+ });
1737
+
1152
1738
  it("omitted pkgRoot uses built-in PKG_ROOT; exec mocked", () => {
1153
1739
  const dest = tmp("grunt-realroot-");
1154
1740
  const exec = vi.fn();
1155
1741
  init(dest, { execFileSync: exec });
1156
1742
  expect(fs.existsSync(path.join(dest, "AGENTS.md"))).toBe(true);
1743
+ const agents = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
1744
+ const claude = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
1745
+ expect(agents.split("<!-- grunt:begin -->")).toHaveLength(2);
1746
+ expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
1747
+ expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
1748
+ expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
1157
1749
  expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
1750
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1751
+ expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(true);
1752
+ expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(true);
1753
+ expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(true);
1158
1754
  expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
1755
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1159
1756
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1160
1757
  ["install"],
1161
- ["run", "rulesync:generate"],
1162
- ["run", "sync:globals:apply"],
1163
- ["run", "rulesync:check"],
1758
+ ["run", "grunt:rulesync:generate"],
1759
+ ["run", "grunt:sync:globals:apply"],
1760
+ ["run", "grunt:rulesync:check"],
1164
1761
  ]);
1165
1762
  });
1166
1763
 
@@ -1171,8 +1768,8 @@ describe("init", () => {
1171
1768
  init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true });
1172
1769
  expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
1173
1770
  ["npm", ["install"]],
1174
- ["npm", ["run", "rulesync:generate"]],
1175
- ["npm", ["run", "rulesync:check"]],
1771
+ ["npm", ["run", "grunt:rulesync:generate"]],
1772
+ ["npm", ["run", "grunt:rulesync:check"]],
1176
1773
  ]);
1177
1774
  });
1178
1775
 
@@ -1187,9 +1784,9 @@ describe("init", () => {
1187
1784
  init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
1188
1785
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1189
1786
  ["install"],
1190
- ["run", "rulesync:generate"],
1191
- ["run", "sync:globals:apply"],
1192
- ["run", "rulesync:check"],
1787
+ ["run", "grunt:rulesync:generate"],
1788
+ ["run", "grunt:sync:globals:apply"],
1789
+ ["run", "grunt:rulesync:check"],
1193
1790
  ]);
1194
1791
  });
1195
1792
 
@@ -1204,8 +1801,8 @@ describe("init", () => {
1204
1801
  init(dest, { pkgRoot, execFileSync: exec });
1205
1802
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1206
1803
  ["install"],
1207
- ["run", "rulesync:generate"],
1208
- ["run", "rulesync:check"],
1804
+ ["run", "grunt:rulesync:generate"],
1805
+ ["run", "grunt:rulesync:check"],
1209
1806
  ]);
1210
1807
  });
1211
1808
 
@@ -1242,9 +1839,9 @@ describe("init", () => {
1242
1839
  expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
1243
1840
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1244
1841
  ["install"],
1245
- ["run", "rulesync:generate"],
1246
- ["run", "sync:globals:apply"],
1247
- ["run", "rulesync:check"],
1842
+ ["run", "grunt:rulesync:generate"],
1843
+ ["run", "grunt:sync:globals:apply"],
1844
+ ["run", "grunt:rulesync:check"],
1248
1845
  ]);
1249
1846
  });
1250
1847
 
@@ -1255,7 +1852,7 @@ describe("init", () => {
1255
1852
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "consumer claude\n");
1256
1853
  fs.writeFileSync(path.join(dest, "GEMINI.md"), "consumer gemini\n");
1257
1854
  const exec = vi.fn((_cmd, args) => {
1258
- if (Array.isArray(args) && args.includes("rulesync:generate")) {
1855
+ if (Array.isArray(args) && args.includes("grunt:rulesync:generate")) {
1259
1856
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
1260
1857
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
1261
1858
  fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
@@ -1280,6 +1877,32 @@ describe("init", () => {
1280
1877
  );
1281
1878
  });
1282
1879
 
1880
+ it("wrapped package src + generate clobber stays begin=1 end=1", () => {
1881
+ const pkgRoot = stubPkgRoot();
1882
+ for (const f of GUARDED_MD_FILES) {
1883
+ fs.writeFileSync(
1884
+ path.join(pkgRoot, f),
1885
+ `<!-- grunt:begin -->\n${f} content\n<!-- grunt:end -->\n`,
1886
+ );
1887
+ }
1888
+ const dest = tmp("grunt-wrap-gen-");
1889
+ const exec = vi.fn((_cmd, args) => {
1890
+ if (Array.isArray(args) && args.includes("grunt:rulesync:generate")) {
1891
+ fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
1892
+ fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
1893
+ }
1894
+ });
1895
+ init(dest, { pkgRoot, execFileSync: exec });
1896
+ const agents = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
1897
+ const claude = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
1898
+ expect(agents).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
1899
+ expect(claude).toBe("<!-- grunt:begin -->\ngenerated claude\n<!-- grunt:end -->\n");
1900
+ expect(agents.split("<!-- grunt:begin -->")).toHaveLength(2);
1901
+ expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
1902
+ expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
1903
+ expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
1904
+ });
1905
+
1283
1906
  it("onPhase merge then npm phases stop before exec", () => {
1284
1907
  const pkgRoot = stubPkgRoot();
1285
1908
  const dest = tmp("grunt-onphase-");
@@ -1300,9 +1923,9 @@ describe("init", () => {
1300
1923
  ]);
1301
1924
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1302
1925
  ["install"],
1303
- ["run", "rulesync:generate"],
1304
- ["run", "sync:globals:apply"],
1305
- ["run", "rulesync:check"],
1926
+ ["run", "grunt:rulesync:generate"],
1927
+ ["run", "grunt:sync:globals:apply"],
1928
+ ["run", "grunt:rulesync:check"],
1306
1929
  ]);
1307
1930
  });
1308
1931
 
@@ -1318,25 +1941,25 @@ describe("init", () => {
1318
1941
 
1319
1942
  it("warns when dest skill differs from packaged then force-overwrites; keeps extras", () => {
1320
1943
  const pkgRoot = stubPkgRoot();
1321
- const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "parent");
1944
+ const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "explain");
1322
1945
  fs.mkdirSync(pkgSkill, { recursive: true });
1323
- fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-parent\n");
1946
+ fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-explain\n");
1324
1947
  const dest = tmp("grunt-sk-conflict-");
1325
- const destSkill = path.join(dest, ".rulesync", "skills", "parent");
1948
+ const destSkill = path.join(dest, ".rulesync", "skills", "explain");
1326
1949
  const destExtra = path.join(dest, ".rulesync", "skills", "my-extra");
1327
1950
  fs.mkdirSync(destSkill, { recursive: true });
1328
1951
  fs.mkdirSync(destExtra, { recursive: true });
1329
- fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-parent\n");
1952
+ fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-explain\n");
1330
1953
  fs.writeFileSync(path.join(destExtra, "SKILL.md"), "keep-me\n");
1331
1954
  const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1332
1955
  init(dest, { pkgRoot, execFileSync: vi.fn(), skipGlobals: true });
1333
1956
  expect(
1334
1957
  warnSpy.mock.calls.some(
1335
- (c) => String(c[0]).includes("parent") && String(c[0]).includes("re-init overwrites"),
1958
+ (c) => String(c[0]).includes("explain") && String(c[0]).includes("re-init overwrites"),
1336
1959
  ),
1337
1960
  ).toBe(true);
1338
1961
  warnSpy.mockRestore();
1339
- expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-parent\n");
1962
+ expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-explain\n");
1340
1963
  expect(fs.readFileSync(path.join(destExtra, "SKILL.md"), "utf8")).toBe("keep-me\n");
1341
1964
  });
1342
1965
  });