@lovrozagar/grunt 0.3.0 → 0.3.2

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.
@@ -20,7 +20,7 @@ You CAN run npm/git/bash via `run_terminal_command`. Do NOT wait on MCP. Do NOT
20
20
  Run tools. Return output. Never spawn. Never implement complex/feature solutions (implementer agent). Never plan (thinker agent).
21
21
  May write simple structural folders/files (scaffold/mkdir/touch/boilerplate only). Not feature solutions.
22
22
  Low-reason write: mechanical/repetitive/obvious; volume OK.
23
- Scratch/tmp → `.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
23
+ Scratch/tmp → `{workspaceRoot}/.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
24
24
 
25
25
  `job: search|exec` → first action `node scripts/grunt-job.mjs --job search|exec --query …`; whole reply = stdout.
26
26
  `job: test` → grunt-job; `FALLBACK` (exit 2) → LLM tools.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: orchestrator
3
- description: 'Parent spawn supervisor. Always spawn+prompt. Echo [agent]: only. No parent search/write except /parent.'
3
+ description: 'Parent spawn supervisor. Always spawn+prompt. Echo [orchestrator]: or child role tag only. No parent search/write except /parent.'
4
4
  tools:
5
5
  - view_file
6
6
  - grep_search
@@ -13,7 +13,7 @@ inheritMcp: false
13
13
  ---
14
14
  Voice: `.rulesync/reference/output.md` — must follow.
15
15
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
16
- You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[agent]:` + child output only.
16
+ You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
17
17
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
18
18
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
19
19
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  {
15
- "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command",
15
+ "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command|Write|Edit|write|search_replace",
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
@@ -10,7 +10,7 @@ Session mode, not one-turn. Persists until `/cascade` or session end.
10
10
  This session: cascade suspended. You are one normal agent.
11
11
 
12
12
  - Do not spawn. No grunt|implementer|thinker.
13
- - No `[agent]:` recap. Answer the user directly.
13
+ - No recap tags. Do not recap `[orchestrator]:`. Answer the user directly.
14
14
  - No spawn-first rule. No `need:`/`verdict:` grammar.
15
15
  - Voice unchanged: `.rulesync/reference/output.md`. `/explain` is still the voice escape.
16
16
 
@@ -26,7 +26,7 @@ You CAN run npm/git/bash via `Bash`. Do NOT wait on MCP. Do NOT request MCP for
26
26
  Run tools. Return output. Never spawn. Never implement complex/feature solutions (implementer agent). Never plan (thinker agent).
27
27
  May write simple structural folders/files (scaffold/mkdir/touch/boilerplate only). Not feature solutions.
28
28
  Low-reason write: mechanical/repetitive/obvious; volume OK.
29
- Scratch/tmp → `.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
29
+ Scratch/tmp → `{workspaceRoot}/.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
30
30
 
31
31
  `job: search|exec` → first action `node scripts/grunt-job.mjs --job search|exec --query …`; whole reply = stdout.
32
32
  `job: test` → grunt-job; `FALLBACK` (exit 2) → LLM tools.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: orchestrator
3
3
  description: >-
4
- Parent spawn supervisor. Always spawn+prompt. Echo [agent]: only. No parent
5
- search/write except /parent.
4
+ Parent spawn supervisor. Always spawn+prompt. Echo [orchestrator]: or child
5
+ role tag only. No parent search/write except /parent.
6
6
  model: haiku
7
7
  tools:
8
8
  - Read
@@ -16,7 +16,7 @@ effort: low
16
16
  ---
17
17
  Voice: `.rulesync/reference/output.md` — must follow.
18
18
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
19
- You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[agent]:` + child output only.
19
+ You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
20
20
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
21
21
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
22
22
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
@@ -21,7 +21,7 @@
21
21
  ]
22
22
  },
23
23
  {
24
- "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command",
24
+ "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command|Write|Edit|write|search_replace",
25
25
  "hooks": [
26
26
  {
27
27
  "type": "command",
@@ -10,7 +10,7 @@ Session mode, not one-turn. Persists until `/cascade` or session end.
10
10
  This session: cascade suspended. You are one normal agent.
11
11
 
12
12
  - Do not spawn. No grunt|implementer|thinker.
13
- - No `[agent]:` recap. Answer the user directly.
13
+ - No recap tags. Do not recap `[orchestrator]:`. Answer the user directly.
14
14
  - No spawn-first rule. No `need:`/`verdict:` grammar.
15
15
  - Voice unchanged: `.rulesync/reference/output.md`. `/explain` is still the voice escape.
16
16
 
@@ -13,7 +13,7 @@ You CAN run npm/git/bash via `run_terminal_command`. Do NOT wait on MCP. Do NOT
13
13
  Run tools. Return output. Never spawn. Never implement complex/feature solutions (implementer agent). Never plan (thinker agent).
14
14
  May write simple structural folders/files (scaffold/mkdir/touch/boilerplate only). Not feature solutions.
15
15
  Low-reason write: mechanical/repetitive/obvious; volume OK.
16
- Scratch/tmp → `.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
16
+ Scratch/tmp → `{workspaceRoot}/.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
17
17
 
18
18
  `job: search|exec` → first action `node scripts/grunt-job.mjs --job search|exec --query …`; whole reply = stdout.
19
19
  `job: test` → grunt-job; `FALLBACK` (exit 2) → LLM tools.
@@ -1,5 +1,5 @@
1
1
  name = "orchestrator"
2
- description = "Parent spawn supervisor. Always spawn+prompt. Echo [agent]: only. No parent search/write except /parent."
2
+ description = "Parent spawn supervisor. Always spawn+prompt. Echo [orchestrator]: or child role tag only. No parent search/write except /parent."
3
3
  model = "gpt-5.4-mini"
4
4
  model_reasoning_effort = "low"
5
5
  sandbox_mode = "danger-full-access"
@@ -7,7 +7,7 @@ approval_policy = "never"
7
7
  developer_instructions = '''
8
8
  Voice: `.rulesync/reference/output.md` — must follow.
9
9
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
10
- You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[agent]:` + child output only.
10
+ You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
11
11
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
12
12
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
13
13
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
package/.codex/hooks.json CHANGED
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  {
15
- "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command",
15
+ "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command|Write|Edit|write|search_replace",
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
@@ -15,7 +15,7 @@ You CAN run npm/git/bash via `run_terminal_command`. Do NOT wait on MCP. Do NOT
15
15
  Run tools. Return output. Never spawn. Never implement complex/feature solutions (implementer agent). Never plan (thinker agent).
16
16
  May write simple structural folders/files (scaffold/mkdir/touch/boilerplate only). Not feature solutions.
17
17
  Low-reason write: mechanical/repetitive/obvious; volume OK.
18
- Scratch/tmp → `.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
18
+ Scratch/tmp → `{workspaceRoot}/.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
19
19
 
20
20
  `job: search|exec` → first action `node scripts/grunt-job.mjs --job search|exec --query …`; whole reply = stdout.
21
21
  `job: test` → grunt-job; `FALLBACK` (exit 2) → LLM tools.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: orchestrator
3
- description: 'Parent spawn supervisor. Always spawn+prompt. Echo [agent]: only. No parent search/write except /parent.'
3
+ description: 'Parent spawn supervisor. Always spawn+prompt. Echo [orchestrator]: or child role tag only. No parent search/write except /parent.'
4
4
  model: grok-4.5
5
5
  permission_mode: bypassPermissions
6
6
  agents_md: false
@@ -9,7 +9,7 @@ tools: spawn_subagent, read_file, grep, list_dir, write, todo_write, get_command
9
9
  ---
10
10
  Voice: `.rulesync/reference/output.md` — must follow.
11
11
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
12
- You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[agent]:` + child output only.
12
+ You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
13
13
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
14
14
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
15
15
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
@@ -2,19 +2,26 @@
2
2
  /** Parent-orchestrator gate: spawn/peek/kill/todo + persistPlan/persistHandoff writes.
3
3
  Parent Read/Grep/Glob/Bash/Web denied unless parent-escape (fat-gate still).
4
4
  SubagentStop intercepts need: search|exec with grunt-job verdicts.
5
- Stop: [agent]:/[handoff]: recap | parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
6
- Fail-open: parse/crash → empty stdout, exit 0. Explicit JSON only when denying.
5
+ Stop: any-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.
7
8
  */
8
9
  import fs from "node:fs";
9
10
  import path from "node:path";
10
11
  import { fileURLToPath, pathToFileURL } from "node:url";
11
- import { rewriteSpawnToolInput } from "../../scripts/scrub-spawn-prompt.mjs";
12
12
  import {
13
+ MAX_PROMPT_CHARS,
14
+ rewriteSpawnToolInput,
15
+ spawnCapReason,
16
+ } from "../../scripts/scrub-spawn-prompt.mjs";
17
+ import {
18
+ denyResponse,
13
19
  fatHookOutput,
14
20
  fileSizeBytes,
15
21
  isWorkspaceGruntJob,
16
22
  processFatTools,
17
23
  resolveReadPath,
24
+ rewriteGruntScratchPath,
18
25
  subagentTypeOf,
19
26
  } from "../../scripts/gate-fat-tools.mjs";
20
27
  import { persistPlan } from "../../scripts/persist-plan.mjs";
@@ -24,26 +31,24 @@ import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
24
31
  import { logTelemetry, ORCHESTRATOR_LOGS_DIR } from "../../scripts/telemetry.mjs";
25
32
 
26
33
  const DENY_REASON = "parent is orchestrator; spawn grunt|implementer|thinker";
27
- const STOP_REASONS = [
28
- "Violation: parent replied without a [agent]: recap or /parent escape.\n" +
34
+ export const STOP_REASONS = [
35
+ "Violation: parent replied without a recap tag.\n" +
29
36
  "DO NOT stop. Continue IN THIS RESPONSE:\n" +
30
- "1. Spawn grunt|implementer|thinker for the pending work.\n" +
31
- "2. Reply with the `[agent]:` recap only.\n" +
32
- "If every spawned child already returned and the turn is done, reply with the `[agent]:` recap now.\n" +
33
- "If context is long, use /handoff.",
34
- "Second violation: still no [agent]: recap.\n" +
37
+ "1. Spawn grunt|implementer|thinker if work remains.\n" +
38
+ "2. Recap with `[orchestrator]:` or the child role tag (`[grunt]:` `[implementer]:` `[thinker]:` `[handoff]:`).\n" +
39
+ "If every spawned child already returned and the turn is done, recap now.",
40
+ "Second violation: still no recap tag.\n" +
35
41
  "DO NOT stop. Right now, in this same response:\n" +
36
- "1. Spawn grunt|implementer|thinker.\n" +
37
- "2. Then reply with only the `[agent]:` recap line.\n" +
38
- "Already done and every child returned? Send the `[agent]:` recap immediately.\n" +
39
- "Context too long? Use /handoff.",
42
+ "1. Spawn grunt|implementer|thinker if needed.\n" +
43
+ "2. Then recap only with `[orchestrator]:` or the matching child role tag.\n" +
44
+ "Already done and every child returned? Send that recap immediately.",
40
45
  "Third violation: recap still missing.\n" +
41
46
  "This is the last check before fail-open. DO NOT stop:\n" +
42
- "1. Spawn the correct agent for the remaining work.\n" +
43
- "2. Reply with `[agent]:` recap only, nothing else.\n" +
44
- "Complete with all children returned? Send the recap now.\n" +
45
- "Long context? Use /handoff.",
47
+ "1. Spawn the correct agent for remaining work if needed.\n" +
48
+ "2. Reply with `[orchestrator]:` or the child role tag only, nothing else.\n" +
49
+ "Complete with all children returned? Send the recap now.",
46
50
  ];
51
+ const TRANSCRIPT_TAIL_BYTES = 512 * 1024;
47
52
  const PARENT_TOOLS = new Set([
48
53
  "todowrite",
49
54
  "getcommandorsubagentoutput",
@@ -107,7 +112,8 @@ function preToolUse(data) {
107
112
  return 0;
108
113
  }
109
114
  if (SPAWN_TOOLS.has(toolKey)) {
110
- const updated = rewriteSpawn(toolInput);
115
+ const updated = rewriteSpawn(toolInput, data);
116
+ if (updated && updated.__denied) return 0;
111
117
  if (updated) {
112
118
  emit({
113
119
  hookSpecificOutput: {
@@ -222,12 +228,14 @@ function parentWrite(data, toolInput) {
222
228
  return 0;
223
229
  }
224
230
  const ws = workspaceRootOf(data);
225
- const rawPath =
231
+ let rawPath =
226
232
  toolInput.file_path ||
227
233
  toolInput.filePath ||
228
234
  toolInput.path ||
229
235
  toolInput.target_file ||
230
236
  "";
237
+ const rewritten = rewriteGruntScratchPath(rawPath, ws);
238
+ if (rewritten) rawPath = rewritten;
231
239
  const handoff = isUnderHandoffs(rawPath, ws);
232
240
  if (!handoff && !isUnderPlans(rawPath, ws)) {
233
241
  emit({ decision: "deny", reason: DENY_REASON });
@@ -256,9 +264,20 @@ function parentWrite(data, toolInput) {
256
264
  return 0;
257
265
  }
258
266
 
259
- function rewriteSpawn(toolInput) {
267
+ function rewriteSpawn(toolInput, data) {
260
268
  // Type default + intent-scrub in one updatedInput (last-wins with scrub-spawn).
261
- return rewriteSpawnToolInput(toolInput, { defaultGrunt: true });
269
+ const updated = rewriteSpawnToolInput(toolInput, { defaultGrunt: true });
270
+ const prompt =
271
+ updated && typeof updated.prompt === "string"
272
+ ? updated.prompt
273
+ : toolInput && typeof toolInput.prompt === "string"
274
+ ? toolInput.prompt
275
+ : "";
276
+ if (prompt.length > MAX_PROMPT_CHARS) {
277
+ emit(denyResponse(spawnCapReason(workspaceRootOf(data))));
278
+ return { __denied: true };
279
+ }
280
+ return updated;
262
281
  }
263
282
 
264
283
  function postToolUse(data) {
@@ -305,24 +324,113 @@ function userPromptSubmit(data) {
305
324
  return 0;
306
325
  }
307
326
 
308
- function isChildRecap(msg) {
327
+ const RECAP_TAG_RE =
328
+ /^\[(?:orchestrator|grunt|implementer|thinker|handoff)\]:/;
329
+
330
+ export function isRecap(msg) {
309
331
  const lines = String(msg || "").split("\n");
310
- let firstNonEmpty = "";
311
332
  for (const line of lines) {
312
- if (line.trim() !== "") {
313
- firstNonEmpty = line;
314
- break;
333
+ const stripped = line.replace(/^[\s`*_>]+/, "");
334
+ if (RECAP_TAG_RE.test(stripped)) return true;
335
+ }
336
+ return false;
337
+ }
338
+
339
+ function payloadAssistantMessage(data) {
340
+ if (!data || typeof data !== "object") return "";
341
+ const camel = data.lastAssistantMessage;
342
+ if (camel != null && String(camel) !== "") return String(camel);
343
+ const snake = data.last_assistant_message;
344
+ if (snake != null && String(snake) !== "") return String(snake);
345
+ return "";
346
+ }
347
+
348
+ function isAssistantRecord(obj) {
349
+ if (!obj || typeof obj !== "object") return false;
350
+ const type = String(obj.type || "").toLowerCase();
351
+ if (type === "user" || type === "human" || type === "tool" || type === "tool_result") {
352
+ return false;
353
+ }
354
+ if (type === "assistant") return true;
355
+ const role = String(
356
+ obj.role || (obj.message && obj.message.role) || "",
357
+ ).toLowerCase();
358
+ if (role === "user" || role === "human") return false;
359
+ return role === "assistant";
360
+ }
361
+
362
+ function assistantTextFromRecord(obj) {
363
+ if (!isAssistantRecord(obj)) return null;
364
+ const msg = obj.message && typeof obj.message === "object" ? obj.message : obj;
365
+ const content = msg.content ?? obj.content;
366
+ if (Array.isArray(content)) {
367
+ const texts = [];
368
+ for (const part of content) {
369
+ if (!part || typeof part !== "object") continue;
370
+ const pt = String(part.type || "");
371
+ if (pt === "tool_use" || pt === "tool_result" || pt === "function_call") continue;
372
+ if (typeof part.text === "string" && part.text) texts.push(part.text);
315
373
  }
374
+ const joined = texts.join("\n").trim();
375
+ return joined ? joined : null;
316
376
  }
317
- const stripped = firstNonEmpty.replace(/^[\s`*_>]+/, "");
318
- return /^\[(?:grunt|implementer|thinker|handoff)\]:/.test(stripped);
377
+ if (typeof content === "string" && content.trim()) return content;
378
+ if (typeof obj.text === "string" && obj.text.trim()) return obj.text;
379
+ return null;
380
+ }
381
+
382
+ export function lastAssistantFromTranscript(filePath) {
383
+ try {
384
+ if (!filePath) return "";
385
+ const st = fs.statSync(filePath);
386
+ const start = Math.max(0, st.size - TRANSCRIPT_TAIL_BYTES);
387
+ const fd = fs.openSync(filePath, "r");
388
+ try {
389
+ const buf = Buffer.alloc(st.size - start);
390
+ fs.readSync(fd, buf, 0, buf.length, start);
391
+ const text = buf.toString("utf8");
392
+ const lines = text.split("\n");
393
+ if (start > 0 && lines.length) lines.shift();
394
+ let last = "";
395
+ for (const line of lines) {
396
+ const t = line.trim();
397
+ if (!t) continue;
398
+ let rec;
399
+ try {
400
+ rec = JSON.parse(t);
401
+ } catch {
402
+ continue;
403
+ }
404
+ const extracted = assistantTextFromRecord(rec);
405
+ if (extracted) last = extracted;
406
+ }
407
+ return last;
408
+ } finally {
409
+ fs.closeSync(fd);
410
+ }
411
+ } catch {
412
+ return "";
413
+ }
414
+ }
415
+
416
+ function logParentStop(data, fields) {
417
+ logTelemetry("parent-stop", fields, workspaceRootOf(data));
319
418
  }
320
419
 
321
420
  function stop(data) {
322
421
  if (data.subagentType || data.subagent_type) {
323
422
  return interceptNeed(data, "Stop");
324
423
  }
325
- if (data.stopHookActive || data.stop_hook_active) return 0;
424
+ if (data.stopHookActive || data.stop_hook_active) {
425
+ logParentStop(data, {
426
+ recap: false,
427
+ recapSource: "none",
428
+ attempt: 0,
429
+ failOpen: true,
430
+ stopHookActive: true,
431
+ });
432
+ return 0;
433
+ }
326
434
  const reason = String(data.reason || "");
327
435
  if (reason && reason !== "end_turn") return 0;
328
436
 
@@ -335,8 +443,28 @@ function stop(data) {
335
443
  return 0;
336
444
  }
337
445
 
338
- const msg = data.lastAssistantMessage || data.last_assistant_message || "";
339
- if (isChildRecap(msg)) return 0;
446
+ const payloadMsg = payloadAssistantMessage(data);
447
+ let recapSource = "none";
448
+ let msg = "";
449
+ if (payloadMsg) {
450
+ msg = payloadMsg;
451
+ recapSource = "payload";
452
+ } else {
453
+ const tp = data.transcript_path || data.transcriptPath || "";
454
+ msg = lastAssistantFromTranscript(tp);
455
+ if (msg) recapSource = "transcript";
456
+ }
457
+
458
+ if (isRecap(msg)) {
459
+ logParentStop(data, {
460
+ recap: true,
461
+ recapSource,
462
+ attempt: 0,
463
+ failOpen: false,
464
+ stopHookActive: false,
465
+ });
466
+ return 0;
467
+ }
340
468
 
341
469
  const stopStamp = stampPath(data, "stop-block");
342
470
  let n = 0;
@@ -344,11 +472,27 @@ function stop(data) {
344
472
  n = parseInt(fs.readFileSync(stopStamp, "utf8"), 10);
345
473
  if (!Number.isFinite(n) || n < 0) n = 0;
346
474
  }
347
- if (n >= MAX_STOP) return 0;
475
+ if (n >= MAX_STOP) {
476
+ logParentStop(data, {
477
+ recap: false,
478
+ recapSource,
479
+ attempt: n,
480
+ failOpen: true,
481
+ stopHookActive: false,
482
+ });
483
+ return 0;
484
+ }
348
485
  if (stopStamp) {
349
486
  fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
350
487
  fs.writeFileSync(stopStamp, String(n + 1));
351
488
  }
489
+ logParentStop(data, {
490
+ recap: false,
491
+ recapSource,
492
+ attempt: n + 1,
493
+ failOpen: false,
494
+ stopHookActive: false,
495
+ });
352
496
  const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
353
497
  emit({ decision: "block", reason: reasonText });
354
498
  return 0;
@@ -10,7 +10,7 @@ Session mode, not one-turn. Persists until `/cascade` or session end.
10
10
  This session: cascade suspended. You are one normal agent.
11
11
 
12
12
  - Do not spawn. No grunt|implementer|thinker.
13
- - No `[agent]:` recap. Answer the user directly.
13
+ - No recap tags. Do not recap `[orchestrator]:`. Answer the user directly.
14
14
  - No spawn-first rule. No `need:`/`verdict:` grammar.
15
15
  - Voice unchanged: `.rulesync/reference/output.md`. `/explain` is still the voice escape.
16
16
 
@@ -11,8 +11,9 @@
11
11
  "timeout": 5
12
12
  },
13
13
  {
14
- // Fat Read/Grep/Glob/Bash: inject limits, path denylist, implementer dump deny.
15
- "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command",
14
+ // Fat Read/Grep/Glob/Bash + Write/Edit scratch rewrite (never deny writes).
15
+ // Parent-deny stays Grok-only (orchestrate-parent.js).
16
+ "matcher": "Read|read_file|Grep|grep|Glob|list_dir|Bash|run_terminal_command|Write|Edit|write|search_replace",
16
17
  "type": "command",
17
18
  "command": "node \"${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/scripts/gate-fat-tools.mjs\"",
18
19
  "timeout": 5
@@ -28,9 +28,9 @@ Every 60s peek is real host status (no `sleep` then guess). `timeout_ms=60000` e
28
28
  Quote host fields only (`status`, elapsed since spawn, last activity/output snippet) or quote the error. Never invent.
29
29
  Classify `done` | `alive` | `stuck` vs prior peek: **done** (completed/failed), **alive** (running + fields/output changed), **stuck** (running + unchanged). Unchanged snippet = stuck, not “still working”.
30
30
  Loop until done. No auto-kill. Kill only if the user asks and the host has a kill tool.
31
- Then print recap as `[agent]: …` with real child output. Do not redo the child's work.
31
+ Then print recap as `[orchestrator]: …` (or the child role tag) with real child output. Do not redo the child's work.
32
32
 
33
- Parent Stop (`MAX_STOP=3`): do not waive fenced/impl finals for `tools-used` stamp. Allow only `[grunt|implementer|thinker|handoff]:` recap prefix, or consume `parent-escape-{sid}` once. Else block. No `isCheap` / trivia / long definitions.
33
+ Parent Stop (`MAX_STOP=3`): do not waive fenced/impl finals for `tools-used` stamp. Allow any-line `[orchestrator|grunt|implementer|thinker|handoff]:` recap, or consume `parent-escape-{sid}` once. Else block. No `isCheap` / trivia / long definitions.
34
34
  `/handoff` is the other in-parent turn: context large + work unfinished → parent writes one `.tmp/grunt/handoffs/` file from its own transcript (no child sees the session), recaps `[handoff]: serial=… path=…`, tells the user to continue in a new session. No spawn, one write, not a mode.
35
35
  `/solo` is the session mode (not one-turn): `beforeSubmitPrompt` writes `.tmp/orchestrator-logs/grunt-off-{sid}`, every gate short-circuits while it exists, `/cascade` unlinks it. Everything above resumes next turn. Antigravity: skill-only, no stamp create (see hooks.md). `/parent` is one-turn (not a mode): UserPromptSubmit writes `.tmp/orchestrator-logs/parent-escape-{sid}`; Stop consumes+unlinks once. Next Stop resumes spawn/recap rules. Skills point here; do not paste this file.
36
36
 
@@ -12,7 +12,7 @@ Claude / Codex / Antigravity PreToolUse still run `scripts/scrub-spawn-prompt.mj
12
12
 
13
13
  Parent `run_terminal_command` is deny-by-default except `node` (optional leading `rtk`) invoking `{workspace}/scripts/grunt-job.mjs` with `--job search|exec` and `--query`. Optional `--path` `--glob` (repeatable) `--cwd`. Not `web|test`, not other scripts. META strips `--query` value first so `|` inside query is allowed; pipes/`&&` outside query still deny. Never `cd &&`. Exec shell-meta in query is grunt-job FALLBACK (exit 2), not a parent-bash allow. Unknown flags → FALLBACK.
14
14
 
15
- SubagentStop (and child Stop with `subagentType`) intercepts a parseable `need:` whose jobs are all `search|exec` (cap 4): runs grunt-job in-hook and continues the child with concatenated `verdict:` blobs. Mixed/web/test, parse fail, FALLBACK, `stopHookActive`, or more than 3 intercepts per child session → empty stdout (allow stop; parent LLM path). Parent `write` is deny-by-default except `.tmp/plans/` (`persist-plan.mjs`) and `.tmp/grunt/handoffs/` (`persist-handoff.mjs`); both validate + rewrite path/content, invalid body denies. Parent Stop (`MAX_STOP=3`): do not waive impl finals for `tools-used`. Allow only `[grunt|implementer|thinker|handoff]:` recap prefix, or consume `parent-escape-{sid}` once (`/parent`). No `isCheap` / trivia. Stamps: `tools-used` / `stop-block` / `need-intercept` / `parent-escape`. Skills point to cascade; do not paste protocol here.
15
+ SubagentStop (and child Stop with `subagentType`) intercepts a parseable `need:` whose jobs are all `search|exec` (cap 4): runs grunt-job in-hook and continues the child with concatenated `verdict:` blobs. Mixed/web/test, parse fail, FALLBACK, `stopHookActive`, or more than 3 intercepts per child session → empty stdout (allow stop; parent LLM path). Parent `write` is deny-by-default except `.tmp/plans/` (`persist-plan.mjs`) and `.tmp/grunt/handoffs/` (`persist-handoff.mjs`); both validate + rewrite path/content, invalid body denies. Parent Stop (`MAX_STOP=3`): do not waive impl finals for `tools-used`. Allow any-line `[orchestrator|grunt|implementer|thinker|handoff]:` recap, or consume `parent-escape-{sid}` once (`/parent`). No `isCheap` / trivia. Stamps: `tools-used` / `stop-block` / `need-intercept` / `parent-escape`. Skills point to cascade; do not paste protocol here.
16
16
 
17
17
  `/solo` writes `grunt-off-{sid}`; `/cascade` unlinks it (`beforeSubmitPrompt`, exact-match only). While set: PreToolUse allows parent tools (fat gate still runs) and skips the `defaultGrunt` spawn rewrite; Stop returns 0 **before** the `parent-escape` consume, so solo never burns the one-turn stamp. Session-scoped, fail-closed.
18
18
 
@@ -8,7 +8,7 @@ All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
8
8
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
9
9
  Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
10
10
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
11
- First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
11
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
12
12
  grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution. thinker ← plan/deep reason.
13
13
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
14
14
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. `timeout_ms=60000` every peek. GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
@@ -13,7 +13,7 @@ All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
13
13
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
14
14
  Parent = orchestrator. You do not talk. Spawn wait echo.
15
15
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
16
- First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
16
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
17
17
  grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution. thinker ← plan/deep reason.
18
18
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
19
19
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. `timeout_ms=60000` every peek. GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
@@ -10,7 +10,7 @@ Session mode, not one-turn. Persists until `/cascade` or session end.
10
10
  This session: cascade suspended. You are one normal agent.
11
11
 
12
12
  - Do not spawn. No grunt|implementer|thinker.
13
- - No `[agent]:` recap. Answer the user directly.
13
+ - No recap tags. Do not recap `[orchestrator]:`. Answer the user directly.
14
14
  - No spawn-first rule. No `need:`/`verdict:` grammar.
15
15
  - Voice unchanged: `.rulesync/reference/output.md`. `/explain` is still the voice escape.
16
16
 
@@ -38,7 +38,7 @@ You CAN run npm/git/bash via `run_terminal_command`. Do NOT wait on MCP. Do NOT
38
38
  Run tools. Return output. Never spawn. Never implement complex/feature solutions (implementer agent). Never plan (thinker agent).
39
39
  May write simple structural folders/files (scaffold/mkdir/touch/boilerplate only). Not feature solutions.
40
40
  Low-reason write: mechanical/repetitive/obvious; volume OK.
41
- Scratch/tmp → `.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
41
+ Scratch/tmp → `{workspaceRoot}/.tmp/grunt/` only (mkdir as needed). Never `.tmp/` root, `.tmp/plans/`, `.tmp/grunt/handoffs/`, or `.tmp/orchestrator-logs/`. Repo scaffold/mkdir/touch/boilerplate stays in the real tree.
42
42
 
43
43
  `job: search|exec` → first action `node scripts/grunt-job.mjs --job search|exec --query …`; whole reply = stdout.
44
44
  `job: test` → grunt-job; `FALLBACK` (exit 2) → LLM tools.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: orchestrator
3
- description: "Parent spawn supervisor. Always spawn+prompt. Echo [agent]: only. No parent search/write except /parent."
3
+ description: "Parent spawn supervisor. Always spawn+prompt. Echo [orchestrator]: or child role tag only. No parent search/write except /parent."
4
4
  targets:
5
5
  - claudecode
6
6
  - grokcli
@@ -34,7 +34,7 @@ geminicli:
34
34
  ---
35
35
  Voice: `.rulesync/reference/output.md` — must follow.
36
36
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
37
- You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[agent]:` + child output only.
37
+ You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
38
38
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
39
39
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
40
40
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
package/AGENTS.md CHANGED
@@ -3,7 +3,7 @@ All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
3
3
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
4
4
  Parent = orchestrator. You do not talk. Spawn wait echo.
5
5
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
6
- First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
6
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
7
7
  grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution. thinker ← plan/deep reason.
8
8
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
9
9
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. `timeout_ms=60000` every peek. GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
package/CLAUDE.md CHANGED
@@ -3,7 +3,7 @@ All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
3
3
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
4
4
  Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
5
5
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
6
- First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
6
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
7
7
  grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution. thinker ← plan/deep reason.
8
8
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
9
9
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. `timeout_ms=60000` every peek. GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
package/README.md CHANGED
@@ -13,8 +13,18 @@ npx @lovrozagar/grunt
13
13
 
14
14
  Same as `npx @lovrozagar/grunt init`. Merge SoT, `npm install`, `rulesync:generate`, `sync:globals:apply`, `rulesync:check`. Do not `npm test` as a consumer.
15
15
 
16
+ `--skip-globals` skips `sync:globals:apply`. Re-init auto-skips globals when `<!-- grunt:begin -->` is in AGENTS.md/CLAUDE.md or `scripts/telemetry.mjs` exists. First init (no sentinel, no telemetry) still applies globals unless flagged.
17
+
16
18
  Contributors to this repo only: local `npm i && npm test`.
17
19
 
20
+ # Version bump
21
+
22
+ ```
23
+ npm i -D @lovrozagar/grunt@latest && npx @lovrozagar/grunt init
24
+ ```
25
+
26
+ Owned trees/scripts refresh. Extra `.rulesync` files kept. Patches to grunt-owned files lost. Globals: first init applies; re-init auto-skips (or pass `--skip-globals`).
27
+
18
28
  # GOAL
19
29
 
20
30
  - synced configs across all 4/5 major cli providers (grok build, claude code, codex, gemini cli, antigravity)! Emit matrix: grok/claude/codex/antigravity (`rulesync generate`) then `emit-mcp-policy` + `emit-gemini` (`GEMINI.md`, `.gemini/agents/{id}/agent.md`; MCP `.gemini/settings.json`) + `emit-agent-shell-tools` (Claude grunt body `Bash`; other hosts `run_terminal_command`). No `-t geminicli`.
@@ -27,7 +37,7 @@ Contributors to this repo only: local `npm i && npm test`.
27
37
  context engineering
28
38
  - max reasoning and effort where needed and minimal reasoning and effort where not needed
29
39
  - agents:
30
- - orchestrator (low model, low effort) -> always spawn+prompt; never user-facing tokens except `[agent]:` echo. `/parent` one-turn only; `/handoff` writes `.tmp/grunt/handoffs/{serial}-{slug}-{stamp}.md` and hands the session to a fresh one.
40
+ - orchestrator (low model, low effort) -> always spawn+prompt; never user-facing tokens except `[orchestrator]:` (or child role tag) echo. `/parent` one-turn only; `/handoff` writes `.tmp/grunt/handoffs/{serial}-{slug}-{stamp}.md` and hands the session to a fresh one.
31
41
  - subagents:
32
42
  - grunt (low model, low effort) -> tools. facts/search/exec/git/web/test/low-reason mechanical write. never feature solution. never spawn.
33
43
  - implementer (medium model, medium effort) -> write already-defined solution. does not plan or invent design. never spawn.