@lovrozagar/grunt 0.3.11 → 0.4.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 (61) hide show
  1. package/.agents/agents/grunt.md +3 -2
  2. package/.agents/agents/implementer.md +4 -2
  3. package/.agents/agents/orchestrator.md +19 -12
  4. package/.agents/agents/thinker.md +7 -2
  5. package/.agents/skills/browser/SKILL.md +17 -0
  6. package/.agents/skills/explain/SKILL.md +2 -0
  7. package/.agents/skills/handoff/SKILL.md +3 -3
  8. package/.agents/skills/pickup/SKILL.md +64 -0
  9. package/.agents/skills/solo/SKILL.md +2 -2
  10. package/.claude/agents/grunt.md +3 -2
  11. package/.claude/agents/implementer.md +6 -2
  12. package/.claude/agents/orchestrator.md +19 -12
  13. package/.claude/agents/thinker.md +9 -2
  14. package/.claude/skills/browser/SKILL.md +17 -0
  15. package/.claude/skills/explain/SKILL.md +2 -0
  16. package/.claude/skills/handoff/SKILL.md +3 -3
  17. package/.claude/skills/pickup/SKILL.md +64 -0
  18. package/.claude/skills/solo/SKILL.md +2 -2
  19. package/.codex/agents/grunt.toml +3 -2
  20. package/.codex/agents/implementer.toml +4 -2
  21. package/.codex/agents/orchestrator.toml +19 -12
  22. package/.codex/agents/thinker.toml +7 -2
  23. package/.grok/agents/grunt.md +3 -2
  24. package/.grok/agents/implementer.md +4 -2
  25. package/.grok/agents/orchestrator.md +19 -12
  26. package/.grok/agents/thinker.md +7 -2
  27. package/.grok/hooks/orchestrate-parent.js +26 -10
  28. package/.grok/skills/browser/SKILL.md +17 -0
  29. package/.grok/skills/explain/SKILL.md +2 -0
  30. package/.grok/skills/handoff/SKILL.md +3 -3
  31. package/.grok/skills/pickup/SKILL.md +64 -0
  32. package/.grok/skills/solo/SKILL.md +2 -2
  33. package/.rulesync/reference/browser.md +59 -0
  34. package/.rulesync/reference/cascade.md +13 -6
  35. package/.rulesync/reference/hooks.md +2 -2
  36. package/.rulesync/reference/map.md +6 -3
  37. package/.rulesync/reference/output.md +38 -1
  38. package/.rulesync/reference/rtk.md +4 -1
  39. package/.rulesync/rules/CLAUDE.md +18 -11
  40. package/.rulesync/rules/overview.md +18 -11
  41. package/.rulesync/skills/browser/SKILL.md +19 -0
  42. package/.rulesync/skills/explain/SKILL.md +2 -0
  43. package/.rulesync/skills/handoff/SKILL.md +3 -3
  44. package/.rulesync/skills/pickup/SKILL.md +64 -0
  45. package/.rulesync/skills/solo/SKILL.md +2 -2
  46. package/.rulesync/subagents/grunt.md +3 -2
  47. package/.rulesync/subagents/implementer.md +4 -2
  48. package/.rulesync/subagents/orchestrator.md +19 -12
  49. package/.rulesync/subagents/thinker.md +7 -2
  50. package/AGENTS.md +20 -11
  51. package/CLAUDE.md +20 -11
  52. package/README.md +40 -10
  53. package/cli/grunt.mjs +2 -2
  54. package/cli/grunt.test.ts +3 -3
  55. package/cli/init.mjs +196 -26
  56. package/cli/init.test.ts +444 -21
  57. package/package.json +11 -4
  58. package/scripts/browser.mjs +764 -0
  59. package/scripts/doctor.mjs +252 -0
  60. package/scripts/emit-gemini.mjs +7 -1
  61. package/scripts/guarded-roots.mjs +83 -0
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /** Unified prereq doctor. Print-only install hints. Never runs installs. */
3
+ import { execFileSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ export const CHROMIUM_BINS = [
9
+ "chromium",
10
+ "chromium-browser",
11
+ "google-chrome",
12
+ "google-chrome-stable",
13
+ "chrome",
14
+ "msedge",
15
+ "microsoft-edge",
16
+ ];
17
+
18
+ export const LIGHTPANDA_REPO = "https://github.com/lightpanda-io/browser";
19
+ const NODE_URL = "https://nodejs.org";
20
+ const RTK_CURL =
21
+ "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh";
22
+ const RTK_DOCS = "https://www.rtk-ai.app/docs/getting-started/installation/";
23
+ const LP_INSTALL_SH = "curl -fsSL https://pkg.lightpanda.io/install.sh | bash";
24
+ const RULESYNC_SCHEMA = "rulesync schema doctor: npm run rulesync:doctor";
25
+ const REQUIRED = ["node", "npm", "git", "rtk", "rulesync", "lightpanda", "chromium"];
26
+
27
+ export function nodeMajor(version) {
28
+ const m = String(version || "").match(/(\d+)/);
29
+ return m ? Number(m[1]) : 0;
30
+ }
31
+
32
+ export function whichBin(name, pathEnv = process.env.PATH, platform = process.platform) {
33
+ if (!name) return "";
34
+ const delim =
35
+ platform === "win32" && String(pathEnv || "").includes(";") ? ";" : path.delimiter;
36
+ const dirs = String(pathEnv || "").split(delim);
37
+ const names = [name];
38
+ if (platform === "win32" && !path.extname(name)) {
39
+ const pathext = String(process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM");
40
+ for (const raw of pathext.split(";")) {
41
+ if (!raw) continue;
42
+ names.push(name + raw);
43
+ names.push(name + raw.toLowerCase());
44
+ }
45
+ names.push(`${name}.exe`);
46
+ }
47
+ const uniq = [...new Set(names)];
48
+ for (const dir of dirs) {
49
+ if (!dir) continue;
50
+ for (const n of uniq) {
51
+ const candidate = path.join(dir, n);
52
+ try {
53
+ fs.accessSync(candidate, fs.constants.X_OK);
54
+ return candidate;
55
+ } catch {
56
+ if (platform === "win32") {
57
+ try {
58
+ fs.accessSync(candidate, fs.constants.F_OK);
59
+ return candidate;
60
+ } catch {
61
+ /* try next */
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ return "";
68
+ }
69
+
70
+ function delimOf(pathEnv, platform) {
71
+ return platform === "win32" && String(pathEnv || "").includes(";") ? ";" : path.delimiter;
72
+ }
73
+
74
+ function withNmBin(cwd, pathEnv, platform) {
75
+ const nm = path.join(cwd || "", "node_modules", ".bin");
76
+ const d = delimOf(pathEnv, platform);
77
+ return `${nm}${d}${pathEnv || ""}`;
78
+ }
79
+
80
+ function readNodeVersion(bin) {
81
+ try {
82
+ return String(
83
+ execFileSync(bin, ["-v"], {
84
+ encoding: "utf8",
85
+ timeout: 8000,
86
+ stdio: ["ignore", "pipe", "pipe"],
87
+ }),
88
+ ).trim();
89
+ } catch {
90
+ return "";
91
+ }
92
+ }
93
+
94
+ function lookupChromium(pathEnv, platform) {
95
+ for (const n of CHROMIUM_BINS) {
96
+ const hit = whichBin(n, pathEnv, platform);
97
+ if (hit) return hit;
98
+ }
99
+ return "";
100
+ }
101
+
102
+ export function installHints(id, platform = process.platform) {
103
+ if (id === "all") {
104
+ const keys = ["node", "npm", "git", "rtk", "rulesync", "lightpanda", "chromium"];
105
+ return keys.flatMap((k) => installHints(k, platform));
106
+ }
107
+ if (id === "node") {
108
+ if (platform === "win32") {
109
+ return [`${NODE_URL} (≥22)`, "nvm / OS pkg", "winget install OpenJS.NodeJS.LTS"];
110
+ }
111
+ return [`${NODE_URL} (≥22)`, "nvm / OS pkg"];
112
+ }
113
+ if (id === "npm") {
114
+ return ["npm ships with Node ≥22", NODE_URL];
115
+ }
116
+ if (id === "git") {
117
+ if (platform === "win32") return ["winget install Git.Git"];
118
+ if (platform === "darwin") return ["brew install git"];
119
+ return ["sudo apt install git"];
120
+ }
121
+ if (id === "rtk") {
122
+ if (platform === "win32") {
123
+ return ["rtk.exe on PATH (release zip) or WSL curl", RTK_DOCS];
124
+ }
125
+ return [RTK_CURL, "brew install rtk"];
126
+ }
127
+ if (id === "rulesync") {
128
+ return ["npm i -D rulesync", "npx"];
129
+ }
130
+ if (id === "lightpanda") {
131
+ if (platform === "win32") {
132
+ return [
133
+ "Windows: native Lightpanda not required; WSL2 only",
134
+ "wsl --install",
135
+ `in WSL: ${LP_INSTALL_SH}`,
136
+ LIGHTPANDA_REPO,
137
+ ];
138
+ }
139
+ if (platform === "darwin") {
140
+ return [LP_INSTALL_SH, "brew tap lightpanda-io/browser", "brew install lightpanda-io/browser/lightpanda", LIGHTPANDA_REPO];
141
+ }
142
+ return [LP_INSTALL_SH, LIGHTPANDA_REPO];
143
+ }
144
+ if (id === "chromium") {
145
+ if (platform === "win32") {
146
+ return [
147
+ "winget install Google.Chrome",
148
+ "winget install Microsoft.Edge",
149
+ "add chrome.exe or msedge.exe to PATH or shim",
150
+ ];
151
+ }
152
+ if (platform === "darwin") {
153
+ return ["brew install --cask chromium", "brew install --cask google-chrome"];
154
+ }
155
+ return ["sudo apt install chromium"];
156
+ }
157
+ return [];
158
+ }
159
+
160
+ function row(name, status, extra = "") {
161
+ const a = String(name).padEnd(10);
162
+ const b = String(status).padEnd(18);
163
+ const c = extra ? String(extra).trim() : "";
164
+ return c ? `${a} ${b} ${c}` : `${a} ${b}`;
165
+ }
166
+
167
+ export function runDoctor({
168
+ cwd = process.cwd(),
169
+ pathEnv = process.env.PATH,
170
+ platform = process.platform,
171
+ execPath = process.execPath,
172
+ nodeVersion = process.version,
173
+ } = {}) {
174
+ const nmPath = withNmBin(cwd, pathEnv, platform);
175
+ const pathNode = whichBin("node", pathEnv, platform);
176
+ const nodeBin = pathNode || execPath || "";
177
+ let ver = "";
178
+ if (nodeBin) {
179
+ ver =
180
+ execPath && nodeBin === execPath
181
+ ? String(nodeVersion || process.version)
182
+ : readNodeVersion(nodeBin);
183
+ }
184
+ const major = nodeMajor(ver);
185
+ const nodeOk = Boolean(nodeBin) && major >= 22;
186
+
187
+ const npm = whichBin("npm", pathEnv, platform);
188
+ const git = whichBin("git", pathEnv, platform);
189
+ const rtk = whichBin("rtk", nmPath, platform);
190
+ const rulesync = whichBin("rulesync", nmPath, platform);
191
+ const lightpanda = whichBin("lightpanda", pathEnv, platform);
192
+ const chromium = lookupChromium(pathEnv, platform);
193
+ const gh = whichBin("gh", pathEnv, platform);
194
+
195
+ const found = {
196
+ node: nodeOk ? nodeBin : "",
197
+ npm,
198
+ git,
199
+ rtk,
200
+ rulesync,
201
+ lightpanda,
202
+ chromium,
203
+ };
204
+
205
+ const lines = [];
206
+ if (nodeBin && !nodeOk) {
207
+ lines.push(row("node", "need ≥22", `${ver} (${nodeBin})`));
208
+ } else if (nodeOk) {
209
+ lines.push(row("node", "ok", `${ver} (${nodeBin})`));
210
+ } else {
211
+ lines.push(row("node", "missing"));
212
+ }
213
+ lines.push(npm ? row("npm", "ok", npm) : row("npm", "missing"));
214
+ lines.push(git ? row("git", "ok", git) : row("git", "missing"));
215
+ lines.push(rtk ? row("rtk", "ok", rtk) : row("rtk", "missing"));
216
+ lines.push(rulesync ? row("rulesync", "ok", rulesync) : row("rulesync", "missing"));
217
+ lines.push(lightpanda ? row("lightpanda", "ok", lightpanda) : row("lightpanda", "missing"));
218
+ lines.push(chromium ? row("chromium", "ok", chromium) : row("chromium", "missing"));
219
+ lines.push(gh ? row("gh", "ok", gh) : row("gh", "missing (optional)"));
220
+
221
+ const missingRequired = REQUIRED.filter((k) => (k === "node" ? !nodeOk : !found[k]));
222
+ if (missingRequired.length) {
223
+ lines.push("");
224
+ lines.push("install (print-only; not run):");
225
+ const seen = new Set();
226
+ for (const k of missingRequired) {
227
+ for (const h of installHints(k, platform)) {
228
+ if (seen.has(h)) continue;
229
+ seen.add(h);
230
+ lines.push(h);
231
+ }
232
+ }
233
+ }
234
+
235
+ lines.push("");
236
+ lines.push(RULESYNC_SCHEMA);
237
+ const stdout = `${lines.join("\n")}\n`;
238
+ return { code: missingRequired.length ? 1 : 0, stdout, stderr: "" };
239
+ }
240
+
241
+ function main() {
242
+ const r = runDoctor();
243
+ if (r.stdout) process.stdout.write(r.stdout);
244
+ if (r.stderr) process.stderr.write(r.stderr);
245
+ process.exit(r.code);
246
+ }
247
+
248
+ const thisFile = fileURLToPath(import.meta.url);
249
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
250
+ if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
251
+ main();
252
+ }
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { guardedMarkdownDrift, writeMergedGuardedFile } from "../cli/init.mjs";
6
7
 
7
8
  export const AGENT_IDS = ["orchestrator", "implementer", "thinker", "grunt"];
8
9
  const SSOT_REL = ".rulesync/subagents";
@@ -173,6 +174,10 @@ export function emitGemini({ workspaceRoot, check = false } = {}) {
173
174
  if (check) {
174
175
  const drift = [];
175
176
  for (const f of files) {
177
+ if (f.rel === GEMINI_MD_REL) {
178
+ if (guardedMarkdownDrift(path.join(ws, f.rel), f.text)) drift.push(f.rel);
179
+ continue;
180
+ }
176
181
  const disk = readText(path.join(ws, f.rel));
177
182
  if (disk !== f.text) drift.push(f.rel);
178
183
  }
@@ -185,7 +190,8 @@ export function emitGemini({ workspaceRoot, check = false } = {}) {
185
190
  for (const f of files) {
186
191
  const abs = path.join(ws, f.rel);
187
192
  fs.mkdirSync(path.dirname(abs), { recursive: true });
188
- fs.writeFileSync(abs, f.text);
193
+ if (f.rel === GEMINI_MD_REL) writeMergedGuardedFile(abs, f.text);
194
+ else fs.writeFileSync(abs, f.text);
189
195
  }
190
196
  return { ok: true, check: false };
191
197
  }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /** Snapshot/remerge AGENTS.md CLAUDE.md GEMINI.md around generate/check/watch. */
3
+ import { execSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import path from "node:path";
7
+ import {
8
+ GUARDED_ROOT_FILES,
9
+ healGuardedRootFile,
10
+ remergeGuardedRoots,
11
+ snapshotGuardedRoots,
12
+ withGuardedCheckInteriors,
13
+ } from "../cli/init.mjs";
14
+
15
+ const GENERATE_RAW = "rulesync:generate:raw";
16
+ const CHECK_RAW = "rulesync:check:raw";
17
+ const WATCH_RAW = "rulesync:watch:raw";
18
+
19
+ export function attachGuardedRootWatchers(cwd, heal) {
20
+ const w = fs.watch(cwd, (_event, filename) => {
21
+ if (!filename) return;
22
+ const base = path.basename(String(filename));
23
+ if (!GUARDED_ROOT_FILES.includes(base)) return;
24
+ heal(base);
25
+ });
26
+ return () => {
27
+ try {
28
+ w.close();
29
+ } catch {
30
+ /* ignore */
31
+ }
32
+ };
33
+ }
34
+
35
+ export function runGuardedRoots(mode, { cwd = process.cwd(), exec = execSync, attachWatchers } = {}) {
36
+ if (mode === "generate") {
37
+ const snap = snapshotGuardedRoots(cwd);
38
+ try {
39
+ exec(`npm run ${GENERATE_RAW}`, { cwd, stdio: "inherit", shell: true });
40
+ } finally {
41
+ remergeGuardedRoots(cwd, snap);
42
+ }
43
+ return;
44
+ }
45
+ if (mode === "check") {
46
+ withGuardedCheckInteriors(cwd, () => {
47
+ exec(`npm run ${CHECK_RAW}`, { cwd, stdio: "inherit", shell: true });
48
+ });
49
+ return;
50
+ }
51
+ if (mode === "watch") {
52
+ const snap = snapshotGuardedRoots(cwd);
53
+ const heal = (file) => healGuardedRootFile(cwd, file, snap);
54
+ const attach = attachWatchers || ((opts) => attachGuardedRootWatchers(opts.cwd, opts.heal));
55
+ const stop = attach({ cwd, snap, heal });
56
+ try {
57
+ exec(`npm run ${WATCH_RAW}`, { cwd, stdio: "inherit", shell: true });
58
+ } finally {
59
+ try {
60
+ stop?.();
61
+ } catch {
62
+ /* ignore */
63
+ }
64
+ remergeGuardedRoots(cwd, snap);
65
+ }
66
+ return;
67
+ }
68
+ throw new Error("usage: guarded-roots.mjs generate|check|watch");
69
+ }
70
+
71
+ function main() {
72
+ try {
73
+ runGuardedRoots(process.argv[2] || "");
74
+ return 0;
75
+ } catch (err) {
76
+ process.stderr.write((err && err.message ? err.message : String(err)) + "\n");
77
+ return 1;
78
+ }
79
+ }
80
+
81
+ const thisFile = fileURLToPath(import.meta.url);
82
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
83
+ if (invoked === thisFile) process.exit(main());