@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.
- package/.agents/agents/grunt.md +2 -23
- package/.agents/agents/orchestrator.md +5 -45
- package/.agents/skills/ask/SKILL.md +11 -0
- package/.agents/skills/auto/SKILL.md +13 -0
- package/.agents/skills/browser/SKILL.md +4 -7
- 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 -108
- 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 +54 -0
- package/.agents/skills/write-plan/SKILL.md +5 -74
- package/.claude/agents/grunt.md +2 -25
- package/.claude/agents/orchestrator.md +12 -45
- package/.claude/skills/ask/SKILL.md +11 -0
- package/.claude/skills/auto/SKILL.md +13 -0
- package/.claude/skills/browser/SKILL.md +4 -7
- 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 -108
- 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 +54 -0
- package/.claude/skills/write-plan/SKILL.md +5 -74
- package/.codex/agents/grunt.toml +2 -25
- package/.codex/agents/orchestrator.toml +4 -45
- package/.grok/agents/grunt.md +2 -23
- package/.grok/agents/orchestrator.md +4 -43
- package/.grok/global-settings.toml +1 -3
- package/.grok/hooks/orchestrate-parent.js +278 -228
- package/.grok/skills/ask/SKILL.md +11 -0
- package/.grok/skills/auto/SKILL.md +13 -0
- package/.grok/skills/browser/SKILL.md +4 -7
- 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 -108
- 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 +54 -0
- package/.grok/skills/write-plan/SKILL.md +5 -74
- package/.rulesync/hooks.jsonc +1 -1
- package/.rulesync/mcp-policy.jsonc +0 -2
- package/.rulesync/reference/INDEX.md +30 -17
- package/.rulesync/reference/browser.md +13 -6
- package/.rulesync/reference/cascade.md +6 -84
- package/.rulesync/reference/clasp.md +21 -0
- package/.rulesync/reference/google-workspace.md +79 -0
- package/.rulesync/reference/hooks.md +7 -9
- 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 -31
- package/.rulesync/reference/output.md +3 -41
- package/.rulesync/reference/plan-format.md +14 -12
- 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 -9
- package/.rulesync/reference/speak.md +59 -0
- package/.rulesync/rules/CLAUDE.md +3 -44
- package/.rulesync/rules/overview.md +9 -42
- package/.rulesync/skills/ask/SKILL.md +11 -0
- package/.rulesync/skills/auto/SKILL.md +11 -0
- 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 -116
- 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 +54 -0
- package/.rulesync/skills/write-plan/SKILL.md +5 -80
- package/.rulesync/subagents/grunt.md +2 -23
- package/.rulesync/subagents/orchestrator.md +14 -54
- package/AGENTS.md +9 -42
- package/CHANGELOG.md +89 -0
- package/CLAUDE.md +1 -44
- package/README.md +97 -88
- package/cli/grunt.mjs +29 -13
- package/cli/grunt.test.ts +62 -20
- package/cli/init.mjs +218 -31
- package/cli/init.test.ts +669 -46
- package/cli/prompt.mjs +8 -0
- package/cli/prompt.test.ts +14 -0
- package/package.json +13 -1
- 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/persist-plan.mjs +17 -2
- package/scripts/persist-tmp.mjs +123 -0
- package/scripts/scrub-spawn-prompt.mjs +4 -51
- 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 -28
- package/.agents/skills/cascade/SKILL.md +0 -26
- package/.agents/skills/parent/SKILL.md +0 -6
- package/.agents/skills/solo/SKILL.md +0 -23
- package/.claude/agents/implementer.md +0 -37
- package/.claude/agents/thinker.md +0 -35
- package/.claude/skills/cascade/SKILL.md +0 -26
- package/.claude/skills/parent/SKILL.md +0 -6
- package/.claude/skills/solo/SKILL.md +0 -23
- package/.codex/agents/implementer.toml +0 -26
- package/.codex/agents/thinker.toml +0 -25
- package/.grok/agents/implementer.md +0 -27
- package/.grok/agents/thinker.md +0 -26
- 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 -26
- package/.grok/skills/parent/SKILL.md +0 -6
- package/.grok/skills/solo/SKILL.md +0 -23
- package/.rulesync/skills/cascade/SKILL.md +0 -26
- package/.rulesync/skills/parent/SKILL.md +0 -6
- package/.rulesync/skills/solo/SKILL.md +0 -23
- package/.rulesync/subagents/implementer.md +0 -49
- package/.rulesync/subagents/thinker.md +0 -48
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/** PreToolUse
|
|
3
|
-
|
|
4
|
-
SubagentStop intercepts need: search|exec with grunt-job verdicts.
|
|
5
|
-
Stop: first-non-empty-line [orchestrator]:/[grunt]:/[implementer]:/[thinker]:/[handoff]: recap
|
|
6
|
-
| parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
|
|
7
|
-
Empty lastAssistantMessage → transcript_path tail-scan. Fail-open: parse/crash → empty stdout, exit 0.
|
|
2
|
+
/** PreToolUse fat-gate + persist rewrite. Stop writes session receipt. SubagentStop intercepts need: search|exec|slice|fetch.
|
|
3
|
+
Fail-open: parse/crash → empty stdout, exit 0.
|
|
8
4
|
*/
|
|
9
5
|
import fs from "node:fs";
|
|
10
6
|
import path from "node:path";
|
|
@@ -17,33 +13,21 @@ import {
|
|
|
17
13
|
import {
|
|
18
14
|
denyResponse,
|
|
19
15
|
fatHookOutput,
|
|
20
|
-
isWorkspaceGruntJob,
|
|
21
|
-
isParentOrchestrator,
|
|
22
16
|
processFatTools,
|
|
23
17
|
rewriteGruntScratchPath,
|
|
24
|
-
subagentTypeOf,
|
|
25
18
|
} from "../../scripts/gate-fat-tools.mjs";
|
|
26
19
|
import { persistPlan } from "../../scripts/persist-plan.mjs";
|
|
27
20
|
import { persistHandoff } from "../../scripts/persist-handoff.mjs";
|
|
21
|
+
import { persistImplementation, isFullImplementationBody, validateImplementation } from "../../scripts/persist-implementation.mjs";
|
|
22
|
+
import { persistTmp } from "../../scripts/persist-tmp.mjs";
|
|
28
23
|
import { parseNeed } from "../../scripts/parse-need.mjs";
|
|
29
24
|
import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
|
|
30
25
|
|
|
31
|
-
export const ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export const
|
|
35
|
-
"Spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover 1./2. each on own line after.",
|
|
36
|
-
"Still spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover 1./2. each on own line after.",
|
|
37
|
-
"Last spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover 1./2. each on own line after.",
|
|
38
|
-
];
|
|
26
|
+
export const ORCHESTRATOR_LOGS_DIR = ".tmp/grunt/orchestrator-logs";
|
|
27
|
+
/** One-release dual-read; drop next release. */
|
|
28
|
+
export const LEGACY_ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
|
|
29
|
+
export const DENY_REASON = "denied";
|
|
39
30
|
const TRANSCRIPT_TAIL_BYTES = 512 * 1024;
|
|
40
|
-
const PARENT_TOOLS = new Set([
|
|
41
|
-
"todowrite",
|
|
42
|
-
"getcommandorsubagentoutput",
|
|
43
|
-
"gettaskoutput",
|
|
44
|
-
"killcommandorsubagent",
|
|
45
|
-
"killtask",
|
|
46
|
-
]);
|
|
47
31
|
const SPAWN_TOOLS = new Set([
|
|
48
32
|
"spawnsubagent",
|
|
49
33
|
"task",
|
|
@@ -56,26 +40,16 @@ const WRITE_TOOLS = new Set([
|
|
|
56
40
|
"searchreplace",
|
|
57
41
|
"replacefilecontent",
|
|
58
42
|
]);
|
|
59
|
-
const
|
|
60
|
-
const SKILL_TOOLS = new Set(["skill"]);
|
|
61
|
-
const PARENT_SKILLS = new Set([
|
|
62
|
-
"parent",
|
|
63
|
-
"explain",
|
|
64
|
-
"solo",
|
|
65
|
-
"cascade",
|
|
66
|
-
"handoff",
|
|
67
|
-
"pickup",
|
|
68
|
-
"write-plan",
|
|
69
|
-
"implement-plan",
|
|
70
|
-
]);
|
|
71
|
-
const INTERCEPT_JOBS = new Set(["search", "exec"]);
|
|
72
|
-
const MAX_STOP = 3;
|
|
43
|
+
const INTERCEPT_JOBS = new Set(["search", "exec", "slice", "fetch"]);
|
|
73
44
|
const MAX_INTERCEPT = 3;
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
45
|
+
const MAX_STOP = 3;
|
|
46
|
+
const AUTO_RE = /^\s*\/auto\s*$/;
|
|
47
|
+
const ASK_RE = /^\s*\/ask\s*$/;
|
|
48
|
+
export const SESSION_GATE_STAMP = "session-gate";
|
|
49
|
+
/** One-release dual-read of 0.5 leftover stamp. */
|
|
50
|
+
export const AUTO_ASK_STAMP = "auto-ask";
|
|
51
|
+
export const ASK_STOP_REASON = "ask after this step";
|
|
52
|
+
const WAIT_GRUNT = "[orchestrator]: wait grunt";
|
|
79
53
|
|
|
80
54
|
function main() {
|
|
81
55
|
try {
|
|
@@ -86,7 +60,6 @@ function main() {
|
|
|
86
60
|
"",
|
|
87
61
|
);
|
|
88
62
|
if (event === "pretooluse") return preToolUse(data || {});
|
|
89
|
-
if (event === "posttooluse") return postToolUse(data || {});
|
|
90
63
|
if (event === "userpromptsubmit") return userPromptSubmit(data || {});
|
|
91
64
|
if (event === "stop") return stop(data || {});
|
|
92
65
|
if (event === "subagentstop") return interceptNeed(data || {}, "SubagentStop");
|
|
@@ -102,18 +75,6 @@ function preToolUse(data) {
|
|
|
102
75
|
let toolInput = data.toolInput;
|
|
103
76
|
if (toolInput == null) toolInput = data.tool_input;
|
|
104
77
|
|
|
105
|
-
const sub = subagentTypeOf(data);
|
|
106
|
-
if (sub || !isParentOrchestrator(data)) {
|
|
107
|
-
const code = emitFat(data);
|
|
108
|
-
return code == null ? 0 : code;
|
|
109
|
-
}
|
|
110
|
-
if (isSoloMode(data)) {
|
|
111
|
-
// Single-agent session: no parent-deny, no spawn rewrite. Fat gate still applies.
|
|
112
|
-
const fatCode = emitFat(data);
|
|
113
|
-
if (fatCode !== null) return fatCode;
|
|
114
|
-
emit({ decision: "allow" });
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
117
78
|
if (SPAWN_TOOLS.has(toolKey)) {
|
|
118
79
|
const updated = rewriteSpawn(toolInput, data);
|
|
119
80
|
if (updated && updated.__denied) return 0;
|
|
@@ -130,45 +91,15 @@ function preToolUse(data) {
|
|
|
130
91
|
return 0;
|
|
131
92
|
}
|
|
132
93
|
if (WRITE_TOOLS.has(toolKey)) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (PARENT_TOOLS.has(toolKey)) {
|
|
136
|
-
emit({ decision: "allow" });
|
|
137
|
-
return 0;
|
|
138
|
-
}
|
|
139
|
-
if (BASH_TOOLS.has(toolKey) && isAllowedParentGruntJob(bashCommandOf(toolInput), workspaceRootOf(data))) {
|
|
140
|
-
emit({ decision: "allow" });
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
143
|
-
if (SKILL_TOOLS.has(toolKey) && isAllowedParentSkill(toolInput)) {
|
|
144
|
-
emit({ decision: "allow" });
|
|
145
|
-
return 0;
|
|
146
|
-
}
|
|
147
|
-
if (hasParentEscape(data)) {
|
|
148
|
-
const fatCode = emitFat(data);
|
|
149
|
-
if (fatCode !== null) return fatCode;
|
|
150
|
-
emit({ decision: "allow" });
|
|
151
|
-
return 0;
|
|
94
|
+
const persistCode = parentWrite(data, toolInput);
|
|
95
|
+
if (persistCode !== null) return persistCode;
|
|
152
96
|
}
|
|
153
|
-
|
|
97
|
+
const fatCode = emitFat(data);
|
|
98
|
+
if (fatCode !== null) return fatCode;
|
|
99
|
+
emit({ decision: "allow" });
|
|
154
100
|
return 0;
|
|
155
101
|
}
|
|
156
102
|
|
|
157
|
-
/** Session flag, not one-turn. Fail-closed: an unreadable stamp keeps grunt on. */
|
|
158
|
-
export function isSoloMode(data) {
|
|
159
|
-
try {
|
|
160
|
-
const p = soloStampPath(data);
|
|
161
|
-
return Boolean(p && fs.existsSync(p));
|
|
162
|
-
} catch {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function hasParentEscape(data) {
|
|
168
|
-
const p = stampPath(data, "parent-escape");
|
|
169
|
-
return Boolean(p && fs.existsSync(p));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
103
|
function emitFat(data) {
|
|
173
104
|
const fat = fatHookOutput(processFatTools(data));
|
|
174
105
|
if (!fat) return null;
|
|
@@ -198,49 +129,35 @@ function isUnderDir(filePath, workspaceRoot, segments) {
|
|
|
198
129
|
}
|
|
199
130
|
|
|
200
131
|
export function isUnderPlans(filePath, workspaceRoot) {
|
|
201
|
-
return isUnderDir(filePath, workspaceRoot, [".tmp", "plans"]);
|
|
132
|
+
return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "plans"]);
|
|
202
133
|
}
|
|
203
134
|
|
|
204
135
|
export function isUnderHandoffs(filePath, workspaceRoot) {
|
|
205
136
|
return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "handoffs"]);
|
|
206
137
|
}
|
|
207
138
|
|
|
208
|
-
export function
|
|
209
|
-
return
|
|
139
|
+
export function isUnderImplementations(filePath, workspaceRoot) {
|
|
140
|
+
return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "implementations"]);
|
|
210
141
|
}
|
|
211
142
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return "";
|
|
222
|
-
}
|
|
223
|
-
const raw =
|
|
224
|
-
toolInput.skill ??
|
|
225
|
-
toolInput.skill_name ??
|
|
226
|
-
toolInput.skillName ??
|
|
227
|
-
toolInput.name ??
|
|
228
|
-
"";
|
|
229
|
-
const s = String(raw).trim().toLowerCase().replace(/\\/g, "/");
|
|
230
|
-
const base = s.split("/").pop() || "";
|
|
231
|
-
return base.replace(/\.md$/, "").replace(/_/g, "-");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function isAllowedParentSkill(toolInput) {
|
|
235
|
-
return PARENT_SKILLS.has(skillNameOf(toolInput));
|
|
236
|
-
}
|
|
143
|
+
export const TMP_RESERVED_DIRS = new Set([
|
|
144
|
+
"plans",
|
|
145
|
+
"handoffs",
|
|
146
|
+
"implementations",
|
|
147
|
+
"browser",
|
|
148
|
+
"orchestrator-logs",
|
|
149
|
+
"stash",
|
|
150
|
+
"sessions",
|
|
151
|
+
]);
|
|
237
152
|
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
153
|
+
export function isUnderTmp(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 root = path.resolve(workspaceRoot, ".tmp", "grunt");
|
|
159
|
+
if (path.dirname(abs) !== root) return false;
|
|
160
|
+
return !TMP_RESERVED_DIRS.has(path.basename(abs));
|
|
244
161
|
}
|
|
245
162
|
|
|
246
163
|
function parentWrite(data, toolInput) {
|
|
@@ -257,22 +174,59 @@ function parentWrite(data, toolInput) {
|
|
|
257
174
|
"";
|
|
258
175
|
const rewritten = rewriteGruntScratchPath(rawPath, ws);
|
|
259
176
|
if (rewritten) rawPath = rewritten;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
177
|
+
let persist;
|
|
178
|
+
let invalid = "invalid plan";
|
|
179
|
+
if (isUnderPlans(rawPath, ws)) {
|
|
180
|
+
persist = persistPlan;
|
|
181
|
+
invalid = "invalid plan";
|
|
182
|
+
} else if (isUnderHandoffs(rawPath, ws)) {
|
|
183
|
+
persist = persistHandoff;
|
|
184
|
+
invalid = "invalid handoff";
|
|
185
|
+
} else if (isUnderImplementations(rawPath, ws)) {
|
|
186
|
+
persist = persistImplementation;
|
|
187
|
+
invalid = "invalid implementation";
|
|
188
|
+
const abs = path.isAbsolute(rawPath)
|
|
189
|
+
? path.resolve(rawPath)
|
|
190
|
+
: path.resolve(ws, rawPath);
|
|
191
|
+
const content = typeof toolInput.content === "string" ? toolInput.content : "";
|
|
192
|
+
const named = /^\s*IMPL_NAME:/m.test(content);
|
|
193
|
+
let exists = false;
|
|
194
|
+
try {
|
|
195
|
+
exists = fs.statSync(abs).isFile();
|
|
196
|
+
} catch {
|
|
197
|
+
exists = false;
|
|
198
|
+
}
|
|
199
|
+
if (!named && exists) {
|
|
200
|
+
if (!isFullImplementationBody(content)) return null;
|
|
201
|
+
const errors = validateImplementation(path.basename(abs), content);
|
|
202
|
+
if (errors.length) {
|
|
203
|
+
emit({ decision: "deny", reason: errors.join("; ") || invalid });
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const next = Object.assign({}, toolInput, {
|
|
207
|
+
file_path: abs,
|
|
208
|
+
content,
|
|
209
|
+
});
|
|
210
|
+
emit({
|
|
211
|
+
hookSpecificOutput: {
|
|
212
|
+
hookEventName: "PreToolUse",
|
|
213
|
+
updatedInput: next,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
264
216
|
return 0;
|
|
265
217
|
}
|
|
266
|
-
|
|
267
|
-
|
|
218
|
+
} else if (isUnderTmp(rawPath, ws)) {
|
|
219
|
+
persist = persistTmp;
|
|
220
|
+
invalid = "invalid tmp";
|
|
221
|
+
} else {
|
|
222
|
+
return null;
|
|
268
223
|
}
|
|
269
224
|
const content = typeof toolInput.content === "string" ? toolInput.content : "";
|
|
270
|
-
const persist = handoff ? persistHandoff : persistPlan;
|
|
271
225
|
const result = persist({ workspaceRoot: ws, content });
|
|
272
226
|
if (!result.ok) {
|
|
273
227
|
emit({
|
|
274
228
|
decision: "deny",
|
|
275
|
-
reason: result.error ||
|
|
229
|
+
reason: result.error || invalid,
|
|
276
230
|
});
|
|
277
231
|
return 0;
|
|
278
232
|
}
|
|
@@ -290,7 +244,6 @@ function parentWrite(data, toolInput) {
|
|
|
290
244
|
}
|
|
291
245
|
|
|
292
246
|
function rewriteSpawn(toolInput, data) {
|
|
293
|
-
// Type default + intent-scrub in one updatedInput (last-wins with scrub-spawn).
|
|
294
247
|
const updated = rewriteSpawnToolInput(toolInput, { defaultGrunt: true });
|
|
295
248
|
const prompt =
|
|
296
249
|
updated && typeof updated.prompt === "string"
|
|
@@ -305,69 +258,93 @@ function rewriteSpawn(toolInput, data) {
|
|
|
305
258
|
return updated;
|
|
306
259
|
}
|
|
307
260
|
|
|
308
|
-
function postToolUse(data) {
|
|
309
|
-
if (data.subagentType || data.subagent_type) return 0;
|
|
310
|
-
const stamp = stampPath(data, "tools-used");
|
|
311
|
-
if (!stamp) return 0;
|
|
312
|
-
fs.mkdirSync(path.dirname(stamp), { recursive: true });
|
|
313
|
-
fs.writeFileSync(stamp, "1");
|
|
314
|
-
return 0;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
261
|
function userPromptOf(data) {
|
|
318
262
|
if (!data || typeof data !== "object") return "";
|
|
319
263
|
return String(data.prompt ?? data.userPrompt ?? data.user_prompt ?? data.content ?? "");
|
|
320
264
|
}
|
|
321
265
|
|
|
322
|
-
function
|
|
323
|
-
|
|
266
|
+
function isHostStopBanner(prompt) {
|
|
267
|
+
const p = String(prompt || "");
|
|
268
|
+
if (/^\s*Stop hook feedback:/.test(p)) return true;
|
|
269
|
+
if (/Blocked by stop hook/.test(p)) return true;
|
|
270
|
+
if (/task[-_ ]?notification/i.test(p)) return true;
|
|
271
|
+
return false;
|
|
324
272
|
}
|
|
325
273
|
|
|
326
|
-
function
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (solo) {
|
|
336
|
-
fs.mkdirSync(path.dirname(solo), { recursive: true });
|
|
337
|
-
fs.writeFileSync(solo, "1");
|
|
338
|
-
}
|
|
339
|
-
} else if (CASCADE_RE.test(prompt)) {
|
|
340
|
-
unlinkQuiet(soloStampPath(data));
|
|
341
|
-
}
|
|
342
|
-
if (isParentEscapePrompt(prompt)) {
|
|
343
|
-
const p = stampPath(data, "parent-escape");
|
|
344
|
-
if (p) {
|
|
345
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
346
|
-
fs.writeFileSync(p, "1");
|
|
347
|
-
}
|
|
348
|
-
} else {
|
|
349
|
-
unlinkQuiet(stampPath(data, "parent-escape"));
|
|
274
|
+
function countLines(file) {
|
|
275
|
+
try {
|
|
276
|
+
return fs
|
|
277
|
+
.readFileSync(file, "utf8")
|
|
278
|
+
.split("\n")
|
|
279
|
+
.map((l) => l.trim())
|
|
280
|
+
.filter(Boolean).length;
|
|
281
|
+
} catch {
|
|
282
|
+
return 0;
|
|
350
283
|
}
|
|
351
|
-
return 0;
|
|
352
284
|
}
|
|
353
285
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
286
|
+
function sessionReceiptContext(data) {
|
|
287
|
+
const sid = sessionIdOf(data);
|
|
288
|
+
if (!sid || sid === "default") return "";
|
|
289
|
+
const dir = path.join(
|
|
290
|
+
workspaceRootOf(data),
|
|
291
|
+
".tmp",
|
|
292
|
+
"grunt",
|
|
293
|
+
"sessions",
|
|
294
|
+
sid,
|
|
295
|
+
);
|
|
296
|
+
if (!fs.existsSync(dir)) return "";
|
|
297
|
+
const wrote = countLines(path.join(dir, "wrote.txt"));
|
|
298
|
+
const read = countLines(path.join(dir, "read.txt"));
|
|
299
|
+
return `${wrote} wrote, ${read} read. session=.tmp/grunt/sessions/${sid}/`;
|
|
300
|
+
}
|
|
357
301
|
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
302
|
+
function writeSessionReceipt(data, msg) {
|
|
303
|
+
const sid = sessionIdOf(data);
|
|
304
|
+
if (!sid || sid === "default") return;
|
|
305
|
+
const dir = path.join(
|
|
306
|
+
workspaceRootOf(data),
|
|
307
|
+
".tmp",
|
|
308
|
+
"grunt",
|
|
309
|
+
"sessions",
|
|
310
|
+
sid,
|
|
311
|
+
);
|
|
312
|
+
try {
|
|
313
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
314
|
+
} catch {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const wrote = countLines(path.join(dir, "wrote.txt"));
|
|
318
|
+
const read = countLines(path.join(dir, "read.txt"));
|
|
319
|
+
const first = firstNonEmptyStripped(msg).slice(0, 200);
|
|
320
|
+
const lines = [
|
|
321
|
+
first,
|
|
322
|
+
`${wrote} wrote, ${read} read.`,
|
|
323
|
+
`session=.tmp/grunt/sessions/${sid}/`,
|
|
324
|
+
].filter(Boolean);
|
|
325
|
+
try {
|
|
326
|
+
fs.writeFileSync(path.join(dir, "receipt.txt"), lines.join("\n") + "\n");
|
|
327
|
+
} catch {
|
|
328
|
+
/* fail-open */
|
|
362
329
|
}
|
|
363
|
-
return "";
|
|
364
330
|
}
|
|
365
331
|
|
|
366
|
-
|
|
367
|
-
|
|
332
|
+
function userPromptSubmit(data) {
|
|
333
|
+
const prompt = userPromptOf(data);
|
|
334
|
+
if (isHostStopBanner(prompt)) return 0;
|
|
335
|
+
unlinkStamp(data, "stop-block");
|
|
336
|
+
applySessionGateSlash(data, prompt);
|
|
337
|
+
const ctx = effectiveGruntContext(data);
|
|
338
|
+
if (!ctx) return 0;
|
|
339
|
+
emit({
|
|
340
|
+
hookSpecificOutput: {
|
|
341
|
+
hookEventName: "UserPromptSubmit",
|
|
342
|
+
additionalContext: ctx,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
return 0;
|
|
368
346
|
}
|
|
369
347
|
|
|
370
|
-
/** Mid-turn wait must be exactly `[orchestrator]: wait grunt` (no leftover lines). */
|
|
371
348
|
export function isWaitGruntExact(msg) {
|
|
372
349
|
const nonempty = String(msg || "")
|
|
373
350
|
.split("\n")
|
|
@@ -376,6 +353,23 @@ export function isWaitGruntExact(msg) {
|
|
|
376
353
|
return nonempty[0].replace(/^[\s`*_>]+/, "") === WAIT_GRUNT;
|
|
377
354
|
}
|
|
378
355
|
|
|
356
|
+
export function hasStepAsk(msg) {
|
|
357
|
+
if (isWaitGruntExact(msg)) return true;
|
|
358
|
+
return String(msg || "")
|
|
359
|
+
.split("\n")
|
|
360
|
+
.map((l) => l.replace(/^[\s`*_>]+/, "").trim())
|
|
361
|
+
.filter(Boolean)
|
|
362
|
+
.some((l) => l.includes("?"));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function firstNonEmptyStripped(msg) {
|
|
366
|
+
for (const line of String(msg || "").split("\n")) {
|
|
367
|
+
if (!line.trim()) continue;
|
|
368
|
+
return line.replace(/^[\s`*_>]+/, "");
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
|
|
379
373
|
function payloadAssistantMessage(data) {
|
|
380
374
|
if (!data || typeof data !== "object") return "";
|
|
381
375
|
const camel = data.lastAssistantMessage;
|
|
@@ -463,44 +457,21 @@ function stop(data) {
|
|
|
463
457
|
const reason = String(data.reason || "");
|
|
464
458
|
if (reason && reason !== "end_turn") return 0;
|
|
465
459
|
|
|
466
|
-
// Before the parent-escape consume: solo must never burn the one-turn stamp.
|
|
467
|
-
if (isSoloMode(data)) return 0;
|
|
468
|
-
|
|
469
|
-
const escapeStamp = stampPath(data, "parent-escape");
|
|
470
|
-
if (escapeStamp && fs.existsSync(escapeStamp)) {
|
|
471
|
-
unlinkQuiet(escapeStamp);
|
|
472
|
-
return 0;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
460
|
const payloadMsg = payloadAssistantMessage(data);
|
|
476
461
|
let msg = payloadMsg;
|
|
477
462
|
if (!msg) {
|
|
478
463
|
const tp = data.transcript_path || data.transcriptPath || "";
|
|
479
464
|
msg = lastAssistantFromTranscript(tp);
|
|
480
465
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const stopStamp = stampPath(data, "stop-block");
|
|
490
|
-
let n = 0;
|
|
491
|
-
if (stopStamp && fs.existsSync(stopStamp)) {
|
|
492
|
-
n = parseInt(fs.readFileSync(stopStamp, "utf8"), 10);
|
|
493
|
-
if (!Number.isFinite(n) || n < 0) n = 0;
|
|
494
|
-
}
|
|
495
|
-
if (n >= MAX_STOP) {
|
|
466
|
+
writeSessionReceipt(data, msg);
|
|
467
|
+
if (isWaitGruntExact(msg)) return 0;
|
|
468
|
+
if (sessionGateOf(data) === "ask" && !hasStepAsk(msg)) {
|
|
469
|
+
let n = readStampInt(data, "stop-block");
|
|
470
|
+
if (n >= MAX_STOP) return 0;
|
|
471
|
+
writeStamp(data, "stop-block", String(n + 1));
|
|
472
|
+
emit({ decision: "block", reason: ASK_STOP_REASON });
|
|
496
473
|
return 0;
|
|
497
474
|
}
|
|
498
|
-
if (stopStamp) {
|
|
499
|
-
fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
|
|
500
|
-
fs.writeFileSync(stopStamp, String(n + 1));
|
|
501
|
-
}
|
|
502
|
-
const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
|
|
503
|
-
emit({ decision: "block", reason: reasonText });
|
|
504
475
|
return 0;
|
|
505
476
|
}
|
|
506
477
|
|
|
@@ -522,12 +493,7 @@ function interceptNeed(data, hookEventName) {
|
|
|
522
493
|
return 0;
|
|
523
494
|
}
|
|
524
495
|
|
|
525
|
-
|
|
526
|
-
let n = 0;
|
|
527
|
-
if (interceptStamp && fs.existsSync(interceptStamp)) {
|
|
528
|
-
n = parseInt(fs.readFileSync(interceptStamp, "utf8"), 10);
|
|
529
|
-
if (!Number.isFinite(n) || n < 0) n = 0;
|
|
530
|
-
}
|
|
496
|
+
let n = readStampInt(data, "need-intercept");
|
|
531
497
|
if (n >= MAX_INTERCEPT) {
|
|
532
498
|
return 0;
|
|
533
499
|
}
|
|
@@ -547,6 +513,9 @@ function interceptNeed(data, hookEventName) {
|
|
|
547
513
|
cwd: jobCwd || cwd,
|
|
548
514
|
path: job.path,
|
|
549
515
|
glob: job.glob,
|
|
516
|
+
stash: job.stash,
|
|
517
|
+
from: job.from,
|
|
518
|
+
to: job.to,
|
|
550
519
|
});
|
|
551
520
|
}
|
|
552
521
|
} catch {
|
|
@@ -557,10 +526,7 @@ function interceptNeed(data, hookEventName) {
|
|
|
557
526
|
}
|
|
558
527
|
parts.push(String(result.text || "").trimEnd());
|
|
559
528
|
}
|
|
560
|
-
|
|
561
|
-
fs.mkdirSync(path.dirname(interceptStamp), { recursive: true });
|
|
562
|
-
fs.writeFileSync(interceptStamp, String(n + 1));
|
|
563
|
-
}
|
|
529
|
+
writeStamp(data, "need-intercept", String(n + 1));
|
|
564
530
|
const reason = parts.join("\n");
|
|
565
531
|
emit({
|
|
566
532
|
decision: "block",
|
|
@@ -579,6 +545,37 @@ function stampPath(data, prefix) {
|
|
|
579
545
|
return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
|
|
580
546
|
}
|
|
581
547
|
|
|
548
|
+
function legacyStampPath(data, prefix) {
|
|
549
|
+
const root = workspaceRootOf(data);
|
|
550
|
+
const sid = sessionIdOf(data) || "default";
|
|
551
|
+
if (!root) return null;
|
|
552
|
+
return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function resolveStamp(data, prefix) {
|
|
556
|
+
const neu = stampPath(data, prefix);
|
|
557
|
+
if (neu && fs.existsSync(neu)) return neu;
|
|
558
|
+
const old = legacyStampPath(data, prefix);
|
|
559
|
+
if (old && fs.existsSync(old)) return old;
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function writeStamp(data, prefix, body) {
|
|
564
|
+
const p = stampPath(data, prefix);
|
|
565
|
+
if (!p) return null;
|
|
566
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
567
|
+
fs.writeFileSync(p, body);
|
|
568
|
+
return p;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function readStampInt(data, prefix) {
|
|
572
|
+
const p = resolveStamp(data, prefix);
|
|
573
|
+
if (!p) return 0;
|
|
574
|
+
const n = parseInt(fs.readFileSync(p, "utf8"), 10);
|
|
575
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
576
|
+
return n;
|
|
577
|
+
}
|
|
578
|
+
|
|
582
579
|
function sessionIdOf(data) {
|
|
583
580
|
return String(
|
|
584
581
|
process.env.GROK_SESSION_ID ||
|
|
@@ -587,21 +584,74 @@ function sessionIdOf(data) {
|
|
|
587
584
|
);
|
|
588
585
|
}
|
|
589
586
|
|
|
590
|
-
|
|
591
|
-
|
|
587
|
+
function unlinkQuiet(p) {
|
|
588
|
+
if (!p) return;
|
|
589
|
+
try {
|
|
590
|
+
fs.unlinkSync(p);
|
|
591
|
+
} catch {
|
|
592
|
+
/* missing is fine */
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function unlinkStamp(data, prefix) {
|
|
597
|
+
unlinkQuiet(stampPath(data, prefix));
|
|
598
|
+
unlinkQuiet(legacyStampPath(data, prefix));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function sessionGateStampPath(data, prefix) {
|
|
592
602
|
const root = workspaceRootOf(data);
|
|
593
603
|
const sid = sessionIdOf(data);
|
|
594
|
-
if (!root || !sid) return null;
|
|
595
|
-
return path.join(root, ORCHESTRATOR_LOGS_DIR,
|
|
604
|
+
if (!root || !sid || sid === "default") return null;
|
|
605
|
+
return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
|
|
596
606
|
}
|
|
597
607
|
|
|
598
|
-
function
|
|
608
|
+
function resolveSessionGateStamp(data) {
|
|
609
|
+
for (const prefix of [SESSION_GATE_STAMP, AUTO_ASK_STAMP]) {
|
|
610
|
+
const neu = sessionGateStampPath(data, prefix);
|
|
611
|
+
if (neu && fs.existsSync(neu)) return neu;
|
|
612
|
+
const old = path.join(
|
|
613
|
+
workspaceRootOf(data),
|
|
614
|
+
LEGACY_ORCHESTRATOR_LOGS_DIR,
|
|
615
|
+
prefix + "-" + sessionIdOf(data),
|
|
616
|
+
);
|
|
617
|
+
if (sessionIdOf(data) && fs.existsSync(old)) return old;
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function applySessionGateSlash(data, prompt) {
|
|
623
|
+
let slash = "";
|
|
624
|
+
if (AUTO_RE.test(prompt)) slash = "auto";
|
|
625
|
+
else if (ASK_RE.test(prompt)) slash = "ask";
|
|
626
|
+
if (!slash) return;
|
|
627
|
+
const p = sessionGateStampPath(data, SESSION_GATE_STAMP);
|
|
599
628
|
if (!p) return;
|
|
629
|
+
unlinkQuiet(sessionGateStampPath(data, AUTO_ASK_STAMP));
|
|
630
|
+
if (slash === "auto") {
|
|
631
|
+
unlinkQuiet(p);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
635
|
+
fs.writeFileSync(p, slash);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function sessionGateOf(data) {
|
|
600
639
|
try {
|
|
601
|
-
|
|
640
|
+
const p = resolveSessionGateStamp(data);
|
|
641
|
+
if (p) {
|
|
642
|
+
const body = fs.readFileSync(p, "utf8").trim();
|
|
643
|
+
if (body === "auto" || body === "ask") return body;
|
|
644
|
+
}
|
|
602
645
|
} catch {
|
|
603
|
-
|
|
646
|
+
/* fall through */
|
|
604
647
|
}
|
|
648
|
+
return "auto";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function effectiveGruntContext(data) {
|
|
652
|
+
const gate = sessionGateOf(data);
|
|
653
|
+
const receipt = sessionReceiptContext(data);
|
|
654
|
+
return receipt ? `sessionGate=${gate}. ${receipt}` : `sessionGate=${gate}`;
|
|
605
655
|
}
|
|
606
656
|
|
|
607
657
|
function eventKey(s) {
|