@lovrozagar/grunt 0.1.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 (79) hide show
  1. package/.agents/agents/grunt.md +36 -0
  2. package/.agents/agents/implementer.md +40 -0
  3. package/.agents/agents/orchestrator.md +50 -0
  4. package/.agents/agents/thinker.md +27 -0
  5. package/.agents/hooks.json +26 -0
  6. package/.agents/mcp_config.json +3 -0
  7. package/.claude/agents/grunt.md +44 -0
  8. package/.claude/agents/implementer.md +46 -0
  9. package/.claude/agents/orchestrator.md +55 -0
  10. package/.claude/agents/thinker.md +34 -0
  11. package/.claude/settings.json +36 -0
  12. package/.codex/agents/grunt.toml +30 -0
  13. package/.codex/agents/implementer.toml +34 -0
  14. package/.codex/agents/orchestrator.toml +45 -0
  15. package/.codex/agents/thinker.toml +23 -0
  16. package/.codex/config.toml +2 -0
  17. package/.codex/hooks.json +26 -0
  18. package/.grok/agents/grunt.md +31 -0
  19. package/.grok/agents/implementer.md +35 -0
  20. package/.grok/agents/orchestrator.md +47 -0
  21. package/.grok/agents/thinker.md +25 -0
  22. package/.grok/config.toml +6 -0
  23. package/.grok/global-settings.toml +35 -0
  24. package/.grok/hooks/orchestrate-parent.js +476 -0
  25. package/.grok/hooks/orchestrate-parent.json +60 -0
  26. package/.grok/hooks/rtk-grok.js +137 -0
  27. package/.grok/hooks/rtk.json +16 -0
  28. package/.grok/hooks/rulesync.json +3 -0
  29. package/.grok/parent.md +1 -0
  30. package/.grok/roles/grunt.toml +5 -0
  31. package/.grok/roles/implementer.toml +5 -0
  32. package/.grok/roles/thinker.toml +5 -0
  33. package/.grok/skills/explain/SKILL.md +15 -0
  34. package/.grok/skills/implement-plan/SKILL.md +91 -0
  35. package/.grok/skills/parent/SKILL.md +12 -0
  36. package/.grok/skills/shared/plan-format.md +158 -0
  37. package/.grok/skills/write-plan/SKILL.md +84 -0
  38. package/.mcp.json +3 -0
  39. package/.rulesync/global-settings/antigravity.json +1 -0
  40. package/.rulesync/global-settings/claude.json +1 -0
  41. package/.rulesync/global-settings/codex.toml +1 -0
  42. package/.rulesync/global-settings/manifest.json +37 -0
  43. package/.rulesync/hooks.jsonc +22 -0
  44. package/.rulesync/mcp-policy.jsonc +15 -0
  45. package/.rulesync/reference/cascade.md +85 -0
  46. package/.rulesync/reference/hooks.md +19 -0
  47. package/.rulesync/reference/map.md +57 -0
  48. package/.rulesync/reference/rtk.md +23 -0
  49. package/.rulesync/rules/CLAUDE.md +13 -0
  50. package/.rulesync/rules/overview.md +23 -0
  51. package/.rulesync/skills/explain/SKILL.md +15 -0
  52. package/.rulesync/skills/parent/SKILL.md +12 -0
  53. package/.rulesync/skills/terse/SKILL.md +8 -0
  54. package/.rulesync/subagents/grunt.md +51 -0
  55. package/.rulesync/subagents/implementer.md +54 -0
  56. package/.rulesync/subagents/orchestrator.md +69 -0
  57. package/.rulesync/subagents/thinker.md +44 -0
  58. package/AGENTS.md +12 -0
  59. package/CLAUDE.md +7 -0
  60. package/LICENSE +21 -0
  61. package/README.md +56 -0
  62. package/bin/grunt.js +4 -0
  63. package/cli/grunt.mjs +54 -0
  64. package/cli/grunt.test.ts +117 -0
  65. package/cli/init.mjs +113 -0
  66. package/cli/init.test.ts +268 -0
  67. package/package.json +77 -0
  68. package/scripts/check-globals.mjs +98 -0
  69. package/scripts/emit-mcp-policy.mjs +434 -0
  70. package/scripts/gate-fat-tools.mjs +485 -0
  71. package/scripts/grunt-job.mjs +482 -0
  72. package/scripts/parse-need.mjs +75 -0
  73. package/scripts/persist-plan.mjs +309 -0
  74. package/scripts/purge-global-mcps.mjs +291 -0
  75. package/scripts/scrub-spawn-prompt.mjs +219 -0
  76. package/scripts/scrub-text +58 -0
  77. package/scripts/scrub-text-lib.mjs +210 -0
  78. package/scripts/sync-global-settings.mjs +239 -0
  79. package/scripts/telemetry.mjs +38 -0
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ /** PreToolUse: intent-scrub spawn `prompt`. Fail-open: unchanged/error → empty stdout, exit 0. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { scrubText } from "./scrub-text-lib.mjs";
7
+
8
+ const ALLOWED_TYPES = new Set(["grunt", "implementer", "thinker"]);
9
+ export const MAX_PROMPT_CHARS = 8000;
10
+ export const TRUNCATE_SUFFIX = "…[truncated]";
11
+ /** `router` is a legacy transcript prefix; keep matching it to strip old pastes. */
12
+ const TRANSCRIPT_PREFIX =
13
+ /^\s*(?:\[(?:orchestrator|router|implementer|thinker|grunt)\]:|Human:|Assistant:)\s*/i;
14
+ const YOU_ARE_SUBAGENT =
15
+ /^\s*You are (?:grunt|implementer|thinker) subagent\b/;
16
+ const VERDICT_LINE = /^\s*verdict:\s*/;
17
+
18
+ export function stripTranscripts(text) {
19
+ const lines = String(text).split(/\n/);
20
+ const out = [];
21
+ let keptYouAre = false;
22
+ for (const line of lines) {
23
+ if (VERDICT_LINE.test(line)) {
24
+ out.push(line);
25
+ continue;
26
+ }
27
+ if (TRANSCRIPT_PREFIX.test(line)) continue;
28
+ if (YOU_ARE_SUBAGENT.test(line)) {
29
+ if (keptYouAre) continue;
30
+ keptYouAre = true;
31
+ }
32
+ out.push(line);
33
+ }
34
+ return out.join("\n");
35
+ }
36
+
37
+ export function extractVerdictBlocks(text) {
38
+ const rawLines = String(text).split(/\n/);
39
+ const lines = [];
40
+ for (const line of rawLines) {
41
+ const idx = line.search(/(?:^|\s)verdict:\s/);
42
+ if (idx > 0) {
43
+ const before = line.slice(0, idx).trimEnd();
44
+ const after = line.slice(idx).trimStart();
45
+ if (before) lines.push(before);
46
+ lines.push(after);
47
+ } else {
48
+ lines.push(line);
49
+ }
50
+ }
51
+ const body = [];
52
+ const blocks = [];
53
+ let current = null;
54
+ for (const line of lines) {
55
+ if (VERDICT_LINE.test(line)) {
56
+ if (current) blocks.push(current.join("\n"));
57
+ current = [line];
58
+ continue;
59
+ }
60
+ if (current) {
61
+ if (current.length < 8) current.push(line);
62
+ else {
63
+ blocks.push(current.join("\n"));
64
+ current = null;
65
+ body.push(line);
66
+ }
67
+ continue;
68
+ }
69
+ body.push(line);
70
+ }
71
+ if (current) blocks.push(current.join("\n"));
72
+ return { body: body.join("\n"), blocks };
73
+ }
74
+
75
+ export function truncatePrompt(text, max = MAX_PROMPT_CHARS) {
76
+ const s = String(text);
77
+ if (s.length <= max) return s;
78
+ const { body, blocks } = extractVerdictBlocks(s);
79
+ const verdictPart = blocks.length ? "\n" + blocks.join("\n") : "";
80
+ const budget = max - verdictPart.length - TRUNCATE_SUFFIX.length;
81
+ const cut = Math.max(0, budget);
82
+ return body.slice(0, cut) + TRUNCATE_SUFFIX + verdictPart;
83
+ }
84
+
85
+ const INLINE_TRANSCRIPT =
86
+ /\s*(?:\[(?:orchestrator|router|implementer|thinker|grunt)\]:|Human:|Assistant:)\s*/gi;
87
+ const EXTRA_YOU_ARE =
88
+ /\s*You are (?:grunt|implementer|thinker) subagent\b[^.]*\.?/g;
89
+
90
+ export function stripInlineTranscripts(text) {
91
+ let s = String(text);
92
+ const first = s.match(
93
+ /^\s*You are (?:grunt|implementer|thinker) subagent\b[^.]*\.?\s*/,
94
+ );
95
+ const prefix = first ? first[0] : "";
96
+ let rest = first ? s.slice(first[0].length) : s;
97
+ rest = rest.replace(INLINE_TRANSCRIPT, " ");
98
+ rest = rest.replace(EXTRA_YOU_ARE, " ");
99
+ return (prefix + rest).replace(/[ \t]{2,}/g, " ").trim();
100
+ }
101
+
102
+ export function capSpawnPrompt(prompt) {
103
+ let s = stripTranscripts(String(prompt));
104
+ const extracted = extractVerdictBlocks(s);
105
+ s = scrubText(extracted.body, { intent: true });
106
+ s = stripTranscripts(s);
107
+ s = stripInlineTranscripts(s);
108
+ const again = extractVerdictBlocks(s);
109
+ const blocks = extracted.blocks.concat(again.blocks);
110
+ const verdictPart = blocks.length ? "\n" + blocks.join("\n") : "";
111
+ const maxCore = Math.max(0, MAX_PROMPT_CHARS - verdictPart.length);
112
+ let core = again.body;
113
+ if (core.length > maxCore) {
114
+ const cut = Math.max(0, maxCore - TRUNCATE_SUFFIX.length);
115
+ core = core.slice(0, cut) + TRUNCATE_SUFFIX;
116
+ }
117
+ return (core + verdictPart).trim();
118
+ }
119
+
120
+ export function rewriteSpawnToolInput(toolInput, { defaultGrunt = false } = {}) {
121
+ if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
122
+ return defaultGrunt ? { subagent_type: "grunt" } : null;
123
+ }
124
+
125
+ const next = Object.assign({}, toolInput);
126
+ let changed = false;
127
+
128
+ if (defaultGrunt) {
129
+ const cur =
130
+ toolInput.subagent_type != null
131
+ ? String(toolInput.subagent_type).trim()
132
+ : toolInput.subagentType != null
133
+ ? String(toolInput.subagentType).trim()
134
+ : "";
135
+ if (!ALLOWED_TYPES.has(cur)) {
136
+ next.subagent_type = "grunt";
137
+ if (Object.prototype.hasOwnProperty.call(toolInput, "subagentType")) {
138
+ next.subagentType = "grunt";
139
+ }
140
+ changed = true;
141
+ }
142
+ }
143
+
144
+ if (typeof next.prompt === "string") {
145
+ const scrubbed = capSpawnPrompt(next.prompt);
146
+ if (scrubbed !== next.prompt) {
147
+ next.prompt = scrubbed;
148
+ changed = true;
149
+ }
150
+ }
151
+
152
+ return changed ? next : null;
153
+ }
154
+
155
+ export function grokDefaultGrunt() {
156
+ return Boolean(process.env.GROK_HOOK_EVENT);
157
+ }
158
+
159
+ export function processHookPayload(data) {
160
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
161
+ let toolInput = data.toolInput;
162
+ if (toolInput == null) toolInput = data.tool_input;
163
+ return rewriteSpawnToolInput(toolInput, {
164
+ defaultGrunt: grokDefaultGrunt(),
165
+ });
166
+ }
167
+
168
+ export function hookResponse(updated) {
169
+ return {
170
+ hookSpecificOutput: {
171
+ hookEventName: "PreToolUse",
172
+ updatedInput: updated,
173
+ },
174
+ };
175
+ }
176
+
177
+ function main() {
178
+ try {
179
+ const data = readJsonValue();
180
+ const updated = processHookPayload(data);
181
+ if (!updated) return 0;
182
+ process.stdout.write(JSON.stringify(hookResponse(updated)));
183
+ return 0;
184
+ } catch {
185
+ return 0;
186
+ }
187
+ }
188
+
189
+ function readJsonValue() {
190
+ let buf = Buffer.alloc(0);
191
+ const tmp = Buffer.alloc(8192);
192
+ for (;;) {
193
+ let n;
194
+ try {
195
+ n = fs.readSync(0, tmp, 0, tmp.length, null);
196
+ } catch {
197
+ break;
198
+ }
199
+ if (n === 0) break;
200
+ buf = Buffer.concat([buf, tmp.subarray(0, n)]);
201
+ try {
202
+ return JSON.parse(buf.toString("utf8"));
203
+ } catch {
204
+ // incomplete
205
+ }
206
+ }
207
+ if (!buf.length) return undefined;
208
+ try {
209
+ return JSON.parse(buf.toString("utf8"));
210
+ } catch {
211
+ return undefined;
212
+ }
213
+ }
214
+
215
+ const thisFile = fileURLToPath(import.meta.url);
216
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
217
+ if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
218
+ process.exit(main());
219
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import {
5
+ parseArgs,
6
+ autoMd,
7
+ scrubText,
8
+ helpText,
9
+ decodeUtf8,
10
+ } from "./scrub-text-lib.mjs";
11
+
12
+ function readInputs(files) {
13
+ if (!files.length) {
14
+ try {
15
+ return decodeUtf8(fs.readFileSync(0), "stdin");
16
+ } catch (e) {
17
+ process.stderr.write(`scrub-text: ${e.message}\n`);
18
+ process.exit(1);
19
+ }
20
+ }
21
+ const chunks = [];
22
+ for (const f of files) {
23
+ try {
24
+ chunks.push(decodeUtf8(fs.readFileSync(f), f));
25
+ } catch (e) {
26
+ process.stderr.write(`scrub-text: ${e.message}\n`);
27
+ process.exit(1);
28
+ }
29
+ }
30
+ return chunks.join("\n");
31
+ }
32
+
33
+ function emit(text) {
34
+ if (!text) return;
35
+ process.stdout.write(text);
36
+ if (!text.endsWith("\n")) process.stdout.write("\n");
37
+ }
38
+
39
+ export function main(argv) {
40
+ const args = parseArgs(argv);
41
+ if (args.unknown.length && !args.help) {
42
+ process.stderr.write(
43
+ "usage: scrub-text [--help] [--intent] [--md] [--no-md] [FILE ...]\n" +
44
+ `scrub-text: error: unrecognized arguments: ${args.unknown.join(" ")}\n`,
45
+ );
46
+ process.exit(2);
47
+ }
48
+ if (args.help) {
49
+ process.stdout.write(helpText());
50
+ return 0;
51
+ }
52
+ const md = autoMd(args.files, args.md, args.noMd);
53
+ const raw = readInputs(args.files);
54
+ emit(scrubText(raw, { md, intent: args.intent }));
55
+ return 0;
56
+ }
57
+
58
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,210 @@
1
+ import path from "node:path";
2
+
3
+ export const MD_SUFFIXES = new Set([".md", ".markdown"]);
4
+ const FENCE_RE = /^( {0,3})(`{3,}|~{3,})/;
5
+ const INTENT_PREFIXES = [
6
+ "can you please",
7
+ "could you please",
8
+ "would you please",
9
+ "can you",
10
+ "could you",
11
+ "would you",
12
+ "please",
13
+ "i want you to",
14
+ "i need you to",
15
+ "i would like you to",
16
+ "i'd like you to",
17
+ ];
18
+ const INTENT_MID = [
19
+ [String.raw`\bmake me an\b`, "make"],
20
+ [String.raw`\bmake me a\b`, "make"],
21
+ [String.raw`\bmake me\b`, "make"],
22
+ [String.raw`\bi want the\b`, ""],
23
+ [String.raw`\bi need the\b`, ""],
24
+ [String.raw`\bi want a\b`, ""],
25
+ [String.raw`\bi need a\b`, ""],
26
+ [String.raw`\bi want\b`, ""],
27
+ [String.raw`\bi need\b`, ""],
28
+ [String.raw`\bto\s*,`, ","],
29
+ ];
30
+ const FILLER_PREFIX = new RegExp(
31
+ "^(?:" + INTENT_PREFIXES.join("|") + ")\\s+",
32
+ "i",
33
+ );
34
+ const FILLER_SUFFIX = /\s+(?:thank you|thanks|please)[.!]*\s*$/i;
35
+ const COMMA_WS = /\s*,\s*/g;
36
+ const WS_RUN = /[ \t]+/g;
37
+ const decoder = new TextDecoder("utf-8", { fatal: true });
38
+
39
+ export function helpText() {
40
+ return `Usage: scrub-text [--intent] [--md] [--no-md] [FILE...]
41
+ Normalize UTF-8 text for agents. Reads stdin if no FILE.
42
+
43
+ --intent One-line paste: drop blanks; strip filler (please / can you /
44
+ i want / make me a / to , …). Typos kept; no spellcheck.
45
+ --md Markdown-aware (preserve fences, headings, lists)
46
+ --no-md Disable auto --md for .md/.markdown files
47
+ -h, --help This help
48
+
49
+ Default: UTF-8, newlines to \\n, strip trailing whitespace, collapse extra
50
+ blank lines and (plain text) internal whitespace. .md/.markdown files enable
51
+ --md unless --no-md.
52
+
53
+ Not RTK (shell stdout). Use this for user paste and markdown files.
54
+ `;
55
+ }
56
+
57
+ export function parseArgs(argv) {
58
+ const args = {
59
+ help: false,
60
+ intent: false,
61
+ md: false,
62
+ noMd: false,
63
+ files: [],
64
+ unknown: [],
65
+ };
66
+ let i = 0;
67
+ while (i < argv.length) {
68
+ const a = argv[i];
69
+ if (a === "--") {
70
+ args.files.push(...argv.slice(i + 1));
71
+ break;
72
+ }
73
+ if (a === "--help" || a === "-h") {
74
+ args.help = true;
75
+ } else if (a === "--intent") {
76
+ args.intent = true;
77
+ } else if (a === "--md") {
78
+ args.md = true;
79
+ } else if (a === "--no-md") {
80
+ args.noMd = true;
81
+ } else if (a.startsWith("-") && a !== "-") {
82
+ args.unknown.push(a);
83
+ } else {
84
+ args.files.push(a);
85
+ }
86
+ i += 1;
87
+ }
88
+ return args;
89
+ }
90
+
91
+ export function decodeUtf8(data, label) {
92
+ try {
93
+ return decoder.decode(data);
94
+ } catch (e) {
95
+ throw new Error(`${label}: ${e.message}`);
96
+ }
97
+ }
98
+
99
+ export function autoMd(files, forceMd, noMd) {
100
+ if (noMd) return false;
101
+ if (forceMd) return true;
102
+ if (!files.length) return false;
103
+ return files.every((f) => MD_SUFFIXES.has(path.extname(f).toLowerCase()));
104
+ }
105
+
106
+ export function normalizeNewlines(text) {
107
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
108
+ }
109
+
110
+ export function collapseBlankRuns(lines, maxBlank) {
111
+ const out = [];
112
+ let blanks = 0;
113
+ for (const line of lines) {
114
+ if (line === "") {
115
+ blanks += 1;
116
+ if (blanks <= maxBlank) out.push("");
117
+ } else {
118
+ blanks = 0;
119
+ out.push(line);
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+
125
+ export function trimEdgeBlanks(lines) {
126
+ let start = 0;
127
+ let end = lines.length;
128
+ while (start < end && lines[start] === "") start += 1;
129
+ while (end > start && lines[end - 1] === "") end -= 1;
130
+ return lines.slice(start, end);
131
+ }
132
+
133
+ export function collapseInternalWs(line) {
134
+ return line.replace(WS_RUN, " ").trim();
135
+ }
136
+
137
+ export function fenceMatch(line) {
138
+ return line.match(FENCE_RE);
139
+ }
140
+
141
+ export function normalizePlain(text) {
142
+ text = normalizeNewlines(text);
143
+ let lines = text.split("\n").map((ln) => collapseInternalWs(ln.replace(/[ \t]+$/g, "")));
144
+ lines = collapseBlankRuns(lines, 1);
145
+ lines = trimEdgeBlanks(lines);
146
+ const nonempty = lines.filter((ln) => ln);
147
+ if (nonempty.length && nonempty.every((ln) => ln.length <= 80)) {
148
+ lines = nonempty;
149
+ }
150
+ return lines.join("\n");
151
+ }
152
+
153
+ export function normalizeMd(text) {
154
+ text = normalizeNewlines(text);
155
+ const rawLines = text.split("\n");
156
+ const out = [];
157
+ let fenceMarker = null;
158
+ for (const line of rawLines) {
159
+ if (fenceMarker !== null) {
160
+ out.push(line);
161
+ const m = fenceMatch(line);
162
+ if (
163
+ m &&
164
+ m[2][0] === fenceMarker[0] &&
165
+ m[2].length >= fenceMarker.length
166
+ ) {
167
+ fenceMarker = null;
168
+ }
169
+ continue;
170
+ }
171
+ const m = fenceMatch(line);
172
+ if (m) {
173
+ fenceMarker = m[2];
174
+ out.push(line.replace(/[ \t]+$/g, ""));
175
+ continue;
176
+ }
177
+ out.push(line.replace(/[ \t]+$/g, ""));
178
+ }
179
+ return trimEdgeBlanks(collapseBlankRuns(out, 2)).join("\n");
180
+ }
181
+
182
+ export function stripFiller(text) {
183
+ let prev = null;
184
+ while (prev !== text) {
185
+ prev = text;
186
+ text = text.replace(FILLER_PREFIX, "").trim();
187
+ text = text.replace(FILLER_SUFFIX, "").trim();
188
+ for (const [pat, repl] of INTENT_MID) {
189
+ text = text.replace(new RegExp(pat, "gi"), repl);
190
+ }
191
+ text = text.replace(COMMA_WS, " ");
192
+ text = text.replace(WS_RUN, " ").trim();
193
+ }
194
+ return text;
195
+ }
196
+
197
+ export function intentMode(text) {
198
+ const lines = text
199
+ .split("\n")
200
+ .map((ln) => ln.trim())
201
+ .filter(Boolean);
202
+ const joined = lines.join(" ").replace(WS_RUN, " ").trim();
203
+ return stripFiller(joined);
204
+ }
205
+
206
+ export function scrubText(raw, { md = false, intent = false } = {}) {
207
+ let text = md ? normalizeMd(raw) : normalizePlain(raw);
208
+ if (intent) text = intentMode(text);
209
+ return text;
210
+ }