@lovrozagar/grunt 0.3.10 → 0.4.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 (50) hide show
  1. package/.agents/agents/grunt.md +1 -0
  2. package/.agents/agents/implementer.md +3 -2
  3. package/.agents/agents/orchestrator.md +4 -3
  4. package/.agents/skills/browser/SKILL.md +17 -0
  5. package/.agents/skills/handoff/SKILL.md +2 -2
  6. package/.agents/skills/pickup/SKILL.md +64 -0
  7. package/.agents/skills/solo/SKILL.md +1 -1
  8. package/.claude/agents/grunt.md +1 -0
  9. package/.claude/agents/implementer.md +3 -2
  10. package/.claude/agents/orchestrator.md +4 -3
  11. package/.claude/skills/browser/SKILL.md +17 -0
  12. package/.claude/skills/handoff/SKILL.md +2 -2
  13. package/.claude/skills/pickup/SKILL.md +64 -0
  14. package/.claude/skills/solo/SKILL.md +1 -1
  15. package/.codex/agents/grunt.toml +1 -0
  16. package/.codex/agents/implementer.toml +3 -2
  17. package/.codex/agents/orchestrator.toml +4 -3
  18. package/.grok/agents/grunt.md +1 -0
  19. package/.grok/agents/implementer.md +3 -2
  20. package/.grok/agents/orchestrator.md +4 -3
  21. package/.grok/hooks/orchestrate-parent.js +4 -3
  22. package/.grok/skills/browser/SKILL.md +17 -0
  23. package/.grok/skills/handoff/SKILL.md +2 -2
  24. package/.grok/skills/pickup/SKILL.md +64 -0
  25. package/.grok/skills/solo/SKILL.md +1 -1
  26. package/.rulesync/reference/browser.md +59 -0
  27. package/.rulesync/reference/cascade.md +2 -1
  28. package/.rulesync/reference/hooks.md +1 -1
  29. package/.rulesync/reference/map.md +6 -3
  30. package/.rulesync/reference/rtk.md +4 -1
  31. package/.rulesync/rules/CLAUDE.md +3 -2
  32. package/.rulesync/rules/overview.md +3 -2
  33. package/.rulesync/skills/browser/SKILL.md +19 -0
  34. package/.rulesync/skills/handoff/SKILL.md +2 -2
  35. package/.rulesync/skills/pickup/SKILL.md +64 -0
  36. package/.rulesync/skills/solo/SKILL.md +1 -1
  37. package/.rulesync/subagents/grunt.md +1 -0
  38. package/.rulesync/subagents/implementer.md +3 -2
  39. package/.rulesync/subagents/orchestrator.md +4 -3
  40. package/AGENTS.md +3 -2
  41. package/CLAUDE.md +3 -2
  42. package/README.md +35 -5
  43. package/cli/grunt.mjs +2 -2
  44. package/cli/grunt.test.ts +3 -3
  45. package/cli/init.mjs +2 -0
  46. package/cli/init.test.ts +2 -0
  47. package/package.json +5 -2
  48. package/scripts/browser.mjs +764 -0
  49. package/scripts/doctor.mjs +252 -0
  50. package/scripts/gate-fat-tools.mjs +19 -4
@@ -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
+ }
@@ -510,7 +510,20 @@ function loadInProgressPlans(workspaceRoot) {
510
510
  return out;
511
511
  }
512
512
 
513
- function implementerWriteDeny(input, workspaceRoot) {
513
+ function specTextOf(data) {
514
+ if (!data || typeof data !== "object" || Array.isArray(data)) return "";
515
+ const parts = [];
516
+ for (const k of ["prompt", "userPrompt", "user_prompt", "content"]) {
517
+ if (typeof data[k] === "string" && data[k]) parts.push(data[k]);
518
+ }
519
+ const agent = data.agent;
520
+ if (agent && typeof agent === "object" && !Array.isArray(agent)) {
521
+ if (typeof agent.prompt === "string" && agent.prompt) parts.push(agent.prompt);
522
+ }
523
+ return parts.join("\n");
524
+ }
525
+
526
+ function implementerWriteDeny(input, workspaceRoot, data) {
514
527
  const ws = path.resolve(workspaceRoot);
515
528
  const plans = loadInProgressPlans(ws);
516
529
  const allow = new Set();
@@ -520,14 +533,16 @@ function implementerWriteDeny(input, workspaceRoot) {
520
533
  allow.add(extracted);
521
534
  }
522
535
  }
523
- const hasInProgress = plans.length > 0;
536
+ const promptPaths = extractPlanPaths(specTextOf(data), ws);
537
+ for (const abs of promptPaths) allow.add(abs);
538
+ const restrictive = promptPaths.size > 0;
524
539
  for (const key of WRITE_PATH_FIELDS) {
525
540
  if (typeof input[key] !== "string" || !input[key]) continue;
526
541
  const abs = resolveWriteAbs(input[key], ws);
527
542
  if (!abs) continue;
528
543
  if (isUnderPlansDir(abs, ws)) continue;
529
544
  if (allow.has(abs)) continue;
530
- if (isUnsolicitedDocPath(abs) || hasInProgress) {
545
+ if (isUnsolicitedDocPath(abs) || restrictive) {
531
546
  return { type: "deny", reason: REASON_IMPLEMENTER_WRITE };
532
547
  }
533
548
  }
@@ -556,7 +571,7 @@ export function processFatTools(data) {
556
571
  }
557
572
  }
558
573
  if (subagentTypeOf(data) === "implementer") {
559
- const denied = implementerWriteDeny(next, ws);
574
+ const denied = implementerWriteDeny(next, ws, data);
560
575
  if (denied) return denied;
561
576
  }
562
577
  return changed ? { type: "rewrite", updatedInput: next } : null;