@lovrozagar/grunt 0.1.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 (64) hide show
  1. package/.agents/agents/grunt.md +14 -15
  2. package/.agents/agents/implementer.md +9 -21
  3. package/.agents/agents/orchestrator.md +10 -38
  4. package/.agents/agents/thinker.md +7 -11
  5. package/.agents/hooks.json +11 -0
  6. package/.agents/skills/commit/SKILL.md +21 -0
  7. package/.agents/skills/commit-and-push/SKILL.md +15 -0
  8. package/.agents/skills/explain/SKILL.md +14 -0
  9. package/.agents/skills/handoff/SKILL.md +84 -0
  10. package/.agents/skills/parent/SKILL.md +11 -0
  11. package/.agents/skills/solo/SKILL.md +21 -0
  12. package/.claude/agents/grunt.md +15 -18
  13. package/.claude/agents/implementer.md +9 -24
  14. package/.claude/agents/orchestrator.md +11 -41
  15. package/.claude/agents/thinker.md +7 -14
  16. package/.claude/settings.json +34 -0
  17. package/.claude/skills/commit/SKILL.md +21 -0
  18. package/.claude/skills/commit-and-push/SKILL.md +15 -0
  19. package/.claude/skills/explain/SKILL.md +14 -0
  20. package/.claude/skills/handoff/SKILL.md +84 -0
  21. package/.claude/skills/parent/SKILL.md +11 -0
  22. package/.claude/skills/solo/SKILL.md +21 -0
  23. package/.codex/agents/grunt.toml +15 -16
  24. package/.codex/agents/implementer.toml +10 -22
  25. package/.codex/agents/orchestrator.toml +11 -39
  26. package/.codex/agents/thinker.toml +8 -12
  27. package/.codex/hooks.json +33 -0
  28. package/.grok/agents/grunt.md +14 -15
  29. package/.grok/agents/implementer.md +9 -21
  30. package/.grok/agents/orchestrator.md +10 -39
  31. package/.grok/agents/thinker.md +7 -12
  32. package/.grok/hooks/orchestrate-parent.js +126 -73
  33. package/.grok/roles/thinker.toml +1 -1
  34. package/.grok/skills/commit/SKILL.md +21 -0
  35. package/.grok/skills/commit-and-push/SKILL.md +15 -0
  36. package/.grok/skills/handoff/SKILL.md +84 -0
  37. package/.grok/skills/solo/SKILL.md +21 -0
  38. package/.rulesync/hooks.jsonc +23 -0
  39. package/.rulesync/reference/cascade.md +13 -19
  40. package/.rulesync/reference/hooks.md +5 -1
  41. package/.rulesync/reference/map.md +20 -4
  42. package/.rulesync/reference/output.md +7 -0
  43. package/.rulesync/rules/CLAUDE.md +11 -8
  44. package/.rulesync/rules/overview.md +11 -13
  45. package/.rulesync/skills/commit/SKILL.md +22 -0
  46. package/.rulesync/skills/commit-and-push/SKILL.md +16 -0
  47. package/.rulesync/skills/handoff/SKILL.md +84 -0
  48. package/.rulesync/skills/solo/SKILL.md +21 -0
  49. package/.rulesync/subagents/grunt.md +20 -18
  50. package/.rulesync/subagents/implementer.md +15 -24
  51. package/.rulesync/subagents/orchestrator.md +16 -42
  52. package/.rulesync/subagents/thinker.md +13 -15
  53. package/AGENTS.md +11 -12
  54. package/CLAUDE.md +11 -7
  55. package/README.md +11 -9
  56. package/cli/init.mjs +105 -6
  57. package/cli/init.test.ts +303 -6
  58. package/package.json +12 -6
  59. package/scripts/emit-agent-shell-tools.mjs +118 -0
  60. package/scripts/emit-gemini.mjs +219 -0
  61. package/scripts/emit-mcp-policy.mjs +30 -0
  62. package/scripts/hooks-union.mjs +391 -0
  63. package/scripts/persist-handoff.mjs +231 -0
  64. package/.rulesync/skills/terse/SKILL.md +0 -8
@@ -1,25 +1,20 @@
1
1
  ---
2
2
  name: thinker
3
- description: 'High-tier reasoning agent. Hard design, multi-file debug, architectural decisions. Returns need: for fat dumps to parent.'
3
+ description: Plan/deep reason. Edge cases/pitfalls. Read-only. Never spawn. No bash.
4
4
  model: grok-4.6
5
5
  agents_md: false
6
6
  mcpInheritance: none
7
- permission_mode: plan
7
+ permission_mode: bypassPermissions
8
8
  tools: read_file, grep, list_dir
9
9
  ---
10
- You are **thinker**: high-tier reasoning agent. Read-only. Never spawn. No bash.
10
+ Voice: `.rulesync/reference/output.md` — must follow.
11
11
 
12
- en-US unless asked. EVERY turn/reply — chat, trivia, meta, protocol: maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. No complete-sentence padding. No host/blog communication style.
13
- Keep need:/verdict:/plan grammar. Only `/explain` escapes.
12
+ Plan the solution. Deep reason. Never spawn. Never implement (implementer agent). Never run for simple tool usage (grunt agent).
13
+ Read-only. No bash. Tiny Read/Grep/list_dir only if needed to plan. Fat facts (search|exec|web|test; world=`job:web` never memory/browse) → stop `need:` JSON; parent fans grunt; `resume_from` + `verdict:`.
14
+ Return the spec/plan. Flag edge cases/pitfalls.
14
15
 
15
- **Hard problems**: architectural design, multi-file debug, tricky refactoring, cross-system decisions. Return `need:` if fat dump required.
16
-
17
- Tiny Read/Grep/list_dir in-child. `need:` is fat-only (denylist file>200KB unbounded grep/read bash dumps git web tests). See cascade.
18
-
19
- Dump stop JSON only:
16
+ Fat dump → stop on this JSON only (≤4/batch; no serial known-parallel):
20
17
 
21
18
  ```
22
19
  need: [{"job":"search","query":"..."}]
23
20
  ```
24
-
25
- One stop may list multiple jobs; do not emit N sequential single-job stops for known-parallel dumps.
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
- /** Parent-orchestrator gate: allow spawn/read/todo; deny write/edit/bash/web/MCP.
3
- Fat Read/Grep/Glob limits + path denylist via gate-fat-tools (one updatedInput).
4
- Parent bash: only node scripts/grunt-job.mjs --job search|exec [--path --glob --cwd].
2
+ /** Parent-orchestrator gate: spawn/peek/kill/todo + persistPlan/persistHandoff writes.
3
+ Parent Read/Grep/Glob/Bash/Web denied unless parent-escape (fat-gate still).
5
4
  SubagentStop intercepts need: search|exec with grunt-job verdicts.
6
- Stop: recap [agent]: / parent-escape stamp; do not waive impl on tools-used.
5
+ Stop: [agent]:/[handoff]: recap | parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
7
6
  Fail-open: parse/crash → empty stdout, exit 0. Explicit JSON only when denying.
8
7
  */
9
8
  import fs from "node:fs";
@@ -19,35 +18,54 @@ import {
19
18
  subagentTypeOf,
20
19
  } from "../../scripts/gate-fat-tools.mjs";
21
20
  import { persistPlan } from "../../scripts/persist-plan.mjs";
21
+ import { persistHandoff } from "../../scripts/persist-handoff.mjs";
22
22
  import { parseNeed } from "../../scripts/parse-need.mjs";
23
23
  import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
24
24
  import { logTelemetry, ORCHESTRATOR_LOGS_DIR } from "../../scripts/telemetry.mjs";
25
25
 
26
26
  const DENY_REASON = "parent is orchestrator; spawn grunt|implementer|thinker";
27
- const STOP_REASON =
28
- "parent is orchestrator; spawn grunt|implementer|thinker; do not complete in-parent";
29
- const ALLOWED_TOOLS = new Set([
30
- "spawnsubagent",
31
- "task",
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
+ ];
47
+ const PARENT_TOOLS = new Set([
32
48
  "todowrite",
33
49
  "getcommandorsubagentoutput",
34
50
  "gettaskoutput",
35
51
  "killcommandorsubagent",
36
52
  "killtask",
37
- "readfile",
38
- "read",
39
- "grep",
40
- "grepsearch",
41
- "listdir",
42
- "glob",
43
53
  ]);
44
- const SPAWN_TOOLS = new Set(["spawnsubagent", "task"]);
54
+ const SPAWN_TOOLS = new Set([
55
+ "spawnsubagent",
56
+ "task",
57
+ "agent",
58
+ "spawnagent",
59
+ ]);
45
60
  const WRITE_TOOLS = new Set(["write"]);
46
- const BASH_TOOLS = new Set(["bash", "runterminalcommand"]);
47
61
  const READ_TOOLS = new Set(["readfile", "read"]);
48
62
  const INTERCEPT_JOBS = new Set(["search", "exec"]);
49
63
  const MAX_STOP = 3;
50
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";
51
69
 
52
70
 
53
71
  function main() {
@@ -81,6 +99,13 @@ function preToolUse(data) {
81
99
  const code = emitFat(data);
82
100
  return code == null ? 0 : code;
83
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
+ }
84
109
  if (SPAWN_TOOLS.has(toolKey)) {
85
110
  const updated = rewriteSpawn(toolInput);
86
111
  if (updated) {
@@ -98,10 +123,11 @@ function preToolUse(data) {
98
123
  if (WRITE_TOOLS.has(toolKey)) {
99
124
  return parentWrite(data, toolInput);
100
125
  }
101
- if (BASH_TOOLS.has(toolKey)) {
102
- return parentBash(data, toolInput);
126
+ if (PARENT_TOOLS.has(toolKey)) {
127
+ emit({ decision: "allow" });
128
+ return 0;
103
129
  }
104
- if (ALLOWED_TOOLS.has(toolKey)) {
130
+ if (hasParentEscape(data)) {
105
131
  const fatCode = emitFat(data);
106
132
  if (fatCode !== null) return fatCode;
107
133
  emit({ decision: "allow" });
@@ -111,6 +137,21 @@ function preToolUse(data) {
111
137
  return 0;
112
138
  }
113
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
+
150
+ function hasParentEscape(data) {
151
+ const p = stampPath(data, "parent-escape");
152
+ return Boolean(p && fs.existsSync(p));
153
+ }
154
+
114
155
  function logPreTool(data, toolKey, toolInput) {
115
156
  const fields = {};
116
157
  if (SPAWN_TOOLS.has(toolKey) && toolInput && typeof toolInput === "object") {
@@ -145,37 +186,34 @@ function emitFat(data) {
145
186
  function workspaceRootOf(data) {
146
187
  return (
147
188
  process.env.GROK_WORKSPACE_ROOT ||
148
- (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() ||
149
193
  ""
150
194
  );
151
195
  }
152
196
 
153
- export function isUnderPlans(filePath, workspaceRoot) {
197
+ function isUnderDir(filePath, workspaceRoot, segments) {
154
198
  if (!filePath || !workspaceRoot) return false;
155
199
  const abs = path.isAbsolute(filePath)
156
200
  ? path.resolve(filePath)
157
201
  : path.resolve(workspaceRoot, filePath);
158
- const plans = path.resolve(workspaceRoot, ".tmp", "plans");
159
- const rel = path.relative(plans, abs);
202
+ const dir = path.resolve(workspaceRoot, ...segments);
203
+ const rel = path.relative(dir, abs);
160
204
  return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
161
205
  }
162
206
 
163
- export function isAllowedParentGruntJob(command, workspaceRoot) {
164
- return isWorkspaceGruntJob(command, workspaceRoot, ["search", "exec"]);
207
+ export function isUnderPlans(filePath, workspaceRoot) {
208
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "plans"]);
165
209
  }
166
210
 
167
- function parentBash(data, toolInput) {
168
- if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
169
- emit({ decision: "deny", reason: DENY_REASON });
170
- return 0;
171
- }
172
- const cmd = typeof toolInput.command === "string" ? toolInput.command : "";
173
- if (!isAllowedParentGruntJob(cmd, workspaceRootOf(data))) {
174
- emit({ decision: "deny", reason: DENY_REASON });
175
- return 0;
176
- }
177
- emit({ decision: "allow" });
178
- return 0;
211
+ export function isUnderHandoffs(filePath, workspaceRoot) {
212
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "handoffs"]);
213
+ }
214
+
215
+ export function isAllowedParentGruntJob(command, workspaceRoot) {
216
+ return isWorkspaceGruntJob(command, workspaceRoot, ["search", "exec"]);
179
217
  }
180
218
 
181
219
  function parentWrite(data, toolInput) {
@@ -190,16 +228,18 @@ function parentWrite(data, toolInput) {
190
228
  toolInput.path ||
191
229
  toolInput.target_file ||
192
230
  "";
193
- if (!isUnderPlans(rawPath, ws)) {
231
+ const handoff = isUnderHandoffs(rawPath, ws);
232
+ if (!handoff && !isUnderPlans(rawPath, ws)) {
194
233
  emit({ decision: "deny", reason: DENY_REASON });
195
234
  return 0;
196
235
  }
197
236
  const content = typeof toolInput.content === "string" ? toolInput.content : "";
198
- const result = persistPlan({ workspaceRoot: ws, content });
237
+ const persist = handoff ? persistHandoff : persistPlan;
238
+ const result = persist({ workspaceRoot: ws, content });
199
239
  if (!result.ok) {
200
240
  emit({
201
241
  decision: "deny",
202
- reason: result.error || "invalid plan",
242
+ reason: result.error || (handoff ? "invalid handoff" : "invalid plan"),
203
243
  });
204
244
  return 0;
205
245
  }
@@ -242,7 +282,18 @@ function isParentEscapePrompt(prompt) {
242
282
  function userPromptSubmit(data) {
243
283
  unlinkQuiet(stampPath(data, "tools-used"));
244
284
  unlinkQuiet(stampPath(data, "stop-block"));
245
- 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)) {
246
297
  const p = stampPath(data, "parent-escape");
247
298
  if (p) {
248
299
  fs.mkdirSync(path.dirname(p), { recursive: true });
@@ -255,16 +306,29 @@ function userPromptSubmit(data) {
255
306
  }
256
307
 
257
308
  function isChildRecap(msg) {
258
- 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);
259
319
  }
260
320
 
261
321
  function stop(data) {
262
322
  if (data.subagentType || data.subagent_type) {
263
323
  return interceptNeed(data, "Stop");
264
324
  }
325
+ if (data.stopHookActive || data.stop_hook_active) return 0;
265
326
  const reason = String(data.reason || "");
266
327
  if (reason && reason !== "end_turn") return 0;
267
328
 
329
+ // Before the parent-escape consume: solo must never burn the one-turn stamp.
330
+ if (isSoloMode(data)) return 0;
331
+
268
332
  const escapeStamp = stampPath(data, "parent-escape");
269
333
  if (escapeStamp && fs.existsSync(escapeStamp)) {
270
334
  unlinkQuiet(escapeStamp);
@@ -272,8 +336,7 @@ function stop(data) {
272
336
  }
273
337
 
274
338
  const msg = data.lastAssistantMessage || data.last_assistant_message || "";
275
- if (isCheap(msg) || isChildRecap(msg)) return 0;
276
- if (!looksLikeImplementation(msg)) return 0;
339
+ if (isChildRecap(msg)) return 0;
277
340
 
278
341
  const stopStamp = stampPath(data, "stop-block");
279
342
  let n = 0;
@@ -286,7 +349,8 @@ function stop(data) {
286
349
  fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
287
350
  fs.writeFileSync(stopStamp, String(n + 1));
288
351
  }
289
- emit({ decision: "block", reason: STOP_REASON });
352
+ const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
353
+ emit({ decision: "block", reason: reasonText });
290
354
  return 0;
291
355
  }
292
356
 
@@ -382,7 +446,6 @@ function interceptNeed(data, hookEventName) {
382
446
  );
383
447
  emit({
384
448
  decision: "block",
385
- reason,
386
449
  hookSpecificOutput: {
387
450
  hookEventName,
388
451
  additionalContext: reason,
@@ -391,37 +454,27 @@ function interceptNeed(data, hookEventName) {
391
454
  return 0;
392
455
  }
393
456
 
394
- function looksLikeImplementation(msg) {
395
- if (typeof msg !== "string") return false;
396
- const t = msg;
397
- if (t.includes("```")) return true;
398
- if (/(?:^|\n)(?:diff --git |--- [^\n]+\n\+\+\+ |@@ )/.test(t)) return true;
399
- if (/\b(?:write|create|edit)[-_ ]file\b/i.test(t)) return true;
400
- const patchLines = t.split(/\n/).filter((l) => /^[+-](?![+-])/.test(l)).length;
401
- if (patchLines >= 8) return true;
402
- return false;
457
+ function stampPath(data, prefix) {
458
+ const root = workspaceRootOf(data);
459
+ const sid = sessionIdOf(data) || "default";
460
+ if (!root) return null;
461
+ return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
403
462
  }
404
463
 
405
- function isCheap(msg) {
406
- if (typeof msg !== "string") return false;
407
- const t = msg.trim();
408
- if (!t || t.length > 500) return false;
409
- if (t.includes("```")) return false;
410
- const sentences = t.split(/(?<=[.!?])(?:\s+|$)/).filter((s) => s.trim());
411
- if (sentences.length > 2) return false;
412
- const lines = t.split(/\n/).filter((l) => l.trim());
413
- return lines.length <= 4;
464
+ function sessionIdOf(data) {
465
+ return String(
466
+ process.env.GROK_SESSION_ID ||
467
+ (data && (data.sessionId || data.session_id)) ||
468
+ "",
469
+ );
414
470
  }
415
471
 
416
- function stampPath(data, prefix) {
417
- const root =
418
- process.env.GROK_WORKSPACE_ROOT ||
419
- (data && (data.workspaceRoot || data.workspace_root)) ||
420
- "";
421
- const sid =
422
- process.env.GROK_SESSION_ID || (data && (data.sessionId || data.session_id)) || "";
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);
423
476
  if (!root || !sid) return null;
424
- return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
477
+ return path.join(root, ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
425
478
  }
426
479
 
427
480
  function unlinkQuiet(p) {
@@ -1,5 +1,5 @@
1
1
  description = "Deep reasoner role"
2
- default_capability_mode = "read-only"
2
+ default_capability_mode = "all"
3
3
  reasoning_effort = "high"
4
4
  model = "grok-4.6"
5
5
  default_fork_context = false
@@ -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.
@@ -17,6 +17,29 @@
17
17
  "command": "node \"${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/scripts/gate-fat-tools.mjs\"",
18
18
  "timeout": 5
19
19
  }
20
+ ],
21
+ // Parent Stop + /parent stamp. PreToolUse parent-deny stays Grok-only
22
+ // (child subagentType/agent_type unverified on these hosts).
23
+ "beforeSubmitPrompt": [
24
+ {
25
+ "type": "command",
26
+ "command": "node \"${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/.grok/hooks/orchestrate-parent.js\"",
27
+ "timeout": 5
28
+ }
29
+ ],
30
+ "stop": [
31
+ {
32
+ "type": "command",
33
+ "command": "node \"${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/.grok/hooks/orchestrate-parent.js\"",
34
+ "timeout": 30
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
+ }
20
43
  ]
21
44
  }
22
45
  }
@@ -8,37 +8,31 @@ Shipped protocol: **parent-only spawn**. Only the parent **orchestrator** sessio
8
8
 
9
9
  ## Parent orchestrator
10
10
 
11
- 1. Super-trivial / yes-no / definition-from-prompt with zero tools **and absolutely sure** → parent answers; no grunt/child. Parent does not web_search. `2+2` → answer.
12
- 2. Not absolutely sure / fresh or world facts → spawn grunt `job: web`. Do not answer from memory.
13
- 3. Cheap reply already in context: answer. No tools.
14
- 4. Tiny lookup (one file, ≤10-hit grep, list_dir): do it here. No bash. Do not spawn. Limits auto-injected; denylist / file>200KB / head_limit>500 → spawn grunt.
15
- 5. Fat dump: `spawn_subagent` type=grunt with `job: search|exec|web|test`. Isolated window. Expect `verdict:`.
16
- 6. Low-reason write (mechanical/repetitive/obvious; volume OK): spawn grunt. Mid reason / feature judgment: spawn implementer. Design / architecture / hard debug: spawn thinker.
17
- 7. Unsure → implementer.
11
+ Always spawn + prompt. First token = spawn. No try-then-spawn. No parent probe. No tiny-read/grep/list_dir/bash/grunt-job for the user. No skip-spawn. `/parent` is the only parent-tool escape.
12
+
13
+ 1. Tools (facts / search / exec / git / web / test / low-reason mechanical write) → grunt.
14
+ 2. Write defined solution → implementer.
15
+ 3. Plan / deep reason (edge cases / pitfalls) → thinker.
16
+ 4. Fresh/world facts → grunt `job: web`. Never answer from memory.
18
17
 
19
18
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked.
20
19
 
21
- Implementer/thinker may tiny-Read/Grep/list_dir themselves. `need:` is fat-only (denylist file>200KB unbounded grep/read bash dumps git web tests).
20
+ Implementer/thinker may tiny-Read/Grep/list_dir themselves. Fat dumps via `need:` JSON.
22
21
 
23
22
  Long parent sessions rely on two-pass compaction; prefer a new parent session per task rather than unbounded resume chains. `[features] two_pass_compaction = true` must live in `~/.grok/config.toml` (from `.grok/global-settings.toml` via `npm run sync:globals` dry-run / `npm run sync:globals:apply`). Project `.grok/config.toml` cannot set `[features]`. Do not add a SessionStart hook.
24
23
 
25
- ## Pre-spawn
26
-
27
- If dumps are known (paths, rg, test cmd): parent and implementer run `node scripts/grunt-job.mjs --job search|exec --query …` in-session and pass those `verdict:` blobs into the child prompt. Optional `--path` `--glob` (repeatable) `--cwd`. `--query` is one argv; regex `|` OK. Never `cd &&`. Unknown flags or exec shell-meta in query → FALLBACK (exit 2). Do not start the child and `need:` for a dump already known. Thinker stays read-only (no bash): fat search|exec via `need:` + SubagentStop intercept.
28
-
29
- For spawned grunt `job: search|exec`, first action is still `node scripts/grunt-job.mjs --job search|exec --query …` and echo stdout as the whole reply. Bounded `job: test` may try the same script (`--job test`); `job: web` and messy `job: test` stay LLM grunt. If the script prints `FALLBACK` (exit 2), LLM grunt may use tools.
30
-
31
24
  ## Wait / peek
32
25
 
33
26
  Parent-only. Non-blocking spawn so each child has a host id immediately. Per-child, not one global timer.
34
- Every ≤60s peek is real host status (no `sleep` then guess). First return ≤60s is the peek (earlier if the child finishes).
27
+ Every 60s peek is real host status (no `sleep` then guess). `timeout_ms=60000` every peek.
35
28
  Quote host fields only (`status`, elapsed since spawn, last activity/output snippet) or quote the error. Never invent.
36
29
  Classify `done` | `alive` | `stuck` vs prior peek: **done** (completed/failed), **alive** (running + fields/output changed), **stuck** (running + unchanged). Unchanged snippet = stuck, not “still working”.
37
30
  Loop until done. No auto-kill. Kill only if the user asks and the host has a kill tool.
38
31
  Then print recap as `[agent]: …` with real child output. Do not redo the child's work.
39
32
 
40
- Parent Stop (`MAX_STOP=3`): do not waive fenced/impl finals for `tools-used` stamp. Allow cheap trivia, `[grunt|implementer|thinker]:` recap prefix, or consume `parent-escape-{sid}` once. Else block.
41
- `/parent` is one-turn (not a mode): UserPromptSubmit writes `.tmp/orchestrator-logs/parent-escape-{sid}`; Stop consumes+unlinks once. Next Stop resumes spawn/cheap/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.
42
36
 
43
37
  Host mapping (in-tree only; do not invent peek/kill APIs). GAP rows: no fake peeks, no auto-kill.
44
38
 
@@ -66,7 +60,7 @@ Parent: parse with `scripts/parse-need.mjs` (same grammar). If parse succeeds, f
66
60
 
67
61
  Resume prompt = `You are {agent} subagent.` plus the new verdicts. Do not re-send the original task, cascade, or prior verdicts (`resume_from` already has the transcript).
68
62
 
69
- Superterse does not change need:/verdict:/plan grammar.
63
+ Voice: [output](output.md). Does not change need:/verdict:/plan grammar.
70
64
 
71
65
  ## Isolation grunt `verdict:`
72
66
 
@@ -78,7 +72,7 @@ n: <count>
78
72
 
79
73
  Fail: first 3 error lines. ≤8 lines. No dumps, recap, HTML, JSON, full logs.
80
74
 
81
- Implementer/thinker: fat dumps via `need:` JSON. Implementer shell via rtk; cat/rg/curl/tests without rtk → fat `need:`. For search|exec that already fits 8-line `verdict:`, implementer runs `node scripts/grunt-job.mjs` in-session instead of `need:` (`--path`/`--glob`/`--cwd` OK; never `cd &&`).
75
+ Implementer/thinker: fat dumps via `need:` JSON. Implementer shell via rtk; cat/rg/curl/tests without rtk → fat `need:`.
82
76
 
83
77
  Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
84
78
 
@@ -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 cheap, `[grunt|implementer|thinker]:` recap prefix, or consume `parent-escape-{sid}` once (`/parent`). 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