@lovrozagar/grunt 0.5.2 → 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.
- package/.agents/agents/grunt.md +2 -24
- package/.agents/agents/orchestrator.md +5 -50
- package/.agents/skills/ask/SKILL.md +5 -12
- package/.agents/skills/auto/SKILL.md +7 -12
- package/.agents/skills/browser/SKILL.md +4 -8
- package/.agents/skills/clasp/SKILL.md +11 -0
- package/.agents/skills/commit/SKILL.md +3 -1
- package/.agents/skills/commit-push/SKILL.md +2 -0
- package/.agents/skills/commit-push-deploy/SKILL.md +2 -0
- package/.agents/skills/commit-push-release/SKILL.md +2 -0
- package/.agents/skills/explain/SKILL.md +7 -13
- package/.agents/skills/google-workspace/SKILL.md +13 -0
- package/.agents/skills/handoff/SKILL.md +7 -11
- package/.agents/skills/implement-plan/SKILL.md +5 -109
- package/.agents/skills/listen/SKILL.md +15 -0
- package/.agents/skills/pickup/SKILL.md +6 -12
- package/.agents/skills/speak/SKILL.md +13 -0
- package/.agents/skills/tmp/SKILL.md +6 -10
- package/.agents/skills/write-plan/SKILL.md +5 -77
- package/.claude/agents/grunt.md +2 -26
- package/.claude/agents/orchestrator.md +12 -50
- package/.claude/skills/ask/SKILL.md +5 -12
- package/.claude/skills/auto/SKILL.md +7 -12
- package/.claude/skills/browser/SKILL.md +4 -8
- package/.claude/skills/clasp/SKILL.md +11 -0
- package/.claude/skills/commit/SKILL.md +3 -1
- package/.claude/skills/commit-push/SKILL.md +2 -0
- package/.claude/skills/commit-push-deploy/SKILL.md +2 -0
- package/.claude/skills/commit-push-release/SKILL.md +2 -0
- package/.claude/skills/explain/SKILL.md +7 -13
- package/.claude/skills/google-workspace/SKILL.md +13 -0
- package/.claude/skills/handoff/SKILL.md +7 -11
- package/.claude/skills/implement-plan/SKILL.md +5 -109
- package/.claude/skills/listen/SKILL.md +15 -0
- package/.claude/skills/pickup/SKILL.md +6 -12
- package/.claude/skills/speak/SKILL.md +13 -0
- package/.claude/skills/tmp/SKILL.md +6 -10
- package/.claude/skills/write-plan/SKILL.md +5 -77
- package/.codex/agents/grunt.toml +2 -26
- package/.codex/agents/orchestrator.toml +4 -50
- package/.grok/agents/grunt.md +2 -24
- package/.grok/agents/orchestrator.md +4 -48
- package/.grok/global-settings.toml +1 -3
- package/.grok/hooks/orchestrate-parent.js +190 -358
- package/.grok/skills/ask/SKILL.md +5 -12
- package/.grok/skills/auto/SKILL.md +7 -12
- package/.grok/skills/browser/SKILL.md +4 -8
- package/.grok/skills/clasp/SKILL.md +11 -0
- package/.grok/skills/commit/SKILL.md +3 -1
- package/.grok/skills/commit-push/SKILL.md +2 -0
- package/.grok/skills/commit-push-deploy/SKILL.md +2 -0
- package/.grok/skills/commit-push-release/SKILL.md +2 -0
- package/.grok/skills/explain/SKILL.md +7 -13
- package/.grok/skills/google-workspace/SKILL.md +13 -0
- package/.grok/skills/handoff/SKILL.md +7 -11
- package/.grok/skills/implement-plan/SKILL.md +5 -109
- package/.grok/skills/listen/SKILL.md +15 -0
- package/.grok/skills/pickup/SKILL.md +6 -12
- package/.grok/skills/speak/SKILL.md +13 -0
- package/.grok/skills/tmp/SKILL.md +6 -10
- package/.grok/skills/write-plan/SKILL.md +5 -77
- package/.rulesync/hooks.jsonc +1 -1
- package/.rulesync/mcp-policy.jsonc +0 -2
- package/.rulesync/reference/INDEX.md +30 -20
- package/.rulesync/reference/browser.md +13 -6
- package/.rulesync/reference/cascade.md +6 -86
- package/.rulesync/reference/clasp.md +21 -0
- package/.rulesync/reference/google-workspace.md +79 -0
- package/.rulesync/reference/hooks.md +7 -11
- package/.rulesync/reference/implementation-format.md +113 -0
- package/.rulesync/reference/law.md +5 -3
- package/.rulesync/reference/listen.md +80 -0
- package/.rulesync/reference/map.md +39 -36
- package/.rulesync/reference/output.md +3 -51
- package/.rulesync/reference/plan-format.md +4 -2
- package/.rulesync/reference/refs-map.md +12 -5
- package/.rulesync/reference/rtk.md +3 -2
- package/.rulesync/reference/scope.md +37 -0
- package/.rulesync/reference/setup.md +27 -0
- package/.rulesync/reference/skills-map.md +13 -12
- package/.rulesync/reference/speak.md +59 -0
- package/.rulesync/rules/CLAUDE.md +3 -49
- package/.rulesync/rules/overview.md +9 -47
- package/.rulesync/skills/ask/SKILL.md +5 -12
- package/.rulesync/skills/auto/SKILL.md +5 -12
- package/.rulesync/skills/browser/SKILL.md +4 -8
- package/.rulesync/skills/clasp/SKILL.md +9 -0
- package/.rulesync/skills/commit/SKILL.md +3 -1
- package/.rulesync/skills/commit-push/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-deploy/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-release/SKILL.md +2 -0
- package/.rulesync/skills/explain/SKILL.md +7 -14
- package/.rulesync/skills/google-workspace/SKILL.md +11 -0
- package/.rulesync/skills/handoff/SKILL.md +7 -11
- package/.rulesync/skills/implement-plan/SKILL.md +5 -119
- package/.rulesync/skills/listen/SKILL.md +13 -0
- package/.rulesync/skills/pickup/SKILL.md +6 -12
- package/.rulesync/skills/speak/SKILL.md +13 -0
- package/.rulesync/skills/tmp/SKILL.md +6 -10
- package/.rulesync/skills/write-plan/SKILL.md +5 -84
- package/.rulesync/subagents/grunt.md +2 -24
- package/.rulesync/subagents/orchestrator.md +14 -59
- package/AGENTS.md +9 -47
- package/CHANGELOG.md +56 -0
- package/CLAUDE.md +1 -49
- package/README.md +88 -80
- package/cli/grunt.mjs +18 -2
- package/cli/grunt.test.ts +33 -0
- package/cli/init.mjs +103 -10
- package/cli/init.test.ts +147 -35
- package/cli/prompt.mjs +8 -0
- package/cli/prompt.test.ts +14 -0
- package/package.json +7 -2
- package/scripts/browser.mjs +98 -5
- package/scripts/doctor.mjs +82 -1
- package/scripts/emit-gemini.mjs +1 -1
- package/scripts/emit-mcp-policy.mjs +1 -1
- package/scripts/gate-fat-tools.mjs +223 -173
- package/scripts/google-workspace.mjs +713 -0
- package/scripts/grunt-job.mjs +453 -69
- package/scripts/listen.mjs +829 -0
- package/scripts/parse-need.mjs +21 -2
- package/scripts/persist-implementation.mjs +278 -0
- package/scripts/scrub-spawn-prompt.mjs +3 -50
- package/scripts/setup.mjs +746 -0
- package/scripts/speak.mjs +291 -0
- package/.agents/agents/implementer.md +0 -32
- package/.agents/agents/thinker.md +0 -29
- package/.agents/skills/cascade/SKILL.md +0 -28
- package/.agents/skills/parent/SKILL.md +0 -6
- package/.agents/skills/solo/SKILL.md +0 -25
- package/.claude/agents/implementer.md +0 -37
- package/.claude/agents/thinker.md +0 -36
- package/.claude/skills/cascade/SKILL.md +0 -28
- package/.claude/skills/parent/SKILL.md +0 -6
- package/.claude/skills/solo/SKILL.md +0 -25
- package/.codex/agents/implementer.toml +0 -26
- package/.codex/agents/thinker.toml +0 -26
- package/.grok/agents/implementer.md +0 -27
- package/.grok/agents/thinker.md +0 -27
- package/.grok/parent.md +0 -1
- package/.grok/roles/implementer.toml +0 -5
- package/.grok/roles/thinker.toml +0 -5
- package/.grok/skills/cascade/SKILL.md +0 -28
- package/.grok/skills/parent/SKILL.md +0 -6
- package/.grok/skills/solo/SKILL.md +0 -25
- package/.rulesync/grunt.config.jsonc +0 -7
- package/.rulesync/skills/cascade/SKILL.md +0 -28
- package/.rulesync/skills/parent/SKILL.md +0 -6
- package/.rulesync/skills/solo/SKILL.md +0 -25
- package/.rulesync/subagents/implementer.md +0 -49
- package/.rulesync/subagents/thinker.md +0 -49
- package/scripts/grunt-config.mjs +0 -50
package/scripts/doctor.mjs
CHANGED
|
@@ -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(
|
|
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) {
|
package/scripts/emit-gemini.mjs
CHANGED
|
@@ -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", "
|
|
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", "
|
|
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 =
|
|
@@ -4,13 +4,13 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Session Grep head_limit when the model omits one. */
|
|
8
8
|
export const DEFAULT_GREP_HEAD_LIMIT = 50;
|
|
9
|
-
/**
|
|
9
|
+
/** Session Read line limit when the model omits one. */
|
|
10
10
|
export const DEFAULT_READ_LIMIT = 200;
|
|
11
|
-
/**
|
|
11
|
+
/** Unknown non-grunt child Grep head_limit when omitted. */
|
|
12
12
|
export const CHILD_GREP_HEAD_LIMIT = 150;
|
|
13
|
-
/**
|
|
13
|
+
/** Unknown non-grunt child Read line limit when omitted. */
|
|
14
14
|
export const CHILD_READ_LIMIT = 400;
|
|
15
15
|
/** Deny if requested Grep head_limit or Read limit exceeds this. */
|
|
16
16
|
export const MAX_REQUEST_LIMIT = 500;
|
|
@@ -30,15 +30,13 @@ export const DENY_LOCKFILES = new Set([
|
|
|
30
30
|
"yarn.lock",
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
|
-
export const REASON_DENYLIST =
|
|
34
|
-
|
|
35
|
-
export const
|
|
36
|
-
|
|
37
|
-
export const
|
|
38
|
-
export const
|
|
39
|
-
"
|
|
40
|
-
export const REASON_THINKER_TOOLS = "need: grunt job:search query:…";
|
|
41
|
-
export const REASON_IMPLEMENTER_WRITE = "need: path not in spec/plan";
|
|
33
|
+
export const REASON_DENYLIST = "path denylist; use package API";
|
|
34
|
+
export const REASON_HEAD_LIMIT = "head_limit>500";
|
|
35
|
+
export const REASON_FILE_SIZE = "file >200KB";
|
|
36
|
+
export const REASON_IMPLEMENTER_BASH = "need: grunt job:exec|test query:…";
|
|
37
|
+
export const REASON_BASH_DUMP = REASON_IMPLEMENTER_BASH;
|
|
38
|
+
export const REASON_REREAD =
|
|
39
|
+
"already wrote this session; Read with offset+limit";
|
|
42
40
|
|
|
43
41
|
const READ_TOOLS = new Set(["read", "readfile"]);
|
|
44
42
|
const GREP_TOOLS = new Set(["grep", "grepsearch"]);
|
|
@@ -71,7 +69,36 @@ const BASH_WEB = /\b(curl|wget)\b/;
|
|
|
71
69
|
const BASH_TEST =
|
|
72
70
|
/\b(?:npm|pnpm|yarn|bun)\s+test\b|\bcargo\s+test\b|\bpytest\b|\bvitest\b|\bjest\b/;
|
|
73
71
|
export const SHELL_META = /[|&;`$(){}<>\n\r]/;
|
|
74
|
-
const GRUNT_JOB_FLAGS = new Set([
|
|
72
|
+
const GRUNT_JOB_FLAGS = new Set([
|
|
73
|
+
"--job",
|
|
74
|
+
"--query",
|
|
75
|
+
"--path",
|
|
76
|
+
"--glob",
|
|
77
|
+
"--cwd",
|
|
78
|
+
"--stash",
|
|
79
|
+
"--from",
|
|
80
|
+
"--to",
|
|
81
|
+
]);
|
|
82
|
+
const PRODUCT_ROOT_NAMES = new Set([
|
|
83
|
+
"README.md",
|
|
84
|
+
"CHANGELOG.md",
|
|
85
|
+
"AGENTS.md",
|
|
86
|
+
"CLAUDE.md",
|
|
87
|
+
"GEMINI.md",
|
|
88
|
+
"package.json",
|
|
89
|
+
"LICENSE",
|
|
90
|
+
]);
|
|
91
|
+
const PRODUCT_ROOT_DIRS = new Set([
|
|
92
|
+
"src",
|
|
93
|
+
"scripts",
|
|
94
|
+
".rulesync",
|
|
95
|
+
".grok",
|
|
96
|
+
".claude",
|
|
97
|
+
".agents",
|
|
98
|
+
"cli",
|
|
99
|
+
]);
|
|
100
|
+
const SCRATCH_ROOT_NAMES = new Set(["notes.md", "scratch.md", "output.txt"]);
|
|
101
|
+
const BASH_TREE = /\b(ls|tree)\b/;
|
|
75
102
|
|
|
76
103
|
export function eventKey(s) {
|
|
77
104
|
return String(s || "")
|
|
@@ -225,6 +252,104 @@ export function rewriteGruntScratchPath(filePath, workspaceRoot) {
|
|
|
225
252
|
return dest;
|
|
226
253
|
}
|
|
227
254
|
|
|
255
|
+
export function sessionIdOf(data) {
|
|
256
|
+
const sid = String(
|
|
257
|
+
(data && (data.sessionId || data.session_id)) || "",
|
|
258
|
+
).trim();
|
|
259
|
+
if (!sid || sid === "default") return "";
|
|
260
|
+
return sid;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function wroteListPath(workspaceRoot, sid) {
|
|
264
|
+
if (!workspaceRoot || !sid) return "";
|
|
265
|
+
return path.join(workspaceRoot, ".tmp", "grunt", "sessions", sid, "wrote.txt");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function recordWrote(workspaceRoot, sid, filePath) {
|
|
269
|
+
if (!workspaceRoot || !sid || !filePath) return;
|
|
270
|
+
const abs = path.isAbsolute(filePath)
|
|
271
|
+
? path.resolve(filePath)
|
|
272
|
+
: path.resolve(workspaceRoot, filePath);
|
|
273
|
+
const rel = path.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
274
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return;
|
|
275
|
+
const dest = wroteListPath(workspaceRoot, sid);
|
|
276
|
+
try {
|
|
277
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
let cur = "";
|
|
282
|
+
try {
|
|
283
|
+
cur = fs.readFileSync(dest, "utf8");
|
|
284
|
+
} catch {
|
|
285
|
+
cur = "";
|
|
286
|
+
}
|
|
287
|
+
const lines = cur.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
|
288
|
+
if (lines.includes(rel)) return;
|
|
289
|
+
lines.push(rel);
|
|
290
|
+
fs.writeFileSync(dest, lines.join("\n") + "\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function wasWroteThisSession(workspaceRoot, sid, filePath) {
|
|
294
|
+
if (!workspaceRoot || !sid || !filePath) return false;
|
|
295
|
+
const dest = wroteListPath(workspaceRoot, sid);
|
|
296
|
+
let cur = "";
|
|
297
|
+
try {
|
|
298
|
+
cur = fs.readFileSync(dest, "utf8");
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const abs = path.isAbsolute(filePath)
|
|
303
|
+
? path.resolve(filePath)
|
|
304
|
+
: path.resolve(workspaceRoot, filePath);
|
|
305
|
+
const rel = path.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
306
|
+
return cur.split(/\n/).some((l) => l.trim() === rel);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const SCRATCH_ROOT_RE = /^(dump|scratch|notes|output)(\.|$)/i;
|
|
310
|
+
|
|
311
|
+
export function rewriteScratchWrite(filePath, workspaceRoot) {
|
|
312
|
+
const existing = rewriteGruntScratchPath(filePath, workspaceRoot);
|
|
313
|
+
if (existing) return existing;
|
|
314
|
+
if (filePath == null || !workspaceRoot) return null;
|
|
315
|
+
const wsRoot = path.resolve(workspaceRoot);
|
|
316
|
+
const abs = path.isAbsolute(filePath)
|
|
317
|
+
? path.resolve(filePath)
|
|
318
|
+
: path.resolve(wsRoot, filePath);
|
|
319
|
+
const rel = path.relative(wsRoot, abs).replace(/\\/g, "/");
|
|
320
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
321
|
+
const segs = rel.split("/");
|
|
322
|
+
const base = segs[segs.length - 1] || "";
|
|
323
|
+
if (PRODUCT_ROOT_NAMES.has(base) && segs.length === 1) return null;
|
|
324
|
+
if (PRODUCT_ROOT_DIRS.has(segs[0])) return null;
|
|
325
|
+
if (rel.startsWith(".tmp/grunt/")) return null;
|
|
326
|
+
if (segs[0] === "tmp" || (segs[0] === ".tmp" && segs[1] !== "grunt")) {
|
|
327
|
+
const rest = segs[0] === "tmp" ? segs.slice(1) : segs.slice(2);
|
|
328
|
+
const dest = path.resolve(wsRoot, ".tmp", "grunt", ...rest);
|
|
329
|
+
const check = path.relative(path.resolve(wsRoot, ".tmp", "grunt"), dest);
|
|
330
|
+
if (!check || check.startsWith("..") || path.isAbsolute(check)) return null;
|
|
331
|
+
return dest;
|
|
332
|
+
}
|
|
333
|
+
if (
|
|
334
|
+
segs.length === 1 &&
|
|
335
|
+
(SCRATCH_ROOT_NAMES.has(base) ||
|
|
336
|
+
/\.tmp$/i.test(base) ||
|
|
337
|
+
SCRATCH_ROOT_RE.test(base))
|
|
338
|
+
) {
|
|
339
|
+
return path.resolve(wsRoot, ".tmp", "grunt", base);
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function quoteQuery(q) {
|
|
345
|
+
return `"${String(q).replace(/"/g, '\\"')}"`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function gruntJobCommand(workspaceRoot, args) {
|
|
349
|
+
const script = path.join(workspaceRoot, "scripts", "grunt-job.mjs");
|
|
350
|
+
return `node ${script} ${args}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
228
353
|
export function workspaceRootOf(data) {
|
|
229
354
|
return (
|
|
230
355
|
(data && (data.workspaceRoot || data.workspace_root || data.cwd)) ||
|
|
@@ -389,6 +514,7 @@ export function parseGruntJobCommand(command, workspaceRoot) {
|
|
|
389
514
|
if (!rest) return null;
|
|
390
515
|
let job = "";
|
|
391
516
|
let hasQuery = false;
|
|
517
|
+
let hasStash = false;
|
|
392
518
|
for (let j = 0; j < rest.length; j++) {
|
|
393
519
|
const a = rest[j];
|
|
394
520
|
let flag = a;
|
|
@@ -403,23 +529,27 @@ export function parseGruntJobCommand(command, workspaceRoot) {
|
|
|
403
529
|
job = String(inline ? a.slice(a.indexOf("=") + 1) : rest[j] || "").toLowerCase();
|
|
404
530
|
} else if (flag === "--query") {
|
|
405
531
|
hasQuery = true;
|
|
532
|
+
} else if (flag === "--stash") {
|
|
533
|
+
hasStash = true;
|
|
406
534
|
}
|
|
407
535
|
}
|
|
408
|
-
return { job, hasQuery };
|
|
536
|
+
return { job, hasQuery, hasStash };
|
|
409
537
|
}
|
|
410
538
|
|
|
411
539
|
export function isWorkspaceGruntJob(command, workspaceRoot, allowedJobs) {
|
|
412
540
|
const parsed = parseGruntJobCommand(command, workspaceRoot);
|
|
413
|
-
if (!parsed
|
|
414
|
-
const allow = allowedJobs || ["search", "exec", "test"];
|
|
415
|
-
|
|
541
|
+
if (!parsed) return false;
|
|
542
|
+
const allow = allowedJobs || ["search", "exec", "test", "slice", "fetch"];
|
|
543
|
+
if (!allow.includes(parsed.job)) return false;
|
|
544
|
+
if (parsed.job === "slice") return parsed.hasStash || parsed.hasQuery;
|
|
545
|
+
return parsed.hasQuery;
|
|
416
546
|
}
|
|
417
547
|
|
|
418
548
|
export function implementerBashReason(command, workspaceRoot) {
|
|
419
549
|
const cmd = String(command || "");
|
|
420
550
|
if (bashTargetsDenylist(cmd)) return REASON_DENYLIST;
|
|
421
551
|
if (alreadyRtk(cmd)) return null;
|
|
422
|
-
if (isWorkspaceGruntJob(cmd, workspaceRoot, ["search", "exec", "test"])) {
|
|
552
|
+
if (isWorkspaceGruntJob(cmd, workspaceRoot, ["search", "exec", "test", "slice", "fetch"])) {
|
|
423
553
|
return null;
|
|
424
554
|
}
|
|
425
555
|
const wsGrunt = isWorkspaceGruntJobScript(cmd, workspaceRoot);
|
|
@@ -430,125 +560,6 @@ export function implementerBashReason(command, workspaceRoot) {
|
|
|
430
560
|
return null;
|
|
431
561
|
}
|
|
432
562
|
|
|
433
|
-
function resolveWriteAbs(filePath, workspaceRoot) {
|
|
434
|
-
if (!filePath || typeof filePath !== "string") return null;
|
|
435
|
-
return path.isAbsolute(filePath)
|
|
436
|
-
? path.resolve(filePath)
|
|
437
|
-
: path.resolve(workspaceRoot, filePath);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function isUnderPlansDir(abs, workspaceRoot) {
|
|
441
|
-
if (!abs || !workspaceRoot) return false;
|
|
442
|
-
const dir = path.resolve(workspaceRoot, ".tmp", "grunt", "plans");
|
|
443
|
-
if (abs === dir) return true;
|
|
444
|
-
const rel = path.relative(dir, abs);
|
|
445
|
-
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function isUnsolicitedDocPath(abs) {
|
|
449
|
-
if (!abs) return false;
|
|
450
|
-
if (path.basename(abs) === "README.md") return true;
|
|
451
|
-
const segs = abs.replace(/\\/g, "/").split("/");
|
|
452
|
-
return segs.includes("docs") || segs.includes("examples");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function addPlanPath(set, raw, workspaceRoot) {
|
|
456
|
-
let s = String(raw || "").trim();
|
|
457
|
-
if (!s) return;
|
|
458
|
-
if (/^https?:\/\//i.test(s)) return;
|
|
459
|
-
s = s.replace(/[.,;:]+$/, "");
|
|
460
|
-
if (!s || /^https?:\/\//i.test(s)) return;
|
|
461
|
-
if (/^\/[A-Za-z][\w-]*$/.test(s)) return;
|
|
462
|
-
if (!s.includes("/") && !s.includes("\\") && !s.includes(".")) return;
|
|
463
|
-
const abs = resolveWriteAbs(s, workspaceRoot);
|
|
464
|
-
if (abs) set.add(abs);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function extractPlanPaths(text, workspaceRoot) {
|
|
468
|
-
const set = new Set();
|
|
469
|
-
const body = String(text || "");
|
|
470
|
-
for (const m of body.matchAll(/`([^`\n]+)`/g)) {
|
|
471
|
-
addPlanPath(set, m[1], workspaceRoot);
|
|
472
|
-
}
|
|
473
|
-
for (const m of body.matchAll(
|
|
474
|
-
/(^|[\s("'[=])(\/(?:[A-Za-z0-9._@+-]+\/)+[A-Za-z0-9._@+-]+)/g,
|
|
475
|
-
)) {
|
|
476
|
-
addPlanPath(set, m[2], workspaceRoot);
|
|
477
|
-
}
|
|
478
|
-
return set;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function inProgressStatus(text) {
|
|
482
|
-
const s = String(text || "");
|
|
483
|
-
if (!s.startsWith("---\n")) return false;
|
|
484
|
-
const end = s.indexOf("\n---\n", 4);
|
|
485
|
-
if (end === -1) return false;
|
|
486
|
-
return /^status:\s*in-progress\s*$/m.test(s.slice(4, end));
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function loadInProgressPlans(workspaceRoot) {
|
|
490
|
-
const dir = path.resolve(workspaceRoot, ".tmp", "grunt", "plans");
|
|
491
|
-
let names;
|
|
492
|
-
try {
|
|
493
|
-
names = fs.readdirSync(dir);
|
|
494
|
-
} catch {
|
|
495
|
-
return [];
|
|
496
|
-
}
|
|
497
|
-
const out = [];
|
|
498
|
-
for (const name of names) {
|
|
499
|
-
if (!name.endsWith(".md")) continue;
|
|
500
|
-
const abs = path.resolve(dir, name);
|
|
501
|
-
let text;
|
|
502
|
-
try {
|
|
503
|
-
text = fs.readFileSync(abs, "utf8");
|
|
504
|
-
} catch {
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
if (!inProgressStatus(text)) continue;
|
|
508
|
-
out.push({ abs, text });
|
|
509
|
-
}
|
|
510
|
-
return out;
|
|
511
|
-
}
|
|
512
|
-
|
|
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) {
|
|
527
|
-
const ws = path.resolve(workspaceRoot);
|
|
528
|
-
const plans = loadInProgressPlans(ws);
|
|
529
|
-
const allow = new Set();
|
|
530
|
-
for (const p of plans) {
|
|
531
|
-
allow.add(p.abs);
|
|
532
|
-
for (const extracted of extractPlanPaths(p.text, ws)) {
|
|
533
|
-
allow.add(extracted);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
const promptPaths = extractPlanPaths(specTextOf(data), ws);
|
|
537
|
-
for (const abs of promptPaths) allow.add(abs);
|
|
538
|
-
const restrictive = promptPaths.size > 0;
|
|
539
|
-
for (const key of WRITE_PATH_FIELDS) {
|
|
540
|
-
if (typeof input[key] !== "string" || !input[key]) continue;
|
|
541
|
-
const abs = resolveWriteAbs(input[key], ws);
|
|
542
|
-
if (!abs) continue;
|
|
543
|
-
if (isUnderPlansDir(abs, ws)) continue;
|
|
544
|
-
if (allow.has(abs)) continue;
|
|
545
|
-
if (isUnsolicitedDocPath(abs) || restrictive) {
|
|
546
|
-
return { type: "deny", reason: REASON_IMPLEMENTER_WRITE };
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
563
|
/**
|
|
553
564
|
* @returns {null | { type: "deny", reason: string } | { type: "rewrite", updatedInput: object }}
|
|
554
565
|
*/
|
|
@@ -564,15 +575,19 @@ export function processFatTools(data) {
|
|
|
564
575
|
let changed = false;
|
|
565
576
|
for (const key of WRITE_PATH_FIELDS) {
|
|
566
577
|
if (typeof next[key] !== "string" || !next[key]) continue;
|
|
567
|
-
const dest =
|
|
578
|
+
const dest = rewriteScratchWrite(next[key], ws);
|
|
568
579
|
if (dest && dest !== next[key]) {
|
|
569
580
|
next[key] = dest;
|
|
570
581
|
changed = true;
|
|
571
582
|
}
|
|
572
583
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
584
|
+
const sid = sessionIdOf(data);
|
|
585
|
+
if (sid) {
|
|
586
|
+
for (const key of WRITE_PATH_FIELDS) {
|
|
587
|
+
if (typeof next[key] === "string" && next[key]) {
|
|
588
|
+
recordWrote(ws, sid, next[key]);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
576
591
|
}
|
|
577
592
|
return changed ? { type: "rewrite", updatedInput: next } : null;
|
|
578
593
|
}
|
|
@@ -585,10 +600,6 @@ export function processFatTools(data) {
|
|
|
585
600
|
const isGlob = GLOB_TOOLS.has(toolKey);
|
|
586
601
|
const isBash = BASH_TOOLS.has(toolKey);
|
|
587
602
|
|
|
588
|
-
if (sub === "thinker" && (isGrep || isGlob || isBash)) {
|
|
589
|
-
return { type: "deny", reason: REASON_THINKER_TOOLS };
|
|
590
|
-
}
|
|
591
|
-
|
|
592
603
|
if (isRead || isGrep || isGlob) {
|
|
593
604
|
for (const v of collectPathValues(input)) {
|
|
594
605
|
if (pathIsDenied(v)) return { type: "deny", reason: REASON_DENYLIST };
|
|
@@ -609,22 +620,24 @@ export function processFatTools(data) {
|
|
|
609
620
|
}
|
|
610
621
|
}
|
|
611
622
|
|
|
612
|
-
if (isGrep) {
|
|
613
|
-
const n = requestedLimit(input, "head_limit", "headLimit");
|
|
614
|
-
if (n != null && n > MAX_REQUEST_LIMIT) {
|
|
615
|
-
return { type: "deny", reason: REASON_HEAD_LIMIT };
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
623
|
if (isRead) {
|
|
619
|
-
const n = requestedLimit(input, "limit", "limit");
|
|
620
|
-
if (n != null && n > MAX_REQUEST_LIMIT) {
|
|
621
|
-
return { type: "deny", reason: REASON_HEAD_LIMIT };
|
|
622
|
-
}
|
|
623
624
|
const abs = resolveReadPath(readFilePath(input), data);
|
|
624
625
|
const size = fileSizeBytes(abs);
|
|
625
626
|
if (size != null && size > DENY_FILE_BYTES) {
|
|
626
627
|
return { type: "deny", reason: REASON_FILE_SIZE };
|
|
627
628
|
}
|
|
629
|
+
const sid = sessionIdOf(data);
|
|
630
|
+
const hasSlice =
|
|
631
|
+
hasLimitField(input, "offset", "offset") ||
|
|
632
|
+
hasLimitField(input, "limit", "limit");
|
|
633
|
+
if (
|
|
634
|
+
sid &&
|
|
635
|
+
abs &&
|
|
636
|
+
!hasSlice &&
|
|
637
|
+
wasWroteThisSession(workspaceRootOf(data), sid, abs)
|
|
638
|
+
) {
|
|
639
|
+
return { type: "deny", reason: REASON_REREAD };
|
|
640
|
+
}
|
|
628
641
|
}
|
|
629
642
|
|
|
630
643
|
const grepDefault = parent ? DEFAULT_GREP_HEAD_LIMIT : CHILD_GREP_HEAD_LIMIT;
|
|
@@ -632,24 +645,37 @@ export function processFatTools(data) {
|
|
|
632
645
|
const next = Object.assign({}, input);
|
|
633
646
|
let changed = false;
|
|
634
647
|
if (isGrep) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
648
|
+
const n = requestedLimit(input, "head_limit", "headLimit");
|
|
649
|
+
if (n != null && n > MAX_REQUEST_LIMIT) {
|
|
650
|
+
if (usesCamelInput(input)) next.headLimit = grepDefault;
|
|
651
|
+
else next.head_limit = grepDefault;
|
|
652
|
+
changed = true;
|
|
653
|
+
} else {
|
|
654
|
+
changed =
|
|
655
|
+
setMissingLimit(
|
|
656
|
+
next,
|
|
657
|
+
input,
|
|
658
|
+
"head_limit",
|
|
659
|
+
"headLimit",
|
|
660
|
+
grepDefault,
|
|
661
|
+
) || changed;
|
|
662
|
+
}
|
|
643
663
|
}
|
|
644
664
|
if (isRead) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
665
|
+
const n = requestedLimit(input, "limit", "limit");
|
|
666
|
+
if (n != null && n > MAX_REQUEST_LIMIT) {
|
|
667
|
+
next.limit = readDefault;
|
|
668
|
+
changed = true;
|
|
669
|
+
} else {
|
|
670
|
+
changed =
|
|
671
|
+
setMissingLimit(
|
|
672
|
+
next,
|
|
673
|
+
input,
|
|
674
|
+
"limit",
|
|
675
|
+
"limit",
|
|
676
|
+
readDefault,
|
|
677
|
+
) || changed;
|
|
678
|
+
}
|
|
653
679
|
}
|
|
654
680
|
if (isGlob) {
|
|
655
681
|
changed = setMissingIgnore(next, input) || changed;
|
|
@@ -662,8 +688,32 @@ export function processFatTools(data) {
|
|
|
662
688
|
if (bashTargetsDenylist(cmd)) {
|
|
663
689
|
return { type: "deny", reason: REASON_DENYLIST };
|
|
664
690
|
}
|
|
691
|
+
const ws = workspaceRootOf(data);
|
|
692
|
+
if (isWorkspaceGruntJobScript(cmd, ws)) return null;
|
|
693
|
+
const next = Object.assign({}, input);
|
|
694
|
+
if (BASH_WEB.test(cmd)) {
|
|
695
|
+
const url = (cmd.match(/https?:\/\/\S+/) || [])[0] || "";
|
|
696
|
+
if (url) {
|
|
697
|
+
next.command = gruntJobCommand(
|
|
698
|
+
ws,
|
|
699
|
+
`--job fetch --query ${quoteQuery(url)}`,
|
|
700
|
+
);
|
|
701
|
+
return { type: "rewrite", updatedInput: next };
|
|
702
|
+
}
|
|
703
|
+
return { type: "deny", reason: REASON_DENYLIST };
|
|
704
|
+
}
|
|
705
|
+
if (BASH_DUMP_CMDS.test(cmd) && !alreadyRtk(cmd)) {
|
|
706
|
+
const job = BASH_SEARCH.test(cmd) ? "search" : "exec";
|
|
707
|
+
const q = cmd.replace(/^\s*(rtk\s+)?/, "").trim();
|
|
708
|
+
next.command = gruntJobCommand(ws, `--job ${job} --query ${quoteQuery(q)}`);
|
|
709
|
+
return { type: "rewrite", updatedInput: next };
|
|
710
|
+
}
|
|
711
|
+
if (BASH_TREE.test(cmd) && !alreadyRtk(cmd)) {
|
|
712
|
+
next.command = `rtk ${cmd.trim()}`;
|
|
713
|
+
return { type: "rewrite", updatedInput: next };
|
|
714
|
+
}
|
|
665
715
|
if (sub === "implementer") {
|
|
666
|
-
const reason = implementerBashReason(cmd,
|
|
716
|
+
const reason = implementerBashReason(cmd, ws);
|
|
667
717
|
if (reason) return { type: "deny", reason };
|
|
668
718
|
}
|
|
669
719
|
return null;
|