@lovrozagar/grunt 0.4.2 → 0.5.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/.agents/agents/grunt.md +2 -1
- package/.agents/agents/implementer.md +1 -1
- package/.agents/agents/orchestrator.md +19 -13
- package/.agents/agents/thinker.md +8 -6
- package/.agents/skills/ask/SKILL.md +18 -0
- package/.agents/skills/auto/SKILL.md +18 -0
- package/.agents/skills/browser/SKILL.md +4 -3
- package/.agents/skills/cascade/SKILL.md +5 -3
- package/.agents/skills/commit-and-push/SKILL.md +3 -3
- package/.agents/skills/explain/SKILL.md +1 -0
- package/.agents/skills/handoff/SKILL.md +1 -1
- package/.agents/skills/implement-plan/SKILL.md +117 -0
- package/.agents/skills/solo/SKILL.md +5 -3
- package/.agents/skills/tmp/SKILL.md +58 -0
- package/.agents/skills/write-plan/SKILL.md +85 -0
- package/.claude/agents/grunt.md +2 -1
- package/.claude/agents/implementer.md +1 -1
- package/.claude/agents/orchestrator.md +19 -13
- package/.claude/agents/thinker.md +8 -6
- package/.claude/skills/ask/SKILL.md +18 -0
- package/.claude/skills/auto/SKILL.md +18 -0
- package/.claude/skills/browser/SKILL.md +4 -3
- package/.claude/skills/cascade/SKILL.md +5 -3
- package/.claude/skills/commit-and-push/SKILL.md +3 -3
- package/.claude/skills/explain/SKILL.md +1 -0
- package/.claude/skills/handoff/SKILL.md +1 -1
- package/.claude/skills/implement-plan/SKILL.md +117 -0
- package/.claude/skills/solo/SKILL.md +5 -3
- package/.claude/skills/tmp/SKILL.md +58 -0
- package/.claude/skills/write-plan/SKILL.md +85 -0
- package/.codex/agents/grunt.toml +2 -1
- package/.codex/agents/implementer.toml +1 -1
- package/.codex/agents/orchestrator.toml +19 -13
- package/.codex/agents/thinker.toml +8 -6
- package/.grok/agents/grunt.md +2 -1
- package/.grok/agents/implementer.md +1 -1
- package/.grok/agents/orchestrator.md +19 -13
- package/.grok/agents/thinker.md +8 -6
- package/.grok/hooks/orchestrate-parent.js +280 -62
- package/.grok/skills/ask/SKILL.md +18 -0
- package/.grok/skills/auto/SKILL.md +18 -0
- package/.grok/skills/browser/SKILL.md +4 -3
- package/.grok/skills/cascade/SKILL.md +5 -3
- package/.grok/skills/commit-and-push/SKILL.md +3 -3
- package/.grok/skills/explain/SKILL.md +1 -0
- package/.grok/skills/handoff/SKILL.md +1 -1
- package/.grok/skills/implement-plan/SKILL.md +49 -20
- package/.grok/skills/solo/SKILL.md +5 -3
- package/.grok/skills/tmp/SKILL.md +58 -0
- package/.grok/skills/write-plan/SKILL.md +17 -14
- package/.rulesync/grunt.config.jsonc +7 -0
- package/.rulesync/reference/INDEX.md +46 -0
- package/.rulesync/reference/cascade.md +19 -15
- package/.rulesync/reference/hooks.md +6 -4
- package/.rulesync/reference/law.md +9 -0
- package/.rulesync/reference/map.md +20 -10
- package/.rulesync/reference/output.md +23 -8
- package/{.grok/skills/shared → .rulesync/reference}/plan-format.md +12 -11
- package/.rulesync/reference/refs-map.md +14 -0
- package/.rulesync/reference/skills-map.md +23 -0
- package/.rulesync/rules/CLAUDE.md +22 -15
- package/.rulesync/rules/overview.md +22 -15
- package/.rulesync/skills/ask/SKILL.md +18 -0
- package/.rulesync/skills/auto/SKILL.md +18 -0
- package/.rulesync/skills/browser/SKILL.md +2 -2
- package/.rulesync/skills/cascade/SKILL.md +5 -3
- package/.rulesync/skills/commit-and-push/SKILL.md +3 -3
- package/.rulesync/skills/explain/SKILL.md +1 -0
- package/.rulesync/skills/handoff/SKILL.md +1 -1
- package/.rulesync/skills/implement-plan/SKILL.md +127 -0
- package/.rulesync/skills/solo/SKILL.md +5 -3
- package/.rulesync/skills/tmp/SKILL.md +58 -0
- package/.rulesync/skills/write-plan/SKILL.md +92 -0
- package/.rulesync/subagents/grunt.md +2 -1
- package/.rulesync/subagents/implementer.md +1 -1
- package/.rulesync/subagents/orchestrator.md +19 -13
- package/.rulesync/subagents/thinker.md +8 -6
- package/AGENTS.md +22 -15
- package/CHANGELOG.md +56 -0
- package/CLAUDE.md +22 -15
- package/README.md +27 -22
- package/cli/grunt.mjs +12 -12
- package/cli/grunt.test.ts +29 -20
- package/cli/init.mjs +135 -22
- package/cli/init.test.ts +624 -36
- package/package.json +11 -4
- package/scripts/doctor.mjs +64 -2
- package/scripts/emit-maps.mjs +422 -0
- package/scripts/gate-fat-tools.mjs +2 -2
- package/scripts/grunt-config.mjs +50 -0
- package/scripts/guarded-roots.mjs +4 -7
- package/scripts/persist-plan.mjs +17 -2
- package/scripts/persist-tmp.mjs +123 -0
- package/scripts/pipeline.mjs +67 -0
- package/scripts/scrub-spawn-prompt.mjs +1 -1
- package/scripts/skill-conflicts.mjs +71 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -27,13 +27,17 @@
|
|
|
27
27
|
"scripts/check-globals.mjs",
|
|
28
28
|
"scripts/emit-agent-shell-tools.mjs",
|
|
29
29
|
"scripts/emit-gemini.mjs",
|
|
30
|
+
"scripts/emit-maps.mjs",
|
|
30
31
|
"scripts/guarded-roots.mjs",
|
|
31
32
|
"scripts/emit-mcp-policy.mjs",
|
|
32
33
|
"scripts/gate-fat-tools.mjs",
|
|
33
34
|
"scripts/hooks-union.mjs",
|
|
35
|
+
"scripts/pipeline.mjs",
|
|
34
36
|
"scripts/grunt-job.mjs",
|
|
37
|
+
"scripts/grunt-config.mjs",
|
|
35
38
|
"scripts/parse-need.mjs",
|
|
36
39
|
"scripts/persist-handoff.mjs",
|
|
40
|
+
"scripts/persist-tmp.mjs",
|
|
37
41
|
"scripts/persist-plan.mjs",
|
|
38
42
|
"scripts/purge-global-mcps.mjs",
|
|
39
43
|
"scripts/scrub-spawn-prompt.mjs",
|
|
@@ -41,6 +45,7 @@
|
|
|
41
45
|
"scripts/sync-global-settings.mjs",
|
|
42
46
|
"scripts/browser.mjs",
|
|
43
47
|
"scripts/doctor.mjs",
|
|
48
|
+
"scripts/skill-conflicts.mjs",
|
|
44
49
|
"scripts/scrub-text",
|
|
45
50
|
".rulesync",
|
|
46
51
|
".grok",
|
|
@@ -63,11 +68,8 @@
|
|
|
63
68
|
},
|
|
64
69
|
"scripts": {
|
|
65
70
|
"rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
|
|
66
|
-
"rulesync:generate:raw": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks && node scripts/emit-mcp-policy.mjs && node scripts/emit-gemini.mjs && node scripts/emit-agent-shell-tools.mjs && node scripts/hooks-union.mjs",
|
|
67
71
|
"rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
|
|
68
|
-
"rulesync:watch:raw": "node scripts/emit-mcp-policy.mjs && node scripts/emit-gemini.mjs && node scripts/emit-agent-shell-tools.mjs && rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
|
|
69
72
|
"rulesync:check": "node ./scripts/guarded-roots.mjs check",
|
|
70
|
-
"rulesync:check:raw": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,skills --check && rulesync generate -t codexcli,antigravity-cli,grokcli -f subagents --check && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks --check && node scripts/emit-mcp-policy.mjs --check && node scripts/emit-gemini.mjs --check && node scripts/emit-agent-shell-tools.mjs --check && node scripts/check-globals.mjs && node scripts/hooks-union.mjs --check",
|
|
71
73
|
"hooks:union": "node scripts/hooks-union.mjs",
|
|
72
74
|
"hooks:check": "node scripts/hooks-union.mjs --check",
|
|
73
75
|
"doctor": "node ./scripts/doctor.mjs",
|
|
@@ -77,6 +79,11 @@
|
|
|
77
79
|
"sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
|
|
78
80
|
"purge:global-mcps": "node scripts/purge-global-mcps.mjs",
|
|
79
81
|
"purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
|
|
82
|
+
"antigravity": "antigravity --dangerously-skip-permissions",
|
|
83
|
+
"claude": "claude --dangerously-skip-permissions",
|
|
84
|
+
"codex": "codex --dangerously-bypass-approvals-and-sandbox",
|
|
85
|
+
"gemini": "gemini --yolo",
|
|
86
|
+
"grok": "grok --yolo",
|
|
80
87
|
"test": "vitest run --coverage"
|
|
81
88
|
},
|
|
82
89
|
"devDependencies": {
|
package/scripts/doctor.mjs
CHANGED
|
@@ -4,6 +4,12 @@ import { execFileSync } 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 {
|
|
8
|
+
PACKAGED_SKILLS_REL,
|
|
9
|
+
WORKSPACE_SKILLS_REL,
|
|
10
|
+
findSkillContentConflicts,
|
|
11
|
+
formatSkillConflictWarn,
|
|
12
|
+
} from "./skill-conflicts.mjs";
|
|
7
13
|
|
|
8
14
|
export const CHROMIUM_BINS = [
|
|
9
15
|
"chromium",
|
|
@@ -23,6 +29,12 @@ const RTK_DOCS = "https://www.rtk-ai.app/docs/getting-started/installation/";
|
|
|
23
29
|
const LP_INSTALL_SH = "curl -fsSL https://pkg.lightpanda.io/install.sh | bash";
|
|
24
30
|
const RULESYNC_SCHEMA = "rulesync schema doctor: npm run rulesync:doctor";
|
|
25
31
|
const REQUIRED = ["node", "npm", "git", "rtk", "rulesync", "lightpanda", "chromium"];
|
|
32
|
+
export const REQUIRED_MAP_FILES = [
|
|
33
|
+
{ name: "INDEX.md", rel: ".rulesync/reference/INDEX.md" },
|
|
34
|
+
{ name: "skills-map.md", rel: ".rulesync/reference/skills-map.md" },
|
|
35
|
+
{ name: "refs-map.md", rel: ".rulesync/reference/refs-map.md" },
|
|
36
|
+
{ name: "law.md", rel: ".rulesync/reference/law.md" },
|
|
37
|
+
];
|
|
26
38
|
|
|
27
39
|
export function nodeMajor(version) {
|
|
28
40
|
const m = String(version || "").match(/(\d+)/);
|
|
@@ -158,12 +170,35 @@ export function installHints(id, platform = process.platform) {
|
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
function row(name, status, extra = "") {
|
|
161
|
-
const a = String(name).padEnd(
|
|
173
|
+
const a = String(name).padEnd(14);
|
|
162
174
|
const b = String(status).padEnd(18);
|
|
163
175
|
const c = extra ? String(extra).trim() : "";
|
|
164
176
|
return c ? `${a} ${b} ${c}` : `${a} ${b}`;
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
function mapFilePresent(cwd, rel) {
|
|
180
|
+
try {
|
|
181
|
+
return fs.statSync(path.join(cwd, rel)).isFile();
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isDir(abs) {
|
|
188
|
+
try {
|
|
189
|
+
return fs.statSync(abs).isDirectory();
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Product SoT: reference tree or skills (emit-maps in play). No .rulesync / no those trees → bins-only. */
|
|
196
|
+
export function mapsRequired(cwd) {
|
|
197
|
+
const rs = path.join(cwd || "", ".rulesync");
|
|
198
|
+
if (!isDir(rs)) return false;
|
|
199
|
+
return isDir(path.join(rs, "reference")) || isDir(path.join(rs, "skills"));
|
|
200
|
+
}
|
|
201
|
+
|
|
167
202
|
export function runDoctor({
|
|
168
203
|
cwd = process.cwd(),
|
|
169
204
|
pathEnv = process.env.PATH,
|
|
@@ -232,10 +267,37 @@ export function runDoctor({
|
|
|
232
267
|
}
|
|
233
268
|
}
|
|
234
269
|
|
|
270
|
+
const missingMaps = [];
|
|
271
|
+
if (mapsRequired(cwd)) {
|
|
272
|
+
for (const f of REQUIRED_MAP_FILES) {
|
|
273
|
+
if (mapFilePresent(cwd, f.rel)) {
|
|
274
|
+
lines.push(row(f.name, "ok", f.rel));
|
|
275
|
+
} else {
|
|
276
|
+
missingMaps.push(f);
|
|
277
|
+
lines.push(row(f.name, "missing", f.rel));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (missingMaps.length) {
|
|
281
|
+
lines.push(`maps missing: ${missingMaps.map((f) => f.name).join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const skillConflicts = findSkillContentConflicts({
|
|
286
|
+
workspaceSkillsDir: path.join(cwd || "", WORKSPACE_SKILLS_REL),
|
|
287
|
+
packagedSkillsDir: path.join(cwd || "", PACKAGED_SKILLS_REL),
|
|
288
|
+
});
|
|
289
|
+
if (skillConflicts.length) {
|
|
290
|
+
lines.push("");
|
|
291
|
+
lines.push("skill conflicts (warn; re-init overwrites grunt-owned names):");
|
|
292
|
+
for (const c of skillConflicts) {
|
|
293
|
+
lines.push(formatSkillConflictWarn(c.name));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
235
297
|
lines.push("");
|
|
236
298
|
lines.push(RULESYNC_SCHEMA);
|
|
237
299
|
const stdout = `${lines.join("\n")}\n`;
|
|
238
|
-
return { code: missingRequired.length ? 1 : 0, stdout, stderr: "" };
|
|
300
|
+
return { code: missingRequired.length || missingMaps.length ? 1 : 0, stdout, stderr: "" };
|
|
239
301
|
}
|
|
240
302
|
|
|
241
303
|
function main() {
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Emit INDEX (aggregate) + skills-map/refs-map slices from one skills+refs scan; law.md is a hand slice. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { splitFrontmatter } from "./emit-agent-shell-tools.mjs";
|
|
7
|
+
|
|
8
|
+
export const INDEX_REL = ".rulesync/reference/INDEX.md";
|
|
9
|
+
export const SKILLS_MAP_REL = ".rulesync/reference/skills-map.md";
|
|
10
|
+
export const REFS_MAP_REL = ".rulesync/reference/refs-map.md";
|
|
11
|
+
export const LAW_REL = ".rulesync/reference/law.md";
|
|
12
|
+
const SKILLS_REL = ".rulesync/skills";
|
|
13
|
+
const REFS_REL = ".rulesync/reference";
|
|
14
|
+
const PACKAGED_SKILLS_REL = path.join("node_modules", "@lovrozagar", "grunt", ".rulesync", "skills");
|
|
15
|
+
const BANNER = "<!-- Generated by emit-maps.mjs. Do not hand-edit. -->\n";
|
|
16
|
+
const DESC_MAX = 120;
|
|
17
|
+
const HUGE_BYTES = 512 * 1024;
|
|
18
|
+
const SKIP_REF_NAMES = new Set(["skills-map.md", "refs-map.md", "INDEX.md"]);
|
|
19
|
+
|
|
20
|
+
export function parseArgv(argv) {
|
|
21
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
22
|
+
let check = false;
|
|
23
|
+
for (const a of args) {
|
|
24
|
+
if (a === "--check") {
|
|
25
|
+
check = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
return { ok: false, error: `unknown flag: ${a}` };
|
|
29
|
+
}
|
|
30
|
+
return { ok: true, check };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function unquoteYaml(raw) {
|
|
34
|
+
const s = String(raw).trim();
|
|
35
|
+
if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(s);
|
|
38
|
+
} catch {
|
|
39
|
+
return s.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (s.length >= 2 && s[0] === "'" && s[s.length - 1] === "'") {
|
|
43
|
+
return s.slice(1, -1).replace(/''/g, "'");
|
|
44
|
+
}
|
|
45
|
+
return s;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseFlowSeq(s) {
|
|
49
|
+
const t = String(s).trim();
|
|
50
|
+
if (!t.startsWith("[") || !t.endsWith("]")) return null;
|
|
51
|
+
const inner = t.slice(1, -1).trim();
|
|
52
|
+
if (!inner) return [];
|
|
53
|
+
return inner.split(",").map((x) => unquoteYaml(x.trim())).filter((x) => x !== "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function indentOf(line) {
|
|
57
|
+
const m = String(line).match(/^[ \t]*/);
|
|
58
|
+
return m ? m[0].length : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseYamlDoc(fm) {
|
|
62
|
+
const lines = String(fm).split(/\r?\n/);
|
|
63
|
+
let i = 0;
|
|
64
|
+
function parseBlock(baseIndent) {
|
|
65
|
+
const obj = {};
|
|
66
|
+
while (i < lines.length) {
|
|
67
|
+
const line = lines[i];
|
|
68
|
+
if (!line.trim() || /^\s*#/.test(line)) {
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const ind = indentOf(line);
|
|
73
|
+
if (ind < baseIndent) break;
|
|
74
|
+
if (baseIndent > 0 && ind > baseIndent) break;
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
const km = trimmed.match(/^([^:#\s][^:]*):\s*(.*?)\s*$/);
|
|
77
|
+
if (!km) {
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const key = km[1].trim();
|
|
82
|
+
const val = km[2];
|
|
83
|
+
i += 1;
|
|
84
|
+
if (val === ">" || val === "|") {
|
|
85
|
+
const parts = [];
|
|
86
|
+
while (i < lines.length) {
|
|
87
|
+
const n = lines[i];
|
|
88
|
+
if (!n.trim()) {
|
|
89
|
+
parts.push("");
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (indentOf(n) <= ind) break;
|
|
94
|
+
parts.push(n.trim());
|
|
95
|
+
i += 1;
|
|
96
|
+
}
|
|
97
|
+
obj[key] = parts.filter(Boolean).join(" ");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (val === "") {
|
|
101
|
+
if (i < lines.length && indentOf(lines[i]) > ind) {
|
|
102
|
+
const next = lines[i].trim();
|
|
103
|
+
if (next.startsWith("- ")) {
|
|
104
|
+
const arr = [];
|
|
105
|
+
while (i < lines.length && indentOf(lines[i]) > ind && lines[i].trim().startsWith("- ")) {
|
|
106
|
+
arr.push(unquoteYaml(lines[i].trim().replace(/^- /, "")));
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
obj[key] = arr;
|
|
110
|
+
} else {
|
|
111
|
+
obj[key] = parseBlock(indentOf(lines[i]));
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
obj[key] = {};
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const seq = parseFlowSeq(val);
|
|
119
|
+
obj[key] = seq != null ? seq : unquoteYaml(val);
|
|
120
|
+
}
|
|
121
|
+
return obj;
|
|
122
|
+
}
|
|
123
|
+
return parseBlock(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function oneLine(s, max = DESC_MAX) {
|
|
127
|
+
const t = String(s ?? "").replace(/\s+/g, " ").trim();
|
|
128
|
+
if (t.length <= max) return t;
|
|
129
|
+
return `${t.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function cell(s) {
|
|
133
|
+
return oneLine(s).replace(/\|/g, "\\|");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readText(abs) {
|
|
137
|
+
try {
|
|
138
|
+
return fs.readFileSync(abs, "utf8");
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err && err.code === "ENOENT") return null;
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function lstat(abs) {
|
|
146
|
+
try {
|
|
147
|
+
return fs.lstatSync(abs);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err && err.code === "ENOENT") return null;
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function posixRel(...parts) {
|
|
155
|
+
return parts.join("/").replace(/\\/g, "/");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mapRefs(parsed) {
|
|
159
|
+
if (Array.isArray(parsed["map.refs"])) return parsed["map.refs"].map(String);
|
|
160
|
+
if (typeof parsed["map.refs"] === "string" && parsed["map.refs"]) return [parsed["map.refs"]];
|
|
161
|
+
const m = parsed.map;
|
|
162
|
+
if (m && Array.isArray(m.refs)) return m.refs.map(String);
|
|
163
|
+
if (m && typeof m.refs === "string" && m.refs) return [m.refs];
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function scalarTask(v) {
|
|
168
|
+
return typeof v === "string" && v !== "" ? v : "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function mapTask(parsed) {
|
|
172
|
+
const dotted = scalarTask(parsed["map.task"]);
|
|
173
|
+
if (dotted) return dotted;
|
|
174
|
+
const m = parsed.map;
|
|
175
|
+
if (m && typeof m === "object" && !Array.isArray(m)) return scalarTask(m.task);
|
|
176
|
+
return "";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function tagsOf(parsed) {
|
|
180
|
+
if (Array.isArray(parsed.tags)) return parsed.tags.map(String);
|
|
181
|
+
if (typeof parsed.tags === "string" && parsed.tags) {
|
|
182
|
+
const seq = parseFlowSeq(parsed.tags);
|
|
183
|
+
if (seq) return seq.map(String);
|
|
184
|
+
return [parsed.tags];
|
|
185
|
+
}
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function firstH1(body) {
|
|
190
|
+
const m = String(body).match(/^#\s+(.+?)\s*$/m);
|
|
191
|
+
return m ? m[1].replace(/\s+#+\s*$/, "").trim() : "";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function firstSummary(body) {
|
|
195
|
+
const lines = String(body).split(/\r?\n/);
|
|
196
|
+
let pastH1 = false;
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const t = line.trim();
|
|
199
|
+
if (!pastH1) {
|
|
200
|
+
if (/^#\s+/.test(t)) pastH1 = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!t) continue;
|
|
204
|
+
if (/^#/.test(t) || /^```/.test(t) || /^<!--/.test(t) || /^---/.test(t)) continue;
|
|
205
|
+
return oneLine(t);
|
|
206
|
+
}
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseMarkdown(raw) {
|
|
211
|
+
const src = String(raw ?? "");
|
|
212
|
+
const split = splitFrontmatter(src);
|
|
213
|
+
if (!split.ok) return { parsed: {}, body: src };
|
|
214
|
+
return { parsed: parseYamlDoc(split.fm), body: split.body };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function listSkillDirs(ws) {
|
|
218
|
+
const root = path.join(ws, SKILLS_REL);
|
|
219
|
+
let names;
|
|
220
|
+
try {
|
|
221
|
+
names = fs.readdirSync(root, { withFileTypes: true });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err && err.code === "ENOENT") return [];
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const ent of names) {
|
|
228
|
+
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
229
|
+
const skillAbs = path.join(root, ent.name, "SKILL.md");
|
|
230
|
+
const st = lstat(skillAbs);
|
|
231
|
+
if (st && st.isFile()) out.push({ name: ent.name, abs: skillAbs });
|
|
232
|
+
}
|
|
233
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function walkRefMd(dir, relBase, acc) {
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (err && err.code === "ENOENT") return acc;
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
for (const ent of entries) {
|
|
246
|
+
if (ent.name.startsWith(".")) continue;
|
|
247
|
+
const abs = path.join(dir, ent.name);
|
|
248
|
+
const rel = relBase ? posixRel(relBase, ent.name) : posixRel(REFS_REL, ent.name);
|
|
249
|
+
if (ent.isDirectory()) {
|
|
250
|
+
walkRefMd(abs, rel, acc);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
254
|
+
if (SKIP_REF_NAMES.has(ent.name)) continue;
|
|
255
|
+
acc.push({ abs, rel });
|
|
256
|
+
}
|
|
257
|
+
return acc;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function packagedSkillExists(ws, name) {
|
|
261
|
+
const abs = path.join(ws, PACKAGED_SKILLS_REL, name, "SKILL.md");
|
|
262
|
+
const st = lstat(abs);
|
|
263
|
+
return Boolean(st && st.isFile());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function skillFields(ws, skill) {
|
|
267
|
+
const raw = readText(skill.abs);
|
|
268
|
+
if (raw == null) return null;
|
|
269
|
+
const { parsed, body } = parseMarkdown(raw);
|
|
270
|
+
const name = parsed.name != null && parsed.name !== "" ? String(parsed.name) : skill.name;
|
|
271
|
+
const descSrc =
|
|
272
|
+
parsed.description != null && parsed.description !== "" ? parsed.description : firstSummary(body);
|
|
273
|
+
const commandPath = parsed.commandPath != null ? String(parsed.commandPath) : "";
|
|
274
|
+
const refs = mapRefs(parsed).join(", ");
|
|
275
|
+
const task = mapTask(parsed);
|
|
276
|
+
const origin = packagedSkillExists(ws, skill.name) ? "grunt" : "local";
|
|
277
|
+
return { name, origin, descSrc, commandPath, refs, task };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function skillRowFromFields(f) {
|
|
281
|
+
return `| ${cell(f.name)} | ${cell(f.origin)} | ${cell(f.descSrc)} | ${cell(f.task)} | ${cell(f.commandPath)} | ${cell(f.refs)} |`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function refRow(file) {
|
|
285
|
+
const st = lstat(file.abs);
|
|
286
|
+
if (!st || !st.isFile()) return null;
|
|
287
|
+
const fallbackTitle = path.basename(file.rel, ".md");
|
|
288
|
+
if (st.size > HUGE_BYTES) {
|
|
289
|
+
return `| ${cell(file.rel)} | ${cell(fallbackTitle)} | | |`;
|
|
290
|
+
}
|
|
291
|
+
const raw = readText(file.abs);
|
|
292
|
+
if (raw == null || raw.includes("\0")) {
|
|
293
|
+
return `| ${cell(file.rel)} | ${cell(fallbackTitle)} | | |`;
|
|
294
|
+
}
|
|
295
|
+
const { parsed, body } = parseMarkdown(raw);
|
|
296
|
+
const title = firstH1(body) || fallbackTitle;
|
|
297
|
+
const tags = tagsOf(parsed).join(", ");
|
|
298
|
+
const summary = firstSummary(body);
|
|
299
|
+
return `| ${cell(file.rel)} | ${cell(title)} | ${cell(tags)} | ${cell(summary)} |`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function skillsTable(rows) {
|
|
303
|
+
return [
|
|
304
|
+
"| name | origin | description | task | commandPath | refs |",
|
|
305
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
306
|
+
...rows,
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function refsTable(rows) {
|
|
311
|
+
return [
|
|
312
|
+
"| path | title | tags | summary |",
|
|
313
|
+
"| --- | --- | --- | --- |",
|
|
314
|
+
...rows,
|
|
315
|
+
].join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function renderSkillsMap(rows) {
|
|
319
|
+
return `${BANNER}\n# Skills map\n\n${skillsTable(rows)}\n`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderRefsMap(rows) {
|
|
323
|
+
return `${BANNER}\n# Refs map\n\n${refsTable(rows)}\n`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readLawBody(ws) {
|
|
327
|
+
const abs = path.join(ws, LAW_REL);
|
|
328
|
+
const st = lstat(abs);
|
|
329
|
+
if (!st || !st.isFile()) {
|
|
330
|
+
return { ok: false, error: `missing: ${LAW_REL}` };
|
|
331
|
+
}
|
|
332
|
+
const raw = readText(abs);
|
|
333
|
+
if (raw == null) {
|
|
334
|
+
return { ok: false, error: `missing: ${LAW_REL}` };
|
|
335
|
+
}
|
|
336
|
+
const { body } = parseMarkdown(raw);
|
|
337
|
+
return { ok: true, body: String(body ?? "").replace(/^\uFEFF/, "").replace(/\s+$/, "") };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function renderIndex(lawBody, skillRows, refRows) {
|
|
341
|
+
return `${BANNER}\n${lawBody}\n\n# Skills\n\n${skillsTable(skillRows)}\n\n# Refs\n\n${refsTable(refRows)}\n`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function desiredFiles(ws) {
|
|
345
|
+
const law = readLawBody(ws);
|
|
346
|
+
if (!law.ok) return law;
|
|
347
|
+
const fields = listSkillDirs(ws).map((s) => skillFields(ws, s)).filter(Boolean);
|
|
348
|
+
const skills = fields.map((f) => skillRowFromFields(f));
|
|
349
|
+
const refs = walkRefMd(path.join(ws, REFS_REL), "", [])
|
|
350
|
+
.sort((a, b) => a.rel.localeCompare(b.rel))
|
|
351
|
+
.map(refRow)
|
|
352
|
+
.filter(Boolean);
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
files: [
|
|
356
|
+
{ rel: INDEX_REL, text: renderIndex(law.body, skills, refs) },
|
|
357
|
+
{ rel: SKILLS_MAP_REL, text: renderSkillsMap(skills) },
|
|
358
|
+
{ rel: REFS_MAP_REL, text: renderRefsMap(refs) },
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function emitMaps({ workspaceRoot, check = false } = {}) {
|
|
364
|
+
const ws = workspaceRoot || process.cwd();
|
|
365
|
+
const rulesync = path.join(ws, ".rulesync");
|
|
366
|
+
const st = lstat(rulesync);
|
|
367
|
+
if (!st || !st.isDirectory()) {
|
|
368
|
+
return { ok: true, check, skipped: true };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const desired = desiredFiles(ws);
|
|
372
|
+
if (!desired.ok) {
|
|
373
|
+
return { ok: false, check, error: desired.error };
|
|
374
|
+
}
|
|
375
|
+
const files = desired.files;
|
|
376
|
+
if (check) {
|
|
377
|
+
const drift = [];
|
|
378
|
+
for (const f of files) {
|
|
379
|
+
const disk = readText(path.join(ws, f.rel));
|
|
380
|
+
if (disk !== f.text) drift.push(f.rel);
|
|
381
|
+
}
|
|
382
|
+
if (drift.length) {
|
|
383
|
+
return { ok: false, check: true, error: `drift: ${drift.join(", ")}`, drift };
|
|
384
|
+
}
|
|
385
|
+
return { ok: true, check: true, drift: [] };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (const f of files) {
|
|
389
|
+
const abs = path.join(ws, f.rel);
|
|
390
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
391
|
+
fs.writeFileSync(abs, f.text);
|
|
392
|
+
}
|
|
393
|
+
return { ok: true, check: false };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function main() {
|
|
397
|
+
try {
|
|
398
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
399
|
+
if (!parsed.ok) {
|
|
400
|
+
process.stderr.write((parsed.error || "invalid argv") + "\n");
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
const result = emitMaps({
|
|
404
|
+
workspaceRoot: process.cwd(),
|
|
405
|
+
check: parsed.check,
|
|
406
|
+
});
|
|
407
|
+
if (!result.ok) {
|
|
408
|
+
process.stderr.write((result.error || "emit failed") + "\n");
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
return 0;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
process.stderr.write(String(err && err.message ? err.message : err) + "\n");
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
419
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
420
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
421
|
+
process.exit(main());
|
|
422
|
+
}
|
|
@@ -439,7 +439,7 @@ function resolveWriteAbs(filePath, workspaceRoot) {
|
|
|
439
439
|
|
|
440
440
|
function isUnderPlansDir(abs, workspaceRoot) {
|
|
441
441
|
if (!abs || !workspaceRoot) return false;
|
|
442
|
-
const dir = path.resolve(workspaceRoot, ".tmp", "plans");
|
|
442
|
+
const dir = path.resolve(workspaceRoot, ".tmp", "grunt", "plans");
|
|
443
443
|
if (abs === dir) return true;
|
|
444
444
|
const rel = path.relative(dir, abs);
|
|
445
445
|
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
@@ -487,7 +487,7 @@ function inProgressStatus(text) {
|
|
|
487
487
|
}
|
|
488
488
|
|
|
489
489
|
function loadInProgressPlans(workspaceRoot) {
|
|
490
|
-
const dir = path.resolve(workspaceRoot, ".tmp", "plans");
|
|
490
|
+
const dir = path.resolve(workspaceRoot, ".tmp", "grunt", "plans");
|
|
491
491
|
let names;
|
|
492
492
|
try {
|
|
493
493
|
names = fs.readdirSync(dir);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Load leftoverGate + spawnMode from `.rulesync/grunt.config.jsonc`. leftover fail-closed `"ask"`; spawn fail-closed `"cascade"`. Keys independent. */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const CONFIG_REL = ".rulesync/grunt.config.jsonc";
|
|
6
|
+
export const LEFTOVER_GATES = new Set(["auto", "ask"]);
|
|
7
|
+
export const SPAWN_MODES = new Set(["solo", "cascade"]);
|
|
8
|
+
|
|
9
|
+
export function stripJsonc(text) {
|
|
10
|
+
return String(text ?? "")
|
|
11
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
12
|
+
.replace(/\/\/.*$/gm, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readConfigObject(workspaceRoot) {
|
|
16
|
+
if (!workspaceRoot) return null;
|
|
17
|
+
const raw = fs.readFileSync(path.join(workspaceRoot, CONFIG_REL), "utf8");
|
|
18
|
+
const obj = JSON.parse(stripJsonc(raw));
|
|
19
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return null;
|
|
20
|
+
if (obj.version !== 1) return null;
|
|
21
|
+
return obj;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @returns {"auto"|"ask"} */
|
|
25
|
+
export function loadLeftoverGate(workspaceRoot) {
|
|
26
|
+
try {
|
|
27
|
+
const obj = readConfigObject(workspaceRoot);
|
|
28
|
+
if (!obj) return "ask";
|
|
29
|
+
if (obj.leftoverGate === "auto" || obj.leftoverGate === "ask") {
|
|
30
|
+
return obj.leftoverGate;
|
|
31
|
+
}
|
|
32
|
+
return "ask";
|
|
33
|
+
} catch {
|
|
34
|
+
return "ask";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @returns {"solo"|"cascade"} */
|
|
39
|
+
export function loadSpawnMode(workspaceRoot) {
|
|
40
|
+
try {
|
|
41
|
+
const obj = readConfigObject(workspaceRoot);
|
|
42
|
+
if (!obj) return "cascade";
|
|
43
|
+
if (obj.spawnMode === "solo" || obj.spawnMode === "cascade") {
|
|
44
|
+
return obj.spawnMode;
|
|
45
|
+
}
|
|
46
|
+
return "cascade";
|
|
47
|
+
} catch {
|
|
48
|
+
return "cascade";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -11,10 +11,7 @@ import {
|
|
|
11
11
|
snapshotGuardedRoots,
|
|
12
12
|
withGuardedCheckInteriors,
|
|
13
13
|
} from "../cli/init.mjs";
|
|
14
|
-
|
|
15
|
-
const GENERATE_RAW = "rulesync:generate:raw";
|
|
16
|
-
const CHECK_RAW = "rulesync:check:raw";
|
|
17
|
-
const WATCH_RAW = "rulesync:watch:raw";
|
|
14
|
+
import { runPipeline } from "./pipeline.mjs";
|
|
18
15
|
|
|
19
16
|
export function attachGuardedRootWatchers(cwd, heal) {
|
|
20
17
|
const w = fs.watch(cwd, (_event, filename) => {
|
|
@@ -36,7 +33,7 @@ export function runGuardedRoots(mode, { cwd = process.cwd(), exec = execSync, at
|
|
|
36
33
|
if (mode === "generate") {
|
|
37
34
|
const snap = snapshotGuardedRoots(cwd);
|
|
38
35
|
try {
|
|
39
|
-
|
|
36
|
+
runPipeline("generate", { cwd, exec });
|
|
40
37
|
} finally {
|
|
41
38
|
remergeGuardedRoots(cwd, snap);
|
|
42
39
|
}
|
|
@@ -44,7 +41,7 @@ export function runGuardedRoots(mode, { cwd = process.cwd(), exec = execSync, at
|
|
|
44
41
|
}
|
|
45
42
|
if (mode === "check") {
|
|
46
43
|
withGuardedCheckInteriors(cwd, () => {
|
|
47
|
-
|
|
44
|
+
runPipeline("check", { cwd, exec });
|
|
48
45
|
});
|
|
49
46
|
return;
|
|
50
47
|
}
|
|
@@ -54,7 +51,7 @@ export function runGuardedRoots(mode, { cwd = process.cwd(), exec = execSync, at
|
|
|
54
51
|
const attach = attachWatchers || ((opts) => attachGuardedRootWatchers(opts.cwd, opts.heal));
|
|
55
52
|
const stop = attach({ cwd, snap, heal });
|
|
56
53
|
try {
|
|
57
|
-
|
|
54
|
+
runPipeline("watch", { cwd, exec });
|
|
58
55
|
} finally {
|
|
59
56
|
try {
|
|
60
57
|
stop?.();
|