@lovrozagar/grunt 0.4.1 → 0.4.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.
@@ -17,11 +17,9 @@ import {
17
17
  import {
18
18
  denyResponse,
19
19
  fatHookOutput,
20
- fileSizeBytes,
21
20
  isWorkspaceGruntJob,
22
21
  isParentOrchestrator,
23
22
  processFatTools,
24
- resolveReadPath,
25
23
  rewriteGruntScratchPath,
26
24
  subagentTypeOf,
27
25
  } from "../../scripts/gate-fat-tools.mjs";
@@ -29,8 +27,8 @@ import { persistPlan } from "../../scripts/persist-plan.mjs";
29
27
  import { persistHandoff } from "../../scripts/persist-handoff.mjs";
30
28
  import { parseNeed } from "../../scripts/parse-need.mjs";
31
29
  import { resolveJobCwd, runJob } from "../../scripts/grunt-job.mjs";
32
- import { logTelemetry, ORCHESTRATOR_LOGS_DIR } from "../../scripts/telemetry.mjs";
33
30
 
31
+ export const ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
34
32
  export const DENY_REASON =
35
33
  "First action=spawn implementer|grunt|thinker. Deny expected. Only /solo this session escapes.";
36
34
  export const STOP_REASONS = [
@@ -70,7 +68,6 @@ const PARENT_SKILLS = new Set([
70
68
  "write-plan",
71
69
  "implement-plan",
72
70
  ]);
73
- const READ_TOOLS = new Set(["readfile", "read"]);
74
71
  const INTERCEPT_JOBS = new Set(["search", "exec"]);
75
72
  const MAX_STOP = 3;
76
73
  const MAX_INTERCEPT = 3;
@@ -104,7 +101,6 @@ function preToolUse(data) {
104
101
  const toolKey = eventKey(toolName);
105
102
  let toolInput = data.toolInput;
106
103
  if (toolInput == null) toolInput = data.tool_input;
107
- logPreTool(data, toolKey, toolInput);
108
104
 
109
105
  const sub = subagentTypeOf(data);
110
106
  if (sub || !isParentOrchestrator(data)) {
@@ -173,30 +169,6 @@ function hasParentEscape(data) {
173
169
  return Boolean(p && fs.existsSync(p));
174
170
  }
175
171
 
176
- function logPreTool(data, toolKey, toolInput) {
177
- const fields = {};
178
- if (SPAWN_TOOLS.has(toolKey) && toolInput && typeof toolInput === "object") {
179
- fields.spawnType = String(
180
- toolInput.subagent_type || toolInput.subagentType || "",
181
- );
182
- const rf = toolInput.resume_from ?? toolInput.resumeFrom;
183
- fields.resumeFromCount =
184
- rf == null || rf === "" ? 0 : Array.isArray(rf) ? rf.length : 1;
185
- }
186
- if (READ_TOOLS.has(toolKey) && toolInput && typeof toolInput === "object") {
187
- const raw =
188
- toolInput.target_file ||
189
- toolInput.file_path ||
190
- toolInput.targetFile ||
191
- toolInput.filePath ||
192
- "";
193
- fields.fileSizeBytes = fileSizeBytes(resolveReadPath(raw, data));
194
- }
195
- if (Object.keys(fields).length) {
196
- logTelemetry("pretool", fields, workspaceRootOf(data));
197
- }
198
- }
199
-
200
172
  function emitFat(data) {
201
173
  const fat = fatHookOutput(processFatTools(data));
202
174
  if (!fat) return null;
@@ -481,22 +453,11 @@ export function lastAssistantFromTranscript(filePath) {
481
453
  }
482
454
  }
483
455
 
484
- function logParentStop(data, fields) {
485
- logTelemetry("parent-stop", fields, workspaceRootOf(data));
486
- }
487
-
488
456
  function stop(data) {
489
457
  if (data.subagentType || data.subagent_type) {
490
458
  return interceptNeed(data, "Stop");
491
459
  }
492
460
  if (data.stopHookActive || data.stop_hook_active) {
493
- logParentStop(data, {
494
- recap: false,
495
- recapSource: "none",
496
- attempt: 0,
497
- failOpen: true,
498
- stopHookActive: true,
499
- });
500
461
  return 0;
501
462
  }
502
463
  const reason = String(data.reason || "");
@@ -512,28 +473,16 @@ function stop(data) {
512
473
  }
513
474
 
514
475
  const payloadMsg = payloadAssistantMessage(data);
515
- let recapSource = "none";
516
- let msg = "";
517
- if (payloadMsg) {
518
- msg = payloadMsg;
519
- recapSource = "payload";
520
- } else {
476
+ let msg = payloadMsg;
477
+ if (!msg) {
521
478
  const tp = data.transcript_path || data.transcriptPath || "";
522
479
  msg = lastAssistantFromTranscript(tp);
523
- if (msg) recapSource = "transcript";
524
480
  }
525
481
 
526
482
  const waitFirst = firstNonEmptyStripped(msg) === WAIT_GRUNT;
527
483
  if (waitFirst && !isWaitGruntExact(msg)) {
528
484
  // fall through to block — leftover lines not allowed on mid-turn wait
529
485
  } else if (isRecap(msg)) {
530
- logParentStop(data, {
531
- recap: true,
532
- recapSource,
533
- attempt: 0,
534
- failOpen: false,
535
- stopHookActive: false,
536
- });
537
486
  return 0;
538
487
  }
539
488
 
@@ -544,26 +493,12 @@ function stop(data) {
544
493
  if (!Number.isFinite(n) || n < 0) n = 0;
545
494
  }
546
495
  if (n >= MAX_STOP) {
547
- logParentStop(data, {
548
- recap: false,
549
- recapSource,
550
- attempt: n,
551
- failOpen: true,
552
- stopHookActive: false,
553
- });
554
496
  return 0;
555
497
  }
556
498
  if (stopStamp) {
557
499
  fs.mkdirSync(path.dirname(stopStamp), { recursive: true });
558
500
  fs.writeFileSync(stopStamp, String(n + 1));
559
501
  }
560
- logParentStop(data, {
561
- recap: false,
562
- recapSource,
563
- attempt: n + 1,
564
- failOpen: false,
565
- stopHookActive: false,
566
- });
567
502
  const reasonText = STOP_REASONS[Math.min(n, STOP_REASONS.length - 1)];
568
503
  emit({ decision: "block", reason: reasonText });
569
504
  return 0;
@@ -572,30 +507,18 @@ function stop(data) {
572
507
  function interceptNeed(data, hookEventName) {
573
508
  const ws = workspaceRootOf(data);
574
509
  if (data.stopHookActive || data.stop_hook_active) {
575
- logTelemetry(
576
- "stop",
577
- { parseOk: false, intercept: "none" },
578
- ws,
579
- );
580
510
  return 0;
581
511
  }
582
512
  const msg = data.lastAssistantMessage || data.last_assistant_message || "";
583
513
  const parsed = parseNeed(msg);
584
514
  if (!parsed.ok) {
585
- logTelemetry("stop", { parseOk: false, intercept: "none" }, ws);
586
515
  return 0;
587
516
  }
588
517
  const jobs = parsed.jobs;
589
- const names = jobs.map((j) => j.job);
590
518
  if (
591
519
  jobs.length > 4 ||
592
520
  !jobs.every((j) => INTERCEPT_JOBS.has(j.job))
593
521
  ) {
594
- logTelemetry(
595
- "stop",
596
- { parseOk: true, jobs: names, intercept: "none" },
597
- ws,
598
- );
599
522
  return 0;
600
523
  }
601
524
 
@@ -606,11 +529,6 @@ function interceptNeed(data, hookEventName) {
606
529
  if (!Number.isFinite(n) || n < 0) n = 0;
607
530
  }
608
531
  if (n >= MAX_INTERCEPT) {
609
- logTelemetry(
610
- "stop",
611
- { parseOk: true, jobs: names, intercept: "none" },
612
- ws,
613
- );
614
532
  return 0;
615
533
  }
616
534
 
@@ -635,16 +553,6 @@ function interceptNeed(data, hookEventName) {
635
553
  result = { fallback: true, text: "FALLBACK\n" };
636
554
  }
637
555
  if (!result || result.fallback) {
638
- logTelemetry(
639
- "stop",
640
- {
641
- parseOk: true,
642
- jobs: names,
643
- intercept: "FALLBACK",
644
- query: job.query,
645
- },
646
- ws,
647
- );
648
556
  return 0;
649
557
  }
650
558
  parts.push(String(result.text || "").trimEnd());
@@ -654,11 +562,6 @@ function interceptNeed(data, hookEventName) {
654
562
  fs.writeFileSync(interceptStamp, String(n + 1));
655
563
  }
656
564
  const reason = parts.join("\n");
657
- logTelemetry(
658
- "stop",
659
- { parseOk: true, jobs: names, intercept: "grunt-job" },
660
- ws,
661
- );
662
565
  emit({
663
566
  decision: "block",
664
567
  hookSpecificOutput: {
@@ -4,17 +4,17 @@ tags: [hooks]
4
4
 
5
5
  # Hooks
6
6
 
7
- SessionStart: keep empty (token baseline). Do not inject context. Do not register a SessionStart hook on `orchestrate-parent.json`.
7
+ SessionStart: keep empty (token baseline). Do not inject context. Do not register a SessionStart hook on `../../.grok/hooks/orchestrate-parent.json`.
8
8
 
9
9
  Grok PostToolUse / UserPromptSubmit / SessionStart are observe-only — do not attempt output scrub; compress via RTK / spawn / isolation verdict only. Do not add PostToolUse scrub hooks.
10
10
 
11
- Claude / Codex / Antigravity PreToolUse still run `scripts/scrub-spawn-prompt.mjs` and `scripts/gate-fat-tools.mjs`. Grok does not — spawn/fat rewrite is `orchestrate-parent.js` only. RTK stays `rtk.json`.
11
+ Claude / Codex / Antigravity PreToolUse still run `../../scripts/scrub-spawn-prompt.mjs` and `../../scripts/gate-fat-tools.mjs`. Grok does not — spawn/fat rewrite is `../../.grok/hooks/orchestrate-parent.js` only. RTK stays `../../.grok/hooks/rtk.json`.
12
12
 
13
- Parent Read|Write|Edit|Bash|Skill deny unless `/solo` (session) or `/parent` last-ditch stamp. Grok PreToolUse has no matcher (Read included). Skills spawn-first; do not instruct parent Read/Bash. `.tmp/plans/` and `.tmp/grunt/handoffs/` Write still persist. Parent Bash still allowed for workspace `grunt-job.mjs` `--job search|exec`. Children with a type skip parent-deny (fat-only). Skill allowlist without escape: `parent|explain|solo|cascade|handoff|pickup|write-plan|implement-plan`. Deny reason: `First action=spawn implementer|grunt|thinker. Deny expected. Only /solo this session escapes.`. Parent `end_turn` is tagged recap (advise leftover numbered pick on own lines after) or `[orchestrator]: wait grunt`. Mid-turn stays exactly `[orchestrator]: wait grunt`.
13
+ Parent Read|Write|Edit|Bash|Skill deny unless `/solo` (session) or `/parent` last-ditch stamp. Grok PreToolUse has no matcher (Read included). Skills spawn-first; do not instruct parent Read/Bash. `.tmp/plans/` and `.tmp/grunt/handoffs/` Write still persist. Parent Bash still allowed for workspace `../../scripts/grunt-job.mjs` `--job search|exec`. Children with a type skip parent-deny (fat-only). Skill allowlist without escape: `parent|explain|solo|cascade|handoff|pickup|write-plan|implement-plan`. Deny reason: `First action=spawn implementer|grunt|thinker. Deny expected. Only /solo this session escapes.`. Parent `end_turn` is tagged recap (advise leftover numbered pick on own lines after) or `[orchestrator]: wait grunt`. Mid-turn stays exactly `[orchestrator]: wait grunt`.
14
14
 
15
15
  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.
16
16
 
17
- 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`. Parent `end_turn` is tagged recap (advise leftover numbered pick on own lines after) or `[orchestrator]: wait grunt`. Mid-turn stays exactly `[orchestrator]: wait grunt`. First non-empty line must match legal `[orchestrator|grunt|implementer|thinker|handoff]:`. Claude Agent launch ≠ done; in-flight host Stop → wait recap `[orchestrator]: wait grunt` only; no SendMessage. Or consume `parent-escape-{sid}` once (`/parent` last-ditch, not “use parent tools”). No `isCheap` / trivia. Stamps: `tools-used` / `stop-block` / `need-intercept` / `parent-escape`. Skills point to cascade; do not paste protocol here.
17
+ 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/` (`../../scripts/persist-plan.mjs`) and `.tmp/grunt/handoffs/` (`../../scripts/persist-handoff.mjs`); both validate + rewrite path/content, invalid body denies. Parent Stop (`MAX_STOP=3`): do not waive impl finals for `tools-used`. Parent `end_turn` is tagged recap (advise leftover numbered pick on own lines after) or `[orchestrator]: wait grunt`. Mid-turn stays exactly `[orchestrator]: wait grunt`. First non-empty line must match legal `[orchestrator|grunt|implementer|thinker|handoff]:`. Claude Agent launch ≠ done; in-flight host Stop → wait recap `[orchestrator]: wait grunt` only; no SendMessage. Or consume `parent-escape-{sid}` once (`/parent` last-ditch, not “use parent tools”). No `isCheap` / trivia. Stamps: `tools-used` / `stop-block` / `need-intercept` / `parent-escape`. Skills point to cascade; do not paste protocol here.
18
18
 
19
19
  `/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.
20
20
 
@@ -21,7 +21,7 @@ Cheap outline. Not a file dump.
21
21
  - Default output: `.rulesync/reference/output.md` (overview.md → AGENTS.md; also agent bodies + CLAUDE.md, not auto-injected)
22
22
 
23
23
  ## Grok hand files
24
- - `.grok/hooks/orchestrate-parent.js` + `orchestrate-parent.json` — parent spawn/fat/stop/plan-write / SubagentStop intercept
24
+ - `.grok/hooks/orchestrate-parent.js` + `.grok/hooks/orchestrate-parent.json` — parent spawn/fat/stop/plan-write / SubagentStop intercept
25
25
  - `.grok/hooks/rtk.json` — RTK
26
26
  - `.grok/skills/{write-plan,implement-plan,shared}/` — hand-only (no rulesync SSOT); point to cascade, do not paste
27
27
  - `.grok/skills/{parent,explain,handoff,pickup,solo,cascade,commit,commit-and-push,commit-push,commit-push-deploy,commit-push-release}/` — generated from `.rulesync/skills/`; do not hand-edit
@@ -45,12 +45,11 @@ Cheap outline. Not a file dump.
45
45
  - `scripts/emit-mcp-policy.mjs` — emit project MCP deny-default from `.rulesync/mcp-policy.jsonc` (not `rulesync -f mcp`); owns `.gemini/settings.json` `mcpServers` (merge extra keys)
46
46
  - `scripts/emit-gemini.mjs` — `GEMINI.md` (`@AGENTS.md`) + `.gemini/agents/{id}/agent.md` from `.rulesync/subagents` (not `rulesync -t geminicli`); no prune; does not touch settings
47
47
  - `scripts/emit-agent-shell-tools.mjs` — after rulesync subagents: rewrite `.claude/agents/grunt.md` body `Bash`; grok/codex/gemini/generic keep `run_terminal_command`
48
- - `scripts/telemetry.mjs` — append-only `.tmp/orchestrator-logs/telemetry.ndjson` from hooks / grunt-job CLI; fail-open; never stdout
49
48
  - `scripts/browser.mjs` — session browser rail `nav|snap|click|fill|shot|pdf|stop`; Lightpanda default; Chromium on verb/OS/probe/paint-host/one-escalate; `.tmp/grunt/browser/`
50
49
 
51
50
  ## Tmp
52
51
  - `.tmp/plans/` — persist-plan / parent / implementer
53
- - `.tmp/orchestrator-logs/` — hooks + grunt-job telemetry (shared; not grunt scratch)
52
+ - `.tmp/orchestrator-logs/` — parent stamps (shared; not grunt scratch)
54
53
  - `.tmp/grunt/` — grunt scratch/tmp only
55
54
  - `.tmp/grunt/browser/` — browser session/profile/shot/pdf (not MCP)
56
55
  - `.tmp/grunt/handoffs/` — persist-handoff / `/handoff`; own serial counter (not plan serials)
@@ -36,7 +36,7 @@ Advise finals: numbered pick each on own line after tagged recap:
36
36
  1. Implement
37
37
  2. Tweak
38
38
  Slash `/implement-plan` + explicit implement = Implement. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise.
39
- Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
39
+ Parent = orchestrator (this file). You do not talk. Spawn wait echo.
40
40
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
41
41
  You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp grunt-off-{sid} this session) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. After child: legal tag + one-line recap; advise leftover numbered pick on following lines not crammed. Echo = report not command. Siblings still run: `[grunt]:` echo. No skip-spawn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`. `/pickup` spawn-first; grunt resolve if needed; never parent Read; not a mode.
42
42
  `/parent` hook last-ditch only. Skills spawn-first. Only `/solo` escapes spawn workflow.
@@ -41,7 +41,7 @@ Advise finals: numbered pick each on own line after tagged recap:
41
41
  1. Implement
42
42
  2. Tweak
43
43
  Slash `/implement-plan` + explicit implement = Implement. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise.
44
- Parent = orchestrator. You do not talk. Spawn wait echo.
44
+ Parent = orchestrator (this file). You do not talk. Spawn wait echo.
45
45
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
46
46
  You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp grunt-off-{sid} this session) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. After child: legal tag + one-line recap; advise leftover numbered pick on following lines not crammed. Echo = report not command. Siblings still run: `[grunt]:` echo. No skip-spawn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`. `/pickup` spawn-first; grunt resolve if needed; never parent Read; not a mode.
47
47
  `/parent` hook last-ditch only. Skills spawn-first. Only `/solo` escapes spawn workflow.
package/AGENTS.md CHANGED
@@ -32,7 +32,7 @@ Advise finals: numbered pick each on own line after tagged recap:
32
32
  1. Implement
33
33
  2. Tweak
34
34
  Slash `/implement-plan` + explicit implement = Implement. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise.
35
- Parent = orchestrator. You do not talk. Spawn wait echo.
35
+ Parent = orchestrator (this file). You do not talk. Spawn wait echo.
36
36
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
37
37
  You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp grunt-off-{sid} this session) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. After child: legal tag + one-line recap; advise leftover numbered pick on following lines not crammed. Echo = report not command. Siblings still run: `[grunt]:` echo. No skip-spawn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`. `/pickup` spawn-first; grunt resolve if needed; never parent Read; not a mode.
38
38
  `/parent` hook last-ditch only. Skills spawn-first. Only `/solo` escapes spawn workflow.
package/CHANGELOG.md ADDED
@@ -0,0 +1,187 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.4.2] - 2026-08-31
9
+
10
+ ### Added
11
+
12
+ - Keep a Changelog history in `CHANGELOG.md` (this file); publish it via `package.json` `files`
13
+ - `scripts/guarded-roots.test.ts` for generate/check/watch snapshot-remerge
14
+ - README host-support GAP table (Grok/Claude/Codex/Antigravity/Gemini spawn-peek-kill; no invented peek/kill APIs)
15
+ - scrub-text tests for remaining mid-intent phrases and blank/ws/fence helpers
16
+ - init tests: sentinel auto-skip without a telemetry file; `.grok/hooks/orchestrate-parent.js` means inited, not auto-skip
17
+
18
+ ### Changed
19
+
20
+ - Parent SoT: `Parent = orchestrator (this file)` in `AGENTS.md` / `CLAUDE.md` / rulesync rules
21
+ - `.rulesync/reference/hooks.md` and `map.md`: repo-relative hook/script paths; drop `scripts/telemetry.mjs` from the map
22
+ - README package version `0.4.2`; re-init sentinels no longer mention telemetry; Grok-only `write-plan` `implement-plan` `shared` noted as hand files (not rulesync SSOT); published `files` list documents `guarded-roots.mjs` + `CHANGELOG.md` and drops telemetry
23
+ - `cli/init.mjs`: drop `telemetry.mjs` from `PRODUCT_SCRIPTS`; `shouldAutoSkipGlobals` no longer treats `scripts/telemetry.mjs` as a sentinel
24
+ - `scripts/grunt-job.mjs` and `.grok/hooks/orchestrate-parent.js`: strip `logTelemetry`; `ORCHESTRATOR_LOGS_DIR` inlined on the hook
25
+ - Tests drop NDJSON telemetry assertions (`cli/init.test.ts`, `scripts/orchestrate-parent.test.ts`, `scripts/gate-fat-tools.test.ts`)
26
+
27
+ ### Removed
28
+
29
+ - Product telemetry: `scripts/telemetry.mjs` (append-only `.tmp/orchestrator-logs/telemetry.ndjson` from hooks / grunt-job; fail-open)
30
+ - Telemetry as a re-init / globals auto-skip sentinel
31
+ - `scripts/telemetry.mjs` from the published `files` list (`scripts/telemetry.test.ts` already absent)
32
+
33
+ ## [0.4.1] - 2026-08-31
34
+
35
+ ### Added
36
+
37
+ - Init snapshot/remerge of guarded roots `AGENTS.md` `CLAUDE.md` `GEMINI.md` around generate/check/watch
38
+ - Orchestrator advise-first Implement picks; `ok` / `yes` / `continue` ≠ implement after conversational advise
39
+
40
+ ## [0.4.0] - 2026-08-31
41
+
42
+ ### Added
43
+
44
+ - Lightpanda-first session browser rail `nav|snap|click|fill|shot|pdf|stop` plus unified doctor
45
+ - Browser skill shipped to host trees
46
+ - `/pickup` spawn-first handoff counterpart (not a mode)
47
+
48
+ ## [0.3.11] - 2026-08-28
49
+
50
+ ### Added
51
+
52
+ - Prompt-is-spec implementer write allowlist (gate)
53
+
54
+ ## [0.3.10] - 2026-08-28
55
+
56
+ ### Added
57
+
58
+ - `commit-push` release and deploy skills
59
+
60
+ ### Changed
61
+
62
+ - README rewritten as a full product guide
63
+
64
+ ### Removed
65
+
66
+ - Architecture Excalidraw diagram
67
+
68
+ ## [0.3.9] - 2026-08-28
69
+
70
+ ### Changed
71
+
72
+ - Clack CLI prompts
73
+
74
+ ### Fixed
75
+
76
+ - Orchestrator spawn-first deny Read; clarify `DENY_REASON`
77
+
78
+ ## [0.3.8] - 2026-08-27
79
+
80
+ ### Added
81
+
82
+ - `/cascade` exit-solo skill
83
+
84
+ ## [0.3.7] - 2026-08-27
85
+
86
+ ### Fixed
87
+
88
+ - Consumer hook permissions deny without `Write(platform)`
89
+
90
+ ## [0.3.6] - 2026-08-27
91
+
92
+ ### Fixed
93
+
94
+ - Deny parent Write; voice Stop
95
+
96
+ ## [0.3.5] - 2026-08-27
97
+
98
+ ### Fixed
99
+
100
+ - Hook `package.json` `"type": "module"` so Stop ESM loads
101
+ - Positive-only Stop; thinker offload
102
+
103
+ ## [0.3.4] - 2026-08-27
104
+
105
+ ### Fixed
106
+
107
+ - Recap tags; stop-block reset
108
+
109
+ ## [0.3.3] - 2026-08-27
110
+
111
+ ### Added
112
+
113
+ - Stop walls with always-do XOR map
114
+
115
+ ## [0.3.2] - 2026-08-27
116
+
117
+ ### Added
118
+
119
+ - `--skip-globals` and smarter init merge
120
+
121
+ ## [0.3.1] - 2026-08-27
122
+
123
+ ### Added
124
+
125
+ - CI publish `@lovrozagar/grunt` to GitHub Packages
126
+
127
+ ### Fixed
128
+
129
+ - Stop recap, spawn cap, scratch rewrite
130
+
131
+ ## [0.3.0] - 2026-08-27
132
+
133
+ ### Added
134
+
135
+ - Skills: `handoff`, `solo`, `commit`, `commit-and-push`, `explain`, `parent` (rulesync + host trees)
136
+ - Larger init pipeline: `emit-gemini`, `emit-agent-shell-tools`, `emit-mcp-policy`, `hooks-union`, `persist-handoff`
137
+ - Gemini CLI agent shells + settings emit
138
+ - Orchestrate-parent expansion; host `hooks.json`
139
+
140
+ ### Changed
141
+
142
+ - Cascade/map/rules sync; package `0.3.0`
143
+
144
+ ## [0.2.0] - 2026-08-27
145
+
146
+ ### Added
147
+
148
+ - Agents context
149
+
150
+ ### Changed
151
+
152
+ - Slim agent prompts; max permissions
153
+
154
+ ## [0.1.0] - 2026-08-26
155
+
156
+ First published tag. No `v0.0.x`.
157
+
158
+ ### Added
159
+
160
+ - `@lovrozagar/grunt` npm package: deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo (`npx @lovrozagar/grunt` / `init`)
161
+ - Host emit matrix: Grok, Claude Code, Codex, Antigravity; Gemini CLI tracked as a generate gap
162
+ - Agents: orchestrator (parent) plus grunt / implementer / thinker
163
+ - Product scripts including `grunt-job`, persist-plan, emit-mcp-policy, gate-fat-tools, scrub-spawn-prompt, scrub-text, check-globals, sync-global-settings, purge-global-mcps, and `scripts/telemetry.mjs` NDJSON (present from the first commit)
164
+ - Skills: `explain`, `parent`, `terse`; Grok-only `write-plan` / `implement-plan`
165
+ - GitHub Actions CI and npm `0.1.0` release
166
+
167
+ ### Fixed
168
+
169
+ - Terse and spawn rules
170
+
171
+ [0.4.2]: https://github.com/lovrozagar/grunt/compare/v0.4.1...v0.4.2
172
+ [0.4.1]: https://github.com/lovrozagar/grunt/compare/v0.4.0...v0.4.1
173
+ [0.4.0]: https://github.com/lovrozagar/grunt/compare/v0.3.11...v0.4.0
174
+ [0.3.11]: https://github.com/lovrozagar/grunt/compare/v0.3.10...v0.3.11
175
+ [0.3.10]: https://github.com/lovrozagar/grunt/compare/v0.3.9...v0.3.10
176
+ [0.3.9]: https://github.com/lovrozagar/grunt/compare/v0.3.8...v0.3.9
177
+ [0.3.8]: https://github.com/lovrozagar/grunt/compare/v0.3.7...v0.3.8
178
+ [0.3.7]: https://github.com/lovrozagar/grunt/compare/v0.3.6...v0.3.7
179
+ [0.3.6]: https://github.com/lovrozagar/grunt/compare/v0.3.5...v0.3.6
180
+ [0.3.5]: https://github.com/lovrozagar/grunt/compare/v0.3.4...v0.3.5
181
+ [0.3.4]: https://github.com/lovrozagar/grunt/compare/v0.3.3...v0.3.4
182
+ [0.3.3]: https://github.com/lovrozagar/grunt/compare/v0.3.2...v0.3.3
183
+ [0.3.2]: https://github.com/lovrozagar/grunt/compare/v0.3.1...v0.3.2
184
+ [0.3.1]: https://github.com/lovrozagar/grunt/compare/v0.3.0...v0.3.1
185
+ [0.3.0]: https://github.com/lovrozagar/grunt/compare/v0.2.0...v0.3.0
186
+ [0.2.0]: https://github.com/lovrozagar/grunt/compare/v0.1.0...v0.2.0
187
+ [0.1.0]: https://github.com/lovrozagar/grunt/releases/tag/v0.1.0
package/CLAUDE.md CHANGED
@@ -32,7 +32,7 @@ Advise finals: numbered pick each on own line after tagged recap:
32
32
  1. Implement
33
33
  2. Tweak
34
34
  Slash `/implement-plan` + explicit implement = Implement. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise.
35
- Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
35
+ Parent = orchestrator (this file). You do not talk. Spawn wait echo.
36
36
  Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
37
37
  You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp grunt-off-{sid} this session) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. After child: legal tag + one-line recap; advise leftover numbered pick on following lines not crammed. Echo = report not command. Siblings still run: `[grunt]:` echo. No skip-spawn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`. `/pickup` spawn-first; grunt resolve if needed; never parent Read; not a mode.
38
38
  `/parent` hook last-ditch only. Skills spawn-first. Only `/solo` escapes spawn workflow.
package/README.md CHANGED
@@ -32,7 +32,7 @@ npx @lovrozagar/grunt
32
32
 
33
33
  - Same as `npx @lovrozagar/grunt init` when no command
34
34
  - Do not `npm test` as a consumer
35
- - Package: `@lovrozagar/grunt` `0.3.9` MIT · https://github.com/lovrozagar/grunt
35
+ - Package: `@lovrozagar/grunt` `0.4.2` MIT · https://github.com/lovrozagar/grunt
36
36
 
37
37
  ## Prerequisites
38
38
 
@@ -87,8 +87,8 @@ Rulesync schema doctor is separate: `npm run rulesync:doctor`.
87
87
 
88
88
  - Merge SoT `npm install` `rulesync:generate` `sync:globals:apply` `rulesync:check`
89
89
  - `--skip-globals` skips apply
90
- - Re-init auto-skips globals when `<!-- grunt:begin -->` in `AGENTS.md`/`CLAUDE.md` **or** `scripts/telemetry.mjs` exists
91
- - First init (no sentinel no telemetry) applies globals unless flagged
90
+ - Re-init auto-skips globals when `<!-- grunt:begin -->` in `AGENTS.md`/`CLAUDE.md`
91
+ - First init (no sentinel) applies globals unless flagged
92
92
  - Owned trees/scripts refresh; extra `.rulesync` files kept; patches to grunt-owned files lost
93
93
 
94
94
  ## Version bump
@@ -120,10 +120,13 @@ Lightpanda-first session CLI: `node scripts/browser.mjs nav|snap|click|fill|shot
120
120
 
121
121
  ## Skills
122
122
 
123
- Present under `.claude` / `.rulesync` / `.agents`:
123
+ Present under `.claude` / `.rulesync` / `.agents` (`rulesync -f skills` mirrors SSOT):
124
124
 
125
125
  - `browser` `cascade` `commit` `commit-and-push` `commit-push` `commit-push-deploy` `commit-push-release` `explain` `handoff` `pickup` `parent` `solo`
126
- - Grok-only: `write-plan` `implement-plan` (`/write-plan` plan-only → `next: /implement-plan`; advise unlock = pick **1. Implement**, not `ok`/`yes`/`go`; slash `/implement-plan` still valid alias)
126
+
127
+ Grok-only (hand files under `.grok/skills/`; no `.rulesync/skills` SSOT; not mirrored):
128
+
129
+ - `write-plan` `implement-plan` `shared` (`/write-plan` plan-only → `next: /implement-plan`; advise unlock = pick **1. Implement**, not `ok`/`yes`/`go`; slash `/implement-plan` still valid alias)
127
130
 
128
131
  ## Generate
129
132
 
@@ -141,7 +144,7 @@ Emit writes other-CLI trees from `.rulesync` for the **next** process of that CL
141
144
  ## Config
142
145
 
143
146
  - SoT merge into consumer repo; existing configs kept where not grunt-owned
144
- - Globals: first init apply; re-init auto-skip (sentinel / telemetry) or `--skip-globals`
147
+ - Globals: first init apply; re-init auto-skip (sentinel) or `--skip-globals`
145
148
  - `sync-globals` / `purge-mcps`: dry-run default; `--apply` writes
146
149
  - Hosts: grok claude codex gemini antigravity
147
150
 
@@ -155,6 +158,18 @@ Draw **one** CLI host bubble. That bubble is **this** session’s CLI: Grok Buil
155
158
 
156
159
  User-visible conversation attaches only to the parent. Children never talk to the user. Children never spawn.
157
160
 
161
+ ### Host support (GAP)
162
+
163
+ Not feature-parity across hosts. In-tree mapping only; do not invent peek/kill APIs. GAP rows: no fake peeks, no auto-kill; block on spawn return and classify `done`.
164
+
165
+ | Host | Spawn | Peek | Kill |
166
+ | --- | --- | --- | --- |
167
+ | Grok | `spawn_subagent` `background:true` → `task_id` | `get_command_or_subagent_output` + `timeout_ms=60000` | `kill_command_or_subagent` (user-ask only) |
168
+ | Claude Code | `Agent` if the parent session exposes it | GAP unless an in-tree schema names a status/output tool on that id (do not invent `TaskOutput`); else block on Agent return and classify `done`. Agent launch ≠ child done; in-flight host Stop → only `[orchestrator]: wait grunt`; no SendMessage | GAP unless in-tree; no auto-kill |
169
+ | Codex | host agent/call | GAP; block on host agent/call return; classify `done` | GAP; no auto-kill |
170
+ | Antigravity | main-session parent | GAP peek/kill; main-session parent | GAP; no auto-kill |
171
+ | Gemini | not emitted; tracked gap | GAP; no fake peeks | GAP; no auto-kill |
172
+
158
173
  ### Nested diagram
159
174
 
160
175
  Same topology as the session recap; every legal edge labeled.
@@ -318,15 +333,17 @@ Repo-relative (repository root):
318
333
 
319
334
  ## Layout
320
335
 
321
- Published (`package.json` `files`): `bin/grunt.js` `cli` `scripts/check-globals.mjs` `scripts/emit-agent-shell-tools.mjs` `scripts/emit-gemini.mjs` `scripts/emit-mcp-policy.mjs` `scripts/gate-fat-tools.mjs` `scripts/hooks-union.mjs` `scripts/grunt-job.mjs` `scripts/parse-need.mjs` `scripts/persist-handoff.mjs` `scripts/persist-plan.mjs` `scripts/purge-global-mcps.mjs` `scripts/scrub-spawn-prompt.mjs` `scripts/scrub-text-lib.mjs` `scripts/sync-global-settings.mjs` `scripts/telemetry.mjs` `scripts/browser.mjs` `scripts/doctor.mjs` `scripts/scrub-text` `.rulesync` `.grok` `.codex` `.claude` `.agents` `AGENTS.md` `CLAUDE.md` `.mcp.json` `README.md` `LICENSE`
336
+ Published (`package.json` `files`): `bin/grunt.js` `cli` `scripts/check-globals.mjs` `scripts/emit-agent-shell-tools.mjs` `scripts/emit-gemini.mjs` `scripts/guarded-roots.mjs` `scripts/emit-mcp-policy.mjs` `scripts/gate-fat-tools.mjs` `scripts/hooks-union.mjs` `scripts/grunt-job.mjs` `scripts/parse-need.mjs` `scripts/persist-handoff.mjs` `scripts/persist-plan.mjs` `scripts/purge-global-mcps.mjs` `scripts/scrub-spawn-prompt.mjs` `scripts/scrub-text-lib.mjs` `scripts/sync-global-settings.mjs` `scripts/browser.mjs` `scripts/doctor.mjs` `scripts/scrub-text` `.rulesync` `.grok` `.codex` `.claude` `.agents` `AGENTS.md` `CLAUDE.md` `.mcp.json` `README.md` `LICENSE` `CHANGELOG.md`
337
+
338
+ No `scripts/*.test.ts` `scripts/fixtures/` `docs/` `coverage/` `vitest.config.ts` in `files`. `cli` dir ships whole (includes `cli/*.test.ts`).
322
339
 
323
- No `scripts/*.test.ts` `scripts/fixtures/` `docs/` `coverage/` `vitest.config.ts` in `files`.
340
+ Not packed: `GEMINI.md` `.gemini/` — `emit-gemini.mjs` writes them on `generate` / `init` (`GEMINI.md` → `@AGENTS.md`). Not in `files`.
324
341
 
325
- Repo root (also): `coverage/` `.gemini/` — no `src/` no `CONTRIBUTING` no `docs/`
342
+ Repo root (also): `coverage/` `.gemini/` `GEMINI.md` — no `src/` no `CONTRIBUTING` no `docs/`
326
343
 
327
344
  - `bin/grunt.js` — CLI bin
328
345
  - `cli/grunt.mjs` — commands
329
- - `scripts/` — generate emits `telemetry.mjs` (re-init sentinel) `scrub-text`
346
+ - `scripts/` — init copies product scripts (`grunt-job.mjs` `scrub-text` …). Re-init sentinels: `.grok/hooks/orchestrate-parent.js` `.rulesync` `<!-- grunt:begin -->`
330
347
  - `.rulesync/` — SoT (subagents skills reference)
331
348
  - `.claude/` `.grok/` `.codex/` `.agents/` `.gemini/` — host emit
332
349
  - `coverage/` — vitest local not published
package/cli/init.mjs CHANGED
@@ -21,7 +21,6 @@ const PRODUCT_SCRIPTS = [
21
21
  "scrub-spawn-prompt.mjs",
22
22
  "scrub-text-lib.mjs",
23
23
  "sync-global-settings.mjs",
24
- "telemetry.mjs",
25
24
  "browser.mjs",
26
25
  "doctor.mjs",
27
26
  "scrub-text",
@@ -74,14 +73,13 @@ function destHasGruntSentinel(dest) {
74
73
 
75
74
  export function destAlreadyInited(dest) {
76
75
  return (
77
- fs.existsSync(path.join(dest, "scripts", "telemetry.mjs")) ||
78
76
  fs.existsSync(path.join(dest, ".grok", "hooks", "orchestrate-parent.js")) ||
79
77
  fs.existsSync(path.join(dest, ".rulesync"))
80
78
  )
81
79
  }
82
80
 
83
81
  export function shouldAutoSkipGlobals(dest) {
84
- return destHasGruntSentinel(dest) || fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))
82
+ return destHasGruntSentinel(dest)
85
83
  }
86
84
 
87
85
  function detectNewline(text) {
package/cli/init.test.ts CHANGED
@@ -56,7 +56,6 @@ const PRODUCT_FILES = [
56
56
  "scrub-spawn-prompt.mjs",
57
57
  "scrub-text-lib.mjs",
58
58
  "sync-global-settings.mjs",
59
- "telemetry.mjs",
60
59
  "browser.mjs",
61
60
  "doctor.mjs",
62
61
  ];
@@ -1003,8 +1002,8 @@ describe("init", () => {
1003
1002
  expect(pkg.name).toBe("fixture-pkg");
1004
1003
  expect(fs.existsSync(path.join(pkgRoot, ".tmp"))).toBe(true);
1005
1004
  expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
1006
- expect(fs.readFileSync(path.join(pkgRoot, "scripts", "telemetry.mjs"), "utf8")).toBe(
1007
- "telemetry.mjs",
1005
+ expect(fs.readFileSync(path.join(pkgRoot, "scripts", "grunt-job.mjs"), "utf8")).toBe(
1006
+ "grunt-job.mjs",
1008
1007
  );
1009
1008
  // self-skip: markdown/settings stay exactly as-is, no sentinel wrap
1010
1009
  expect(fs.readFileSync(path.join(pkgRoot, "CLAUDE.md"), "utf8")).toBe("CLAUDE.md content");
@@ -1124,11 +1123,13 @@ describe("init", () => {
1124
1123
  ]);
1125
1124
  });
1126
1125
 
1127
- it("applyGlobals true still applies when telemetry auto-skip would fire", () => {
1126
+ it("applyGlobals true still applies when sentinel auto-skip would fire", () => {
1128
1127
  const pkgRoot = stubPkgRoot();
1129
1128
  const dest = tmp("grunt-force-globals-");
1130
- fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1131
- fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
1129
+ fs.writeFileSync(
1130
+ path.join(dest, "AGENTS.md"),
1131
+ "<!-- grunt:begin -->\nold\n<!-- grunt:end -->\n",
1132
+ );
1132
1133
  const exec = vi.fn();
1133
1134
  init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
1134
1135
  expect(exec.mock.calls.map((c) => c[1])).toEqual([
@@ -1139,20 +1140,6 @@ describe("init", () => {
1139
1140
  ]);
1140
1141
  });
1141
1142
 
1142
- it("auto-skip globals when telemetry.mjs exists", () => {
1143
- const pkgRoot = stubPkgRoot();
1144
- const dest = tmp("grunt-auto-tel-");
1145
- fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1146
- fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
1147
- const exec = vi.fn();
1148
- init(dest, { pkgRoot, execFileSync: exec });
1149
- expect(exec.mock.calls.map((c) => c[1])).toEqual([
1150
- ["install"],
1151
- ["run", "rulesync:generate"],
1152
- ["run", "rulesync:check"],
1153
- ]);
1154
- });
1155
-
1156
1143
  it("auto-skip globals when AGENTS.md already has a grunt sentinel", () => {
1157
1144
  const pkgRoot = stubPkgRoot();
1158
1145
  const dest = tmp("grunt-auto-sent-");
@@ -1174,8 +1161,7 @@ describe("init", () => {
1174
1161
  const dest = tmp("grunt-rerun-nosent-");
1175
1162
  fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
1176
1163
  fs.writeFileSync(path.join(dest, "AGENTS.md"), "hand-written consumer doc\n");
1177
- fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
1178
- fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
1164
+ fs.mkdirSync(path.join(dest, ".rulesync"), { recursive: true });
1179
1165
  fs.writeFileSync(path.join(dest, "CLAUDE.grunt.md"), "stale side");
1180
1166
  const exec = vi.fn();
1181
1167
  init(dest, { pkgRoot, execFileSync: exec });
@@ -1292,6 +1278,14 @@ describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
1292
1278
  expect(shouldAutoSkipGlobals(dest)).toBe(false);
1293
1279
  });
1294
1280
 
1281
+ it(".grok/hooks/orchestrate-parent.js means inited, not auto-skip", () => {
1282
+ const dest = tmp("inspect-hook-");
1283
+ fs.mkdirSync(path.join(dest, ".grok", "hooks"), { recursive: true });
1284
+ fs.writeFileSync(path.join(dest, ".grok", "hooks", "orchestrate-parent.js"), "");
1285
+ expect(destAlreadyInited(dest)).toBe(true);
1286
+ expect(shouldAutoSkipGlobals(dest)).toBe(false);
1287
+ });
1288
+
1295
1289
  it("sentinel auto-skips globals", () => {
1296
1290
  const dest = tmp("inspect-sent-");
1297
1291
  fs.writeFileSync(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrozagar/grunt",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
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",
@@ -39,7 +39,6 @@
39
39
  "scripts/scrub-spawn-prompt.mjs",
40
40
  "scripts/scrub-text-lib.mjs",
41
41
  "scripts/sync-global-settings.mjs",
42
- "scripts/telemetry.mjs",
43
42
  "scripts/browser.mjs",
44
43
  "scripts/doctor.mjs",
45
44
  "scripts/scrub-text",
@@ -52,7 +51,8 @@
52
51
  "CLAUDE.md",
53
52
  ".mcp.json",
54
53
  "README.md",
55
- "LICENSE"
54
+ "LICENSE",
55
+ "CHANGELOG.md"
56
56
  ],
57
57
  "type": "module",
58
58
  "publishConfig": {
@@ -4,7 +4,6 @@ import { spawnSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
- import { logTelemetry } from "./telemetry.mjs";
8
7
 
9
8
  export const FALLBACK = "FALLBACK";
10
9
  export const SHELL_META = /[|&;`$(){}<>\n\r]/;
@@ -443,14 +442,12 @@ function main() {
443
442
  try {
444
443
  const parsed = parseArgv(process.argv.slice(2));
445
444
  if (parsed.unknown) {
446
- logTelemetry("grunt-job", { fallback: true, query: parsed.query }, process.cwd());
447
445
  process.stdout.write(FALLBACK + "\n");
448
446
  return 2;
449
447
  }
450
448
  const ws = process.cwd();
451
449
  const cwd = parsed.cwd ? resolveJobCwd(parsed.cwd, ws) : ws;
452
450
  if (!cwd) {
453
- logTelemetry("grunt-job", { fallback: true, query: parsed.query }, ws);
454
451
  process.stdout.write(FALLBACK + "\n");
455
452
  return 2;
456
453
  }
@@ -461,15 +458,9 @@ function main() {
461
458
  path: parsed.path,
462
459
  glob: parsed.glob,
463
460
  });
464
- logTelemetry(
465
- "grunt-job",
466
- { job: parsed.job, fallback: Boolean(result.fallback), query: parsed.query },
467
- ws,
468
- );
469
461
  process.stdout.write(result.text);
470
462
  return result.code;
471
463
  } catch {
472
- logTelemetry("grunt-job", { fallback: true }, process.cwd());
473
464
  process.stdout.write(FALLBACK + "\n");
474
465
  return 2;
475
466
  }
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
- /** Append-only hook sidecar. Never throws. Never writes stdout. */
3
- import fs from "node:fs";
4
- import path from "node:path";
5
-
6
- const QUERY_MAX = 120;
7
-
8
- export const ORCHESTRATOR_LOGS_DIR = ".tmp/orchestrator-logs";
9
-
10
- export function telemetryPath(workspaceRoot) {
11
- const root =
12
- workspaceRoot ||
13
- process.env.GROK_WORKSPACE_ROOT ||
14
- process.cwd();
15
- return path.join(root, ORCHESTRATOR_LOGS_DIR, "telemetry.ndjson");
16
- }
17
-
18
- function truncateQuery(q) {
19
- const s = String(q ?? "");
20
- if (s.length <= QUERY_MAX) return s;
21
- return s.slice(0, QUERY_MAX);
22
- }
23
-
24
- export function logTelemetry(event, fields, workspaceRoot) {
25
- try {
26
- const rec = { ts: Date.now(), event: String(event || "") };
27
- const src = fields && typeof fields === "object" ? fields : {};
28
- for (const [k, v] of Object.entries(src)) {
29
- if (k === "query" && v != null) rec.query = truncateQuery(v);
30
- else rec[k] = v;
31
- }
32
- const dest = telemetryPath(workspaceRoot);
33
- fs.mkdirSync(path.dirname(dest), { recursive: true });
34
- fs.appendFileSync(dest, JSON.stringify(rec) + "\n");
35
- } catch {
36
- // fail-open
37
- }
38
- }