@lovrozagar/grunt 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.agents/agents/grunt.md +2 -1
  2. package/.agents/agents/implementer.md +1 -1
  3. package/.agents/agents/orchestrator.md +17 -12
  4. package/.agents/agents/thinker.md +6 -5
  5. package/.agents/skills/ask/SKILL.md +18 -0
  6. package/.agents/skills/auto/SKILL.md +18 -0
  7. package/.agents/skills/browser/SKILL.md +4 -3
  8. package/.agents/skills/cascade/SKILL.md +5 -3
  9. package/.agents/skills/handoff/SKILL.md +1 -1
  10. package/.agents/skills/implement-plan/SKILL.md +12 -11
  11. package/.agents/skills/solo/SKILL.md +5 -3
  12. package/.agents/skills/tmp/SKILL.md +58 -0
  13. package/.agents/skills/write-plan/SKILL.md +11 -8
  14. package/.claude/agents/grunt.md +2 -1
  15. package/.claude/agents/implementer.md +1 -1
  16. package/.claude/agents/orchestrator.md +17 -12
  17. package/.claude/agents/thinker.md +6 -5
  18. package/.claude/skills/ask/SKILL.md +18 -0
  19. package/.claude/skills/auto/SKILL.md +18 -0
  20. package/.claude/skills/browser/SKILL.md +4 -3
  21. package/.claude/skills/cascade/SKILL.md +5 -3
  22. package/.claude/skills/handoff/SKILL.md +1 -1
  23. package/.claude/skills/implement-plan/SKILL.md +12 -11
  24. package/.claude/skills/solo/SKILL.md +5 -3
  25. package/.claude/skills/tmp/SKILL.md +58 -0
  26. package/.claude/skills/write-plan/SKILL.md +11 -8
  27. package/.codex/agents/grunt.toml +2 -1
  28. package/.codex/agents/implementer.toml +1 -1
  29. package/.codex/agents/orchestrator.toml +17 -12
  30. package/.codex/agents/thinker.toml +6 -5
  31. package/.grok/agents/grunt.md +2 -1
  32. package/.grok/agents/implementer.md +1 -1
  33. package/.grok/agents/orchestrator.md +17 -12
  34. package/.grok/agents/thinker.md +6 -5
  35. package/.grok/hooks/orchestrate-parent.js +280 -62
  36. package/.grok/skills/ask/SKILL.md +18 -0
  37. package/.grok/skills/auto/SKILL.md +18 -0
  38. package/.grok/skills/browser/SKILL.md +4 -3
  39. package/.grok/skills/cascade/SKILL.md +5 -3
  40. package/.grok/skills/handoff/SKILL.md +1 -1
  41. package/.grok/skills/implement-plan/SKILL.md +12 -11
  42. package/.grok/skills/solo/SKILL.md +5 -3
  43. package/.grok/skills/tmp/SKILL.md +58 -0
  44. package/.grok/skills/write-plan/SKILL.md +11 -8
  45. package/.rulesync/grunt.config.jsonc +7 -0
  46. package/.rulesync/reference/INDEX.md +7 -4
  47. package/.rulesync/reference/cascade.md +15 -13
  48. package/.rulesync/reference/hooks.md +6 -4
  49. package/.rulesync/reference/law.md +1 -1
  50. package/.rulesync/reference/map.md +14 -9
  51. package/.rulesync/reference/output.md +18 -8
  52. package/.rulesync/reference/plan-format.md +10 -10
  53. package/.rulesync/reference/skills-map.md +6 -3
  54. package/.rulesync/rules/CLAUDE.md +18 -13
  55. package/.rulesync/rules/overview.md +18 -13
  56. package/.rulesync/skills/ask/SKILL.md +18 -0
  57. package/.rulesync/skills/auto/SKILL.md +18 -0
  58. package/.rulesync/skills/browser/SKILL.md +2 -2
  59. package/.rulesync/skills/cascade/SKILL.md +5 -3
  60. package/.rulesync/skills/handoff/SKILL.md +1 -1
  61. package/.rulesync/skills/implement-plan/SKILL.md +14 -11
  62. package/.rulesync/skills/solo/SKILL.md +5 -3
  63. package/.rulesync/skills/tmp/SKILL.md +58 -0
  64. package/.rulesync/skills/write-plan/SKILL.md +12 -8
  65. package/.rulesync/subagents/grunt.md +2 -1
  66. package/.rulesync/subagents/implementer.md +1 -1
  67. package/.rulesync/subagents/orchestrator.md +17 -12
  68. package/.rulesync/subagents/thinker.md +6 -5
  69. package/AGENTS.md +18 -13
  70. package/CHANGELOG.md +33 -0
  71. package/CLAUDE.md +18 -13
  72. package/README.md +17 -16
  73. package/cli/grunt.mjs +12 -12
  74. package/cli/grunt.test.ts +29 -20
  75. package/cli/init.mjs +116 -22
  76. package/cli/init.test.ts +545 -34
  77. package/package.json +8 -1
  78. package/scripts/gate-fat-tools.mjs +2 -2
  79. package/scripts/grunt-config.mjs +50 -0
  80. package/scripts/persist-plan.mjs +17 -2
  81. package/scripts/persist-tmp.mjs +123 -0
  82. package/scripts/scrub-spawn-prompt.mjs +1 -1
@@ -2,7 +2,7 @@
2
2
  /** PreToolUse parent-deny on Claude+Grok+Antigravity; Stop = first-non-empty-line recap; stop-block survives host banners; SubagentStop is single-registration intercept.
3
3
  Parent Read/Grep/Glob/Bash/Web denied unless parent-escape (fat-gate still).
4
4
  SubagentStop intercepts need: search|exec with grunt-job verdicts.
5
- Stop: first-non-empty-line [orchestrator]:/[grunt]:/[implementer]:/[thinker]:/[handoff]: recap
5
+ Stop: first-non-empty-line [orchestrator]:/[grunt]:/[implementer]:/[thinker]:/[handoff]:/[tmp]: recap
6
6
  | parent-escape once; else block. MAX_STOP=3. No isCheap/trivia.
7
7
  Empty lastAssistantMessage → transcript_path tail-scan. Fail-open: parse/crash → empty stdout, exit 0.
8
8
  */
@@ -25,16 +25,20 @@ import {
25
25
  } from "../../scripts/gate-fat-tools.mjs";
26
26
  import { persistPlan } from "../../scripts/persist-plan.mjs";
27
27
  import { persistHandoff } from "../../scripts/persist-handoff.mjs";
28
+ import { persistTmp } from "../../scripts/persist-tmp.mjs";
28
29
  import { parseNeed } from "../../scripts/parse-need.mjs";
29
30
  import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
31
+ import { loadLeftoverGate, loadSpawnMode } from "../../scripts/grunt-config.mjs";
30
32
 
31
- export const ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
33
+ export const ORCHESTRATOR_LOGS_DIR = ".tmp/grunt/orchestrator-logs";
34
+ /** One-release dual-read; drop next release. */
35
+ export const LEGACY_ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
32
36
  export const DENY_REASON =
33
37
  "First action=spawn implementer|grunt|thinker. Deny expected. Only /solo this session escapes.";
34
38
  export const STOP_REASONS = [
35
- "Spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover 1./2. each on own line after.",
36
- "Still spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover 1./2. each on own line after.",
37
- "Last spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover 1./2. each on own line after.",
39
+ "Spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover numbered pick each on own line after (echo printed leftover; always-print typed triple).",
40
+ "Still spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover numbered pick each on own line after (echo printed leftover; always-print typed triple).",
41
+ "Last spawn: ⚠/validate/sim findings or writes remain after Implement pick/`/implement-plan`/explicit implement or parent just tried to Write → spawn implementer. Else facts → grunt. Else no spec/not small/simple → thinker then recap-stop. Else small/simple/defined → implementer. Thinker recap ≠ spec-ready. `ok`/`yes` ≠ implement. Else recap `[orchestrator]:` one recap line; advise leftover numbered pick each on own line after (echo printed leftover; always-print typed triple).",
38
42
  ];
39
43
  const TRANSCRIPT_TAIL_BYTES = 512 * 1024;
40
44
  const PARENT_TOOLS = new Set([
@@ -58,12 +62,15 @@ const WRITE_TOOLS = new Set([
58
62
  ]);
59
63
  const BASH_TOOLS = new Set(["bash", "runterminalcommand", "runcommand"]);
60
64
  const SKILL_TOOLS = new Set(["skill"]);
61
- const PARENT_SKILLS = new Set([
65
+ export const PARENT_SKILLS = new Set([
62
66
  "parent",
63
67
  "explain",
64
68
  "solo",
65
69
  "cascade",
70
+ "auto",
71
+ "ask",
66
72
  "handoff",
73
+ "tmp",
67
74
  "pickup",
68
75
  "write-plan",
69
76
  "implement-plan",
@@ -74,7 +81,11 @@ const MAX_INTERCEPT = 3;
74
81
  /** `/solo` enters single-agent mode; `/cascade` restores the orchestrator. */
75
82
  const SOLO_RE = /^\s*\/solo\s*$/;
76
83
  const CASCADE_RE = /^\s*\/cascade\s*$/;
84
+ const AUTO_RE = /^\s*\/auto\s*$/;
85
+ const ASK_RE = /^\s*\/ask\s*$/;
77
86
  const SOLO_STAMP = "grunt-off";
87
+ export const AUTO_ASK_STAMP = "auto-ask";
88
+ export const SPAWN_MODE_STAMP = "spawn-mode";
78
89
 
79
90
 
80
91
  function main() {
@@ -154,19 +165,17 @@ function preToolUse(data) {
154
165
  return 0;
155
166
  }
156
167
 
157
- /** Session flag, not one-turn. Fail-closed: an unreadable stamp keeps grunt on. */
168
+ /** Session flag, not one-turn. Fail-closed: unreadable spawn-mode stamp is not solo. */
158
169
  export function isSoloMode(data) {
159
170
  try {
160
- const p = soloStampPath(data);
161
- return Boolean(p && fs.existsSync(p));
171
+ return spawnModeOf(data) === "solo";
162
172
  } catch {
163
173
  return false;
164
174
  }
165
175
  }
166
176
 
167
177
  function hasParentEscape(data) {
168
- const p = stampPath(data, "parent-escape");
169
- return Boolean(p && fs.existsSync(p));
178
+ return Boolean(resolveStamp(data, "parent-escape"));
170
179
  }
171
180
 
172
181
  function emitFat(data) {
@@ -198,13 +207,30 @@ function isUnderDir(filePath, workspaceRoot, segments) {
198
207
  }
199
208
 
200
209
  export function isUnderPlans(filePath, workspaceRoot) {
201
- return isUnderDir(filePath, workspaceRoot, [".tmp", "plans"]);
210
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "plans"]);
202
211
  }
203
212
 
204
213
  export function isUnderHandoffs(filePath, workspaceRoot) {
205
214
  return isUnderDir(filePath, workspaceRoot, [".tmp", "grunt", "handoffs"]);
206
215
  }
207
216
 
217
+ export const TMP_RESERVED_DIRS = new Set([
218
+ "plans",
219
+ "handoffs",
220
+ "browser",
221
+ "orchestrator-logs",
222
+ ]);
223
+
224
+ export function isUnderTmp(filePath, workspaceRoot) {
225
+ if (!filePath || !workspaceRoot) return false;
226
+ const abs = path.isAbsolute(filePath)
227
+ ? path.resolve(filePath)
228
+ : path.resolve(workspaceRoot, filePath);
229
+ const root = path.resolve(workspaceRoot, ".tmp", "grunt");
230
+ if (path.dirname(abs) !== root) return false;
231
+ return !TMP_RESERVED_DIRS.has(path.basename(abs));
232
+ }
233
+
208
234
  export function isAllowedParentGruntJob(command, workspaceRoot) {
209
235
  return isWorkspaceGruntJob(command, workspaceRoot, ["search", "exec"]);
210
236
  }
@@ -257,8 +283,18 @@ function parentWrite(data, toolInput) {
257
283
  "";
258
284
  const rewritten = rewriteGruntScratchPath(rawPath, ws);
259
285
  if (rewritten) rawPath = rewritten;
260
- const handoff = isUnderHandoffs(rawPath, ws);
261
- if (!handoff && !isUnderPlans(rawPath, ws)) {
286
+ let persist;
287
+ let invalid = "invalid plan";
288
+ if (isUnderPlans(rawPath, ws)) {
289
+ persist = persistPlan;
290
+ invalid = "invalid plan";
291
+ } else if (isUnderHandoffs(rawPath, ws)) {
292
+ persist = persistHandoff;
293
+ invalid = "invalid handoff";
294
+ } else if (isUnderTmp(rawPath, ws)) {
295
+ persist = persistTmp;
296
+ invalid = "invalid tmp";
297
+ } else {
262
298
  if (hasParentEscape(data)) {
263
299
  emit({ decision: "allow" });
264
300
  return 0;
@@ -267,12 +303,11 @@ function parentWrite(data, toolInput) {
267
303
  return 0;
268
304
  }
269
305
  const content = typeof toolInput.content === "string" ? toolInput.content : "";
270
- const persist = handoff ? persistHandoff : persistPlan;
271
306
  const result = persist({ workspaceRoot: ws, content });
272
307
  if (!result.ok) {
273
308
  emit({
274
309
  decision: "deny",
275
- reason: result.error || (handoff ? "invalid handoff" : "invalid plan"),
310
+ reason: result.error || invalid,
276
311
  });
277
312
  return 0;
278
313
  }
@@ -307,10 +342,7 @@ function rewriteSpawn(toolInput, data) {
307
342
 
308
343
  function postToolUse(data) {
309
344
  if (data.subagentType || data.subagent_type) return 0;
310
- const stamp = stampPath(data, "tools-used");
311
- if (!stamp) return 0;
312
- fs.mkdirSync(path.dirname(stamp), { recursive: true });
313
- fs.writeFileSync(stamp, "1");
345
+ writeStamp(data, "tools-used", "1");
314
346
  return 0;
315
347
  }
316
348
 
@@ -324,35 +356,41 @@ function isParentEscapePrompt(prompt) {
324
356
  }
325
357
 
326
358
  function userPromptSubmit(data) {
327
- unlinkQuiet(stampPath(data, "tools-used"));
359
+ unlinkStamp(data, "tools-used");
328
360
  const prompt = userPromptOf(data);
329
361
  if (!isHostStopBanner(prompt)) {
330
- unlinkQuiet(stampPath(data, "stop-block"));
331
- }
332
- // Sticky: only /solo and /cascade move it. Every other prompt leaves it alone.
333
- if (SOLO_RE.test(prompt)) {
334
- const solo = soloStampPath(data);
335
- if (solo) {
336
- fs.mkdirSync(path.dirname(solo), { recursive: true });
337
- fs.writeFileSync(solo, "1");
362
+ unlinkStamp(data, "stop-block");
363
+ }
364
+ // Sticky: only exact /solo and /cascade move spawn-mode. Always unlink grunt-off.
365
+ if (SOLO_RE.test(prompt) || CASCADE_RE.test(prompt)) {
366
+ const token = SOLO_RE.test(prompt) ? "solo" : "cascade";
367
+ const cfg = loadSpawnMode(workspaceRootOf(data));
368
+ if (token === cfg) {
369
+ unlinkSpawnModeStamp(data);
370
+ } else {
371
+ writeSpawnModeStamp(data, token);
372
+ }
373
+ unlinkSoloStamp(data);
374
+ }
375
+ if (AUTO_RE.test(prompt) || ASK_RE.test(prompt)) {
376
+ const token = AUTO_RE.test(prompt) ? "auto" : "ask";
377
+ const cfg = loadLeftoverGate(workspaceRootOf(data));
378
+ if (token === cfg) {
379
+ unlinkAutoAskStamp(data);
380
+ } else {
381
+ writeAutoAskStamp(data, token);
338
382
  }
339
- } else if (CASCADE_RE.test(prompt)) {
340
- unlinkQuiet(soloStampPath(data));
341
383
  }
342
384
  if (isParentEscapePrompt(prompt)) {
343
- const p = stampPath(data, "parent-escape");
344
- if (p) {
345
- fs.mkdirSync(path.dirname(p), { recursive: true });
346
- fs.writeFileSync(p, "1");
347
- }
385
+ writeStamp(data, "parent-escape", "1");
348
386
  } else {
349
- unlinkQuiet(stampPath(data, "parent-escape"));
387
+ unlinkStamp(data, "parent-escape");
350
388
  }
351
389
  return 0;
352
390
  }
353
391
 
354
392
  const RECAP_TAG_RE =
355
- /^\[(?:orchestrator|grunt|implementer|thinker|handoff)\]:/;
393
+ /^\[(?:orchestrator|grunt|implementer|thinker|handoff|tmp)\]:/;
356
394
  const WAIT_GRUNT = "[orchestrator]: wait grunt";
357
395
 
358
396
  function firstNonEmptyStripped(msg) {
@@ -376,6 +414,38 @@ export function isWaitGruntExact(msg) {
376
414
  return nonempty[0].replace(/^[\s`*_>]+/, "") === WAIT_GRUNT;
377
415
  }
378
416
 
417
+ function nonemptyLines(msg) {
418
+ return String(msg || "")
419
+ .split("\n")
420
+ .filter((l) => l.trim());
421
+ }
422
+
423
+ /** Format-only leftover triple. No leftover-number → skill map. */
424
+ export function hasLeftoverTriple(msg) {
425
+ const lines = String(msg || "")
426
+ .split("\n")
427
+ .map((l) => l.replace(/^[\s`*_>]+/, "").trim());
428
+ const hit = (re) => lines.some((l) => re.test(l));
429
+ const tweak = hit(/^3\.\s+Tweak$/);
430
+ const impl =
431
+ hit(/^1\.\s+Implement with verbal plan$/) &&
432
+ hit(/^2\.\s+Implement with file plan$/);
433
+ const write =
434
+ hit(/^1\.\s+Write with verbal plan$/) &&
435
+ hit(/^2\.\s+Write with file plan$/);
436
+ return Boolean(tweak && (impl || write));
437
+ }
438
+
439
+ /** Ask leftover-required: any `[thinker]:`; `[orchestrator]:` iff >1 nonempty line (except exact wait-grunt); `[grunt]|[implementer]|[handoff]|[tmp]:` exempt. Auto waives in Stop. */
440
+ export function leftoverRequiredAsk(msg) {
441
+ if (isWaitGruntExact(msg)) return false;
442
+ const first = firstNonEmptyStripped(msg);
443
+ if (/^\[(?:grunt|implementer|handoff|tmp)\]:/.test(first)) return false;
444
+ if (/^\[thinker\]:/.test(first)) return true;
445
+ if (/^\[orchestrator\]:/.test(first)) return nonemptyLines(msg).length > 1;
446
+ return false;
447
+ }
448
+
379
449
  function payloadAssistantMessage(data) {
380
450
  if (!data || typeof data !== "object") return "";
381
451
  const camel = data.lastAssistantMessage;
@@ -466,9 +536,8 @@ function stop(data) {
466
536
  // Before the parent-escape consume: solo must never burn the one-turn stamp.
467
537
  if (isSoloMode(data)) return 0;
468
538
 
469
- const escapeStamp = stampPath(data, "parent-escape");
470
- if (escapeStamp && fs.existsSync(escapeStamp)) {
471
- unlinkQuiet(escapeStamp);
539
+ if (resolveStamp(data, "parent-escape")) {
540
+ unlinkStamp(data, "parent-escape");
472
541
  return 0;
473
542
  }
474
543
 
@@ -483,22 +552,22 @@ function stop(data) {
483
552
  if (waitFirst && !isWaitGruntExact(msg)) {
484
553
  // fall through to block — leftover lines not allowed on mid-turn wait
485
554
  } else if (isRecap(msg)) {
486
- return 0;
555
+ if (
556
+ leftoverGateOf(data) !== "auto" &&
557
+ leftoverRequiredAsk(msg) &&
558
+ !hasLeftoverTriple(msg)
559
+ ) {
560
+ // fall through to block — leftover-required under ask
561
+ } else {
562
+ return 0;
563
+ }
487
564
  }
488
565
 
489
- const stopStamp = stampPath(data, "stop-block");
490
- let n = 0;
491
- if (stopStamp && fs.existsSync(stopStamp)) {
492
- n = parseInt(fs.readFileSync(stopStamp, "utf8"), 10);
493
- if (!Number.isFinite(n) || n < 0) n = 0;
494
- }
566
+ let n = readStampInt(data, "stop-block");
495
567
  if (n >= MAX_STOP) {
496
568
  return 0;
497
569
  }
498
- if (stopStamp) {
499
- fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
500
- fs.writeFileSync(stopStamp, String(n + 1));
501
- }
570
+ writeStamp(data, "stop-block", String(n + 1));
502
571
  const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
503
572
  emit({ decision: "block", reason: reasonText });
504
573
  return 0;
@@ -522,12 +591,7 @@ function interceptNeed(data, hookEventName) {
522
591
  return 0;
523
592
  }
524
593
 
525
- const interceptStamp = stampPath(data, "need-intercept");
526
- let n = 0;
527
- if (interceptStamp && fs.existsSync(interceptStamp)) {
528
- n = parseInt(fs.readFileSync(interceptStamp, "utf8"), 10);
529
- if (!Number.isFinite(n) || n < 0) n = 0;
530
- }
594
+ let n = readStampInt(data, "need-intercept");
531
595
  if (n >= MAX_INTERCEPT) {
532
596
  return 0;
533
597
  }
@@ -557,10 +621,7 @@ function interceptNeed(data, hookEventName) {
557
621
  }
558
622
  parts.push(String(result.text || "").trimEnd());
559
623
  }
560
- if (interceptStamp) {
561
- fs.mkdirSync(path.dirname(interceptStamp), { recursive: true });
562
- fs.writeFileSync(interceptStamp, String(n + 1));
563
- }
624
+ writeStamp(data, "need-intercept", String(n + 1));
564
625
  const reason = parts.join("\n");
565
626
  emit({
566
627
  decision: "block",
@@ -579,6 +640,42 @@ function stampPath(data, prefix) {
579
640
  return path.join(root, ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
580
641
  }
581
642
 
643
+ function legacyStampPath(data, prefix) {
644
+ const root = workspaceRootOf(data);
645
+ const sid = sessionIdOf(data) || "default";
646
+ if (!root) return null;
647
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, prefix + "-" + sid);
648
+ }
649
+
650
+ function resolveStamp(data, prefix) {
651
+ const neu = stampPath(data, prefix);
652
+ if (neu && fs.existsSync(neu)) return neu;
653
+ const old = legacyStampPath(data, prefix);
654
+ if (old && fs.existsSync(old)) return old;
655
+ return null;
656
+ }
657
+
658
+ function writeStamp(data, prefix, body) {
659
+ const p = stampPath(data, prefix);
660
+ if (!p) return null;
661
+ fs.mkdirSync(path.dirname(p), { recursive: true });
662
+ fs.writeFileSync(p, body);
663
+ return p;
664
+ }
665
+
666
+ function unlinkStamp(data, prefix) {
667
+ unlinkQuiet(stampPath(data, prefix));
668
+ unlinkQuiet(legacyStampPath(data, prefix));
669
+ }
670
+
671
+ function readStampInt(data, prefix) {
672
+ const p = resolveStamp(data, prefix);
673
+ if (!p) return 0;
674
+ const n = parseInt(fs.readFileSync(p, "utf8"), 10);
675
+ if (!Number.isFinite(n) || n < 0) return 0;
676
+ return n;
677
+ }
678
+
582
679
  function sessionIdOf(data) {
583
680
  return String(
584
681
  process.env.GROK_SESSION_ID ||
@@ -595,6 +692,127 @@ function soloStampPath(data) {
595
692
  return path.join(root, ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
596
693
  }
597
694
 
695
+ function legacySoloStampPath(data) {
696
+ const root = workspaceRootOf(data);
697
+ const sid = sessionIdOf(data);
698
+ if (!root || !sid) return null;
699
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, SOLO_STAMP + "-" + sid);
700
+ }
701
+
702
+ function resolveSoloStamp(data) {
703
+ const neu = soloStampPath(data);
704
+ if (neu && fs.existsSync(neu)) return neu;
705
+ const old = legacySoloStampPath(data);
706
+ if (old && fs.existsSync(old)) return old;
707
+ return null;
708
+ }
709
+
710
+ function unlinkSoloStamp(data) {
711
+ unlinkQuiet(soloStampPath(data));
712
+ unlinkQuiet(legacySoloStampPath(data));
713
+ }
714
+
715
+ /** Spawn-mode stamp: never share a `default` stamp across sid-less sessions. */
716
+ export function spawnModeStampPath(data) {
717
+ const root = workspaceRootOf(data);
718
+ const sid = sessionIdOf(data);
719
+ if (!root || !sid) return null;
720
+ return path.join(root, ORCHESTRATOR_LOGS_DIR, SPAWN_MODE_STAMP + "-" + sid);
721
+ }
722
+
723
+ function legacySpawnModeStampPath(data) {
724
+ const root = workspaceRootOf(data);
725
+ const sid = sessionIdOf(data);
726
+ if (!root || !sid) return null;
727
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, SPAWN_MODE_STAMP + "-" + sid);
728
+ }
729
+
730
+ function resolveSpawnModeStamp(data) {
731
+ const neu = spawnModeStampPath(data);
732
+ if (neu && fs.existsSync(neu)) return neu;
733
+ const old = legacySpawnModeStampPath(data);
734
+ if (old && fs.existsSync(old)) return old;
735
+ return null;
736
+ }
737
+
738
+ function writeSpawnModeStamp(data, body) {
739
+ const p = spawnModeStampPath(data);
740
+ if (!p) return null;
741
+ fs.mkdirSync(path.dirname(p), { recursive: true });
742
+ fs.writeFileSync(p, body);
743
+ return p;
744
+ }
745
+
746
+ function unlinkSpawnModeStamp(data) {
747
+ unlinkQuiet(spawnModeStampPath(data));
748
+ unlinkQuiet(legacySpawnModeStampPath(data));
749
+ }
750
+
751
+ /** Valid stamp solo|cascade wins; else grunt-off presence as solo; else config. Unreadable/bad body ignored. */
752
+ export function spawnModeOf(data) {
753
+ try {
754
+ const p = resolveSpawnModeStamp(data);
755
+ if (p) {
756
+ const body = fs.readFileSync(p, "utf8").trim();
757
+ if (body === "solo" || body === "cascade") return body;
758
+ }
759
+ } catch {
760
+ // unreadable stamp → fall through
761
+ }
762
+ if (resolveSoloStamp(data)) return "solo";
763
+ return loadSpawnMode(workspaceRootOf(data));
764
+ }
765
+
766
+ /** Leftover-gate stamp: never share a `default` stamp across sid-less sessions. */
767
+ export function autoAskStampPath(data) {
768
+ const root = workspaceRootOf(data);
769
+ const sid = sessionIdOf(data);
770
+ if (!root || !sid) return null;
771
+ return path.join(root, ORCHESTRATOR_LOGS_DIR, AUTO_ASK_STAMP + "-" + sid);
772
+ }
773
+
774
+ function legacyAutoAskStampPath(data) {
775
+ const root = workspaceRootOf(data);
776
+ const sid = sessionIdOf(data);
777
+ if (!root || !sid) return null;
778
+ return path.join(root, LEGACY_ORCHESTRATOR_LOGS_DIR, AUTO_ASK_STAMP + "-" + sid);
779
+ }
780
+
781
+ function resolveAutoAskStamp(data) {
782
+ const neu = autoAskStampPath(data);
783
+ if (neu && fs.existsSync(neu)) return neu;
784
+ const old = legacyAutoAskStampPath(data);
785
+ if (old && fs.existsSync(old)) return old;
786
+ return null;
787
+ }
788
+
789
+ function writeAutoAskStamp(data, body) {
790
+ const p = autoAskStampPath(data);
791
+ if (!p) return null;
792
+ fs.mkdirSync(path.dirname(p), { recursive: true });
793
+ fs.writeFileSync(p, body);
794
+ return p;
795
+ }
796
+
797
+ function unlinkAutoAskStamp(data) {
798
+ unlinkQuiet(autoAskStampPath(data));
799
+ unlinkQuiet(legacyAutoAskStampPath(data));
800
+ }
801
+
802
+ /** Stamp body auto|ask wins; else config; else ask. Bad stamp body ignored. */
803
+ export function leftoverGateOf(data) {
804
+ try {
805
+ const p = resolveAutoAskStamp(data);
806
+ if (p) {
807
+ const body = fs.readFileSync(p, "utf8").trim();
808
+ if (body === "auto" || body === "ask") return body;
809
+ }
810
+ } catch {
811
+ // unreadable stamp → fall through
812
+ }
813
+ return loadLeftoverGate(workspaceRootOf(data));
814
+ }
815
+
598
816
  function unlinkQuiet(p) {
599
817
  if (!p) return;
600
818
  try {
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: ask
3
+ description: >
4
+ Session leftover-gate. /ask sets leftover-gate ask for this session — leftover
5
+ triple wait on advise-class recaps. Not leftover y/n. Not pick 3 Tweak. Not
6
+ spawn-escape. /auto sets auto.
7
+ ---
8
+ Session leftover-gate, not spawn-escape. Not leftover y/n. Not pick 3 Tweak.
9
+
10
+ Whole-prompt `/ask` only (`/ask foo` no-op). Persists until `/auto` or session end.
11
+
12
+ Effective leftover-gate: stamp body `auto`|`ask` > `.rulesync/grunt.config.jsonc` `leftoverGate` > `ask`. Fail-closed `ask`.
13
+
14
+ Claude/Codex/Grok: `/ask` via submit hook. If slash token equals committed config `leftoverGate`, unlink `.tmp/grunt/orchestrator-logs/auto-ask-{sid}`; else write stamp body `ask`. Requires real sid (never `default`). Agents/Antigravity: instruction-only (this skill body); cannot create stamp via `/ask`; Stop still honors a pre-existing stamp if present. Session-scoped, never global.
15
+
16
+ Ask: always-print typed leftover triple on advise-class final recap. Write-typed leftover never spawns implementer. Not leftover pick 3.
17
+
18
+ Not `/solo`. Spawn-first still. Recap tags still.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: auto
3
+ description: >
4
+ Session leftover-gate. /auto sets leftover-gate auto for this session —
5
+ Implement-typed pick2 chains write-plan persist then implement-plan {n} and
6
+ skips leftover wait. Not autofix. Not spawn-escape. /ask restores ask.
7
+ ---
8
+ Session leftover-gate, not spawn-escape. Not autofix. Not leftover y/n.
9
+
10
+ Whole-prompt `/auto` only (`/auto foo` no-op). Persists until `/ask` or session end.
11
+
12
+ Effective leftover-gate: stamp body `auto`|`ask` > `.rulesync/grunt.config.jsonc` `leftoverGate` > `ask`. Fail-closed `ask`.
13
+
14
+ Claude/Codex/Grok: `/auto` via submit hook. If slash token equals committed config `leftoverGate`, unlink `.tmp/grunt/orchestrator-logs/auto-ask-{sid}`; else write stamp body `auto`. Requires real sid (never `default`). Agents/Antigravity: instruction-only (this skill body); cannot create stamp via `/auto`; Stop still honors a pre-existing stamp if present. Session-scoped, never global.
15
+
16
+ effective=auto + Implement-typed: write-plan persist then implement-plan `{n}`; skip leftover triple wait. Write-typed under auto still leftover wait; never spawn implementer.
17
+
18
+ Not `/solo`. Spawn-first still. Recap tags still.
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: browser
3
3
  description: >
4
- Session browser rail. /browser or browse a URL. Call node scripts/browser.mjs
5
- nav|snap|click|fill|shot|pdf|stop. Lightpanda default; snap is the default
6
- read. Windows → Chromium. Never MCP. Never env knobs. Never raw Playwright.
4
+ Live URL/interact → this; not websearch. /browser or browse a URL. Call node
5
+ scripts/browser.mjs nav|snap|click|fill|shot|pdf|stop. Lightpanda default;
6
+ snap is the default read. Windows → Chromium. Never MCP. Never env knobs.
7
+ Never raw Playwright.
7
8
  ---
8
9
  Zero-config. `node scripts/browser.mjs <verb>`.
9
10
 
@@ -13,14 +13,16 @@ Session mode, not one-turn. Persists until `/solo` or session end.
13
13
  This session: cascade restored. You are the parent orchestrator.
14
14
 
15
15
  - Spawn grunt|implementer|thinker.
16
- - Recap tags. Recap `[orchestrator]:` `[grunt]:` `[implementer]:` `[thinker]:` `[handoff]:`.
16
+ - Recap tags. Recap `[orchestrator]:` `[grunt]:` `[implementer]:` `[thinker]:` `[handoff]:` `[tmp]:`.
17
17
  - Spawn-first rule. `need:`/`verdict:` grammar.
18
18
  - Voice unchanged: `.rulesync/reference/output.md`. `/explain` is still the voice escape.
19
19
 
20
20
  Fat-tool caps stay on — token guards, not cascade.
21
21
 
22
- Claude/Codex/Grok: `/cascade` via submit hook unlinks `.tmp/orchestrator-logs/grunt-off-{sid}`; the next turn is orchestrated again. Already orchestrated: no-op. Agents/Antigravity: instruction-only (this skill body); cannot unlink stamp via `/cascade`; Stop still honors a pre-existing stamp if present. Session-scoped, never global.
22
+ Effective spawn: stamp body `solo`|`cascade` > one-release `grunt-off-{sid}` presence as solo > `.rulesync/grunt.config.jsonc` `spawnMode` > `cascade`. Fail-closed `cascade`. Unreadable/bad stamp body ignored (dual-read grunt-off else config). Never fail-closed solo.
23
+
24
+ Claude/Codex/Grok: `/cascade` via submit hook. If slash token equals committed config `spawnMode`, unlink `.tmp/grunt/orchestrator-logs/spawn-mode-{sid}`; else write stamp body `cascade`. Always unlink `grunt-off-{sid}` (new+legacy). Requires real sid (never `default`). Already matching config with no leftover grunt-off: no-op. Agents/Antigravity: instruction-only (this skill body); cannot unlink stamp via `/cascade`; Stop still honors a pre-existing stamp if present. Session-scoped, never global.
23
25
 
24
26
  `/explain` is voice-only not spawn-escape. After unlink first token = spawn.
25
27
 
26
- After `/solo`, or when spawn/recap/need|verdict must resume.
28
+ After `/solo`, or when spawn/recap/need|verdict must resume. Leftover-gate `/auto`/`/ask` is not spawn-escape.
@@ -80,6 +80,6 @@ Do not dump the handoff body.
80
80
 
81
81
  - No spawn. One write. `/handoff` is not a mode.
82
82
  - Never parent Read/Bash. Write handoffs dir only.
83
- - Handoff ≠ plan: no Steps/Verify phases, no `.tmp/plans/` write.
83
+ - Handoff ≠ plan: no Steps/Verify phases, no `.tmp/grunt/plans/` write.
84
84
  - Never rewrite or renumber an existing handoff; new session = new serial.
85
85
  - Protocol: `.rulesync/reference/cascade.md`. Do not paste it here.
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: implement-plan
3
3
  description: >
4
- Implementer executes a local .tmp/plans checklist: continue, resume, or pick
5
- among plans. Empty /implement-plan resumes a unique in-progress or starts a
6
- unique ready plan; else lists and needs serial. Marks [x], skips completed
4
+ Implementer executes a local .tmp/grunt/plans checklist: continue, resume, or
5
+ pick among plans. Empty /implement-plan resumes a unique in-progress or starts
6
+ a unique ready plan; else lists and needs serial. Marks [x], skips completed
7
7
  leaves. Not /implement review-loop, not /execute-plan PR DAG, not /pickup, not
8
8
  /run-plan.
9
9
  ---
10
10
  # implement-plan
11
11
 
12
- Parent: spawn grunt to list/validate `.tmp/plans/` when the target is unknown or empty-arg; then spawn implementer with abs plan path + `{repo}/.rulesync/reference/plan-format.md` + first open leaf. Recap from implementer output only. No parent Read/Grep/Bash. Agent-agnostic (grunt then implementer). Not `/pickup` (handoffs only). Not `/run-plan`. Not `/execute-plan` (PR DAG).
12
+ Parent: spawn grunt to list/validate `.tmp/grunt/plans/` when the target is unknown or empty-arg; then spawn implementer with abs plan path + `{repo}/.rulesync/reference/plan-format.md` + first open leaf. Recap from implementer output only. No parent Read/Grep/Bash. Agent-agnostic (grunt then implementer). Not `/pickup` (handoffs only). Not `/run-plan`. Not `/execute-plan` (PR DAG).
13
13
 
14
14
  Format SSOT: `.rulesync/reference/plan-format.md`.
15
15
 
@@ -22,7 +22,7 @@ Todos: `resolve` → `implement` → `verify`. One serial per run. Partial progr
22
22
 
23
23
  ## Resolve
24
24
 
25
- Arg = `$ARGUMENTS` stripped. Listing/validate: spawn grunt (`job:search|exec`) on `.tmp/plans/`. Do not parent Read/Grep/Bash.
25
+ Arg = `$ARGUMENTS` stripped. Listing/validate: spawn grunt (`job:search|exec`) on `.tmp/grunt/plans/`. Do not parent Read/Grep/Bash.
26
26
 
27
27
  Eligible auto = valid plan-format checkbox grammar (`N [ ]` / `N.M [ ]`; not `- [ ]`; not `1. [ ]`). Invalid grammar: exclude from auto. Drop `done` (all `[x]` or FM `status: done`) from auto. Never silent latest. Never auto-pick among many.
28
28
 
@@ -34,7 +34,7 @@ Eligible auto = valid plan-format checkbox grammar (`N [ ]` / `N.M [ ]`; not `-
34
34
 
35
35
  ### Empty arg (`/implement-plan`)
36
36
 
37
- Grunt lists/validates `.tmp/plans/*.md`. Then:
37
+ Grunt lists/validates `.tmp/grunt/plans/*.md`. Then:
38
38
 
39
39
  | condition | action |
40
40
  |---|---|
@@ -54,9 +54,9 @@ That file only; skip `[x]`; remaining leaves. Write-plan remainder stays `/imple
54
54
  | input | action |
55
55
  |---|---|
56
56
  | int (`1`, `0001`) | files whose leading number == that int |
57
- | `{serial}-slug` or `*.md` or path | that file if under `.tmp/plans/` |
57
+ | `{serial}-slug` or `*.md` or path | that file if under `.tmp/grunt/plans/` |
58
58
  | slug only (`add-auth`) | unique match `*-{slug}-*.md` or `*-{slug}.md` |
59
- | `.tmp/plans` missing / no files | `no plans. /write-plan first` stop |
59
+ | `.tmp/grunt/plans` missing / no files | `no plans. /write-plan first` stop |
60
60
  | 0 matches | `no plan serial={n}. have: {list}` stop |
61
61
  | >1 matches | `ambiguous: {paths}` stop |
62
62
  | all boxes already `[x]` / `status: done` | `[implementer]: already done path=...` **no implementer spawn** |
@@ -93,7 +93,7 @@ Wait. Spawn fail → `[implementer]: failed: {err}` stop.
93
93
  Recap from implementer output only. No parent Read/Grep/Bash.
94
94
 
95
95
  ```
96
- [implementer]: serial={int} path=.tmp/plans/{file} status={status} done={x}/{total}
96
+ [implementer]: serial={int} path=.tmp/grunt/plans/{file} status={status} done={x}/{total}
97
97
  files: {abs paths}
98
98
  {if leftover:}
99
99
  leftover:
@@ -104,8 +104,9 @@ leftover:
104
104
  ## Rules
105
105
 
106
106
  - Skip `[x]`; only flip boxes; no renumber/rewrite leaf text.
107
- - No review-fix loop. Not bundled `/implement`. Leftover pick 1 (verbal plan) ≠ this skill (no `.tmp/plans`; no write-plan; no implement-plan). Bare `implement`/`implementer` → pick1, not this skill.
108
- - Slash `/implement-plan {n}` = disk/file run. Empty `/implement-plan` = unique-resume / unique-start / else list. Leftover pick 2 may invoke this sequencing without user slash (after write-plan persist; `plan=/abs/...` only). Always-do leftover match; leftover `2` ≠ Skill-name. Verbal leftover ≠ implement-plan.
107
+ - No review-fix loop. Not bundled `/implement`. Implement leftover pick 1 (`Implement with verbal plan`) ≠ this skill (no `.tmp/grunt/plans`; no write-plan; no implement-plan). Bare `implement`/`implementer` → pick1 iff Implement-typed, not this skill; else recap “no implementer this remainder”.
108
+ - Slash `/implement-plan {n}` = disk/file run. Empty `/implement-plan` = unique-resume / unique-start / else list. Implement leftover pick 2 may invoke this sequencing without user slash (after write-plan persist; `plan=/abs/...` only). Write leftover pick 2 does not spawn implementer this turn. Always-do leftover match; leftover `2` ≠ Skill-name. Verbal leftover ≠ implement-plan.
109
+ - effective=auto + Implement-typed may chain this sequencing without user slash after write-plan persist (`plan=/abs/...`). Write-typed under auto: no implementer this turn; leftover wait.
109
110
  - No auto `/write-plan` chain. Escalation → tell user to `/write-plan` a follow-up.
110
111
  - Omit `model`. `subagent_type: implementer` only for this skill.
111
112
  - First prompt sentence: `You are implementer subagent.`