@lovrozagar/grunt 0.5.0 → 0.5.2

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 (82) hide show
  1. package/.agents/agents/grunt.md +2 -1
  2. package/.agents/agents/implementer.md +1 -1
  3. package/.agents/agents/orchestrator.md +17 -12
  4. package/.agents/agents/thinker.md +6 -5
  5. package/.agents/skills/ask/SKILL.md +18 -0
  6. package/.agents/skills/auto/SKILL.md +18 -0
  7. package/.agents/skills/browser/SKILL.md +4 -3
  8. package/.agents/skills/cascade/SKILL.md +5 -3
  9. package/.agents/skills/handoff/SKILL.md +1 -1
  10. package/.agents/skills/implement-plan/SKILL.md +12 -11
  11. package/.agents/skills/solo/SKILL.md +5 -3
  12. package/.agents/skills/tmp/SKILL.md +58 -0
  13. package/.agents/skills/write-plan/SKILL.md +11 -8
  14. package/.claude/agents/grunt.md +2 -1
  15. package/.claude/agents/implementer.md +1 -1
  16. package/.claude/agents/orchestrator.md +17 -12
  17. package/.claude/agents/thinker.md +6 -5
  18. package/.claude/skills/ask/SKILL.md +18 -0
  19. package/.claude/skills/auto/SKILL.md +18 -0
  20. package/.claude/skills/browser/SKILL.md +4 -3
  21. package/.claude/skills/cascade/SKILL.md +5 -3
  22. package/.claude/skills/handoff/SKILL.md +1 -1
  23. package/.claude/skills/implement-plan/SKILL.md +12 -11
  24. package/.claude/skills/solo/SKILL.md +5 -3
  25. package/.claude/skills/tmp/SKILL.md +58 -0
  26. package/.claude/skills/write-plan/SKILL.md +11 -8
  27. package/.codex/agents/grunt.toml +2 -1
  28. package/.codex/agents/implementer.toml +1 -1
  29. package/.codex/agents/orchestrator.toml +17 -12
  30. package/.codex/agents/thinker.toml +6 -5
  31. package/.grok/agents/grunt.md +2 -1
  32. package/.grok/agents/implementer.md +1 -1
  33. package/.grok/agents/orchestrator.md +17 -12
  34. package/.grok/agents/thinker.md +6 -5
  35. package/.grok/hooks/orchestrate-parent.js +280 -62
  36. package/.grok/skills/ask/SKILL.md +18 -0
  37. package/.grok/skills/auto/SKILL.md +18 -0
  38. package/.grok/skills/browser/SKILL.md +4 -3
  39. package/.grok/skills/cascade/SKILL.md +5 -3
  40. package/.grok/skills/handoff/SKILL.md +1 -1
  41. package/.grok/skills/implement-plan/SKILL.md +12 -11
  42. package/.grok/skills/solo/SKILL.md +5 -3
  43. package/.grok/skills/tmp/SKILL.md +58 -0
  44. package/.grok/skills/write-plan/SKILL.md +11 -8
  45. package/.rulesync/grunt.config.jsonc +7 -0
  46. package/.rulesync/reference/INDEX.md +7 -4
  47. package/.rulesync/reference/cascade.md +15 -13
  48. package/.rulesync/reference/hooks.md +6 -4
  49. package/.rulesync/reference/law.md +1 -1
  50. package/.rulesync/reference/map.md +14 -9
  51. package/.rulesync/reference/output.md +18 -8
  52. package/.rulesync/reference/plan-format.md +10 -10
  53. package/.rulesync/reference/skills-map.md +6 -3
  54. package/.rulesync/rules/CLAUDE.md +18 -13
  55. package/.rulesync/rules/overview.md +18 -13
  56. package/.rulesync/skills/ask/SKILL.md +18 -0
  57. package/.rulesync/skills/auto/SKILL.md +18 -0
  58. package/.rulesync/skills/browser/SKILL.md +2 -2
  59. package/.rulesync/skills/cascade/SKILL.md +5 -3
  60. package/.rulesync/skills/handoff/SKILL.md +1 -1
  61. package/.rulesync/skills/implement-plan/SKILL.md +14 -11
  62. package/.rulesync/skills/solo/SKILL.md +5 -3
  63. package/.rulesync/skills/tmp/SKILL.md +58 -0
  64. package/.rulesync/skills/write-plan/SKILL.md +12 -8
  65. package/.rulesync/subagents/grunt.md +2 -1
  66. package/.rulesync/subagents/implementer.md +1 -1
  67. package/.rulesync/subagents/orchestrator.md +17 -12
  68. package/.rulesync/subagents/thinker.md +6 -5
  69. package/AGENTS.md +18 -13
  70. package/CHANGELOG.md +33 -0
  71. package/CLAUDE.md +18 -13
  72. package/README.md +17 -16
  73. package/cli/grunt.mjs +12 -12
  74. package/cli/grunt.test.ts +29 -20
  75. package/cli/init.mjs +116 -22
  76. package/cli/init.test.ts +545 -34
  77. package/package.json +8 -1
  78. package/scripts/gate-fat-tools.mjs +2 -2
  79. package/scripts/grunt-config.mjs +50 -0
  80. package/scripts/persist-plan.mjs +17 -2
  81. package/scripts/persist-tmp.mjs +123 -0
  82. package/scripts/scrub-spawn-prompt.mjs +1 -1
package/cli/init.test.ts CHANGED
@@ -1,16 +1,21 @@
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
+ LAUNCH_SCRIPTS,
14
19
  mergeClaudeSettings,
15
20
  mergeGitignore,
16
21
  mergeGuardedContent,
@@ -20,10 +25,12 @@ import {
20
25
  samePath,
21
26
  shouldAutoSkipGlobals,
22
27
  snapshotGuardedRoots,
28
+ toGruntScriptName,
23
29
  withGuardedCheckInteriors,
24
30
  writeMergedGuardedFile,
25
31
  } from "./init.mjs";
26
32
  import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
33
+ import { stripJsonc } from "../scripts/grunt-config.mjs";
27
34
 
28
35
  const tmpDirs: string[] = [];
29
36
  afterEach(() => {
@@ -39,6 +46,12 @@ function tmp(prefix: string) {
39
46
  }
40
47
 
41
48
  const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"];
49
+ const GRUNT_CONFIG_REL = path.join(".rulesync", "grunt.config.jsonc");
50
+ const GRUNT_CONFIG_DEFAULTS = {
51
+ version: 1,
52
+ leftoverGate: "ask",
53
+ spawnMode: "cascade",
54
+ };
42
55
  const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"];
43
56
  const PRODUCT_FILES = [
44
57
  "check-globals.mjs",
@@ -51,8 +64,10 @@ const PRODUCT_FILES = [
51
64
  "hooks-union.mjs",
52
65
  "pipeline.mjs",
53
66
  "grunt-job.mjs",
67
+ "grunt-config.mjs",
54
68
  "parse-need.mjs",
55
69
  "persist-handoff.mjs",
70
+ "persist-tmp.mjs",
56
71
  "persist-plan.mjs",
57
72
  "purge-global-mcps.mjs",
58
73
  "scrub-spawn-prompt.mjs",
@@ -103,6 +118,12 @@ const SRC_CLAUDE_SETTINGS = {
103
118
  enabledMcpjsonServers: [],
104
119
  };
105
120
 
121
+ function readGruntConfig(root: string) {
122
+ const p = path.join(root, GRUNT_CONFIG_REL);
123
+ expect(fs.existsSync(p)).toBe(true);
124
+ return JSON.parse(stripJsonc(fs.readFileSync(p, "utf8")));
125
+ }
126
+
106
127
  function stubPkgRoot(pkg: Record<string, unknown> = {
107
128
  name: "fixture-pkg",
108
129
  scripts: {
@@ -122,6 +143,10 @@ function stubPkgRoot(pkg: Record<string, unknown> = {
122
143
  fs.mkdirSync(path.join(root, d));
123
144
  fs.writeFileSync(path.join(root, d, "marker"), d);
124
145
  }
146
+ fs.writeFileSync(
147
+ path.join(root, GRUNT_CONFIG_REL),
148
+ `${JSON.stringify(GRUNT_CONFIG_DEFAULTS, null, 2)}\n`,
149
+ );
125
150
  fs.writeFileSync(
126
151
  path.join(root, ".claude", "settings.json"),
127
152
  `${JSON.stringify(SRC_CLAUDE_SETTINGS, null, 2)}\n`,
@@ -290,6 +315,67 @@ describe("mergeGuardedMarkdown", () => {
290
315
  );
291
316
  });
292
317
 
318
+ it("already-guarded package src stays begin=1 end=1", () => {
319
+ const pkgRoot = stubPkgRoot();
320
+ fs.writeFileSync(
321
+ path.join(pkgRoot, "CLAUDE.md"),
322
+ "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
323
+ );
324
+ const dest = tmp("md-wrapped-src-");
325
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
326
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
327
+ expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
328
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
329
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
330
+ });
331
+
332
+ it("merge heals dest with trailing orphan grunt:end", () => {
333
+ const pkgRoot = stubPkgRoot();
334
+ const dest = tmp("md-orphan-end-");
335
+ fs.writeFileSync(
336
+ path.join(dest, "CLAUDE.md"),
337
+ "<!-- grunt:begin -->\nold\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
338
+ );
339
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
340
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
341
+ expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
342
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
343
+ });
344
+
345
+ it("dest stacked researcher shape heals to exactly one pair", () => {
346
+ const pkgRoot = stubPkgRoot();
347
+ fs.writeFileSync(
348
+ path.join(pkgRoot, "CLAUDE.md"),
349
+ "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
350
+ );
351
+ const dest = tmp("md-stacked-");
352
+ fs.writeFileSync(
353
+ path.join(dest, "CLAUDE.md"),
354
+ "<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
355
+ );
356
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
357
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
358
+ expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
359
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
360
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
361
+ });
362
+
363
+ it("dest stacked keeps user prefix/suffix outside the pair", () => {
364
+ const pkgRoot = stubPkgRoot();
365
+ const dest = tmp("md-stacked-user-");
366
+ fs.writeFileSync(
367
+ path.join(dest, "CLAUDE.md"),
368
+ "prefix\n<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\nsuffix\n",
369
+ );
370
+ mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
371
+ const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
372
+ expect(out).toBe(
373
+ "<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nprefix\nsuffix\n",
374
+ );
375
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
376
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
377
+ });
378
+
293
379
  it("does not fuzzy-dedupe unmarked protocol-looking user text", () => {
294
380
  const pkgRoot = stubPkgRoot();
295
381
  const dest = tmp("md-proto-");
@@ -363,6 +449,23 @@ describe("snapshot/remerge guarded roots", () => {
363
449
  );
364
450
  });
365
451
 
452
+ it("remerge after wrapped-src merge+generate does not reattach orphan end", () => {
453
+ const pkgRoot = stubPkgRoot();
454
+ fs.writeFileSync(
455
+ path.join(pkgRoot, "AGENTS.md"),
456
+ "<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\n",
457
+ );
458
+ const dest = tmp("snap-wrap-src-");
459
+ mergeGuardedMarkdown(dest, pkgRoot, "AGENTS.md");
460
+ const snap = snapshotGuardedRoots(dest);
461
+ fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
462
+ remergeGuardedRoots(dest, snap);
463
+ const out = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
464
+ expect(out).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
465
+ expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
466
+ expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
467
+ });
468
+
366
469
  it("withGuardedCheckInteriors is restored even if check throws", () => {
367
470
  const dest = tmp("check-int-");
368
471
  const live =
@@ -378,6 +481,22 @@ describe("snapshot/remerge guarded roots", () => {
378
481
  });
379
482
 
380
483
  it("compose/extract/drift/write helpers cover remaining branches", () => {
484
+ expect(
485
+ extractUserMarkdown(
486
+ "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
487
+ ),
488
+ ).toBe("");
489
+ expect(
490
+ extractUserMarkdown(
491
+ "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\nkeep\n<!-- grunt:end -->\n",
492
+ ),
493
+ ).toBe("keep\n");
494
+ expect(
495
+ composeGuardedMarkdown(
496
+ "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
497
+ "<!-- grunt:end -->\n",
498
+ ),
499
+ ).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
381
500
  expect(composeGuardedMarkdown("body", null)).toBe(
382
501
  "<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
383
502
  );
@@ -402,7 +521,9 @@ describe("snapshot/remerge guarded roots", () => {
402
521
  expect(mergeGuardedContent("keep\n", "g\r\n")).toBe(
403
522
  "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\nkeep\n",
404
523
  );
405
- expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toContain("only");
524
+ expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toBe(
525
+ "<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
526
+ );
406
527
  const dest = tmp("md-helpers-");
407
528
  const missing = path.join(dest, "nope.md");
408
529
  expect(guardedMarkdownDrift(missing, "g")).toBe(true);
@@ -423,6 +544,73 @@ describe("snapshot/remerge guarded roots", () => {
423
544
  expect(writeMergedGuardedFile(huge, "g")).toBe("aborted-unsafe");
424
545
  });
425
546
 
547
+ it("unclosed begin unwraps interior; inline orphan end is stripped", () => {
548
+ expect(extractGruntBody("<!-- grunt:begin -->\nhello")).toBe("hello");
549
+ const composed = composeGuardedMarkdown("<!-- grunt:begin -->\nhello", "");
550
+ expect(composed).toBe("<!-- grunt:begin -->\nhello\n<!-- grunt:end -->\n");
551
+ expect(composed.split(SENTINEL_BEGIN)).toHaveLength(2);
552
+ expect(composed.split(SENTINEL_END)).toHaveLength(2);
553
+ expect(composed).not.toContain(`${SENTINEL_BEGIN}\n${SENTINEL_BEGIN}`);
554
+ expect(mergeGuardedContent("note\n<!-- grunt:begin -->\nstale", "new")).toBe(
555
+ "<!-- grunt:begin -->\nnew\n<!-- grunt:end -->\nnote\n",
556
+ );
557
+ expect(extractUserMarkdown("keep <!-- grunt:end --> me")).not.toContain(SENTINEL_END);
558
+ expect(extractUserMarkdown("keep <!-- grunt:end --> me")).toBe("keep me");
559
+ expect(extractUserMarkdown("keep\n<!-- grunt:end -->\nme")).toBe("keep\nme");
560
+ expect(extractUserMarkdown("keep\n <!-- grunt:end --> \nme")).toBe("keep\nme");
561
+ expect(extractGruntBody("<!-- grunt:begin -->\r\nhello")).toBe("hello");
562
+ expect(extractGruntBody("<!-- grunt:begin -->")).toBe("");
563
+ const emptyWrap = composeGuardedMarkdown("<!-- grunt:begin -->", "");
564
+ expect(emptyWrap.split(SENTINEL_BEGIN)).toHaveLength(2);
565
+ expect(emptyWrap).toBe("<!-- grunt:begin -->\n\n<!-- grunt:end -->\n");
566
+ const mixed =
567
+ "<!-- grunt:begin -->\nok\n<!-- grunt:end -->\nuser bit\n<!-- grunt:begin -->\nstale";
568
+ expect(extractGruntBody(mixed)).toBe("ok");
569
+ expect(extractUserMarkdown(mixed)).toBe("user bit\n");
570
+ expect(
571
+ composeGuardedMarkdown("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n", ""),
572
+ ).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
573
+ });
574
+
575
+ it("extract/compose peel stacked dest and trailing orphan end", () => {
576
+ const stacked =
577
+ "<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\n";
578
+ expect(extractGruntBody(stacked)).toBe("BODY");
579
+ const stackedOut = composeGuardedMarkdown(stacked, "");
580
+ expect(stackedOut).toBe("<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n");
581
+ expect(stackedOut.split(SENTINEL_BEGIN)).toHaveLength(2);
582
+ expect(stackedOut.split(SENTINEL_END)).toHaveLength(2);
583
+ expect(composeGuardedMarkdown("BODY\n<!-- grunt:end -->\n", "")).toBe(
584
+ "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n",
585
+ );
586
+ expect(extractGruntBody("BODY\n\n<!-- grunt:end -->")).toBe("BODY");
587
+ expect(mergeGuardedContent(stacked, "SRC")).toBe(
588
+ "<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\n",
589
+ );
590
+ expect(mergeGuardedContent("keep\n<!-- grunt:end -->\n", "SRC")).toBe(
591
+ "<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\nkeep\n",
592
+ );
593
+ expect(
594
+ extractGruntBody("<!-- grunt:begin --> \nBODY\n<!-- grunt:end --> \n"),
595
+ ).toBe("BODY");
596
+ expect(
597
+ extractGruntBody(" <!-- grunt:begin -->\nBODY\n <!-- grunt:end -->\n"),
598
+ ).toBe("BODY");
599
+ expect(extractGruntBody("<!-- grunt:begin -->\n<!-- grunt:end -->")).toBe("");
600
+ expect(
601
+ extractGruntBody(
602
+ "<!-- grunt:begin -->\nsee <!-- grunt:end --> here\n<!-- grunt:end -->\n",
603
+ ),
604
+ ).toBe("see <!-- grunt:end --> here");
605
+ const midLine =
606
+ "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\nstill user\n<!-- grunt:end -->\n";
607
+ expect(extractGruntBody(midLine)).toBe("BODY");
608
+ expect(extractUserMarkdown(midLine)).toBe("still user\n");
609
+ expect(composeGuardedMarkdown(extractGruntBody(midLine) ?? "", extractUserMarkdown(midLine))).toBe(
610
+ "<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\nstill user\n",
611
+ );
612
+ });
613
+
426
614
  it("remerge restores unsafe, missing, binary current, and created files", () => {
427
615
  const dest = tmp("remerge-edges-");
428
616
  const agents = path.join(dest, "AGENTS.md");
@@ -786,6 +974,13 @@ describe("mergeClaudeSettings", () => {
786
974
  });
787
975
  });
788
976
 
977
+ describe("toGruntScriptName", () => {
978
+ it("prefixes unprefixed keys and does not double-prefix", () => {
979
+ expect(toGruntScriptName("rulesync:generate")).toBe("grunt:rulesync:generate");
980
+ expect(toGruntScriptName("grunt:doctor")).toBe("grunt:doctor");
981
+ });
982
+ });
983
+
789
984
  describe("mergePackageJson", () => {
790
985
  it("creates {} dest and merges scripts skipping test, injects deps, sorts keys", () => {
791
986
  const pkgRoot = stubPkgRoot();
@@ -793,11 +988,25 @@ describe("mergePackageJson", () => {
793
988
  mergePackageJson(dest, pkgRoot);
794
989
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
795
990
  expect(out.scripts).toEqual({
796
- alpha: "a",
797
- "rulesync:generate": "gen",
798
- zeta: "z",
991
+ "grunt:alpha": "a",
992
+ "grunt:antigravity": LAUNCH_SCRIPTS.antigravity,
993
+ "grunt:claude": LAUNCH_SCRIPTS.claude,
994
+ "grunt:codex": LAUNCH_SCRIPTS.codex,
995
+ "grunt:gemini": LAUNCH_SCRIPTS.gemini,
996
+ "grunt:grok": LAUNCH_SCRIPTS.grok,
997
+ "grunt:rulesync:generate": "gen",
998
+ "grunt:zeta": "z",
799
999
  });
800
- expect(Object.keys(out.scripts)).toEqual(["alpha", "rulesync:generate", "zeta"]);
1000
+ expect(Object.keys(out.scripts)).toEqual([
1001
+ "grunt:alpha",
1002
+ "grunt:antigravity",
1003
+ "grunt:claude",
1004
+ "grunt:codex",
1005
+ "grunt:gemini",
1006
+ "grunt:grok",
1007
+ "grunt:rulesync:generate",
1008
+ "grunt:zeta",
1009
+ ]);
801
1010
  expect(out.devDependencies).toEqual({
802
1011
  rulesync: "latest",
803
1012
  "smol-toml": "^1.8.0",
@@ -817,14 +1026,21 @@ describe("mergePackageJson", () => {
817
1026
  devDependencies: { lodash: "4" },
818
1027
  }),
819
1028
  );
1029
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
820
1030
  mergePackageJson(dest, pkgRoot);
821
1031
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
822
1032
  expect(out.scripts.test).toBe("jest");
823
1033
  expect(out.scripts.alpha).toBe("old");
1034
+ expect(out.scripts["grunt:alpha"]).toBe("a");
1035
+ expect(out.scripts["grunt:zeta"]).toBe("z");
824
1036
  expect(out.scripts.foo).toBe("bar");
825
1037
  expect(out.devDependencies.lodash).toBe("4");
826
1038
  expect(out.devDependencies["smol-toml"]).toBe("^1.8.0");
827
1039
  expect(out.devDependencies.rulesync).toBe("latest");
1040
+ expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
1041
+ true,
1042
+ );
1043
+ warnSpy.mockRestore();
828
1044
  });
829
1045
 
830
1046
  it("preserves dest suffix after an upgraded grunt script prefix", () => {
@@ -844,9 +1060,10 @@ describe("mergePackageJson", () => {
844
1060
  );
845
1061
  mergePackageJson(dest, pkgRoot);
846
1062
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
847
- expect(out.scripts["rulesync:generate"]).toBe(
1063
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
848
1064
  "rulesync generate -t bar && node scripts/codex-sync.mjs",
849
1065
  );
1066
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
850
1067
  });
851
1068
 
852
1069
  it("preserves suffix after a multi-command grunt script upgrade", () => {
@@ -869,7 +1086,7 @@ describe("mergePackageJson", () => {
869
1086
  );
870
1087
  mergePackageJson(dest, pkgRoot);
871
1088
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
872
- expect(out.scripts["rulesync:generate"]).toBe(
1089
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
873
1090
  "rulesync generate -t bar && node scripts/emit.mjs && node scripts/codex-sync.mjs",
874
1091
  );
875
1092
  });
@@ -889,6 +1106,7 @@ describe("mergePackageJson", () => {
889
1106
  mergePackageJson(dest, pkgRoot);
890
1107
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
891
1108
  expect(out.scripts.alpha).toBe("echo custom && extra");
1109
+ expect(out.scripts["grunt:alpha"]).toBe("a");
892
1110
  expect(warnSpy).toHaveBeenCalled();
893
1111
  warnSpy.mockRestore();
894
1112
  });
@@ -910,6 +1128,7 @@ describe("mergePackageJson", () => {
910
1128
  mergePackageJson(dest, pkgRoot);
911
1129
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
912
1130
  expect(out.scripts["rulesync:generate"]).toBe("echo custom;echo extra");
1131
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
913
1132
  expect(warnSpy).toHaveBeenCalled();
914
1133
  warnSpy.mockRestore();
915
1134
  });
@@ -927,7 +1146,8 @@ describe("mergePackageJson", () => {
927
1146
  );
928
1147
  mergePackageJson(dest, pkgRoot);
929
1148
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
930
- expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
1149
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
1150
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
931
1151
  });
932
1152
 
933
1153
  it("keeps dest when it already starts with the new src value plus suffix", () => {
@@ -941,7 +1161,8 @@ describe("mergePackageJson", () => {
941
1161
  fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ scripts: { "rulesync:generate": cur } }));
942
1162
  mergePackageJson(dest, pkgRoot);
943
1163
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
944
- expect(out.scripts["rulesync:generate"]).toBe(cur);
1164
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(cur);
1165
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
945
1166
  });
946
1167
 
947
1168
  it("keeps unrelated custom script values and dest-only keys, warns on skip", () => {
@@ -957,9 +1178,11 @@ describe("mergePackageJson", () => {
957
1178
  mergePackageJson(dest, pkgRoot);
958
1179
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
959
1180
  expect(out.scripts.alpha).toBe("echo custom");
1181
+ expect(out.scripts["grunt:alpha"]).toBe("a");
960
1182
  expect(out.scripts.foo).toBe("bar");
961
1183
  expect(out.scripts.test).toBe("jest");
962
- expect(out.scripts.zeta).toBe("z");
1184
+ expect(out.scripts["grunt:zeta"]).toBe("z");
1185
+ expect(out.scripts.zeta).toBeUndefined();
963
1186
  expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
964
1187
  true,
965
1188
  );
@@ -980,7 +1203,8 @@ describe("mergePackageJson", () => {
980
1203
  const dest = tmp("pj-raw-src-");
981
1204
  mergePackageJson(dest, pkgRoot);
982
1205
  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");
1206
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1207
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
984
1208
  expect(out.scripts["rulesync:generate:raw"]).toBeUndefined();
985
1209
  expect(out.scripts["rulesync:check:raw"]).toBeUndefined();
986
1210
  expect(out.scripts["rulesync:watch:raw"]).toBeUndefined();
@@ -1001,7 +1225,7 @@ describe("mergePackageJson", () => {
1001
1225
  );
1002
1226
  mergePackageJson(dest, pkgRoot);
1003
1227
  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");
1228
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1005
1229
  expect(out.scripts["rulesync:generate:raw"]).toBe("echo consumer-raw");
1006
1230
  });
1007
1231
 
@@ -1020,7 +1244,9 @@ describe("mergePackageJson", () => {
1020
1244
  );
1021
1245
  mergePackageJson(dest, pkgRoot);
1022
1246
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1023
- expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
1247
+ expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
1248
+ expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
1249
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
1024
1250
  });
1025
1251
 
1026
1252
  it("early return when name is @lovrozagar/grunt", () => {
@@ -1040,9 +1266,257 @@ describe("mergePackageJson", () => {
1040
1266
  const dest = tmp("pj-noscripts-");
1041
1267
  mergePackageJson(dest, pkgRoot);
1042
1268
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1043
- expect(out.scripts).toEqual({});
1269
+ expect(out.scripts).toEqual({
1270
+ "grunt:antigravity": LAUNCH_SCRIPTS.antigravity,
1271
+ "grunt:claude": LAUNCH_SCRIPTS.claude,
1272
+ "grunt:codex": LAUNCH_SCRIPTS.codex,
1273
+ "grunt:gemini": LAUNCH_SCRIPTS.gemini,
1274
+ "grunt:grok": LAUNCH_SCRIPTS.grok,
1275
+ });
1044
1276
  expect(out.devDependencies).toEqual({ rulesync: "2", "smol-toml": "1" });
1045
1277
  });
1278
+
1279
+ it("golden autorun-apps: suffix keep, doctor move, check npm run rewrite, old keys gone", () => {
1280
+ const pkgRoot = stubPkgRoot({
1281
+ name: "fixture-pkg",
1282
+ scripts: {
1283
+ "rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
1284
+ "rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
1285
+ "rulesync:check": "node ./scripts/guarded-roots.mjs check",
1286
+ "hooks:union": "node scripts/hooks-union.mjs",
1287
+ "hooks:check": "node scripts/hooks-union.mjs --check",
1288
+ doctor: "node ./scripts/doctor.mjs",
1289
+ "rulesync:doctor": "rulesync doctor",
1290
+ "sync:globals": "node scripts/sync-global-settings.mjs",
1291
+ "sync:globals:check": "node scripts/check-globals.mjs",
1292
+ "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
1293
+ "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
1294
+ "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
1295
+ test: "vitest run --coverage",
1296
+ },
1297
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1298
+ });
1299
+ const dest = tmp("pj-autorun-");
1300
+ fs.writeFileSync(
1301
+ path.join(dest, "package.json"),
1302
+ JSON.stringify({
1303
+ scripts: {
1304
+ "apps:doctor": "node scripts/audit.mjs",
1305
+ "apps:mental-model:viewer:build":
1306
+ "npm --prefix scripts/excalidraw/viewer install && npm --prefix scripts/excalidraw/viewer run build",
1307
+ check:
1308
+ "node scripts/portability-check.mjs && npm run rulesync:check && npm run lint && npm test && node scripts/media-fixtures.mjs --check",
1309
+ "codex:sync": "node scripts/codex-sync.mjs",
1310
+ doctor: "node ./scripts/doctor.mjs",
1311
+ "hooks:check": "node scripts/hooks-union.mjs --check",
1312
+ "hooks:union": "node scripts/hooks-union.mjs",
1313
+ lint: "eslint scripts tests eslint.config.mjs",
1314
+ "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
1315
+ "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
1316
+ "rulesync:check": "node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1317
+ "rulesync:doctor": "rulesync doctor",
1318
+ "rulesync:generate": "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1319
+ "rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
1320
+ "sync:globals": "node scripts/sync-global-settings.mjs",
1321
+ "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
1322
+ "sync:globals:check": "node scripts/check-globals.mjs",
1323
+ test: "node scripts/run-tests.mjs",
1324
+ },
1325
+ }),
1326
+ );
1327
+ mergePackageJson(dest, pkgRoot);
1328
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1329
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
1330
+ "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
1331
+ );
1332
+ expect(out.scripts["grunt:rulesync:check"]).toBe(
1333
+ "node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
1334
+ );
1335
+ expect(out.scripts["grunt:rulesync:watch"]).toBe("node ./scripts/guarded-roots.mjs watch");
1336
+ expect(out.scripts["grunt:rulesync:doctor"]).toBe("rulesync doctor");
1337
+ expect(out.scripts["grunt:hooks:union"]).toBe("node scripts/hooks-union.mjs");
1338
+ expect(out.scripts["grunt:hooks:check"]).toBe("node scripts/hooks-union.mjs --check");
1339
+ expect(out.scripts["grunt:sync:globals"]).toBe("node scripts/sync-global-settings.mjs");
1340
+ expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
1341
+ expect(out.scripts["grunt:sync:globals:check"]).toBe("node scripts/check-globals.mjs");
1342
+ expect(out.scripts["grunt:purge:global-mcps"]).toBe("node scripts/purge-global-mcps.mjs");
1343
+ expect(out.scripts["grunt:purge:global-mcps:apply"]).toBe("node scripts/purge-global-mcps.mjs --apply");
1344
+ expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
1345
+ expect(out.scripts.doctor).toBeUndefined();
1346
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
1347
+ expect(out.scripts["rulesync:check"]).toBeUndefined();
1348
+ expect(out.scripts["rulesync:watch"]).toBeUndefined();
1349
+ expect(out.scripts["hooks:union"]).toBeUndefined();
1350
+ expect(out.scripts["sync:globals"]).toBeUndefined();
1351
+ expect(out.scripts.check).toBe(
1352
+ "node scripts/portability-check.mjs && npm run grunt:rulesync:check && npm run lint && npm test && node scripts/media-fixtures.mjs --check",
1353
+ );
1354
+ expect(out.scripts["apps:doctor"]).toBe("node scripts/audit.mjs");
1355
+ expect(out.scripts["codex:sync"]).toBe("node scripts/codex-sync.mjs");
1356
+ expect(out.scripts.lint).toBe("eslint scripts tests eslint.config.mjs");
1357
+ expect(out.scripts.test).toBe("node scripts/run-tests.mjs");
1358
+ });
1359
+
1360
+ it("leaves custom doctor and writes grunt:doctor from SoT", () => {
1361
+ const pkgRoot = stubPkgRoot({
1362
+ name: "fixture-pkg",
1363
+ scripts: { doctor: "node ./scripts/doctor.mjs" },
1364
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1365
+ });
1366
+ const dest = tmp("pj-custom-doctor-");
1367
+ fs.writeFileSync(
1368
+ path.join(dest, "package.json"),
1369
+ JSON.stringify({ scripts: { doctor: "node scripts/audit.mjs" } }),
1370
+ );
1371
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1372
+ mergePackageJson(dest, pkgRoot);
1373
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1374
+ expect(out.scripts.doctor).toBe("node scripts/audit.mjs");
1375
+ expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
1376
+ expect(warnSpy).toHaveBeenCalled();
1377
+ warnSpy.mockRestore();
1378
+ });
1379
+
1380
+ it("merges destKey when both prefixed and owned legacy exist, deletes legacy", () => {
1381
+ const pkgRoot = stubPkgRoot({
1382
+ name: "fixture-pkg",
1383
+ scripts: { "rulesync:generate": "rulesync generate -t bar" },
1384
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1385
+ });
1386
+ const dest = tmp("pj-both-");
1387
+ fs.writeFileSync(
1388
+ path.join(dest, "package.json"),
1389
+ JSON.stringify({
1390
+ scripts: {
1391
+ "rulesync:generate": "rulesync generate -t foo && node scripts/codex-sync.mjs",
1392
+ "grunt:rulesync:generate": "rulesync generate -t old && node scripts/other.mjs",
1393
+ },
1394
+ }),
1395
+ );
1396
+ mergePackageJson(dest, pkgRoot);
1397
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1398
+ expect(out.scripts["grunt:rulesync:generate"]).toBe(
1399
+ "rulesync generate -t bar && node scripts/other.mjs",
1400
+ );
1401
+ expect(out.scripts["rulesync:generate"]).toBeUndefined();
1402
+ });
1403
+
1404
+ it("does not rewrite npm run rulesync:check:raw when only rulesync:check migrated", () => {
1405
+ const pkgRoot = stubPkgRoot({
1406
+ name: "fixture-pkg",
1407
+ scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
1408
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1409
+ });
1410
+ const dest = tmp("pj-raw-rewrite-");
1411
+ fs.writeFileSync(
1412
+ path.join(dest, "package.json"),
1413
+ JSON.stringify({
1414
+ scripts: {
1415
+ "rulesync:check": "node ./scripts/guarded-roots.mjs check",
1416
+ "rulesync:check:raw": "echo consumer-raw",
1417
+ check: "npm run rulesync:check && npm run rulesync:check:raw",
1418
+ },
1419
+ }),
1420
+ );
1421
+ mergePackageJson(dest, pkgRoot);
1422
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1423
+ expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
1424
+ expect(out.scripts["rulesync:check"]).toBeUndefined();
1425
+ expect(out.scripts["rulesync:check:raw"]).toBe("echo consumer-raw");
1426
+ expect(out.scripts.check).toBe("npm run grunt:rulesync:check && npm run rulesync:check:raw");
1427
+ });
1428
+
1429
+ it("skips non-string dest scripts when rewriting npm run refs", () => {
1430
+ const pkgRoot = stubPkgRoot({
1431
+ name: "fixture-pkg",
1432
+ scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
1433
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
1434
+ });
1435
+ const dest = tmp("pj-nonstr-");
1436
+ fs.writeFileSync(
1437
+ path.join(dest, "package.json"),
1438
+ JSON.stringify({
1439
+ scripts: {
1440
+ extra: 1,
1441
+ check: "npm run rulesync:check",
1442
+ },
1443
+ }),
1444
+ );
1445
+ mergePackageJson(dest, pkgRoot);
1446
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1447
+ expect(out.scripts.extra).toBe(1);
1448
+ expect(out.scripts.check).toBe("npm run grunt:rulesync:check");
1449
+ expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
1450
+ });
1451
+
1452
+ it("emits grunt:claude into a consumer package.json", () => {
1453
+ const pkgRoot = stubPkgRoot();
1454
+ const dest = tmp("pj-launch-emit-");
1455
+ mergePackageJson(dest, pkgRoot);
1456
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1457
+ expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
1458
+ });
1459
+
1460
+ it("leaves a consumer's bare claude launcher untouched, no warn emitted", () => {
1461
+ const pkgRoot = stubPkgRoot();
1462
+ const dest = tmp("pj-consumer-claude-");
1463
+ fs.writeFileSync(
1464
+ path.join(dest, "package.json"),
1465
+ JSON.stringify({ scripts: { claude: "npm run build && claude" } }),
1466
+ );
1467
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1468
+ mergePackageJson(dest, pkgRoot);
1469
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1470
+ expect(out.scripts.claude).toBe("npm run build && claude");
1471
+ expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
1472
+ expect(warn).not.toHaveBeenCalled();
1473
+ warn.mockRestore();
1474
+ });
1475
+
1476
+ it("does not rewrite npm run claude inside a consumer script", () => {
1477
+ const pkgRoot = stubPkgRoot();
1478
+ const dest = tmp("pj-npmrun-claude-");
1479
+ fs.writeFileSync(
1480
+ path.join(dest, "package.json"),
1481
+ JSON.stringify({
1482
+ scripts: { claude: "npm run build && claude", start: "npm run claude" },
1483
+ }),
1484
+ );
1485
+ mergePackageJson(dest, pkgRoot);
1486
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1487
+ expect(out.scripts.start).toBe("npm run claude");
1488
+ });
1489
+
1490
+ it("purges a stale grunt:yolo:claude with the exact old value; an edited variant survives", () => {
1491
+ const pkgRoot = stubPkgRoot();
1492
+ const dest = tmp("pj-purge-stale-");
1493
+ fs.writeFileSync(
1494
+ path.join(dest, "package.json"),
1495
+ JSON.stringify({
1496
+ scripts: {
1497
+ "grunt:yolo:claude": LAUNCH_SCRIPTS.claude,
1498
+ "grunt:yolo:codex": `${LAUNCH_SCRIPTS.codex} --extra`,
1499
+ },
1500
+ }),
1501
+ );
1502
+ mergePackageJson(dest, pkgRoot);
1503
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1504
+ expect(out.scripts["grunt:yolo:claude"]).toBeUndefined();
1505
+ expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
1506
+ expect(out.scripts["grunt:yolo:codex"]).toBe(`${LAUNCH_SCRIPTS.codex} --extra`);
1507
+ });
1508
+
1509
+ it("drift: every bare launcher key in grunt's package.json equals LAUNCH_SCRIPTS[key]", () => {
1510
+ const pkg = JSON.parse(
1511
+ fs.readFileSync(
1512
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
1513
+ "utf8",
1514
+ ),
1515
+ );
1516
+ for (const [k, v] of Object.entries(LAUNCH_SCRIPTS)) {
1517
+ expect(pkg.scripts[k]).toBe(v);
1518
+ }
1519
+ });
1046
1520
  });
1047
1521
 
1048
1522
  describe("init", () => {
@@ -1076,6 +1550,9 @@ describe("init", () => {
1076
1550
  if (d === ".claude") continue;
1077
1551
  expect(fs.readFileSync(path.join(dest, d, "marker"), "utf8")).toBe(d);
1078
1552
  }
1553
+ expect(readGruntConfig(dest)).toEqual(GRUNT_CONFIG_DEFAULTS);
1554
+ expect(PRODUCT_FILES).toContain("grunt-config.mjs");
1555
+ expect(fs.statSync(path.join(dest, "scripts", "grunt-config.mjs")).isFile()).toBe(true);
1079
1556
  expect(fs.readFileSync(path.join(dest, ".claude", "marker"), "utf8")).toBe(".claude");
1080
1557
  const settings = JSON.parse(
1081
1558
  fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
@@ -1103,9 +1580,9 @@ describe("init", () => {
1103
1580
 
1104
1581
  expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
1105
1582
  ["npm", ["install"]],
1106
- ["npm", ["run", "rulesync:generate"]],
1107
- ["npm", ["run", "sync:globals:apply"]],
1108
- ["npm", ["run", "rulesync:check"]],
1583
+ ["npm", ["run", "grunt:rulesync:generate"]],
1584
+ ["npm", ["run", "grunt:sync:globals:apply"]],
1585
+ ["npm", ["run", "grunt:rulesync:check"]],
1109
1586
  ]);
1110
1587
  for (const call of exec.mock.calls) {
1111
1588
  expect(call[2]).toEqual({ cwd: dest, stdio: "inherit" });
@@ -1154,13 +1631,21 @@ describe("init", () => {
1154
1631
  const exec = vi.fn();
1155
1632
  init(dest, { execFileSync: exec });
1156
1633
  expect(fs.existsSync(path.join(dest, "AGENTS.md"))).toBe(true);
1634
+ const agents = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
1635
+ const claude = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
1636
+ expect(agents.split("<!-- grunt:begin -->")).toHaveLength(2);
1637
+ expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
1638
+ expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
1639
+ expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
1157
1640
  expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
1641
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(true);
1158
1642
  expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
1643
+ expect(readGruntConfig(dest)).toEqual(GRUNT_CONFIG_DEFAULTS);
1159
1644
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1160
1645
  ["install"],
1161
- ["run", "rulesync:generate"],
1162
- ["run", "sync:globals:apply"],
1163
- ["run", "rulesync:check"],
1646
+ ["run", "grunt:rulesync:generate"],
1647
+ ["run", "grunt:sync:globals:apply"],
1648
+ ["run", "grunt:rulesync:check"],
1164
1649
  ]);
1165
1650
  });
1166
1651
 
@@ -1171,8 +1656,8 @@ describe("init", () => {
1171
1656
  init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true });
1172
1657
  expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
1173
1658
  ["npm", ["install"]],
1174
- ["npm", ["run", "rulesync:generate"]],
1175
- ["npm", ["run", "rulesync:check"]],
1659
+ ["npm", ["run", "grunt:rulesync:generate"]],
1660
+ ["npm", ["run", "grunt:rulesync:check"]],
1176
1661
  ]);
1177
1662
  });
1178
1663
 
@@ -1187,9 +1672,9 @@ describe("init", () => {
1187
1672
  init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
1188
1673
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1189
1674
  ["install"],
1190
- ["run", "rulesync:generate"],
1191
- ["run", "sync:globals:apply"],
1192
- ["run", "rulesync:check"],
1675
+ ["run", "grunt:rulesync:generate"],
1676
+ ["run", "grunt:sync:globals:apply"],
1677
+ ["run", "grunt:rulesync:check"],
1193
1678
  ]);
1194
1679
  });
1195
1680
 
@@ -1204,8 +1689,8 @@ describe("init", () => {
1204
1689
  init(dest, { pkgRoot, execFileSync: exec });
1205
1690
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1206
1691
  ["install"],
1207
- ["run", "rulesync:generate"],
1208
- ["run", "rulesync:check"],
1692
+ ["run", "grunt:rulesync:generate"],
1693
+ ["run", "grunt:rulesync:check"],
1209
1694
  ]);
1210
1695
  });
1211
1696
 
@@ -1242,9 +1727,9 @@ describe("init", () => {
1242
1727
  expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
1243
1728
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1244
1729
  ["install"],
1245
- ["run", "rulesync:generate"],
1246
- ["run", "sync:globals:apply"],
1247
- ["run", "rulesync:check"],
1730
+ ["run", "grunt:rulesync:generate"],
1731
+ ["run", "grunt:sync:globals:apply"],
1732
+ ["run", "grunt:rulesync:check"],
1248
1733
  ]);
1249
1734
  });
1250
1735
 
@@ -1255,7 +1740,7 @@ describe("init", () => {
1255
1740
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "consumer claude\n");
1256
1741
  fs.writeFileSync(path.join(dest, "GEMINI.md"), "consumer gemini\n");
1257
1742
  const exec = vi.fn((_cmd, args) => {
1258
- if (Array.isArray(args) && args.includes("rulesync:generate")) {
1743
+ if (Array.isArray(args) && args.includes("grunt:rulesync:generate")) {
1259
1744
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
1260
1745
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
1261
1746
  fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
@@ -1280,6 +1765,32 @@ describe("init", () => {
1280
1765
  );
1281
1766
  });
1282
1767
 
1768
+ it("wrapped package src + generate clobber stays begin=1 end=1", () => {
1769
+ const pkgRoot = stubPkgRoot();
1770
+ for (const f of GUARDED_MD_FILES) {
1771
+ fs.writeFileSync(
1772
+ path.join(pkgRoot, f),
1773
+ `<!-- grunt:begin -->\n${f} content\n<!-- grunt:end -->\n`,
1774
+ );
1775
+ }
1776
+ const dest = tmp("grunt-wrap-gen-");
1777
+ const exec = vi.fn((_cmd, args) => {
1778
+ if (Array.isArray(args) && args.includes("grunt:rulesync:generate")) {
1779
+ fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
1780
+ fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
1781
+ }
1782
+ });
1783
+ init(dest, { pkgRoot, execFileSync: exec });
1784
+ const agents = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
1785
+ const claude = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
1786
+ expect(agents).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
1787
+ expect(claude).toBe("<!-- grunt:begin -->\ngenerated claude\n<!-- grunt:end -->\n");
1788
+ expect(agents.split("<!-- grunt:begin -->")).toHaveLength(2);
1789
+ expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
1790
+ expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
1791
+ expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
1792
+ });
1793
+
1283
1794
  it("onPhase merge then npm phases stop before exec", () => {
1284
1795
  const pkgRoot = stubPkgRoot();
1285
1796
  const dest = tmp("grunt-onphase-");
@@ -1300,9 +1811,9 @@ describe("init", () => {
1300
1811
  ]);
1301
1812
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1302
1813
  ["install"],
1303
- ["run", "rulesync:generate"],
1304
- ["run", "sync:globals:apply"],
1305
- ["run", "rulesync:check"],
1814
+ ["run", "grunt:rulesync:generate"],
1815
+ ["run", "grunt:sync:globals:apply"],
1816
+ ["run", "grunt:rulesync:check"],
1306
1817
  ]);
1307
1818
  });
1308
1819