@lovrozagar/grunt 0.6.2 → 0.8.0
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/.agents/skills/su/SKILL.md +11 -0
- package/.claude/settings.json +11 -0
- package/.claude/skills/su/SKILL.md +11 -0
- package/.codex/hooks.json +11 -0
- package/.grok/hooks/orchestrate-parent.js +3 -8
- package/.grok/skills/su/SKILL.md +11 -0
- package/.rulesync/hooks.jsonc +8 -0
- package/.rulesync/reference/INDEX.md +3 -2
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/reference/law.md +1 -1
- package/.rulesync/reference/refs-map.md +1 -1
- package/.rulesync/reference/skills-map.md +1 -0
- package/.rulesync/rules/overview.md +2 -0
- package/.rulesync/skills/su/SKILL.md +11 -0
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +28 -0
- package/README.md +57 -23
- package/cli/grunt.mjs +99 -22
- package/cli/grunt.test.ts +168 -12
- package/cli/init.mjs +102 -18
- package/cli/init.test.ts +281 -2
- package/package.json +4 -1
- package/scripts/check-globals.mjs +13 -5
- package/scripts/doctor.mjs +62 -12
- package/scripts/folder-map.mjs +300 -0
- package/scripts/gate-fat-tools.mjs +9 -5
- package/scripts/hooks-union.mjs +15 -4
- package/scripts/package-manager.mjs +150 -0
- package/scripts/scrub-spawn-prompt.mjs +1 -1
- package/scripts/session-map.mjs +72 -0
package/scripts/doctor.mjs
CHANGED
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
findSkillContentConflicts,
|
|
12
12
|
formatSkillConflictWarn,
|
|
13
13
|
} from "./skill-conflicts.mjs";
|
|
14
|
+
import {
|
|
15
|
+
PACKAGE_MANAGERS,
|
|
16
|
+
detectPackageManager,
|
|
17
|
+
runScriptLine,
|
|
18
|
+
} from "./package-manager.mjs";
|
|
14
19
|
|
|
15
20
|
export const CHROMIUM_BINS = [
|
|
16
21
|
"chromium",
|
|
@@ -28,8 +33,7 @@ const RTK_CURL =
|
|
|
28
33
|
"curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh";
|
|
29
34
|
const RTK_DOCS = "https://www.rtk-ai.app/docs/getting-started/installation/";
|
|
30
35
|
const LP_INSTALL_SH = "curl -fsSL https://pkg.lightpanda.io/install.sh | bash";
|
|
31
|
-
const
|
|
32
|
-
const REQUIRED = ["node", "npm", "git", "rtk", "rulesync", "lightpanda", "chromium"];
|
|
36
|
+
const REQUIRED = ["node", "git", "rtk", "rulesync", "lightpanda", "chromium"];
|
|
33
37
|
export const REQUIRED_MAP_FILES = [
|
|
34
38
|
{ name: "INDEX.md", rel: ".rulesync/reference/INDEX.md" },
|
|
35
39
|
{ name: "skills-map.md", rel: ".rulesync/reference/skills-map.md" },
|
|
@@ -127,7 +131,7 @@ function lookupChromium(pathEnv, platform) {
|
|
|
127
131
|
|
|
128
132
|
export function installHints(id, platform = process.platform) {
|
|
129
133
|
if (id === "all") {
|
|
130
|
-
const keys = ["node", "
|
|
134
|
+
const keys = ["node", "package-manager", "git", "rtk", "rulesync", "lightpanda", "chromium"];
|
|
131
135
|
return keys.flatMap((k) => installHints(k, platform));
|
|
132
136
|
}
|
|
133
137
|
if (id === "node") {
|
|
@@ -139,6 +143,22 @@ export function installHints(id, platform = process.platform) {
|
|
|
139
143
|
if (id === "npm") {
|
|
140
144
|
return ["npm ships with Node ≥22", NODE_URL];
|
|
141
145
|
}
|
|
146
|
+
if (id === "yarn") {
|
|
147
|
+
return ["corepack enable", "npm i -g yarn", "https://yarnpkg.com/getting-started/install"];
|
|
148
|
+
}
|
|
149
|
+
if (id === "pnpm") {
|
|
150
|
+
return ["corepack enable", "npm i -g pnpm", "https://pnpm.io/installation"];
|
|
151
|
+
}
|
|
152
|
+
if (id === "bun") {
|
|
153
|
+
return ["curl -fsSL https://bun.sh/install | bash", "https://bun.sh"];
|
|
154
|
+
}
|
|
155
|
+
if (id === "package-manager") {
|
|
156
|
+
return [
|
|
157
|
+
"npm ships with Node ≥22",
|
|
158
|
+
"corepack enable (yarn / pnpm)",
|
|
159
|
+
"curl -fsSL https://bun.sh/install | bash",
|
|
160
|
+
];
|
|
161
|
+
}
|
|
142
162
|
if (id === "git") {
|
|
143
163
|
if (platform === "win32") return ["winget install Git.Git"];
|
|
144
164
|
if (platform === "darwin") return ["brew install git"];
|
|
@@ -151,7 +171,13 @@ export function installHints(id, platform = process.platform) {
|
|
|
151
171
|
return [RTK_CURL, "brew install rtk"];
|
|
152
172
|
}
|
|
153
173
|
if (id === "rulesync") {
|
|
154
|
-
return [
|
|
174
|
+
return [
|
|
175
|
+
"npm i -D rulesync",
|
|
176
|
+
"yarn add -D rulesync",
|
|
177
|
+
"pnpm add -D rulesync",
|
|
178
|
+
"bun add -D rulesync",
|
|
179
|
+
"npx",
|
|
180
|
+
];
|
|
155
181
|
}
|
|
156
182
|
if (id === "lightpanda") {
|
|
157
183
|
if (platform === "win32") {
|
|
@@ -296,7 +322,17 @@ export function runDoctor({
|
|
|
296
322
|
const major = nodeMajor(ver);
|
|
297
323
|
const nodeOk = Boolean(nodeBin) && major >= 22;
|
|
298
324
|
|
|
299
|
-
const
|
|
325
|
+
const pmBins = Object.fromEntries(
|
|
326
|
+
PACKAGE_MANAGERS.map((name) => [name, whichBin(name, pathEnv, platform)]),
|
|
327
|
+
);
|
|
328
|
+
const detectedPm = detectPackageManager({ cwd, env });
|
|
329
|
+
const wantedPm = detectedPm.manager
|
|
330
|
+
? [detectedPm.manager]
|
|
331
|
+
: detectedPm.candidates?.length
|
|
332
|
+
? detectedPm.candidates
|
|
333
|
+
: PACKAGE_MANAGERS;
|
|
334
|
+
const presentPm = wantedPm.filter((name) => pmBins[name]);
|
|
335
|
+
const pmOk = presentPm.length > 0;
|
|
300
336
|
const git = whichBin("git", pathEnv, platform);
|
|
301
337
|
const rtk = whichBin("rtk", nmPath, platform);
|
|
302
338
|
const rulesync = whichBin("rulesync", nmPath, platform);
|
|
@@ -310,7 +346,6 @@ export function runDoctor({
|
|
|
310
346
|
|
|
311
347
|
const found = {
|
|
312
348
|
node: nodeOk ? nodeBin : "",
|
|
313
|
-
npm,
|
|
314
349
|
git,
|
|
315
350
|
rtk,
|
|
316
351
|
rulesync,
|
|
@@ -326,7 +361,17 @@ export function runDoctor({
|
|
|
326
361
|
} else {
|
|
327
362
|
lines.push(row("node", "missing"));
|
|
328
363
|
}
|
|
329
|
-
|
|
364
|
+
if (pmOk) {
|
|
365
|
+
lines.push(
|
|
366
|
+
row(
|
|
367
|
+
"package-manager",
|
|
368
|
+
"ok",
|
|
369
|
+
presentPm.map((name) => `${name} (${pmBins[name]})`).join(", "),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
} else {
|
|
373
|
+
lines.push(row("package-manager", "missing", wantedPm.join(" | ")));
|
|
374
|
+
}
|
|
330
375
|
lines.push(git ? row("git", "ok", git) : row("git", "missing"));
|
|
331
376
|
lines.push(rtk ? row("rtk", "ok", rtk) : row("rtk", "missing"));
|
|
332
377
|
lines.push(rulesync ? row("rulesync", "ok", rulesync) : row("rulesync", "missing"));
|
|
@@ -348,15 +393,20 @@ export function runDoctor({
|
|
|
348
393
|
lines.push(speak.ok ? row("speak", "ok", speak.extra) : row("speak", "missing (optional)"));
|
|
349
394
|
|
|
350
395
|
const missingRequired = REQUIRED.filter((k) => (k === "node" ? !nodeOk : !found[k]));
|
|
396
|
+
if (!pmOk) missingRequired.push("package-manager");
|
|
351
397
|
if (missingRequired.length) {
|
|
352
398
|
lines.push("");
|
|
353
399
|
lines.push("install (print-only; not run):");
|
|
354
400
|
const seen = new Set();
|
|
355
401
|
for (const k of missingRequired) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
402
|
+
const hintIds =
|
|
403
|
+
k === "package-manager" && wantedPm.length === 1 ? wantedPm : [k];
|
|
404
|
+
for (const id of hintIds) {
|
|
405
|
+
for (const h of installHints(id, platform)) {
|
|
406
|
+
if (seen.has(h)) continue;
|
|
407
|
+
seen.add(h);
|
|
408
|
+
lines.push(h);
|
|
409
|
+
}
|
|
360
410
|
}
|
|
361
411
|
}
|
|
362
412
|
}
|
|
@@ -389,7 +439,7 @@ export function runDoctor({
|
|
|
389
439
|
}
|
|
390
440
|
|
|
391
441
|
lines.push("");
|
|
392
|
-
lines.push(
|
|
442
|
+
lines.push(`rulesync schema doctor: ${runScriptLine(detectedPm.manager, "rulesync:doctor")}`);
|
|
393
443
|
const stdout = `${lines.join("\n")}\n`;
|
|
394
444
|
return { code: missingRequired.length || missingMaps.length ? 1 : 0, stdout, stderr: "" };
|
|
395
445
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Code-only folder map for agents. Folders, never files.
|
|
4
|
+
*
|
|
5
|
+
* Source: git ls-files (tracked + untracked, gitignore applied). A folder is
|
|
6
|
+
* shown when it or a descendant holds a code file and no segment is skipped.
|
|
7
|
+
* Package roots (package.json, go.mod, Cargo.toml, pyproject.toml) carry
|
|
8
|
+
* `# name path`. Single-child chains collapse; plain leaf dirs fold at foldAt.
|
|
9
|
+
*
|
|
10
|
+
* node scripts/folder-map.mjs [dir]
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
import { stripJsonc } from "./emit-mcp-policy.mjs";
|
|
17
|
+
|
|
18
|
+
export const DEFAULTS = Object.freeze({
|
|
19
|
+
budget: 5000,
|
|
20
|
+
depthCap: 6,
|
|
21
|
+
foldAt: 12,
|
|
22
|
+
codeExt: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "go", "rs", "sql", "vue", "svelte", "astro"],
|
|
23
|
+
skip: [
|
|
24
|
+
"_gen", "gen", "generated", "dist", "build", "assets", "static", "public",
|
|
25
|
+
"fixtures", "__fixtures__", "__snapshots__", "__mocks__", "testdata",
|
|
26
|
+
"vendor", "migrations", "locales", "i18n",
|
|
27
|
+
],
|
|
28
|
+
include: [],
|
|
29
|
+
exclude: [],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const CONFIG_REL = ".rulesync/grunt.map.jsonc";
|
|
33
|
+
const MANIFESTS = ["package.json", "go.mod", "Cargo.toml", "pyproject.toml"];
|
|
34
|
+
|
|
35
|
+
/** DEFAULTS merged with known keys from CONFIG_REL; bad file → defaults + warn. */
|
|
36
|
+
export function loadMapConfig(root, warn = (m) => process.stderr.write(`${m}\n`)) {
|
|
37
|
+
const abs = path.join(root, CONFIG_REL);
|
|
38
|
+
if (!fs.existsSync(abs)) return { ...DEFAULTS };
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = JSON.parse(stripJsonc(fs.readFileSync(abs, "utf8")));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
warn(`grunt map: ignoring ${CONFIG_REL}: ${err.message}`);
|
|
44
|
+
return { ...DEFAULTS };
|
|
45
|
+
}
|
|
46
|
+
const out = { ...DEFAULTS };
|
|
47
|
+
for (const [k, v] of Object.entries(raw || {})) {
|
|
48
|
+
if (!(k in DEFAULTS)) continue;
|
|
49
|
+
const ok = Array.isArray(DEFAULTS[k])
|
|
50
|
+
? Array.isArray(v) && v.every((x) => typeof x === "string")
|
|
51
|
+
: Number.isFinite(v) && v > 0;
|
|
52
|
+
if (ok) out[k] = v;
|
|
53
|
+
else warn(`grunt map: ignoring ${CONFIG_REL} key ${k}`);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function git(args, cwd) {
|
|
59
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
60
|
+
return r.status === 0 ? r.stdout : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Repo top and cwd-relative prefix, or null outside git. */
|
|
64
|
+
export function repoRoot(cwd) {
|
|
65
|
+
const top = git(["rev-parse", "--show-toplevel"], cwd);
|
|
66
|
+
return top ? top.trim() : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Tracked + untracked-unignored files, repo-relative, posix. */
|
|
70
|
+
export function listFiles(root) {
|
|
71
|
+
const out = git(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], root);
|
|
72
|
+
if (out == null) return null;
|
|
73
|
+
return [...new Set(out.split("\0").filter(Boolean))];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `*` = one segment, `**` = any depth; matches the dir and everything under it. */
|
|
77
|
+
function globRe(glob) {
|
|
78
|
+
const body = String(glob)
|
|
79
|
+
.replace(/\/+$/, "")
|
|
80
|
+
.split("**")
|
|
81
|
+
.map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*"))
|
|
82
|
+
.join(".*");
|
|
83
|
+
return new RegExp(`^${body}(/|$)`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function packageName(root, dir, manifest) {
|
|
87
|
+
try {
|
|
88
|
+
const text = fs.readFileSync(path.join(root, dir, manifest), "utf8");
|
|
89
|
+
if (manifest === "package.json") return JSON.parse(text).name || "";
|
|
90
|
+
if (manifest === "go.mod") return (text.match(/^module\s+(\S+)/m) || [])[1] || "";
|
|
91
|
+
return (text.match(/^name\s*=\s*"([^"]+)"/m) || [])[1] || "";
|
|
92
|
+
} catch {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Raw tree from file paths. Node: { name, path, kids: Map, own, pkg }.
|
|
99
|
+
* `own` = holds a kept file directly. `pkg` is the package name ("" when
|
|
100
|
+
* unnamed) on package roots, else null.
|
|
101
|
+
*/
|
|
102
|
+
export function buildTree(root, files, opts = {}) {
|
|
103
|
+
const o = { ...DEFAULTS, ...opts };
|
|
104
|
+
const code = new RegExp(`\\.(${o.codeExt.join("|")})$`);
|
|
105
|
+
const skip = new Set(o.skip);
|
|
106
|
+
const include = o.include.map(globRe);
|
|
107
|
+
const exclude = o.exclude.map(globRe);
|
|
108
|
+
const manifests = new Map();
|
|
109
|
+
for (const f of files) {
|
|
110
|
+
const base = path.posix.basename(f);
|
|
111
|
+
if (MANIFESTS.includes(base) && !manifests.has(path.posix.dirname(f))) {
|
|
112
|
+
manifests.set(path.posix.dirname(f), base);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const top = { name: "", path: "", kids: new Map(), own: false, pkg: null };
|
|
116
|
+
for (const f of files) {
|
|
117
|
+
const dir = path.posix.dirname(f);
|
|
118
|
+
if (dir === ".") continue;
|
|
119
|
+
if (exclude.some((re) => re.test(dir))) continue;
|
|
120
|
+
const forced = include.some((re) => re.test(dir));
|
|
121
|
+
const segs = dir.split("/");
|
|
122
|
+
if (!forced) {
|
|
123
|
+
if (!code.test(f)) continue;
|
|
124
|
+
if (segs.some((s) => s.startsWith(".") || skip.has(s))) continue;
|
|
125
|
+
}
|
|
126
|
+
let node = top;
|
|
127
|
+
for (const s of segs) {
|
|
128
|
+
let next = node.kids.get(s);
|
|
129
|
+
if (!next) {
|
|
130
|
+
const p = node.path ? `${node.path}/${s}` : s;
|
|
131
|
+
const m = manifests.get(p);
|
|
132
|
+
next = { name: s, path: p, kids: new Map(), own: false, pkg: m ? packageName(root, p, m) : null };
|
|
133
|
+
node.kids.set(s, next);
|
|
134
|
+
}
|
|
135
|
+
node = next;
|
|
136
|
+
}
|
|
137
|
+
node.own = true;
|
|
138
|
+
}
|
|
139
|
+
return top;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Display tree: chains collapsed, leaf dirs folded.
|
|
144
|
+
* Row: { kind: "dir"|"fold", label, path, pkg, kids: Row[] }.
|
|
145
|
+
*/
|
|
146
|
+
export function displayRows(node, opts = {}) {
|
|
147
|
+
const o = { ...DEFAULTS, ...opts };
|
|
148
|
+
const names = [...node.kids.keys()].sort();
|
|
149
|
+
const leaves = names.filter((n) => {
|
|
150
|
+
const k = node.kids.get(n);
|
|
151
|
+
return k.kids.size === 0 && k.pkg == null;
|
|
152
|
+
});
|
|
153
|
+
const folded = leaves.length >= o.foldAt ? new Set(leaves) : new Set();
|
|
154
|
+
const rows = [];
|
|
155
|
+
if (folded.size) {
|
|
156
|
+
rows.push({ kind: "fold", label: leaves, path: node.path, pkg: null, kids: [] });
|
|
157
|
+
}
|
|
158
|
+
for (const n of names) {
|
|
159
|
+
if (folded.has(n)) continue;
|
|
160
|
+
let k = node.kids.get(n);
|
|
161
|
+
let label = n;
|
|
162
|
+
// Collapse pass-through dirs only; a dir with its own code stays a row.
|
|
163
|
+
while (k.kids.size === 1 && k.pkg == null && !k.own) {
|
|
164
|
+
const [only] = k.kids.values();
|
|
165
|
+
label = `${label}/${only.name}`;
|
|
166
|
+
k = only;
|
|
167
|
+
}
|
|
168
|
+
rows.push({ kind: "dir", label, path: k.path, pkg: k.pkg, kids: displayRows(k, o) });
|
|
169
|
+
}
|
|
170
|
+
return rows;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function rowText(row, depth, { more = false, base = "" } = {}) {
|
|
174
|
+
const pad = " ".repeat(depth);
|
|
175
|
+
if (row.kind === "fold") {
|
|
176
|
+
const shown = row.label.slice(0, 6).join(", ");
|
|
177
|
+
return `${pad}(${row.label.length} leaf dirs: ${shown}${row.label.length > 6 ? ", …" : ""})`;
|
|
178
|
+
}
|
|
179
|
+
const full = base ? `${base}/${row.path}` : row.path;
|
|
180
|
+
let line = `${pad}${row.label}/`;
|
|
181
|
+
if (row.pkg != null) line += row.pkg ? ` # ${row.pkg} ${full}` : ` # ${full}`;
|
|
182
|
+
if (more) line += " …";
|
|
183
|
+
return line;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function tokens(text) {
|
|
187
|
+
return Math.ceil(text.length / 4);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Pick rows under the budget, breadth-first by display level.
|
|
192
|
+
*
|
|
193
|
+
* Level 0 always shows. Each deeper level is one unit per top-level subtree:
|
|
194
|
+
* a subtree's rows at that level show together or not at all, so a parent's
|
|
195
|
+
* kids are never half-listed. Cost is exact in chars: a row with kids is
|
|
196
|
+
* charged its `…` marker until its kids land. When a level does
|
|
197
|
+
* not fit, cheapest subtrees take an equal share first; a subtree that misses
|
|
198
|
+
* its share stops expanding. Rows past depthCap (from the nearest package
|
|
199
|
+
* root) never expand.
|
|
200
|
+
*/
|
|
201
|
+
function selectRows(rows, o, base) {
|
|
202
|
+
const levels = [];
|
|
203
|
+
const visit = (list, depth, top, pkgDepth) => {
|
|
204
|
+
for (const row of list) {
|
|
205
|
+
const rel = row.pkg != null ? 0 : pkgDepth;
|
|
206
|
+
row.top = top ?? row;
|
|
207
|
+
row.capped = row.kids.length > 0 && rel >= o.depthCap;
|
|
208
|
+
row.shown = false;
|
|
209
|
+
(levels[depth] ||= []).push(row);
|
|
210
|
+
if (!row.capped) visit(row.kids, depth + 1, row.top, rel + 1);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
visit(rows, 0, null, 1);
|
|
214
|
+
const cost = (row, depth, more) => rowText(row, depth, { more, base }).length + 1;
|
|
215
|
+
const pointer = (row, depth) => cost(row, depth, true) - cost(row, depth, false);
|
|
216
|
+
const charge = (row, depth) => cost(row, depth, row.kids.length > 0);
|
|
217
|
+
|
|
218
|
+
let used = 0;
|
|
219
|
+
for (const row of levels[0] || []) {
|
|
220
|
+
row.shown = true;
|
|
221
|
+
used += charge(row, 0);
|
|
222
|
+
}
|
|
223
|
+
const limit = o.budget * 4;
|
|
224
|
+
let active = new Set(levels[0] || []);
|
|
225
|
+
for (let d = 1; d < levels.length && active.size; d++) {
|
|
226
|
+
const units = new Map();
|
|
227
|
+
for (const row of levels[d]) {
|
|
228
|
+
if (!active.has(row.top)) continue;
|
|
229
|
+
const u = units.get(row.top) || { rows: [], parents: new Set(), cost: 0 };
|
|
230
|
+
u.rows.push(row);
|
|
231
|
+
u.cost += charge(row, d);
|
|
232
|
+
units.set(row.top, u);
|
|
233
|
+
}
|
|
234
|
+
for (const row of levels[d - 1]) {
|
|
235
|
+
const u = units.get(row.top);
|
|
236
|
+
if (u && row.shown && row.kids.length && !row.capped) u.cost -= pointer(row, d - 1);
|
|
237
|
+
}
|
|
238
|
+
const next = new Set();
|
|
239
|
+
let left = limit - used;
|
|
240
|
+
const order = [...units.entries()].sort((a, b) => a[1].cost - b[1].cost);
|
|
241
|
+
order.forEach(([top, u], i) => {
|
|
242
|
+
const share = left / (order.length - i);
|
|
243
|
+
if (u.cost > share) return;
|
|
244
|
+
for (const row of u.rows) row.shown = true;
|
|
245
|
+
left -= u.cost;
|
|
246
|
+
used += u.cost;
|
|
247
|
+
next.add(top);
|
|
248
|
+
});
|
|
249
|
+
active = next;
|
|
250
|
+
}
|
|
251
|
+
const lines = [];
|
|
252
|
+
const walk = (list, depth) => {
|
|
253
|
+
for (const row of list) {
|
|
254
|
+
if (!row.shown) continue;
|
|
255
|
+
const open = row.kids.length > 0 && row.kids.some((k) => k.shown);
|
|
256
|
+
lines.push(rowText(row, depth, { more: row.kids.length > 0 && !open, base }));
|
|
257
|
+
if (open) walk(row.kids, depth + 1);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
walk(rows, 0);
|
|
261
|
+
return lines;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Map text for `root` (any dir inside a git repo), scoped to `dir` when set.
|
|
266
|
+
* Returns { text, rows, tokens } or null outside git.
|
|
267
|
+
*/
|
|
268
|
+
export function folderMap({ root, dir = "", ...opts } = {}) {
|
|
269
|
+
const top = repoRoot(root);
|
|
270
|
+
if (!top) return null;
|
|
271
|
+
const files = listFiles(top);
|
|
272
|
+
if (!files) return null;
|
|
273
|
+
const o = { ...loadMapConfig(top), ...opts };
|
|
274
|
+
const scope = dir ? path.relative(top, path.resolve(root, dir)).split(path.sep).join("/") : "";
|
|
275
|
+
const scoped = scope
|
|
276
|
+
? files.filter((f) => f.startsWith(`${scope}/`)).map((f) => f.slice(scope.length + 1))
|
|
277
|
+
: files;
|
|
278
|
+
const tree = buildTree(scope ? path.join(top, scope) : top, scoped, o);
|
|
279
|
+
const lines = selectRows(displayRows(tree, o), o, scope);
|
|
280
|
+
const text = lines.length ? `${lines.join("\n")}\n` : "";
|
|
281
|
+
return { text, rows: lines.length, tokens: tokens(text) };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Text for `grunt map [dir]` / `node scripts/folder-map.mjs [dir]` run from `cwd`. */
|
|
285
|
+
export function mapCommand(cwd, dir = "") {
|
|
286
|
+
const out = folderMap({ root: cwd, dir });
|
|
287
|
+
if (!out) return "grunt map: not a git repository\n";
|
|
288
|
+
return out.text || "grunt map: no code folders\n";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function main() {
|
|
292
|
+
process.stdout.write(mapCommand(process.cwd(), process.argv[2] || ""));
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
297
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
298
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
299
|
+
process.exit(main());
|
|
300
|
+
}
|
|
@@ -190,9 +190,10 @@ export function pathIsDenied(p) {
|
|
|
190
190
|
return n.split("/").some((seg) => DENY_PATH_SEGMENTS.has(seg));
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/** Top-level "block" is the only legacy deny Claude/Codex validate; allow = empty stdout. */
|
|
193
194
|
export function denyResponse(reason) {
|
|
194
195
|
return {
|
|
195
|
-
decision: "
|
|
196
|
+
decision: "block",
|
|
196
197
|
reason,
|
|
197
198
|
hookSpecificOutput: {
|
|
198
199
|
hookEventName: "PreToolUse",
|
|
@@ -341,8 +342,9 @@ export function rewriteScratchWrite(filePath, workspaceRoot) {
|
|
|
341
342
|
return null;
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
/** POSIX single-quote so `, $, and \ stay literal. */
|
|
346
|
+
export function quoteQuery(q) {
|
|
347
|
+
return `'${String(q).replace(/'/g, "'\\''")}'`;
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
export function gruntJobCommand(workspaceRoot, args) {
|
|
@@ -702,13 +704,15 @@ export function processFatTools(data) {
|
|
|
702
704
|
}
|
|
703
705
|
return { type: "deny", reason: REASON_DENYLIST };
|
|
704
706
|
}
|
|
705
|
-
|
|
707
|
+
// grunt-job and rtk take one plain command; compound shell passes through.
|
|
708
|
+
const compound = SHELL_META.test(cmd);
|
|
709
|
+
if (BASH_DUMP_CMDS.test(cmd) && !alreadyRtk(cmd) && !compound) {
|
|
706
710
|
const job = BASH_SEARCH.test(cmd) ? "search" : "exec";
|
|
707
711
|
const q = cmd.replace(/^\s*(rtk\s+)?/, "").trim();
|
|
708
712
|
next.command = gruntJobCommand(ws, `--job ${job} --query ${quoteQuery(q)}`);
|
|
709
713
|
return { type: "rewrite", updatedInput: next };
|
|
710
714
|
}
|
|
711
|
-
if (BASH_TREE.test(cmd) && !alreadyRtk(cmd)) {
|
|
715
|
+
if (BASH_TREE.test(cmd) && !alreadyRtk(cmd) && !compound) {
|
|
712
716
|
next.command = `rtk ${cmd.trim()}`;
|
|
713
717
|
return { type: "rewrite", updatedInput: next };
|
|
714
718
|
}
|
package/scripts/hooks-union.mjs
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Claude/Codex native event is UserPromptSubmit (mapped from SoT beforeSubmitPrompt).
|
|
7
7
|
* Antigravity has neither beforeSubmitPrompt nor subagentStop.
|
|
8
8
|
* Grok SessionStart stays empty on orchestrate-parent.json.
|
|
9
|
+
* Grunt SessionStart: session-map.mjs (folder map) when shipped, first in the group;
|
|
10
|
+
* Claude/Codex only (rulesync emits no Antigravity sessionStart).
|
|
9
11
|
*
|
|
10
12
|
* Consumer extras (if those scripts exist): SessionStart, check-behind,
|
|
11
13
|
* validate+sim. Path deny from consumer `.rulesync/permissions.json`.
|
|
@@ -30,6 +32,9 @@ export const SCRUB =
|
|
|
30
32
|
'node "${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/scripts/scrub-spawn-prompt.mjs"';
|
|
31
33
|
export const GATE_FAT =
|
|
32
34
|
'node "${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/scripts/gate-fat-tools.mjs"';
|
|
35
|
+
export const MAP =
|
|
36
|
+
'node "${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}/scripts/session-map.mjs"';
|
|
37
|
+
const MAP_NEEDLE = "session-map.mjs";
|
|
33
38
|
|
|
34
39
|
export const AUTORUN_NEEDLES = [
|
|
35
40
|
"daily-pull-check.mjs",
|
|
@@ -173,8 +178,14 @@ function desiredPre(root) {
|
|
|
173
178
|
return pre;
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
function
|
|
181
|
+
function mapNeedles(root) {
|
|
182
|
+
return hasScript(root, MAP_NEEDLE) ? [MAP_NEEDLE] : [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `map: false` for hosts rulesync emits no sessionStart for (Antigravity). */
|
|
186
|
+
function desiredSession(root, { map = true } = {}) {
|
|
177
187
|
const hooks = [];
|
|
188
|
+
if (map && hasScript(root, MAP_NEEDLE)) hooks.push(hookCmd(MAP, 5));
|
|
178
189
|
if (hasScript(root, "daily-pull-check.mjs")) hooks.push(hookCmd(PULL));
|
|
179
190
|
if (hasScript(root, "rtk-check.mjs")) hooks.push(hookCmd(RTK));
|
|
180
191
|
if (hasScript(root, "daily-fleet-check.mjs")) hooks.push(hookCmd(FLEET));
|
|
@@ -264,7 +275,7 @@ function applyAgents(root) {
|
|
|
264
275
|
const live = loadJson(abs);
|
|
265
276
|
const rulesync = isPlainObject(live.rulesync) ? { ...live.rulesync } : {};
|
|
266
277
|
rulesync.PreToolUse = upsertGroups(rulesync.PreToolUse, desiredPre(root));
|
|
267
|
-
const session = desiredSession(root);
|
|
278
|
+
const session = desiredSession(root, { map: false });
|
|
268
279
|
if (session) {
|
|
269
280
|
rulesync.SessionStart = [{ matcher: ".*", hooks: session[0].hooks }];
|
|
270
281
|
}
|
|
@@ -320,12 +331,12 @@ export function checkUnion(opts = {}) {
|
|
|
320
331
|
{
|
|
321
332
|
id: "claude",
|
|
322
333
|
path: ".claude/settings.json",
|
|
323
|
-
needles: [...GRUNT_NEEDLES, ...autorun, ...loadConsumerDeny(root)],
|
|
334
|
+
needles: [...GRUNT_NEEDLES, ...mapNeedles(root), ...autorun, ...loadConsumerDeny(root)],
|
|
324
335
|
},
|
|
325
336
|
{
|
|
326
337
|
id: "codex",
|
|
327
338
|
path: ".codex/hooks.json",
|
|
328
|
-
needles: [...GRUNT_NEEDLES, ...autorun],
|
|
339
|
+
needles: [...GRUNT_NEEDLES, ...mapNeedles(root), ...autorun],
|
|
329
340
|
},
|
|
330
341
|
{
|
|
331
342
|
id: "agents",
|