@lovrozagar/grunt 0.5.2 → 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 -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 +39 -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 +56 -0
  105. package/CLAUDE.md +1 -49
  106. package/README.md +88 -80
  107. package/cli/grunt.mjs +18 -2
  108. package/cli/grunt.test.ts +33 -0
  109. package/cli/init.mjs +103 -10
  110. package/cli/init.test.ts +147 -35
  111. package/cli/prompt.mjs +8 -0
  112. package/cli/prompt.test.ts +14 -0
  113. package/package.json +7 -2
  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/scrub-spawn-prompt.mjs +3 -50
  125. package/scripts/setup.mjs +746 -0
  126. package/scripts/speak.mjs +291 -0
  127. package/.agents/agents/implementer.md +0 -32
  128. package/.agents/agents/thinker.md +0 -29
  129. package/.agents/skills/cascade/SKILL.md +0 -28
  130. package/.agents/skills/parent/SKILL.md +0 -6
  131. package/.agents/skills/solo/SKILL.md +0 -25
  132. package/.claude/agents/implementer.md +0 -37
  133. package/.claude/agents/thinker.md +0 -36
  134. package/.claude/skills/cascade/SKILL.md +0 -28
  135. package/.claude/skills/parent/SKILL.md +0 -6
  136. package/.claude/skills/solo/SKILL.md +0 -25
  137. package/.codex/agents/implementer.toml +0 -26
  138. package/.codex/agents/thinker.toml +0 -26
  139. package/.grok/agents/implementer.md +0 -27
  140. package/.grok/agents/thinker.md +0 -27
  141. package/.grok/parent.md +0 -1
  142. package/.grok/roles/implementer.toml +0 -5
  143. package/.grok/roles/thinker.toml +0 -5
  144. package/.grok/skills/cascade/SKILL.md +0 -28
  145. package/.grok/skills/parent/SKILL.md +0 -6
  146. package/.grok/skills/solo/SKILL.md +0 -25
  147. package/.rulesync/grunt.config.jsonc +0 -7
  148. package/.rulesync/skills/cascade/SKILL.md +0 -28
  149. package/.rulesync/skills/parent/SKILL.md +0 -6
  150. package/.rulesync/skills/solo/SKILL.md +0 -25
  151. package/.rulesync/subagents/implementer.md +0 -49
  152. package/.rulesync/subagents/thinker.md +0 -49
  153. package/scripts/grunt-config.mjs +0 -50
package/cli/init.test.ts CHANGED
@@ -15,6 +15,12 @@ import {
15
15
  guardedMarkdownDrift,
16
16
  healGuardedRootFile,
17
17
  init,
18
+ pruneRetired,
19
+ RETIRED_SKILLS,
20
+ RETIRED_AGENTS,
21
+ RETIRED_SCRIPTS,
22
+ RETIRED_PATHS,
23
+ RESERVED_SKILLS,
18
24
  LAUNCH_SCRIPTS,
19
25
  mergeClaudeSettings,
20
26
  mergeGitignore,
@@ -30,7 +36,6 @@ import {
30
36
  writeMergedGuardedFile,
31
37
  } from "./init.mjs";
32
38
  import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
33
- import { stripJsonc } from "../scripts/grunt-config.mjs";
34
39
 
35
40
  const tmpDirs: string[] = [];
36
41
  afterEach(() => {
@@ -46,12 +51,6 @@ function tmp(prefix: string) {
46
51
  }
47
52
 
48
53
  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
54
  const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"];
56
55
  const PRODUCT_FILES = [
57
56
  "check-globals.mjs",
@@ -64,9 +63,9 @@ const PRODUCT_FILES = [
64
63
  "hooks-union.mjs",
65
64
  "pipeline.mjs",
66
65
  "grunt-job.mjs",
67
- "grunt-config.mjs",
68
66
  "parse-need.mjs",
69
67
  "persist-handoff.mjs",
68
+ "persist-implementation.mjs",
70
69
  "persist-tmp.mjs",
71
70
  "persist-plan.mjs",
72
71
  "purge-global-mcps.mjs",
@@ -74,6 +73,10 @@ const PRODUCT_FILES = [
74
73
  "scrub-text-lib.mjs",
75
74
  "sync-global-settings.mjs",
76
75
  "browser.mjs",
76
+ "speak.mjs",
77
+ "listen.mjs",
78
+ "google-workspace.mjs",
79
+ "setup.mjs",
77
80
  "doctor.mjs",
78
81
  "skill-conflicts.mjs",
79
82
  ];
@@ -118,12 +121,6 @@ const SRC_CLAUDE_SETTINGS = {
118
121
  enabledMcpjsonServers: [],
119
122
  };
120
123
 
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
124
  function stubPkgRoot(pkg: Record<string, unknown> = {
128
125
  name: "fixture-pkg",
129
126
  scripts: {
@@ -143,10 +140,6 @@ function stubPkgRoot(pkg: Record<string, unknown> = {
143
140
  fs.mkdirSync(path.join(root, d));
144
141
  fs.writeFileSync(path.join(root, d, "marker"), d);
145
142
  }
146
- fs.writeFileSync(
147
- path.join(root, GRUNT_CONFIG_REL),
148
- `${JSON.stringify(GRUNT_CONFIG_DEFAULTS, null, 2)}\n`,
149
- );
150
143
  fs.writeFileSync(
151
144
  path.join(root, ".claude", "settings.json"),
152
145
  `${JSON.stringify(SRC_CLAUDE_SETTINGS, null, 2)}\n`,
@@ -179,18 +172,22 @@ describe("samePath", () => {
179
172
  });
180
173
  });
181
174
 
175
+ const GI_TMP = ".tmp/\n";
176
+
182
177
  describe("mergeGitignore", () => {
183
178
  it("creates .tmp/ when missing", () => {
184
179
  const dest = tmp("gi-miss-");
185
180
  mergeGitignore(dest);
186
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
181
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(GI_TMP);
187
182
  });
188
183
 
189
- it("noop when .tmp/ present", () => {
184
+ it("noop when .tmp/ already present", () => {
190
185
  const dest = tmp("gi-slash-");
191
186
  fs.writeFileSync(path.join(dest, ".gitignore"), "foo\n.tmp/\nbar\n");
192
187
  mergeGitignore(dest);
193
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe("foo\n.tmp/\nbar\n");
188
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
189
+ "foo\n.tmp/\nbar\n",
190
+ );
194
191
  });
195
192
 
196
193
  it("noop when .tmp present", () => {
@@ -200,12 +197,21 @@ describe("mergeGitignore", () => {
200
197
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp\n");
201
198
  });
202
199
 
200
+ it("noop when .tmp/ present in mixed file", () => {
201
+ const dest = tmp("gi-both-");
202
+ fs.writeFileSync(path.join(dest, ".gitignore"), `foo\n${GI_TMP}bar\n`);
203
+ mergeGitignore(dest);
204
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
205
+ `foo\n${GI_TMP}bar\n`,
206
+ );
207
+ });
208
+
203
209
  it("appends after trailing newline", () => {
204
210
  const dest = tmp("gi-nl-");
205
211
  fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/\n");
206
212
  mergeGitignore(dest);
207
213
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
208
- "node_modules/\n.tmp/\n",
214
+ `node_modules/\n${GI_TMP}`,
209
215
  );
210
216
  });
211
217
 
@@ -214,7 +220,7 @@ describe("mergeGitignore", () => {
214
220
  fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/");
215
221
  mergeGitignore(dest);
216
222
  expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
217
- "node_modules/\n.tmp/\n",
223
+ `node_modules/\n${GI_TMP}`,
218
224
  );
219
225
  });
220
226
 
@@ -222,7 +228,7 @@ describe("mergeGitignore", () => {
222
228
  const dest = tmp("gi-empty-");
223
229
  fs.writeFileSync(path.join(dest, ".gitignore"), "");
224
230
  mergeGitignore(dest);
225
- expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
231
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(GI_TMP);
226
232
  });
227
233
  });
228
234
 
@@ -1550,9 +1556,9 @@ describe("init", () => {
1550
1556
  if (d === ".claude") continue;
1551
1557
  expect(fs.readFileSync(path.join(dest, d, "marker"), "utf8")).toBe(d);
1552
1558
  }
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);
1559
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1560
+ expect(PRODUCT_FILES).not.toContain("grunt-config.mjs");
1561
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1556
1562
  expect(fs.readFileSync(path.join(dest, ".claude", "marker"), "utf8")).toBe(".claude");
1557
1563
  const settings = JSON.parse(
1558
1564
  fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
@@ -1626,6 +1632,109 @@ describe("init", () => {
1626
1632
  expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
1627
1633
  });
1628
1634
 
1635
+ it("prunes retired 0.5 skills and agents on dest", () => {
1636
+ const pkgRoot = stubPkgRoot();
1637
+ const dest = tmp("grunt-prune-");
1638
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "solo"), { recursive: true });
1639
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "solo", "SKILL.md"), "old");
1640
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "parent"), { recursive: true });
1641
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "cascade"), { recursive: true });
1642
+ fs.mkdirSync(path.join(dest, ".claude", "agents"), { recursive: true });
1643
+ fs.writeFileSync(path.join(dest, ".claude", "agents", "implementer.md"), "old");
1644
+ fs.writeFileSync(path.join(dest, ".claude", "agents", "thinker.md"), "old");
1645
+ expect(RETIRED_SKILLS).toEqual(["parent", "solo", "cascade"]);
1646
+ expect(RETIRED_AGENTS).toEqual(["implementer", "thinker"]);
1647
+ expect(RETIRED_SCRIPTS).toEqual(["telemetry.mjs", "grunt-config.mjs"]);
1648
+ expect(RETIRED_PATHS).toEqual([
1649
+ ".grok/parent.md",
1650
+ ".grok/skills/shared",
1651
+ ".rulesync/grunt.config.jsonc",
1652
+ ".rulesync/grunt.config.local.jsonc",
1653
+ ".rulesync/grunt.config.local.jsonc.example",
1654
+ ]);
1655
+ expect(RESERVED_SKILLS).toEqual([
1656
+ "ask",
1657
+ "auto",
1658
+ "browser",
1659
+ "clasp",
1660
+ "commit",
1661
+ "commit-and-push",
1662
+ "commit-push",
1663
+ "commit-push-deploy",
1664
+ "commit-push-release",
1665
+ "explain",
1666
+ "google-workspace",
1667
+ "handoff",
1668
+ "implement-plan",
1669
+ "listen",
1670
+ "pickup",
1671
+ "speak",
1672
+ "tmp",
1673
+ "write-plan",
1674
+ ]);
1675
+ expect(RESERVED_SKILLS).not.toEqual(expect.arrayContaining(RETIRED_SKILLS));
1676
+ init(dest, { pkgRoot, execFileSync: vi.fn() });
1677
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "solo"))).toBe(false);
1678
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "parent"))).toBe(false);
1679
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "cascade"))).toBe(false);
1680
+ expect(fs.existsSync(path.join(dest, ".claude", "agents", "implementer.md"))).toBe(
1681
+ false,
1682
+ );
1683
+ expect(fs.existsSync(path.join(dest, ".claude", "agents", "thinker.md"))).toBe(false);
1684
+ pruneRetired(dest, { pkgRoot });
1685
+ });
1686
+
1687
+ it("prunes retired scripts/paths, reserved-not-packaged skills, and grok roles; keeps consumer extras", () => {
1688
+ const pkgRoot = stubPkgRoot();
1689
+ fs.mkdirSync(path.join(pkgRoot, ".rulesync", "skills", "tmp"), { recursive: true });
1690
+ fs.writeFileSync(path.join(pkgRoot, ".rulesync", "skills", "tmp", "SKILL.md"), "pkg tmp");
1691
+ const dest = tmp("grunt-prune-extra-");
1692
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "tmp"), { recursive: true });
1693
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "tmp", "SKILL.md"), "old tmp");
1694
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "browser"), { recursive: true });
1695
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "browser", "SKILL.md"), "stale reserved");
1696
+ fs.mkdirSync(path.join(dest, ".rulesync", "skills", "acme"), { recursive: true });
1697
+ fs.writeFileSync(path.join(dest, ".rulesync", "skills", "acme", "SKILL.md"), "consumer");
1698
+ fs.mkdirSync(path.join(dest, ".grok", "roles"), { recursive: true });
1699
+ fs.writeFileSync(path.join(dest, ".grok", "roles", "implementer.toml"), "old");
1700
+ fs.writeFileSync(path.join(dest, ".grok", "roles", "thinker.toml"), "old");
1701
+ fs.writeFileSync(path.join(dest, ".grok", "parent.md"), "pointer");
1702
+ fs.mkdirSync(path.join(dest, ".grok", "skills", "shared"), { recursive: true });
1703
+ fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1704
+ fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "old telemetry");
1705
+ fs.writeFileSync(path.join(dest, "scripts", "grunt-config.mjs"), "old config");
1706
+ fs.writeFileSync(path.join(dest, "scripts", "mine.mjs"), "keep");
1707
+ fs.writeFileSync(path.join(dest, ".rulesync", "grunt.config.jsonc"), '{"version":1}\n');
1708
+ fs.writeFileSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc"), '{"sessionGate":"ask"}\n');
1709
+ fs.writeFileSync(
1710
+ path.join(dest, ".rulesync", "grunt.config.local.jsonc.example"),
1711
+ '{"sessionGate":"ask"}\n',
1712
+ );
1713
+ fs.mkdirSync(path.join(dest, ".gemini", "agents", "implementer"), { recursive: true });
1714
+ fs.writeFileSync(path.join(dest, ".gemini", "agents", "implementer", "agent.md"), "old");
1715
+ init(dest, { pkgRoot, execFileSync: vi.fn() });
1716
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "tmp", "SKILL.md"), "utf8")).toBe(
1717
+ "pkg tmp",
1718
+ );
1719
+ expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "browser"))).toBe(false);
1720
+ expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "acme", "SKILL.md"), "utf8")).toBe(
1721
+ "consumer",
1722
+ );
1723
+ expect(fs.existsSync(path.join(dest, ".grok", "roles", "implementer.toml"))).toBe(false);
1724
+ expect(fs.existsSync(path.join(dest, ".grok", "roles", "thinker.toml"))).toBe(false);
1725
+ expect(fs.existsSync(path.join(dest, ".grok", "parent.md"))).toBe(false);
1726
+ expect(fs.existsSync(path.join(dest, ".grok", "skills", "shared"))).toBe(false);
1727
+ expect(fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))).toBe(false);
1728
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1729
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1730
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc"))).toBe(false);
1731
+ expect(
1732
+ fs.existsSync(path.join(dest, ".rulesync", "grunt.config.local.jsonc.example")),
1733
+ ).toBe(false);
1734
+ expect(fs.readFileSync(path.join(dest, "scripts", "mine.mjs"), "utf8")).toBe("keep");
1735
+ expect(fs.existsSync(path.join(dest, ".gemini", "agents", "implementer"))).toBe(false);
1736
+ });
1737
+
1629
1738
  it("omitted pkgRoot uses built-in PKG_ROOT; exec mocked", () => {
1630
1739
  const dest = tmp("grunt-realroot-");
1631
1740
  const exec = vi.fn();
@@ -1638,9 +1747,12 @@ describe("init", () => {
1638
1747
  expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
1639
1748
  expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
1640
1749
  expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
1641
- expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(true);
1750
+ expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
1751
+ expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(true);
1752
+ expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(true);
1753
+ expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(true);
1642
1754
  expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
1643
- expect(readGruntConfig(dest)).toEqual(GRUNT_CONFIG_DEFAULTS);
1755
+ expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
1644
1756
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
1645
1757
  ["install"],
1646
1758
  ["run", "grunt:rulesync:generate"],
@@ -1829,25 +1941,25 @@ describe("init", () => {
1829
1941
 
1830
1942
  it("warns when dest skill differs from packaged then force-overwrites; keeps extras", () => {
1831
1943
  const pkgRoot = stubPkgRoot();
1832
- const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "parent");
1944
+ const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "explain");
1833
1945
  fs.mkdirSync(pkgSkill, { recursive: true });
1834
- fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-parent\n");
1946
+ fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-explain\n");
1835
1947
  const dest = tmp("grunt-sk-conflict-");
1836
- const destSkill = path.join(dest, ".rulesync", "skills", "parent");
1948
+ const destSkill = path.join(dest, ".rulesync", "skills", "explain");
1837
1949
  const destExtra = path.join(dest, ".rulesync", "skills", "my-extra");
1838
1950
  fs.mkdirSync(destSkill, { recursive: true });
1839
1951
  fs.mkdirSync(destExtra, { recursive: true });
1840
- fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-parent\n");
1952
+ fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-explain\n");
1841
1953
  fs.writeFileSync(path.join(destExtra, "SKILL.md"), "keep-me\n");
1842
1954
  const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1843
1955
  init(dest, { pkgRoot, execFileSync: vi.fn(), skipGlobals: true });
1844
1956
  expect(
1845
1957
  warnSpy.mock.calls.some(
1846
- (c) => String(c[0]).includes("parent") && String(c[0]).includes("re-init overwrites"),
1958
+ (c) => String(c[0]).includes("explain") && String(c[0]).includes("re-init overwrites"),
1847
1959
  ),
1848
1960
  ).toBe(true);
1849
1961
  warnSpy.mockRestore();
1850
- expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-parent\n");
1962
+ expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-explain\n");
1851
1963
  expect(fs.readFileSync(path.join(destExtra, "SKILL.md"), "utf8")).toBe("keep-me\n");
1852
1964
  });
1853
1965
  });
package/cli/prompt.mjs CHANGED
@@ -31,6 +31,14 @@ export async function confirm(opts) {
31
31
  return bailIfCancel(await clack.confirm(opts))
32
32
  }
33
33
 
34
+ export async function text(opts) {
35
+ return bailIfCancel(await clack.text(opts))
36
+ }
37
+
38
+ export async function password(opts) {
39
+ return bailIfCancel(await clack.password(opts))
40
+ }
41
+
34
42
  export function spinner() {
35
43
  return clack.spinner()
36
44
  }
@@ -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.0",
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",
@@ -34,9 +34,9 @@
34
34
  "scripts/hooks-union.mjs",
35
35
  "scripts/pipeline.mjs",
36
36
  "scripts/grunt-job.mjs",
37
- "scripts/grunt-config.mjs",
38
37
  "scripts/parse-need.mjs",
39
38
  "scripts/persist-handoff.mjs",
39
+ "scripts/persist-implementation.mjs",
40
40
  "scripts/persist-tmp.mjs",
41
41
  "scripts/persist-plan.mjs",
42
42
  "scripts/purge-global-mcps.mjs",
@@ -44,6 +44,10 @@
44
44
  "scripts/scrub-text-lib.mjs",
45
45
  "scripts/sync-global-settings.mjs",
46
46
  "scripts/browser.mjs",
47
+ "scripts/speak.mjs",
48
+ "scripts/listen.mjs",
49
+ "scripts/google-workspace.mjs",
50
+ "scripts/setup.mjs",
47
51
  "scripts/doctor.mjs",
48
52
  "scripts/skill-conflicts.mjs",
49
53
  "scripts/scrub-text",
@@ -73,6 +77,7 @@
73
77
  "hooks:union": "node scripts/hooks-union.mjs",
74
78
  "hooks:check": "node scripts/hooks-union.mjs --check",
75
79
  "doctor": "node ./scripts/doctor.mjs",
80
+ "setup": "node ./scripts/setup.mjs",
76
81
  "rulesync:doctor": "rulesync doctor",
77
82
  "sync:globals": "node scripts/sync-global-settings.mjs",
78
83
  "sync:globals:check": "node scripts/check-globals.mjs",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /** Session browser rail: nav|snap|click|fill|shot|pdf|stop|doctor|ensure. Lightpanda default. */
2
+ /** Session browser rail: nav|snap|click|fill|scroll|wait|hover|select|shot|pdf|stop|doctor|ensure. Lightpanda default. */
3
3
  import { spawn } from "node:child_process";
4
4
  import crypto from "node:crypto";
5
5
  import fs from "node:fs";
@@ -7,6 +7,7 @@ import http from "node:http";
7
7
  import net from "node:net";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { writeStash } from "./grunt-job.mjs";
10
11
  import {
11
12
  CHROMIUM_BINS,
12
13
  LIGHTPANDA_REPO as DOCTOR_LP_REPO,
@@ -64,9 +65,21 @@ export function isPaintHost(url) {
64
65
  if ((h === "google.com" || h.endsWith(".google.com")) && u.pathname.startsWith("/earth")) {
65
66
  return true;
66
67
  }
68
+ if (/(^|\.)amazon\./.test(h)) return true;
67
69
  return false;
68
70
  }
69
71
 
72
+ const BLOCKED_SNAP_RE =
73
+ /enable javascript|javascript (is )?(required|disabled)|checking your browser|just a moment|captcha|access denied|sorry.*robot|continue shopping|attention required|cf-browser-verification|verify you are a human/i;
74
+
75
+ export function isBlockedSnap(markdown, lines) {
76
+ const refs = Array.isArray(lines) ? lines.length : 0;
77
+ const md = String(markdown || "").trim();
78
+ if (BLOCKED_SNAP_RE.test(md)) return true;
79
+ const body = md.replace(/^#\s+\S+\s*$/m, "").trim();
80
+ return refs === 0 && body.length < 80;
81
+ }
82
+
70
83
  export function lookupBins(pathEnv, platform = process.platform) {
71
84
  const lightpanda = whichBin("lightpanda", pathEnv, platform);
72
85
  let chromium = "";
@@ -348,8 +361,10 @@ async function handleCdp(state, method, params = {}) {
348
361
  state.markdown = `# ${state.url}\n\nhello`;
349
362
  return { frameId: "f1" };
350
363
  case "LP.getMarkdown":
364
+ if (state.thinSnap) return { markdown: `# ${state.url || ""}\n` };
351
365
  return { markdown: state.markdown || `# ${state.url || ""}\n\nhello` };
352
366
  case "Accessibility.getFullAXTree":
367
+ if (state.thinSnap) return { nodes: [] };
353
368
  return { nodes: state.axNodes || defaultAx(state.url) };
354
369
  case "DOM.resolveNode": {
355
370
  const id = params.backendNodeId;
@@ -378,6 +393,7 @@ export async function serveCdp(state = {}, { port = 0 } = {}) {
378
393
  url: "about:blank",
379
394
  markdown: "",
380
395
  failProbe: false,
396
+ thinSnap: false,
381
397
  engine: "lightpanda",
382
398
  ...state,
383
399
  };
@@ -606,7 +622,7 @@ export async function runBrowser(argv, opts = {}) {
606
622
  const verb = String(argv[0] || "").toLowerCase();
607
623
  try {
608
624
  if (!verb || verb === "help" || verb === "-h" || verb === "--help") {
609
- return ok("nav|snap|click|fill|shot|pdf|stop|doctor|ensure");
625
+ return ok("nav|snap|click|fill|scroll|wait|hover|select|shot|pdf|stop|doctor|ensure");
610
626
  }
611
627
  if (verb === "doctor" || verb === "ensure") {
612
628
  return runDoctor({ pathEnv: env.PATH, platform, cwd });
@@ -650,16 +666,79 @@ export async function runBrowser(argv, opts = {}) {
650
666
  return ok(`engine: ${session.engine}\nurl: ${session.lastURL}`);
651
667
  }
652
668
  if (verb === "snap") {
653
- const session = readSession(cwd);
669
+ let session = readSession(cwd);
654
670
  if (!session || !alive(session.pid)) return fail("no browser session; run nav");
655
- const { markdown, lines } = await snapshot(session);
671
+ let { markdown, lines } = await snapshot(session);
672
+ if (
673
+ session.engine === "lightpanda" &&
674
+ (session.swapCount || 0) < 1 &&
675
+ bins.chromium &&
676
+ isBlockedSnap(markdown, lines)
677
+ ) {
678
+ session = await swapToChromium(session, ctx);
679
+ ({ markdown, lines } = await snapshot(session));
680
+ }
656
681
  writeSession(cwd, session);
682
+ const raw = [markdown.trim(), "", ...lines].join("\n");
683
+ const headings = String(markdown || "")
684
+ .split(/\n/)
685
+ .filter((l) => /^#{1,6}\s/.test(l));
686
+ const shown = [...headings.slice(0, 6), ...lines.slice(0, 24)];
687
+ if (raw.length > 32 * 1024 || lines.length > 6) {
688
+ const stash = writeStash(cwd, "snap", raw);
689
+ return ok(
690
+ `engine: ${session.engine}\nurl: ${session.lastURL}\n${shown.length} shown. stash=${stash}\n${shown.join("\n")}`,
691
+ );
692
+ }
657
693
  const body = [markdown.trim(), "", ...lines].filter((x, i, a) => x !== "" || a[i - 1] !== "").join("\n");
658
694
  return ok(`engine: ${session.engine}\nurl: ${session.lastURL}\n\n${body}`);
659
695
  }
660
- if (verb === "click" || verb === "fill") {
696
+ if (verb === "wait") {
661
697
  const session = readSession(cwd);
662
698
  if (!session || !alive(session.pid)) return fail("no browser session; run nav");
699
+ const raw = String(argv[1] || "1000");
700
+ const ms = Math.min(15000, Math.max(0, Number(raw) || 0));
701
+ await sleep(ms);
702
+ return ok(`wait: ${ms}`);
703
+ }
704
+ if (verb === "scroll") {
705
+ let session = readSession(cwd);
706
+ if (!session || !alive(session.pid)) return fail("no browser session; run nav");
707
+ const arg = String(argv[1] || "down");
708
+ const refs = session.lastRefs || {};
709
+ const rec = refs[arg];
710
+ const act = async (sess) =>
711
+ withCdp(sess, async (cdp) => {
712
+ if (rec) {
713
+ const resolved = await cdp.send("DOM.resolveNode", { backendNodeId: rec.backendNodeId });
714
+ const objectId = resolved.object && resolved.object.objectId;
715
+ if (!objectId) throw new Error("stale");
716
+ await cdp.send("Runtime.callFunctionOn", {
717
+ objectId,
718
+ functionDeclaration: "function() { this.scrollIntoView({ block: 'center' }); }",
719
+ });
720
+ return;
721
+ }
722
+ let dy = 600;
723
+ if (arg === "up") dy = -600;
724
+ else if (/^-?\d+$/.test(arg)) dy = Number(arg);
725
+ await cdp.send("Runtime.evaluate", {
726
+ expression: `window.scrollBy(0, ${dy})`,
727
+ });
728
+ });
729
+ try {
730
+ await act(session);
731
+ } catch (err) {
732
+ const msg = String(err && err.message ? err.message : err);
733
+ if (/stale|missing/i.test(msg)) return fail(`stale ref: ${arg}`);
734
+ return fail(msg);
735
+ }
736
+ writeSession(cwd, session);
737
+ return ok(`scroll: ${arg}`);
738
+ }
739
+ if (verb === "click" || verb === "fill" || verb === "hover" || verb === "select") {
740
+ let session = readSession(cwd);
741
+ if (!session || !alive(session.pid)) return fail("no browser session; run nav");
663
742
  const refs = session.lastRefs || {};
664
743
  if (!Object.keys(refs).length) return fail("no snap refs; run snap");
665
744
  const refId = String(argv[1] || "");
@@ -667,6 +746,7 @@ export async function runBrowser(argv, opts = {}) {
667
746
  if (!rec) return fail(`missing ref: ${refId}`);
668
747
  const text = argv.slice(2).join(" ");
669
748
  if (verb === "fill" && !text) return fail("fill <ref> <text>");
749
+ if (verb === "select" && !text) return fail("select <ref> <value>");
670
750
  const act = async (sess) =>
671
751
  withCdp(sess, async (cdp) => {
672
752
  const resolved = await cdp.send("DOM.resolveNode", { backendNodeId: rec.backendNodeId });
@@ -677,6 +757,19 @@ export async function runBrowser(argv, opts = {}) {
677
757
  objectId,
678
758
  functionDeclaration: "function() { this.click(); }",
679
759
  });
760
+ } else if (verb === "hover") {
761
+ await cdp.send("Runtime.callFunctionOn", {
762
+ objectId,
763
+ functionDeclaration:
764
+ "function() { this.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); }",
765
+ });
766
+ } else if (verb === "select") {
767
+ await cdp.send("Runtime.callFunctionOn", {
768
+ objectId,
769
+ arguments: [{ value: text }],
770
+ functionDeclaration:
771
+ "function(v) { this.value = v; this.dispatchEvent(new Event('change', { bubbles: true })); }",
772
+ });
680
773
  } else {
681
774
  await cdp.send("Runtime.callFunctionOn", {
682
775
  objectId,