@lovrozagar/grunt 0.4.2 → 0.5.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.
Files changed (96) hide show
  1. package/.agents/agents/grunt.md +2 -1
  2. package/.agents/agents/implementer.md +1 -1
  3. package/.agents/agents/orchestrator.md +19 -13
  4. package/.agents/agents/thinker.md +8 -6
  5. package/.agents/skills/ask/SKILL.md +18 -0
  6. package/.agents/skills/auto/SKILL.md +18 -0
  7. package/.agents/skills/browser/SKILL.md +4 -3
  8. package/.agents/skills/cascade/SKILL.md +5 -3
  9. package/.agents/skills/commit-and-push/SKILL.md +3 -3
  10. package/.agents/skills/explain/SKILL.md +1 -0
  11. package/.agents/skills/handoff/SKILL.md +1 -1
  12. package/.agents/skills/implement-plan/SKILL.md +117 -0
  13. package/.agents/skills/solo/SKILL.md +5 -3
  14. package/.agents/skills/tmp/SKILL.md +58 -0
  15. package/.agents/skills/write-plan/SKILL.md +85 -0
  16. package/.claude/agents/grunt.md +2 -1
  17. package/.claude/agents/implementer.md +1 -1
  18. package/.claude/agents/orchestrator.md +19 -13
  19. package/.claude/agents/thinker.md +8 -6
  20. package/.claude/skills/ask/SKILL.md +18 -0
  21. package/.claude/skills/auto/SKILL.md +18 -0
  22. package/.claude/skills/browser/SKILL.md +4 -3
  23. package/.claude/skills/cascade/SKILL.md +5 -3
  24. package/.claude/skills/commit-and-push/SKILL.md +3 -3
  25. package/.claude/skills/explain/SKILL.md +1 -0
  26. package/.claude/skills/handoff/SKILL.md +1 -1
  27. package/.claude/skills/implement-plan/SKILL.md +117 -0
  28. package/.claude/skills/solo/SKILL.md +5 -3
  29. package/.claude/skills/tmp/SKILL.md +58 -0
  30. package/.claude/skills/write-plan/SKILL.md +85 -0
  31. package/.codex/agents/grunt.toml +2 -1
  32. package/.codex/agents/implementer.toml +1 -1
  33. package/.codex/agents/orchestrator.toml +19 -13
  34. package/.codex/agents/thinker.toml +8 -6
  35. package/.grok/agents/grunt.md +2 -1
  36. package/.grok/agents/implementer.md +1 -1
  37. package/.grok/agents/orchestrator.md +19 -13
  38. package/.grok/agents/thinker.md +8 -6
  39. package/.grok/hooks/orchestrate-parent.js +280 -62
  40. package/.grok/skills/ask/SKILL.md +18 -0
  41. package/.grok/skills/auto/SKILL.md +18 -0
  42. package/.grok/skills/browser/SKILL.md +4 -3
  43. package/.grok/skills/cascade/SKILL.md +5 -3
  44. package/.grok/skills/commit-and-push/SKILL.md +3 -3
  45. package/.grok/skills/explain/SKILL.md +1 -0
  46. package/.grok/skills/handoff/SKILL.md +1 -1
  47. package/.grok/skills/implement-plan/SKILL.md +49 -20
  48. package/.grok/skills/solo/SKILL.md +5 -3
  49. package/.grok/skills/tmp/SKILL.md +58 -0
  50. package/.grok/skills/write-plan/SKILL.md +17 -14
  51. package/.rulesync/grunt.config.jsonc +7 -0
  52. package/.rulesync/reference/INDEX.md +46 -0
  53. package/.rulesync/reference/cascade.md +19 -15
  54. package/.rulesync/reference/hooks.md +6 -4
  55. package/.rulesync/reference/law.md +9 -0
  56. package/.rulesync/reference/map.md +20 -10
  57. package/.rulesync/reference/output.md +23 -8
  58. package/{.grok/skills/shared → .rulesync/reference}/plan-format.md +12 -11
  59. package/.rulesync/reference/refs-map.md +14 -0
  60. package/.rulesync/reference/skills-map.md +23 -0
  61. package/.rulesync/rules/CLAUDE.md +22 -15
  62. package/.rulesync/rules/overview.md +22 -15
  63. package/.rulesync/skills/ask/SKILL.md +18 -0
  64. package/.rulesync/skills/auto/SKILL.md +18 -0
  65. package/.rulesync/skills/browser/SKILL.md +2 -2
  66. package/.rulesync/skills/cascade/SKILL.md +5 -3
  67. package/.rulesync/skills/commit-and-push/SKILL.md +3 -3
  68. package/.rulesync/skills/explain/SKILL.md +1 -0
  69. package/.rulesync/skills/handoff/SKILL.md +1 -1
  70. package/.rulesync/skills/implement-plan/SKILL.md +127 -0
  71. package/.rulesync/skills/solo/SKILL.md +5 -3
  72. package/.rulesync/skills/tmp/SKILL.md +58 -0
  73. package/.rulesync/skills/write-plan/SKILL.md +92 -0
  74. package/.rulesync/subagents/grunt.md +2 -1
  75. package/.rulesync/subagents/implementer.md +1 -1
  76. package/.rulesync/subagents/orchestrator.md +19 -13
  77. package/.rulesync/subagents/thinker.md +8 -6
  78. package/AGENTS.md +22 -15
  79. package/CHANGELOG.md +56 -0
  80. package/CLAUDE.md +22 -15
  81. package/README.md +27 -22
  82. package/cli/grunt.mjs +12 -12
  83. package/cli/grunt.test.ts +29 -20
  84. package/cli/init.mjs +135 -22
  85. package/cli/init.test.ts +624 -36
  86. package/package.json +11 -4
  87. package/scripts/doctor.mjs +64 -2
  88. package/scripts/emit-maps.mjs +422 -0
  89. package/scripts/gate-fat-tools.mjs +2 -2
  90. package/scripts/grunt-config.mjs +50 -0
  91. package/scripts/guarded-roots.mjs +4 -7
  92. package/scripts/persist-plan.mjs +17 -2
  93. package/scripts/persist-tmp.mjs +123 -0
  94. package/scripts/pipeline.mjs +67 -0
  95. package/scripts/scrub-spawn-prompt.mjs +1 -1
  96. package/scripts/skill-conflicts.mjs +71 -0
@@ -8,13 +8,15 @@ developer_instructions = '''
8
8
  Voice: `.rulesync/reference/output.md` — must follow.
9
9
 
10
10
  Plan/advise/recommend/how/why/explain. Deep reason. Never spawn. Never implement (implementer agent). Never run for simple tool usage (grunt agent).
11
- Read-only. No bash. Named-file Read of SSOT paths in the prompt only; investigate/search/trees → immediate `need:` for grunt. Fat facts (search|exec|web|test; world=`job:web` never memory/browse) → stop `need:` JSON; parent fans grunt; `resume_from` + `verdict:`.
11
+ Read-only. No bash. Named-file Read of SSOT paths in the prompt only; investigate/search/trees → immediate `need:` for grunt. Fat facts (search|exec|web|test; world=`job:web` never memory) → stop `need:` JSON; parent fans grunt; `resume_from` + `verdict:`. Browse-vs-web advise only: INDEX `browser` row + `.rulesync/reference/browser.md`; route; do not browse. How-does-the-page-look → grunt not self.
12
12
  Return the spec/plan. Flag edge cases/pitfalls.
13
- First line = output.md recap. Plan/advice body after. Advice ≠ completed product work.
14
- Advise finals end with numbered pick:
15
- 1. Implement
16
- 2. Tweak
17
- Slash `/implement-plan` + explicit implement = Implement aliases. `ok`/`yes`/`y`/`continue` ≠ Implement.
13
+ First line = recap. Recap **must** be `{decided}. {how-capsule}. {why-clause}`. Why-clause = rejected alt + constraint (not dump not “because”). Labels optional (`Why:` / em-dash OK); do not mandate `## Why`. Body may expand How/Why/edges — not parent-echoed. Advice ≠ completed product work. `need:` JSON still JSON-only (no why mix).
14
+ Advise leftover: one empty blank line after recap/body then numbered pick each on own line. Always-print typed leftover triple (advise-class final recap only). One verb per recap; never jammed Implement/write. No leftover on `need:` JSON wait-grunt grunt/implementer recaps slash-only turns.
15
+ 1. {Implement|Write} with verbal plan
16
+ 2. {same verb} with file plan
17
+ 3. Tweak
18
+ Type Implement = in-repo product writes. Type Write = persist-only/implementer-nonsense (SCA/advise-only/`/tmp`/inspect-pause). Recap remainder still names the human gate; leftover is not `4. Contact bank`. `/handoff` stays slash not Write pick1. Print all three. No omit 1/2. No frozen 4. Do not relabel 3. Parent echoes printed leftover. Thinker does not spawn. Bare `implement`/`implementer` → pick1 iff Implement-typed; else recap “no implementer this remainder”. Bare `write` → pick1 iff Write-typed. Type-mismatch → that recap no spawn. `ok`/`yes`/`y`/`continue` ≠ implement.
19
+ Owned-defect: recap `Fix {path+bug}`. How=patch. Why=rejected workaround + we-own-it. Leftover always-print typed triple (Implement when in-repo writes). Workaround in why not remainder.
18
20
 
19
21
  Fat dump → stop on this JSON only (≤4/batch; no serial known-parallel):
20
22
 
@@ -15,11 +15,12 @@ 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/advise (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 → `{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.
18
+ Scratch/tmp → `{workspaceRoot}/.tmp/grunt/` root files only (mkdir as needed). Never `.tmp/` root. Never persist-owned reserved dirs `plans|handoffs|browser|orchestrator-logs` except owning scripts. No nested `.tmp/grunt/tmp/`. 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.
22
22
  `job: web` and messy test → LLM grunt (web_search/web_fetch).
23
+ Live page ≠ `job:web`. Read pasted `.rulesync/skills/browser/SKILL.md`. `.rulesync/reference/browser.md` only if engine/doctor. No `job:browse`.
23
24
  Browser → `node scripts/browser.mjs <verb>` (`nav|snap|click|fill|shot|pdf|stop`). Lightpanda default. Never MCP. Never raw Playwright. Never env knobs.
24
25
  Isolation/write reply ≤8 lines. Verdict grammar only — no dumps/recap/HTML/JSON/full logs:
25
26
 
@@ -9,7 +9,7 @@ tools: read_file, grep, list_dir, write, search_replace, run_terminal_command, w
9
9
  ---
10
10
  Voice: `.rulesync/reference/output.md` — must follow.
11
11
 
12
- Implement specified solution. User prompt may be the spec; `.tmp/plans` not required for small/defined writes. Never spawn. Never run for simple tool usage (grunt agent). Never plan/advise (thinker agent). Never how/why/explain judgment.
12
+ Implement specified solution. User prompt may be the spec; `.tmp/grunt/plans` not required for small/defined writes. Never spawn. Never run for simple tool usage (grunt agent). Never plan/advise (thinker agent). Never how/why/explain judgment.
13
13
  After write: validate+sim (mandatory). Fix hard failures. Return findings. Never skip validation
14
14
  DOM ⚠ → `node scripts/browser.mjs snap`. Visual ⚠ → `node scripts/browser.mjs shot`.
15
15
  Read/Grep/list_dir only if needed for edit. Prefer: implement the spec; dump via `need:`.
@@ -8,15 +8,17 @@ mcpInheritance: none
8
8
  tools: spawn_subagent, write, todo_write, get_command_or_subagent_output, kill_command_or_subagent
9
9
  ---
10
10
  Voice: `.rulesync/reference/output.md` — cite once; apply every turn.
11
- en-US unless asked. every output maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. Parent and child. Every turn, not final-only. no mid-turn chat; no narration while Agent runs; user-visible = legal `[role]:` + **one-line** recap. Advise leftover: numbered pick each on own line after recap. Zero user-visible tokens before spawn/peek. In-flight host Stop → only `[orchestrator]: wait grunt`.
11
+ en-US unless asked. every output maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. Parent and child. Every turn, not final-only. no mid-turn chat; no narration while Agent runs; user-visible = legal `[role]:` + **one-line** recap. Advise leftover: one empty blank line after recap/body then numbered pick each on own line. Zero user-visible tokens before spawn/peek. In-flight host Stop → only `[orchestrator]: wait grunt`.
12
12
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste.
13
13
  Always-do:
14
14
 
15
15
  | signal | next |
16
16
  |---|---|
17
- | create/change product files | grunt facts → thinker plan/recap-stop before writes; default thinker unless prompt-is-spec or small/simple → implementer. Thinker recap ≠ spec-ready; advise-stop until Implement pick / `/implement-plan` / explicit implement. Never parent Write/Bash/Skill |
18
- | plan/spec ready + file writes remain | Implement pick / `/implement-plan` / explicit implement only; persist `/write-plan` path + continue. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise. Thinker recap alone = recap-stop |
17
+ | create/change product files | grunt facts → thinker plan/recap-stop before writes; default thinker unless prompt-is-spec or small/simple → implementer. Thinker recap ≠ spec-ready; advise-stop until pick1/pick2 / `/implement-plan {n}` / explicit implement (=pick1 iff Implement-typed). Never parent Write/Bash/Skill |
18
+ | plan/spec ready + file writes remain | leftover pick1/pick2 / `/implement-plan {n}` / explicit implement (=pick1 iff Implement-typed) only. Implement pick1 = spawn implementer + last [thinker] recap as spec; no `.tmp/grunt/plans`; no write-plan; no implement-plan. Implement pick2 = persist `.tmp/grunt/plans/{n}` then implementer `plan=/abs/...` one-shot skip pause. Write pick1 = `/tmp` persist recap; no implementer. Write pick2 = write-plan persist inspect-pause; remainder `/implement-plan {n}`; no implementer. slash `/write-plan` ≠ leftover 2. `ok`/`yes`/`y`/`continue` ≠ either. Thinker recap alone = recap-stop |
19
19
  | tools/facts/git/test/mechanical write | spawn grunt |
20
+ | snippet / cite / "what is X" | spawn grunt `job:web`. URL-in-a-cite ≠ browse |
21
+ | URL / browse / click / fill / snap / live DOM | spawn grunt; child prompt must include abs `.rulesync/skills/browser/SKILL.md` and `.rulesync/reference/browser.md`; Read then `node scripts/browser.mjs`; Lightpanda default; never websearch that page |
20
22
  | world fact | spawn grunt `job:web` |
21
23
  | write defined solution | spawn implementer; **blocked if advise-class stems present** unless prompt-is-spec / obvious comment-spec false+ |
22
24
  | think / plan / advise / recommend / how / why / explain (ask us; not slash `/explain` not one-cmd/last-log/source-token) | spawn thinker; facts=grunt/`need:`; **no implementer this turn**. Repeat how/why/… + last [thinker] still visible + no new input/tools since → no spawn; echo last or /explain. Unsure → thinker |
@@ -27,22 +29,26 @@ Always-do:
27
29
  | children done + no findings | recap; advise-first stop wins. Else stop only if no writes remain and spawn count > 0 when the user asked for a file change |
28
30
  | `/parent` | spawn-first; hook last-ditch `parent-escape`; never Read/Bash |
29
31
  | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
32
+ | `/tmp` | one-turn write `.tmp/grunt/`; recap `[tmp]:` |
30
33
  | `/pickup` | spawn-first; grunt resolve if needed; never parent Read; not a mode |
31
34
  | `/write-plan` plan-only | recap; remainder `/implement-plan` (ask) |
32
35
  | `/explain` | spawn if facts/work; then human recap of child output; screenshot/visible=context no Read |
33
- | `/solo` | session escape only; stamp grunt-off-{sid}; else spawn-first |
34
- | `/cascade` | unlink solo stamp; spawn-first |
36
+ | `/solo` | session escape; stamp spawn-mode-{sid} iff slash≠config; always unlink grunt-off; else spawn-first |
37
+ | `/cascade` | restore cascade; stamp spawn-mode-{sid} iff slash≠config; always unlink grunt-off; spawn-first |
38
+ | `/auto` | session leftover-gate auto; stamp auto-ask-{sid} iff slash≠config; not spawn-escape; not autofix |
39
+ | `/ask` | session leftover-gate ask; stamp auto-ask-{sid} iff slash≠config; not leftover y/n; not pick 3 |
35
40
 
36
- Precedence: slash → obvious fact/false+/reuse → advise-class row → prompt-is-spec → write. Advise-class + write-class → thinker first; implementer only after spec / `/implement-plan`.
41
+ Precedence: slash → obvious fact/false+/reuse → advise-class row → prompt-is-spec → write. Advise-class + write-class → thinker first; implementer only after spec / pick1 / pick2 / `/implement-plan {n}`.
37
42
  False+: I think terraform plan /implement-plan /solo source tokens one-cmd how- last-log why.
38
- Advise finals: numbered pick each on own line after tagged recap:
39
- 1. Implement
40
- 2. Tweak
41
- Slash `/implement-plan` + explicit implement = Implement. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise.
42
- You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp grunt-off-{sid} this session) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = legal tag + **one-line** recap; advise leftover numbered pick on following lines not crammed. Table = routing; echo = report not command. Siblings still run: `[grunt]:` echo. Facts/search/trees → grunt, never thinker.
43
+ Advise leftover: numbered pick each on own line after tagged recap; one empty blank line immediately before leftover 1. (leftover last; not adjacent to recap/body). Always-print typed leftover triple (advise-class final recap only). One verb per recap; never jammed Implement/write. No leftover on `need:` JSON wait-grunt grunt/implementer recaps slash-only turns.
44
+ 1. {Implement|Write} with verbal plan
45
+ 2. {same verb} with file plan
46
+ 3. Tweak
47
+ Type Implement = in-repo product writes. Type Write = persist-only/implementer-nonsense (SCA/advise-only/`/tmp`/inspect-pause). Recap remainder still names the human gate; leftover is not `4. Contact bank`. `/handoff` stays slash not Write pick1. No omit 1/2. No frozen 4. Do not relabel 3. Parent echoes printed leftover. Match number or full leftover label (or unique tail `verbal plan` / `file plan`). Always-do leftover match printed lines; not Skill-name (`2` ≠ write-plan). Implement pick1 = spawn implementer; last [thinker] recap as spec; no `.tmp/grunt/plans`; no write-plan; no implement-plan. Implement pick2 = persist `.tmp/grunt/plans/{n}` then spawn implementer `plan=/abs/...` one-shot skip pause. Write pick1 = `/tmp` persist recap/body under `.tmp/grunt/`; no implementer. Write pick2 = write-plan persist inspect-pause; remainder `/implement-plan {n}`; no implementer this turn. 3 / Tweak → stay advise; extra text = revision notes → thinker; bare 3/Tweak = stop wait notes. Bare `implement`/`implementer` → pick1 iff Implement-typed; else recap “no implementer this remainder”; no spawn. Bare `write` → pick1 iff Write-typed; else not a match. Type-mismatch → that recap no spawn. Alias `Implementer with …` → pick1/pick2 iff Implement-typed. `/write-plan` persist-only inspect-pause; remainder `/implement-plan {n}` (ask); ≠ leftover pick 2. `/implement-plan {n}` disk/file ≠ verbal. `ok`/`yes`/`y`/`continue` ≠ either pick.
48
+ You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp spawn-mode-{sid} iff slash≠config; unlink grunt-off) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = legal tag + **one-line** recap; advise leftover numbered pick on following lines after one empty blank line not crammed (echo printed leftover; always-print typed triple). Table = routing; echo = report not command. Siblings still run: `[grunt]:` echo. Facts/search/trees → grunt, never thinker.
43
49
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
44
- grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution or prompt-is-spec or small/simple; blocked if advise-class. thinker ← think/plan/advise/recommend/how/why/explain (default product change; no implementer this turn).
45
- Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
50
+ grunt ← tools. Snippet/cite/"what is X" → grunt `job: web`. URL/browse/click/fill/snap/live DOM → grunt; paste skill+browser.md abs paths; never websearch that page. Never memory. implementer ← specified solution or prompt-is-spec or small/simple; blocked if advise-class. thinker ← think/plan/advise/recommend/how/why/explain (default product change; no implementer this turn).
51
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only. Browse tags match → prompt must include abs `.rulesync/skills/browser/SKILL.md` and `.rulesync/reference/browser.md`.
46
52
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. Grok: `get_command_or_subagent_output` `timeout_ms=60000` every peek. Else GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
47
53
  Child `need:` JSON → parse-need + parallel grunt; one `resume_from` + **new** verdicts only; max 3; no re-send task; no fresh-spawn.
48
54
  `/parent` hook last-ditch only. Skills spawn-first. Only `/solo` escapes spawn workflow. `/pickup` spawn-first; grunt resolve if needed; never parent Read; not a mode.
@@ -10,13 +10,15 @@ tools: read_file
10
10
  Voice: `.rulesync/reference/output.md` — must follow.
11
11
 
12
12
  Plan/advise/recommend/how/why/explain. Deep reason. Never spawn. Never implement (implementer agent). Never run for simple tool usage (grunt agent).
13
- Read-only. No bash. Named-file Read of SSOT paths in the prompt only; investigate/search/trees → immediate `need:` for grunt. Fat facts (search|exec|web|test; world=`job:web` never memory/browse) → stop `need:` JSON; parent fans grunt; `resume_from` + `verdict:`.
13
+ Read-only. No bash. Named-file Read of SSOT paths in the prompt only; investigate/search/trees → immediate `need:` for grunt. Fat facts (search|exec|web|test; world=`job:web` never memory) → stop `need:` JSON; parent fans grunt; `resume_from` + `verdict:`. Browse-vs-web advise only: INDEX `browser` row + `.rulesync/reference/browser.md`; route; do not browse. How-does-the-page-look → grunt not self.
14
14
  Return the spec/plan. Flag edge cases/pitfalls.
15
- First line = output.md recap. Plan/advice body after. Advice ≠ completed product work.
16
- Advise finals end with numbered pick:
17
- 1. Implement
18
- 2. Tweak
19
- Slash `/implement-plan` + explicit implement = Implement aliases. `ok`/`yes`/`y`/`continue` ≠ Implement.
15
+ First line = recap. Recap **must** be `{decided}. {how-capsule}. {why-clause}`. Why-clause = rejected alt + constraint (not dump not “because”). Labels optional (`Why:` / em-dash OK); do not mandate `## Why`. Body may expand How/Why/edges — not parent-echoed. Advice ≠ completed product work. `need:` JSON still JSON-only (no why mix).
16
+ Advise leftover: one empty blank line after recap/body then numbered pick each on own line. Always-print typed leftover triple (advise-class final recap only). One verb per recap; never jammed Implement/write. No leftover on `need:` JSON wait-grunt grunt/implementer recaps slash-only turns.
17
+ 1. {Implement|Write} with verbal plan
18
+ 2. {same verb} with file plan
19
+ 3. Tweak
20
+ Type Implement = in-repo product writes. Type Write = persist-only/implementer-nonsense (SCA/advise-only/`/tmp`/inspect-pause). Recap remainder still names the human gate; leftover is not `4. Contact bank`. `/handoff` stays slash not Write pick1. Print all three. No omit 1/2. No frozen 4. Do not relabel 3. Parent echoes printed leftover. Thinker does not spawn. Bare `implement`/`implementer` → pick1 iff Implement-typed; else recap “no implementer this remainder”. Bare `write` → pick1 iff Write-typed. Type-mismatch → that recap no spawn. `ok`/`yes`/`y`/`continue` ≠ implement.
21
+ Owned-defect: recap `Fix {path+bug}`. How=patch. Why=rejected workaround + we-own-it. Leftover always-print typed triple (Implement when in-repo writes). Workaround in why not remainder.
20
22
 
21
23
  Fat dump → stop on this JSON only (≤4/batch; no serial known-parallel):
22
24
 
@@ -2,7 +2,7 @@
2
2
  /** PreToolUse parent-deny on Claude+Grok+Antigravity; Stop = first-non-empty-line recap; stop-block survives host banners; SubagentStop is single-registration intercept.
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: first-non-empty-line [orchestrator]:/[grunt]:/[implementer]:/[thinker]:/[handoff]: recap
5
+ Stop: first-non-empty-line [orchestrator]:/[grunt]:/[implementer]:/[thinker]:/[handoff]:/[tmp]: recap
6
6
  | parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
7
7
  Empty lastAssistantMessage → transcript_path tail-scan. Fail-open: parse/crash → empty stdout, exit 0.
8
8
  */
@@ -25,16 +25,20 @@ import {
25
25
  } from "../../scripts/gate-fat-tools.mjs";
26
26
  import { persistPlan } from "../../scripts/persist-plan.mjs";
27
27
  import { persistHandoff } from "../../scripts/persist-handoff.mjs";
28
+ import { persistTmp } from "../../scripts/persist-tmp.mjs";
28
29
  import { parseNeed } from "../../scripts/parse-need.mjs";
29
30
  import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
31
+ import { loadLeftoverGate, loadSpawnMode } from "../../scripts/grunt-config.mjs";
30
32
 
31
- export const ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
33
+ export const ORCHESTRATOR_LOGS_DIR = ".tmp/grunt/orchestrator-logs";
34
+ /** One-release dual-read; drop next release. */
35
+ export const LEGACY_ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
32
36
  export const DENY_REASON =
33
37
  "First action=spawn implementer|grunt|thinker. Deny expected. Only /solo this session escapes.";
34
38
  export const STOP_REASONS = [
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.",
39
+ "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 numbered pick each on own line after (echo printed leftover; always-print typed triple).",
40
+ "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 numbered pick each on own line after (echo printed leftover; always-print typed triple).",
41
+ "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 numbered pick each on own line after (echo printed leftover; always-print typed triple).",
38
42
  ];
39
43
  const TRANSCRIPT_TAIL_BYTES = 512 * 1024;
40
44
  const PARENT_TOOLS = new Set([
@@ -58,12 +62,15 @@ const WRITE_TOOLS = new Set([
58
62
  ]);
59
63
  const BASH_TOOLS = new Set(["bash", "runterminalcommand", "runcommand"]);
60
64
  const SKILL_TOOLS = new Set(["skill"]);
61
- const PARENT_SKILLS = new Set([
65
+ export const PARENT_SKILLS = new Set([
62
66
  "parent",
63
67
  "explain",
64
68
  "solo",
65
69
  "cascade",
70
+ "auto",
71
+ "ask",
66
72
  "handoff",
73
+ "tmp",
67
74
  "pickup",
68
75
  "write-plan",
69
76
  "implement-plan",
@@ -74,7 +81,11 @@ const MAX_INTERCEPT = 3;
74
81
  /** `/solo` enters single-agent mode; `/cascade` restores the orchestrator. */
75
82
  const SOLO_RE = /^\s*\/solo\s*$/;
76
83
  const CASCADE_RE = /^\s*\/cascade\s*$/;
84
+ const AUTO_RE = /^\s*\/auto\s*$/;
85
+ const ASK_RE = /^\s*\/ask\s*$/;
77
86
  const SOLO_STAMP = "grunt-off";
87
+ export const AUTO_ASK_STAMP = "auto-ask";
88
+ export const SPAWN_MODE_STAMP = "spawn-mode";
78
89
 
79
90
 
80
91
  function main() {
@@ -154,19 +165,17 @@ function preToolUse(data) {
154
165
  return 0;
155
166
  }
156
167
 
157
- /** Session flag, not one-turn. Fail-closed: an unreadable stamp keeps grunt on. */
168
+ /** Session flag, not one-turn. Fail-closed: unreadable spawn-mode stamp is not solo. */
158
169
  export function isSoloMode(data) {
159
170
  try {
160
- const p = soloStampPath(data);
161
- return Boolean(p && fs.existsSync(p));
171
+ return spawnModeOf(data) === "solo";
162
172
  } catch {
163
173
  return false;
164
174
  }
165
175
  }
166
176
 
167
177
  function hasParentEscape(data) {
168
- const p = stampPath(data, "parent-escape");
169
- return Boolean(p && fs.existsSync(p));
178
+ return Boolean(resolveStamp(data, "parent-escape"));
170
179
  }
171
180
 
172
181
  function emitFat(data) {
@@ -198,13 +207,30 @@ function isUnderDir(filePath, workspaceRoot, segments) {
198
207
  }
199
208
 
200
209
  export function isUnderPlans(filePath, workspaceRoot) {
201
- return isUnderDir(filePath, workspaceRoot, [".tmp", "plans"]);
210
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "plans"]);
202
211
  }
203
212
 
204
213
  export function isUnderHandoffs(filePath, workspaceRoot) {
205
214
  return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "handoffs"]);
206
215
  }
207
216
 
217
+ export const TMP_RESERVED_DIRS = new Set([
218
+ "plans",
219
+ "handoffs",
220
+ "browser",
221
+ "orchestrator-logs",
222
+ ]);
223
+
224
+ export function isUnderTmp(filePath, workspaceRoot) {
225
+ if (!filePath || !workspaceRoot) return false;
226
+ const abs = path.isAbsolute(filePath)
227
+ ? path.resolve(filePath)
228
+ : path.resolve(workspaceRoot, filePath);
229
+ const root = path.resolve(workspaceRoot, ".tmp", "grunt");
230
+ if (path.dirname(abs) !== root) return false;
231
+ return !TMP_RESERVED_DIRS.has(path.basename(abs));
232
+ }
233
+
208
234
  export function isAllowedParentGruntJob(command, workspaceRoot) {
209
235
  return isWorkspaceGruntJob(command, workspaceRoot, ["search", "exec"]);
210
236
  }
@@ -257,8 +283,18 @@ function parentWrite(data, toolInput) {
257
283
  "";
258
284
  const rewritten = rewriteGruntScratchPath(rawPath, ws);
259
285
  if (rewritten) rawPath = rewritten;
260
- const handoff = isUnderHandoffs(rawPath, ws);
261
- if (!handoff && !isUnderPlans(rawPath, ws)) {
286
+ let persist;
287
+ let invalid = "invalid plan";
288
+ if (isUnderPlans(rawPath, ws)) {
289
+ persist = persistPlan;
290
+ invalid = "invalid plan";
291
+ } else if (isUnderHandoffs(rawPath, ws)) {
292
+ persist = persistHandoff;
293
+ invalid = "invalid handoff";
294
+ } else if (isUnderTmp(rawPath, ws)) {
295
+ persist = persistTmp;
296
+ invalid = "invalid tmp";
297
+ } else {
262
298
  if (hasParentEscape(data)) {
263
299
  emit({ decision: "allow" });
264
300
  return 0;
@@ -267,12 +303,11 @@ function parentWrite(data, toolInput) {
267
303
  return 0;
268
304
  }
269
305
  const content = typeof toolInput.content === "string" ? toolInput.content : "";
270
- const persist = handoff ? persistHandoff : persistPlan;
271
306
  const result = persist({ workspaceRoot: ws, content });
272
307
  if (!result.ok) {
273
308
  emit({
274
309
  decision: "deny",
275
- reason: result.error || (handoff ? "invalid handoff" : "invalid plan"),
310
+ reason: result.error || invalid,
276
311
  });
277
312
  return 0;
278
313
  }
@@ -307,10 +342,7 @@ function rewriteSpawn(toolInput, data) {
307
342
 
308
343
  function postToolUse(data) {
309
344
  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");
345
+ writeStamp(data, "tools-used", "1");
314
346
  return 0;
315
347
  }
316
348
 
@@ -324,35 +356,41 @@ function isParentEscapePrompt(prompt) {
324
356
  }
325
357
 
326
358
  function userPromptSubmit(data) {
327
- unlinkQuiet(stampPath(data, "tools-used"));
359
+ unlinkStamp(data, "tools-used");
328
360
  const prompt = userPromptOf(data);
329
361
  if (!isHostStopBanner(prompt)) {
330
- unlinkQuiet(stampPath(data, "stop-block"));
331
- }
332
- // Sticky: only /solo and /cascade move it. Every other prompt leaves it alone.
333
- if (SOLO_RE.test(prompt)) {
334
- const solo = soloStampPath(data);
335
- if (solo) {
336
- fs.mkdirSync(path.dirname(solo), { recursive: true });
337
- fs.writeFileSync(solo, "1");
362
+ unlinkStamp(data, "stop-block");
363
+ }
364
+ // Sticky: only exact /solo and /cascade move spawn-mode. Always unlink grunt-off.
365
+ if (SOLO_RE.test(prompt) || CASCADE_RE.test(prompt)) {
366
+ const token = SOLO_RE.test(prompt) ? "solo" : "cascade";
367
+ const cfg = loadSpawnMode(workspaceRootOf(data));
368
+ if (token === cfg) {
369
+ unlinkSpawnModeStamp(data);
370
+ } else {
371
+ writeSpawnModeStamp(data, token);
372
+ }
373
+ unlinkSoloStamp(data);
374
+ }
375
+ if (AUTO_RE.test(prompt) || ASK_RE.test(prompt)) {
376
+ const token = AUTO_RE.test(prompt) ? "auto" : "ask";
377
+ const cfg = loadLeftoverGate(workspaceRootOf(data));
378
+ if (token === cfg) {
379
+ unlinkAutoAskStamp(data);
380
+ } else {
381
+ writeAutoAskStamp(data, token);
338
382
  }
339
- } else if (CASCADE_RE.test(prompt)) {
340
- unlinkQuiet(soloStampPath(data));
341
383
  }
342
384
  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
- }
385
+ writeStamp(data, "parent-escape", "1");
348
386
  } else {
349
- unlinkQuiet(stampPath(data, "parent-escape"));
387
+ unlinkStamp(data, "parent-escape");
350
388
  }
351
389
  return 0;
352
390
  }
353
391
 
354
392
  const RECAP_TAG_RE =
355
- /^\[(?:orchestrator|grunt|implementer|thinker|handoff)\]:/;
393
+ /^\[(?:orchestrator|grunt|implementer|thinker|handoff|tmp)\]:/;
356
394
  const WAIT_GRUNT = "[orchestrator]: wait grunt";
357
395
 
358
396
  function firstNonEmptyStripped(msg) {
@@ -376,6 +414,38 @@ export function isWaitGruntExact(msg) {
376
414
  return nonempty[0].replace(/^[\s`*_>]+/, "") === WAIT_GRUNT;
377
415
  }
378
416
 
417
+ function nonemptyLines(msg) {
418
+ return String(msg || "")
419
+ .split("\n")
420
+ .filter((l) => l.trim());
421
+ }
422
+
423
+ /** Format-only leftover triple. No leftover-number → skill map. */
424
+ export function hasLeftoverTriple(msg) {
425
+ const lines = String(msg || "")
426
+ .split("\n")
427
+ .map((l) => l.replace(/^[\s`*_>]+/, "").trim());
428
+ const hit = (re) => lines.some((l) => re.test(l));
429
+ const tweak = hit(/^3\.\s+Tweak$/);
430
+ const impl =
431
+ hit(/^1\.\s+Implement with verbal plan$/) &&
432
+ hit(/^2\.\s+Implement with file plan$/);
433
+ const write =
434
+ hit(/^1\.\s+Write with verbal plan$/) &&
435
+ hit(/^2\.\s+Write with file plan$/);
436
+ return Boolean(tweak && (impl || write));
437
+ }
438
+
439
+ /** Ask leftover-required: any `[thinker]:`; `[orchestrator]:` iff >1 nonempty line (except exact wait-grunt); `[grunt]|[implementer]|[handoff]|[tmp]:` exempt. Auto waives in Stop. */
440
+ export function leftoverRequiredAsk(msg) {
441
+ if (isWaitGruntExact(msg)) return false;
442
+ const first = firstNonEmptyStripped(msg);
443
+ if (/^\[(?:grunt|implementer|handoff|tmp)\]:/.test(first)) return false;
444
+ if (/^\[thinker\]:/.test(first)) return true;
445
+ if (/^\[orchestrator\]:/.test(first)) return nonemptyLines(msg).length > 1;
446
+ return false;
447
+ }
448
+
379
449
  function payloadAssistantMessage(data) {
380
450
  if (!data || typeof data !== "object") return "";
381
451
  const camel = data.lastAssistantMessage;
@@ -466,9 +536,8 @@ function stop(data) {
466
536
  // Before the parent-escape consume: solo must never burn the one-turn stamp.
467
537
  if (isSoloMode(data)) return 0;
468
538
 
469
- const escapeStamp = stampPath(data, "parent-escape");
470
- if (escapeStamp && fs.existsSync(escapeStamp)) {
471
- unlinkQuiet(escapeStamp);
539
+ if (resolveStamp(data, "parent-escape")) {
540
+ unlinkStamp(data, "parent-escape");
472
541
  return 0;
473
542
  }
474
543
 
@@ -483,22 +552,22 @@ function stop(data) {
483
552
  if (waitFirst && !isWaitGruntExact(msg)) {
484
553
  // fall through to block — leftover lines not allowed on mid-turn wait
485
554
  } else if (isRecap(msg)) {
486
- return 0;
555
+ if (
556
+ leftoverGateOf(data) !== "auto" &&
557
+ leftoverRequiredAsk(msg) &&
558
+ !hasLeftoverTriple(msg)
559
+ ) {
560
+ // fall through to block — leftover-required under ask
561
+ } else {
562
+ return 0;
563
+ }
487
564
  }
488
565
 
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
- }
566
+ let n = readStampInt(data, "stop-block");
495
567
  if (n >= MAX_STOP) {
496
568
  return 0;
497
569
  }
498
- if (stopStamp) {
499
- fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
500
- fs.writeFileSync(stopStamp, String(n + 1));
501
- }
570
+ writeStamp(data, "stop-block", String(n + 1));
502
571
  const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
503
572
  emit({ decision: "block", reason: reasonText });
504
573
  return 0;
@@ -522,12 +591,7 @@ function interceptNeed(data, hookEventName) {
522
591
  return 0;
523
592
  }
524
593
 
525
- const interceptStamp = stampPath(data, "need-intercept");
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
- }
594
+ let n = readStampInt(data, "need-intercept");
531
595
  if (n >= MAX_INTERCEPT) {
532
596
  return 0;
533
597
  }
@@ -557,10 +621,7 @@ function interceptNeed(data, hookEventName) {
557
621
  }
558
622
  parts.push(String(result.text || "").trimEnd());
559
623
  }
560
- if (interceptStamp) {
561
- fs.mkdirSync(path.dirname(interceptStamp), { recursive: true });
562
- fs.writeFileSync(interceptStamp, String(n + 1));
563
- }
624
+ writeStamp(data, "need-intercept", String(n + 1));
564
625
  const reason = parts.join("\n");
565
626
  emit({
566
627
  decision: "block",
@@ -579,6 +640,42 @@ function stampPath(data, prefix) {
579
640
  return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
580
641
  }
581
642
 
643
+ function legacyStampPath(data, prefix) {
644
+ const root = workspaceRootOf(data);
645
+ const sid = sessionIdOf(data) || "default";
646
+ if (!root) return null;
647
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
648
+ }
649
+
650
+ function resolveStamp(data, prefix) {
651
+ const neu = stampPath(data, prefix);
652
+ if (neu && fs.existsSync(neu)) return neu;
653
+ const old = legacyStampPath(data, prefix);
654
+ if (old && fs.existsSync(old)) return old;
655
+ return null;
656
+ }
657
+
658
+ function writeStamp(data, prefix, body) {
659
+ const p = stampPath(data, prefix);
660
+ if (!p) return null;
661
+ fs.mkdirSync(path.dirname(p), { recursive: true });
662
+ fs.writeFileSync(p, body);
663
+ return p;
664
+ }
665
+
666
+ function unlinkStamp(data, prefix) {
667
+ unlinkQuiet(stampPath(data, prefix));
668
+ unlinkQuiet(legacyStampPath(data, prefix));
669
+ }
670
+
671
+ function readStampInt(data, prefix) {
672
+ const p = resolveStamp(data, prefix);
673
+ if (!p) return 0;
674
+ const n = parseInt(fs.readFileSync(p, "utf8"), 10);
675
+ if (!Number.isFinite(n) || n < 0) return 0;
676
+ return n;
677
+ }
678
+
582
679
  function sessionIdOf(data) {
583
680
  return String(
584
681
  process.env.GROK_SESSION_ID ||
@@ -595,6 +692,127 @@ function soloStampPath(data) {
595
692
  return path.join(root, ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
596
693
  }
597
694
 
695
+ function legacySoloStampPath(data) {
696
+ const root = workspaceRootOf(data);
697
+ const sid = sessionIdOf(data);
698
+ if (!root || !sid) return null;
699
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
700
+ }
701
+
702
+ function resolveSoloStamp(data) {
703
+ const neu = soloStampPath(data);
704
+ if (neu && fs.existsSync(neu)) return neu;
705
+ const old = legacySoloStampPath(data);
706
+ if (old && fs.existsSync(old)) return old;
707
+ return null;
708
+ }
709
+
710
+ function unlinkSoloStamp(data) {
711
+ unlinkQuiet(soloStampPath(data));
712
+ unlinkQuiet(legacySoloStampPath(data));
713
+ }
714
+
715
+ /** Spawn-mode stamp: never share a `default` stamp across sid-less sessions. */
716
+ export function spawnModeStampPath(data) {
717
+ const root = workspaceRootOf(data);
718
+ const sid = sessionIdOf(data);
719
+ if (!root || !sid) return null;
720
+ return path.join(root, ORCHESTRATOR_LOGS_DIR, SPAWN_MODE_STAMP + "-" + sid);
721
+ }
722
+
723
+ function legacySpawnModeStampPath(data) {
724
+ const root = workspaceRootOf(data);
725
+ const sid = sessionIdOf(data);
726
+ if (!root || !sid) return null;
727
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, SPAWN_MODE_STAMP + "-" + sid);
728
+ }
729
+
730
+ function resolveSpawnModeStamp(data) {
731
+ const neu = spawnModeStampPath(data);
732
+ if (neu && fs.existsSync(neu)) return neu;
733
+ const old = legacySpawnModeStampPath(data);
734
+ if (old && fs.existsSync(old)) return old;
735
+ return null;
736
+ }
737
+
738
+ function writeSpawnModeStamp(data, body) {
739
+ const p = spawnModeStampPath(data);
740
+ if (!p) return null;
741
+ fs.mkdirSync(path.dirname(p), { recursive: true });
742
+ fs.writeFileSync(p, body);
743
+ return p;
744
+ }
745
+
746
+ function unlinkSpawnModeStamp(data) {
747
+ unlinkQuiet(spawnModeStampPath(data));
748
+ unlinkQuiet(legacySpawnModeStampPath(data));
749
+ }
750
+
751
+ /** Valid stamp solo|cascade wins; else grunt-off presence as solo; else config. Unreadable/bad body ignored. */
752
+ export function spawnModeOf(data) {
753
+ try {
754
+ const p = resolveSpawnModeStamp(data);
755
+ if (p) {
756
+ const body = fs.readFileSync(p, "utf8").trim();
757
+ if (body === "solo" || body === "cascade") return body;
758
+ }
759
+ } catch {
760
+ // unreadable stamp → fall through
761
+ }
762
+ if (resolveSoloStamp(data)) return "solo";
763
+ return loadSpawnMode(workspaceRootOf(data));
764
+ }
765
+
766
+ /** Leftover-gate stamp: never share a `default` stamp across sid-less sessions. */
767
+ export function autoAskStampPath(data) {
768
+ const root = workspaceRootOf(data);
769
+ const sid = sessionIdOf(data);
770
+ if (!root || !sid) return null;
771
+ return path.join(root, ORCHESTRATOR_LOGS_DIR, AUTO_ASK_STAMP + "-" + sid);
772
+ }
773
+
774
+ function legacyAutoAskStampPath(data) {
775
+ const root = workspaceRootOf(data);
776
+ const sid = sessionIdOf(data);
777
+ if (!root || !sid) return null;
778
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, AUTO_ASK_STAMP + "-" + sid);
779
+ }
780
+
781
+ function resolveAutoAskStamp(data) {
782
+ const neu = autoAskStampPath(data);
783
+ if (neu && fs.existsSync(neu)) return neu;
784
+ const old = legacyAutoAskStampPath(data);
785
+ if (old && fs.existsSync(old)) return old;
786
+ return null;
787
+ }
788
+
789
+ function writeAutoAskStamp(data, body) {
790
+ const p = autoAskStampPath(data);
791
+ if (!p) return null;
792
+ fs.mkdirSync(path.dirname(p), { recursive: true });
793
+ fs.writeFileSync(p, body);
794
+ return p;
795
+ }
796
+
797
+ function unlinkAutoAskStamp(data) {
798
+ unlinkQuiet(autoAskStampPath(data));
799
+ unlinkQuiet(legacyAutoAskStampPath(data));
800
+ }
801
+
802
+ /** Stamp body auto|ask wins; else config; else ask. Bad stamp body ignored. */
803
+ export function leftoverGateOf(data) {
804
+ try {
805
+ const p = resolveAutoAskStamp(data);
806
+ if (p) {
807
+ const body = fs.readFileSync(p, "utf8").trim();
808
+ if (body === "auto" || body === "ask") return body;
809
+ }
810
+ } catch {
811
+ // unreadable stamp → fall through
812
+ }
813
+ return loadLeftoverGate(workspaceRootOf(data));
814
+ }
815
+
598
816
  function unlinkQuiet(p) {
599
817
  if (!p) return;
600
818
  try {