@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.
- package/.agents/agents/grunt.md +2 -1
- package/.agents/agents/implementer.md +1 -1
- package/.agents/agents/orchestrator.md +19 -13
- package/.agents/agents/thinker.md +8 -6
- package/.agents/skills/ask/SKILL.md +18 -0
- package/.agents/skills/auto/SKILL.md +18 -0
- package/.agents/skills/browser/SKILL.md +4 -3
- package/.agents/skills/cascade/SKILL.md +5 -3
- package/.agents/skills/commit-and-push/SKILL.md +3 -3
- package/.agents/skills/explain/SKILL.md +1 -0
- package/.agents/skills/handoff/SKILL.md +1 -1
- package/.agents/skills/implement-plan/SKILL.md +117 -0
- package/.agents/skills/solo/SKILL.md +5 -3
- package/.agents/skills/tmp/SKILL.md +58 -0
- package/.agents/skills/write-plan/SKILL.md +85 -0
- package/.claude/agents/grunt.md +2 -1
- package/.claude/agents/implementer.md +1 -1
- package/.claude/agents/orchestrator.md +19 -13
- package/.claude/agents/thinker.md +8 -6
- package/.claude/skills/ask/SKILL.md +18 -0
- package/.claude/skills/auto/SKILL.md +18 -0
- package/.claude/skills/browser/SKILL.md +4 -3
- package/.claude/skills/cascade/SKILL.md +5 -3
- package/.claude/skills/commit-and-push/SKILL.md +3 -3
- package/.claude/skills/explain/SKILL.md +1 -0
- package/.claude/skills/handoff/SKILL.md +1 -1
- package/.claude/skills/implement-plan/SKILL.md +117 -0
- package/.claude/skills/solo/SKILL.md +5 -3
- package/.claude/skills/tmp/SKILL.md +58 -0
- package/.claude/skills/write-plan/SKILL.md +85 -0
- package/.codex/agents/grunt.toml +2 -1
- package/.codex/agents/implementer.toml +1 -1
- package/.codex/agents/orchestrator.toml +19 -13
- package/.codex/agents/thinker.toml +8 -6
- package/.grok/agents/grunt.md +2 -1
- package/.grok/agents/implementer.md +1 -1
- package/.grok/agents/orchestrator.md +19 -13
- package/.grok/agents/thinker.md +8 -6
- package/.grok/hooks/orchestrate-parent.js +280 -62
- package/.grok/skills/ask/SKILL.md +18 -0
- package/.grok/skills/auto/SKILL.md +18 -0
- package/.grok/skills/browser/SKILL.md +4 -3
- package/.grok/skills/cascade/SKILL.md +5 -3
- package/.grok/skills/commit-and-push/SKILL.md +3 -3
- package/.grok/skills/explain/SKILL.md +1 -0
- package/.grok/skills/handoff/SKILL.md +1 -1
- package/.grok/skills/implement-plan/SKILL.md +49 -20
- package/.grok/skills/solo/SKILL.md +5 -3
- package/.grok/skills/tmp/SKILL.md +58 -0
- package/.grok/skills/write-plan/SKILL.md +17 -14
- package/.rulesync/grunt.config.jsonc +7 -0
- package/.rulesync/reference/INDEX.md +46 -0
- package/.rulesync/reference/cascade.md +19 -15
- package/.rulesync/reference/hooks.md +6 -4
- package/.rulesync/reference/law.md +9 -0
- package/.rulesync/reference/map.md +20 -10
- package/.rulesync/reference/output.md +23 -8
- package/{.grok/skills/shared → .rulesync/reference}/plan-format.md +12 -11
- package/.rulesync/reference/refs-map.md +14 -0
- package/.rulesync/reference/skills-map.md +23 -0
- package/.rulesync/rules/CLAUDE.md +22 -15
- package/.rulesync/rules/overview.md +22 -15
- package/.rulesync/skills/ask/SKILL.md +18 -0
- package/.rulesync/skills/auto/SKILL.md +18 -0
- package/.rulesync/skills/browser/SKILL.md +2 -2
- package/.rulesync/skills/cascade/SKILL.md +5 -3
- package/.rulesync/skills/commit-and-push/SKILL.md +3 -3
- package/.rulesync/skills/explain/SKILL.md +1 -0
- package/.rulesync/skills/handoff/SKILL.md +1 -1
- package/.rulesync/skills/implement-plan/SKILL.md +127 -0
- package/.rulesync/skills/solo/SKILL.md +5 -3
- package/.rulesync/skills/tmp/SKILL.md +58 -0
- package/.rulesync/skills/write-plan/SKILL.md +92 -0
- package/.rulesync/subagents/grunt.md +2 -1
- package/.rulesync/subagents/implementer.md +1 -1
- package/.rulesync/subagents/orchestrator.md +19 -13
- package/.rulesync/subagents/thinker.md +8 -6
- package/AGENTS.md +22 -15
- package/CHANGELOG.md +56 -0
- package/CLAUDE.md +22 -15
- package/README.md +27 -22
- package/cli/grunt.mjs +12 -12
- package/cli/grunt.test.ts +29 -20
- package/cli/init.mjs +135 -22
- package/cli/init.test.ts +624 -36
- package/package.json +11 -4
- package/scripts/doctor.mjs +64 -2
- package/scripts/emit-maps.mjs +422 -0
- package/scripts/gate-fat-tools.mjs +2 -2
- package/scripts/grunt-config.mjs +50 -0
- package/scripts/guarded-roots.mjs +4 -7
- package/scripts/persist-plan.mjs +17 -2
- package/scripts/persist-tmp.mjs +123 -0
- package/scripts/pipeline.mjs +67 -0
- package/scripts/scrub-spawn-prompt.mjs +1 -1
- 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
|
|
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 =
|
|
14
|
-
Advise
|
|
15
|
-
1. Implement
|
|
16
|
-
2.
|
|
17
|
-
|
|
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
|
|
package/.grok/agents/grunt.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
18
|
-
| plan/spec ready + file writes remain |
|
|
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
|
|
34
|
-
| `/cascade` |
|
|
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
|
|
39
|
-
1. Implement
|
|
40
|
-
2.
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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.
|
package/.grok/agents/thinker.md
CHANGED
|
@@ -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
|
|
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 =
|
|
16
|
-
Advise
|
|
17
|
-
1. Implement
|
|
18
|
-
2.
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
359
|
+
unlinkStamp(data, "tools-used");
|
|
328
360
|
const prompt = userPromptOf(data);
|
|
329
361
|
if (!isHostStopBanner(prompt)) {
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
// Sticky: only /solo and /cascade move
|
|
333
|
-
if (SOLO_RE.test(prompt)) {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|