@lovrozagar/grunt 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.agents/agents/grunt.md +4 -2
  2. package/.agents/hooks.json +11 -0
  3. package/.agents/skills/commit/SKILL.md +21 -0
  4. package/.agents/skills/commit-and-push/SKILL.md +15 -0
  5. package/.agents/skills/explain/SKILL.md +14 -0
  6. package/.agents/skills/handoff/SKILL.md +84 -0
  7. package/.agents/skills/parent/SKILL.md +11 -0
  8. package/.agents/skills/solo/SKILL.md +21 -0
  9. package/.claude/agents/grunt.md +2 -0
  10. package/.claude/settings.json +33 -0
  11. package/.claude/skills/commit/SKILL.md +21 -0
  12. package/.claude/skills/commit-and-push/SKILL.md +15 -0
  13. package/.claude/skills/explain/SKILL.md +14 -0
  14. package/.claude/skills/handoff/SKILL.md +84 -0
  15. package/.claude/skills/parent/SKILL.md +11 -0
  16. package/.claude/skills/solo/SKILL.md +21 -0
  17. package/.codex/agents/grunt.toml +4 -2
  18. package/.codex/hooks.json +33 -0
  19. package/.grok/agents/grunt.md +4 -2
  20. package/.grok/hooks/orchestrate-parent.js +109 -19
  21. package/.grok/skills/commit/SKILL.md +21 -0
  22. package/.grok/skills/commit-and-push/SKILL.md +15 -0
  23. package/.grok/skills/handoff/SKILL.md +84 -0
  24. package/.grok/skills/solo/SKILL.md +21 -0
  25. package/.rulesync/hooks.jsonc +8 -1
  26. package/.rulesync/reference/cascade.md +3 -2
  27. package/.rulesync/reference/hooks.md +5 -1
  28. package/.rulesync/reference/map.md +18 -3
  29. package/.rulesync/rules/CLAUDE.md +2 -1
  30. package/.rulesync/rules/overview.md +2 -1
  31. package/.rulesync/skills/commit/SKILL.md +22 -0
  32. package/.rulesync/skills/commit-and-push/SKILL.md +16 -0
  33. package/.rulesync/skills/handoff/SKILL.md +84 -0
  34. package/.rulesync/skills/solo/SKILL.md +21 -0
  35. package/.rulesync/subagents/grunt.md +6 -2
  36. package/.rulesync/subagents/implementer.md +2 -0
  37. package/.rulesync/subagents/orchestrator.md +2 -0
  38. package/.rulesync/subagents/thinker.md +2 -0
  39. package/AGENTS.md +2 -1
  40. package/CLAUDE.md +2 -1
  41. package/README.md +2 -2
  42. package/cli/init.mjs +105 -6
  43. package/cli/init.test.ts +303 -6
  44. package/package.json +12 -6
  45. package/scripts/emit-agent-shell-tools.mjs +118 -0
  46. package/scripts/emit-gemini.mjs +219 -0
  47. package/scripts/emit-mcp-policy.mjs +23 -0
  48. package/scripts/hooks-union.mjs +391 -0
  49. package/scripts/persist-handoff.mjs +231 -0
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- /** Parent-orchestrator gate: spawn/peek/kill/todo + persistPlan writes.
2
+ /** Parent-orchestrator gate: spawn/peek/kill/todo + persistPlan/persistHandoff writes.
3
3
  Parent Read/Grep/Glob/Bash/Web denied unless parent-escape (fat-gate still).
4
4
  SubagentStop intercepts need: search|exec with grunt-job verdicts.
5
- Stop: [agent]: recap | parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
5
+ Stop: [agent]:/[handoff]: recap | parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
6
6
  Fail-open: parse/crash → empty stdout, exit 0. Explicit JSON only when denying.
7
7
  */
8
8
  import fs from "node:fs";
@@ -18,13 +18,32 @@ import {
18
18
  subagentTypeOf,
19
19
  } from "../../scripts/gate-fat-tools.mjs";
20
20
  import { persistPlan } from "../../scripts/persist-plan.mjs";
21
+ import { persistHandoff } from "../../scripts/persist-handoff.mjs";
21
22
  import { parseNeed } from "../../scripts/parse-need.mjs";
22
23
  import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
23
24
  import { logTelemetry, ORCHESTRATOR_LOGS_DIR } from "../../scripts/telemetry.mjs";
24
25
 
25
26
  const DENY_REASON = "parent is orchestrator; spawn grunt|implementer|thinker";
26
- const STOP_REASON =
27
- "parent is orchestrator; spawn grunt|implementer|thinker; do not complete in-parent";
27
+ const STOP_REASONS = [
28
+ "Violation: parent replied without a [agent]: recap or /parent escape.\n" +
29
+ "DO NOT stop. Continue IN THIS RESPONSE:\n" +
30
+ "1. Spawn grunt|implementer|thinker for the pending work.\n" +
31
+ "2. Reply with the `[agent]:` recap only.\n" +
32
+ "If every spawned child already returned and the turn is done, reply with the `[agent]:` recap now.\n" +
33
+ "If context is long, use /handoff.",
34
+ "Second violation: still no [agent]: recap.\n" +
35
+ "DO NOT stop. Right now, in this same response:\n" +
36
+ "1. Spawn grunt|implementer|thinker.\n" +
37
+ "2. Then reply with only the `[agent]:` recap line.\n" +
38
+ "Already done and every child returned? Send the `[agent]:` recap immediately.\n" +
39
+ "Context too long? Use /handoff.",
40
+ "Third violation: recap still missing.\n" +
41
+ "This is the last check before fail-open. DO NOT stop:\n" +
42
+ "1. Spawn the correct agent for the remaining work.\n" +
43
+ "2. Reply with `[agent]:` recap only, nothing else.\n" +
44
+ "Complete with all children returned? Send the recap now.\n" +
45
+ "Long context? Use /handoff.",
46
+ ];
28
47
  const PARENT_TOOLS = new Set([
29
48
  "todowrite",
30
49
  "getcommandorsubagentoutput",
@@ -43,6 +62,10 @@ const READ_TOOLS = new Set(["readfile", "read"]);
43
62
  const INTERCEPT_JOBS = new Set(["search", "exec"]);
44
63
  const MAX_STOP = 3;
45
64
  const MAX_INTERCEPT = 3;
65
+ /** `/solo` enters single-agent mode; `/cascade` restores the orchestrator. */
66
+ const SOLO_RE = /^\s*\/solo\s*$/;
67
+ const CASCADE_RE = /^\s*\/cascade\s*$/;
68
+ const SOLO_STAMP = "grunt-off";
46
69
 
47
70
 
48
71
  function main() {
@@ -76,6 +99,13 @@ function preToolUse(data) {
76
99
  const code = emitFat(data);
77
100
  return code == null ? 0 : code;
78
101
  }
102
+ if (isSoloMode(data)) {
103
+ // Single-agent session: no parent-deny, no spawn rewrite. Fat gate still applies.
104
+ const fatCode = emitFat(data);
105
+ if (fatCode !== null) return fatCode;
106
+ emit({ decision: "allow" });
107
+ return 0;
108
+ }
79
109
  if (SPAWN_TOOLS.has(toolKey)) {
80
110
  const updated = rewriteSpawn(toolInput);
81
111
  if (updated) {
@@ -107,6 +137,16 @@ function preToolUse(data) {
107
137
  return 0;
108
138
  }
109
139
 
140
+ /** Session flag, not one-turn. Fail-closed: an unreadable stamp keeps grunt on. */
141
+ export function isSoloMode(data) {
142
+ try {
143
+ const p = soloStampPath(data);
144
+ return Boolean(p && fs.existsSync(p));
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
110
150
  function hasParentEscape(data) {
111
151
  const p = stampPath(data, "parent-escape");
112
152
  return Boolean(p && fs.existsSync(p));
@@ -146,21 +186,32 @@ function emitFat(data) {
146
186
  function workspaceRootOf(data) {
147
187
  return (
148
188
  process.env.GROK_WORKSPACE_ROOT ||
149
- (data && (data.workspaceRoot || data.workspace_root || data.cwd)) ||
189
+ (data && (data.workspaceRoot || data.workspace_root)) ||
190
+ (data && data.cwd) ||
191
+ process.env.CLAUDE_PROJECT_DIR ||
192
+ process.cwd() ||
150
193
  ""
151
194
  );
152
195
  }
153
196
 
154
- export function isUnderPlans(filePath, workspaceRoot) {
197
+ function isUnderDir(filePath, workspaceRoot, segments) {
155
198
  if (!filePath || !workspaceRoot) return false;
156
199
  const abs = path.isAbsolute(filePath)
157
200
  ? path.resolve(filePath)
158
201
  : path.resolve(workspaceRoot, filePath);
159
- const plans = path.resolve(workspaceRoot, ".tmp", "plans");
160
- const rel = path.relative(plans, abs);
202
+ const dir = path.resolve(workspaceRoot, ...segments);
203
+ const rel = path.relative(dir, abs);
161
204
  return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
162
205
  }
163
206
 
207
+ export function isUnderPlans(filePath, workspaceRoot) {
208
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "plans"]);
209
+ }
210
+
211
+ export function isUnderHandoffs(filePath, workspaceRoot) {
212
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "handoffs"]);
213
+ }
214
+
164
215
  export function isAllowedParentGruntJob(command, workspaceRoot) {
165
216
  return isWorkspaceGruntJob(command, workspaceRoot, ["search", "exec"]);
166
217
  }
@@ -177,16 +228,18 @@ function parentWrite(data, toolInput) {
177
228
  toolInput.path ||
178
229
  toolInput.target_file ||
179
230
  "";
180
- if (!isUnderPlans(rawPath, ws)) {
231
+ const handoff = isUnderHandoffs(rawPath, ws);
232
+ if (!handoff && !isUnderPlans(rawPath, ws)) {
181
233
  emit({ decision: "deny", reason: DENY_REASON });
182
234
  return 0;
183
235
  }
184
236
  const content = typeof toolInput.content === "string" ? toolInput.content : "";
185
- const result = persistPlan({ workspaceRoot: ws, content });
237
+ const persist = handoff ? persistHandoff : persistPlan;
238
+ const result = persist({ workspaceRoot: ws, content });
186
239
  if (!result.ok) {
187
240
  emit({
188
241
  decision: "deny",
189
- reason: result.error || "invalid plan",
242
+ reason: result.error || (handoff ? "invalid handoff" : "invalid plan"),
190
243
  });
191
244
  return 0;
192
245
  }
@@ -229,7 +282,18 @@ function isParentEscapePrompt(prompt) {
229
282
  function userPromptSubmit(data) {
230
283
  unlinkQuiet(stampPath(data, "tools-used"));
231
284
  unlinkQuiet(stampPath(data, "stop-block"));
232
- if (isParentEscapePrompt(userPromptOf(data))) {
285
+ const prompt = userPromptOf(data);
286
+ // Sticky: only /solo and /cascade move it. Every other prompt leaves it alone.
287
+ if (SOLO_RE.test(prompt)) {
288
+ const solo = soloStampPath(data);
289
+ if (solo) {
290
+ fs.mkdirSync(path.dirname(solo), { recursive: true });
291
+ fs.writeFileSync(solo, "1");
292
+ }
293
+ } else if (CASCADE_RE.test(prompt)) {
294
+ unlinkQuiet(soloStampPath(data));
295
+ }
296
+ if (isParentEscapePrompt(prompt)) {
233
297
  const p = stampPath(data, "parent-escape");
234
298
  if (p) {
235
299
  fs.mkdirSync(path.dirname(p), { recursive: true });
@@ -242,16 +306,29 @@ function userPromptSubmit(data) {
242
306
  }
243
307
 
244
308
  function isChildRecap(msg) {
245
- return /^\s*\[(?:grunt|implementer|thinker)\]:/.test(String(msg || ""));
309
+ const lines = String(msg || "").split("\n");
310
+ let firstNonEmpty = "";
311
+ for (const line of lines) {
312
+ if (line.trim() !== "") {
313
+ firstNonEmpty = line;
314
+ break;
315
+ }
316
+ }
317
+ const stripped = firstNonEmpty.replace(/^[\s`*_>]+/, "");
318
+ return /^\[(?:grunt|implementer|thinker|handoff)\]:/.test(stripped);
246
319
  }
247
320
 
248
321
  function stop(data) {
249
322
  if (data.subagentType || data.subagent_type) {
250
323
  return interceptNeed(data, "Stop");
251
324
  }
325
+ if (data.stopHookActive || data.stop_hook_active) return 0;
252
326
  const reason = String(data.reason || "");
253
327
  if (reason && reason !== "end_turn") return 0;
254
328
 
329
+ // Before the parent-escape consume: solo must never burn the one-turn stamp.
330
+ if (isSoloMode(data)) return 0;
331
+
255
332
  const escapeStamp = stampPath(data, "parent-escape");
256
333
  if (escapeStamp && fs.existsSync(escapeStamp)) {
257
334
  unlinkQuiet(escapeStamp);
@@ -272,7 +349,8 @@ function stop(data) {
272
349
  fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
273
350
  fs.writeFileSync(stopStamp, String(n + 1));
274
351
  }
275
- emit({ decision: "block", reason: STOP_REASON });
352
+ const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
353
+ emit({ decision: "block", reason: reasonText });
276
354
  return 0;
277
355
  }
278
356
 
@@ -368,7 +446,6 @@ function interceptNeed(data, hookEventName) {
368
446
  );
369
447
  emit({
370
448
  decision: "block",
371
- reason,
372
449
  hookSpecificOutput: {
373
450
  hookEventName,
374
451
  additionalContext: reason,
@@ -379,14 +456,27 @@ function interceptNeed(data, hookEventName) {
379
456
 
380
457
  function stampPath(data, prefix) {
381
458
  const root = workspaceRootOf(data);
382
- const sid =
383
- process.env.GROK_SESSION_ID ||
384
- (data && (data.sessionId || data.session_id)) ||
385
- "default";
459
+ const sid = sessionIdOf(data) || "default";
386
460
  if (!root) return null;
387
461
  return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
388
462
  }
389
463
 
464
+ function sessionIdOf(data) {
465
+ return String(
466
+ process.env.GROK_SESSION_ID ||
467
+ (data && (data.sessionId || data.session_id)) ||
468
+ "",
469
+ );
470
+ }
471
+
472
+ /** Solo is a mode: never share a `default` stamp across sid-less sessions. */
473
+ function soloStampPath(data) {
474
+ const root = workspaceRootOf(data);
475
+ const sid = sessionIdOf(data);
476
+ if (!root || !sid) return null;
477
+ return path.join(root, ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
478
+ }
479
+
390
480
  function unlinkQuiet(p) {
391
481
  if (!p) return;
392
482
  try {
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: commit
3
+ description: >
4
+ Inspect the diff and commit with a Conventional Commits subject line. Use for
5
+ /commit, "commit this", "commit these changes". Never pushes.
6
+ ---
7
+ Inspect `git status` + `git diff` (staged and unstaged) before the message. Never push.
8
+
9
+ Stage only user intent. Never blind `git add -A` / `git add .`. Split unrelated concerns into multiple commits.
10
+
11
+ Message = Conventional Commits **subject only**: `type(scope): subject` (scope optional).
12
+
13
+ Types: feat fix docs style refactor perf test build ci chore revert.
14
+
15
+ Subject: imperative lowercase no trailing period.
16
+
17
+ NO body. NO footer. NO `Co-Authored-By`. NO `Generated with`. NO 🤖. NO trailers. No AI attribution. Work belongs to the human.
18
+
19
+ Never `git push`.
20
+
21
+ Protocol: `.rulesync/reference/cascade.md`. Do not paste cascade.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: commit-and-push
3
+ description: >
4
+ Commit like /commit, then push to the current branch's upstream. Use for
5
+ /commit-and-push, "commit and push", "commit this and push".
6
+ ---
7
+ Run commit skill rules first (`.rulesync/skills/commit/SKILL.md`). Then push.
8
+
9
+ If on default branch, create/switch to a feature branch first — never push default directly.
10
+
11
+ Push upstream; if no upstream `git push -u origin <current-branch>`.
12
+
13
+ Never force-push.
14
+
15
+ Protocol: `.rulesync/reference/cascade.md`. Do not paste cascade.
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: handoff
3
+ description: >
4
+ Parent writes a session handoff under .tmp/grunt/handoffs/ and tells the user
5
+ to continue in a fresh session. Use for /handoff, "context is getting long",
6
+ "hand this off", "start a new session". Not a plan, not a design doc.
7
+ ---
8
+ # handoff
9
+
10
+ One turn, in-parent. Parent authors from its own transcript — no child sees this session, so **do not spawn**. Write once, report path, stop.
11
+
12
+ ## Invocation
13
+
14
+ `/handoff [focus]`
15
+
16
+ Focus narrows what the handoff carries. Empty → whole session.
17
+
18
+ ## When
19
+
20
+ Context large + work unfinished → write handoff, then advise a new session. Also on host/tool churn, or before a long unrelated detour.
21
+
22
+ Nothing done yet (no edits, no findings) → `nothing to hand off` and stop. Do not write an empty file.
23
+
24
+ ## Path
25
+
26
+ `.tmp/grunt/handoffs/{serial}-{slug}-{YYYYMMDDTHHMMSSZ}.md`
27
+
28
+ - serial: unpadded int ≥ 1, next after existing handoffs (own counter; not plan serials)
29
+ - slug: lower, non `[a-z0-9]` → `-`, collapse, trim 50
30
+ - stamp: `created` with `-` and `:` dropped (`20260827T143000Z`)
31
+
32
+ ## Format
33
+
34
+ ```yaml
35
+ ---
36
+ serial: 1
37
+ name: <slug>
38
+ status: open # open | resumed | done
39
+ created: 2026-08-27T14:30:00Z # UTC, seconds, no ms
40
+ source: "<≤120 chars of the session task>"
41
+ ---
42
+ ```
43
+
44
+ Headings, this order only: `# {slug}`, `## Goal`, `## State`, `## Context`, `## Next`, `## Watch-outs`.
45
+
46
+ | Section | Amount |
47
+ |---|---|
48
+ | Goal | 1–3 sentences. What the session is trying to land. |
49
+ | State | bullets. Done / changed / green-red, with absolute paths. |
50
+ | Context | bullets. Abs paths, decisions, verdict facts the next session needs. |
51
+ | Next | checkbox steps, `N [ ]` phase + `N.M [ ]` leaf, all `[ ]` fresh |
52
+ | Watch-outs | bullets. Footguns. `(none)` allowed. |
53
+
54
+ Boxes: `^{id} [ ] {text}$`, id `N` / `N.M` / `N.M.K`. Not `- [ ]`, not `1. [ ]`. Every phase has ≥1 leaf. Fresh handoff has zero `[x]`.
55
+
56
+ Facts only. Absolute paths. No transcript paste, no dump, no secrets/tokens.
57
+
58
+ ## Write
59
+
60
+ Line 1 of the body: `HANDOFF_NAME: <3-6 word name>`, then the body only.
61
+
62
+ Parent `write`s under `.tmp/grunt/handoffs/` (any filename in that dir). Grok `orchestrate-parent.js` runs `scripts/persist-handoff.mjs`: serial/slug from `HANDOFF_NAME:` / heading, injects frontmatter, rewrites path + content. Invalid handoff or a write outside that dir is denied — fix the body, do not retry elsewhere.
63
+
64
+ Host without that hook: name the file per **Path** yourself, or pipe the body to `node scripts/persist-handoff.mjs --workspace {repo}` and use its `path`.
65
+
66
+ ## Report
67
+
68
+ ```
69
+ [handoff]: serial={int} path=.tmp/grunt/handoffs/{serial}-{slug}-{stamp}.md
70
+ next: start a new session; first action = read that path
71
+ ```
72
+
73
+ Do not dump the handoff body.
74
+
75
+ ## Picking one up
76
+
77
+ New session, `/handoff` not needed: read the newest `.tmp/grunt/handoffs/*.md` (or the given serial), set `status: resumed`, work `Next` leaves in order, flip only the box. All `[x]` → `status: done`. Do not re-plan; a stale leaf → `/write-plan` a follow-up.
78
+
79
+ ## Rules
80
+
81
+ - No spawn. One write. `/handoff` is not a mode.
82
+ - Handoff ≠ plan: no Steps/Verify phases, no `.tmp/plans/` write.
83
+ - Never rewrite or renumber an existing handoff; new session = new serial.
84
+ - Protocol: `.rulesync/reference/cascade.md`. Do not paste it here.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: solo
3
+ description: >
4
+ Session mode. /solo disables the grunt cascade for this session — one normal
5
+ agent, no spawn, no orchestrator. /cascade restores it. Unlike /parent and
6
+ /explain this IS a mode: it persists until /cascade or session end.
7
+ ---
8
+ Session mode, not one-turn. Persists until `/cascade` or session end.
9
+
10
+ This session: cascade suspended. You are one normal agent.
11
+
12
+ - Do not spawn. No grunt|implementer|thinker.
13
+ - No `[agent]:` recap. Answer the user directly.
14
+ - No spawn-first rule. No `need:`/`verdict:` grammar.
15
+ - Voice unchanged: `.rulesync/reference/output.md`. `/explain` is still the voice escape.
16
+
17
+ Fat-tool caps stay on — token guards, not cascade.
18
+
19
+ Claude/Codex/Grok: `/solo` via submit hook writes `.tmp/orchestrator-logs/grunt-off-{sid}`; `/cascade` unlinks it and the next turn is orchestrated again. Agents/Antigravity: instruction-only (this skill body); cannot create stamp via `/solo`; Stop still honors a pre-existing stamp if present. Session-scoped, never global.
20
+
21
+ For advisory/design turns, debugging the cascade itself, or back-and-forth where spawning costs more than it saves.
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  // Parent Stop + /parent stamp. PreToolUse parent-deny stays Grok-only
22
22
  // (child subagentType/agent_type unverified on these hosts).
23
- "userPromptSubmit": [
23
+ "beforeSubmitPrompt": [
24
24
  {
25
25
  "type": "command",
26
26
  "command": "node \"${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/.grok/hooks/orchestrate-parent.js\"",
@@ -33,6 +33,13 @@
33
33
  "command": "node \"${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/.grok/hooks/orchestrate-parent.js\"",
34
34
  "timeout": 30
35
35
  }
36
+ ],
37
+ "subagentStop": [
38
+ {
39
+ "type": "command",
40
+ "command": "node \"${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/.grok/hooks/orchestrate-parent.js\"",
41
+ "timeout": 30
42
+ }
36
43
  ]
37
44
  }
38
45
  }
@@ -30,8 +30,9 @@ Classify `done` | `alive` | `stuck` vs prior peek: **done** (completed/failed),
30
30
  Loop until done. No auto-kill. Kill only if the user asks and the host has a kill tool.
31
31
  Then print recap as `[agent]: …` with real child output. Do not redo the child's work.
32
32
 
33
- Parent Stop (`MAX_STOP=3`): do not waive fenced/impl finals for `tools-used` stamp. Allow only `[grunt|implementer|thinker]:` recap prefix, or consume `parent-escape-{sid}` once. Else block. No `isCheap` / trivia / long definitions.
34
- `/parent` is one-turn (not a mode): UserPromptSubmit writes `.tmp/orchestrator-logs/parent-escape-{sid}`; Stop consumes+unlinks once. Next Stop resumes spawn/recap rules. Skills point here; do not paste this file.
33
+ Parent Stop (`MAX_STOP=3`): do not waive fenced/impl finals for `tools-used` stamp. Allow only `[grunt|implementer|thinker|handoff]:` recap prefix, or consume `parent-escape-{sid}` once. Else block. No `isCheap` / trivia / long definitions.
34
+ `/handoff` is the other in-parent turn: context large + work unfinished → parent writes one `.tmp/grunt/handoffs/` file from its own transcript (no child sees the session), recaps `[handoff]: serial=… path=…`, tells the user to continue in a new session. No spawn, one write, not a mode.
35
+ `/solo` is the session mode (not one-turn): `beforeSubmitPrompt` writes `.tmp/orchestrator-logs/grunt-off-{sid}`, every gate short-circuits while it exists, `/cascade` unlinks it. Everything above resumes next turn. Antigravity: skill-only, no stamp create (see hooks.md). `/parent` is one-turn (not a mode): UserPromptSubmit writes `.tmp/orchestrator-logs/parent-escape-{sid}`; Stop consumes+unlinks once. Next Stop resumes spawn/recap rules. Skills point here; do not paste this file.
35
36
 
36
37
  Host mapping (in-tree only; do not invent peek/kill APIs). GAP rows: no fake peeks, no auto-kill.
37
38
 
@@ -12,7 +12,11 @@ Claude / Codex / Antigravity PreToolUse still run `scripts/scrub-spawn-prompt.mj
12
12
 
13
13
  Parent `run_terminal_command` is deny-by-default except `node` (optional leading `rtk`) invoking `{workspace}/scripts/grunt-job.mjs` with `--job search|exec` and `--query`. Optional `--path` `--glob` (repeatable) `--cwd`. Not `web|test`, not other scripts. META strips `--query` value first so `|` inside query is allowed; pipes/`&&` outside query still deny. Never `cd &&`. Exec shell-meta in query is grunt-job FALLBACK (exit 2), not a parent-bash allow. Unknown flags → FALLBACK.
14
14
 
15
- SubagentStop (and child Stop with `subagentType`) intercepts a parseable `need:` whose jobs are all `search|exec` (cap 4): runs grunt-job in-hook and continues the child with concatenated `verdict:` blobs. Mixed/web/test, parse fail, FALLBACK, `stopHookActive`, or more than 3 intercepts per child session → empty stdout (allow stop; parent LLM path). Parent Stop (`MAX_STOP=3`): do not waive impl finals for `tools-used`. Allow only `[grunt|implementer|thinker]:` recap prefix, or consume `parent-escape-{sid}` once (`/parent`). No `isCheap` / trivia. Stamps: `tools-used` / `stop-block` / `need-intercept` / `parent-escape`. Skills point to cascade; do not paste protocol here.
15
+ SubagentStop (and child Stop with `subagentType`) intercepts a parseable `need:` whose jobs are all `search|exec` (cap 4): runs grunt-job in-hook and continues the child with concatenated `verdict:` blobs. Mixed/web/test, parse fail, FALLBACK, `stopHookActive`, or more than 3 intercepts per child session → empty stdout (allow stop; parent LLM path). Parent `write` is deny-by-default except `.tmp/plans/` (`persist-plan.mjs`) and `.tmp/grunt/handoffs/` (`persist-handoff.mjs`); both validate + rewrite path/content, invalid body denies. Parent Stop (`MAX_STOP=3`): do not waive impl finals for `tools-used`. Allow only `[grunt|implementer|thinker|handoff]:` recap prefix, or consume `parent-escape-{sid}` once (`/parent`). No `isCheap` / trivia. Stamps: `tools-used` / `stop-block` / `need-intercept` / `parent-escape`. Skills point to cascade; do not paste protocol here.
16
+
17
+ `/solo` writes `grunt-off-{sid}`; `/cascade` unlinks it (`beforeSubmitPrompt`, exact-match only). While set: PreToolUse allows parent tools (fat gate still runs) and skips the `defaultGrunt` spawn rewrite; Stop returns 0 **before** the `parent-escape` consume, so solo never burns the one-turn stamp. Session-scoped, fail-closed.
18
+
19
+ rulesync canonical event is `beforeSubmitPrompt` (→ Claude/Codex `UserPromptSubmit`), not `userPromptSubmit` — an unknown key makes Zod reject the whole `.rulesync/hooks.jsonc`, emitting nothing while `--check` still reports up to date. Antigravity supports neither `beforeSubmitPrompt` nor `subagentStop`; `/solo` degrades there to the skill body alone.
16
20
 
17
21
  Stop and SubagentStop timeout is 30s (grunt-job cap is 20s). PreToolUse / PostToolUse / UserPromptSubmit stay at 5s. If the intercept times out, the hook fail-opens and the child stops without verdicts.
18
22
 
@@ -22,13 +22,14 @@ Cheap outline. Not a file dump.
22
22
  ## Grok hand files
23
23
  - `.grok/hooks/orchestrate-parent.js` + `orchestrate-parent.json` — parent spawn/fat/stop/plan-write / SubagentStop intercept
24
24
  - `.grok/hooks/rtk.json` — RTK
25
- - `.grok/skills/{write-plan,implement-plan,shared,parent}/` — `/parent` one-turn; point to cascade, do not paste
25
+ - `.grok/skills/{write-plan,implement-plan,shared}/` — hand-only (no rulesync SSOT); point to cascade, do not paste
26
+ - `.grok/skills/{parent,explain,handoff,solo,commit,commit-and-push}/` — generated from `.rulesync/skills/`; do not hand-edit
26
27
  - `.grok/parent.md` — 1-line pointer to orchestrator agent; not SessionStart
27
28
  - `.grok/roles/*.toml`
28
29
  - `.grok/global-settings.toml` — merged into `~/.grok/config.toml` by `scripts/sync-global-settings.mjs` (not auto-loaded; project config cannot set `[features]`)
29
30
  - `.rulesync/global-settings/` — host manifest and reserved noop payloads
30
31
  - `.rulesync/mcp-policy.jsonc` — MCP deny-default SSOT (`default: deny`, `allow: []`)
31
- - `.rulesync/skills/parent/` — `/parent` skill SSOT; copy lives in `.grok/skills/parent/`
32
+ - `.rulesync/skills/{parent,explain,handoff,solo,commit,commit-and-push}/` — skill SSOT; `rulesync -f skills` emits `.grok/skills/`, `.claude/skills/`, `.agents/skills/` byte-equal. `/cascade` has no skill (hook-only: a `grunt`-adjacent listing entry risks mis-routing `Skill` vs `Agent`)
32
33
 
33
34
  ## Scripts
34
35
  - `scripts/check-globals.mjs` — `$HOME/.grok/config.toml` `[agent].name==orchestrator` + `[features].two_pass_compaction==true`; project `.grok/config.toml` must not have `[features]`/`[agent]`; wired as `sync:globals:check` (also after emit-mcp in `rulesync:check`)
@@ -36,17 +37,28 @@ Cheap outline. Not a file dump.
36
37
  - `scripts/scrub-spawn-prompt.mjs`
37
38
  - `scripts/grunt-job.mjs` — flags `--job` `--query` `--path` `--glob` (repeatable) `--cwd`; regex in `--query` OK; never `cd &&`; unknown flags / exec shell-meta → FALLBACK
38
39
  - `scripts/parse-need.mjs`
40
+ - `scripts/persist-handoff.mjs` — `.tmp/grunt/handoffs/{serial}-{slug}-{stamp}.md`; `HANDOFF_NAME:` → slug; H2 `Goal|State|Context|Next|Watch-outs`; `status` `open|resumed|done`
39
41
  - `scripts/persist-plan.mjs`
40
42
  - `scripts/sync-global-settings.mjs` — dry-run-by-default merge of `.grok/global-settings.toml` into `$HOME/.grok/config.toml`; manifest in `.rulesync/global-settings/`; `skipKeyPattern` skips MCP; never copies project MCP into `$HOME/.grok`
41
43
  - `scripts/purge-global-mcps.mjs` — dry-run-by-default purge of stubborn global MCP sources (`$HOME/.grok/config.toml` plugins + `disabled_mcp_servers`, `$HOME/.cursor/mcp.json` `MCP_DOCKER`); `--apply` writes; never project MCP
42
- - `scripts/emit-mcp-policy.mjs` — emit project MCP deny-default from `.rulesync/mcp-policy.jsonc` (not `rulesync -f mcp`)
44
+ - `scripts/emit-mcp-policy.mjs` — emit project MCP deny-default from `.rulesync/mcp-policy.jsonc` (not `rulesync -f mcp`); owns `.gemini/settings.json` `mcpServers` (merge extra keys)
45
+ - `scripts/emit-gemini.mjs` — `GEMINI.md` (`@AGENTS.md`) + `.gemini/agents/{id}/agent.md` from `.rulesync/subagents` (not `rulesync -t geminicli`); no prune; does not touch settings
46
+ - `scripts/emit-agent-shell-tools.mjs` — after rulesync subagents: rewrite `.claude/agents/grunt.md` body `Bash`; grok/codex/gemini/generic keep `run_terminal_command`
43
47
  - `scripts/telemetry.mjs` — append-only `.tmp/orchestrator-logs/telemetry.ndjson` from hooks / grunt-job CLI; fail-open; never stdout
44
48
 
49
+ ## Tmp
50
+ - `.tmp/plans/` — persist-plan / parent / implementer
51
+ - `.tmp/orchestrator-logs/` — hooks + grunt-job telemetry (shared; not grunt scratch)
52
+ - `.tmp/grunt/` — grunt scratch/tmp only
53
+ - `.tmp/grunt/handoffs/` — persist-handoff / `/handoff`; own serial counter (not plan serials)
54
+
45
55
  ## Stamps (`.tmp/orchestrator-logs/`)
46
56
  - `tools-used` / `stop-block` / `need-intercept` / `parent-escape` — Stop does not waive impl finals for `tools-used`; `/parent` writes `parent-escape-{sid}` once
57
+ - `grunt-off-{sid}` — `/solo` session mode. Sticky: only `/solo` / `/cascade` move it. Requires a real sid (never the `default` fallback). Read fail-closed — an unreadable stamp keeps grunt on
47
58
 
48
59
  ## Generated (do not hand-edit; committed, not gitignored)
49
60
  - `AGENTS.md`, `CLAUDE.md`
61
+ - `.claude/skills/*`, `.agents/skills/*`, `.grok/skills/{parent,explain,handoff,solo,commit,commit-and-push}/` (from `.rulesync/skills/`)
50
62
  - `.grok/agents/*`
51
63
  - `.claude/agents/*`
52
64
  - `.codex/agents/*`
@@ -56,3 +68,6 @@ Cheap outline. Not a file dump.
56
68
  - `.claude/settings.json` MCP keys (`enableAllProjectMcpServers`, `enabledMcpjsonServers`, `permissions.deny` `mcp__*`); Agent denies/hooks stay
57
69
  - `.codex/config.toml` `mcp_servers` (absent when allow empty); keep `[agents] max_depth`
58
70
  - `.agents/mcp_config.json` — `mcpServers` from policy allow only
71
+ - `GEMINI.md` — `@AGENTS.md` pointer (`emit-gemini.mjs`)
72
+ - `.gemini/agents/{orchestrator,implementer,thinker,grunt}/agent.md` — `name` `description` `model` only (`emit-gemini.mjs`)
73
+ - `.gemini/settings.json` — `mcpServers` from policy allow only (`emit-mcp-policy.mjs`; extra keys preserved)
@@ -4,10 +4,11 @@ targets:
4
4
  - claudecode
5
5
  ---
6
6
  Voice: `.rulesync/reference/output.md`
7
+ All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
7
8
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
8
9
  Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
9
10
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
10
- First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn.
11
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
11
12
  grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution. thinker ← plan/deep reason.
12
13
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
13
14
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. `timeout_ms=60000` every peek. GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
@@ -9,10 +9,11 @@ globs:
9
9
  - "**/*"
10
10
  ---
11
11
  Voice: `.rulesync/reference/output.md` once.
12
+ All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
12
13
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
13
14
  Parent = orchestrator. You do not talk. Spawn wait echo.
14
15
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
15
- First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn.
16
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[agent]:` echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
16
17
  grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution. thinker ← plan/deep reason.
17
18
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
18
19
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. `timeout_ms=60000` every peek. GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: commit
3
+ description: >
4
+ Inspect the diff and commit with a Conventional Commits subject line. Use for
5
+ /commit, "commit this", "commit these changes". Never pushes.
6
+ ---
7
+
8
+ Inspect `git status` + `git diff` (staged and unstaged) before the message. Never push.
9
+
10
+ Stage only user intent. Never blind `git add -A` / `git add .`. Split unrelated concerns into multiple commits.
11
+
12
+ Message = Conventional Commits **subject only**: `type(scope): subject` (scope optional).
13
+
14
+ Types: feat fix docs style refactor perf test build ci chore revert.
15
+
16
+ Subject: imperative lowercase no trailing period.
17
+
18
+ NO body. NO footer. NO `Co-Authored-By`. NO `Generated with`. NO 🤖. NO trailers. No AI attribution. Work belongs to the human.
19
+
20
+ Never `git push`.
21
+
22
+ Protocol: `.rulesync/reference/cascade.md`. Do not paste cascade.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: commit-and-push
3
+ description: >
4
+ Commit like /commit, then push to the current branch's upstream. Use for
5
+ /commit-and-push, "commit and push", "commit this and push".
6
+ ---
7
+
8
+ Run commit skill rules first (`.rulesync/skills/commit/SKILL.md`). Then push.
9
+
10
+ If on default branch, create/switch to a feature branch first — never push default directly.
11
+
12
+ Push upstream; if no upstream `git push -u origin <current-branch>`.
13
+
14
+ Never force-push.
15
+
16
+ Protocol: `.rulesync/reference/cascade.md`. Do not paste cascade.
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: handoff
3
+ description: >
4
+ Parent writes a session handoff under .tmp/grunt/handoffs/ and tells the user
5
+ to continue in a fresh session. Use for /handoff, "context is getting long",
6
+ "hand this off", "start a new session". Not a plan, not a design doc.
7
+ ---
8
+ # handoff
9
+
10
+ One turn, in-parent. Parent authors from its own transcript — no child sees this session, so **do not spawn**. Write once, report path, stop.
11
+
12
+ ## Invocation
13
+
14
+ `/handoff [focus]`
15
+
16
+ Focus narrows what the handoff carries. Empty → whole session.
17
+
18
+ ## When
19
+
20
+ Context large + work unfinished → write handoff, then advise a new session. Also on host/tool churn, or before a long unrelated detour.
21
+
22
+ Nothing done yet (no edits, no findings) → `nothing to hand off` and stop. Do not write an empty file.
23
+
24
+ ## Path
25
+
26
+ `.tmp/grunt/handoffs/{serial}-{slug}-{YYYYMMDDTHHMMSSZ}.md`
27
+
28
+ - serial: unpadded int ≥ 1, next after existing handoffs (own counter; not plan serials)
29
+ - slug: lower, non `[a-z0-9]` → `-`, collapse, trim 50
30
+ - stamp: `created` with `-` and `:` dropped (`20260827T143000Z`)
31
+
32
+ ## Format
33
+
34
+ ```yaml
35
+ ---
36
+ serial: 1
37
+ name: <slug>
38
+ status: open # open | resumed | done
39
+ created: 2026-08-27T14:30:00Z # UTC, seconds, no ms
40
+ source: "<≤120 chars of the session task>"
41
+ ---
42
+ ```
43
+
44
+ Headings, this order only: `# {slug}`, `## Goal`, `## State`, `## Context`, `## Next`, `## Watch-outs`.
45
+
46
+ | Section | Amount |
47
+ |---|---|
48
+ | Goal | 1–3 sentences. What the session is trying to land. |
49
+ | State | bullets. Done / changed / green-red, with absolute paths. |
50
+ | Context | bullets. Abs paths, decisions, verdict facts the next session needs. |
51
+ | Next | checkbox steps, `N [ ]` phase + `N.M [ ]` leaf, all `[ ]` fresh |
52
+ | Watch-outs | bullets. Footguns. `(none)` allowed. |
53
+
54
+ Boxes: `^{id} [ ] {text}$`, id `N` / `N.M` / `N.M.K`. Not `- [ ]`, not `1. [ ]`. Every phase has ≥1 leaf. Fresh handoff has zero `[x]`.
55
+
56
+ Facts only. Absolute paths. No transcript paste, no dump, no secrets/tokens.
57
+
58
+ ## Write
59
+
60
+ Line 1 of the body: `HANDOFF_NAME: <3-6 word name>`, then the body only.
61
+
62
+ Parent `write`s under `.tmp/grunt/handoffs/` (any filename in that dir). Grok `orchestrate-parent.js` runs `scripts/persist-handoff.mjs`: serial/slug from `HANDOFF_NAME:` / heading, injects frontmatter, rewrites path + content. Invalid handoff or a write outside that dir is denied — fix the body, do not retry elsewhere.
63
+
64
+ Host without that hook: name the file per **Path** yourself, or pipe the body to `node scripts/persist-handoff.mjs --workspace {repo}` and use its `path`.
65
+
66
+ ## Report
67
+
68
+ ```
69
+ [handoff]: serial={int} path=.tmp/grunt/handoffs/{serial}-{slug}-{stamp}.md
70
+ next: start a new session; first action = read that path
71
+ ```
72
+
73
+ Do not dump the handoff body.
74
+
75
+ ## Picking one up
76
+
77
+ New session, `/handoff` not needed: read the newest `.tmp/grunt/handoffs/*.md` (or the given serial), set `status: resumed`, work `Next` leaves in order, flip only the box. All `[x]` → `status: done`. Do not re-plan; a stale leaf → `/write-plan` a follow-up.
78
+
79
+ ## Rules
80
+
81
+ - No spawn. One write. `/handoff` is not a mode.
82
+ - Handoff ≠ plan: no Steps/Verify phases, no `.tmp/plans/` write.
83
+ - Never rewrite or renumber an existing handoff; new session = new serial.
84
+ - Protocol: `.rulesync/reference/cascade.md`. Do not paste it here.