@lovrozagar/grunt 0.5.2 → 0.6.1

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 (157) hide show
  1. package/.agents/agents/grunt.md +2 -24
  2. package/.agents/agents/orchestrator.md +5 -50
  3. package/.agents/skills/ask/SKILL.md +5 -12
  4. package/.agents/skills/auto/SKILL.md +7 -12
  5. package/.agents/skills/browser/SKILL.md +4 -8
  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 -109
  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 +6 -10
  19. package/.agents/skills/write-plan/SKILL.md +5 -77
  20. package/.claude/agents/grunt.md +2 -26
  21. package/.claude/agents/orchestrator.md +12 -50
  22. package/.claude/skills/ask/SKILL.md +5 -12
  23. package/.claude/skills/auto/SKILL.md +7 -12
  24. package/.claude/skills/browser/SKILL.md +4 -8
  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 -109
  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 +6 -10
  38. package/.claude/skills/write-plan/SKILL.md +5 -77
  39. package/.codex/agents/grunt.toml +2 -26
  40. package/.codex/agents/orchestrator.toml +4 -50
  41. package/.grok/agents/grunt.md +2 -24
  42. package/.grok/agents/orchestrator.md +4 -48
  43. package/.grok/global-settings.toml +1 -3
  44. package/.grok/hooks/orchestrate-parent.js +190 -358
  45. package/.grok/skills/ask/SKILL.md +5 -12
  46. package/.grok/skills/auto/SKILL.md +7 -12
  47. package/.grok/skills/browser/SKILL.md +4 -8
  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 -109
  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 +6 -10
  61. package/.grok/skills/write-plan/SKILL.md +5 -77
  62. package/.rulesync/hooks.jsonc +1 -1
  63. package/.rulesync/mcp-policy.jsonc +0 -2
  64. package/.rulesync/reference/INDEX.md +30 -20
  65. package/.rulesync/reference/browser.md +13 -6
  66. package/.rulesync/reference/cascade.md +6 -86
  67. package/.rulesync/reference/clasp.md +21 -0
  68. package/.rulesync/reference/google-workspace.md +79 -0
  69. package/.rulesync/reference/hooks.md +7 -11
  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 +40 -36
  74. package/.rulesync/reference/output.md +3 -51
  75. package/.rulesync/reference/plan-format.md +4 -2
  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 -12
  81. package/.rulesync/reference/speak.md +59 -0
  82. package/.rulesync/rules/CLAUDE.md +3 -49
  83. package/.rulesync/rules/overview.md +9 -47
  84. package/.rulesync/skills/ask/SKILL.md +5 -12
  85. package/.rulesync/skills/auto/SKILL.md +5 -12
  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 -119
  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 +6 -10
  100. package/.rulesync/skills/write-plan/SKILL.md +5 -84
  101. package/.rulesync/subagents/grunt.md +2 -24
  102. package/.rulesync/subagents/orchestrator.md +14 -59
  103. package/AGENTS.md +9 -47
  104. package/CHANGELOG.md +67 -0
  105. package/CLAUDE.md +1 -49
  106. package/README.md +89 -80
  107. package/cli/grunt.mjs +21 -3
  108. package/cli/grunt.test.ts +47 -3
  109. package/cli/init.mjs +134 -234
  110. package/cli/init.test.ts +241 -58
  111. package/cli/prompt.mjs +9 -36
  112. package/cli/prompt.test.ts +14 -0
  113. package/package.json +10 -2
  114. package/scripts/browser.mjs +128 -7
  115. package/scripts/doctor.mjs +104 -10
  116. package/scripts/emit-gemini.mjs +2 -2
  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/guarded-md.mjs +230 -0
  122. package/scripts/guarded-roots.mjs +1 -1
  123. package/scripts/interactive.mjs +15 -0
  124. package/scripts/listen.mjs +829 -0
  125. package/scripts/parse-need.mjs +21 -2
  126. package/scripts/persist-implementation.mjs +278 -0
  127. package/scripts/prompt.mjs +32 -0
  128. package/scripts/scrub-spawn-prompt.mjs +3 -50
  129. package/scripts/setup.mjs +746 -0
  130. package/scripts/speak.mjs +291 -0
  131. package/.agents/agents/implementer.md +0 -32
  132. package/.agents/agents/thinker.md +0 -29
  133. package/.agents/skills/cascade/SKILL.md +0 -28
  134. package/.agents/skills/parent/SKILL.md +0 -6
  135. package/.agents/skills/solo/SKILL.md +0 -25
  136. package/.claude/agents/implementer.md +0 -37
  137. package/.claude/agents/thinker.md +0 -36
  138. package/.claude/skills/cascade/SKILL.md +0 -28
  139. package/.claude/skills/parent/SKILL.md +0 -6
  140. package/.claude/skills/solo/SKILL.md +0 -25
  141. package/.codex/agents/implementer.toml +0 -26
  142. package/.codex/agents/thinker.toml +0 -26
  143. package/.grok/agents/implementer.md +0 -27
  144. package/.grok/agents/thinker.md +0 -27
  145. package/.grok/parent.md +0 -1
  146. package/.grok/roles/implementer.toml +0 -5
  147. package/.grok/roles/thinker.toml +0 -5
  148. package/.grok/skills/cascade/SKILL.md +0 -28
  149. package/.grok/skills/parent/SKILL.md +0 -6
  150. package/.grok/skills/solo/SKILL.md +0 -25
  151. package/.rulesync/grunt.config.jsonc +0 -7
  152. package/.rulesync/skills/cascade/SKILL.md +0 -28
  153. package/.rulesync/skills/parent/SKILL.md +0 -6
  154. package/.rulesync/skills/solo/SKILL.md +0 -25
  155. package/.rulesync/subagents/implementer.md +0 -49
  156. package/.rulesync/subagents/thinker.md +0 -49
  157. package/scripts/grunt-config.mjs +0 -50
package/cli/init.test.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -5,6 +6,7 @@ import { fileURLToPath } from "node:url";
5
6
  import { afterEach, describe, expect, it, vi } from "vitest";
6
7
  import {
7
8
  MAX_GUARDED_MARKDOWN_BYTES,
9
+ PRODUCT_SCRIPTS,
8
10
  composeGuardedMarkdown,
9
11
  destAlreadyInited,
10
12
  extractGruntBody,
@@ -15,6 +17,12 @@ import {
15
17
  guardedMarkdownDrift,
16
18
  healGuardedRootFile,
17
19
  init,
20
+ pruneRetired,
21
+ RETIRED_SKILLS,
22
+ RETIRED_AGENTS,
23
+ RETIRED_SCRIPTS,
24
+ RETIRED_PATHS,
25
+ RESERVED_SKILLS,
18
26
  LAUNCH_SCRIPTS,
19
27
  mergeClaudeSettings,
20
28
  mergeGitignore,
@@ -30,7 +38,6 @@ import {
30
38
  writeMergedGuardedFile,
31
39
  } from "./init.mjs";
32
40
  import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
33
- import { stripJsonc } from "../scripts/grunt-config.mjs";
34
41
 
35
42
  const tmpDirs: string[] = [];
36
43
  afterEach(() => {
@@ -46,37 +53,9 @@ function tmp(prefix: string) {
46
53
  }
47
54
 
48
55
  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
- };
55
56
  const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"];
56
- const PRODUCT_FILES = [
57
- "check-globals.mjs",
58
- "emit-agent-shell-tools.mjs",
59
- "emit-gemini.mjs",
60
- "emit-maps.mjs",
61
- "guarded-roots.mjs",
62
- "emit-mcp-policy.mjs",
63
- "gate-fat-tools.mjs",
64
- "hooks-union.mjs",
65
- "pipeline.mjs",
66
- "grunt-job.mjs",
67
- "grunt-config.mjs",
68
- "parse-need.mjs",
69
- "persist-handoff.mjs",
70
- "persist-tmp.mjs",
71
- "persist-plan.mjs",
72
- "purge-global-mcps.mjs",
73
- "scrub-spawn-prompt.mjs",
74
- "scrub-text-lib.mjs",
75
- "sync-global-settings.mjs",
76
- "browser.mjs",
77
- "doctor.mjs",
78
- "skill-conflicts.mjs",
79
- ];
57
+ const PRODUCT_FILES = PRODUCT_SCRIPTS.filter((name) => name !== "scrub-text");
58
+ const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
80
59
 
81
60
  const SRC_CLAUDE_SETTINGS = {
82
61
  permissions: {
@@ -118,12 +97,6 @@ const SRC_CLAUDE_SETTINGS = {
118
97
  enabledMcpjsonServers: [],
119
98
  };
120
99
 
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
-
127
100
  function stubPkgRoot(pkg: Record<string, unknown> = {
128
101
  name: "fixture-pkg",
129
102
  scripts: {
@@ -143,10 +116,6 @@ function stubPkgRoot(pkg: Record<string, unknown> = {
143
116
  fs.mkdirSync(path.join(root, d));
144
117
  fs.writeFileSync(path.join(root, d, "marker"), d);
145
118
  }
146
- fs.writeFileSync(
147
- path.join(root, GRUNT_CONFIG_REL),
148
- `${JSON.stringify(GRUNT_CONFIG_DEFAULTS, null, 2)}\n`,
149
- );
150
119
  fs.writeFileSync(
151
120
  path.join(root, ".claude", "settings.json"),
152
121
  `${JSON.stringify(SRC_CLAUDE_SETTINGS, null, 2)}\n`,
@@ -179,18 +148,22 @@ describe("samePath", () => {
179
148
  });
180
149
  });
181
150
 
151
+ const GI_TMP = ".tmp/\n";
152
+
182
153
  describe("mergeGitignore", () => {
183
154
  it("creates .tmp/ when missing", () => {
184
155
  const dest = tmp("gi-miss-");
185
156
  mergeGitignore(dest);
186
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
157
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(GI_TMP);
187
158
  });
188
159
 
189
- it("noop when .tmp/ present", () => {
160
+ it("noop when .tmp/ already present", () => {
190
161
  const dest = tmp("gi-slash-");
191
162
  fs.writeFileSync(path.join(dest, ".gitignore"), "foo\n.tmp/\nbar\n");
192
163
  mergeGitignore(dest);
193
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe("foo\n.tmp/\nbar\n");
164
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
165
+ "foo\n.tmp/\nbar\n",
166
+ );
194
167
  });
195
168
 
196
169
  it("noop when .tmp present", () => {
@@ -200,12 +173,21 @@ describe("mergeGitignore", () => {
200
173
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp\n");
201
174
  });
202
175
 
176
+ it("noop when .tmp/ present in mixed file", () => {
177
+ const dest = tmp("gi-both-");
178
+ fs.writeFileSync(path.join(dest, ".gitignore"), `foo\n${GI_TMP}bar\n`);
179
+ mergeGitignore(dest);
180
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
181
+ `foo\n${GI_TMP}bar\n`,
182
+ );
183
+ });
184
+
203
185
  it("appends after trailing newline", () => {
204
186
  const dest = tmp("gi-nl-");
205
187
  fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/\n");
206
188
  mergeGitignore(dest);
207
189
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
208
- "node_modules/\n.tmp/\n",
190
+ `node_modules/\n${GI_TMP}`,
209
191
  );
210
192
  });
211
193
 
@@ -214,7 +196,7 @@ describe("mergeGitignore", () => {
214
196
  fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/");
215
197
  mergeGitignore(dest);
216
198
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
217
- "node_modules/\n.tmp/\n",
199
+ `node_modules/\n${GI_TMP}`,
218
200
  );
219
201
  });
220
202
 
@@ -222,7 +204,7 @@ describe("mergeGitignore", () => {
222
204
  const dest = tmp("gi-empty-");
223
205
  fs.writeFileSync(path.join(dest, ".gitignore"), "");
224
206
  mergeGitignore(dest);
225
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
207
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(GI_TMP);
226
208
  });
227
209
  });
228
210
 
@@ -1550,9 +1532,9 @@ describe("init", () => {
1550
1532
  if (d === ".claude") continue;
1551
1533
  expect(fs.readFileSync(path.join(dest, d, "marker"), "utf8")).toBe(d);
1552
1534
  }
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);
1535
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1536
+ expect(PRODUCT_FILES).not.toContain("grunt-config.mjs");
1537
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1556
1538
  expect(fs.readFileSync(path.join(dest, ".claude", "marker"), "utf8")).toBe(".claude");
1557
1539
  const settings = JSON.parse(
1558
1540
  fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
@@ -1626,6 +1608,109 @@ describe("init", () => {
1626
1608
  expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
1627
1609
  });
1628
1610
 
1611
+ it("prunes retired 0.5 skills and agents on dest", () => {
1612
+ const pkgRoot = stubPkgRoot();
1613
+ const dest = tmp("grunt-prune-");
1614
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "solo"), { recursive: true });
1615
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "solo", "SKILL.md"), "old");
1616
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "parent"), { recursive: true });
1617
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "cascade"), { recursive: true });
1618
+ fs.mkdirSync(path.join(dest, ".claude", "agents"), { recursive: true });
1619
+ fs.writeFileSync(path.join(dest, ".claude", "agents", "implementer.md"), "old");
1620
+ fs.writeFileSync(path.join(dest, ".claude", "agents", "thinker.md"), "old");
1621
+ expect(RETIRED_SKILLS).toEqual(["parent", "solo", "cascade"]);
1622
+ expect(RETIRED_AGENTS).toEqual(["implementer", "thinker"]);
1623
+ expect(RETIRED_SCRIPTS).toEqual(["telemetry.mjs", "grunt-config.mjs"]);
1624
+ expect(RETIRED_PATHS).toEqual([
1625
+ ".grok/parent.md",
1626
+ ".grok/skills/shared",
1627
+ ".rulesync/grunt.config.jsonc",
1628
+ ".rulesync/grunt.config.local.jsonc",
1629
+ ".rulesync/grunt.config.local.jsonc.example",
1630
+ ]);
1631
+ expect(RESERVED_SKILLS).toEqual([
1632
+ "ask",
1633
+ "auto",
1634
+ "browser",
1635
+ "clasp",
1636
+ "commit",
1637
+ "commit-and-push",
1638
+ "commit-push",
1639
+ "commit-push-deploy",
1640
+ "commit-push-release",
1641
+ "explain",
1642
+ "google-workspace",
1643
+ "handoff",
1644
+ "implement-plan",
1645
+ "listen",
1646
+ "pickup",
1647
+ "speak",
1648
+ "tmp",
1649
+ "write-plan",
1650
+ ]);
1651
+ expect(RESERVED_SKILLS).not.toEqual(expect.arrayContaining(RETIRED_SKILLS));
1652
+ init(dest, { pkgRoot, execFileSync: vi.fn() });
1653
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "solo"))).toBe(false);
1654
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "parent"))).toBe(false);
1655
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "cascade"))).toBe(false);
1656
+ expect(fs.existsSync(path.join(dest, ".claude", "agents", "implementer.md"))).toBe(
1657
+ false,
1658
+ );
1659
+ expect(fs.existsSync(path.join(dest, ".claude", "agents", "thinker.md"))).toBe(false);
1660
+ pruneRetired(dest, { pkgRoot });
1661
+ });
1662
+
1663
+ it("prunes retired scripts/paths, reserved-not-packaged skills, and grok roles; keeps consumer extras", () => {
1664
+ const pkgRoot = stubPkgRoot();
1665
+ fs.mkdirSync(path.join(pkgRoot, ".rulesync", "skills", "tmp"), { recursive: true });
1666
+ fs.writeFileSync(path.join(pkgRoot, ".rulesync", "skills", "tmp", "SKILL.md"), "pkg tmp");
1667
+ const dest = tmp("grunt-prune-extra-");
1668
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "tmp"), { recursive: true });
1669
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "tmp", "SKILL.md"), "old tmp");
1670
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "browser"), { recursive: true });
1671
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "browser", "SKILL.md"), "stale reserved");
1672
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "acme"), { recursive: true });
1673
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "acme", "SKILL.md"), "consumer");
1674
+ fs.mkdirSync(path.join(dest, ".grok", "roles"), { recursive: true });
1675
+ fs.writeFileSync(path.join(dest, ".grok", "roles", "implementer.toml"), "old");
1676
+ fs.writeFileSync(path.join(dest, ".grok", "roles", "thinker.toml"), "old");
1677
+ fs.writeFileSync(path.join(dest, ".grok", "parent.md"), "pointer");
1678
+ fs.mkdirSync(path.join(dest, ".grok", "skills", "shared"), { recursive: true });
1679
+ fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1680
+ fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "old telemetry");
1681
+ fs.writeFileSync(path.join(dest, "scripts", "grunt-config.mjs"), "old config");
1682
+ fs.writeFileSync(path.join(dest, "scripts", "mine.mjs"), "keep");
1683
+ fs.writeFileSync(path.join(dest, ".rulesync", "grunt.config.jsonc"), '{"version":1}\n');
1684
+ fs.writeFileSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc"), '{"sessionGate":"ask"}\n');
1685
+ fs.writeFileSync(
1686
+ path.join(dest, ".rulesync", "grunt.config.local.jsonc.example"),
1687
+ '{"sessionGate":"ask"}\n',
1688
+ );
1689
+ fs.mkdirSync(path.join(dest, ".gemini", "agents", "implementer"), { recursive: true });
1690
+ fs.writeFileSync(path.join(dest, ".gemini", "agents", "implementer", "agent.md"), "old");
1691
+ init(dest, { pkgRoot, execFileSync: vi.fn() });
1692
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "tmp", "SKILL.md"), "utf8")).toBe(
1693
+ "pkg tmp",
1694
+ );
1695
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "browser"))).toBe(false);
1696
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "acme", "SKILL.md"), "utf8")).toBe(
1697
+ "consumer",
1698
+ );
1699
+ expect(fs.existsSync(path.join(dest, ".grok", "roles", "implementer.toml"))).toBe(false);
1700
+ expect(fs.existsSync(path.join(dest, ".grok", "roles", "thinker.toml"))).toBe(false);
1701
+ expect(fs.existsSync(path.join(dest, ".grok", "parent.md"))).toBe(false);
1702
+ expect(fs.existsSync(path.join(dest, ".grok", "skills", "shared"))).toBe(false);
1703
+ expect(fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))).toBe(false);
1704
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1705
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1706
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc"))).toBe(false);
1707
+ expect(
1708
+ fs.existsSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc.example")),
1709
+ ).toBe(false);
1710
+ expect(fs.readFileSync(path.join(dest, "scripts", "mine.mjs"), "utf8")).toBe("keep");
1711
+ expect(fs.existsSync(path.join(dest, ".gemini", "agents", "implementer"))).toBe(false);
1712
+ });
1713
+
1629
1714
  it("omitted pkgRoot uses built-in PKG_ROOT; exec mocked", () => {
1630
1715
  const dest = tmp("grunt-realroot-");
1631
1716
  const exec = vi.fn();
@@ -1638,9 +1723,12 @@ describe("init", () => {
1638
1723
  expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
1639
1724
  expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
1640
1725
  expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
1641
- expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(true);
1726
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1727
+ expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(true);
1728
+ expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(true);
1729
+ expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(true);
1642
1730
  expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
1643
- expect(readGruntConfig(dest)).toEqual(GRUNT_CONFIG_DEFAULTS);
1731
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1644
1732
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1645
1733
  ["install"],
1646
1734
  ["run", "grunt:rulesync:generate"],
@@ -1829,25 +1917,25 @@ describe("init", () => {
1829
1917
 
1830
1918
  it("warns when dest skill differs from packaged then force-overwrites; keeps extras", () => {
1831
1919
  const pkgRoot = stubPkgRoot();
1832
- const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "parent");
1920
+ const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "explain");
1833
1921
  fs.mkdirSync(pkgSkill, { recursive: true });
1834
- fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-parent\n");
1922
+ fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-explain\n");
1835
1923
  const dest = tmp("grunt-sk-conflict-");
1836
- const destSkill = path.join(dest, ".rulesync", "skills", "parent");
1924
+ const destSkill = path.join(dest, ".rulesync", "skills", "explain");
1837
1925
  const destExtra = path.join(dest, ".rulesync", "skills", "my-extra");
1838
1926
  fs.mkdirSync(destSkill, { recursive: true });
1839
1927
  fs.mkdirSync(destExtra, { recursive: true });
1840
- fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-parent\n");
1928
+ fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-explain\n");
1841
1929
  fs.writeFileSync(path.join(destExtra, "SKILL.md"), "keep-me\n");
1842
1930
  const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1843
1931
  init(dest, { pkgRoot, execFileSync: vi.fn(), skipGlobals: true });
1844
1932
  expect(
1845
1933
  warnSpy.mock.calls.some(
1846
- (c) => String(c[0]).includes("parent") && String(c[0]).includes("re-init overwrites"),
1934
+ (c) => String(c[0]).includes("explain") && String(c[0]).includes("re-init overwrites"),
1847
1935
  ),
1848
1936
  ).toBe(true);
1849
1937
  warnSpy.mockRestore();
1850
- expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-parent\n");
1938
+ expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-explain\n");
1851
1939
  expect(fs.readFileSync(path.join(destExtra, "SKILL.md"), "utf8")).toBe("keep-me\n");
1852
1940
  });
1853
1941
  });
@@ -1884,3 +1972,98 @@ describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
1884
1972
  });
1885
1973
  });
1886
1974
 
1975
+ function spawnDest(dest: string, args: string[], env: NodeJS.ProcessEnv = process.env) {
1976
+ return spawnSync(process.execPath, args, {
1977
+ cwd: dest,
1978
+ encoding: "utf8",
1979
+ env,
1980
+ });
1981
+ }
1982
+
1983
+ const PARENT_IMPORT_RE =
1984
+ /(?:from\s+|import\s*\(\s*)["'`](\.\.[^"'`]*)["'`]/g;
1985
+ const RELATIVE_IMPORT_RE =
1986
+ /(?:from\s+|import\s*\(\s*)["'`](\.[^"'`]+)["'`]/g;
1987
+
1988
+ function relativeSpecs(text: string, re: RegExp) {
1989
+ const out: string[] = [];
1990
+ const copy = new RegExp(re.source, "g");
1991
+ let m: RegExpExecArray | null;
1992
+ while ((m = copy.exec(text))) out.push(m[1]);
1993
+ return out;
1994
+ }
1995
+
1996
+ describe("product scripts are consumer-safe", () => {
1997
+ it("package.json files lists every PRODUCT_SCRIPTS entry", () => {
1998
+ const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"));
1999
+ for (const name of PRODUCT_SCRIPTS) {
2000
+ expect(pkg.files).toContain(`scripts/${name}`);
2001
+ }
2002
+ });
2003
+
2004
+ it("do not parent-import (from or import()) — init copies scripts/ only", () => {
2005
+ for (const name of PRODUCT_FILES) {
2006
+ const text = fs.readFileSync(path.join(PKG_ROOT, "scripts", name), "utf8");
2007
+ expect(relativeSpecs(text, PARENT_IMPORT_RE), name).toEqual([]);
2008
+ }
2009
+ });
2010
+
2011
+ it("repro: dest node ./scripts/guarded-roots.mjs generate misses ../cli/init.mjs", () => {
2012
+ const dest = tmp("repro-cli-miss-");
2013
+ fs.mkdirSync(path.join(dest, "scripts"));
2014
+ fs.writeFileSync(
2015
+ path.join(dest, "scripts", "broken.mjs"),
2016
+ 'import { snapshotGuardedRoots } from "../cli/init.mjs";\n',
2017
+ );
2018
+ const r = spawnDest(dest, ["./scripts/broken.mjs", "generate"]);
2019
+ expect(r.status).not.toBe(0);
2020
+ expect(r.stderr).toMatch(/ERR_MODULE_NOT_FOUND/);
2021
+ expect(r.stderr).toMatch(/cli\/init\.mjs/);
2022
+ });
2023
+
2024
+ it("after init, copied scripts' relative imports exist and generate loads", () => {
2025
+ const dest = tmp("init-generate-");
2026
+ init(dest, {
2027
+ pkgRoot: PKG_ROOT,
2028
+ execFileSync: vi.fn(),
2029
+ skipGlobals: true,
2030
+ applyGlobals: false,
2031
+ });
2032
+ expect(fs.existsSync(path.join(dest, "cli"))).toBe(false);
2033
+ expect(fs.existsSync(path.join(dest, "scripts", "guarded-roots.mjs"))).toBe(true);
2034
+
2035
+ for (const name of PRODUCT_FILES) {
2036
+ const file = path.join(dest, "scripts", name);
2037
+ expect(fs.existsSync(file), name).toBe(true);
2038
+ const text = fs.readFileSync(file, "utf8");
2039
+ for (const spec of relativeSpecs(text, RELATIVE_IMPORT_RE)) {
2040
+ const resolved = path.resolve(path.dirname(file), spec);
2041
+ expect(fs.existsSync(resolved), `${name} -> ${spec}`).toBe(true);
2042
+ }
2043
+ }
2044
+
2045
+ const loaded = spawnDest(dest, ["./scripts/guarded-roots.mjs"]);
2046
+ expect(loaded.status).toBe(1);
2047
+ expect(loaded.stderr).toBe("usage: guarded-roots.mjs generate|check|watch\n");
2048
+ expect(loaded.stderr).not.toMatch(/ERR_MODULE_NOT_FOUND/);
2049
+
2050
+ const gen = spawnDest(dest, ["./scripts/guarded-roots.mjs", "generate"], {
2051
+ ...process.env,
2052
+ PATH: ["/usr/bin", "/bin"].join(path.delimiter),
2053
+ });
2054
+ expect(gen.stderr).not.toMatch(/ERR_MODULE_NOT_FOUND/);
2055
+ expect(gen.stderr).not.toMatch(/cli\/init\.mjs/);
2056
+ expect(`${gen.stderr}${gen.stdout}`).toMatch(/rulesync|spawn|not found|ENOENT/i);
2057
+
2058
+ for (const name of ["guarded-roots.mjs", "emit-gemini.mjs", "setup.mjs"]) {
2059
+ const r = spawnDest(dest, [
2060
+ "--input-type=module",
2061
+ "-e",
2062
+ `import(${JSON.stringify(`./scripts/${name}`)})`,
2063
+ ]);
2064
+ expect(r.status, `${name} load\n${r.stderr}`).toBe(0);
2065
+ expect(r.stderr, name).not.toMatch(/ERR_MODULE_NOT_FOUND/);
2066
+ }
2067
+ });
2068
+ });
2069
+
package/cli/prompt.mjs CHANGED
@@ -1,36 +1,9 @@
1
- import * as clack from "@clack/prompts"
2
-
3
- export function isInteractive({
4
- argv = process.argv.slice(2),
5
- env = process.env,
6
- stdin = process.stdin,
7
- stdout = process.stdout,
8
- } = {}) {
9
- if (!stdin?.isTTY || !stdout?.isTTY) return false
10
- const ci = env.CI
11
- if (ci != null && ci !== "" && ci !== "0" && ci !== "false") return false
12
- for (const a of argv) {
13
- if (a === "--yes" || a === "-y" || a === "--non-interactive") return false
14
- }
15
- return true
16
- }
17
-
18
- export function bailIfCancel(value) {
19
- if (clack.isCancel(value)) {
20
- clack.cancel("Aborted")
21
- process.exit(0)
22
- }
23
- return value
24
- }
25
-
26
- export async function select(opts) {
27
- return bailIfCancel(await clack.select(opts))
28
- }
29
-
30
- export async function confirm(opts) {
31
- return bailIfCancel(await clack.confirm(opts))
32
- }
33
-
34
- export function spinner() {
35
- return clack.spinner()
36
- }
1
+ export {
2
+ bailIfCancel,
3
+ confirm,
4
+ isInteractive,
5
+ password,
6
+ select,
7
+ spinner,
8
+ text,
9
+ } from "../scripts/prompt.mjs"
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  const clackSelect = vi.hoisted(() => vi.fn());
4
4
  const clackConfirm = vi.hoisted(() => vi.fn());
5
+ const clackText = vi.hoisted(() => vi.fn());
6
+ const clackPassword = vi.hoisted(() => vi.fn());
5
7
  const clackSpinner = vi.hoisted(() => vi.fn());
6
8
  const clackIsCancel = vi.hoisted(() => vi.fn());
7
9
  const clackCancel = vi.hoisted(() => vi.fn());
@@ -9,6 +11,8 @@ const clackCancel = vi.hoisted(() => vi.fn());
9
11
  vi.mock("@clack/prompts", () => ({
10
12
  select: clackSelect,
11
13
  confirm: clackConfirm,
14
+ text: clackText,
15
+ password: clackPassword,
12
16
  spinner: clackSpinner,
13
17
  isCancel: clackIsCancel,
14
18
  cancel: clackCancel,
@@ -18,8 +22,10 @@ import {
18
22
  bailIfCancel,
19
23
  confirm,
20
24
  isInteractive,
25
+ password,
21
26
  select,
22
27
  spinner,
28
+ text,
23
29
  } from "./prompt.mjs";
24
30
 
25
31
  function tty(isTTY: boolean) {
@@ -196,4 +202,12 @@ describe("clack wrappers", () => {
196
202
  expect(spinner()).toBe(s);
197
203
  expect(clackSpinner).toHaveBeenCalledOnce();
198
204
  });
205
+
206
+ it("text and password bail then return", async () => {
207
+ clackText.mockResolvedValue("proj");
208
+ clackPassword.mockResolvedValue("sk_x");
209
+ clackIsCancel.mockReturnValue(false);
210
+ await expect(text({ message: "Project ID" })).resolves.toBe("proj");
211
+ await expect(password({ message: "API key" })).resolves.toBe("sk_x");
212
+ });
199
213
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrozagar/grunt",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
5
5
  "keywords": [
6
6
  "agents",
@@ -28,15 +28,16 @@
28
28
  "scripts/emit-agent-shell-tools.mjs",
29
29
  "scripts/emit-gemini.mjs",
30
30
  "scripts/emit-maps.mjs",
31
+ "scripts/guarded-md.mjs",
31
32
  "scripts/guarded-roots.mjs",
32
33
  "scripts/emit-mcp-policy.mjs",
33
34
  "scripts/gate-fat-tools.mjs",
34
35
  "scripts/hooks-union.mjs",
35
36
  "scripts/pipeline.mjs",
36
37
  "scripts/grunt-job.mjs",
37
- "scripts/grunt-config.mjs",
38
38
  "scripts/parse-need.mjs",
39
39
  "scripts/persist-handoff.mjs",
40
+ "scripts/persist-implementation.mjs",
40
41
  "scripts/persist-tmp.mjs",
41
42
  "scripts/persist-plan.mjs",
42
43
  "scripts/purge-global-mcps.mjs",
@@ -44,6 +45,12 @@
44
45
  "scripts/scrub-text-lib.mjs",
45
46
  "scripts/sync-global-settings.mjs",
46
47
  "scripts/browser.mjs",
48
+ "scripts/speak.mjs",
49
+ "scripts/listen.mjs",
50
+ "scripts/google-workspace.mjs",
51
+ "scripts/interactive.mjs",
52
+ "scripts/prompt.mjs",
53
+ "scripts/setup.mjs",
47
54
  "scripts/doctor.mjs",
48
55
  "scripts/skill-conflicts.mjs",
49
56
  "scripts/scrub-text",
@@ -73,6 +80,7 @@
73
80
  "hooks:union": "node scripts/hooks-union.mjs",
74
81
  "hooks:check": "node scripts/hooks-union.mjs --check",
75
82
  "doctor": "node ./scripts/doctor.mjs",
83
+ "setup": "node ./scripts/setup.mjs",
76
84
  "rulesync:doctor": "rulesync doctor",
77
85
  "sync:globals": "node scripts/sync-global-settings.mjs",
78
86
  "sync:globals:check": "node scripts/check-globals.mjs",