@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/grunt.test.ts CHANGED
@@ -6,6 +6,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  const init = vi.hoisted(() => vi.fn());
7
7
  const destAlreadyInited = vi.hoisted(() => vi.fn(() => false));
8
8
  const shouldAutoSkipGlobals = vi.hoisted(() => vi.fn(() => false));
9
+ const toGruntScriptName = vi.hoisted(
10
+ () => (k) => (String(k).startsWith("grunt:") ? k : `grunt:${k}`),
11
+ );
9
12
  const execFileSync = vi.hoisted(() => vi.fn());
10
13
  const isInteractive = vi.hoisted(() => vi.fn(() => false));
11
14
  const select = vi.hoisted(() => vi.fn());
@@ -15,7 +18,14 @@ const spinner = vi.hoisted(() =>
15
18
  );
16
19
  const bailIfCancel = vi.hoisted(() => vi.fn((v) => v));
17
20
 
18
- vi.mock("./init.mjs", () => ({ init, destAlreadyInited, shouldAutoSkipGlobals }));
21
+ vi.mock("./init.mjs", () => ({
22
+ init,
23
+ destAlreadyInited,
24
+ shouldAutoSkipGlobals,
25
+ toGruntScriptName,
26
+ RESERVED_SKILLS: ["browser", "tmp", "write-plan"],
27
+ GRUNT_NPM_PREFIX: "grunt:",
28
+ }));
19
29
  vi.mock("node:child_process", () => ({ execFileSync }));
20
30
  vi.mock("./prompt.mjs", () => ({
21
31
  isInteractive,
@@ -32,12 +42,14 @@ const USAGE = `Usage: grunt [command]
32
42
  Default (no command): TTY menu; else init — full setup
33
43
 
34
44
  Commands:
35
- init Full setup: merge SoT, npm install, rulesync:generate, sync:globals:apply, rulesync:check
36
- generate npm run rulesync:generate
37
- check npm run rulesync:check
38
- sync-globals npm run sync:globals (dry-run; --apply to write)
39
- purge-mcps npm run purge:global-mcps (dry-run; --apply to write)
40
- doctor npm run doctor
45
+ init Full setup: merge SoT, npm install, grunt:rulesync:generate, grunt:sync:globals:apply, grunt:rulesync:check
46
+ generate npm run grunt:rulesync:generate
47
+ check npm run grunt:rulesync:check
48
+ sync-globals npm run grunt:sync:globals (dry-run; --apply to write)
49
+ purge-mcps npm run grunt:purge:global-mcps (dry-run; --apply to write)
50
+ doctor npm run grunt:doctor
51
+ setup npm run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
52
+ upgrade Re-init: copy owned files, prune retired grunt-owned names, print reserved skills
41
53
  help Show this help
42
54
  version Print package version
43
55
 
@@ -148,22 +160,22 @@ describe("start", () => {
148
160
  },
149
161
  );
150
162
 
151
- it("generate npm-runs rulesync:generate", async () => {
163
+ it("generate npm-runs grunt:rulesync:generate", async () => {
152
164
  process.argv = ["node", "grunt", "generate"];
153
165
  await start();
154
166
  expect(execFileSync).toHaveBeenCalledOnce();
155
- expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:generate"], {
167
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "grunt:rulesync:generate"], {
156
168
  cwd: process.cwd(),
157
169
  stdio: "inherit",
158
170
  });
159
171
  expect(init).not.toHaveBeenCalled();
160
172
  });
161
173
 
162
- it("check npm-runs rulesync:check", async () => {
174
+ it("check npm-runs grunt:rulesync:check", async () => {
163
175
  process.argv = ["node", "grunt", "check"];
164
176
  await start();
165
177
  expect(execFileSync).toHaveBeenCalledOnce();
166
- expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:check"], {
178
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "grunt:rulesync:check"], {
167
179
  cwd: process.cwd(),
168
180
  stdio: "inherit",
169
181
  });
@@ -173,7 +185,7 @@ describe("start", () => {
173
185
  process.argv = ["node", "grunt", "sync-globals", "--yes"];
174
186
  await start();
175
187
  expect(execFileSync).toHaveBeenCalledOnce();
176
- expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "sync:globals"], {
188
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "grunt:sync:globals"], {
177
189
  cwd: process.cwd(),
178
190
  stdio: "inherit",
179
191
  });
@@ -184,7 +196,7 @@ describe("start", () => {
184
196
  await start();
185
197
  expect(execFileSync).toHaveBeenCalledWith(
186
198
  "npm",
187
- ["run", "sync:globals", "--", "--host", "grok"],
199
+ ["run", "grunt:sync:globals", "--", "--host", "grok"],
188
200
  { cwd: process.cwd(), stdio: "inherit" },
189
201
  );
190
202
  });
@@ -194,7 +206,7 @@ describe("start", () => {
194
206
  await start();
195
207
  expect(execFileSync).toHaveBeenCalledWith(
196
208
  "npm",
197
- ["run", "sync:globals:apply", "--", "--host", "grok"],
209
+ ["run", "grunt:sync:globals:apply", "--", "--host", "grok"],
198
210
  { cwd: process.cwd(), stdio: "inherit" },
199
211
  );
200
212
  });
@@ -212,7 +224,7 @@ describe("start", () => {
212
224
  await start();
213
225
  expect(execFileSync).toHaveBeenCalledWith(
214
226
  "npm",
215
- ["run", "sync:globals", "--", "--host", "claude"],
227
+ ["run", "grunt:sync:globals", "--", "--host", "claude"],
216
228
  { cwd: process.cwd(), stdio: "inherit" },
217
229
  );
218
230
  });
@@ -220,7 +232,7 @@ describe("start", () => {
220
232
  it("purge-mcps dry-run default", async () => {
221
233
  process.argv = ["node", "grunt", "purge-mcps"];
222
234
  await start();
223
- expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "purge:global-mcps"], {
235
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "grunt:purge:global-mcps"], {
224
236
  cwd: process.cwd(),
225
237
  stdio: "inherit",
226
238
  });
@@ -231,20 +243,48 @@ describe("start", () => {
231
243
  await start();
232
244
  expect(execFileSync).toHaveBeenCalledWith(
233
245
  "npm",
234
- ["run", "purge:global-mcps:apply"],
246
+ ["run", "grunt:purge:global-mcps:apply"],
235
247
  { cwd: process.cwd(), stdio: "inherit" },
236
248
  );
237
249
  });
238
250
 
239
- it("doctor npm-runs doctor", async () => {
251
+ it("doctor npm-runs grunt:doctor", async () => {
240
252
  process.argv = ["node", "grunt", "doctor"];
241
253
  await start();
242
- expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "doctor"], {
254
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "grunt:doctor"], {
243
255
  cwd: process.cwd(),
244
256
  stdio: "inherit",
245
257
  });
246
258
  });
247
259
 
260
+ it("setup npm-runs grunt:setup", async () => {
261
+ process.argv = ["node", "grunt", "setup"];
262
+ await start();
263
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "grunt:setup"], {
264
+ cwd: process.cwd(),
265
+ stdio: "inherit",
266
+ });
267
+ });
268
+
269
+ it("setup npm-runs grunt:setup with extra args", async () => {
270
+ process.argv = ["node", "grunt", "setup", "speak", "--skip-verify"];
271
+ await start();
272
+ expect(execFileSync).toHaveBeenCalledWith(
273
+ "npm",
274
+ ["run", "grunt:setup", "--", "speak", "--skip-verify"],
275
+ { cwd: process.cwd(), stdio: "inherit" },
276
+ );
277
+ });
278
+
279
+ it("upgrade runs init and prints reserved", async () => {
280
+ process.argv = ["node", "grunt", "upgrade"];
281
+ await start();
282
+ expect(init).toHaveBeenCalledOnce();
283
+ expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
284
+ expect(chunks.join("")).toBe("reserved: browser tmp write-plan\n");
285
+ expect(select).not.toHaveBeenCalled();
286
+ });
287
+
248
288
  it("unknown writes usage and exitCode 1", async () => {
249
289
  process.argv = ["node", "grunt", "nope"];
250
290
  await start();
@@ -273,6 +313,8 @@ describe("start", () => {
273
313
  "sync-globals",
274
314
  "purge-mcps",
275
315
  "doctor",
316
+ "setup",
317
+ "upgrade",
276
318
  "help",
277
319
  "quit",
278
320
  ]);
@@ -285,7 +327,7 @@ describe("start", () => {
285
327
  select.mockResolvedValue("generate");
286
328
  process.argv = ["node", "grunt"];
287
329
  await start();
288
- expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:generate"], {
330
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "grunt:rulesync:generate"], {
289
331
  cwd: process.cwd(),
290
332
  stdio: "inherit",
291
333
  });
package/cli/init.mjs CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  WORKSPACE_SKILLS_REL,
7
7
  findSkillContentConflicts,
8
8
  formatSkillConflictWarn,
9
+ listSkillDirNames,
9
10
  } from "../scripts/skill-conflicts.mjs"
10
11
 
11
12
  const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
@@ -23,29 +24,96 @@ const PRODUCT_SCRIPTS = [
23
24
  "grunt-job.mjs",
24
25
  "parse-need.mjs",
25
26
  "persist-handoff.mjs",
27
+ "persist-implementation.mjs",
28
+ "persist-tmp.mjs",
26
29
  "persist-plan.mjs",
27
30
  "purge-global-mcps.mjs",
28
31
  "scrub-spawn-prompt.mjs",
29
32
  "scrub-text-lib.mjs",
30
33
  "sync-global-settings.mjs",
31
34
  "browser.mjs",
35
+ "speak.mjs",
36
+ "listen.mjs",
37
+ "google-workspace.mjs",
38
+ "setup.mjs",
32
39
  "doctor.mjs",
33
40
  "skill-conflicts.mjs",
34
41
  "scrub-text",
35
42
  ]
36
43
 
37
44
  const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"]
45
+ export const RETIRED_SKILLS = ["parent", "solo", "cascade"]
46
+ export const RETIRED_AGENTS = ["implementer", "thinker"]
47
+ /** Cumulative. Dest `scripts/<name>` deleted on init/upgrade even if no longer shipped. */
48
+ export const RETIRED_SCRIPTS = ["telemetry.mjs", "grunt-config.mjs"]
49
+ /** Repo-relative paths grunt used to ship. Deleted on init/upgrade. Consumer extras elsewhere kept. */
50
+ export const RETIRED_PATHS = [
51
+ ".grok/parent.md",
52
+ ".grok/skills/shared",
53
+ ".rulesync/grunt.config.jsonc",
54
+ ".rulesync/grunt.config.local.jsonc",
55
+ ".rulesync/grunt.config.local.jsonc.example",
56
+ ]
57
+ export const RESERVED_SKILLS = [
58
+ "ask",
59
+ "auto",
60
+ "browser",
61
+ "clasp",
62
+ "commit",
63
+ "commit-and-push",
64
+ "commit-push",
65
+ "commit-push-deploy",
66
+ "commit-push-release",
67
+ "explain",
68
+ "google-workspace",
69
+ "handoff",
70
+ "implement-plan",
71
+ "listen",
72
+ "pickup",
73
+ "speak",
74
+ "tmp",
75
+ "write-plan",
76
+ ]
77
+ const SKILL_MIRROR_DIRS = [
78
+ [".rulesync", "skills"],
79
+ [".grok", "skills"],
80
+ [".claude", "skills"],
81
+ [".agents", "skills"],
82
+ ]
83
+ const AGENT_MIRROR_DIRS = [
84
+ [".rulesync", "subagents"],
85
+ [".claude", "agents"],
86
+ [".grok", "agents"],
87
+ [".agents", "agents"],
88
+ [".codex", "agents"],
89
+ ]
38
90
  const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"]
39
91
  export const GUARDED_ROOT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]
40
92
  // .mcp.json is intentionally not copied here: scripts/emit-mcp-policy.mjs
41
- // (run via `rulesync:generate`, see mergeClaudeSettings ordering) owns and
93
+ // (run via consumer `grunt:rulesync:generate`, see mergeClaudeSettings ordering) owns and
42
94
  // merges it, so a plain-copy would just be clobbered/redundant.
95
+ export const GRUNT_NPM_PREFIX = "grunt:"
96
+ export function toGruntScriptName(k) {
97
+ return k.startsWith(GRUNT_NPM_PREFIX) ? k : `${GRUNT_NPM_PREFIX}${k}`
98
+ }
99
+ export const LAUNCH_SCRIPTS = {
100
+ antigravity: "antigravity --dangerously-skip-permissions",
101
+ claude: "claude --dangerously-skip-permissions",
102
+ codex: "codex --dangerously-bypass-approvals-and-sandbox",
103
+ gemini: "gemini --yolo",
104
+ grok: "grok --yolo",
105
+ }
43
106
  const OWNED_HOOK_FILES = ["scrub-spawn-prompt.mjs", "gate-fat-tools.mjs", "orchestrate-parent.js"]
44
107
  export const SENTINEL_BEGIN = "<!-- grunt:begin -->"
45
108
  export const SENTINEL_END = "<!-- grunt:end -->"
46
109
  export const MAX_GUARDED_MARKDOWN_BYTES = 2 * 1024 * 1024
47
- const GRUNT_REGION_RE = /<!-- grunt:begin -->\r?\n?[\s\S]*?<!-- grunt:end -->\r?\n?/g
48
- const GRUNT_INTERIOR_RE = /<!-- grunt:begin -->\r?\n?([\s\S]*?)<!-- grunt:end -->/g
110
+ const GRUNT_REGION_RE =
111
+ /(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:begin -->[ \t]*\r?\n?[\s\S]*?(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:end -->[ \t]*\r?\n?/g
112
+ const GRUNT_INTERIOR_RE =
113
+ /(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:begin -->[ \t]*\r?\n?([\s\S]*?)(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- grunt:end -->/g
114
+ const ORPHAN_END_RE = /(?:^|\r?\n)[ \t]*<!-- grunt:end -->[ \t]*(?=\r?\n|$)|<!-- grunt:end -->/g
115
+ const BEGIN_LINE_RE = /^[ \t]*<!-- grunt:begin -->[ \t]*$/
116
+ const END_LINE_RE = /^[ \t]*<!-- grunt:end -->[ \t]*$/
49
117
 
50
118
  function sortKeys(obj) {
51
119
  return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]))
@@ -59,17 +127,58 @@ export function samePath(a, b) {
59
127
  }
60
128
  }
61
129
 
130
+ const GITIGNORE_ENTRIES = [{ re: /^\.tmp\/?$/, line: ".tmp/" }]
131
+
132
+ function rmQuiet(abs) {
133
+ fs.rmSync(abs, { recursive: true, force: true })
134
+ }
135
+
136
+ export function pruneRetired(dest, { pkgRoot } = {}) {
137
+ const root = path.resolve(dest)
138
+ for (const segs of SKILL_MIRROR_DIRS) {
139
+ for (const name of RETIRED_SKILLS) {
140
+ rmQuiet(path.join(root, ...segs, name))
141
+ }
142
+ }
143
+ if (pkgRoot) {
144
+ const packaged = new Set(
145
+ listSkillDirNames(path.join(path.resolve(pkgRoot), WORKSPACE_SKILLS_REL)),
146
+ )
147
+ for (const segs of SKILL_MIRROR_DIRS) {
148
+ for (const name of RESERVED_SKILLS) {
149
+ if (packaged.has(name)) continue
150
+ rmQuiet(path.join(root, ...segs, name))
151
+ }
152
+ }
153
+ }
154
+ for (const segs of AGENT_MIRROR_DIRS) {
155
+ for (const name of RETIRED_AGENTS) {
156
+ rmQuiet(path.join(root, ...segs, `${name}.md`))
157
+ rmQuiet(path.join(root, ...segs, `${name}.toml`))
158
+ }
159
+ }
160
+ for (const name of RETIRED_AGENTS) {
161
+ rmQuiet(path.join(root, ".gemini", "agents", name))
162
+ rmQuiet(path.join(root, ".grok", "roles", `${name}.toml`))
163
+ }
164
+ for (const name of RETIRED_SCRIPTS) {
165
+ rmQuiet(path.join(root, "scripts", name))
166
+ }
167
+ for (const rel of RETIRED_PATHS) {
168
+ rmQuiet(path.join(root, ...rel.split("/")))
169
+ }
170
+ }
171
+
62
172
  export function mergeGitignore(dest) {
63
173
  const gi = path.join(dest, ".gitignore")
64
- if (!fs.existsSync(gi)) {
65
- fs.writeFileSync(gi, ".tmp/\n")
66
- return
67
- }
68
- const text = fs.readFileSync(gi, "utf8")
69
- const has = text.split(/\r?\n/).some((line) => /^\.tmp\/?$/.test(line))
70
- if (has) return
71
- const prefix = text.endsWith("\n") || text.length === 0 ? "" : "\n"
72
- fs.appendFileSync(gi, `${prefix}.tmp/\n`)
174
+ const text = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : ""
175
+ const present = text.split(/\r?\n/)
176
+ const missing = GITIGNORE_ENTRIES.filter(
177
+ ({ re }) => !present.some((line) => re.test(line)),
178
+ ).map(({ line }) => line)
179
+ if (missing.length === 0) return
180
+ const prefix = text.length === 0 || text.endsWith("\n") ? "" : "\n"
181
+ fs.writeFileSync(gi, `${text}${prefix}${missing.join("\n")}\n`)
73
182
  }
74
183
 
75
184
  function destHasGruntSentinel(dest) {
@@ -118,18 +227,43 @@ function inspectGuardedBuffer(buf) {
118
227
  }
119
228
 
120
229
  export function extractUserMarkdown(text) {
121
- return String(text).replace(cloneRe(GRUNT_REGION_RE), "")
230
+ let user = String(text).replace(cloneRe(GRUNT_REGION_RE), "")
231
+ const i = user.indexOf(SENTINEL_BEGIN)
232
+ if (i >= 0) user = user.slice(0, i)
233
+ user = user.replace(cloneRe(ORPHAN_END_RE), "")
234
+ return userRemainderIsBlank(user) ? "" : user
235
+ }
236
+
237
+ function unwrapStackedMarkers(text) {
238
+ const lines = String(text).split(/\r?\n/)
239
+ let start = 0
240
+ let end = lines.length
241
+ while (start < end && BEGIN_LINE_RE.test(lines[start])) start++
242
+ while (end > start && lines[end - 1] === "") end--
243
+ while (end > start && END_LINE_RE.test(lines[end - 1])) {
244
+ end--
245
+ while (end > start && lines[end - 1] === "") end--
246
+ }
247
+ return lines.slice(start, end).join("\n")
122
248
  }
123
249
 
124
250
  export function extractGruntBody(text) {
251
+ const src = String(text)
125
252
  const parts = []
126
253
  const re = cloneRe(GRUNT_INTERIOR_RE)
127
254
  let m
128
- while ((m = re.exec(String(text)))) {
129
- parts.push(trimTrailingNewlines(m[1]))
255
+ while ((m = re.exec(src))) {
256
+ parts.push(trimTrailingNewlines(unwrapStackedMarkers(m[1])))
257
+ }
258
+ if (parts.length) return parts.join("\n")
259
+ const i = src.indexOf(SENTINEL_BEGIN)
260
+ if (i >= 0) {
261
+ return trimTrailingNewlines(
262
+ unwrapStackedMarkers(src.slice(i + SENTINEL_BEGIN.length).replace(/^[ \t]*\r?\n/, "")),
263
+ )
130
264
  }
131
- if (!parts.length) return null
132
- return parts.join("\n")
265
+ if (!src.includes(SENTINEL_END)) return null
266
+ return trimTrailingNewlines(unwrapStackedMarkers(src))
133
267
  }
134
268
 
135
269
  function userRemainderIsBlank(user) {
@@ -137,9 +271,10 @@ function userRemainderIsBlank(user) {
137
271
  }
138
272
 
139
273
  export function composeGuardedMarkdown(gruntBody, userRemainder, nl = "\n") {
140
- const body = toNewline(trimTrailingNewlines(gruntBody), nl)
274
+ const raw = extractGruntBody(gruntBody) ?? gruntBody
275
+ const body = toNewline(trimTrailingNewlines(raw), nl)
141
276
  const block = `${SENTINEL_BEGIN}${nl}${body}${nl}${SENTINEL_END}${nl}`
142
- const user = userRemainder == null ? "" : String(userRemainder)
277
+ const user = extractUserMarkdown(userRemainder == null ? "" : String(userRemainder))
143
278
  if (!user || userRemainderIsBlank(user)) return block
144
279
  return block + toNewline(user, nl)
145
280
  }
@@ -184,7 +319,7 @@ export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = fals
184
319
  const d = path.join(dest, file)
185
320
  if (samePath(src, d)) return
186
321
  const srcText = fs.readFileSync(src, "utf8")
187
- return writeMergedGuardedFile(d, srcText)
322
+ return writeMergedGuardedFile(d, extractGruntBody(srcText) ?? srcText)
188
323
  }
189
324
 
190
325
  export function snapshotGuardedRoots(root) {
@@ -339,7 +474,13 @@ export function mergeClaudeSettings(destRoot, pkgRoot) {
339
474
  }
340
475
 
341
476
  function looksGruntOwnedPrefix(prefix) {
342
- return /rulesync|sync:globals|^npm run /.test(prefix)
477
+ return /rulesync|sync:globals|^npm run |guarded-roots\.mjs|hooks-union\.mjs|doctor\.mjs|sync-global-settings\.mjs|check-globals\.mjs|purge-global-mcps\.mjs/.test(
478
+ prefix,
479
+ )
480
+ }
481
+
482
+ function escapeRe(s) {
483
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
343
484
  }
344
485
 
345
486
  function commandCount(script) {
@@ -364,14 +505,29 @@ function extraOwnedSuffix(cur, newSrc) {
364
505
  return null
365
506
  }
366
507
 
367
- function mergeScriptValue(name, newSrc, cur) {
368
- if (cur == null) return newSrc
508
+ function mergeScriptValue(newSrc, cur) {
369
509
  if (cur.startsWith(newSrc)) return cur
370
510
  const suffix = extraOwnedSuffix(cur, newSrc)
371
511
  if (suffix != null) return newSrc + suffix
372
- if (looksGruntOwnedPrefix(cur)) return newSrc
373
- console.warn(`script \`${name}\` left untouched (unrelated customization)`)
374
- return cur
512
+ return newSrc
513
+ }
514
+
515
+ function rewriteNpmRunRefs(scripts, srcKeys) {
516
+ const keys = srcKeys
517
+ .filter((k) => k !== toGruntScriptName(k))
518
+ .sort((a, b) => b.length - a.length)
519
+ if (!keys.length) return
520
+ for (const name of Object.keys(scripts)) {
521
+ let val = scripts[name]
522
+ if (typeof val !== "string") continue
523
+ for (const k of keys) {
524
+ val = val.replace(
525
+ new RegExp(`npm run ${escapeRe(k)}(?=$|\\s|;|&)`, "g"),
526
+ `npm run ${toGruntScriptName(k)}`,
527
+ )
528
+ }
529
+ scripts[name] = val
530
+ }
375
531
  }
376
532
 
377
533
  export function mergePackageJson(dest, pkgRoot) {
@@ -384,10 +540,39 @@ export function mergePackageJson(dest, pkgRoot) {
384
540
 
385
541
  const srcPkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"))
386
542
  destPkg.scripts = { ...(destPkg.scripts || {}) }
543
+ const srcKeys = []
387
544
  for (const [k, v] of Object.entries(srcPkg.scripts || {})) {
388
- if (k === "test" || k.endsWith(":raw")) continue
389
- destPkg.scripts[k] = mergeScriptValue(k, v, destPkg.scripts[k])
545
+ if (k === "test" || k.endsWith(":raw") || k in LAUNCH_SCRIPTS) continue
546
+ srcKeys.push(k)
547
+ const destKey = toGruntScriptName(k)
548
+ const current = destPkg.scripts[destKey]
549
+ const legacy = destPkg.scripts[k]
550
+ const ownedLegacy =
551
+ legacy != null &&
552
+ (legacy.startsWith(v) || extraOwnedSuffix(legacy, v) != null || looksGruntOwnedPrefix(legacy))
553
+ if (current != null) {
554
+ destPkg.scripts[destKey] = mergeScriptValue(v, current)
555
+ } else if (ownedLegacy) {
556
+ destPkg.scripts[destKey] = mergeScriptValue(v, legacy)
557
+ } else {
558
+ destPkg.scripts[destKey] = v
559
+ }
560
+ if (k !== destKey && ownedLegacy) {
561
+ delete destPkg.scripts[k]
562
+ } else if (k !== destKey && legacy != null) {
563
+ console.warn(
564
+ `script \`${k}\` left untouched (unrelated customization); writing \`${destKey}\``,
565
+ )
566
+ }
390
567
  }
568
+ for (const [k, v] of Object.entries(LAUNCH_SCRIPTS)) {
569
+ const destKey = toGruntScriptName(k)
570
+ const current = destPkg.scripts[destKey]
571
+ destPkg.scripts[destKey] = current != null ? mergeScriptValue(v, current) : v
572
+ const staleKey = `grunt:yolo:${k}`
573
+ if (destPkg.scripts[staleKey] === v) delete destPkg.scripts[staleKey]
574
+ }
575
+ rewriteNpmRunRefs(destPkg.scripts, srcKeys)
391
576
  destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
392
577
  destPkg.devDependencies["smol-toml"] = srcPkg.devDependencies["smol-toml"]
393
578
  destPkg.devDependencies["rulesync"] = srcPkg.devDependencies["rulesync"]
@@ -442,6 +627,8 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
442
627
  }
443
628
  }
444
629
 
630
+ pruneRetired(dest, { pkgRoot })
631
+
445
632
  for (const file of GUARDED_MD_FILES) {
446
633
  mergeGuardedMarkdown(dest, pkgRoot, file)
447
634
  }
@@ -473,12 +660,12 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
473
660
  }
474
661
  runNpm("install", ["install"])
475
662
  const guardedSnap = snapshotGuardedRoots(dest)
476
- runNpm("generate", ["run", "rulesync:generate"])
663
+ runNpm("generate", ["run", toGruntScriptName("rulesync:generate")])
477
664
  remergeGuardedRoots(dest, guardedSnap)
478
665
  if (!skipGlobalsApply) {
479
- runNpm("sync-globals", ["run", "sync:globals:apply"])
666
+ runNpm("sync-globals", ["run", toGruntScriptName("sync:globals:apply")])
480
667
  }
481
668
  withGuardedCheckInteriors(dest, () => {
482
- runNpm("check", ["run", "rulesync:check"])
669
+ runNpm("check", ["run", toGruntScriptName("rulesync:check")])
483
670
  })
484
671
  }