@lovrozagar/grunt 0.4.2 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.agents/agents/grunt.md +2 -1
  2. package/.agents/agents/implementer.md +1 -1
  3. package/.agents/agents/orchestrator.md +19 -13
  4. package/.agents/agents/thinker.md +8 -6
  5. package/.agents/skills/ask/SKILL.md +18 -0
  6. package/.agents/skills/auto/SKILL.md +18 -0
  7. package/.agents/skills/browser/SKILL.md +4 -3
  8. package/.agents/skills/cascade/SKILL.md +5 -3
  9. package/.agents/skills/commit-and-push/SKILL.md +3 -3
  10. package/.agents/skills/explain/SKILL.md +1 -0
  11. package/.agents/skills/handoff/SKILL.md +1 -1
  12. package/.agents/skills/implement-plan/SKILL.md +117 -0
  13. package/.agents/skills/solo/SKILL.md +5 -3
  14. package/.agents/skills/tmp/SKILL.md +58 -0
  15. package/.agents/skills/write-plan/SKILL.md +85 -0
  16. package/.claude/agents/grunt.md +2 -1
  17. package/.claude/agents/implementer.md +1 -1
  18. package/.claude/agents/orchestrator.md +19 -13
  19. package/.claude/agents/thinker.md +8 -6
  20. package/.claude/skills/ask/SKILL.md +18 -0
  21. package/.claude/skills/auto/SKILL.md +18 -0
  22. package/.claude/skills/browser/SKILL.md +4 -3
  23. package/.claude/skills/cascade/SKILL.md +5 -3
  24. package/.claude/skills/commit-and-push/SKILL.md +3 -3
  25. package/.claude/skills/explain/SKILL.md +1 -0
  26. package/.claude/skills/handoff/SKILL.md +1 -1
  27. package/.claude/skills/implement-plan/SKILL.md +117 -0
  28. package/.claude/skills/solo/SKILL.md +5 -3
  29. package/.claude/skills/tmp/SKILL.md +58 -0
  30. package/.claude/skills/write-plan/SKILL.md +85 -0
  31. package/.codex/agents/grunt.toml +2 -1
  32. package/.codex/agents/implementer.toml +1 -1
  33. package/.codex/agents/orchestrator.toml +19 -13
  34. package/.codex/agents/thinker.toml +8 -6
  35. package/.grok/agents/grunt.md +2 -1
  36. package/.grok/agents/implementer.md +1 -1
  37. package/.grok/agents/orchestrator.md +19 -13
  38. package/.grok/agents/thinker.md +8 -6
  39. package/.grok/hooks/orchestrate-parent.js +280 -62
  40. package/.grok/skills/ask/SKILL.md +18 -0
  41. package/.grok/skills/auto/SKILL.md +18 -0
  42. package/.grok/skills/browser/SKILL.md +4 -3
  43. package/.grok/skills/cascade/SKILL.md +5 -3
  44. package/.grok/skills/commit-and-push/SKILL.md +3 -3
  45. package/.grok/skills/explain/SKILL.md +1 -0
  46. package/.grok/skills/handoff/SKILL.md +1 -1
  47. package/.grok/skills/implement-plan/SKILL.md +49 -20
  48. package/.grok/skills/solo/SKILL.md +5 -3
  49. package/.grok/skills/tmp/SKILL.md +58 -0
  50. package/.grok/skills/write-plan/SKILL.md +17 -14
  51. package/.rulesync/grunt.config.jsonc +7 -0
  52. package/.rulesync/reference/INDEX.md +46 -0
  53. package/.rulesync/reference/cascade.md +19 -15
  54. package/.rulesync/reference/hooks.md +6 -4
  55. package/.rulesync/reference/law.md +9 -0
  56. package/.rulesync/reference/map.md +20 -10
  57. package/.rulesync/reference/output.md +23 -8
  58. package/{.grok/skills/shared → .rulesync/reference}/plan-format.md +12 -11
  59. package/.rulesync/reference/refs-map.md +14 -0
  60. package/.rulesync/reference/skills-map.md +23 -0
  61. package/.rulesync/rules/CLAUDE.md +22 -15
  62. package/.rulesync/rules/overview.md +22 -15
  63. package/.rulesync/skills/ask/SKILL.md +18 -0
  64. package/.rulesync/skills/auto/SKILL.md +18 -0
  65. package/.rulesync/skills/browser/SKILL.md +2 -2
  66. package/.rulesync/skills/cascade/SKILL.md +5 -3
  67. package/.rulesync/skills/commit-and-push/SKILL.md +3 -3
  68. package/.rulesync/skills/explain/SKILL.md +1 -0
  69. package/.rulesync/skills/handoff/SKILL.md +1 -1
  70. package/.rulesync/skills/implement-plan/SKILL.md +127 -0
  71. package/.rulesync/skills/solo/SKILL.md +5 -3
  72. package/.rulesync/skills/tmp/SKILL.md +58 -0
  73. package/.rulesync/skills/write-plan/SKILL.md +92 -0
  74. package/.rulesync/subagents/grunt.md +2 -1
  75. package/.rulesync/subagents/implementer.md +1 -1
  76. package/.rulesync/subagents/orchestrator.md +19 -13
  77. package/.rulesync/subagents/thinker.md +8 -6
  78. package/AGENTS.md +22 -15
  79. package/CHANGELOG.md +56 -0
  80. package/CLAUDE.md +22 -15
  81. package/README.md +27 -22
  82. package/cli/grunt.mjs +12 -12
  83. package/cli/grunt.test.ts +29 -20
  84. package/cli/init.mjs +135 -22
  85. package/cli/init.test.ts +624 -36
  86. package/package.json +11 -4
  87. package/scripts/doctor.mjs +64 -2
  88. package/scripts/emit-maps.mjs +422 -0
  89. package/scripts/gate-fat-tools.mjs +2 -2
  90. package/scripts/grunt-config.mjs +50 -0
  91. package/scripts/guarded-roots.mjs +4 -7
  92. package/scripts/persist-plan.mjs +17 -2
  93. package/scripts/persist-tmp.mjs +123 -0
  94. package/scripts/pipeline.mjs +67 -0
  95. package/scripts/scrub-spawn-prompt.mjs +1 -1
  96. package/scripts/skill-conflicts.mjs +71 -0
@@ -1,10 +1,24 @@
1
1
  #!/usr/bin/env node
2
- /** Allocate/slugify/validate/write a local plan under .tmp/plans/. */
2
+ /** Allocate/slugify/validate/write a local plan under .tmp/grunt/plans/. */
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
 
7
- export const PLAN_DIR = ".tmp/plans";
7
+ export const PLAN_DIR = ".tmp/grunt/plans";
8
+ /** One-shot rename source; drop next release. */
9
+ export const LEGACY_PLAN_DIR = ".tmp/plans";
10
+
11
+ /** If `.tmp/plans` exists and `.tmp/grunt/plans` is missing, rename. Both exist: leave old. */
12
+ export function migrateLegacyPlansDir(workspaceRoot) {
13
+ const ws = workspaceRoot || process.cwd();
14
+ const legacy = path.join(ws, LEGACY_PLAN_DIR);
15
+ const next = path.join(ws, PLAN_DIR);
16
+ if (fs.existsSync(legacy) && !fs.existsSync(next)) {
17
+ fs.mkdirSync(path.dirname(next), { recursive: true });
18
+ fs.renameSync(legacy, next);
19
+ }
20
+ return next;
21
+ }
8
22
  /** Old unstamped names and new `{serial}-{slug}-{YYYYMMDDTHHMMSSZ}.md`. */
9
23
  export const FILENAME_RE =
10
24
  /^[0-9]+-[a-z0-9]+(-[a-z0-9]+)*(-\d{8}T\d{6}Z)?\.md$/;
@@ -237,6 +251,7 @@ export function persistPlan({
237
251
  source ||
238
252
  (raw.match(/^\s*source:\s*(.+)$/m) ? undefined : planName);
239
253
  const body = stripPlanName(raw);
254
+ migrateLegacyPlansDir(ws);
240
255
  const plansDir = path.join(ws, PLAN_DIR);
241
256
  fs.mkdirSync(plansDir, { recursive: true });
242
257
  const createdAt =
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /** Allocate/slugify/write a one-off convo artifact under .tmp/grunt/. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import {
7
+ DATETIME_RE,
8
+ compactCreatedStamp,
9
+ ensureGitignore,
10
+ slugify,
11
+ utcDateTime,
12
+ } from "./persist-plan.mjs";
13
+
14
+ export const TMP_DIR = ".tmp/grunt";
15
+ /** `{serial}-{slug}-{YYYYMMDDTHHMMSSZ}.{ext}`; stamp required. */
16
+ export const FILENAME_RE =
17
+ /^[1-9][0-9]*-[a-z0-9]+(-[a-z0-9]+)*-\d{8}T\d{6}Z\.[a-z0-9]+$/;
18
+ export const FILENAME_STAMP_RE = /-\d{8}T\d{6}Z$/;
19
+ const META_LINE_RE = /^\s*TMP_(NAME|EXT):\s*.+\s*$/;
20
+ const NAME_RE = /^\s*TMP_NAME:\s*(.+?)\s*$/m;
21
+ const EXT_RE = /^\s*TMP_EXT:\s*(.+?)\s*$/m;
22
+
23
+ export function extractTmpName(content) {
24
+ const m = String(content).match(NAME_RE);
25
+ return m ? m[1].trim() : "";
26
+ }
27
+
28
+ export function extractTmpExt(content) {
29
+ const m = String(content).match(EXT_RE);
30
+ return m ? m[1].trim() : "";
31
+ }
32
+
33
+ export function stripTmpMeta(content) {
34
+ return String(content)
35
+ .replace(/^\uFEFF/, "")
36
+ .split(/\r?\n/)
37
+ .filter((line) => !META_LINE_RE.test(line))
38
+ .join("\n");
39
+ }
40
+
41
+ export function normalizeTmpExt(raw) {
42
+ let e = String(raw || "")
43
+ .trim()
44
+ .toLowerCase();
45
+ if (e.startsWith(".")) e = e.slice(1);
46
+ if (!/^[a-z0-9]+$/.test(e) || e.length > 16) return "";
47
+ return e;
48
+ }
49
+
50
+ function nextTmpSerial(dir) {
51
+ let next = 1;
52
+ if (!fs.existsSync(dir)) return next;
53
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
54
+ if (!ent.isFile()) continue;
55
+ const m = ent.name.match(/^([0-9]+)-/);
56
+ if (!m) continue;
57
+ const n = parseInt(m[1], 10);
58
+ if (Number.isFinite(n) && n >= next) next = n + 1;
59
+ }
60
+ return next;
61
+ }
62
+
63
+ export function persistTmp({ workspaceRoot, content, created } = {}) {
64
+ const ws = workspaceRoot || process.cwd();
65
+ const raw = String(content || "");
66
+ if (raw.includes("\0")) return { ok: false, error: "binary" };
67
+ const name = extractTmpName(raw);
68
+ if (!name) return { ok: false, error: "missing TMP_NAME" };
69
+ const extRaw = extractTmpExt(raw);
70
+ const ext = extRaw ? normalizeTmpExt(extRaw) : "md";
71
+ if (!ext) return { ok: false, error: "ext" };
72
+ const slug = slugify(name);
73
+ const body = stripTmpMeta(raw);
74
+ const dir = path.join(ws, TMP_DIR);
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ const createdAt = created && DATETIME_RE.test(created) ? created : utcDateTime();
77
+ const stamp = compactCreatedStamp(createdAt);
78
+ let serial = nextTmpSerial(dir);
79
+ let filename = `${serial}-${slug}-${stamp}.${ext}`;
80
+ let dest = path.join(dir, filename);
81
+ let tries = 0;
82
+ while (fs.existsSync(dest) && tries < 5) {
83
+ serial += 1;
84
+ filename = `${serial}-${slug}-${stamp}.${ext}`;
85
+ dest = path.join(dir, filename);
86
+ tries += 1;
87
+ }
88
+ if (fs.existsSync(dest)) return { ok: false, error: "serial collision" };
89
+ if (!FILENAME_RE.test(filename)) return { ok: false, error: "filename" };
90
+ ensureGitignore(ws);
91
+ fs.writeFileSync(dest, body);
92
+ return { ok: true, path: dest, serial, slug, ext, content: body, filename };
93
+ }
94
+
95
+ function main() {
96
+ try {
97
+ const wsIdx = process.argv.indexOf("--workspace");
98
+ const workspaceRoot = wsIdx >= 0 ? process.argv[wsIdx + 1] : process.cwd();
99
+ const content = fs.readFileSync(0, "utf8");
100
+ const result = persistTmp({ workspaceRoot, content });
101
+ if (!result.ok) {
102
+ process.stderr.write((result.error || "invalid tmp") + "\n");
103
+ return 1;
104
+ }
105
+ process.stdout.write(
106
+ JSON.stringify({
107
+ serial: result.serial,
108
+ path: result.path,
109
+ filename: result.filename,
110
+ }) + "\n",
111
+ );
112
+ return 0;
113
+ } catch (err) {
114
+ process.stderr.write(String(err && err.message ? err.message : err) + "\n");
115
+ return 1;
116
+ }
117
+ }
118
+
119
+ const thisFile = fileURLToPath(import.meta.url);
120
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
121
+ if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
122
+ process.exit(main());
123
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /** Inner generate/check/watch chain. Called by guarded-roots; not a public npm script. */
3
+ import { execSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const COMMANDS = {
8
+ generate: [
9
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills",
10
+ "rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks",
11
+ "node scripts/emit-mcp-policy.mjs",
12
+ "node scripts/emit-gemini.mjs",
13
+ "node scripts/emit-agent-shell-tools.mjs",
14
+ "node scripts/emit-maps.mjs",
15
+ "node scripts/hooks-union.mjs",
16
+ ],
17
+ check: [
18
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,skills --check",
19
+ "rulesync generate -t codexcli,antigravity-cli,grokcli -f subagents --check",
20
+ "rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks --check",
21
+ "node scripts/emit-mcp-policy.mjs --check",
22
+ "node scripts/emit-gemini.mjs --check",
23
+ "node scripts/emit-agent-shell-tools.mjs --check",
24
+ "node scripts/emit-maps.mjs --check",
25
+ "node scripts/check-globals.mjs",
26
+ "node scripts/hooks-union.mjs --check",
27
+ ],
28
+ watch: [
29
+ "node scripts/emit-mcp-policy.mjs",
30
+ "node scripts/emit-gemini.mjs",
31
+ "node scripts/emit-agent-shell-tools.mjs",
32
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
33
+ ],
34
+ };
35
+
36
+ export function envWithLocalBin(cwd, env = process.env) {
37
+ const binDir = path.join(cwd, "node_modules", ".bin");
38
+ const merged = { ...env };
39
+ const key = Object.keys(merged).find((k) => k.toLowerCase() === "path") || "PATH";
40
+ merged[key] = `${binDir}${path.delimiter}${merged[key] || ""}`;
41
+ return merged;
42
+ }
43
+
44
+ export function runPipeline(mode, { cwd = process.cwd(), exec = execSync } = {}) {
45
+ const cmds = COMMANDS[mode];
46
+ if (!cmds) {
47
+ throw new Error("usage: pipeline.mjs generate|check|watch");
48
+ }
49
+ const env = envWithLocalBin(cwd);
50
+ for (const cmd of cmds) {
51
+ exec(cmd, { cwd, stdio: "inherit", shell: true, env });
52
+ }
53
+ }
54
+
55
+ function main() {
56
+ try {
57
+ runPipeline(process.argv[2] || "");
58
+ return 0;
59
+ } catch (err) {
60
+ process.stderr.write((err && err.message ? err.message : String(err)) + "\n");
61
+ return 1;
62
+ }
63
+ }
64
+
65
+ const thisFile = fileURLToPath(import.meta.url);
66
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
67
+ if (invoked === thisFile) process.exit(main());
@@ -90,7 +90,7 @@ export function workspaceRootOf(data) {
90
90
  export function spawnCapReason(workspaceRoot) {
91
91
  const root = workspaceRoot || process.cwd() || "";
92
92
  return (
93
- `spawn prompt exceeds ${MAX_PROMPT_CHARS} chars after scrub; write it under ${root}/.tmp/plans/ and re-spawn with that abs path`
93
+ `spawn prompt exceeds ${MAX_PROMPT_CHARS} chars after scrub; write it under ${root}/.tmp/grunt/plans/ and re-spawn with that abs path`
94
94
  );
95
95
  }
96
96
 
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /** Workspace vs packaged grunt skill content conflicts (warn-only). */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ export const WORKSPACE_SKILLS_REL = ".rulesync/skills";
7
+ export const PACKAGED_SKILLS_REL = path.join(
8
+ "node_modules",
9
+ "@lovrozagar",
10
+ "grunt",
11
+ ".rulesync",
12
+ "skills",
13
+ );
14
+
15
+ export function listSkillDirNames(skillsRoot) {
16
+ let entries;
17
+ try {
18
+ entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
19
+ } catch (err) {
20
+ if (err && err.code === "ENOENT") return [];
21
+ throw err;
22
+ }
23
+ const out = [];
24
+ for (const ent of entries) {
25
+ if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
26
+ const skillMd = path.join(skillsRoot, ent.name, "SKILL.md");
27
+ try {
28
+ if (!fs.statSync(skillMd).isFile()) continue;
29
+ } catch {
30
+ continue;
31
+ }
32
+ out.push(ent.name);
33
+ }
34
+ out.sort((a, b) => a.localeCompare(b));
35
+ return out;
36
+ }
37
+
38
+ function readSkillMd(skillsRoot, name) {
39
+ const abs = path.join(skillsRoot, name, "SKILL.md");
40
+ try {
41
+ return fs.readFileSync(abs);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** Names in both trees whose SKILL.md bytes differ. */
48
+ export function findSkillContentConflicts({ workspaceSkillsDir, packagedSkillsDir } = {}) {
49
+ if (!workspaceSkillsDir || !packagedSkillsDir) return [];
50
+ const packaged = listSkillDirNames(packagedSkillsDir);
51
+ if (!packaged.length) return [];
52
+ const conflicts = [];
53
+ for (const name of packaged) {
54
+ const ws = readSkillMd(workspaceSkillsDir, name);
55
+ if (ws == null) continue;
56
+ const pkg = readSkillMd(packagedSkillsDir, name);
57
+ if (pkg == null) continue;
58
+ if (Buffer.compare(ws, pkg) !== 0) {
59
+ conflicts.push({
60
+ name,
61
+ workspacePath: path.join(workspaceSkillsDir, name, "SKILL.md"),
62
+ packagedPath: path.join(packagedSkillsDir, name, "SKILL.md"),
63
+ });
64
+ }
65
+ }
66
+ return conflicts;
67
+ }
68
+
69
+ export function formatSkillConflictWarn(name) {
70
+ return `skill \`${name}\` differs from packaged grunt; re-init overwrites`;
71
+ }