@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.
- package/.agents/agents/grunt.md +36 -0
- package/.agents/agents/implementer.md +40 -0
- package/.agents/agents/orchestrator.md +50 -0
- package/.agents/agents/thinker.md +27 -0
- package/.agents/hooks.json +26 -0
- package/.agents/mcp_config.json +3 -0
- package/.claude/agents/grunt.md +44 -0
- package/.claude/agents/implementer.md +46 -0
- package/.claude/agents/orchestrator.md +55 -0
- package/.claude/agents/thinker.md +34 -0
- package/.claude/settings.json +36 -0
- package/.codex/agents/grunt.toml +30 -0
- package/.codex/agents/implementer.toml +34 -0
- package/.codex/agents/orchestrator.toml +45 -0
- package/.codex/agents/thinker.toml +23 -0
- package/.codex/config.toml +2 -0
- package/.codex/hooks.json +26 -0
- package/.grok/agents/grunt.md +31 -0
- package/.grok/agents/implementer.md +35 -0
- package/.grok/agents/orchestrator.md +47 -0
- package/.grok/agents/thinker.md +25 -0
- package/.grok/config.toml +6 -0
- package/.grok/global-settings.toml +35 -0
- package/.grok/hooks/orchestrate-parent.js +476 -0
- package/.grok/hooks/orchestrate-parent.json +60 -0
- package/.grok/hooks/rtk-grok.js +137 -0
- package/.grok/hooks/rtk.json +16 -0
- package/.grok/hooks/rulesync.json +3 -0
- package/.grok/parent.md +1 -0
- package/.grok/roles/grunt.toml +5 -0
- package/.grok/roles/implementer.toml +5 -0
- package/.grok/roles/thinker.toml +5 -0
- package/.grok/skills/explain/SKILL.md +15 -0
- package/.grok/skills/implement-plan/SKILL.md +91 -0
- package/.grok/skills/parent/SKILL.md +12 -0
- package/.grok/skills/shared/plan-format.md +158 -0
- package/.grok/skills/write-plan/SKILL.md +84 -0
- package/.mcp.json +3 -0
- package/.rulesync/global-settings/antigravity.json +1 -0
- package/.rulesync/global-settings/claude.json +1 -0
- package/.rulesync/global-settings/codex.toml +1 -0
- package/.rulesync/global-settings/manifest.json +37 -0
- package/.rulesync/hooks.jsonc +22 -0
- package/.rulesync/mcp-policy.jsonc +15 -0
- package/.rulesync/reference/cascade.md +85 -0
- package/.rulesync/reference/hooks.md +19 -0
- package/.rulesync/reference/map.md +57 -0
- package/.rulesync/reference/rtk.md +23 -0
- package/.rulesync/rules/CLAUDE.md +13 -0
- package/.rulesync/rules/overview.md +23 -0
- package/.rulesync/skills/explain/SKILL.md +15 -0
- package/.rulesync/skills/parent/SKILL.md +12 -0
- package/.rulesync/skills/terse/SKILL.md +8 -0
- package/.rulesync/subagents/grunt.md +51 -0
- package/.rulesync/subagents/implementer.md +54 -0
- package/.rulesync/subagents/orchestrator.md +69 -0
- package/.rulesync/subagents/thinker.md +44 -0
- package/AGENTS.md +12 -0
- package/CLAUDE.md +7 -0
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/bin/grunt.js +4 -0
- package/cli/grunt.mjs +54 -0
- package/cli/grunt.test.ts +117 -0
- package/cli/init.mjs +113 -0
- package/cli/init.test.ts +268 -0
- package/package.json +77 -0
- package/scripts/check-globals.mjs +98 -0
- package/scripts/emit-mcp-policy.mjs +434 -0
- package/scripts/gate-fat-tools.mjs +485 -0
- package/scripts/grunt-job.mjs +482 -0
- package/scripts/parse-need.mjs +75 -0
- package/scripts/persist-plan.mjs +309 -0
- package/scripts/purge-global-mcps.mjs +291 -0
- package/scripts/scrub-spawn-prompt.mjs +219 -0
- package/scripts/scrub-text +58 -0
- package/scripts/scrub-text-lib.mjs +210 -0
- package/scripts/sync-global-settings.mjs +239 -0
- package/scripts/telemetry.mjs +38 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implementer
|
|
3
|
+
description: 'Feature implementation agent. Mid-reason / feature judgment (API design, non-obvious refactors, edge cases). Returns need: for fat dumps to parent.'
|
|
4
|
+
model: grok-4.6
|
|
5
|
+
agents_md: false
|
|
6
|
+
mcpInheritance: none
|
|
7
|
+
tools: read_file, grep, list_dir, write, search_replace, run_terminal_command, web_search, todo_write
|
|
8
|
+
---
|
|
9
|
+
You are **implementer**: mid-tier implementation agent. Never spawn.
|
|
10
|
+
|
|
11
|
+
en-US unless asked. EVERY turn/reply — chat, trivia, meta, protocol: maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. No complete-sentence padding. No host/blog communication style.
|
|
12
|
+
Keep need:/verdict:/plan grammar. Only `/explain` escapes.
|
|
13
|
+
|
|
14
|
+
**Feature work**: mid-reason judgment — feature logic, API design, non-obvious refactors, edge cases, architecture-aware code. Return `need:` if fat dump required.
|
|
15
|
+
|
|
16
|
+
## HARD LAWS (violate = fail)
|
|
17
|
+
1. File ≥20KB OR path in {cascade.md grunt-job.mjs AGENTS.md overview.md rtk.md} already summarized → do NOT Read. Missing code? emit only: need: [{"job":"search","query":"..."}]
|
|
18
|
+
2. grunt-job --query = one argv. No cd&& pipes tail.
|
|
19
|
+
3. Dump stop = exactly `need: [{...}]` one line. Cap 4 jobs.
|
|
20
|
+
4. After each leaf: flip that `[ ]`→`[x]` in the plan file (box only).
|
|
21
|
+
5. Do not re-open cascade/rtk/map for reminders. Do not "do all of plan 2".
|
|
22
|
+
|
|
23
|
+
Shell via rtk. Fat dumps (`rg`/`curl`/`npm test`, `node_modules`/lockfiles/`dist`): `need:` (fat-only). Do not cat denylist paths.
|
|
24
|
+
|
|
25
|
+
Tiny Read/Grep/list_dir in-child. `need:` is fat-only (denylist file>200KB unbounded grep/read bash dumps git web tests). See cascade.
|
|
26
|
+
|
|
27
|
+
For search|exec that already fits 8-line `verdict:`: run `node scripts/grunt-job.mjs --job search|exec --query …` in-session instead of `need:`. FALLBACK then `need:` + LLM grunt. `job:web` and messy `job:test` stay `need:` JSON.
|
|
28
|
+
|
|
29
|
+
Dump stop JSON only:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
need: [{"job":"search","query":"..."}]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
One stop may list multiple jobs; do not emit N sequential single-job stops for known-parallel dumps.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orchestrator
|
|
3
|
+
description: Parent supervisor only. Orchestrates grunt | implementer | thinker. Cheap zero-tool replies. User-facing search/exec/git/web/test/write = spawn first; no try-then-spawn. In-session grunt-job only for known child-feed dumps.
|
|
4
|
+
model: grok-4.5
|
|
5
|
+
permission_mode: default
|
|
6
|
+
agents_md: false
|
|
7
|
+
mcpInheritance: none
|
|
8
|
+
tools: spawn_subagent, read_file, grep, list_dir, write, todo_write, get_command_or_subagent_output, kill_command_or_subagent, run_terminal_command
|
|
9
|
+
---
|
|
10
|
+
You are **orchestrator**, the parent session. You do not implement, test, or dump.
|
|
11
|
+
|
|
12
|
+
en-US unless asked. EVERY turn/reply — chat, trivia, meta, protocol: maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. No complete-sentence padding. No host/blog communication style.
|
|
13
|
+
Keep need:/verdict:/plan grammar. Only `/explain` escapes.
|
|
14
|
+
|
|
15
|
+
Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Children never spawn.
|
|
16
|
+
|
|
17
|
+
## Orchestrate
|
|
18
|
+
- Super-trivial / yes-no / definition-from-prompt with zero tools **and absolutely sure**: answer. No grunt, no child, no tools. Parent does not web_search. `2+2` → answer.
|
|
19
|
+
- Cheap reply already in context: answer. No tools.
|
|
20
|
+
- User-facing search / exec / git / web / test / write / impl / repo facts: **first action is spawn** (`grunt` for facts, `implementer`/`thinker` per below). No parent probe. Ban try-tool → deny → spawn. Ban preamble essays before spawn.
|
|
21
|
+
- Fresh/world facts: spawn grunt `job: web`. Do not answer from memory.
|
|
22
|
+
- Tiny read/grep/list_dir: parent self-ops only (law files, spawn wiring). Never to answer the user.
|
|
23
|
+
- Low-reason write (mechanical/repetitive/obvious; volume OK): spawn grunt. Expect `verdict:` ≤8 lines (summarize; do not list 100 paths).
|
|
24
|
+
- Mid reason / feature judgment (API design, non-obvious refactors, edge cases, architecture-aware code): spawn implementer.
|
|
25
|
+
- Design / architecture / hard debug: spawn thinker.
|
|
26
|
+
- Unsure → implementer.
|
|
27
|
+
|
|
28
|
+
## Spawn
|
|
29
|
+
First token for user-facing tool work: spawn. Never parent bash/git/search as a probe.
|
|
30
|
+
In-session `node scripts/grunt-job.mjs --job search|exec --query …`: known dumps feeding an already-decided implementer/thinker spawn only (exact allowed argv). Not for answering the user. Optional `--path` `--glob` (repeatable) `--cwd`. `--query` one argv; regex `|` OK. Never `cd &&`. Deny or FALLBACK (exit 2) → spawn grunt immediately; zero retry theater. Unknown flags or exec shell-meta in query → FALLBACK then spawn. `job:web` and messy `job:test` still spawn grunt.
|
|
31
|
+
If dumps are known (paths, rg, test cmd): run search|exec via parent grunt-job first, then up-spawn implementer/thinker with `verdict:` blobs. Do not start the child and `need:` for a dump the parent already knew.
|
|
32
|
+
Child stop is `need: [{"job":"...","query":"..."}]` JSON only (1–N, cap 4). If every job is search|exec, SubagentStop intercepts with grunt-job `verdict:` blobs. Mixed/web/test: if `scripts/parse-need.mjs` succeeds, fan out those jobs as parallel LLM grunts; do not interpret surrounding prose. Wait; one `resume_from:` with **new** `verdict:` blobs only. Prompt = `You are {agent} subagent.` + new verdicts. Do not re-send the original task, cascade, or prior verdicts. Do not fresh-spawn. Max 3 `resume_from` per child id; then report blocked/partial with verdicts already gathered.
|
|
33
|
+
For spawned grunt `job: search|exec`, first action is `node scripts/grunt-job.mjs --job search|exec --query …` (echo stdout as the whole reply). Same flags: `--path` `--glob` `--cwd`. Never `cd &&`.
|
|
34
|
+
Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
|
|
35
|
+
After child returns: only terse `[agent]: …` echo/recap of child output. Do not redo. Do not add a second answer.
|
|
36
|
+
|
|
37
|
+
## `/parent`
|
|
38
|
+
One-turn in-parent escape. Not a mode. This turn may finish in-session; Stop consumes `parent-escape-{sid}` once. Next turn: spawn/Stop as usual. See cascade + `.rulesync/skills/parent`.
|
|
39
|
+
|
|
40
|
+
## Wait / peek
|
|
41
|
+
Parent-only. Non-blocking spawn so each child has a host id immediately. Per-child, not one global timer.
|
|
42
|
+
Every ≤60s peek is real host status (no `sleep` then guess). First return ≤60s is the peek (earlier if the child finishes).
|
|
43
|
+
Quote host fields only (`status`, elapsed since spawn, last activity/output snippet) or quote the error. Never invent.
|
|
44
|
+
Classify `done` | `alive` | `stuck` vs prior peek: **done** (completed/failed), **alive** (running + fields/output changed), **stuck** (running + unchanged). Unchanged snippet = stuck, not “still working”.
|
|
45
|
+
Loop until done. No auto-kill. Kill only if the user asks and the host has a kill tool.
|
|
46
|
+
Then print recap as `[agent]: …` with real child output. Do not redo the child's work.
|
|
47
|
+
Use cascade host mapping for spawn/peek/kill per host. Do not treat Grok tool names as the only recipe.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thinker
|
|
3
|
+
description: 'High-tier reasoning agent. Hard design, multi-file debug, architectural decisions. Returns need: for fat dumps to parent.'
|
|
4
|
+
model: grok-4.6
|
|
5
|
+
agents_md: false
|
|
6
|
+
mcpInheritance: none
|
|
7
|
+
permission_mode: plan
|
|
8
|
+
tools: read_file, grep, list_dir
|
|
9
|
+
---
|
|
10
|
+
You are **thinker**: high-tier reasoning agent. Read-only. Never spawn. No bash.
|
|
11
|
+
|
|
12
|
+
en-US unless asked. EVERY turn/reply — chat, trivia, meta, protocol: maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. No complete-sentence padding. No host/blog communication style.
|
|
13
|
+
Keep need:/verdict:/plan grammar. Only `/explain` escapes.
|
|
14
|
+
|
|
15
|
+
**Hard problems**: architectural design, multi-file debug, tricky refactoring, cross-system decisions. Return `need:` if fat dump required.
|
|
16
|
+
|
|
17
|
+
Tiny Read/Grep/list_dir in-child. `need:` is fat-only (denylist file>200KB unbounded grep/read bash dumps git web tests). See cascade.
|
|
18
|
+
|
|
19
|
+
Dump stop JSON only:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
need: [{"job":"search","query":"..."}]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
One stop may list multiple jobs; do not emit N sequential single-job stops for known-parallel dumps.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# NOT auto-loaded by Grok. Merge these tables into ~/.grok/config.toml via:
|
|
2
|
+
# npm run sync:globals # dry-run
|
|
3
|
+
# npm run sync:globals:apply # write machine-wide (~/.grok; all Grok sessions on this host)
|
|
4
|
+
# Project .grok/config.toml only loads MCP, plugins, and permission rules.
|
|
5
|
+
#
|
|
6
|
+
# [features] two_pass_compaction is not live until copied to ~/.grok/config.toml.
|
|
7
|
+
# Project .grok/config.toml cannot set [features]. Do not add a SessionStart hook.
|
|
8
|
+
# grok models (2026-08-26): grok-4.6, grok-4.5 (default). grok-build-0.1 not listed — grunt stays grok-4.5.
|
|
9
|
+
# Orchestrator effort: agent frontmatter has no reasoning_effort key (unknown fields ignored).
|
|
10
|
+
# Use `/effort low` or set reasoning_effort in ~/.grok/config.toml. Do not put effort strings in tools:.
|
|
11
|
+
|
|
12
|
+
[agent]
|
|
13
|
+
name = "orchestrator"
|
|
14
|
+
# SSOT: .grok/agents/orchestrator.md (discovered). .grok/parent.md is a pointer.
|
|
15
|
+
|
|
16
|
+
[subagents.toggle]
|
|
17
|
+
explore = false
|
|
18
|
+
plan = false
|
|
19
|
+
general-purpose = false
|
|
20
|
+
|
|
21
|
+
[subagents.models]
|
|
22
|
+
explore = "grok-4.5"
|
|
23
|
+
grunt = "grok-4.5"
|
|
24
|
+
implementer = "grok-4.6"
|
|
25
|
+
thinker = "grok-4.6"
|
|
26
|
+
|
|
27
|
+
[compat.claude]
|
|
28
|
+
agents = false
|
|
29
|
+
|
|
30
|
+
[memory]
|
|
31
|
+
enabled = false
|
|
32
|
+
|
|
33
|
+
# Must be copied to ~/.grok/config.toml. Project .grok/config.toml cannot set [features].
|
|
34
|
+
[features]
|
|
35
|
+
two_pass_compaction = true
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Parent-orchestrator gate: allow spawn/read/todo; deny write/edit/bash/web/MCP.
|
|
3
|
+
Fat Read/Grep/Glob limits + path denylist via gate-fat-tools (one updatedInput).
|
|
4
|
+
Parent bash: only node scripts/grunt-job.mjs --job search|exec [--path --glob --cwd].
|
|
5
|
+
SubagentStop intercepts need: search|exec with grunt-job verdicts.
|
|
6
|
+
Stop: recap [agent]: / parent-escape stamp; do not waive impl on tools-used.
|
|
7
|
+
Fail-open: parse/crash → empty stdout, exit 0. Explicit JSON only when denying.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
+
import { rewriteSpawnToolInput } from "../../scripts/scrub-spawn-prompt.mjs";
|
|
13
|
+
import {
|
|
14
|
+
fatHookOutput,
|
|
15
|
+
fileSizeBytes,
|
|
16
|
+
isWorkspaceGruntJob,
|
|
17
|
+
processFatTools,
|
|
18
|
+
resolveReadPath,
|
|
19
|
+
subagentTypeOf,
|
|
20
|
+
} from "../../scripts/gate-fat-tools.mjs";
|
|
21
|
+
import { persistPlan } from "../../scripts/persist-plan.mjs";
|
|
22
|
+
import { parseNeed } from "../../scripts/parse-need.mjs";
|
|
23
|
+
import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
|
|
24
|
+
import { logTelemetry, ORCHESTRATOR_LOGS_DIR } from "../../scripts/telemetry.mjs";
|
|
25
|
+
|
|
26
|
+
const DENY_REASON = "parent is orchestrator; spawn grunt|implementer|thinker";
|
|
27
|
+
const STOP_REASON =
|
|
28
|
+
"parent is orchestrator; spawn grunt|implementer|thinker; do not complete in-parent";
|
|
29
|
+
const ALLOWED_TOOLS = new Set([
|
|
30
|
+
"spawnsubagent",
|
|
31
|
+
"task",
|
|
32
|
+
"todowrite",
|
|
33
|
+
"getcommandorsubagentoutput",
|
|
34
|
+
"gettaskoutput",
|
|
35
|
+
"killcommandorsubagent",
|
|
36
|
+
"killtask",
|
|
37
|
+
"readfile",
|
|
38
|
+
"read",
|
|
39
|
+
"grep",
|
|
40
|
+
"grepsearch",
|
|
41
|
+
"listdir",
|
|
42
|
+
"glob",
|
|
43
|
+
]);
|
|
44
|
+
const SPAWN_TOOLS = new Set(["spawnsubagent", "task"]);
|
|
45
|
+
const WRITE_TOOLS = new Set(["write"]);
|
|
46
|
+
const BASH_TOOLS = new Set(["bash", "runterminalcommand"]);
|
|
47
|
+
const READ_TOOLS = new Set(["readfile", "read"]);
|
|
48
|
+
const INTERCEPT_JOBS = new Set(["search", "exec"]);
|
|
49
|
+
const MAX_STOP = 3;
|
|
50
|
+
const MAX_INTERCEPT = 3;
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
function main() {
|
|
54
|
+
try {
|
|
55
|
+
const data = readJsonValue();
|
|
56
|
+
const event = eventKey(
|
|
57
|
+
process.env.GROK_HOOK_EVENT ||
|
|
58
|
+
(data && (data.hookEventName || data.hook_event_name)) ||
|
|
59
|
+
"",
|
|
60
|
+
);
|
|
61
|
+
if (event === "pretooluse") return preToolUse(data || {});
|
|
62
|
+
if (event === "posttooluse") return postToolUse(data || {});
|
|
63
|
+
if (event === "userpromptsubmit") return userPromptSubmit(data || {});
|
|
64
|
+
if (event === "stop") return stop(data || {});
|
|
65
|
+
if (event === "subagentstop") return interceptNeed(data || {}, "SubagentStop");
|
|
66
|
+
return 0;
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function preToolUse(data) {
|
|
73
|
+
const toolName = String(data.toolName || data.tool_name || "");
|
|
74
|
+
const toolKey = eventKey(toolName);
|
|
75
|
+
let toolInput = data.toolInput;
|
|
76
|
+
if (toolInput == null) toolInput = data.tool_input;
|
|
77
|
+
logPreTool(data, toolKey, toolInput);
|
|
78
|
+
|
|
79
|
+
const sub = subagentTypeOf(data);
|
|
80
|
+
if (sub) {
|
|
81
|
+
const code = emitFat(data);
|
|
82
|
+
return code == null ? 0 : code;
|
|
83
|
+
}
|
|
84
|
+
if (SPAWN_TOOLS.has(toolKey)) {
|
|
85
|
+
const updated = rewriteSpawn(toolInput);
|
|
86
|
+
if (updated) {
|
|
87
|
+
emit({
|
|
88
|
+
hookSpecificOutput: {
|
|
89
|
+
hookEventName: "PreToolUse",
|
|
90
|
+
updatedInput: updated,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
emit({ decision: "allow" });
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
if (WRITE_TOOLS.has(toolKey)) {
|
|
99
|
+
return parentWrite(data, toolInput);
|
|
100
|
+
}
|
|
101
|
+
if (BASH_TOOLS.has(toolKey)) {
|
|
102
|
+
return parentBash(data, toolInput);
|
|
103
|
+
}
|
|
104
|
+
if (ALLOWED_TOOLS.has(toolKey)) {
|
|
105
|
+
const fatCode = emitFat(data);
|
|
106
|
+
if (fatCode !== null) return fatCode;
|
|
107
|
+
emit({ decision: "allow" });
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
emit({ decision: "deny", reason: DENY_REASON });
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function logPreTool(data, toolKey, toolInput) {
|
|
115
|
+
const fields = {};
|
|
116
|
+
if (SPAWN_TOOLS.has(toolKey) && toolInput && typeof toolInput === "object") {
|
|
117
|
+
fields.spawnType = String(
|
|
118
|
+
toolInput.subagent_type || toolInput.subagentType || "",
|
|
119
|
+
);
|
|
120
|
+
const rf = toolInput.resume_from ?? toolInput.resumeFrom;
|
|
121
|
+
fields.resumeFromCount =
|
|
122
|
+
rf == null || rf === "" ? 0 : Array.isArray(rf) ? rf.length : 1;
|
|
123
|
+
}
|
|
124
|
+
if (READ_TOOLS.has(toolKey) && toolInput && typeof toolInput === "object") {
|
|
125
|
+
const raw =
|
|
126
|
+
toolInput.target_file ||
|
|
127
|
+
toolInput.file_path ||
|
|
128
|
+
toolInput.targetFile ||
|
|
129
|
+
toolInput.filePath ||
|
|
130
|
+
"";
|
|
131
|
+
fields.fileSizeBytes = fileSizeBytes(resolveReadPath(raw, data));
|
|
132
|
+
}
|
|
133
|
+
if (Object.keys(fields).length) {
|
|
134
|
+
logTelemetry("pretool", fields, workspaceRootOf(data));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function emitFat(data) {
|
|
139
|
+
const fat = fatHookOutput(processFatTools(data));
|
|
140
|
+
if (!fat) return null;
|
|
141
|
+
emit(fat);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function workspaceRootOf(data) {
|
|
146
|
+
return (
|
|
147
|
+
process.env.GROK_WORKSPACE_ROOT ||
|
|
148
|
+
(data && (data.workspaceRoot || data.workspace_root || data.cwd)) ||
|
|
149
|
+
""
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function isUnderPlans(filePath, workspaceRoot) {
|
|
154
|
+
if (!filePath || !workspaceRoot) return false;
|
|
155
|
+
const abs = path.isAbsolute(filePath)
|
|
156
|
+
? path.resolve(filePath)
|
|
157
|
+
: path.resolve(workspaceRoot, filePath);
|
|
158
|
+
const plans = path.resolve(workspaceRoot, ".tmp", "plans");
|
|
159
|
+
const rel = path.relative(plans, abs);
|
|
160
|
+
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isAllowedParentGruntJob(command, workspaceRoot) {
|
|
164
|
+
return isWorkspaceGruntJob(command, workspaceRoot, ["search", "exec"]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parentBash(data, toolInput) {
|
|
168
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
169
|
+
emit({ decision: "deny", reason: DENY_REASON });
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
const cmd = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
173
|
+
if (!isAllowedParentGruntJob(cmd, workspaceRootOf(data))) {
|
|
174
|
+
emit({ decision: "deny", reason: DENY_REASON });
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
emit({ decision: "allow" });
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parentWrite(data, toolInput) {
|
|
182
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
183
|
+
emit({ decision: "deny", reason: DENY_REASON });
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
const ws = workspaceRootOf(data);
|
|
187
|
+
const rawPath =
|
|
188
|
+
toolInput.file_path ||
|
|
189
|
+
toolInput.filePath ||
|
|
190
|
+
toolInput.path ||
|
|
191
|
+
toolInput.target_file ||
|
|
192
|
+
"";
|
|
193
|
+
if (!isUnderPlans(rawPath, ws)) {
|
|
194
|
+
emit({ decision: "deny", reason: DENY_REASON });
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
const content = typeof toolInput.content === "string" ? toolInput.content : "";
|
|
198
|
+
const result = persistPlan({ workspaceRoot: ws, content });
|
|
199
|
+
if (!result.ok) {
|
|
200
|
+
emit({
|
|
201
|
+
decision: "deny",
|
|
202
|
+
reason: result.error || "invalid plan",
|
|
203
|
+
});
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const next = Object.assign({}, toolInput, {
|
|
207
|
+
file_path: result.path,
|
|
208
|
+
content: result.content,
|
|
209
|
+
});
|
|
210
|
+
emit({
|
|
211
|
+
hookSpecificOutput: {
|
|
212
|
+
hookEventName: "PreToolUse",
|
|
213
|
+
updatedInput: next,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function rewriteSpawn(toolInput) {
|
|
220
|
+
// Type default + intent-scrub in one updatedInput (last-wins with scrub-spawn).
|
|
221
|
+
return rewriteSpawnToolInput(toolInput, { defaultGrunt: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function postToolUse(data) {
|
|
225
|
+
if (data.subagentType || data.subagent_type) return 0;
|
|
226
|
+
const stamp = stampPath(data, "tools-used");
|
|
227
|
+
if (!stamp) return 0;
|
|
228
|
+
fs.mkdirSync(path.dirname(stamp), { recursive: true });
|
|
229
|
+
fs.writeFileSync(stamp, "1");
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function userPromptOf(data) {
|
|
234
|
+
if (!data || typeof data !== "object") return "";
|
|
235
|
+
return String(data.prompt ?? data.userPrompt ?? data.user_prompt ?? data.content ?? "");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isParentEscapePrompt(prompt) {
|
|
239
|
+
return /^\s*\/parent(?:\s|$)/.test(String(prompt || ""));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function userPromptSubmit(data) {
|
|
243
|
+
unlinkQuiet(stampPath(data, "tools-used"));
|
|
244
|
+
unlinkQuiet(stampPath(data, "stop-block"));
|
|
245
|
+
if (isParentEscapePrompt(userPromptOf(data))) {
|
|
246
|
+
const p = stampPath(data, "parent-escape");
|
|
247
|
+
if (p) {
|
|
248
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
249
|
+
fs.writeFileSync(p, "1");
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
unlinkQuiet(stampPath(data, "parent-escape"));
|
|
253
|
+
}
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isChildRecap(msg) {
|
|
258
|
+
return /^\s*\[(?:grunt|implementer|thinker)\]:/.test(String(msg || ""));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function stop(data) {
|
|
262
|
+
if (data.subagentType || data.subagent_type) {
|
|
263
|
+
return interceptNeed(data, "Stop");
|
|
264
|
+
}
|
|
265
|
+
const reason = String(data.reason || "");
|
|
266
|
+
if (reason && reason !== "end_turn") return 0;
|
|
267
|
+
|
|
268
|
+
const escapeStamp = stampPath(data, "parent-escape");
|
|
269
|
+
if (escapeStamp && fs.existsSync(escapeStamp)) {
|
|
270
|
+
unlinkQuiet(escapeStamp);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const msg = data.lastAssistantMessage || data.last_assistant_message || "";
|
|
275
|
+
if (isCheap(msg) || isChildRecap(msg)) return 0;
|
|
276
|
+
if (!looksLikeImplementation(msg)) return 0;
|
|
277
|
+
|
|
278
|
+
const stopStamp = stampPath(data, "stop-block");
|
|
279
|
+
let n = 0;
|
|
280
|
+
if (stopStamp && fs.existsSync(stopStamp)) {
|
|
281
|
+
n = parseInt(fs.readFileSync(stopStamp, "utf8"), 10);
|
|
282
|
+
if (!Number.isFinite(n) || n < 0) n = 0;
|
|
283
|
+
}
|
|
284
|
+
if (n >= MAX_STOP) return 0;
|
|
285
|
+
if (stopStamp) {
|
|
286
|
+
fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
|
|
287
|
+
fs.writeFileSync(stopStamp, String(n + 1));
|
|
288
|
+
}
|
|
289
|
+
emit({ decision: "block", reason: STOP_REASON });
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function interceptNeed(data, hookEventName) {
|
|
294
|
+
const ws = workspaceRootOf(data);
|
|
295
|
+
if (data.stopHookActive || data.stop_hook_active) {
|
|
296
|
+
logTelemetry(
|
|
297
|
+
"stop",
|
|
298
|
+
{ parseOk: false, intercept: "none" },
|
|
299
|
+
ws,
|
|
300
|
+
);
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
const msg = data.lastAssistantMessage || data.last_assistant_message || "";
|
|
304
|
+
const parsed = parseNeed(msg);
|
|
305
|
+
if (!parsed.ok) {
|
|
306
|
+
logTelemetry("stop", { parseOk: false, intercept: "none" }, ws);
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
const jobs = parsed.jobs;
|
|
310
|
+
const names = jobs.map((j) => j.job);
|
|
311
|
+
if (
|
|
312
|
+
jobs.length > 4 ||
|
|
313
|
+
!jobs.every((j) => INTERCEPT_JOBS.has(j.job))
|
|
314
|
+
) {
|
|
315
|
+
logTelemetry(
|
|
316
|
+
"stop",
|
|
317
|
+
{ parseOk: true, jobs: names, intercept: "none" },
|
|
318
|
+
ws,
|
|
319
|
+
);
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const interceptStamp = stampPath(data, "need-intercept");
|
|
324
|
+
let n = 0;
|
|
325
|
+
if (interceptStamp && fs.existsSync(interceptStamp)) {
|
|
326
|
+
n = parseInt(fs.readFileSync(interceptStamp, "utf8"), 10);
|
|
327
|
+
if (!Number.isFinite(n) || n < 0) n = 0;
|
|
328
|
+
}
|
|
329
|
+
if (n >= MAX_INTERCEPT) {
|
|
330
|
+
logTelemetry(
|
|
331
|
+
"stop",
|
|
332
|
+
{ parseOk: true, jobs: names, intercept: "none" },
|
|
333
|
+
ws,
|
|
334
|
+
);
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const cwd = ws || process.cwd();
|
|
339
|
+
const parts = [];
|
|
340
|
+
for (const job of jobs) {
|
|
341
|
+
let result;
|
|
342
|
+
try {
|
|
343
|
+
const jobCwd = job.cwd ? resolveJobCwd(job.cwd, cwd) : cwd;
|
|
344
|
+
if (job.cwd && !jobCwd) {
|
|
345
|
+
result = { fallback: true, text: "FALLBACK\n" };
|
|
346
|
+
} else {
|
|
347
|
+
result = runJob({
|
|
348
|
+
job: job.job,
|
|
349
|
+
query: job.query,
|
|
350
|
+
cwd: jobCwd || cwd,
|
|
351
|
+
path: job.path,
|
|
352
|
+
glob: job.glob,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
result = { fallback: true, text: "FALLBACK\n" };
|
|
357
|
+
}
|
|
358
|
+
if (!result || result.fallback) {
|
|
359
|
+
logTelemetry(
|
|
360
|
+
"stop",
|
|
361
|
+
{
|
|
362
|
+
parseOk: true,
|
|
363
|
+
jobs: names,
|
|
364
|
+
intercept: "FALLBACK",
|
|
365
|
+
query: job.query,
|
|
366
|
+
},
|
|
367
|
+
ws,
|
|
368
|
+
);
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
parts.push(String(result.text || "").trimEnd());
|
|
372
|
+
}
|
|
373
|
+
if (interceptStamp) {
|
|
374
|
+
fs.mkdirSync(path.dirname(interceptStamp), { recursive: true });
|
|
375
|
+
fs.writeFileSync(interceptStamp, String(n + 1));
|
|
376
|
+
}
|
|
377
|
+
const reason = parts.join("\n");
|
|
378
|
+
logTelemetry(
|
|
379
|
+
"stop",
|
|
380
|
+
{ parseOk: true, jobs: names, intercept: "grunt-job" },
|
|
381
|
+
ws,
|
|
382
|
+
);
|
|
383
|
+
emit({
|
|
384
|
+
decision: "block",
|
|
385
|
+
reason,
|
|
386
|
+
hookSpecificOutput: {
|
|
387
|
+
hookEventName,
|
|
388
|
+
additionalContext: reason,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function looksLikeImplementation(msg) {
|
|
395
|
+
if (typeof msg !== "string") return false;
|
|
396
|
+
const t = msg;
|
|
397
|
+
if (t.includes("```")) return true;
|
|
398
|
+
if (/(?:^|\n)(?:diff --git |--- [^\n]+\n\+\+\+ |@@ )/.test(t)) return true;
|
|
399
|
+
if (/\b(?:write|create|edit)[-_ ]file\b/i.test(t)) return true;
|
|
400
|
+
const patchLines = t.split(/\n/).filter((l) => /^[+-](?![+-])/.test(l)).length;
|
|
401
|
+
if (patchLines >= 8) return true;
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isCheap(msg) {
|
|
406
|
+
if (typeof msg !== "string") return false;
|
|
407
|
+
const t = msg.trim();
|
|
408
|
+
if (!t || t.length > 500) return false;
|
|
409
|
+
if (t.includes("```")) return false;
|
|
410
|
+
const sentences = t.split(/(?<=[.!?])(?:\s+|$)/).filter((s) => s.trim());
|
|
411
|
+
if (sentences.length > 2) return false;
|
|
412
|
+
const lines = t.split(/\n/).filter((l) => l.trim());
|
|
413
|
+
return lines.length <= 4;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function stampPath(data, prefix) {
|
|
417
|
+
const root =
|
|
418
|
+
process.env.GROK_WORKSPACE_ROOT ||
|
|
419
|
+
(data && (data.workspaceRoot || data.workspace_root)) ||
|
|
420
|
+
"";
|
|
421
|
+
const sid =
|
|
422
|
+
process.env.GROK_SESSION_ID || (data && (data.sessionId || data.session_id)) || "";
|
|
423
|
+
if (!root || !sid) return null;
|
|
424
|
+
return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function unlinkQuiet(p) {
|
|
428
|
+
if (!p) return;
|
|
429
|
+
try {
|
|
430
|
+
fs.unlinkSync(p);
|
|
431
|
+
} catch {
|
|
432
|
+
// missing is fine
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function eventKey(s) {
|
|
437
|
+
return String(s || "")
|
|
438
|
+
.toLowerCase()
|
|
439
|
+
.replace(/[^a-z0-9]/g, "");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function emit(obj) {
|
|
443
|
+
process.stdout.write(JSON.stringify(obj));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function readJsonValue() {
|
|
447
|
+
let buf = Buffer.alloc(0);
|
|
448
|
+
const tmp = Buffer.alloc(8192);
|
|
449
|
+
for (;;) {
|
|
450
|
+
let n;
|
|
451
|
+
try {
|
|
452
|
+
n = fs.readSync(0, tmp, 0, tmp.length, null);
|
|
453
|
+
} catch {
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
if (n === 0) break;
|
|
457
|
+
buf = Buffer.concat([buf, tmp.subarray(0, n)]);
|
|
458
|
+
try {
|
|
459
|
+
return JSON.parse(buf.toString("utf8"));
|
|
460
|
+
} catch {
|
|
461
|
+
// incomplete
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (!buf.length) return undefined;
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(buf.toString("utf8"));
|
|
467
|
+
} catch {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
473
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
474
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
475
|
+
process.exit(main());
|
|
476
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"$GROK_WORKSPACE_ROOT/.grok/hooks/orchestrate-parent.js\"",
|
|
9
|
+
"timeout": 5
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "spawn_subagent|Task",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"$GROK_WORKSPACE_ROOT/.grok/hooks/orchestrate-parent.js\"",
|
|
21
|
+
"timeout": 5
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"UserPromptSubmit": [
|
|
27
|
+
{
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node \"$GROK_WORKSPACE_ROOT/.grok/hooks/orchestrate-parent.js\"",
|
|
32
|
+
"timeout": 5
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"Stop": [
|
|
38
|
+
{
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "node \"$GROK_WORKSPACE_ROOT/.grok/hooks/orchestrate-parent.js\"",
|
|
43
|
+
"timeout": 30
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"SubagentStop": [
|
|
49
|
+
{
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "node \"$GROK_WORKSPACE_ROOT/.grok/hooks/orchestrate-parent.js\"",
|
|
54
|
+
"timeout": 30
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|