@lovrozagar/grunt 0.5.0 → 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 -23
  2. package/.agents/agents/orchestrator.md +5 -45
  3. package/.agents/skills/ask/SKILL.md +11 -0
  4. package/.agents/skills/auto/SKILL.md +13 -0
  5. package/.agents/skills/browser/SKILL.md +4 -7
  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 -108
  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 +54 -0
  19. package/.agents/skills/write-plan/SKILL.md +5 -74
  20. package/.claude/agents/grunt.md +2 -25
  21. package/.claude/agents/orchestrator.md +12 -45
  22. package/.claude/skills/ask/SKILL.md +11 -0
  23. package/.claude/skills/auto/SKILL.md +13 -0
  24. package/.claude/skills/browser/SKILL.md +4 -7
  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 -108
  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 +54 -0
  38. package/.claude/skills/write-plan/SKILL.md +5 -74
  39. package/.codex/agents/grunt.toml +2 -25
  40. package/.codex/agents/orchestrator.toml +4 -45
  41. package/.grok/agents/grunt.md +2 -23
  42. package/.grok/agents/orchestrator.md +4 -43
  43. package/.grok/global-settings.toml +1 -3
  44. package/.grok/hooks/orchestrate-parent.js +278 -228
  45. package/.grok/skills/ask/SKILL.md +11 -0
  46. package/.grok/skills/auto/SKILL.md +13 -0
  47. package/.grok/skills/browser/SKILL.md +4 -7
  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 -108
  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 +54 -0
  61. package/.grok/skills/write-plan/SKILL.md +5 -74
  62. package/.rulesync/hooks.jsonc +1 -1
  63. package/.rulesync/mcp-policy.jsonc +0 -2
  64. package/.rulesync/reference/INDEX.md +30 -17
  65. package/.rulesync/reference/browser.md +13 -6
  66. package/.rulesync/reference/cascade.md +6 -84
  67. package/.rulesync/reference/clasp.md +21 -0
  68. package/.rulesync/reference/google-workspace.md +79 -0
  69. package/.rulesync/reference/hooks.md +7 -9
  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 -31
  74. package/.rulesync/reference/output.md +3 -41
  75. package/.rulesync/reference/plan-format.md +14 -12
  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 -9
  81. package/.rulesync/reference/speak.md +59 -0
  82. package/.rulesync/rules/CLAUDE.md +3 -44
  83. package/.rulesync/rules/overview.md +9 -42
  84. package/.rulesync/skills/ask/SKILL.md +11 -0
  85. package/.rulesync/skills/auto/SKILL.md +11 -0
  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 -116
  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 +54 -0
  100. package/.rulesync/skills/write-plan/SKILL.md +5 -80
  101. package/.rulesync/subagents/grunt.md +2 -23
  102. package/.rulesync/subagents/orchestrator.md +14 -54
  103. package/AGENTS.md +9 -42
  104. package/CHANGELOG.md +89 -0
  105. package/CLAUDE.md +1 -44
  106. package/README.md +97 -88
  107. package/cli/grunt.mjs +29 -13
  108. package/cli/grunt.test.ts +62 -20
  109. package/cli/init.mjs +218 -31
  110. package/cli/init.test.ts +669 -46
  111. package/cli/prompt.mjs +8 -0
  112. package/cli/prompt.test.ts +14 -0
  113. package/package.json +13 -1
  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/persist-plan.mjs +17 -2
  125. package/scripts/persist-tmp.mjs +123 -0
  126. package/scripts/scrub-spawn-prompt.mjs +4 -51
  127. package/scripts/setup.mjs +746 -0
  128. package/scripts/speak.mjs +291 -0
  129. package/.agents/agents/implementer.md +0 -32
  130. package/.agents/agents/thinker.md +0 -28
  131. package/.agents/skills/cascade/SKILL.md +0 -26
  132. package/.agents/skills/parent/SKILL.md +0 -6
  133. package/.agents/skills/solo/SKILL.md +0 -23
  134. package/.claude/agents/implementer.md +0 -37
  135. package/.claude/agents/thinker.md +0 -35
  136. package/.claude/skills/cascade/SKILL.md +0 -26
  137. package/.claude/skills/parent/SKILL.md +0 -6
  138. package/.claude/skills/solo/SKILL.md +0 -23
  139. package/.codex/agents/implementer.toml +0 -26
  140. package/.codex/agents/thinker.toml +0 -25
  141. package/.grok/agents/implementer.md +0 -27
  142. package/.grok/agents/thinker.md +0 -26
  143. package/.grok/parent.md +0 -1
  144. package/.grok/roles/implementer.toml +0 -5
  145. package/.grok/roles/thinker.toml +0 -5
  146. package/.grok/skills/cascade/SKILL.md +0 -26
  147. package/.grok/skills/parent/SKILL.md +0 -6
  148. package/.grok/skills/solo/SKILL.md +0 -23
  149. package/.rulesync/skills/cascade/SKILL.md +0 -26
  150. package/.rulesync/skills/parent/SKILL.md +0 -6
  151. package/.rulesync/skills/solo/SKILL.md +0 -23
  152. package/.rulesync/subagents/implementer.md +0 -49
  153. package/.rulesync/subagents/thinker.md +0 -48
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.0",
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",
@@ -36,12 +36,18 @@
36
36
  "scripts/grunt-job.mjs",
37
37
  "scripts/parse-need.mjs",
38
38
  "scripts/persist-handoff.mjs",
39
+ "scripts/persist-implementation.mjs",
40
+ "scripts/persist-tmp.mjs",
39
41
  "scripts/persist-plan.mjs",
40
42
  "scripts/purge-global-mcps.mjs",
41
43
  "scripts/scrub-spawn-prompt.mjs",
42
44
  "scripts/scrub-text-lib.mjs",
43
45
  "scripts/sync-global-settings.mjs",
44
46
  "scripts/browser.mjs",
47
+ "scripts/speak.mjs",
48
+ "scripts/listen.mjs",
49
+ "scripts/google-workspace.mjs",
50
+ "scripts/setup.mjs",
45
51
  "scripts/doctor.mjs",
46
52
  "scripts/skill-conflicts.mjs",
47
53
  "scripts/scrub-text",
@@ -71,12 +77,18 @@
71
77
  "hooks:union": "node scripts/hooks-union.mjs",
72
78
  "hooks:check": "node scripts/hooks-union.mjs --check",
73
79
  "doctor": "node ./scripts/doctor.mjs",
80
+ "setup": "node ./scripts/setup.mjs",
74
81
  "rulesync:doctor": "rulesync doctor",
75
82
  "sync:globals": "node scripts/sync-global-settings.mjs",
76
83
  "sync:globals:check": "node scripts/check-globals.mjs",
77
84
  "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
78
85
  "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
79
86
  "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
87
+ "antigravity": "antigravity --dangerously-skip-permissions",
88
+ "claude": "claude --dangerously-skip-permissions",
89
+ "codex": "codex --dangerously-bypass-approvals-and-sandbox",
90
+ "gemini": "gemini --yolo",
91
+ "grok": "grok --yolo",
80
92
  "test": "vitest run --coverage"
81
93
  },
82
94
  "devDependencies": {
@@ -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,
@@ -2,6 +2,7 @@
2
2
  /** Unified prereq doctor. Print-only install hints. Never runs installs. */
3
3
  import { execFileSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
+ import os from "node:os";
5
6
  import path from "node:path";
6
7
  import { fileURLToPath, pathToFileURL } from "node:url";
7
8
  import {
@@ -166,11 +167,72 @@ export function installHints(id, platform = process.platform) {
166
167
  }
167
168
  return ["sudo apt install chromium"];
168
169
  }
170
+ if (id === "ffmpeg") {
171
+ if (platform === "win32") return ["winget install Gyan.FFmpeg"];
172
+ if (platform === "darwin") return ["brew install ffmpeg"];
173
+ return ["sudo apt install ffmpeg"];
174
+ }
175
+ if (id === "whisper-cli") {
176
+ if (platform === "win32") {
177
+ return ["whisper.cpp release: whisper-cli.exe on PATH", "https://github.com/ggml-org/whisper.cpp"];
178
+ }
179
+ if (platform === "darwin") return ["brew install whisper-cpp"];
180
+ return ["https://github.com/ggml-org/whisper.cpp"];
181
+ }
169
182
  return [];
170
183
  }
171
184
 
185
+ function isFile(abs) {
186
+ try {
187
+ return fs.statSync(abs).isFile();
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ export function speakDoctorStatus({ env = process.env, home = os.homedir() } = {}) {
194
+ const names = [];
195
+ if (String(env.ELEVENLABS_API_KEY || "").trim()) names.push("elevenlabs");
196
+ if (String(env.OPENAI_API_KEY || "").trim()) names.push("openai");
197
+ const hasFile = isFile(path.join(home, ".grunt", "speak.json"));
198
+ if (!names.length && !hasFile) return { ok: false, extra: "" };
199
+ return { ok: true, extra: names.length ? names.join(",") : "config" };
200
+ }
201
+
202
+ const WORKSPACE_ACCOUNT_RE = /^[a-zA-Z0-9_-]{1,40}$/;
203
+ const WORKSPACE_KIND_ORDER = ["oauth", "tokens", "adc", "clasprc"];
204
+
205
+ export function workspaceDoctorStatus({ home = os.homedir() } = {}) {
206
+ const grunt = path.join(home, ".grunt");
207
+ const kinds = new Set();
208
+ const scanAccountDir = (dir) => {
209
+ if (isFile(path.join(dir, "google-oauth.json"))) kinds.add("oauth");
210
+ if (isFile(path.join(dir, "tokens.json"))) kinds.add("tokens");
211
+ };
212
+ if (isFile(path.join(grunt, "google-oauth.json"))) kinds.add("oauth");
213
+ if (isFile(path.join(grunt, "workspace-tokens.json"))) kinds.add("tokens");
214
+ scanAccountDir(path.join(grunt, "workspace", "default"));
215
+ const root = path.join(grunt, "workspace");
216
+ try {
217
+ for (const ent of fs.readdirSync(root, { withFileTypes: true })) {
218
+ if (!ent.isDirectory() || !WORKSPACE_ACCOUNT_RE.test(ent.name)) continue;
219
+ scanAccountDir(path.join(root, ent.name));
220
+ }
221
+ } catch {
222
+ /* none */
223
+ }
224
+ if (
225
+ isFile(path.join(home, ".config", "gcloud", "application_default_credentials.json"))
226
+ ) {
227
+ kinds.add("adc");
228
+ }
229
+ if (isFile(path.join(home, ".clasprc.json"))) kinds.add("clasprc");
230
+ if (!kinds.size) return { ok: false, extra: "" };
231
+ return { ok: true, extra: WORKSPACE_KIND_ORDER.filter((k) => kinds.has(k)).join(",") };
232
+ }
233
+
172
234
  function row(name, status, extra = "") {
173
- const a = String(name).padEnd(14);
235
+ const a = String(name).padEnd(18);
174
236
  const b = String(status).padEnd(18);
175
237
  const c = extra ? String(extra).trim() : "";
176
238
  return c ? `${a} ${b} ${c}` : `${a} ${b}`;
@@ -205,6 +267,8 @@ export function runDoctor({
205
267
  platform = process.platform,
206
268
  execPath = process.execPath,
207
269
  nodeVersion = process.version,
270
+ env = process.env,
271
+ home = os.homedir(),
208
272
  } = {}) {
209
273
  const nmPath = withNmBin(cwd, pathEnv, platform);
210
274
  const pathNode = whichBin("node", pathEnv, platform);
@@ -226,6 +290,10 @@ export function runDoctor({
226
290
  const lightpanda = whichBin("lightpanda", pathEnv, platform);
227
291
  const chromium = lookupChromium(pathEnv, platform);
228
292
  const gh = whichBin("gh", pathEnv, platform);
293
+ const clasp = whichBin("clasp", pathEnv, platform);
294
+ const ffmpeg = whichBin("ffmpeg", pathEnv, platform);
295
+ const whisperCli =
296
+ whichBin("whisper-cli", pathEnv, platform) || whichBin("whisper-cpp", pathEnv, platform);
229
297
 
230
298
  const found = {
231
299
  node: nodeOk ? nodeBin : "",
@@ -252,6 +320,19 @@ export function runDoctor({
252
320
  lines.push(lightpanda ? row("lightpanda", "ok", lightpanda) : row("lightpanda", "missing"));
253
321
  lines.push(chromium ? row("chromium", "ok", chromium) : row("chromium", "missing"));
254
322
  lines.push(gh ? row("gh", "ok", gh) : row("gh", "missing (optional)"));
323
+ lines.push(clasp ? row("clasp", "ok", clasp) : row("clasp", "missing (optional)"));
324
+ const workspace = workspaceDoctorStatus({ home });
325
+ lines.push(
326
+ workspace.ok
327
+ ? row("google-workspace", "ok", workspace.extra)
328
+ : row("google-workspace", "missing (optional)"),
329
+ );
330
+ lines.push(ffmpeg ? row("ffmpeg", "ok", ffmpeg) : row("ffmpeg", "missing (optional)"));
331
+ lines.push(
332
+ whisperCli ? row("whisper-cli", "ok", whisperCli) : row("whisper-cli", "missing (optional)"),
333
+ );
334
+ const speak = speakDoctorStatus({ env, home });
335
+ lines.push(speak.ok ? row("speak", "ok", speak.extra) : row("speak", "missing (optional)"));
255
336
 
256
337
  const missingRequired = REQUIRED.filter((k) => (k === "node" ? !nodeOk : !found[k]));
257
338
  if (missingRequired.length) {
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { guardedMarkdownDrift, writeMergedGuardedFile } from "../cli/init.mjs";
7
7
 
8
- export const AGENT_IDS = ["orchestrator", "implementer", "thinker", "grunt"];
8
+ export const AGENT_IDS = ["orchestrator", "grunt"];
9
9
  const SSOT_REL = ".rulesync/subagents";
10
10
  const GEMINI_MD_REL = "GEMINI.md";
11
11
  const AGENTS_REL = ".gemini/agents";
@@ -12,7 +12,7 @@ const MCP_JSON_REL = ".mcp.json";
12
12
  const CODEX_REL = ".codex/config.toml";
13
13
  const AGENTS_MCP_REL = ".agents/mcp_config.json";
14
14
  const GEMINI_SETTINGS_REL = ".gemini/settings.json";
15
- const AGENT_IDS = ["orchestrator", "implementer", "thinker", "grunt"];
15
+ const AGENT_IDS = ["orchestrator", "grunt"];
16
16
  const MCP_DENY = "mcp__*";
17
17
  const AGENT_ORCHESTRATOR_DENY = "Agent(orchestrator)";
18
18
  const GROK_HEADER =