@lovrozagar/grunt 0.8.1 → 0.8.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.
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/rules/overview.md +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/scripts/folder-map.mjs +32 -70
- package/scripts/session-map.mjs +13 -5
|
@@ -4,7 +4,7 @@ tags: [hooks]
|
|
|
4
4
|
|
|
5
5
|
# Hooks
|
|
6
6
|
|
|
7
|
-
SessionStart carries only the folder map: Claude / Codex run `../../scripts/session-map.mjs`, which injects the code-only map from `../../scripts/folder-map.mjs` as `additionalContext` (
|
|
7
|
+
SessionStart carries only the folder map: Claude / Codex run `../../scripts/session-map.mjs`, which injects the code-only map from `../../scripts/folder-map.mjs` as `additionalContext` (capped under Claude Code's 10,000-char per-hook `additionalContext` limit, about 2.4k tokens, below the 5k `grunt map` budget; cache-stable, fail-open) and logs `{rows, tokens}` to `.tmp/grunt/sessions/<sid>/map.json`. Inject nothing else at SessionStart. Antigravity has no rulesync sessionStart. Do not register a SessionStart hook on `../../.grok/hooks/orchestrate-parent.json`.
|
|
8
8
|
|
|
9
9
|
Grok PostToolUse / SessionStart are observe-only — do not attempt output scrub; compress via RTK / spawn / isolation facts only. Do not add PostToolUse scrub hooks. Grok UserPromptSubmit may emit a one-line `sessionGate=auto|ask` plus session receipt (`hookSpecificOutput.additionalContext`). Skip host Stop banners. `/ask` exact-match slash stamps `session-gate-{sid}` to `ask`. `/auto` unlinks. Sid-less: no stamp. Default: `auto`.
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ Default `/auto`: keep going; ask on blockers. `/ask`: one step, then ask.
|
|
|
15
15
|
Ask before destructive git (stash, reset, checkout, restore, clean, rebase, push -f); others share the tree. In a loop, skip it and edit code instead.
|
|
16
16
|
|
|
17
17
|
Read `.rulesync/reference/INDEX.md` once. When a row matches the work, read that reference in full.
|
|
18
|
-
|
|
18
|
+
To learn the repo layout, use the folder map if it is in your context. If it is missing, or you need more depth, run `node scripts/folder-map.mjs [dir]` instead of exploring with `ls` or `find`.
|
|
19
19
|
|
|
20
20
|
Large dumps are compressed (search, test logs, snaps). Scratch and dumps live in `.tmp/grunt/` (gitignored). After a write, recap the path; edit with an offset slice when you need lines.
|
|
21
21
|
Fat dumps: `node scripts/grunt-job.mjs --job search|exec|slice|fetch|test` first; spawn grunt only when the dump needs judgment.
|
package/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ Default `/auto`: keep going; ask on blockers. `/ask`: one step, then ask.
|
|
|
6
6
|
Ask before destructive git (stash, reset, checkout, restore, clean, rebase, push -f); others share the tree. In a loop, skip it and edit code instead.
|
|
7
7
|
|
|
8
8
|
Read `.rulesync/reference/INDEX.md` once. When a row matches the work, read that reference in full.
|
|
9
|
-
|
|
9
|
+
To learn the repo layout, use the folder map if it is in your context. If it is missing, or you need more depth, run `node scripts/folder-map.mjs [dir]` instead of exploring with `ls` or `find`.
|
|
10
10
|
|
|
11
11
|
Large dumps are compressed (search, test logs, snaps). Scratch and dumps live in `.tmp/grunt/` (gitignored). After a write, recap the path; edit with an offset slice when you need lines.
|
|
12
12
|
Fat dumps: `node scripts/grunt-job.mjs --job search|exec|slice|fetch|test` first; spawn grunt only when the dump needs judgment.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.3] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Root rule on the folder map now reads for every agent: use the map when it is in context, else run `node scripts/folder-map.mjs [dir]`, and only in place of `ls` / `find` for learning the repo layout
|
|
15
|
+
|
|
16
|
+
## [0.8.2] - 2026-10-03
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- SessionStart folder map exceeded Claude Code's 10,000-character per-hook `additionalContext` limit on large repos, so only a 2KB preview reached the model. The injected map now stays under that limit
|
|
21
|
+
- Folder map depth is uniform: the deepest level (up to `depthCap`) that fits the size limit, applied to every package, instead of uneven fair-share cuts. Folders leading to a nested package always expand. The injected header names the depth used, or `complete`; the per-session `map.json` logs `depth`
|
|
22
|
+
|
|
10
23
|
## [0.8.1] - 2026-10-03
|
|
11
24
|
|
|
12
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -179,13 +179,13 @@ Emit writes other-CLI trees from `.rulesync` for the **next** process of that CL
|
|
|
179
179
|
|
|
180
180
|
### Folder map
|
|
181
181
|
|
|
182
|
-
Claude and Codex get a folder map at SessionStart (`scripts/session-map.mjs`), so agents skip `ls`/`find` loops. Agents run `node scripts/folder-map.mjs <dir>` (same as `grunt map <dir>`) for depth. Grok and Antigravity get the `AGENTS.md` line only.
|
|
182
|
+
Claude and Codex get a folder map at SessionStart (`scripts/session-map.mjs`), so agents skip `ls`/`find` loops. Agents run `node scripts/folder-map.mjs <dir>` (same as `grunt map <dir>`) for depth. Grok and Antigravity get the `AGENTS.md` line only. The injected map stays under Claude Code's 10,000-character per-hook `additionalContext` limit (about 2.4k tokens); past it, Claude keeps only a 2KB preview inline. `grunt map` itself uses the full `budget`.
|
|
183
183
|
|
|
184
184
|
- Folders only, never files. Source: `git ls-files --cached --others --exclude-standard`, so gitignored paths never show
|
|
185
185
|
- A folder shows when it or a descendant holds a code file (`codeExt`) and no path segment is a dot dir or in `skip`
|
|
186
186
|
- Package roots (`package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`) show `# name repo/relative/path`
|
|
187
187
|
- Pass-through single-child chains collapse (`a/b/c/`). 12+ plain leaf dirs under one parent fold into one row
|
|
188
|
-
-
|
|
188
|
+
- Depth is uniform: the map uses the deepest level (up to `depthCap`, counted from each package root) that fits the size limit, so every package shows the same number of levels. Deeper folders end in `…`; folders that lead to a nested package always expand. The size limit is `budget` × 4 chars, lowered to fit Claude Code's per-hook limit at SessionStart. The injected header names the depth used, or says `complete`
|
|
189
189
|
- Size per session: `.tmp/grunt/sessions/<sid>/map.json`
|
|
190
190
|
|
|
191
191
|
Optional `.rulesync/grunt.map.jsonc` (missing or invalid → defaults):
|
package/package.json
CHANGED
package/scripts/folder-map.mjs
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* shown when it or a descendant holds a code file and no segment is skipped.
|
|
7
7
|
* Package roots (package.json, go.mod, Cargo.toml, pyproject.toml) carry
|
|
8
8
|
* `# name path`. Single-child chains collapse; plain leaf dirs fold at foldAt.
|
|
9
|
+
* Depth is uniform per package: the deepest level that fits the size limit;
|
|
10
|
+
* deeper folders end in `…`.
|
|
9
11
|
*
|
|
10
12
|
* node scripts/folder-map.mjs [dir]
|
|
11
13
|
*/
|
|
@@ -197,83 +199,36 @@ export function tokens(text) {
|
|
|
197
199
|
return Math.ceil(text.length / 4);
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
/** True when a package root sits anywhere below `row`. */
|
|
203
|
+
function holdsPackage(row) {
|
|
204
|
+
return row.kids.some((k) => k.pkg != null || holdsPackage(k));
|
|
205
|
+
}
|
|
206
|
+
|
|
200
207
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* kids are never half-listed. Cost is exact in chars: a row with kids is
|
|
206
|
-
* charged its `…` marker until its kids land. When a level does
|
|
207
|
-
* not fit, cheapest subtrees take an equal share first; a subtree that misses
|
|
208
|
-
* its share stops expanding. Rows past depthCap (from the nearest package
|
|
209
|
-
* root) never expand.
|
|
208
|
+
* Render rows with every package expanded to `cap` levels below its root
|
|
209
|
+
* (top-level non-package rows count from 1). A row past the cap hides its
|
|
210
|
+
* kids and ends in `…`, unless a package root sits below it: package rows
|
|
211
|
+
* always stay reachable, and their own depth restarts at 0.
|
|
210
212
|
*/
|
|
211
|
-
function
|
|
212
|
-
const levels = [];
|
|
213
|
-
const visit = (list, depth, top, pkgDepth) => {
|
|
214
|
-
for (const row of list) {
|
|
215
|
-
const rel = row.pkg != null ? 0 : pkgDepth;
|
|
216
|
-
row.top = top ?? row;
|
|
217
|
-
row.capped = row.kids.length > 0 && rel >= o.depthCap;
|
|
218
|
-
row.shown = false;
|
|
219
|
-
(levels[depth] ||= []).push(row);
|
|
220
|
-
if (!row.capped) visit(row.kids, depth + 1, row.top, rel + 1);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
visit(rows, 0, null, 1);
|
|
224
|
-
const cost = (row, depth, more) => rowText(row, depth, { more, base }).length + 1;
|
|
225
|
-
const pointer = (row, depth) => cost(row, depth, true) - cost(row, depth, false);
|
|
226
|
-
const charge = (row, depth) => cost(row, depth, row.kids.length > 0);
|
|
227
|
-
|
|
228
|
-
let used = 0;
|
|
229
|
-
for (const row of levels[0] || []) {
|
|
230
|
-
row.shown = true;
|
|
231
|
-
used += charge(row, 0);
|
|
232
|
-
}
|
|
233
|
-
const limit = o.budget * 4;
|
|
234
|
-
let active = new Set(levels[0] || []);
|
|
235
|
-
for (let d = 1; d < levels.length && active.size; d++) {
|
|
236
|
-
const units = new Map();
|
|
237
|
-
for (const row of levels[d]) {
|
|
238
|
-
if (!active.has(row.top)) continue;
|
|
239
|
-
const u = units.get(row.top) || { rows: [], parents: new Set(), cost: 0 };
|
|
240
|
-
u.rows.push(row);
|
|
241
|
-
u.cost += charge(row, d);
|
|
242
|
-
units.set(row.top, u);
|
|
243
|
-
}
|
|
244
|
-
for (const row of levels[d - 1]) {
|
|
245
|
-
const u = units.get(row.top);
|
|
246
|
-
if (u && row.shown && row.kids.length && !row.capped) u.cost -= pointer(row, d - 1);
|
|
247
|
-
}
|
|
248
|
-
const next = new Set();
|
|
249
|
-
let left = limit - used;
|
|
250
|
-
const order = [...units.entries()].sort((a, b) => a[1].cost - b[1].cost);
|
|
251
|
-
order.forEach(([top, u], i) => {
|
|
252
|
-
const share = left / (order.length - i);
|
|
253
|
-
if (u.cost > share) return;
|
|
254
|
-
for (const row of u.rows) row.shown = true;
|
|
255
|
-
left -= u.cost;
|
|
256
|
-
used += u.cost;
|
|
257
|
-
next.add(top);
|
|
258
|
-
});
|
|
259
|
-
active = next;
|
|
260
|
-
}
|
|
213
|
+
function renderAtDepth(rows, cap, base) {
|
|
261
214
|
const lines = [];
|
|
262
|
-
const walk = (list, depth) => {
|
|
215
|
+
const walk = (list, depth, pkgDepth) => {
|
|
263
216
|
for (const row of list) {
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
lines.push(rowText(row, depth, { more:
|
|
267
|
-
if (
|
|
217
|
+
const rel = row.pkg != null ? 0 : pkgDepth;
|
|
218
|
+
const hidden = row.kids.length > 0 && rel >= cap && !holdsPackage(row);
|
|
219
|
+
lines.push(rowText(row, depth, { more: hidden, base }));
|
|
220
|
+
if (!hidden) walk(row.kids, depth + 1, rel + 1);
|
|
268
221
|
}
|
|
269
222
|
};
|
|
270
|
-
walk(rows, 0);
|
|
223
|
+
walk(rows, 0, 1);
|
|
271
224
|
return lines;
|
|
272
225
|
}
|
|
273
226
|
|
|
274
227
|
/**
|
|
275
228
|
* Map text for `root` (any dir inside a git repo), scoped to `dir` when set.
|
|
276
|
-
*
|
|
229
|
+
* Depth is uniform: the deepest cap (≤ depthCap) whose text fits the limit,
|
|
230
|
+
* where limit = budget × 4 chars, lowered by `opts.maxChars` (hook caps).
|
|
231
|
+
* Returns { text, rows, tokens, depth } or null outside git.
|
|
277
232
|
*/
|
|
278
233
|
export function folderMap({ root, dir = "", ...opts } = {}) {
|
|
279
234
|
const top = repoRoot(root);
|
|
@@ -282,14 +237,21 @@ export function folderMap({ root, dir = "", ...opts } = {}) {
|
|
|
282
237
|
if (!files) return null;
|
|
283
238
|
const o = { ...loadMapConfig(top), ...opts };
|
|
284
239
|
const scope = dir ? repoPrefix(path.resolve(root, dir)) : "";
|
|
285
|
-
if (scope == null) return { text: "", rows: 0, tokens: 0 };
|
|
240
|
+
if (scope == null) return { text: "", rows: 0, tokens: 0, depth: 0 };
|
|
286
241
|
const scoped = scope
|
|
287
242
|
? files.filter((f) => f.startsWith(`${scope}/`)).map((f) => f.slice(scope.length + 1))
|
|
288
243
|
: files;
|
|
289
|
-
const
|
|
290
|
-
const
|
|
244
|
+
const rows = displayRows(buildTree(scope ? path.join(top, scope) : top, scoped, o), o);
|
|
245
|
+
const limit = Math.min(o.budget * 4, o.maxChars ?? Infinity);
|
|
246
|
+
let lines = [];
|
|
247
|
+
let depth = o.depthCap;
|
|
248
|
+
for (; depth >= 1; depth--) {
|
|
249
|
+
lines = renderAtDepth(rows, depth, scope);
|
|
250
|
+
if (lines.join("\n").length + 1 <= limit) break;
|
|
251
|
+
}
|
|
252
|
+
depth = Math.max(depth, 1);
|
|
291
253
|
const text = lines.length ? `${lines.join("\n")}\n` : "";
|
|
292
|
-
return { text, rows: lines.length, tokens: tokens(text) };
|
|
254
|
+
return { text, rows: lines.length, tokens: tokens(text), depth };
|
|
293
255
|
}
|
|
294
256
|
|
|
295
257
|
/** Text for `grunt map [dir]` / `node scripts/folder-map.mjs [dir]` run from `cwd`. */
|
package/scripts/session-map.mjs
CHANGED
|
@@ -9,8 +9,15 @@ import path from "node:path";
|
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
import { folderMap } from "./folder-map.mjs";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/** First line of the injected map; `depth` = levels per package, null when nothing was cut. */
|
|
13
|
+
export function header(depth) {
|
|
14
|
+
const scope = depth == null ? "complete" : `${depth} levels per package`;
|
|
15
|
+
return `Folder map (code folders, ${scope}; \`…\` = deeper, run \`node scripts/folder-map.mjs <dir>\`):`;
|
|
16
|
+
}
|
|
17
|
+
/** Claude Code caps each hook's additionalContext at 10,000 chars; above it, only a 2KB preview stays inline. */
|
|
18
|
+
export const HOOK_CONTEXT_CHARS = 10000;
|
|
19
|
+
// Covers the header (depth is one or two digits) plus slack.
|
|
20
|
+
const RESERVE = header(99).length + 1 + 100;
|
|
14
21
|
|
|
15
22
|
function readStdin() {
|
|
16
23
|
try {
|
|
@@ -37,14 +44,15 @@ function logSize(root, data, out) {
|
|
|
37
44
|
if (!sid) return;
|
|
38
45
|
const dir = path.join(root, ".tmp", "grunt", "sessions", sid);
|
|
39
46
|
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
-
|
|
47
|
+
const size = { rows: out.rows, tokens: out.tokens, depth: out.depth };
|
|
48
|
+
fs.writeFileSync(path.join(dir, "map.json"), `${JSON.stringify(size)}\n`);
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
function main() {
|
|
44
52
|
try {
|
|
45
53
|
const data = readStdin();
|
|
46
54
|
const root = workspaceRootOf(data);
|
|
47
|
-
const out = folderMap({ root });
|
|
55
|
+
const out = folderMap({ root, maxChars: HOOK_CONTEXT_CHARS - RESERVE });
|
|
48
56
|
if (!out || !out.text) return 0;
|
|
49
57
|
try {
|
|
50
58
|
logSize(root, data, out);
|
|
@@ -55,7 +63,7 @@ function main() {
|
|
|
55
63
|
JSON.stringify({
|
|
56
64
|
hookSpecificOutput: {
|
|
57
65
|
hookEventName: "SessionStart",
|
|
58
|
-
additionalContext: `${
|
|
66
|
+
additionalContext: `${header(/ …$/m.test(out.text) ? out.depth : null)}\n${out.text}`,
|
|
59
67
|
},
|
|
60
68
|
}),
|
|
61
69
|
);
|