@lovrozagar/grunt 0.3.8 → 0.3.10

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 (68) hide show
  1. package/.agents/agents/orchestrator.md +9 -9
  2. package/.agents/hooks.json +1 -1
  3. package/.agents/skills/cascade/SKILL.md +4 -2
  4. package/.agents/skills/commit/SKILL.md +3 -3
  5. package/.agents/skills/commit-and-push/SKILL.md +3 -9
  6. package/.agents/skills/commit-push/SKILL.md +46 -0
  7. package/.agents/skills/commit-push-deploy/SKILL.md +59 -0
  8. package/.agents/skills/commit-push-release/SKILL.md +62 -0
  9. package/.agents/skills/explain/SKILL.md +5 -3
  10. package/.agents/skills/handoff/SKILL.md +3 -2
  11. package/.agents/skills/parent/SKILL.md +3 -8
  12. package/.agents/skills/solo/SKILL.md +2 -0
  13. package/.claude/agents/orchestrator.md +10 -10
  14. package/.claude/settings.json +1 -1
  15. package/.claude/skills/cascade/SKILL.md +4 -2
  16. package/.claude/skills/commit/SKILL.md +3 -3
  17. package/.claude/skills/commit-and-push/SKILL.md +3 -9
  18. package/.claude/skills/commit-push/SKILL.md +46 -0
  19. package/.claude/skills/commit-push-deploy/SKILL.md +59 -0
  20. package/.claude/skills/commit-push-release/SKILL.md +62 -0
  21. package/.claude/skills/explain/SKILL.md +5 -3
  22. package/.claude/skills/handoff/SKILL.md +3 -2
  23. package/.claude/skills/parent/SKILL.md +3 -8
  24. package/.claude/skills/solo/SKILL.md +2 -0
  25. package/.codex/agents/orchestrator.toml +8 -5
  26. package/.codex/hooks.json +1 -1
  27. package/.grok/agents/orchestrator.md +9 -6
  28. package/.grok/hooks/orchestrate-parent.js +2 -1
  29. package/.grok/skills/cascade/SKILL.md +4 -2
  30. package/.grok/skills/commit/SKILL.md +3 -3
  31. package/.grok/skills/commit-and-push/SKILL.md +3 -9
  32. package/.grok/skills/commit-push/SKILL.md +46 -0
  33. package/.grok/skills/commit-push-deploy/SKILL.md +59 -0
  34. package/.grok/skills/commit-push-release/SKILL.md +62 -0
  35. package/.grok/skills/explain/SKILL.md +5 -4
  36. package/.grok/skills/handoff/SKILL.md +3 -2
  37. package/.grok/skills/implement-plan/SKILL.md +8 -12
  38. package/.grok/skills/parent/SKILL.md +3 -9
  39. package/.grok/skills/solo/SKILL.md +2 -0
  40. package/.grok/skills/write-plan/SKILL.md +4 -8
  41. package/.rulesync/hooks.jsonc +2 -1
  42. package/.rulesync/reference/cascade.md +2 -2
  43. package/.rulesync/reference/hooks.md +2 -2
  44. package/.rulesync/reference/map.md +3 -3
  45. package/.rulesync/rules/CLAUDE.md +7 -3
  46. package/.rulesync/rules/overview.md +7 -3
  47. package/.rulesync/skills/cascade/SKILL.md +4 -2
  48. package/.rulesync/skills/commit/SKILL.md +3 -3
  49. package/.rulesync/skills/commit-and-push/SKILL.md +4 -9
  50. package/.rulesync/skills/commit-push/SKILL.md +47 -0
  51. package/.rulesync/skills/commit-push-deploy/SKILL.md +60 -0
  52. package/.rulesync/skills/commit-push-release/SKILL.md +63 -0
  53. package/.rulesync/skills/explain/SKILL.md +5 -3
  54. package/.rulesync/skills/handoff/SKILL.md +3 -2
  55. package/.rulesync/skills/parent/SKILL.md +3 -9
  56. package/.rulesync/skills/solo/SKILL.md +2 -0
  57. package/.rulesync/subagents/orchestrator.md +11 -8
  58. package/AGENTS.md +7 -3
  59. package/CLAUDE.md +7 -3
  60. package/README.md +312 -51
  61. package/bin/grunt.js +3 -1
  62. package/cli/grunt.mjs +148 -15
  63. package/cli/grunt.test.ts +304 -25
  64. package/cli/init.mjs +65 -43
  65. package/cli/init.test.ts +78 -0
  66. package/cli/prompt.mjs +36 -0
  67. package/cli/prompt.test.ts +199 -0
  68. package/package.json +4 -1
package/README.md CHANGED
@@ -1,68 +1,329 @@
1
- # GRUNT
1
+ # Grunt
2
2
 
3
- Building Grunt - a multi agent orchestrator workflow
3
+ Multi-agent orchestrator SoT — deep-merge rulesync trees product scripts and generate pipeline into a consumer repo
4
4
 
5
- # Install
5
+ OSS drop-in; merges existing configs; switches default provider flow to Grunt.
6
6
 
7
- Node.js 22+.
7
+ - [Install](#install)
8
+ - [Usage](#usage)
9
+ - [CLI](#cli)
10
+ - [Init](#init)
11
+ - [Version bump](#version-bump)
12
+ - [Agents](#agents)
13
+ - [Skills](#skills)
14
+ - [Generate](#generate)
15
+ - [Config](#config)
16
+ - [Architecture](#architecture)
17
+ - [Layout](#layout)
18
+ - [Develop](#develop)
19
+ - [Examples](#examples)
20
+ - [License](#license)
21
+
22
+ ## Install
23
+
24
+ - Node.js 22+
25
+ - Consumer:
8
26
 
9
27
  ```
10
28
  npm i -D @lovrozagar/grunt
11
29
  npx @lovrozagar/grunt
12
30
  ```
13
31
 
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.
32
+ - Same as `npx @lovrozagar/grunt init` when no command
33
+ - Do not `npm test` as a consumer
34
+ - Package: `@lovrozagar/grunt` `0.3.9` MIT · https://github.com/lovrozagar/grunt
35
+
36
+ ## Usage
37
+
38
+ - TTY no command → menu (init default; generate check sync-globals purge-mcps doctor help quit)
39
+ - Piped / CI / `--yes` / `-y` / `--non-interactive` no command → still `init`
40
+ - Bin: `grunt` → `./bin/grunt.js` (`type: module`)
41
+
42
+ ## CLI
43
+
44
+ ### Commands
45
+
46
+ - `init` → `init()` — merge SoT, `npm install`, `rulesync:generate`, `sync:globals:apply`, `rulesync:check`
47
+ - `generate` → `npm run rulesync:generate`
48
+ - `check` → `npm run rulesync:check`
49
+ - `sync-globals` → `npm run sync:globals` (dry-run); `--apply` → `sync:globals:apply`
50
+ - `purge-mcps` → `npm run purge:global-mcps` (dry-run); `--apply` → `purge:global-mcps:apply`
51
+ - `doctor` → `npm run rulesync:doctor`
52
+ - `help`
53
+ - `version`
54
+
55
+ ### Flags
56
+
57
+ - `--skip-globals` — skip `sync:globals:apply` on init
58
+ - `--yes` / `-y` / `--non-interactive` — no TTY menu; default command still `init`. Not `--apply`
59
+ - `--apply` — write for `sync-globals` / `purge-mcps`
60
+ - `--host <id>` — `sync-globals --host <id>`
15
61
 
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.
62
+ ## Init
17
63
 
18
- Contributors to this repo only: local `npm i && npm test`.
64
+ - Merge SoT `npm install` `rulesync:generate` `sync:globals:apply` `rulesync:check`
65
+ - `--skip-globals` skips apply
66
+ - Re-init auto-skips globals when `<!-- grunt:begin -->` in `AGENTS.md`/`CLAUDE.md` **or** `scripts/telemetry.mjs` exists
67
+ - First init (no sentinel no telemetry) applies globals unless flagged
68
+ - Owned trees/scripts refresh; extra `.rulesync` files kept; patches to grunt-owned files lost
19
69
 
20
- # Version bump
70
+ ## Version bump
21
71
 
22
72
  ```
23
73
  npm i -D @lovrozagar/grunt@latest && npx @lovrozagar/grunt init
24
74
  ```
25
75
 
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
-
28
- # GOAL
29
-
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`.
31
- - max situational speed relative to task intensity
32
- - max free token savings:
33
- maximally superterse without loosing value - sacrifice grammar for sake of concision
34
- compression - RTK AI
35
- subagent re-prompt scrub - script
36
- cache
37
- context engineering
38
- - max reasoning and effort where needed and minimal reasoning and effort where not needed
39
- - agents:
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.
41
- - subagents:
42
- - grunt (low model, low effort) -> tools. facts/search/exec/git/web/test/low-reason mechanical write. never feature solution. never spawn.
43
- - implementer (medium model, medium effort) -> write already-defined solution. does not plan or invent design. never spawn.
44
- - thinker (high model, high effort) -> plan/deep reason. flags edge cases/pitfalls. read-only. never spawn. no bash.
45
-
46
- # Examples
47
-
48
- 1. User - "Create me a react weather app"
49
- Orchestrator spawns and prompts thinker
50
- Thinker outputs plan to Orchestrator
51
- Orchestrator spawns and prompts implementer
52
- Implementer writes to disk, implements the app & outputs to orchestrator
53
- Orchestrator outputs to user
54
-
55
- 2. User - "What is 2+2"
56
- Orchestrator spawns and prompts Grunt
57
- Grunt outputs 4
58
- Orchestrator echoes `[grunt]:` to user
59
-
60
- 3. User - "What is the next movie Marvel cinematic universe will release to theaters"
61
- Orchestrator spawns and prompts Grunt
62
- Grunt runs a web search & outputs to orchestrator
63
- Orchestrator outputs to user
64
-
65
- 4. User - "In .logs folder find the log or logs on 3/6/2021 with tag 'framework bug'"
66
- Orchestrator spawns and prompts Grunt
67
- Grunt searches local folder & outputs to orchestrator
68
- Orchestrator outputs to user
76
+ - Same re-init / globals rules as Init
77
+
78
+ ## Agents
79
+
80
+ SoT: `.rulesync/subagents/{orchestrator grunt implementer thinker}.md`
81
+
82
+ Emit: `.claude/` `.grok/` `.agents/` `.gemini/`
83
+
84
+ - **orchestrator** (parent) — always spawn+prompt; user-facing only `[orchestrator]:` (or child role tag) one-line echo. `/parent` one-turn; `/handoff` writes `.tmp/grunt/handoffs/{serial}-{slug}-{stamp}.md`. Small/low router. First token spawn. No parent Read/Bash/Grep. Does not implement, plan, or fetch world facts
85
+ - **grunt** — tools: facts/search/exec/git/web/test/low-reason mechanical write. Isolation `verdict:`. Never feature solution. Never spawn. World fact: `job: web`
86
+ - **implementer** — write already-defined solution on allowlisted paths. TDD when spec/plan says tests. Validate + sim after write. Fat dumps via `need:`. Never spawn. Never plan. Id is **implementer** (not Implementor)
87
+ - **thinker** — plan/deep reason; edge cases. Read-only named-file Read of prompt SSOT; trees/search/exec/web/test → `need:`. Never spawn. No bash. Never implement
88
+
89
+ Children never spawn. Spawn only grunt|implementer|thinker. Omit model on spawn; frontmatter on agent files picks haiku/sonnet/opus vs grok-4.5 / grok-4.6 / other hosts. Voice: `.rulesync/reference/output.md`. Protocol: `.rulesync/reference/cascade.md`. `AGENTS.md`/`CLAUDE.md` spawn-first. `GEMINI.md` → `@AGENTS.md`. Goals: synced configs across grok build claude code codex gemini cli antigravity; max situational speed; superterse token savings; max/min reasoning by role.
90
+
91
+ ## Skills
92
+
93
+ Present under `.claude` / `.rulesync` / `.agents`:
94
+
95
+ - `cascade` `commit` `commit-and-push` `commit-push` `commit-push-deploy` `commit-push-release` `explain` `handoff` `parent` `solo`
96
+ - Grok-only: `write-plan` `implement-plan` (`/write-plan` plan-only → `next: /implement-plan`)
97
+
98
+ ## Generate
99
+
100
+ Pipeline (no `-t geminicli`):
101
+
102
+ 1. `rulesync generate` — emit grok / claude / codex / antigravity
103
+ 2. `emit-mcp-policy.mjs`
104
+ 3. `emit-gemini.mjs` — `GEMINI.md` `.gemini/agents/{id}/agent.md` MCP `.gemini/settings.json`
105
+ 4. `emit-agent-shell-tools` — Claude grunt body `Bash`; other hosts `run_terminal_command` (hooks-union)
106
+
107
+ `check` / `doctor` = rulesync check / doctor.
108
+
109
+ Emit writes other-CLI trees from `.rulesync` for the **next** process of that CLI. Not a live hop into another host.
110
+
111
+ ## Config
112
+
113
+ - SoT merge into consumer repo; existing configs kept where not grunt-owned
114
+ - Globals: first init apply; re-init auto-skip (sentinel / telemetry) or `--skip-globals`
115
+ - `sync-globals` / `purge-mcps`: dry-run default; `--apply` writes
116
+ - Hosts: grok claude codex gemini antigravity
117
+
118
+ ## Architecture
119
+
120
+ Protocol picture: one CLI host process, parent-only orchestrator in that session, three sibling spawn types (`grunt` | `implementer` | `thinker`), local workspace tools with RTK on Bash stdout only, one vendor Model API outside the host bubble. Not a product walkthrough. `@lovrozagar/grunt` = protocol SoT + CLI (init/generate/check); not a model runtime. Do not paste `.rulesync/reference/cascade.md` here — boxes and edges only.
121
+
122
+ ### Containment
123
+
124
+ Draw **one** CLI host bubble. That bubble is **this** session’s CLI: Grok Build, Claude Code, Codex, Gemini CLI, or Antigravity. The CLI **is** the host — not a peer router beside another CLI. Parent lives **inside** that host session. Children spawn **inside the same process**. Other CLIs = emit/config on disk only; no runtime hop; no shared spawn/peek line.
125
+
126
+ User-visible conversation attaches only to the parent. Children never talk to the user. Children never spawn.
127
+
128
+ ### Nested diagram
129
+
130
+ Same topology as the session recap; every legal edge labeled.
131
+
132
+ ```
133
+ USER
134
+ │
135
+ │ session in/out (parent only; tagged one-line recap)
136
+ │ TUI local; completion tokens from Model API (mixed)
137
+ ▼
138
+ ┌──────────────────────── CLI HOST (this process) ────────────────────────┐
139
+ │ TUI · hooks · emit/generate · RTK PreToolUse · fs workspace │
140
+ │ │
141
+ │ Parent orchestrator (only spawner; spawn-first; no parent Read/Bash) │
142
+ │ legal spawn types: grunt | implementer | thinker │
143
+ │ omit model on spawn; FM on agent files picks haiku/sonnet/opus │
144
+ │ vs grok-4.5 / grok-4.6 / etc. │
145
+ │ │
146
+ │ spawn / peek / resume_from │
147
+ │ │ │
148
+ │ ▼ │
149
+ │ siblings (never spawn; no child→child) │
150
+ │ ┌───────────┬───────────────┬──────────────┐ │
151
+ │ │ grunt │ implementer │ thinker │ │
152
+ │ │ facts/ │ specified │ plan/deep │ │
153
+ │ │ tools │ writes + TDD │ reason │ │
154
+ │ │ Bash+fs │ Write/Bash/ │ Read / need: │ │
155
+ │ │ web/test │ fs │ no Bash │ │
156
+ │ └─────┬─────┴───────┬───────┴───────┬──────┘ │
157
+ │ │ │ │ │
158
+ │ └──────── tool call ──────────┘ │
159
+ │ │ │
160
+ │ ▼ │
161
+ │ host tools (same process) │
162
+ │ fs: Read / Grep / Glob / Write │
163
+ │ Bash / run_terminal_command │
164
+ │ │ │
165
+ │ └──RTK (PreToolUse; Bash/shell stdout only)──► compress │
166
+ │ │
167
+ │ local (not Model API): │
168
+ │ spawn_subagent / Agent · peek │
169
+ │ workspace tools · RTK │
170
+ │ scrub-spawn-prompt · parse-need · grunt-job │
171
+ │ emit / generate │
172
+ │ │
173
+ │ WebSearch / web_fetch = remote-not-LLM (not a second model) │
174
+ │ MCP = denied in-tree; do not draw as a main path │
175
+ │ │
176
+ │ parent + children ──completion─────────────────────────────┐ │
177
+ └──────────────────────────────────────────────────────────────────────┼──┘
178
+ │
179
+ ▼
180
+ Model API
181
+ (this host’s SDK
182
+ only; one box)
183
+ ..
184
+ emit/config
185
+ (no runtime line)
186
+ ..
187
+ other CLIs
188
+ Gemini spawn/peek
189
+ = GAP
190
+ (no fake peeks)
191
+ ```
192
+
193
+ ### `need:` / `resume_from`
194
+
195
+ Implementer or thinker that still needs a fat dump **stops** on a `need:` JSON line. Parent fans those jobs as **parallel grunt** siblings, then one `resume_from` with **new** `verdict:` blobs only. Max **3** `resume_from` per child id. Thinker has no Bash; facts go through this loop. Grunt does not emit `need:` for its own tool use.
196
+
197
+ ```
198
+ implementer | thinker
199
+ │
200
+ │ stop on JSON only (≤4 jobs/batch)
201
+ ▼
202
+ need: [{"job":"search|exec|web|test","query":"..."}]
203
+ │
204
+ │ parent parse-need (or SubagentStop intercept
205
+ │ when every job is search|exec)
206
+ ▼
207
+ parallel grunt spawns (same host; not a new CLI)
208
+ │
209
+ │ isolation
210
+ ▼
211
+ verdict: ok|fail|empty
212
+ │
213
+ │ one resume_from:<child id> + new verdicts only
214
+ │ do not re-send original task; max 3
215
+ ▼
216
+ same child continues (transcript already on resume_from)
217
+ ```
218
+
219
+ ### Node table
220
+
221
+ | node | inside host? | local vs AI-server | notes |
222
+ | --- | --- | --- | --- |
223
+ | User | no | mixed | Speaks only to parent session. Never a child edge. |
224
+ | Session in/out / TUI | yes | mixed | TUI is local; recap tokens come from the model. Legal `[role]:` one-line echo only. |
225
+ | CLI host process | yes (is the box) | local process | Grok Build / Claude Code / Codex / Gemini / Antigravity. Not a peer of another CLI. |
226
+ | Parent orchestrator | yes | AI-server **turn** | Spawn-first low router. No parent Read/Bash/Grep. Lives in this session, not a sidecar. |
227
+ | `grunt` sibling | yes | AI-server **turn** | Facts/tools/mechanical write. Never spawn. Never feature solution. |
228
+ | `implementer` sibling | yes | AI-server **turn** | Specified writes + TDD when tests are in the spec. Agent id is **implementer**. Never spawn. |
229
+ | `thinker` sibling | yes | AI-server **turn** | Plan/deep reason. Read-only. Named-file Read of prompt SSOT; investigate → `need:`. No Bash. Never spawn. |
230
+ | Host spawn / peek / `resume_from` | yes | **local** | `spawn_subagent` / `Agent` and host peek tools. Gemini spawn/peek = GAP; block on return, classify `done`; no fake peeks. |
231
+ | Workspace fs tools | yes | **local** | Read/Grep/Glob/Write (and host aliases). Parent is denied these except documented persist paths / `/solo`. |
232
+ | Bash / `run_terminal_command` | yes | **local** | RTK wraps stdout on PreToolUse. Thinker has no Bash. |
233
+ | RTK | yes | **local** | Bash/shell stdout compression only. Not Read/Grep/Glob/prompts/images. |
234
+ | `scrub-spawn-prompt` / `parse-need` / `grunt-job` | yes | **local** | Hooks and scripts. Isolation `verdict:` is grunt output, not a model hop. |
235
+ | emit / generate / init | yes (this repo / install) | **local** | Writes other-CLI configs. Not a runtime line to those CLIs. |
236
+ | WebSearch / web_fetch | tool from host | **remote-not-LLM** | Network search/fetch. Not a second Model API box. World facts: grunt `job: web`. |
237
+ | MCP | policy deny | n/a | Denied by policy in-tree. Do not draw as a main path. |
238
+ | Model API | **no** (one box outside) | AI-server | This host’s vendor SDK only. Parent and child **turns** complete here. Spawn omits model; frontmatter on `.rulesync/subagents/*.md` picks the model. |
239
+ | Other CLIs | no (not this process) | emit/config only | Same protocol files emitted elsewhere. No hop, no shared peek. |
240
+ | `@lovrozagar/grunt` | package / CLI | local install | Protocol SoT + CLI. Not a model runtime. |
241
+
242
+ ### Edge labels
243
+
244
+ Use these labels. Do not revive “prompt input” or “agent to use”.
245
+
246
+ | edge | meaning |
247
+ | --- | --- |
248
+ | **session in/out** | User ↔ parent only. Children have no user edge. |
249
+ | **spawn** | Parent → `grunt` \| `implementer` \| `thinker`. First sentence: `You are {agent} subagent.` Omit model. |
250
+ | **peek** | Parent reads host status on the child id. Real host fields. `timeout_ms=60000`. GAP hosts: no fake peeks; block on spawn return = `done`. |
251
+ | **resume_from** | Parent continues the **same** child id with new `verdict:` blobs only. Max 3. Not a fresh spawn. |
252
+ | **need:** | Child stop line: fat dump jobs for parent to fan as grunt. Cap 4 jobs per batch. |
253
+ | **verdict:** | Grunt isolation result back onto `resume_from`. `ok\|fail\|empty`. Short facts, not dumps. |
254
+ | **tool call** | Child (or, illegally if parent, denied) → host tools. |
255
+ | **RTK** | PreToolUse Bash/shell → compressed stdout. No other tools. |
256
+ | **completion** | Parent and children → **one** Model API (this host’s SDK). |
257
+ | **emit/config** | Disk write to other CLI trees. Drawn with a broken line or footnote, **not** a session arrow. |
258
+
259
+ ### What not to draw
260
+
261
+ - A **CLI Provider** box as a peer of the host, or a router that hops between CLIs at runtime
262
+ - **Thinker ↔ CLI** as a special extra channel — thinker is a sibling; tools/`need:` like the protocol
263
+ - The spelling **Implementor** — id is **implementer**
264
+ - **Per-agent Model API** boxes — one Model API outside the host, this SDK only
265
+ - **Child → child spawn** — isolation is parent fan-out of grunt siblings (or in-hook grunt-job for interceptable `search|exec` batches)
266
+ - **MCP** as a happy-path tool rail
267
+ - **RTK** on Read/Grep/Glob/prompts/images
268
+ - **User** arrows into grunt/implementer/thinker
269
+ - Gemini **fake peek** loops. GAP: no invented status API; block on return
270
+
271
+ ### Emit footnote and Gemini GAP
272
+
273
+ **Emit.** `rulesync generate` plus in-tree emit scripts write Claude/Codex/Antigravity/Gemini/Grok trees from `.rulesync`. Config on disk for the **next** process of that CLI. Not a live message into another host. Multi-CLI in a diagram = footnote or a second, disconnected host bubble — never an arrow from this session.
274
+
275
+ **Gemini GAP.** Gemini is not emitted as a spawn/peek host in the cascade table. Do not invent peek or kill APIs. If a Gemini session is the host, treat spawn/peek as GAP: no fake peeks, no auto-kill; block on the host call returning and classify `done`. Other GAP rows (cascade host mapping): Claude Code unless an in-tree schema names a status/output tool; Codex peek/kill; Antigravity peek/kill.
276
+
277
+ ### Protocol pointers
278
+
279
+ Repo-relative (repository root):
280
+
281
+ - `.rulesync/reference/cascade.md` — parent-only spawn, peek/kill table, `need:` / `resume_from`, isolation `verdict:`
282
+ - `.rulesync/reference/rtk.md` — Bash/shell stdout compression
283
+ - `.rulesync/reference/map.md` — cheap outline of protocol, scripts, generated trees
284
+ - `.rulesync/subagents/orchestrator.md`
285
+ - `.rulesync/subagents/grunt.md`
286
+ - `.rulesync/subagents/implementer.md`
287
+ - `.rulesync/subagents/thinker.md`
288
+
289
+ ## Layout
290
+
291
+ Published (`package.json` `files`): `bin/grunt.js` `cli` `scripts/check-globals.mjs` `scripts/emit-agent-shell-tools.mjs` `scripts/emit-gemini.mjs` `scripts/emit-mcp-policy.mjs` `scripts/gate-fat-tools.mjs` `scripts/hooks-union.mjs` `scripts/grunt-job.mjs` `scripts/parse-need.mjs` `scripts/persist-handoff.mjs` `scripts/persist-plan.mjs` `scripts/purge-global-mcps.mjs` `scripts/scrub-spawn-prompt.mjs` `scripts/scrub-text-lib.mjs` `scripts/sync-global-settings.mjs` `scripts/telemetry.mjs` `scripts/scrub-text` `.rulesync` `.grok` `.codex` `.claude` `.agents` `AGENTS.md` `CLAUDE.md` `.mcp.json` `README.md` `LICENSE`
292
+
293
+ No `scripts/*.test.ts` `scripts/fixtures/` `docs/` `coverage/` `vitest.config.ts` in `files`.
294
+
295
+ Repo root (also): `coverage/` `.gemini/` — no `src/` no `CONTRIBUTING` no `docs/`
296
+
297
+ - `bin/grunt.js` — CLI bin
298
+ - `cli/grunt.mjs` — commands
299
+ - `scripts/` — generate emits `telemetry.mjs` (re-init sentinel) `scrub-text`
300
+ - `.rulesync/` — SoT (subagents skills reference)
301
+ - `.claude/` `.grok/` `.codex/` `.agents/` `.gemini/` — host emit
302
+ - `coverage/` — vitest local not published
303
+
304
+ ## Develop
305
+
306
+ Contributors to this repo only:
307
+
308
+ ```
309
+ npm i && npm test
310
+ ```
311
+
312
+ - Test: `vitest run --coverage`
313
+ - Test include: `scripts/**/*.test.ts` `cli/**/*.test.ts`
314
+ - Coverage include: `cli/**`
315
+ - Coverage thresholds: 100% lines / functions / branches / statements
316
+ - Dev deps include rulesync vitest typescript
317
+
318
+ ## Examples
319
+
320
+ Keep these four flows only:
321
+
322
+ 1. “Create me a react weather app” → thinker plan → implementer write → orchestrator recap
323
+ 2. “What is 2+2” → grunt → `[grunt]:` echo
324
+ 3. Marvel theatrical next → grunt `job:web` → recap
325
+ 4. `.logs` 3/6/2021 tag `framework bug` → grunt local search → recap
326
+
327
+ ## License
328
+
329
+ MIT © 2026 lovrozagar
package/bin/grunt.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { start } from "../cli/grunt.mjs"
3
3
 
4
- start()
4
+ start().catch(() => {
5
+ process.exit(1)
6
+ })
package/cli/grunt.mjs CHANGED
@@ -2,38 +2,134 @@ import { execFileSync } from "node:child_process"
2
2
  import { readFileSync } from "node:fs"
3
3
  import path from "node:path"
4
4
  import { fileURLToPath } from "node:url"
5
- import { init } from "./init.mjs"
5
+ import { destAlreadyInited, init, shouldAutoSkipGlobals } from "./init.mjs"
6
+ import { confirm, isInteractive, select, spinner } from "./prompt.mjs"
6
7
 
7
8
  const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
8
9
 
9
10
  const USAGE = `Usage: grunt [command]
10
11
 
11
- Default (no command): init — full setup
12
+ Default (no command): TTY menu; else init — full setup
12
13
 
13
14
  Commands:
14
- init Full setup: merge SoT, npm install, rulesync:generate, sync:globals:apply, rulesync:check
15
- generate npm run rulesync:generate
16
- check npm run rulesync:check
17
- help Show this help
18
- version Print package version
15
+ init Full setup: merge SoT, npm install, rulesync:generate, sync:globals:apply, rulesync:check
16
+ generate npm run rulesync:generate
17
+ check npm run rulesync:check
18
+ sync-globals npm run sync:globals (dry-run; --apply to write)
19
+ purge-mcps npm run purge:global-mcps (dry-run; --apply to write)
20
+ doctor npm run rulesync:doctor
21
+ help Show this help
22
+ version Print package version
19
23
 
20
24
  Flags:
21
- --skip-globals Skip sync:globals:apply (auto-skipped when already initialized)
25
+ --skip-globals Skip sync:globals:apply (auto-skipped when already initialized)
26
+ --yes, -y Non-interactive (not --apply)
27
+ --non-interactive Same as --yes
28
+ --apply Write for sync-globals / purge-mcps
29
+ --host <id> sync-globals host
22
30
  `
23
31
 
32
+ const MENU_OPTIONS = [
33
+ { value: "init", label: "init" },
34
+ { value: "generate", label: "generate" },
35
+ { value: "check", label: "check" },
36
+ { value: "sync-globals", label: "sync-globals" },
37
+ { value: "purge-mcps", label: "purge-mcps" },
38
+ { value: "doctor", label: "doctor" },
39
+ { value: "help", label: "help" },
40
+ { value: "quit", label: "quit" },
41
+ ]
42
+
43
+ const YES_FLAGS = new Set(["--yes", "-y", "--non-interactive"])
44
+
24
45
  function pkgVersion() {
25
46
  const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"))
26
47
  return pkg.version
27
48
  }
28
49
 
29
- function npmRun(script) {
30
- execFileSync("npm", ["run", script], { cwd: process.cwd(), stdio: "inherit" })
50
+ function npmRun(script, extra = []) {
51
+ const args = extra.length ? ["run", script, "--", ...extra] : ["run", script]
52
+ execFileSync("npm", args, { cwd: process.cwd(), stdio: "inherit" })
53
+ }
54
+
55
+ function hostValueOk(v) {
56
+ return v != null && v !== "" && !String(v).startsWith("-")
31
57
  }
32
58
 
33
- export function start() {
34
- const args = process.argv.slice(2)
35
- const skipGlobals = args.includes("--skip-globals")
36
- const [cmd] = args.filter((a) => a !== "--skip-globals")
59
+ export function parseArgv(argv) {
60
+ let skipGlobals = false
61
+ let apply = false
62
+ let host
63
+ let hostError = false
64
+ const positionals = []
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const a = argv[i]
67
+ if (a === "--skip-globals") {
68
+ skipGlobals = true
69
+ continue
70
+ }
71
+ if (YES_FLAGS.has(a)) continue
72
+ if (a === "--apply") {
73
+ apply = true
74
+ continue
75
+ }
76
+ if (a === "--host") {
77
+ const v = argv[i + 1]
78
+ if (!hostValueOk(v)) {
79
+ hostError = true
80
+ continue
81
+ }
82
+ host = v
83
+ i += 1
84
+ continue
85
+ }
86
+ if (typeof a === "string" && a.startsWith("--host=")) {
87
+ host = a.slice("--host=".length)
88
+ if (!hostValueOk(host)) hostError = true
89
+ continue
90
+ }
91
+ positionals.push(a)
92
+ }
93
+ return { cmd: positionals[0], skipGlobals, apply, host, hostError }
94
+ }
95
+
96
+ function hostExtra(host) {
97
+ return host ? ["--host", host] : []
98
+ }
99
+
100
+ function bindSpinner() {
101
+ const spin = spinner()
102
+ return (name, action) => {
103
+ if (action === "start") spin.start(name)
104
+ else if (action === "stop") spin.stop()
105
+ }
106
+ }
107
+
108
+ async function runInit(cwd, { skipGlobals, interactive }) {
109
+ if (!interactive) {
110
+ init(cwd, { skipGlobals })
111
+ return
112
+ }
113
+ if (destAlreadyInited(cwd)) {
114
+ const again = await confirm({
115
+ message: "Re-init?",
116
+ initialValue: true,
117
+ })
118
+ if (!again) return
119
+ }
120
+ const autoSkip = skipGlobals || shouldAutoSkipGlobals(cwd)
121
+ const applyGlobals = await confirm({
122
+ message: "Apply globals?",
123
+ initialValue: !autoSkip,
124
+ })
125
+ init(cwd, {
126
+ skipGlobals: !applyGlobals,
127
+ applyGlobals,
128
+ onPhase: bindSpinner(),
129
+ })
130
+ }
131
+
132
+ async function dispatch(cmd, flags, interactive) {
37
133
  if (cmd === "help" || cmd === "--help" || cmd === "-h") {
38
134
  process.stdout.write(USAGE)
39
135
  return
@@ -43,7 +139,10 @@ export function start() {
43
139
  return
44
140
  }
45
141
  if (!cmd || cmd === "init") {
46
- init(process.cwd(), { skipGlobals })
142
+ await runInit(process.cwd(), {
143
+ skipGlobals: flags.skipGlobals,
144
+ interactive: interactive && (!cmd || cmd === "init"),
145
+ })
47
146
  return
48
147
  }
49
148
  if (cmd === "generate") {
@@ -54,6 +153,40 @@ export function start() {
54
153
  npmRun("rulesync:check")
55
154
  return
56
155
  }
156
+ if (cmd === "sync-globals") {
157
+ const script = flags.apply ? "sync:globals:apply" : "sync:globals"
158
+ npmRun(script, hostExtra(flags.host))
159
+ return
160
+ }
161
+ if (cmd === "purge-mcps") {
162
+ npmRun(flags.apply ? "purge:global-mcps:apply" : "purge:global-mcps")
163
+ return
164
+ }
165
+ if (cmd === "doctor") {
166
+ npmRun("rulesync:doctor")
167
+ return
168
+ }
57
169
  process.stdout.write(USAGE)
58
170
  process.exitCode = 1
59
171
  }
172
+
173
+ export async function start() {
174
+ const flags = parseArgv(process.argv.slice(2))
175
+ if (flags.hostError) {
176
+ process.stdout.write(USAGE)
177
+ process.exitCode = 1
178
+ return
179
+ }
180
+ const interactive = isInteractive()
181
+ let cmd = flags.cmd
182
+ if (!cmd && interactive) {
183
+ const choice = await select({
184
+ message: "Command",
185
+ options: MENU_OPTIONS,
186
+ initialValue: "init",
187
+ })
188
+ if (choice === "quit") return
189
+ cmd = choice
190
+ }
191
+ await dispatch(cmd, flags, interactive)
192
+ }