@lovrozagar/grunt 0.4.0 → 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/.agents/agents/grunt.md +2 -2
- package/.agents/agents/implementer.md +3 -2
- package/.agents/agents/orchestrator.md +17 -11
- package/.agents/agents/thinker.md +7 -2
- package/.agents/skills/explain/SKILL.md +2 -0
- package/.agents/skills/handoff/SKILL.md +1 -1
- package/.agents/skills/solo/SKILL.md +1 -1
- package/.claude/agents/grunt.md +2 -2
- package/.claude/agents/implementer.md +5 -2
- package/.claude/agents/orchestrator.md +17 -11
- package/.claude/agents/thinker.md +9 -2
- package/.claude/skills/explain/SKILL.md +2 -0
- package/.claude/skills/handoff/SKILL.md +1 -1
- package/.claude/skills/solo/SKILL.md +1 -1
- package/.codex/agents/grunt.toml +2 -2
- package/.codex/agents/implementer.toml +3 -2
- package/.codex/agents/orchestrator.toml +17 -11
- package/.codex/agents/thinker.toml +7 -2
- package/.grok/agents/grunt.md +2 -2
- package/.grok/agents/implementer.md +3 -2
- package/.grok/agents/orchestrator.md +17 -11
- package/.grok/agents/thinker.md +7 -2
- package/.grok/hooks/orchestrate-parent.js +28 -110
- package/.grok/skills/explain/SKILL.md +2 -0
- package/.grok/skills/handoff/SKILL.md +1 -1
- package/.grok/skills/solo/SKILL.md +1 -1
- package/.rulesync/reference/cascade.md +12 -6
- package/.rulesync/reference/hooks.md +4 -4
- package/.rulesync/reference/map.md +2 -3
- package/.rulesync/reference/output.md +38 -1
- package/.rulesync/rules/CLAUDE.md +18 -12
- package/.rulesync/rules/overview.md +18 -12
- package/.rulesync/skills/explain/SKILL.md +2 -0
- package/.rulesync/skills/handoff/SKILL.md +1 -1
- package/.rulesync/skills/solo/SKILL.md +1 -1
- package/.rulesync/subagents/grunt.md +2 -2
- package/.rulesync/subagents/implementer.md +3 -2
- package/.rulesync/subagents/orchestrator.md +17 -11
- package/.rulesync/subagents/thinker.md +7 -2
- package/AGENTS.md +20 -12
- package/CHANGELOG.md +187 -0
- package/CLAUDE.md +20 -12
- package/README.md +32 -15
- package/cli/init.mjs +195 -29
- package/cli/init.test.ts +458 -43
- package/package.json +11 -7
- package/scripts/emit-gemini.mjs +7 -1
- package/scripts/grunt-job.mjs +0 -9
- package/scripts/guarded-roots.mjs +83 -0
- package/scripts/telemetry.mjs +0 -38
package/cli/init.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const PRODUCT_SCRIPTS = [
|
|
|
9
9
|
"check-globals.mjs",
|
|
10
10
|
"emit-agent-shell-tools.mjs",
|
|
11
11
|
"emit-gemini.mjs",
|
|
12
|
+
"guarded-roots.mjs",
|
|
12
13
|
"emit-mcp-policy.mjs",
|
|
13
14
|
"gate-fat-tools.mjs",
|
|
14
15
|
"hooks-union.mjs",
|
|
@@ -20,7 +21,6 @@ const PRODUCT_SCRIPTS = [
|
|
|
20
21
|
"scrub-spawn-prompt.mjs",
|
|
21
22
|
"scrub-text-lib.mjs",
|
|
22
23
|
"sync-global-settings.mjs",
|
|
23
|
-
"telemetry.mjs",
|
|
24
24
|
"browser.mjs",
|
|
25
25
|
"doctor.mjs",
|
|
26
26
|
"scrub-text",
|
|
@@ -28,12 +28,16 @@ const PRODUCT_SCRIPTS = [
|
|
|
28
28
|
|
|
29
29
|
const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"]
|
|
30
30
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"]
|
|
31
|
+
export const GUARDED_ROOT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]
|
|
31
32
|
// .mcp.json is intentionally not copied here: scripts/emit-mcp-policy.mjs
|
|
32
33
|
// (run via `rulesync:generate`, see mergeClaudeSettings ordering) owns and
|
|
33
34
|
// merges it, so a plain-copy would just be clobbered/redundant.
|
|
34
35
|
const OWNED_HOOK_FILES = ["scrub-spawn-prompt.mjs", "gate-fat-tools.mjs", "orchestrate-parent.js"]
|
|
35
|
-
const SENTINEL_BEGIN = "<!-- grunt:begin -->"
|
|
36
|
-
const SENTINEL_END = "<!-- grunt:end -->"
|
|
36
|
+
export const SENTINEL_BEGIN = "<!-- grunt:begin -->"
|
|
37
|
+
export const SENTINEL_END = "<!-- grunt:end -->"
|
|
38
|
+
export const MAX_GUARDED_MARKDOWN_BYTES = 2 * 1024 * 1024
|
|
39
|
+
const GRUNT_REGION_RE = /<!-- grunt:begin -->\r?\n?[\s\S]*?<!-- grunt:end -->\r?\n?/g
|
|
40
|
+
const GRUNT_INTERIOR_RE = /<!-- grunt:begin -->\r?\n?([\s\S]*?)<!-- grunt:end -->/g
|
|
37
41
|
|
|
38
42
|
function sortKeys(obj) {
|
|
39
43
|
return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]))
|
|
@@ -69,43 +73,208 @@ function destHasGruntSentinel(dest) {
|
|
|
69
73
|
|
|
70
74
|
export function destAlreadyInited(dest) {
|
|
71
75
|
return (
|
|
72
|
-
fs.existsSync(path.join(dest, "scripts", "telemetry.mjs")) ||
|
|
73
76
|
fs.existsSync(path.join(dest, ".grok", "hooks", "orchestrate-parent.js")) ||
|
|
74
77
|
fs.existsSync(path.join(dest, ".rulesync"))
|
|
75
78
|
)
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export function shouldAutoSkipGlobals(dest) {
|
|
79
|
-
return destHasGruntSentinel(dest)
|
|
82
|
+
return destHasGruntSentinel(dest)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectNewline(text) {
|
|
86
|
+
return String(text).includes("\r\n") ? "\r\n" : "\n"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toNewline(text, nl) {
|
|
90
|
+
return String(text).replace(/\r\n/g, "\n").replace(/\n/g, nl)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function trimTrailingNewlines(text) {
|
|
94
|
+
return String(text).replace(/(?:\r?\n)+$/, "")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normEq(a, b) {
|
|
98
|
+
return trimTrailingNewlines(String(a).replace(/\r\n/g, "\n")) ===
|
|
99
|
+
trimTrailingNewlines(String(b).replace(/\r\n/g, "\n"))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cloneRe(re) {
|
|
103
|
+
return new RegExp(re.source, re.flags)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function inspectGuardedBuffer(buf) {
|
|
107
|
+
if (buf.length > MAX_GUARDED_MARKDOWN_BYTES) return "huge"
|
|
108
|
+
if (buf.includes(0)) return "binary"
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function extractUserMarkdown(text) {
|
|
113
|
+
return String(text).replace(cloneRe(GRUNT_REGION_RE), "")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function extractGruntBody(text) {
|
|
117
|
+
const parts = []
|
|
118
|
+
const re = cloneRe(GRUNT_INTERIOR_RE)
|
|
119
|
+
let m
|
|
120
|
+
while ((m = re.exec(String(text)))) {
|
|
121
|
+
parts.push(trimTrailingNewlines(m[1]))
|
|
122
|
+
}
|
|
123
|
+
if (!parts.length) return null
|
|
124
|
+
return parts.join("\n")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function userRemainderIsBlank(user) {
|
|
128
|
+
return String(user).replace(/\r\n/g, "\n").replace(/\n/g, "") === ""
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function composeGuardedMarkdown(gruntBody, userRemainder, nl = "\n") {
|
|
132
|
+
const body = toNewline(trimTrailingNewlines(gruntBody), nl)
|
|
133
|
+
const block = `${SENTINEL_BEGIN}${nl}${body}${nl}${SENTINEL_END}${nl}`
|
|
134
|
+
const user = userRemainder == null ? "" : String(userRemainder)
|
|
135
|
+
if (!user || userRemainderIsBlank(user)) return block
|
|
136
|
+
return block + toNewline(user, nl)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function mergeGuardedContent(existingText, gruntBody) {
|
|
140
|
+
const existing = existingText == null ? "" : String(existingText)
|
|
141
|
+
const nl = existing.includes("\r\n") || String(gruntBody).includes("\r\n")
|
|
142
|
+
? detectNewline(existing || gruntBody)
|
|
143
|
+
: "\n"
|
|
144
|
+
if (!existing) return composeGuardedMarkdown(gruntBody, "", nl)
|
|
145
|
+
const hadSentinel = existing.includes(SENTINEL_BEGIN) && existing.includes(SENTINEL_END)
|
|
146
|
+
const user = extractUserMarkdown(existing)
|
|
147
|
+
if (!hadSentinel && normEq(existing, gruntBody)) {
|
|
148
|
+
return composeGuardedMarkdown(gruntBody, "", nl)
|
|
149
|
+
}
|
|
150
|
+
return composeGuardedMarkdown(gruntBody, user, nl)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function writeMergedGuardedFile(destPath, gruntBody) {
|
|
154
|
+
if (!fs.existsSync(destPath)) {
|
|
155
|
+
fs.writeFileSync(destPath, mergeGuardedContent("", gruntBody))
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
const buf = fs.readFileSync(destPath)
|
|
159
|
+
if (inspectGuardedBuffer(buf)) return "aborted-unsafe"
|
|
160
|
+
fs.writeFileSync(destPath, mergeGuardedContent(buf.toString("utf8"), gruntBody))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function guardedMarkdownDrift(destPath, gruntBody) {
|
|
164
|
+
if (!fs.existsSync(destPath)) return true
|
|
165
|
+
const buf = fs.readFileSync(destPath)
|
|
166
|
+
if (inspectGuardedBuffer(buf)) return true
|
|
167
|
+
const text = buf.toString("utf8")
|
|
168
|
+
const interior = extractGruntBody(text)
|
|
169
|
+
if (interior == null) return !normEq(text, gruntBody)
|
|
170
|
+
return !normEq(interior, gruntBody)
|
|
80
171
|
}
|
|
81
172
|
|
|
82
173
|
export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = false } = {}) {
|
|
174
|
+
void alreadyInited
|
|
83
175
|
const src = path.join(pkgRoot, file)
|
|
84
176
|
const d = path.join(dest, file)
|
|
85
177
|
if (samePath(src, d)) return
|
|
86
178
|
const srcText = fs.readFileSync(src, "utf8")
|
|
179
|
+
return writeMergedGuardedFile(d, srcText)
|
|
180
|
+
}
|
|
87
181
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
182
|
+
export function snapshotGuardedRoots(root) {
|
|
183
|
+
const snap = {}
|
|
184
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
185
|
+
const p = path.join(root, file)
|
|
186
|
+
if (!fs.existsSync(p)) continue
|
|
187
|
+
const buf = fs.readFileSync(p)
|
|
188
|
+
const unsafe = inspectGuardedBuffer(buf)
|
|
189
|
+
if (unsafe) {
|
|
190
|
+
snap[file] = { unsafe, rawBuf: buf }
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
const raw = buf.toString("utf8")
|
|
194
|
+
snap[file] = { raw, user: extractUserMarkdown(raw) }
|
|
91
195
|
}
|
|
196
|
+
return snap
|
|
197
|
+
}
|
|
92
198
|
|
|
93
|
-
|
|
94
|
-
const
|
|
199
|
+
export function remergeGuardedRoots(root, snap) {
|
|
200
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
201
|
+
const p = path.join(root, file)
|
|
202
|
+
const rec = snap[file]
|
|
203
|
+
if (rec?.unsafe) {
|
|
204
|
+
fs.writeFileSync(p, rec.rawBuf)
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
if (!fs.existsSync(p)) {
|
|
208
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
const buf = fs.readFileSync(p)
|
|
212
|
+
if (inspectGuardedBuffer(buf)) {
|
|
213
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
const current = buf.toString("utf8")
|
|
217
|
+
if (rec && current === rec.raw) continue
|
|
218
|
+
const gruntBody = extractGruntBody(current) ?? current
|
|
219
|
+
const nl = detectNewline((rec && rec.raw) || current)
|
|
220
|
+
let user = rec ? rec.user : extractUserMarkdown(current)
|
|
221
|
+
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = ""
|
|
222
|
+
if (!rec && extractGruntBody(current) == null && normEq(current, gruntBody)) user = ""
|
|
223
|
+
fs.writeFileSync(p, composeGuardedMarkdown(gruntBody, user, nl))
|
|
224
|
+
}
|
|
225
|
+
}
|
|
95
226
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
227
|
+
export function healGuardedRootFile(root, file, snap) {
|
|
228
|
+
if (!GUARDED_ROOT_FILES.includes(file)) return
|
|
229
|
+
const p = path.join(root, file)
|
|
230
|
+
const rec = snap[file]
|
|
231
|
+
if (!fs.existsSync(p)) return
|
|
232
|
+
if (rec?.unsafe) {
|
|
233
|
+
fs.writeFileSync(p, rec.rawBuf)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
const buf = fs.readFileSync(p)
|
|
237
|
+
if (inspectGuardedBuffer(buf)) {
|
|
238
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
101
239
|
return
|
|
102
240
|
}
|
|
241
|
+
const current = buf.toString("utf8")
|
|
242
|
+
const interior = extractGruntBody(current)
|
|
243
|
+
if (interior != null) {
|
|
244
|
+
snap[file] = { raw: current, user: extractUserMarkdown(current) }
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (rec && current === rec.raw) return
|
|
248
|
+
const gruntBody = current
|
|
249
|
+
const nl = detectNewline((rec && rec.raw) || current)
|
|
250
|
+
let user = rec ? rec.user : extractUserMarkdown(current)
|
|
251
|
+
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = ""
|
|
252
|
+
if (!rec && normEq(current, gruntBody)) user = ""
|
|
253
|
+
const out = composeGuardedMarkdown(gruntBody, user, nl)
|
|
254
|
+
fs.writeFileSync(p, out)
|
|
255
|
+
snap[file] = { raw: out, user }
|
|
256
|
+
}
|
|
103
257
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
258
|
+
export function withGuardedCheckInteriors(root, fn) {
|
|
259
|
+
const snap = snapshotGuardedRoots(root)
|
|
260
|
+
try {
|
|
261
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
262
|
+
const rec = snap[file]
|
|
263
|
+
if (!rec || rec.unsafe) continue
|
|
264
|
+
const body = extractGruntBody(rec.raw)
|
|
265
|
+
if (body == null) continue
|
|
266
|
+
const nl = detectNewline(rec.raw)
|
|
267
|
+
fs.writeFileSync(path.join(root, file), `${toNewline(trimTrailingNewlines(body), nl)}${nl}`)
|
|
268
|
+
}
|
|
269
|
+
return fn()
|
|
270
|
+
} finally {
|
|
271
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
272
|
+
const rec = snap[file]
|
|
273
|
+
if (!rec) continue
|
|
274
|
+
if (rec.unsafe) fs.writeFileSync(path.join(root, file), rec.rawBuf)
|
|
275
|
+
else fs.writeFileSync(path.join(root, file), rec.raw)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
109
278
|
}
|
|
110
279
|
|
|
111
280
|
export function mergeClaudeSettings(destRoot, pkgRoot) {
|
|
@@ -222,7 +391,6 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
222
391
|
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase } = {}) {
|
|
223
392
|
dest = path.resolve(dest)
|
|
224
393
|
const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
|
|
225
|
-
const alreadyInited = destAlreadyInited(dest)
|
|
226
394
|
const skipGlobalsApply =
|
|
227
395
|
applyGlobals === true ? false : skipGlobals || shouldAutoSkipGlobals(dest)
|
|
228
396
|
|
|
@@ -255,14 +423,8 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
255
423
|
}
|
|
256
424
|
}
|
|
257
425
|
|
|
258
|
-
let skippedSideFile = false
|
|
259
426
|
for (const file of GUARDED_MD_FILES) {
|
|
260
|
-
|
|
261
|
-
skippedSideFile = true
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (skippedSideFile) {
|
|
265
|
-
console.log("AGENTS.md/CLAUDE.md lack markers and were left alone")
|
|
427
|
+
mergeGuardedMarkdown(dest, pkgRoot, file)
|
|
266
428
|
}
|
|
267
429
|
|
|
268
430
|
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true })
|
|
@@ -291,9 +453,13 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
291
453
|
exec("npm", args, { cwd: dest, stdio: "inherit" })
|
|
292
454
|
}
|
|
293
455
|
runNpm("install", ["install"])
|
|
456
|
+
const guardedSnap = snapshotGuardedRoots(dest)
|
|
294
457
|
runNpm("generate", ["run", "rulesync:generate"])
|
|
458
|
+
remergeGuardedRoots(dest, guardedSnap)
|
|
295
459
|
if (!skipGlobalsApply) {
|
|
296
460
|
runNpm("sync-globals", ["run", "sync:globals:apply"])
|
|
297
461
|
}
|
|
298
|
-
|
|
462
|
+
withGuardedCheckInteriors(dest, () => {
|
|
463
|
+
runNpm("check", ["run", "rulesync:check"])
|
|
464
|
+
})
|
|
299
465
|
}
|