@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.test.ts
CHANGED
|
@@ -3,15 +3,27 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import {
|
|
6
|
+
MAX_GUARDED_MARKDOWN_BYTES,
|
|
7
|
+
composeGuardedMarkdown,
|
|
6
8
|
destAlreadyInited,
|
|
9
|
+
extractGruntBody,
|
|
10
|
+
GUARDED_ROOT_FILES,
|
|
11
|
+
guardedMarkdownDrift,
|
|
12
|
+
healGuardedRootFile,
|
|
7
13
|
init,
|
|
8
14
|
mergeClaudeSettings,
|
|
9
15
|
mergeGitignore,
|
|
16
|
+
mergeGuardedContent,
|
|
10
17
|
mergeGuardedMarkdown,
|
|
11
18
|
mergePackageJson,
|
|
19
|
+
remergeGuardedRoots,
|
|
12
20
|
samePath,
|
|
13
21
|
shouldAutoSkipGlobals,
|
|
22
|
+
snapshotGuardedRoots,
|
|
23
|
+
withGuardedCheckInteriors,
|
|
24
|
+
writeMergedGuardedFile,
|
|
14
25
|
} from "./init.mjs";
|
|
26
|
+
import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
|
|
15
27
|
|
|
16
28
|
const tmpDirs: string[] = [];
|
|
17
29
|
afterEach(() => {
|
|
@@ -32,6 +44,7 @@ const PRODUCT_FILES = [
|
|
|
32
44
|
"check-globals.mjs",
|
|
33
45
|
"emit-agent-shell-tools.mjs",
|
|
34
46
|
"emit-gemini.mjs",
|
|
47
|
+
"guarded-roots.mjs",
|
|
35
48
|
"emit-mcp-policy.mjs",
|
|
36
49
|
"gate-fat-tools.mjs",
|
|
37
50
|
"hooks-union.mjs",
|
|
@@ -43,7 +56,6 @@ const PRODUCT_FILES = [
|
|
|
43
56
|
"scrub-spawn-prompt.mjs",
|
|
44
57
|
"scrub-text-lib.mjs",
|
|
45
58
|
"sync-global-settings.mjs",
|
|
46
|
-
"telemetry.mjs",
|
|
47
59
|
"browser.mjs",
|
|
48
60
|
"doctor.mjs",
|
|
49
61
|
];
|
|
@@ -203,22 +215,39 @@ describe("mergeGuardedMarkdown", () => {
|
|
|
203
215
|
);
|
|
204
216
|
});
|
|
205
217
|
|
|
206
|
-
it("dest exists without sentinel:
|
|
218
|
+
it("dest exists without sentinel: prepends wrapped grunt, keeps full old body", () => {
|
|
207
219
|
const pkgRoot = stubPkgRoot();
|
|
208
220
|
const dest = tmp("md-nosentinel-");
|
|
209
221
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
210
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
211
222
|
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
212
223
|
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
213
|
-
"
|
|
224
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
214
225
|
);
|
|
215
|
-
expect(fs.
|
|
216
|
-
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
217
|
-
expect(logSpy.mock.calls[0][0]).toContain("CLAUDE.grunt.md");
|
|
218
|
-
logSpy.mockRestore();
|
|
226
|
+
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
219
227
|
});
|
|
220
228
|
|
|
221
|
-
it("
|
|
229
|
+
it("alreadyInited unmarked still prepends; skip is not safe", () => {
|
|
230
|
+
const pkgRoot = stubPkgRoot();
|
|
231
|
+
const dest = tmp("md-inited-nosent-");
|
|
232
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
233
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md", { alreadyInited: true });
|
|
234
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
235
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
236
|
+
);
|
|
237
|
+
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("empty dest file is treated as missing", () => {
|
|
241
|
+
const pkgRoot = stubPkgRoot();
|
|
242
|
+
const dest = tmp("md-empty-");
|
|
243
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "");
|
|
244
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
245
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
246
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("dest exists with sentinel: grunt block at top, outside kept at bottom", () => {
|
|
222
251
|
const pkgRoot = stubPkgRoot();
|
|
223
252
|
const dest = tmp("md-sentinel-");
|
|
224
253
|
fs.writeFileSync(
|
|
@@ -227,10 +256,368 @@ describe("mergeGuardedMarkdown", () => {
|
|
|
227
256
|
);
|
|
228
257
|
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
229
258
|
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
230
|
-
"
|
|
259
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nbefore\nafter\n",
|
|
231
260
|
);
|
|
232
261
|
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
233
262
|
});
|
|
263
|
+
|
|
264
|
+
it("collapses duplicate sentinel pairs to one top block", () => {
|
|
265
|
+
const pkgRoot = stubPkgRoot();
|
|
266
|
+
const dest = tmp("md-dup-sent-");
|
|
267
|
+
fs.writeFileSync(
|
|
268
|
+
path.join(dest, "CLAUDE.md"),
|
|
269
|
+
"<!-- grunt:begin -->\nold-a\n<!-- grunt:end -->\nkeep-a\n<!-- grunt:begin -->\nold-b\n<!-- grunt:end -->\nkeep-b\n",
|
|
270
|
+
);
|
|
271
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
272
|
+
const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
273
|
+
expect(out).toBe(
|
|
274
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nkeep-a\nkeep-b\n",
|
|
275
|
+
);
|
|
276
|
+
expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("second merge is idempotent and does not double-prepend", () => {
|
|
280
|
+
const pkgRoot = stubPkgRoot();
|
|
281
|
+
const dest = tmp("md-idem-");
|
|
282
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
283
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
284
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
285
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
286
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("does not fuzzy-dedupe unmarked protocol-looking user text", () => {
|
|
291
|
+
const pkgRoot = stubPkgRoot();
|
|
292
|
+
const dest = tmp("md-proto-");
|
|
293
|
+
const user = "Voice: `.rulesync/reference/output.md` — cite once.\nspawn grunt\n";
|
|
294
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), user);
|
|
295
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
296
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
297
|
+
`<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n${user}`,
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("preserves CRLF from dest", () => {
|
|
302
|
+
const pkgRoot = stubPkgRoot();
|
|
303
|
+
const dest = tmp("md-crlf-");
|
|
304
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "user line\r\n");
|
|
305
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
306
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
307
|
+
"<!-- grunt:begin -->\r\nCLAUDE.md content\r\n<!-- grunt:end -->\r\nuser line\r\n",
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("unmarked dest equal to grunt body wraps without duplicating", () => {
|
|
312
|
+
const pkgRoot = stubPkgRoot();
|
|
313
|
+
const dest = tmp("md-equal-");
|
|
314
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "CLAUDE.md content\n");
|
|
315
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
316
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
317
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("binary dest is left untouched", () => {
|
|
322
|
+
const pkgRoot = stubPkgRoot();
|
|
323
|
+
const dest = tmp("md-bin-");
|
|
324
|
+
const raw = Buffer.from([0x68, 0x69, 0x00, 0xff]);
|
|
325
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), raw);
|
|
326
|
+
expect(mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md")).toBe("aborted-unsafe");
|
|
327
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"))).toEqual(raw);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("leaves nested AGENTS.md alone", () => {
|
|
331
|
+
const pkgRoot = stubPkgRoot();
|
|
332
|
+
const dest = tmp("md-nested-");
|
|
333
|
+
fs.mkdirSync(path.join(dest, "pkg"), { recursive: true });
|
|
334
|
+
fs.writeFileSync(path.join(dest, "pkg", "AGENTS.md"), "nested user\n");
|
|
335
|
+
mergeGuardedMarkdown(dest, pkgRoot, "AGENTS.md");
|
|
336
|
+
expect(fs.readFileSync(path.join(dest, "pkg", "AGENTS.md"), "utf8")).toBe("nested user\n");
|
|
337
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
338
|
+
"<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\n",
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("snapshot/remerge guarded roots", () => {
|
|
344
|
+
it("remerge after clobber keeps user bottom and refreshes grunt interior", () => {
|
|
345
|
+
const dest = tmp("snap-clobber-");
|
|
346
|
+
fs.writeFileSync(
|
|
347
|
+
path.join(dest, "AGENTS.md"),
|
|
348
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nconsumer agents\n",
|
|
349
|
+
);
|
|
350
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "user gemini\n");
|
|
351
|
+
const snap = snapshotGuardedRoots(dest);
|
|
352
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
353
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
|
|
354
|
+
remergeGuardedRoots(dest, snap);
|
|
355
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
356
|
+
"<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\nconsumer agents\n",
|
|
357
|
+
);
|
|
358
|
+
expect(fs.readFileSync(path.join(dest, "GEMINI.md"), "utf8")).toBe(
|
|
359
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\nuser gemini\n",
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("withGuardedCheckInteriors is restored even if check throws", () => {
|
|
364
|
+
const dest = tmp("check-int-");
|
|
365
|
+
const live =
|
|
366
|
+
"<!-- grunt:begin -->\ngrunt body\n<!-- grunt:end -->\nuser bottom\n";
|
|
367
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), live);
|
|
368
|
+
expect(() =>
|
|
369
|
+
withGuardedCheckInteriors(dest, () => {
|
|
370
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe("grunt body\n");
|
|
371
|
+
throw new Error("check failed");
|
|
372
|
+
}),
|
|
373
|
+
).toThrow("check failed");
|
|
374
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(live);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("compose/extract/drift/write helpers cover remaining branches", () => {
|
|
378
|
+
expect(composeGuardedMarkdown("body", null)).toBe(
|
|
379
|
+
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
|
|
380
|
+
);
|
|
381
|
+
expect(composeGuardedMarkdown("body", "\n\n")).toBe(
|
|
382
|
+
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
|
|
383
|
+
);
|
|
384
|
+
expect(extractGruntBody("no markers")).toBeNull();
|
|
385
|
+
expect(
|
|
386
|
+
extractGruntBody(
|
|
387
|
+
"<!-- grunt:begin -->\na\n<!-- grunt:end -->\n<!-- grunt:begin -->\nb\n<!-- grunt:end -->\n",
|
|
388
|
+
),
|
|
389
|
+
).toBe("a\nb");
|
|
390
|
+
expect(mergeGuardedContent(null, "g")).toBe(
|
|
391
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
|
|
392
|
+
);
|
|
393
|
+
expect(mergeGuardedContent("", "a\r\nb")).toBe(
|
|
394
|
+
"<!-- grunt:begin -->\r\na\r\nb\r\n<!-- grunt:end -->\r\n",
|
|
395
|
+
);
|
|
396
|
+
expect(composeGuardedMarkdown("b", "\r\n")).toBe(
|
|
397
|
+
"<!-- grunt:begin -->\nb\n<!-- grunt:end -->\n",
|
|
398
|
+
);
|
|
399
|
+
expect(mergeGuardedContent("keep\n", "g\r\n")).toBe(
|
|
400
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\nkeep\n",
|
|
401
|
+
);
|
|
402
|
+
expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toContain("only");
|
|
403
|
+
const dest = tmp("md-helpers-");
|
|
404
|
+
const missing = path.join(dest, "nope.md");
|
|
405
|
+
expect(guardedMarkdownDrift(missing, "g")).toBe(true);
|
|
406
|
+
writeMergedGuardedFile(missing, "g");
|
|
407
|
+
expect(fs.readFileSync(missing, "utf8")).toBe(
|
|
408
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
|
|
409
|
+
);
|
|
410
|
+
expect(guardedMarkdownDrift(missing, "g")).toBe(false);
|
|
411
|
+
expect(guardedMarkdownDrift(missing, "other")).toBe(true);
|
|
412
|
+
fs.writeFileSync(missing, "plain\n");
|
|
413
|
+
expect(guardedMarkdownDrift(missing, "plain\n")).toBe(false);
|
|
414
|
+
expect(guardedMarkdownDrift(missing, "x")).toBe(true);
|
|
415
|
+
const bin = path.join(dest, "bin.md");
|
|
416
|
+
fs.writeFileSync(bin, Buffer.from([0]));
|
|
417
|
+
expect(guardedMarkdownDrift(bin, "g")).toBe(true);
|
|
418
|
+
const huge = path.join(dest, "huge.md");
|
|
419
|
+
fs.writeFileSync(huge, Buffer.alloc(MAX_GUARDED_MARKDOWN_BYTES + 1, 65));
|
|
420
|
+
expect(writeMergedGuardedFile(huge, "g")).toBe("aborted-unsafe");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("remerge restores unsafe, missing, binary current, and created files", () => {
|
|
424
|
+
const dest = tmp("remerge-edges-");
|
|
425
|
+
const agents = path.join(dest, "AGENTS.md");
|
|
426
|
+
const claude = path.join(dest, "CLAUDE.md");
|
|
427
|
+
const gemini = path.join(dest, "GEMINI.md");
|
|
428
|
+
const rawBuf = Buffer.from([0x41, 0x00]);
|
|
429
|
+
fs.writeFileSync(agents, rawBuf);
|
|
430
|
+
fs.writeFileSync(claude, "keep claude\n");
|
|
431
|
+
const snap = snapshotGuardedRoots(dest);
|
|
432
|
+
expect(snap["AGENTS.md"].unsafe).toBe("binary");
|
|
433
|
+
fs.writeFileSync(agents, "clobbered\n");
|
|
434
|
+
fs.rmSync(claude);
|
|
435
|
+
fs.writeFileSync(gemini, "@AGENTS.md\n");
|
|
436
|
+
const binClaude = path.join(dest, "CLAUDE.md");
|
|
437
|
+
remergeGuardedRoots(dest, snap);
|
|
438
|
+
expect(fs.readFileSync(agents)).toEqual(rawBuf);
|
|
439
|
+
expect(fs.readFileSync(claude, "utf8")).toBe("keep claude\n");
|
|
440
|
+
expect(fs.readFileSync(gemini, "utf8")).toBe(
|
|
441
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\n",
|
|
442
|
+
);
|
|
443
|
+
fs.writeFileSync(claude, Buffer.from([0x00]));
|
|
444
|
+
remergeGuardedRoots(dest, snap);
|
|
445
|
+
expect(fs.readFileSync(binClaude, "utf8")).toBe("keep claude\n");
|
|
446
|
+
remergeGuardedRoots(dest, {});
|
|
447
|
+
fs.writeFileSync(gemini, Buffer.from([0x00]));
|
|
448
|
+
remergeGuardedRoots(dest, {});
|
|
449
|
+
expect(fs.readFileSync(gemini)).toEqual(Buffer.from([0x00]));
|
|
450
|
+
fs.rmSync(gemini);
|
|
451
|
+
remergeGuardedRoots(dest, {});
|
|
452
|
+
expect(fs.existsSync(gemini)).toBe(false);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("remerge clears user when unmarked snap equals new grunt body", () => {
|
|
456
|
+
const dest = tmp("remerge-eq-");
|
|
457
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "same\n");
|
|
458
|
+
const snap = snapshotGuardedRoots(dest);
|
|
459
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "same\n");
|
|
460
|
+
remergeGuardedRoots(dest, snap);
|
|
461
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe("same\n");
|
|
462
|
+
fs.writeFileSync(
|
|
463
|
+
path.join(dest, "AGENTS.md"),
|
|
464
|
+
"<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
|
|
465
|
+
);
|
|
466
|
+
remergeGuardedRoots(dest, snap);
|
|
467
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
468
|
+
"<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("withGuardedCheckInteriors skips unmarked and unsafe then restores", () => {
|
|
473
|
+
const dest = tmp("check-skip-");
|
|
474
|
+
const live = "plain user\n";
|
|
475
|
+
const rawBuf = Buffer.from([0x00, 0x01]);
|
|
476
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), live);
|
|
477
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), rawBuf);
|
|
478
|
+
const ret = withGuardedCheckInteriors(dest, () => {
|
|
479
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(live);
|
|
480
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"))).toEqual(rawBuf);
|
|
481
|
+
return 7;
|
|
482
|
+
});
|
|
483
|
+
expect(ret).toBe(7);
|
|
484
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(live);
|
|
485
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"))).toEqual(rawBuf);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("runGuardedRoots generate rematches after inner clobber", () => {
|
|
489
|
+
const dest = tmp("guarded-run-");
|
|
490
|
+
fs.writeFileSync(
|
|
491
|
+
path.join(dest, "AGENTS.md"),
|
|
492
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
493
|
+
);
|
|
494
|
+
const exec = vi.fn(() => {
|
|
495
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "fresh ssot\n");
|
|
496
|
+
});
|
|
497
|
+
runGuardedRoots("generate", { cwd: dest, exec });
|
|
498
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
499
|
+
expect(String(exec.mock.calls[0][0])).toContain("rulesync:generate:raw");
|
|
500
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
501
|
+
"<!-- grunt:begin -->\nfresh ssot\n<!-- grunt:end -->\nkeep me\n",
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("healGuardedRootFile rematches unmarked clobber and keeps later user edits", () => {
|
|
506
|
+
const dest = tmp("heal-watch-");
|
|
507
|
+
fs.writeFileSync(
|
|
508
|
+
path.join(dest, "AGENTS.md"),
|
|
509
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
510
|
+
);
|
|
511
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "user claude\n");
|
|
512
|
+
const snap = snapshotGuardedRoots(dest);
|
|
513
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch gen 1\n");
|
|
514
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
515
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
516
|
+
"<!-- grunt:begin -->\nwatch gen 1\n<!-- grunt:end -->\nkeep me\n",
|
|
517
|
+
);
|
|
518
|
+
fs.writeFileSync(
|
|
519
|
+
path.join(dest, "AGENTS.md"),
|
|
520
|
+
"<!-- grunt:begin -->\nwatch gen 1\n<!-- grunt:end -->\nedited bottom\n",
|
|
521
|
+
);
|
|
522
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
523
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch gen 2\n");
|
|
524
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
525
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
526
|
+
"<!-- grunt:begin -->\nwatch gen 2\n<!-- grunt:end -->\nedited bottom\n",
|
|
527
|
+
);
|
|
528
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "watch claude\n");
|
|
529
|
+
healGuardedRootFile(dest, "CLAUDE.md", snap);
|
|
530
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
531
|
+
"<!-- grunt:begin -->\nwatch claude\n<!-- grunt:end -->\nuser claude\n",
|
|
532
|
+
);
|
|
533
|
+
healGuardedRootFile(dest, "README.md", snap);
|
|
534
|
+
expect(GUARDED_ROOT_FILES).toEqual(["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
|
|
535
|
+
const eq = tmp("heal-eq-");
|
|
536
|
+
fs.writeFileSync(path.join(eq, "AGENTS.md"), "same\n");
|
|
537
|
+
const eqSnap = snapshotGuardedRoots(eq);
|
|
538
|
+
healGuardedRootFile(eq, "AGENTS.md", eqSnap);
|
|
539
|
+
expect(fs.readFileSync(path.join(eq, "AGENTS.md"), "utf8")).toBe("same\n");
|
|
540
|
+
fs.writeFileSync(path.join(eq, "AGENTS.md"), "same");
|
|
541
|
+
healGuardedRootFile(eq, "AGENTS.md", eqSnap);
|
|
542
|
+
expect(fs.readFileSync(path.join(eq, "AGENTS.md"), "utf8")).toBe(
|
|
543
|
+
"<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
|
|
544
|
+
);
|
|
545
|
+
fs.writeFileSync(path.join(eq, "GEMINI.md"), "@AGENTS.md\n");
|
|
546
|
+
healGuardedRootFile(eq, "GEMINI.md", {});
|
|
547
|
+
expect(fs.readFileSync(path.join(eq, "GEMINI.md"), "utf8")).toBe(
|
|
548
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\n",
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("healGuardedRootFile restores unsafe clobber and ignores missing", () => {
|
|
553
|
+
const dest = tmp("heal-unsafe-");
|
|
554
|
+
const rawBuf = Buffer.from([0x00, 0x02]);
|
|
555
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), rawBuf);
|
|
556
|
+
const snap = snapshotGuardedRoots(dest);
|
|
557
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "clobbered\n");
|
|
558
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
559
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"))).toEqual(rawBuf);
|
|
560
|
+
healGuardedRootFile(dest, "GEMINI.md", snap);
|
|
561
|
+
expect(fs.existsSync(path.join(dest, "GEMINI.md"))).toBe(false);
|
|
562
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "ok\n");
|
|
563
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), Buffer.from([0x00]));
|
|
564
|
+
const snap2 = snapshotGuardedRoots(dest);
|
|
565
|
+
snap2["CLAUDE.md"] = { raw: "ok\n", user: "ok\n" };
|
|
566
|
+
healGuardedRootFile(dest, "CLAUDE.md", snap2);
|
|
567
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe("ok\n");
|
|
568
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), Buffer.from([0x00]));
|
|
569
|
+
healGuardedRootFile(dest, "GEMINI.md", {});
|
|
570
|
+
expect(fs.readFileSync(path.join(dest, "GEMINI.md"))).toEqual(Buffer.from([0x00]));
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("runGuardedRoots watch rematches after each inner clobber", () => {
|
|
574
|
+
const dest = tmp("guarded-watch-");
|
|
575
|
+
fs.writeFileSync(
|
|
576
|
+
path.join(dest, "AGENTS.md"),
|
|
577
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
578
|
+
);
|
|
579
|
+
let heal;
|
|
580
|
+
const exec = vi.fn(() => {
|
|
581
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch1\n");
|
|
582
|
+
heal("AGENTS.md");
|
|
583
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
584
|
+
"<!-- grunt:begin -->\nwatch1\n<!-- grunt:end -->\nkeep me\n",
|
|
585
|
+
);
|
|
586
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch2\n");
|
|
587
|
+
heal("AGENTS.md");
|
|
588
|
+
});
|
|
589
|
+
const stop = vi.fn();
|
|
590
|
+
runGuardedRoots("watch", {
|
|
591
|
+
cwd: dest,
|
|
592
|
+
exec,
|
|
593
|
+
attachWatchers: ({ heal: h }) => {
|
|
594
|
+
heal = h;
|
|
595
|
+
return stop;
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
599
|
+
expect(String(exec.mock.calls[0][0])).toContain("rulesync:watch:raw");
|
|
600
|
+
expect(stop).toHaveBeenCalledTimes(1);
|
|
601
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
602
|
+
"<!-- grunt:begin -->\nwatch2\n<!-- grunt:end -->\nkeep me\n",
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("runGuardedRoots watch default watchers still rematch on exit", () => {
|
|
607
|
+
const dest = tmp("guarded-watch-def-");
|
|
608
|
+
fs.writeFileSync(
|
|
609
|
+
path.join(dest, "AGENTS.md"),
|
|
610
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
611
|
+
);
|
|
612
|
+
const exec = vi.fn(() => {
|
|
613
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "from-watch\n");
|
|
614
|
+
});
|
|
615
|
+
runGuardedRoots("watch", { cwd: dest, exec });
|
|
616
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
617
|
+
"<!-- grunt:begin -->\nfrom-watch\n<!-- grunt:end -->\nkeep me\n",
|
|
618
|
+
);
|
|
619
|
+
expect(() => runGuardedRoots("nope")).toThrow(/generate\|check\|watch/);
|
|
620
|
+
});
|
|
234
621
|
});
|
|
235
622
|
|
|
236
623
|
describe("mergeClaudeSettings", () => {
|
|
@@ -615,8 +1002,8 @@ describe("init", () => {
|
|
|
615
1002
|
expect(pkg.name).toBe("fixture-pkg");
|
|
616
1003
|
expect(fs.existsSync(path.join(pkgRoot, ".tmp"))).toBe(true);
|
|
617
1004
|
expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
|
|
618
|
-
expect(fs.readFileSync(path.join(pkgRoot, "scripts", "
|
|
619
|
-
"
|
|
1005
|
+
expect(fs.readFileSync(path.join(pkgRoot, "scripts", "grunt-job.mjs"), "utf8")).toBe(
|
|
1006
|
+
"grunt-job.mjs",
|
|
620
1007
|
);
|
|
621
1008
|
// self-skip: markdown/settings stay exactly as-is, no sentinel wrap
|
|
622
1009
|
expect(fs.readFileSync(path.join(pkgRoot, "CLAUDE.md"), "utf8")).toBe("CLAUDE.md content");
|
|
@@ -736,11 +1123,13 @@ describe("init", () => {
|
|
|
736
1123
|
]);
|
|
737
1124
|
});
|
|
738
1125
|
|
|
739
|
-
it("applyGlobals true still applies when
|
|
1126
|
+
it("applyGlobals true still applies when sentinel auto-skip would fire", () => {
|
|
740
1127
|
const pkgRoot = stubPkgRoot();
|
|
741
1128
|
const dest = tmp("grunt-force-globals-");
|
|
742
|
-
fs.
|
|
743
|
-
|
|
1129
|
+
fs.writeFileSync(
|
|
1130
|
+
path.join(dest, "AGENTS.md"),
|
|
1131
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\n",
|
|
1132
|
+
);
|
|
744
1133
|
const exec = vi.fn();
|
|
745
1134
|
init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
|
|
746
1135
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
@@ -751,20 +1140,6 @@ describe("init", () => {
|
|
|
751
1140
|
]);
|
|
752
1141
|
});
|
|
753
1142
|
|
|
754
|
-
it("auto-skip globals when telemetry.mjs exists", () => {
|
|
755
|
-
const pkgRoot = stubPkgRoot();
|
|
756
|
-
const dest = tmp("grunt-auto-tel-");
|
|
757
|
-
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
|
|
758
|
-
fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
|
|
759
|
-
const exec = vi.fn();
|
|
760
|
-
init(dest, { pkgRoot, execFileSync: exec });
|
|
761
|
-
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
762
|
-
["install"],
|
|
763
|
-
["run", "rulesync:generate"],
|
|
764
|
-
["run", "rulesync:check"],
|
|
765
|
-
]);
|
|
766
|
-
});
|
|
767
|
-
|
|
768
1143
|
it("auto-skip globals when AGENTS.md already has a grunt sentinel", () => {
|
|
769
1144
|
const pkgRoot = stubPkgRoot();
|
|
770
1145
|
const dest = tmp("grunt-auto-sent-");
|
|
@@ -781,43 +1156,75 @@ describe("init", () => {
|
|
|
781
1156
|
]);
|
|
782
1157
|
});
|
|
783
1158
|
|
|
784
|
-
it("re-run
|
|
1159
|
+
it("re-run alreadyInited unmarked still merges live files; stale *.grunt.md unused", () => {
|
|
785
1160
|
const pkgRoot = stubPkgRoot();
|
|
786
1161
|
const dest = tmp("grunt-rerun-nosent-");
|
|
787
1162
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
788
1163
|
fs.writeFileSync(path.join(dest, "AGENTS.md"), "hand-written consumer doc\n");
|
|
789
|
-
fs.mkdirSync(path.join(dest, "
|
|
790
|
-
fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
|
|
1164
|
+
fs.mkdirSync(path.join(dest, ".rulesync"), { recursive: true });
|
|
791
1165
|
fs.writeFileSync(path.join(dest, "CLAUDE.grunt.md"), "stale side");
|
|
792
1166
|
const exec = vi.fn();
|
|
793
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
794
1167
|
init(dest, { pkgRoot, execFileSync: exec });
|
|
795
|
-
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
796
|
-
|
|
1168
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
1169
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
1170
|
+
);
|
|
1171
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
1172
|
+
"<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
1173
|
+
);
|
|
797
1174
|
expect(fs.readFileSync(path.join(dest, "CLAUDE.grunt.md"), "utf8")).toBe("stale side");
|
|
798
1175
|
expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
|
|
799
|
-
expect(logSpy.mock.calls.filter((c) => String(c[0]).includes("lack markers"))).toHaveLength(1);
|
|
800
|
-
logSpy.mockRestore();
|
|
801
1176
|
});
|
|
802
1177
|
|
|
803
|
-
it("first init without sentinel
|
|
1178
|
+
it("first init without sentinel prepends grunt and keeps user bottom", () => {
|
|
804
1179
|
const pkgRoot = stubPkgRoot();
|
|
805
1180
|
const dest = tmp("grunt-first-nosent-");
|
|
806
1181
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
807
1182
|
fs.writeFileSync(path.join(dest, "AGENTS.md"), "hand-written consumer doc\n");
|
|
808
1183
|
const exec = vi.fn();
|
|
809
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
810
1184
|
init(dest, { pkgRoot, execFileSync: exec });
|
|
811
|
-
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
812
|
-
|
|
813
|
-
|
|
1185
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
1186
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
1187
|
+
);
|
|
1188
|
+
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
1189
|
+
expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
|
|
814
1190
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
815
1191
|
["install"],
|
|
816
1192
|
["run", "rulesync:generate"],
|
|
817
1193
|
["run", "sync:globals:apply"],
|
|
818
1194
|
["run", "rulesync:check"],
|
|
819
1195
|
]);
|
|
820
|
-
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it("generate clobber after merge keeps user bottom (idempotent re-init)", () => {
|
|
1199
|
+
const pkgRoot = stubPkgRoot();
|
|
1200
|
+
const dest = tmp("grunt-gen-clobber-");
|
|
1201
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "consumer agents\n");
|
|
1202
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "consumer claude\n");
|
|
1203
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "consumer gemini\n");
|
|
1204
|
+
const exec = vi.fn((_cmd, args) => {
|
|
1205
|
+
if (Array.isArray(args) && args.includes("rulesync:generate")) {
|
|
1206
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
1207
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
|
|
1208
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
init(dest, { pkgRoot, execFileSync: exec });
|
|
1212
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
1213
|
+
"<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\nconsumer agents\n",
|
|
1214
|
+
);
|
|
1215
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
1216
|
+
"<!-- grunt:begin -->\ngenerated claude\n<!-- grunt:end -->\nconsumer claude\n",
|
|
1217
|
+
);
|
|
1218
|
+
expect(fs.readFileSync(path.join(dest, "GEMINI.md"), "utf8")).toBe(
|
|
1219
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\nconsumer gemini\n",
|
|
1220
|
+
);
|
|
1221
|
+
init(dest, { pkgRoot, execFileSync: vi.fn() });
|
|
1222
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
1223
|
+
"<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\nconsumer agents\n",
|
|
1224
|
+
);
|
|
1225
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8").split("consumer agents").length).toBe(
|
|
1226
|
+
2,
|
|
1227
|
+
);
|
|
821
1228
|
});
|
|
822
1229
|
|
|
823
1230
|
it("onPhase merge then npm phases stop before exec", () => {
|
|
@@ -871,6 +1278,14 @@ describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
|
|
|
871
1278
|
expect(shouldAutoSkipGlobals(dest)).toBe(false);
|
|
872
1279
|
});
|
|
873
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
|
+
|
|
874
1289
|
it("sentinel auto-skips globals", () => {
|
|
875
1290
|
const dest = tmp("inspect-sent-");
|
|
876
1291
|
fs.writeFileSync(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Multi-agent orchestrator SoT
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|
|
7
7
|
"grunt",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"scripts/check-globals.mjs",
|
|
28
28
|
"scripts/emit-agent-shell-tools.mjs",
|
|
29
29
|
"scripts/emit-gemini.mjs",
|
|
30
|
+
"scripts/guarded-roots.mjs",
|
|
30
31
|
"scripts/emit-mcp-policy.mjs",
|
|
31
32
|
"scripts/gate-fat-tools.mjs",
|
|
32
33
|
"scripts/hooks-union.mjs",
|
|
@@ -38,7 +39,6 @@
|
|
|
38
39
|
"scripts/scrub-spawn-prompt.mjs",
|
|
39
40
|
"scripts/scrub-text-lib.mjs",
|
|
40
41
|
"scripts/sync-global-settings.mjs",
|
|
41
|
-
"scripts/telemetry.mjs",
|
|
42
42
|
"scripts/browser.mjs",
|
|
43
43
|
"scripts/doctor.mjs",
|
|
44
44
|
"scripts/scrub-text",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"CLAUDE.md",
|
|
52
52
|
".mcp.json",
|
|
53
53
|
"README.md",
|
|
54
|
-
"LICENSE"
|
|
54
|
+
"LICENSE",
|
|
55
|
+
"CHANGELOG.md"
|
|
55
56
|
],
|
|
56
57
|
"type": "module",
|
|
57
58
|
"publishConfig": {
|
|
@@ -61,9 +62,12 @@
|
|
|
61
62
|
"@clack/prompts": "^0.11.0"
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
|
-
"rulesync:generate": "
|
|
65
|
-
"rulesync:
|
|
66
|
-
"rulesync:
|
|
65
|
+
"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
|
+
"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
|
+
"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",
|
|
67
71
|
"hooks:union": "node scripts/hooks-union.mjs",
|
|
68
72
|
"hooks:check": "node scripts/hooks-union.mjs --check",
|
|
69
73
|
"doctor": "node ./scripts/doctor.mjs",
|