@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.
- package/.grok/hooks/orchestrate-parent.js +3 -100
- package/.rulesync/reference/hooks.md +4 -4
- package/.rulesync/reference/map.md +2 -3
- package/.rulesync/rules/CLAUDE.md +1 -1
- package/.rulesync/rules/overview.md +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +187 -0
- package/CLAUDE.md +1 -1
- package/README.md +27 -10
- package/cli/init.mjs +1 -3
- package/cli/init.test.ts +16 -22
- package/package.json +3 -3
- package/scripts/grunt-job.mjs +0 -9
- package/scripts/telemetry.mjs +0 -38
|
@@ -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
|
|
516
|
-
|
|
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
|
|
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
|
|
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
|
|
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/` (
|
|
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` +
|
|
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/` —
|
|
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
|
|
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
|
|
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.
|
|
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`
|
|
91
|
-
- First init (no sentinel
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
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/` —
|
|
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)
|
|
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", "
|
|
1007
|
-
"
|
|
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
|
|
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.
|
|
1131
|
-
|
|
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, "
|
|
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.
|
|
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": {
|
package/scripts/grunt-job.mjs
CHANGED
|
@@ -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
|
}
|
package/scripts/telemetry.mjs
DELETED
|
@@ -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
|
-
}
|