@lovrozagar/grunt 0.4.1 → 0.5.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 (52) hide show
  1. package/.agents/agents/orchestrator.md +8 -7
  2. package/.agents/agents/thinker.md +6 -5
  3. package/.agents/skills/commit-and-push/SKILL.md +3 -3
  4. package/.agents/skills/explain/SKILL.md +1 -0
  5. package/.agents/skills/implement-plan/SKILL.md +116 -0
  6. package/.agents/skills/write-plan/SKILL.md +82 -0
  7. package/.claude/agents/orchestrator.md +8 -7
  8. package/.claude/agents/thinker.md +6 -5
  9. package/.claude/skills/commit-and-push/SKILL.md +3 -3
  10. package/.claude/skills/explain/SKILL.md +1 -0
  11. package/.claude/skills/implement-plan/SKILL.md +116 -0
  12. package/.claude/skills/write-plan/SKILL.md +82 -0
  13. package/.codex/agents/orchestrator.toml +8 -7
  14. package/.codex/agents/thinker.toml +6 -5
  15. package/.grok/agents/orchestrator.md +8 -7
  16. package/.grok/agents/thinker.md +6 -5
  17. package/.grok/hooks/orchestrate-parent.js +3 -100
  18. package/.grok/skills/commit-and-push/SKILL.md +3 -3
  19. package/.grok/skills/explain/SKILL.md +1 -0
  20. package/.grok/skills/implement-plan/SKILL.md +45 -17
  21. package/.grok/skills/write-plan/SKILL.md +11 -11
  22. package/.rulesync/reference/INDEX.md +43 -0
  23. package/.rulesync/reference/cascade.md +11 -9
  24. package/.rulesync/reference/hooks.md +4 -4
  25. package/.rulesync/reference/law.md +9 -0
  26. package/.rulesync/reference/map.md +12 -8
  27. package/.rulesync/reference/output.md +13 -8
  28. package/{.grok/skills/shared → .rulesync/reference}/plan-format.md +2 -1
  29. package/.rulesync/reference/refs-map.md +14 -0
  30. package/.rulesync/reference/skills-map.md +20 -0
  31. package/.rulesync/rules/CLAUDE.md +13 -11
  32. package/.rulesync/rules/overview.md +13 -11
  33. package/.rulesync/skills/commit-and-push/SKILL.md +3 -3
  34. package/.rulesync/skills/explain/SKILL.md +1 -0
  35. package/.rulesync/skills/implement-plan/SKILL.md +124 -0
  36. package/.rulesync/skills/write-plan/SKILL.md +88 -0
  37. package/.rulesync/subagents/orchestrator.md +8 -7
  38. package/.rulesync/subagents/thinker.md +6 -5
  39. package/AGENTS.md +13 -11
  40. package/CHANGELOG.md +210 -0
  41. package/CLAUDE.md +13 -11
  42. package/README.md +37 -16
  43. package/cli/init.mjs +21 -4
  44. package/cli/init.test.ts +97 -26
  45. package/package.json +6 -6
  46. package/scripts/doctor.mjs +64 -2
  47. package/scripts/emit-maps.mjs +422 -0
  48. package/scripts/grunt-job.mjs +0 -9
  49. package/scripts/guarded-roots.mjs +4 -7
  50. package/scripts/pipeline.mjs +67 -0
  51. package/scripts/skill-conflicts.mjs +71 -0
  52. package/scripts/telemetry.mjs +0 -38
package/cli/init.test.ts CHANGED
@@ -44,10 +44,12 @@ const PRODUCT_FILES = [
44
44
  "check-globals.mjs",
45
45
  "emit-agent-shell-tools.mjs",
46
46
  "emit-gemini.mjs",
47
+ "emit-maps.mjs",
47
48
  "guarded-roots.mjs",
48
49
  "emit-mcp-policy.mjs",
49
50
  "gate-fat-tools.mjs",
50
51
  "hooks-union.mjs",
52
+ "pipeline.mjs",
51
53
  "grunt-job.mjs",
52
54
  "parse-need.mjs",
53
55
  "persist-handoff.mjs",
@@ -56,9 +58,9 @@ const PRODUCT_FILES = [
56
58
  "scrub-spawn-prompt.mjs",
57
59
  "scrub-text-lib.mjs",
58
60
  "sync-global-settings.mjs",
59
- "telemetry.mjs",
60
61
  "browser.mjs",
61
62
  "doctor.mjs",
63
+ "skill-conflicts.mjs",
62
64
  ];
63
65
 
64
66
  const SRC_CLAUDE_SETTINGS = {
@@ -496,8 +498,15 @@ describe("snapshot/remerge guarded roots", () => {
496
498
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "fresh ssot\n");
497
499
  });
498
500
  runGuardedRoots("generate", { cwd: dest, exec });
499
- expect(exec).toHaveBeenCalledTimes(1);
500
- expect(String(exec.mock.calls[0][0])).toContain("rulesync:generate:raw");
501
+ expect(exec.mock.calls.map((c) => c[0])).toEqual([
502
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills",
503
+ "rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks",
504
+ "node scripts/emit-mcp-policy.mjs",
505
+ "node scripts/emit-gemini.mjs",
506
+ "node scripts/emit-agent-shell-tools.mjs",
507
+ "node scripts/emit-maps.mjs",
508
+ "node scripts/hooks-union.mjs",
509
+ ]);
501
510
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
502
511
  "<!-- grunt:begin -->\nfresh ssot\n<!-- grunt:end -->\nkeep me\n",
503
512
  );
@@ -596,8 +605,12 @@ describe("snapshot/remerge guarded roots", () => {
596
605
  return stop;
597
606
  },
598
607
  });
599
- expect(exec).toHaveBeenCalledTimes(1);
600
- expect(String(exec.mock.calls[0][0])).toContain("rulesync:watch:raw");
608
+ expect(exec.mock.calls.map((c) => c[0])).toEqual([
609
+ "node scripts/emit-mcp-policy.mjs",
610
+ "node scripts/emit-gemini.mjs",
611
+ "node scripts/emit-agent-shell-tools.mjs",
612
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
613
+ ]);
601
614
  expect(stop).toHaveBeenCalledTimes(1);
602
615
  expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
603
616
  "<!-- grunt:begin -->\nwatch2\n<!-- grunt:end -->\nkeep me\n",
@@ -953,6 +966,45 @@ describe("mergePackageJson", () => {
953
966
  warnSpy.mockRestore();
954
967
  });
955
968
 
969
+ it("does not merge :raw scripts from src", () => {
970
+ const pkgRoot = stubPkgRoot({
971
+ name: "fixture-pkg",
972
+ scripts: {
973
+ "rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
974
+ "rulesync:generate:raw": "echo raw",
975
+ "rulesync:check:raw": "echo check-raw",
976
+ "rulesync:watch:raw": "echo watch-raw",
977
+ },
978
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
979
+ });
980
+ const dest = tmp("pj-raw-src-");
981
+ mergePackageJson(dest, pkgRoot);
982
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
983
+ expect(out.scripts["rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
984
+ expect(out.scripts["rulesync:generate:raw"]).toBeUndefined();
985
+ expect(out.scripts["rulesync:check:raw"]).toBeUndefined();
986
+ expect(out.scripts["rulesync:watch:raw"]).toBeUndefined();
987
+ });
988
+
989
+ it("leaves dest-only :raw keys", () => {
990
+ const pkgRoot = stubPkgRoot({
991
+ name: "fixture-pkg",
992
+ scripts: { "rulesync:generate": "node ./scripts/guarded-roots.mjs generate" },
993
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
994
+ });
995
+ const dest = tmp("pj-raw-dest-");
996
+ fs.writeFileSync(
997
+ path.join(dest, "package.json"),
998
+ JSON.stringify({
999
+ scripts: { "rulesync:generate:raw": "echo consumer-raw" },
1000
+ }),
1001
+ );
1002
+ mergePackageJson(dest, pkgRoot);
1003
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
1004
+ expect(out.scripts["rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
1005
+ expect(out.scripts["rulesync:generate:raw"]).toBe("echo consumer-raw");
1006
+ });
1007
+
956
1008
  it("replaces a grunt-owned script with no suffix", () => {
957
1009
  const pkgRoot = stubPkgRoot({
958
1010
  name: "fixture-pkg",
@@ -1003,8 +1055,8 @@ describe("init", () => {
1003
1055
  expect(pkg.name).toBe("fixture-pkg");
1004
1056
  expect(fs.existsSync(path.join(pkgRoot, ".tmp"))).toBe(true);
1005
1057
  expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
1006
- expect(fs.readFileSync(path.join(pkgRoot, "scripts", "telemetry.mjs"), "utf8")).toBe(
1007
- "telemetry.mjs",
1058
+ expect(fs.readFileSync(path.join(pkgRoot, "scripts", "grunt-job.mjs"), "utf8")).toBe(
1059
+ "grunt-job.mjs",
1008
1060
  );
1009
1061
  // self-skip: markdown/settings stay exactly as-is, no sentinel wrap
1010
1062
  expect(fs.readFileSync(path.join(pkgRoot, "CLAUDE.md"), "utf8")).toBe("CLAUDE.md content");
@@ -1124,11 +1176,13 @@ describe("init", () => {
1124
1176
  ]);
1125
1177
  });
1126
1178
 
1127
- it("applyGlobals true still applies when telemetry auto-skip would fire", () => {
1179
+ it("applyGlobals true still applies when sentinel auto-skip would fire", () => {
1128
1180
  const pkgRoot = stubPkgRoot();
1129
1181
  const dest = tmp("grunt-force-globals-");
1130
- fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1131
- fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
1182
+ fs.writeFileSync(
1183
+ path.join(dest, "AGENTS.md"),
1184
+ "<!-- grunt:begin -->\nold\n<!-- grunt:end -->\n",
1185
+ );
1132
1186
  const exec = vi.fn();
1133
1187
  init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
1134
1188
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
@@ -1139,20 +1193,6 @@ describe("init", () => {
1139
1193
  ]);
1140
1194
  });
1141
1195
 
1142
- it("auto-skip globals when telemetry.mjs exists", () => {
1143
- const pkgRoot = stubPkgRoot();
1144
- const dest = tmp("grunt-auto-tel-");
1145
- fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1146
- fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
1147
- const exec = vi.fn();
1148
- init(dest, { pkgRoot, execFileSync: exec });
1149
- expect(exec.mock.calls.map((c) => c[1])).toEqual([
1150
- ["install"],
1151
- ["run", "rulesync:generate"],
1152
- ["run", "rulesync:check"],
1153
- ]);
1154
- });
1155
-
1156
1196
  it("auto-skip globals when AGENTS.md already has a grunt sentinel", () => {
1157
1197
  const pkgRoot = stubPkgRoot();
1158
1198
  const dest = tmp("grunt-auto-sent-");
@@ -1174,8 +1214,7 @@ describe("init", () => {
1174
1214
  const dest = tmp("grunt-rerun-nosent-");
1175
1215
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
1176
1216
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "hand-written consumer doc\n");
1177
- fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1178
- fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
1217
+ fs.mkdirSync(path.join(dest, ".rulesync"), { recursive: true });
1179
1218
  fs.writeFileSync(path.join(dest, "CLAUDE.grunt.md"), "stale side");
1180
1219
  const exec = vi.fn();
1181
1220
  init(dest, { pkgRoot, execFileSync: exec });
@@ -1276,6 +1315,30 @@ describe("init", () => {
1276
1315
  ["merge", "stop"],
1277
1316
  ]);
1278
1317
  });
1318
+
1319
+ it("warns when dest skill differs from packaged then force-overwrites; keeps extras", () => {
1320
+ const pkgRoot = stubPkgRoot();
1321
+ const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "parent");
1322
+ fs.mkdirSync(pkgSkill, { recursive: true });
1323
+ fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-parent\n");
1324
+ const dest = tmp("grunt-sk-conflict-");
1325
+ const destSkill = path.join(dest, ".rulesync", "skills", "parent");
1326
+ const destExtra = path.join(dest, ".rulesync", "skills", "my-extra");
1327
+ fs.mkdirSync(destSkill, { recursive: true });
1328
+ fs.mkdirSync(destExtra, { recursive: true });
1329
+ fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-parent\n");
1330
+ fs.writeFileSync(path.join(destExtra, "SKILL.md"), "keep-me\n");
1331
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1332
+ init(dest, { pkgRoot, execFileSync: vi.fn(), skipGlobals: true });
1333
+ expect(
1334
+ warnSpy.mock.calls.some(
1335
+ (c) => String(c[0]).includes("parent") && String(c[0]).includes("re-init overwrites"),
1336
+ ),
1337
+ ).toBe(true);
1338
+ warnSpy.mockRestore();
1339
+ expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-parent\n");
1340
+ expect(fs.readFileSync(path.join(destExtra, "SKILL.md"), "utf8")).toBe("keep-me\n");
1341
+ });
1279
1342
  });
1280
1343
 
1281
1344
  describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
@@ -1292,6 +1355,14 @@ describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
1292
1355
  expect(shouldAutoSkipGlobals(dest)).toBe(false);
1293
1356
  });
1294
1357
 
1358
+ it(".grok/hooks/orchestrate-parent.js means inited, not auto-skip", () => {
1359
+ const dest = tmp("inspect-hook-");
1360
+ fs.mkdirSync(path.join(dest, ".grok", "hooks"), { recursive: true });
1361
+ fs.writeFileSync(path.join(dest, ".grok", "hooks", "orchestrate-parent.js"), "");
1362
+ expect(destAlreadyInited(dest)).toBe(true);
1363
+ expect(shouldAutoSkipGlobals(dest)).toBe(false);
1364
+ });
1365
+
1295
1366
  it("sentinel auto-skips globals", () => {
1296
1367
  const dest = tmp("inspect-sent-");
1297
1368
  fs.writeFileSync(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrozagar/grunt",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",
@@ -27,10 +27,12 @@
27
27
  "scripts/check-globals.mjs",
28
28
  "scripts/emit-agent-shell-tools.mjs",
29
29
  "scripts/emit-gemini.mjs",
30
+ "scripts/emit-maps.mjs",
30
31
  "scripts/guarded-roots.mjs",
31
32
  "scripts/emit-mcp-policy.mjs",
32
33
  "scripts/gate-fat-tools.mjs",
33
34
  "scripts/hooks-union.mjs",
35
+ "scripts/pipeline.mjs",
34
36
  "scripts/grunt-job.mjs",
35
37
  "scripts/parse-need.mjs",
36
38
  "scripts/persist-handoff.mjs",
@@ -39,9 +41,9 @@
39
41
  "scripts/scrub-spawn-prompt.mjs",
40
42
  "scripts/scrub-text-lib.mjs",
41
43
  "scripts/sync-global-settings.mjs",
42
- "scripts/telemetry.mjs",
43
44
  "scripts/browser.mjs",
44
45
  "scripts/doctor.mjs",
46
+ "scripts/skill-conflicts.mjs",
45
47
  "scripts/scrub-text",
46
48
  ".rulesync",
47
49
  ".grok",
@@ -52,7 +54,8 @@
52
54
  "CLAUDE.md",
53
55
  ".mcp.json",
54
56
  "README.md",
55
- "LICENSE"
57
+ "LICENSE",
58
+ "CHANGELOG.md"
56
59
  ],
57
60
  "type": "module",
58
61
  "publishConfig": {
@@ -63,11 +66,8 @@
63
66
  },
64
67
  "scripts": {
65
68
  "rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
66
- "rulesync:generate:raw": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks && node scripts/emit-mcp-policy.mjs && node scripts/emit-gemini.mjs && node scripts/emit-agent-shell-tools.mjs && node scripts/hooks-union.mjs",
67
69
  "rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
68
- "rulesync:watch:raw": "node scripts/emit-mcp-policy.mjs && node scripts/emit-gemini.mjs && node scripts/emit-agent-shell-tools.mjs && rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
69
70
  "rulesync:check": "node ./scripts/guarded-roots.mjs check",
70
- "rulesync:check:raw": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,skills --check && rulesync generate -t codexcli,antigravity-cli,grokcli -f subagents --check && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks --check && node scripts/emit-mcp-policy.mjs --check && node scripts/emit-gemini.mjs --check && node scripts/emit-agent-shell-tools.mjs --check && node scripts/check-globals.mjs && node scripts/hooks-union.mjs --check",
71
71
  "hooks:union": "node scripts/hooks-union.mjs",
72
72
  "hooks:check": "node scripts/hooks-union.mjs --check",
73
73
  "doctor": "node ./scripts/doctor.mjs",
@@ -4,6 +4,12 @@ import { execFileSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import {
8
+ PACKAGED_SKILLS_REL,
9
+ WORKSPACE_SKILLS_REL,
10
+ findSkillContentConflicts,
11
+ formatSkillConflictWarn,
12
+ } from "./skill-conflicts.mjs";
7
13
 
8
14
  export const CHROMIUM_BINS = [
9
15
  "chromium",
@@ -23,6 +29,12 @@ const RTK_DOCS = "https://www.rtk-ai.app/docs/getting-started/installation/";
23
29
  const LP_INSTALL_SH = "curl -fsSL https://pkg.lightpanda.io/install.sh | bash";
24
30
  const RULESYNC_SCHEMA = "rulesync schema doctor: npm run rulesync:doctor";
25
31
  const REQUIRED = ["node", "npm", "git", "rtk", "rulesync", "lightpanda", "chromium"];
32
+ export const REQUIRED_MAP_FILES = [
33
+ { name: "INDEX.md", rel: ".rulesync/reference/INDEX.md" },
34
+ { name: "skills-map.md", rel: ".rulesync/reference/skills-map.md" },
35
+ { name: "refs-map.md", rel: ".rulesync/reference/refs-map.md" },
36
+ { name: "law.md", rel: ".rulesync/reference/law.md" },
37
+ ];
26
38
 
27
39
  export function nodeMajor(version) {
28
40
  const m = String(version || "").match(/(\d+)/);
@@ -158,12 +170,35 @@ export function installHints(id, platform = process.platform) {
158
170
  }
159
171
 
160
172
  function row(name, status, extra = "") {
161
- const a = String(name).padEnd(10);
173
+ const a = String(name).padEnd(14);
162
174
  const b = String(status).padEnd(18);
163
175
  const c = extra ? String(extra).trim() : "";
164
176
  return c ? `${a} ${b} ${c}` : `${a} ${b}`;
165
177
  }
166
178
 
179
+ function mapFilePresent(cwd, rel) {
180
+ try {
181
+ return fs.statSync(path.join(cwd, rel)).isFile();
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ function isDir(abs) {
188
+ try {
189
+ return fs.statSync(abs).isDirectory();
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+
195
+ /** Product SoT: reference tree or skills (emit-maps in play). No .rulesync / no those trees → bins-only. */
196
+ export function mapsRequired(cwd) {
197
+ const rs = path.join(cwd || "", ".rulesync");
198
+ if (!isDir(rs)) return false;
199
+ return isDir(path.join(rs, "reference")) || isDir(path.join(rs, "skills"));
200
+ }
201
+
167
202
  export function runDoctor({
168
203
  cwd = process.cwd(),
169
204
  pathEnv = process.env.PATH,
@@ -232,10 +267,37 @@ export function runDoctor({
232
267
  }
233
268
  }
234
269
 
270
+ const missingMaps = [];
271
+ if (mapsRequired(cwd)) {
272
+ for (const f of REQUIRED_MAP_FILES) {
273
+ if (mapFilePresent(cwd, f.rel)) {
274
+ lines.push(row(f.name, "ok", f.rel));
275
+ } else {
276
+ missingMaps.push(f);
277
+ lines.push(row(f.name, "missing", f.rel));
278
+ }
279
+ }
280
+ if (missingMaps.length) {
281
+ lines.push(`maps missing: ${missingMaps.map((f) => f.name).join(", ")}`);
282
+ }
283
+ }
284
+
285
+ const skillConflicts = findSkillContentConflicts({
286
+ workspaceSkillsDir: path.join(cwd || "", WORKSPACE_SKILLS_REL),
287
+ packagedSkillsDir: path.join(cwd || "", PACKAGED_SKILLS_REL),
288
+ });
289
+ if (skillConflicts.length) {
290
+ lines.push("");
291
+ lines.push("skill conflicts (warn; re-init overwrites grunt-owned names):");
292
+ for (const c of skillConflicts) {
293
+ lines.push(formatSkillConflictWarn(c.name));
294
+ }
295
+ }
296
+
235
297
  lines.push("");
236
298
  lines.push(RULESYNC_SCHEMA);
237
299
  const stdout = `${lines.join("\n")}\n`;
238
- return { code: missingRequired.length ? 1 : 0, stdout, stderr: "" };
300
+ return { code: missingRequired.length || missingMaps.length ? 1 : 0, stdout, stderr: "" };
239
301
  }
240
302
 
241
303
  function main() {