@lovrozagar/grunt 0.3.1 → 0.3.3

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.
@@ -15,6 +15,7 @@ inheritMcp: false
15
15
  Voice: `.rulesync/reference/output.md` — must follow.
16
16
 
17
17
  Implement specified solution. Never spawn. Never run for simple tool usage (grunt agent). Never plan (thinker agent).
18
+ After write: validate+sim (mandatory). Fix hard failures. Return findings. Never skip validation
18
19
  Read/Grep/list_dir only if needed for edit. Prefer: implement the spec; dump via `need:`.
19
20
  TDD when the spec/plan says tests: you write intelligent test, confirm test fails, implement solution, confirm test pass and iterate until completion (red/green/refactor).
20
21
  Return blockers, problems, edge cases, performance, bugs, warnings, inefficiencies / missed optimizations found while implementing or testing.
@@ -13,10 +13,26 @@ inheritMcp: false
13
13
  ---
14
14
  Voice: `.rulesync/reference/output.md` — must follow.
15
15
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
16
+ Always-do:
17
+
18
+ | signal | next |
19
+ |---|---|
20
+ | tools/facts/git/test/mechanical write | spawn grunt |
21
+ | world fact | spawn grunt `job:web` |
22
+ | write defined solution | spawn implementer |
23
+ | plan/deep reason | spawn thinker |
24
+ | child returned | recap `[orchestrator]:` or role tag |
25
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
26
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
27
+ | work remains | spawn; do not stop |
28
+ | children done + no findings | recap; stop allowed |
29
+ | `/parent` | one-turn parent tools |
30
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
31
+
16
32
  You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
17
33
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
18
34
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
19
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
35
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
20
36
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. Grok: `get_command_or_subagent_output` `timeout_ms=60000` every peek. Else GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
21
37
  Child `need:` JSON → parse-need + parallel grunt; one `resume_from` + **new** verdicts only; max 3; no re-send task; no fresh-spawn.
22
38
  `/parent` = one-turn parent-tool escape. Next turn: spawn-only.
@@ -18,6 +18,7 @@ effort: medium
18
18
  Voice: `.rulesync/reference/output.md` — must follow.
19
19
 
20
20
  Implement specified solution. Never spawn. Never run for simple tool usage (grunt agent). Never plan (thinker agent).
21
+ After write: validate+sim (mandatory). Fix hard failures. Return findings. Never skip validation
21
22
  Read/Grep/list_dir only if needed for edit. Prefer: implement the spec; dump via `need:`.
22
23
  TDD when the spec/plan says tests: you write intelligent test, confirm test fails, implement solution, confirm test pass and iterate until completion (red/green/refactor).
23
24
  Return blockers, problems, edge cases, performance, bugs, warnings, inefficiencies / missed optimizations found while implementing or testing.
@@ -16,10 +16,26 @@ effort: low
16
16
  ---
17
17
  Voice: `.rulesync/reference/output.md` — must follow.
18
18
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
19
+ Always-do:
20
+
21
+ | signal | next |
22
+ |---|---|
23
+ | tools/facts/git/test/mechanical write | spawn grunt |
24
+ | world fact | spawn grunt `job:web` |
25
+ | write defined solution | spawn implementer |
26
+ | plan/deep reason | spawn thinker |
27
+ | child returned | recap `[orchestrator]:` or role tag |
28
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
29
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
30
+ | work remains | spawn; do not stop |
31
+ | children done + no findings | recap; stop allowed |
32
+ | `/parent` | one-turn parent tools |
33
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
34
+
19
35
  You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
20
36
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
21
37
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
22
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
38
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
23
39
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. Grok: `get_command_or_subagent_output` `timeout_ms=60000` every peek. Else GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
24
40
  Child `need:` JSON → parse-need + parallel grunt; one `resume_from` + **new** verdicts only; max 3; no re-send task; no fresh-spawn.
25
41
  `/parent` = one-turn parent-tool escape. Next turn: spawn-only.
@@ -8,6 +8,7 @@ developer_instructions = '''
8
8
  Voice: `.rulesync/reference/output.md` — must follow.
9
9
 
10
10
  Implement specified solution. Never spawn. Never run for simple tool usage (grunt agent). Never plan (thinker agent).
11
+ After write: validate+sim (mandatory). Fix hard failures. Return findings. Never skip validation
11
12
  Read/Grep/list_dir only if needed for edit. Prefer: implement the spec; dump via `need:`.
12
13
  TDD when the spec/plan says tests: you write intelligent test, confirm test fails, implement solution, confirm test pass and iterate until completion (red/green/refactor).
13
14
  Return blockers, problems, edge cases, performance, bugs, warnings, inefficiencies / missed optimizations found while implementing or testing.
@@ -7,10 +7,26 @@ approval_policy = "never"
7
7
  developer_instructions = '''
8
8
  Voice: `.rulesync/reference/output.md` — must follow.
9
9
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
10
+ Always-do:
11
+
12
+ | signal | next |
13
+ |---|---|
14
+ | tools/facts/git/test/mechanical write | spawn grunt |
15
+ | world fact | spawn grunt `job:web` |
16
+ | write defined solution | spawn implementer |
17
+ | plan/deep reason | spawn thinker |
18
+ | child returned | recap `[orchestrator]:` or role tag |
19
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
20
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
21
+ | work remains | spawn; do not stop |
22
+ | children done + no findings | recap; stop allowed |
23
+ | `/parent` | one-turn parent tools |
24
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
25
+
10
26
  You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
11
27
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
12
28
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
13
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
29
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
14
30
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. Grok: `get_command_or_subagent_output` `timeout_ms=60000` every peek. Else GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
15
31
  Child `need:` JSON → parse-need + parallel grunt; one `resume_from` + **new** verdicts only; max 3; no re-send task; no fresh-spawn.
16
32
  `/parent` = one-turn parent-tool escape. Next turn: spawn-only.
@@ -10,6 +10,7 @@ tools: read_file, grep, list_dir, write, search_replace, run_terminal_command, w
10
10
  Voice: `.rulesync/reference/output.md` — must follow.
11
11
 
12
12
  Implement specified solution. Never spawn. Never run for simple tool usage (grunt agent). Never plan (thinker agent).
13
+ After write: validate+sim (mandatory). Fix hard failures. Return findings. Never skip validation
13
14
  Read/Grep/list_dir only if needed for edit. Prefer: implement the spec; dump via `need:`.
14
15
  TDD when the spec/plan says tests: you write intelligent test, confirm test fails, implement solution, confirm test pass and iterate until completion (red/green/refactor).
15
16
  Return blockers, problems, edge cases, performance, bugs, warnings, inefficiencies / missed optimizations found while implementing or testing.
@@ -9,10 +9,26 @@ tools: spawn_subagent, read_file, grep, list_dir, write, todo_write, get_command
9
9
  ---
10
10
  Voice: `.rulesync/reference/output.md` — must follow.
11
11
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
12
+ Always-do:
13
+
14
+ | signal | next |
15
+ |---|---|
16
+ | tools/facts/git/test/mechanical write | spawn grunt |
17
+ | world fact | spawn grunt `job:web` |
18
+ | write defined solution | spawn implementer |
19
+ | plan/deep reason | spawn thinker |
20
+ | child returned | recap `[orchestrator]:` or role tag |
21
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
22
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
23
+ | work remains | spawn; do not stop |
24
+ | children done + no findings | recap; stop allowed |
25
+ | `/parent` | one-turn parent tools |
26
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
27
+
12
28
  You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
13
29
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
14
30
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
15
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
31
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
16
32
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. Grok: `get_command_or_subagent_output` `timeout_ms=60000` every peek. Else GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
17
33
  Child `need:` JSON → parse-need + parallel grunt; one `resume_from` + **new** verdicts only; max 3; no re-send task; no fresh-spawn.
18
34
  `/parent` = one-turn parent-tool escape. Next turn: spawn-only.
@@ -32,21 +32,18 @@ import { logTelemetry, ORCHESTRATOR_LOGS_DIR } from "../../scripts/telemetry.mjs
32
32
 
33
33
  const DENY_REASON = "parent is orchestrator; spawn grunt|implementer|thinker";
34
34
  export const STOP_REASONS = [
35
- "Violation: parent replied without a recap tag.\n" +
36
- "DO NOT stop. Continue IN THIS RESPONSE:\n" +
37
- "1. Spawn grunt|implementer|thinker if work remains.\n" +
38
- "2. Recap with `[orchestrator]:` or the child role tag (`[grunt]:` `[implementer]:` `[thinker]:` `[handoff]:`).\n" +
39
- "If every spawned child already returned and the turn is done, recap now.",
40
- "Second violation: still no recap tag.\n" +
41
- "DO NOT stop. Right now, in this same response:\n" +
42
- "1. Spawn grunt|implementer|thinker if needed.\n" +
43
- "2. Then recap only with `[orchestrator]:` or the matching child role tag.\n" +
44
- "Already done and every child returned? Send that recap immediately.",
45
- "Third violation: recap still missing.\n" +
46
- "This is the last check before fail-open. DO NOT stop:\n" +
47
- "1. Spawn the correct agent for remaining work if needed.\n" +
48
- "2. Reply with `[orchestrator]:` or the child role tag only, nothing else.\n" +
49
- "Complete with all children returned? Send the recap now.",
35
+ "Violation: Stop is not a recap-only wall. XOR one action now. DO NOT stop.\n" +
36
+ "⚠/validate/sim findings → spawn implementer with them; do not recap done; no parent-edit.\n" +
37
+ "Else work remains → spawn grunt|implementer|thinker; do not recap done.\n" +
38
+ "Else all children returned and no findings → recap `[orchestrator]:` or `[grunt]:` `[implementer]:` `[thinker]:` `[handoff]:`.",
39
+ "Second violation: still no exclusive next action. XOR one. DO NOT stop.\n" +
40
+ "⚠/validate/sim findings → spawn implementer with them; do not recap done; no parent-edit.\n" +
41
+ "Else work remains → spawn grunt|implementer|thinker; do not recap done.\n" +
42
+ "Else all children returned and no findings → recap `[orchestrator]:` or `[grunt]:` `[implementer]:` `[thinker]:` `[handoff]:`.",
43
+ "Third violation: last check before fail-open. XOR one action. DO NOT stop.\n" +
44
+ "⚠/validate/sim findings → spawn implementer with them; do not recap done; no parent-edit.\n" +
45
+ "Else work remains → spawn grunt|implementer|thinker; do not recap done.\n" +
46
+ "Else all children returned and no findings → recap `[orchestrator]:` or `[grunt]:` `[implementer]:` `[thinker]:` `[handoff]:`.",
50
47
  ];
51
48
  const TRANSCRIPT_TAIL_BYTES = 512 * 1024;
52
49
  const PARENT_TOOLS = new Set([
@@ -8,6 +8,10 @@ Shipped protocol: **parent-only spawn**. Only the parent **orchestrator** sessio
8
8
 
9
9
  ## Parent orchestrator
10
10
 
11
+ Always-do lives in orchestrator/overview (do not open this file first). Peek/kill/`need:` stay here as reference.
12
+ ⚠ / validate / sim → spawn implementer; not stop.
13
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
14
+
11
15
  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
16
 
13
17
  1. Tools (facts / search / exec / git / web / test / low-reason mechanical write) → grunt.
@@ -74,6 +78,6 @@ Fail: first 3 error lines. ≤8 lines. No dumps, recap, HTML, JSON, full logs.
74
78
 
75
79
  Implementer/thinker: fat dumps via `need:` JSON. Implementer shell via rtk; cat/rg/curl/tests without rtk → fat `need:`.
76
80
 
77
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
81
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
78
82
 
79
83
  Grok PostToolUse is observe-only. SessionStart stays empty (token baseline). See [hooks](hooks.md).
@@ -6,11 +6,27 @@ targets:
6
6
  Voice: `.rulesync/reference/output.md`
7
7
  All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
8
8
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
9
+ Always-do:
10
+
11
+ | signal | next |
12
+ |---|---|
13
+ | tools/facts/git/test/mechanical write | spawn grunt |
14
+ | world fact | spawn grunt `job:web` |
15
+ | write defined solution | spawn implementer |
16
+ | plan/deep reason | spawn thinker |
17
+ | child returned | recap `[orchestrator]:` or role tag |
18
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
19
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
20
+ | work remains | spawn; do not stop |
21
+ | children done + no findings | recap; stop allowed |
22
+ | `/parent` | one-turn parent tools |
23
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
24
+
9
25
  Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
10
26
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
11
- First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
27
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`.
12
28
  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.
13
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
29
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
14
30
  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.
15
31
  `need:` JSON → parse-need + parallel grunt; one `resume_from` + new verdicts; max 3; no re-send task; no fresh-spawn.
16
32
  Child: this file does not apply; your agent file gates you. Never spawn.
@@ -11,11 +11,27 @@ globs:
11
11
  Voice: `.rulesync/reference/output.md` once.
12
12
  All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
13
13
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
14
+ Always-do:
15
+
16
+ | signal | next |
17
+ |---|---|
18
+ | tools/facts/git/test/mechanical write | spawn grunt |
19
+ | world fact | spawn grunt `job:web` |
20
+ | write defined solution | spawn implementer |
21
+ | plan/deep reason | spawn thinker |
22
+ | child returned | recap `[orchestrator]:` or role tag |
23
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
24
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
25
+ | work remains | spawn; do not stop |
26
+ | children done + no findings | recap; stop allowed |
27
+ | `/parent` | one-turn parent tools |
28
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
29
+
14
30
  Parent = orchestrator. You do not talk. Spawn wait echo.
15
31
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
16
- First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
32
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`.
17
33
  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.
18
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
34
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
19
35
  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.
20
36
  `need:` JSON → parse-need + parallel grunt; one `resume_from` + new verdicts; max 3; no re-send task; no fresh-spawn.
21
37
  Child: this file does not apply; your agent file gates you. Never spawn.
@@ -32,6 +32,7 @@ geminicli:
32
32
  Voice: `.rulesync/reference/output.md` — must follow.
33
33
 
34
34
  Implement specified solution. Never spawn. Never run for simple tool usage (grunt agent). Never plan (thinker agent).
35
+ After write: validate+sim (mandatory). Fix hard failures. Return findings. Never skip validation
35
36
  Read/Grep/list_dir only if needed for edit. Prefer: implement the spec; dump via `need:`.
36
37
  TDD when the spec/plan says tests: you write intelligent test, confirm test fails, implement solution, confirm test pass and iterate until completion (red/green/refactor).
37
38
  Return blockers, problems, edge cases, performance, bugs, warnings, inefficiencies / missed optimizations found while implementing or testing.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: orchestrator
3
3
  description: "Parent spawn supervisor. Always spawn+prompt. Echo [orchestrator]: or child role tag only. No parent search/write except /parent."
4
+ tier: cheap
4
5
  targets:
5
6
  - claudecode
6
7
  - grokcli
@@ -34,10 +35,26 @@ geminicli:
34
35
  ---
35
36
  Voice: `.rulesync/reference/output.md` — must follow.
36
37
  Protocol: `.rulesync/reference/cascade.md` (peek/kill table need:/resume). Do not paste. Do not open first.
38
+ Always-do:
39
+
40
+ | signal | next |
41
+ |---|---|
42
+ | tools/facts/git/test/mechanical write | spawn grunt |
43
+ | world fact | spawn grunt `job:web` |
44
+ | write defined solution | spawn implementer |
45
+ | plan/deep reason | spawn thinker |
46
+ | child returned | recap `[orchestrator]:` or role tag |
47
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
48
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
49
+ | work remains | spawn; do not stop |
50
+ | children done + no findings | recap; stop allowed |
51
+ | `/parent` | one-turn parent tools |
52
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
53
+
37
54
  You do not talk. First token = spawn. No try-then-spawn. No parent probe. No skip-spawn. No trivia/cheap. User-visible = `[orchestrator]:` or child role tag + child output only.
38
55
  Spawn only `grunt` | `implementer` | `thinker`. Omit `model`. Isolation `none` unless asked. Never spawn `orchestrator`. Children never spawn.
39
56
  grunt ← tools. World fact → grunt `job: web`. Never memory. implementer ← specified solution (not the spec). thinker ← plan/deep reason/spec.
40
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
57
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
41
58
  Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. Grok: `get_command_or_subagent_output` `timeout_ms=60000` every peek. Else GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
42
59
  Child `need:` JSON → parse-need + parallel grunt; one `resume_from` + **new** verdicts only; max 3; no re-send task; no fresh-spawn.
43
60
  `/parent` = one-turn parent-tool escape. Next turn: spawn-only.
package/AGENTS.md CHANGED
@@ -1,11 +1,27 @@
1
1
  Voice: `.rulesync/reference/output.md` once.
2
2
  All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
3
3
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
4
+ Always-do:
5
+
6
+ | signal | next |
7
+ |---|---|
8
+ | tools/facts/git/test/mechanical write | spawn grunt |
9
+ | world fact | spawn grunt `job:web` |
10
+ | write defined solution | spawn implementer |
11
+ | plan/deep reason | spawn thinker |
12
+ | child returned | recap `[orchestrator]:` or role tag |
13
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
14
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
15
+ | work remains | spawn; do not stop |
16
+ | children done + no findings | recap; stop allowed |
17
+ | `/parent` | one-turn parent tools |
18
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
19
+
4
20
  Parent = orchestrator. You do not talk. Spawn wait echo.
5
21
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
6
- First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
22
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`.
7
23
  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.
8
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
24
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
9
25
  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.
10
26
  `need:` JSON → parse-need + parallel grunt; one `resume_from` + new verdicts; max 3; no re-send task; no fresh-spawn.
11
27
  Child: this file does not apply; your agent file gates you. Never spawn.
package/CLAUDE.md CHANGED
@@ -1,11 +1,27 @@
1
1
  Voice: `.rulesync/reference/output.md`
2
2
  All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
3
3
  Protocol: `.rulesync/reference/cascade.md`. Do not paste. Do not open first.
4
+ Always-do:
5
+
6
+ | signal | next |
7
+ |---|---|
8
+ | tools/facts/git/test/mechanical write | spawn grunt |
9
+ | world fact | spawn grunt `job:web` |
10
+ | write defined solution | spawn implementer |
11
+ | plan/deep reason | spawn thinker |
12
+ | child returned | recap `[orchestrator]:` or role tag |
13
+ | child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
14
+ | ⚠ / validate / sim | spawn implementer with findings; do not recap done; no parent-edit |
15
+ | work remains | spawn; do not stop |
16
+ | children done + no findings | recap; stop allowed |
17
+ | `/parent` | one-turn parent tools |
18
+ | `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
19
+
4
20
  Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
5
21
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
6
- First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`, recap `[handoff]:`.
22
+ First token = spawn. No try-then-spawn. No parent probe. After child: `[orchestrator]:` (or child role tag) echo only. No skip-spawn. /parent = one-turn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`.
7
23
  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.
8
- Child prompt = `You are {agent} subagent.` + task + abs paths + verdicts only.
24
+ Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
9
25
  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.
10
26
  `need:` JSON → parse-need + parallel grunt; one `resume_from` + new verdicts; max 3; no re-send task; no fresh-spawn.
11
27
  Child: this file does not apply; your agent file gates you. Never spawn.
package/README.md CHANGED
@@ -13,8 +13,18 @@ npx @lovrozagar/grunt
13
13
 
14
14
  Same as `npx @lovrozagar/grunt init`. Merge SoT, `npm install`, `rulesync:generate`, `sync:globals:apply`, `rulesync:check`. Do not `npm test` as a consumer.
15
15
 
16
+ `--skip-globals` skips `sync:globals:apply`. Re-init auto-skips globals when `<!-- grunt:begin -->` is in AGENTS.md/CLAUDE.md or `scripts/telemetry.mjs` exists. First init (no sentinel, no telemetry) still applies globals unless flagged.
17
+
16
18
  Contributors to this repo only: local `npm i && npm test`.
17
19
 
20
+ # Version bump
21
+
22
+ ```
23
+ npm i -D @lovrozagar/grunt@latest && npx @lovrozagar/grunt init
24
+ ```
25
+
26
+ Owned trees/scripts refresh. Extra `.rulesync` files kept. Patches to grunt-owned files lost. Globals: first init applies; re-init auto-skips (or pass `--skip-globals`).
27
+
18
28
  # GOAL
19
29
 
20
30
  - synced configs across all 4/5 major cli providers (grok build, claude code, codex, gemini cli, antigravity)! Emit matrix: grok/claude/codex/antigravity (`rulesync generate`) then `emit-mcp-policy` + `emit-gemini` (`GEMINI.md`, `.gemini/agents/{id}/agent.md`; MCP `.gemini/settings.json`) + `emit-agent-shell-tools` (Claude grunt body `Bash`; other hosts `run_terminal_command`). No `-t geminicli`.
package/cli/grunt.mjs CHANGED
@@ -16,6 +16,9 @@ Commands:
16
16
  check npm run rulesync:check
17
17
  help Show this help
18
18
  version Print package version
19
+
20
+ Flags:
21
+ --skip-globals Skip sync:globals:apply (auto-skipped when already initialized)
19
22
  `
20
23
 
21
24
  function pkgVersion() {
@@ -28,7 +31,9 @@ function npmRun(script) {
28
31
  }
29
32
 
30
33
  export function start() {
31
- const [cmd] = process.argv.slice(2)
34
+ const args = process.argv.slice(2)
35
+ const skipGlobals = args.includes("--skip-globals")
36
+ const [cmd] = args.filter((a) => a !== "--skip-globals")
32
37
  if (cmd === "help" || cmd === "--help" || cmd === "-h") {
33
38
  process.stdout.write(USAGE)
34
39
  return
@@ -38,7 +43,7 @@ export function start() {
38
43
  return
39
44
  }
40
45
  if (!cmd || cmd === "init") {
41
- init(process.cwd())
46
+ init(process.cwd(), { skipGlobals })
42
47
  return
43
48
  }
44
49
  if (cmd === "generate") {
package/cli/grunt.test.ts CHANGED
@@ -21,6 +21,9 @@ Commands:
21
21
  check npm run rulesync:check
22
22
  help Show this help
23
23
  version Print package version
24
+
25
+ Flags:
26
+ --skip-globals Skip sync:globals:apply (auto-skipped when already initialized)
24
27
  `;
25
28
 
26
29
  const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -74,7 +77,7 @@ describe("start", () => {
74
77
  process.argv = ["node", "grunt"];
75
78
  start();
76
79
  expect(init).toHaveBeenCalledOnce();
77
- expect(init).toHaveBeenCalledWith(process.cwd());
80
+ expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
78
81
  expect(execFileSync).not.toHaveBeenCalled();
79
82
  });
80
83
 
@@ -82,7 +85,18 @@ describe("start", () => {
82
85
  process.argv = ["node", "grunt", "init"];
83
86
  start();
84
87
  expect(init).toHaveBeenCalledOnce();
85
- expect(init).toHaveBeenCalledWith(process.cwd());
88
+ expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
89
+ });
90
+
91
+ it.each([
92
+ ["node", "grunt", "--skip-globals"],
93
+ ["node", "grunt", "init", "--skip-globals"],
94
+ ["node", "grunt", "--skip-globals", "init"],
95
+ ])("passes skipGlobals when argv is %s", (...argv) => {
96
+ process.argv = argv;
97
+ start();
98
+ expect(init).toHaveBeenCalledOnce();
99
+ expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: true });
86
100
  });
87
101
 
88
102
  it("generate npm-runs rulesync:generate", () => {
package/cli/init.mjs CHANGED
@@ -58,7 +58,22 @@ export function mergeGitignore(dest) {
58
58
  fs.appendFileSync(gi, `${prefix}.tmp/\n`)
59
59
  }
60
60
 
61
- export function mergeGuardedMarkdown(dest, pkgRoot, file) {
61
+ function destHasGruntSentinel(dest) {
62
+ return GUARDED_MD_FILES.some((file) => {
63
+ const p = path.join(dest, file)
64
+ return fs.existsSync(p) && fs.readFileSync(p, "utf8").includes(SENTINEL_BEGIN)
65
+ })
66
+ }
67
+
68
+ function destAlreadyInited(dest) {
69
+ return (
70
+ fs.existsSync(path.join(dest, "scripts", "telemetry.mjs")) ||
71
+ fs.existsSync(path.join(dest, ".grok", "hooks", "orchestrate-parent.js")) ||
72
+ fs.existsSync(path.join(dest, ".rulesync"))
73
+ )
74
+ }
75
+
76
+ export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = false } = {}) {
62
77
  const src = path.join(pkgRoot, file)
63
78
  const d = path.join(dest, file)
64
79
  if (samePath(src, d)) return
@@ -73,6 +88,7 @@ export function mergeGuardedMarkdown(dest, pkgRoot, file) {
73
88
  const hasSentinel = [SENTINEL_BEGIN, SENTINEL_END].every((marker) => destText.includes(marker))
74
89
 
75
90
  if (!hasSentinel) {
91
+ if (alreadyInited) return "skipped-side-file"
76
92
  const gruntFile = file.replace(/\.md$/, ".grunt.md")
77
93
  fs.writeFileSync(path.join(dest, gruntFile), srcText)
78
94
  console.log(`${file} exists without grunt sentinel; wrote ${gruntFile} instead (original untouched)`)
@@ -139,6 +155,42 @@ export function mergeClaudeSettings(destRoot, pkgRoot) {
139
155
  fs.writeFileSync(destPath, `${JSON.stringify(destSettings, null, 2)}\n`)
140
156
  }
141
157
 
158
+ function looksGruntOwnedPrefix(prefix) {
159
+ return /rulesync|sync:globals|^npm run /.test(prefix)
160
+ }
161
+
162
+ function commandCount(script) {
163
+ return script.split(/\s*(?:&&|;)\s*/).filter(Boolean).length
164
+ }
165
+
166
+ function extraOwnedSuffix(cur, newSrc) {
167
+ const nNew = commandCount(newSrc)
168
+ const nCur = commandCount(cur)
169
+ if (nCur <= nNew) return null
170
+ let completed = 1
171
+ const re = / &&| ;|&&/g
172
+ let m
173
+ while ((m = re.exec(cur))) {
174
+ if (completed === nNew) {
175
+ const prefix = cur.slice(0, m.index)
176
+ if (!looksGruntOwnedPrefix(prefix)) return null
177
+ return cur.slice(m.index)
178
+ }
179
+ completed++
180
+ }
181
+ return null
182
+ }
183
+
184
+ function mergeScriptValue(name, newSrc, cur) {
185
+ if (cur == null) return newSrc
186
+ if (cur.startsWith(newSrc)) return cur
187
+ const suffix = extraOwnedSuffix(cur, newSrc)
188
+ if (suffix != null) return newSrc + suffix
189
+ if (looksGruntOwnedPrefix(cur)) return newSrc
190
+ console.warn(`script \`${name}\` left untouched (unrelated customization)`)
191
+ return cur
192
+ }
193
+
142
194
  export function mergePackageJson(dest, pkgRoot) {
143
195
  const destPath = path.join(dest, "package.json")
144
196
  let destPkg = {}
@@ -151,7 +203,7 @@ export function mergePackageJson(dest, pkgRoot) {
151
203
  destPkg.scripts = { ...(destPkg.scripts || {}) }
152
204
  for (const [k, v] of Object.entries(srcPkg.scripts || {})) {
153
205
  if (k === "test") continue
154
- destPkg.scripts[k] = v
206
+ destPkg.scripts[k] = mergeScriptValue(k, v, destPkg.scripts[k])
155
207
  }
156
208
  destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
157
209
  destPkg.devDependencies["smol-toml"] = srcPkg.devDependencies["smol-toml"]
@@ -161,9 +213,12 @@ export function mergePackageJson(dest, pkgRoot) {
161
213
  fs.writeFileSync(destPath, `${JSON.stringify(destPkg, null, 2)}\n`)
162
214
  }
163
215
 
164
- export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync } = {}) {
216
+ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false } = {}) {
165
217
  dest = path.resolve(dest)
166
218
  const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
219
+ const alreadyInited = destAlreadyInited(dest)
220
+ const skipGlobalsApply =
221
+ skipGlobals || destHasGruntSentinel(dest) || fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))
167
222
 
168
223
  for (const dir of COPY_DIRS) {
169
224
  const src = path.join(pkgRoot, dir)
@@ -182,8 +237,14 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
182
237
  }
183
238
  }
184
239
 
240
+ let skippedSideFile = false
185
241
  for (const file of GUARDED_MD_FILES) {
186
- mergeGuardedMarkdown(dest, pkgRoot, file)
242
+ if (mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited }) === "skipped-side-file") {
243
+ skippedSideFile = true
244
+ }
245
+ }
246
+ if (skippedSideFile) {
247
+ console.log("AGENTS.md/CLAUDE.md lack markers and were left alone")
187
248
  }
188
249
 
189
250
  fs.mkdirSync(path.join(dest, "scripts"), { recursive: true })
@@ -207,6 +268,8 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
207
268
  mergePackageJson(dest, pkgRoot)
208
269
  exec("npm", ["install"], { cwd: dest, stdio: "inherit" })
209
270
  exec("npm", ["run", "rulesync:generate"], { cwd: dest, stdio: "inherit" })
210
- exec("npm", ["run", "sync:globals:apply"], { cwd: dest, stdio: "inherit" })
271
+ if (!skipGlobalsApply) {
272
+ exec("npm", ["run", "sync:globals:apply"], { cwd: dest, stdio: "inherit" })
273
+ }
211
274
  exec("npm", ["run", "rulesync:check"], { cwd: dest, stdio: "inherit" })
212
275
  }
package/cli/init.test.ts CHANGED
@@ -415,13 +415,170 @@ describe("mergePackageJson", () => {
415
415
  mergePackageJson(dest, pkgRoot);
416
416
  const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
417
417
  expect(out.scripts.test).toBe("jest");
418
- expect(out.scripts.alpha).toBe("a");
418
+ expect(out.scripts.alpha).toBe("old");
419
419
  expect(out.scripts.foo).toBe("bar");
420
420
  expect(out.devDependencies.lodash).toBe("4");
421
421
  expect(out.devDependencies["smol-toml"]).toBe("^1.8.0");
422
422
  expect(out.devDependencies.rulesync).toBe("latest");
423
423
  });
424
424
 
425
+ it("preserves dest suffix after an upgraded grunt script prefix", () => {
426
+ const pkgRoot = stubPkgRoot({
427
+ name: "fixture-pkg",
428
+ scripts: { "rulesync:generate": "rulesync generate -t bar" },
429
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
430
+ });
431
+ const dest = tmp("pj-suffix-");
432
+ fs.writeFileSync(
433
+ path.join(dest, "package.json"),
434
+ JSON.stringify({
435
+ scripts: {
436
+ "rulesync:generate": "rulesync generate -t foo && node scripts/codex-sync.mjs",
437
+ },
438
+ }),
439
+ );
440
+ mergePackageJson(dest, pkgRoot);
441
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
442
+ expect(out.scripts["rulesync:generate"]).toBe(
443
+ "rulesync generate -t bar && node scripts/codex-sync.mjs",
444
+ );
445
+ });
446
+
447
+ it("preserves suffix after a multi-command grunt script upgrade", () => {
448
+ const pkgRoot = stubPkgRoot({
449
+ name: "fixture-pkg",
450
+ scripts: {
451
+ "rulesync:generate": "rulesync generate -t bar && node scripts/emit.mjs",
452
+ },
453
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
454
+ });
455
+ const dest = tmp("pj-multi-suffix-");
456
+ fs.writeFileSync(
457
+ path.join(dest, "package.json"),
458
+ JSON.stringify({
459
+ scripts: {
460
+ "rulesync:generate":
461
+ "rulesync generate -t foo && node scripts/emit.mjs && node scripts/codex-sync.mjs",
462
+ },
463
+ }),
464
+ );
465
+ mergePackageJson(dest, pkgRoot);
466
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
467
+ expect(out.scripts["rulesync:generate"]).toBe(
468
+ "rulesync generate -t bar && node scripts/emit.mjs && node scripts/codex-sync.mjs",
469
+ );
470
+ });
471
+
472
+ it("does not treat a non-owned prefix plus extra && as a grunt suffix", () => {
473
+ const pkgRoot = stubPkgRoot({
474
+ name: "fixture-pkg",
475
+ scripts: { alpha: "a" },
476
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
477
+ });
478
+ const dest = tmp("pj-nonowned-suffix-");
479
+ fs.writeFileSync(
480
+ path.join(dest, "package.json"),
481
+ JSON.stringify({ scripts: { alpha: "echo custom && extra" } }),
482
+ );
483
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
484
+ mergePackageJson(dest, pkgRoot);
485
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
486
+ expect(out.scripts.alpha).toBe("echo custom && extra");
487
+ expect(warnSpy).toHaveBeenCalled();
488
+ warnSpy.mockRestore();
489
+ });
490
+
491
+ it("falls through suffix scan when extra commands use bare semicolons", () => {
492
+ const pkgRoot = stubPkgRoot({
493
+ name: "fixture-pkg",
494
+ scripts: { "rulesync:generate": "rulesync generate -t bar" },
495
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
496
+ });
497
+ const dest = tmp("pj-semi-");
498
+ fs.writeFileSync(
499
+ path.join(dest, "package.json"),
500
+ JSON.stringify({
501
+ scripts: { "rulesync:generate": "echo custom;echo extra" },
502
+ }),
503
+ );
504
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
505
+ mergePackageJson(dest, pkgRoot);
506
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
507
+ expect(out.scripts["rulesync:generate"]).toBe("echo custom;echo extra");
508
+ expect(warnSpy).toHaveBeenCalled();
509
+ warnSpy.mockRestore();
510
+ });
511
+
512
+ it("keeps dest when dest script already equals src", () => {
513
+ const pkgRoot = stubPkgRoot({
514
+ name: "fixture-pkg",
515
+ scripts: { "rulesync:generate": "rulesync generate -t bar" },
516
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
517
+ });
518
+ const dest = tmp("pj-equal-");
519
+ fs.writeFileSync(
520
+ path.join(dest, "package.json"),
521
+ JSON.stringify({ scripts: { "rulesync:generate": "rulesync generate -t bar" } }),
522
+ );
523
+ mergePackageJson(dest, pkgRoot);
524
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
525
+ expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
526
+ });
527
+
528
+ it("keeps dest when it already starts with the new src value plus suffix", () => {
529
+ const pkgRoot = stubPkgRoot({
530
+ name: "fixture-pkg",
531
+ scripts: { "rulesync:generate": "rulesync generate -t bar" },
532
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
533
+ });
534
+ const dest = tmp("pj-prefix-");
535
+ const cur = "rulesync generate -t bar && node scripts/codex-sync.mjs";
536
+ fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ scripts: { "rulesync:generate": cur } }));
537
+ mergePackageJson(dest, pkgRoot);
538
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
539
+ expect(out.scripts["rulesync:generate"]).toBe(cur);
540
+ });
541
+
542
+ it("keeps unrelated custom script values and dest-only keys, warns on skip", () => {
543
+ const pkgRoot = stubPkgRoot();
544
+ const dest = tmp("pj-unrelated-");
545
+ fs.writeFileSync(
546
+ path.join(dest, "package.json"),
547
+ JSON.stringify({
548
+ scripts: { alpha: "echo custom", foo: "bar", test: "jest" },
549
+ }),
550
+ );
551
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
552
+ mergePackageJson(dest, pkgRoot);
553
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
554
+ expect(out.scripts.alpha).toBe("echo custom");
555
+ expect(out.scripts.foo).toBe("bar");
556
+ expect(out.scripts.test).toBe("jest");
557
+ expect(out.scripts.zeta).toBe("z");
558
+ expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
559
+ true,
560
+ );
561
+ warnSpy.mockRestore();
562
+ });
563
+
564
+ it("replaces a grunt-owned script with no suffix", () => {
565
+ const pkgRoot = stubPkgRoot({
566
+ name: "fixture-pkg",
567
+ scripts: { "rulesync:generate": "rulesync generate -t bar", "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply" },
568
+ devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
569
+ });
570
+ const dest = tmp("pj-owned-");
571
+ fs.writeFileSync(
572
+ path.join(dest, "package.json"),
573
+ JSON.stringify({
574
+ scripts: { "rulesync:generate": "rulesync generate -t foo" },
575
+ }),
576
+ );
577
+ mergePackageJson(dest, pkgRoot);
578
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
579
+ expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
580
+ });
581
+
425
582
  it("early return when name is @lovrozagar/grunt", () => {
426
583
  const pkgRoot = stubPkgRoot();
427
584
  const dest = tmp("pj-self-");
@@ -562,4 +719,85 @@ describe("init", () => {
562
719
  ["run", "rulesync:check"],
563
720
  ]);
564
721
  });
722
+
723
+ it("--skip-globals skips sync:globals:apply", () => {
724
+ const pkgRoot = stubPkgRoot();
725
+ const dest = tmp("grunt-skip-globals-");
726
+ const exec = vi.fn();
727
+ init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true });
728
+ expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
729
+ ["npm", ["install"]],
730
+ ["npm", ["run", "rulesync:generate"]],
731
+ ["npm", ["run", "rulesync:check"]],
732
+ ]);
733
+ });
734
+
735
+ it("auto-skip globals when telemetry.mjs exists", () => {
736
+ const pkgRoot = stubPkgRoot();
737
+ const dest = tmp("grunt-auto-tel-");
738
+ fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
739
+ fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
740
+ const exec = vi.fn();
741
+ init(dest, { pkgRoot, execFileSync: exec });
742
+ expect(exec.mock.calls.map((c) => c[1])).toEqual([
743
+ ["install"],
744
+ ["run", "rulesync:generate"],
745
+ ["run", "rulesync:check"],
746
+ ]);
747
+ });
748
+
749
+ it("auto-skip globals when AGENTS.md already has a grunt sentinel", () => {
750
+ const pkgRoot = stubPkgRoot();
751
+ const dest = tmp("grunt-auto-sent-");
752
+ fs.writeFileSync(
753
+ path.join(dest, "AGENTS.md"),
754
+ "<!-- grunt:begin -->\nold\n<!-- grunt:end -->\n",
755
+ );
756
+ const exec = vi.fn();
757
+ init(dest, { pkgRoot, execFileSync: exec });
758
+ expect(exec.mock.calls.map((c) => c[1])).toEqual([
759
+ ["install"],
760
+ ["run", "rulesync:generate"],
761
+ ["run", "rulesync:check"],
762
+ ]);
763
+ });
764
+
765
+ it("re-run without sentinel does not create/overwrite *.grunt.md", () => {
766
+ const pkgRoot = stubPkgRoot();
767
+ const dest = tmp("grunt-rerun-nosent-");
768
+ fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
769
+ fs.writeFileSync(path.join(dest, "AGENTS.md"), "hand-written consumer doc\n");
770
+ fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
771
+ fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
772
+ fs.writeFileSync(path.join(dest, "CLAUDE.grunt.md"), "stale side");
773
+ const exec = vi.fn();
774
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
775
+ init(dest, { pkgRoot, execFileSync: exec });
776
+ expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe("hand-written consumer doc\n");
777
+ expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe("hand-written consumer doc\n");
778
+ expect(fs.readFileSync(path.join(dest, "CLAUDE.grunt.md"), "utf8")).toBe("stale side");
779
+ expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
780
+ expect(logSpy.mock.calls.filter((c) => String(c[0]).includes("lack markers"))).toHaveLength(1);
781
+ logSpy.mockRestore();
782
+ });
783
+
784
+ it("first init without sentinel still creates *.grunt.md", () => {
785
+ const pkgRoot = stubPkgRoot();
786
+ const dest = tmp("grunt-first-nosent-");
787
+ fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
788
+ fs.writeFileSync(path.join(dest, "AGENTS.md"), "hand-written consumer doc\n");
789
+ const exec = vi.fn();
790
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
791
+ init(dest, { pkgRoot, execFileSync: exec });
792
+ expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe("hand-written consumer doc\n");
793
+ expect(fs.readFileSync(path.join(dest, "CLAUDE.grunt.md"), "utf8")).toBe("CLAUDE.md content");
794
+ expect(fs.readFileSync(path.join(dest, "AGENTS.grunt.md"), "utf8")).toBe("AGENTS.md content");
795
+ expect(exec.mock.calls.map((c) => c[1])).toEqual([
796
+ ["install"],
797
+ ["run", "rulesync:generate"],
798
+ ["run", "sync:globals:apply"],
799
+ ["run", "rulesync:check"],
800
+ ]);
801
+ logSpy.mockRestore();
802
+ });
565
803
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lovrozagar/grunt",
3
- "version": "0.3.1",
4
- "description": "Multi-agent orchestrator SoT \u2014 deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
3
+ "version": "0.3.3",
4
+ "description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
5
5
  "keywords": [
6
6
  "agents",
7
7
  "grunt",
@@ -47,6 +47,7 @@ export const GIT_HOOKS = [
47
47
  "scripts/git-hooks/pre-push",
48
48
  "scripts/git-hooks/pre-push.mjs",
49
49
  ];
50
+ export const POST_WRITE_MATCHER = "Write|Edit|write|search_replace";
50
51
 
51
52
  const CAVEAT_BEHIND =
52
53
  "NOT ported: PreToolUse check-behind.mjs. Gemini has no confirmed before-tool deny event.";
@@ -180,7 +181,7 @@ function desiredPost(root) {
180
181
  const hooks = [];
181
182
  if (hasScript(root, "validate.mjs")) hooks.push(hookCmd(VALIDATE));
182
183
  if (hasScript(root, "sim.mjs")) hooks.push(hookCmd(SIM));
183
- return hooks.length ? [{ matcher: "Write|Edit", hooks }] : null;
184
+ return hooks.length ? [{ matcher: POST_WRITE_MATCHER, hooks }] : null;
184
185
  }
185
186
 
186
187
  function consumerNamed(root) {
@@ -210,10 +211,10 @@ function consumerNamed(root) {
210
211
  timeout: 5,
211
212
  };
212
213
  if (hasScript(root, "validate.mjs")) {
213
- named["validate-artifact"] = { event: "PostToolUse", matcher: ".*", command: VALIDATE };
214
+ named["validate-artifact"] = { event: "PostToolUse", matcher: POST_WRITE_MATCHER, command: VALIDATE };
214
215
  }
215
216
  if (hasScript(root, "sim.mjs")) {
216
- named["simulate-artifact"] = { event: "PostToolUse", matcher: ".*", command: SIM };
217
+ named["simulate-artifact"] = { event: "PostToolUse", matcher: POST_WRITE_MATCHER, command: SIM };
217
218
  }
218
219
  return named;
219
220
  }
@@ -262,7 +263,7 @@ function applyAgents(root) {
262
263
  }
263
264
  const post = desiredPost(root);
264
265
  if (post) {
265
- rulesync.PostToolUse = [{ matcher: ".*", hooks: post[0].hooks }];
266
+ rulesync.PostToolUse = [{ matcher: POST_WRITE_MATCHER, hooks: post[0].hooks }];
266
267
  }
267
268
  const named = presentAutorun(root).length ? consumerNamed(root) : {};
268
269
  saveJson(abs, { ...live, ...named, rulesync });