@lovrozagar/grunt 0.3.11 → 0.4.1
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 +3 -2
- package/.agents/agents/implementer.md +4 -2
- package/.agents/agents/orchestrator.md +19 -12
- package/.agents/agents/thinker.md +7 -2
- package/.agents/skills/browser/SKILL.md +17 -0
- package/.agents/skills/explain/SKILL.md +2 -0
- package/.agents/skills/handoff/SKILL.md +3 -3
- package/.agents/skills/pickup/SKILL.md +64 -0
- package/.agents/skills/solo/SKILL.md +2 -2
- package/.claude/agents/grunt.md +3 -2
- package/.claude/agents/implementer.md +6 -2
- package/.claude/agents/orchestrator.md +19 -12
- package/.claude/agents/thinker.md +9 -2
- package/.claude/skills/browser/SKILL.md +17 -0
- package/.claude/skills/explain/SKILL.md +2 -0
- package/.claude/skills/handoff/SKILL.md +3 -3
- package/.claude/skills/pickup/SKILL.md +64 -0
- package/.claude/skills/solo/SKILL.md +2 -2
- package/.codex/agents/grunt.toml +3 -2
- package/.codex/agents/implementer.toml +4 -2
- package/.codex/agents/orchestrator.toml +19 -12
- package/.codex/agents/thinker.toml +7 -2
- package/.grok/agents/grunt.md +3 -2
- package/.grok/agents/implementer.md +4 -2
- package/.grok/agents/orchestrator.md +19 -12
- package/.grok/agents/thinker.md +7 -2
- package/.grok/hooks/orchestrate-parent.js +26 -10
- package/.grok/skills/browser/SKILL.md +17 -0
- package/.grok/skills/explain/SKILL.md +2 -0
- package/.grok/skills/handoff/SKILL.md +3 -3
- package/.grok/skills/pickup/SKILL.md +64 -0
- package/.grok/skills/solo/SKILL.md +2 -2
- package/.rulesync/reference/browser.md +59 -0
- package/.rulesync/reference/cascade.md +13 -6
- package/.rulesync/reference/hooks.md +2 -2
- package/.rulesync/reference/map.md +6 -3
- package/.rulesync/reference/output.md +38 -1
- package/.rulesync/reference/rtk.md +4 -1
- package/.rulesync/rules/CLAUDE.md +18 -11
- package/.rulesync/rules/overview.md +18 -11
- package/.rulesync/skills/browser/SKILL.md +19 -0
- package/.rulesync/skills/explain/SKILL.md +2 -0
- package/.rulesync/skills/handoff/SKILL.md +3 -3
- package/.rulesync/skills/pickup/SKILL.md +64 -0
- package/.rulesync/skills/solo/SKILL.md +2 -2
- package/.rulesync/subagents/grunt.md +3 -2
- package/.rulesync/subagents/implementer.md +4 -2
- package/.rulesync/subagents/orchestrator.md +19 -12
- package/.rulesync/subagents/thinker.md +7 -2
- package/AGENTS.md +20 -11
- package/CLAUDE.md +20 -11
- package/README.md +40 -10
- package/cli/grunt.mjs +2 -2
- package/cli/grunt.test.ts +3 -3
- package/cli/init.mjs +196 -26
- package/cli/init.test.ts +444 -21
- package/package.json +11 -4
- package/scripts/browser.mjs +764 -0
- package/scripts/doctor.mjs +252 -0
- package/scripts/emit-gemini.mjs +7 -1
- package/scripts/guarded-roots.mjs +83 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Multi-agent orchestrator SoT — deep-merge rulesync trees product scripts and g
|
|
|
5
5
|
OSS drop-in; merges existing configs; switches default provider flow to Grunt.
|
|
6
6
|
|
|
7
7
|
- [Install](#install)
|
|
8
|
+
- [Prerequisites](#prerequisites)
|
|
8
9
|
- [Usage](#usage)
|
|
9
10
|
- [CLI](#cli)
|
|
10
11
|
- [Init](#init)
|
|
@@ -33,6 +34,29 @@ npx @lovrozagar/grunt
|
|
|
33
34
|
- Do not `npm test` as a consumer
|
|
34
35
|
- Package: `@lovrozagar/grunt` `0.3.9` MIT · https://github.com/lovrozagar/grunt
|
|
35
36
|
|
|
37
|
+
## Prerequisites
|
|
38
|
+
|
|
39
|
+
All OS. Print-only. Never auto-install.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
npx grunt doctor
|
|
43
|
+
node scripts/doctor.mjs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Exit 1 if any required missing; 0 if all required ok. Optional `gh` reported only.
|
|
47
|
+
|
|
48
|
+
| tool | required | install |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| node ≥22 + npm | yes | https://nodejs.org (≥22) · nvm / OS pkg · win `winget install OpenJS.NodeJS.LTS` |
|
|
51
|
+
| git | yes | linux `sudo apt install git` · mac `brew install git` · win `winget install Git.Git` |
|
|
52
|
+
| rtk | yes | linux/mac `curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh \| sh` or `brew install rtk` · win release zip `rtk.exe` on PATH or WSL curl ([docs](https://www.rtk-ai.app/docs/getting-started/installation/)) |
|
|
53
|
+
| rulesync | yes | `npm i -D rulesync` / npx (PATH or npx-able) |
|
|
54
|
+
| lightpanda | yes | `curl -fsSL https://pkg.lightpanda.io/install.sh \| bash` · mac `brew tap lightpanda-io/browser` · win WSL only |
|
|
55
|
+
| chromium-family | yes | linux `sudo apt install chromium` · mac `brew install --cask chromium` or `google-chrome` · win `winget install Google.Chrome` / `Microsoft.Edge` + PATH |
|
|
56
|
+
| gh | no | report only |
|
|
57
|
+
|
|
58
|
+
Rulesync schema doctor is separate: `npm run rulesync:doctor`.
|
|
59
|
+
|
|
36
60
|
## Usage
|
|
37
61
|
|
|
38
62
|
- TTY no command → menu (init default; generate check sync-globals purge-mcps doctor help quit)
|
|
@@ -48,7 +72,7 @@ npx @lovrozagar/grunt
|
|
|
48
72
|
- `check` → `npm run rulesync:check`
|
|
49
73
|
- `sync-globals` → `npm run sync:globals` (dry-run); `--apply` → `sync:globals:apply`
|
|
50
74
|
- `purge-mcps` → `npm run purge:global-mcps` (dry-run); `--apply` → `purge:global-mcps:apply`
|
|
51
|
-
- `doctor` → `npm run rulesync:doctor`
|
|
75
|
+
- `doctor` → `npm run doctor` (`node scripts/doctor.mjs`). Rulesync schema: `npm run rulesync:doctor`
|
|
52
76
|
- `help`
|
|
53
77
|
- `version`
|
|
54
78
|
|
|
@@ -81,19 +105,25 @@ SoT: `.rulesync/subagents/{orchestrator grunt implementer thinker}.md`
|
|
|
81
105
|
|
|
82
106
|
Emit: `.claude/` `.grok/` `.agents/` `.gemini/`
|
|
83
107
|
|
|
84
|
-
- **orchestrator** (parent) — always spawn+prompt; user-facing
|
|
108
|
+
- **orchestrator** (parent) — always spawn+prompt; user-facing `[orchestrator]:` (or child role tag) one-line recap; advise leftover numbered pick each on own line after that recap (do not cram `1. Implement 2. Tweak` onto the recap line). `/parent` one-turn; `/handoff` writes `.tmp/grunt/handoffs/{serial}-{slug}-{stamp}.md`; `/pickup` spawn-first pickup (inverse of `/handoff`; not a mode). `/solo` session escape; `/cascade` restores it. Small/low router. First token spawn. No parent Read/Bash/Grep. Does not implement, plan, or fetch world facts
|
|
85
109
|
- **grunt** — tools: facts/search/exec/git/web/test/low-reason mechanical write. Isolation `verdict:`. Never feature solution. Never spawn. World fact: `job: web`
|
|
86
110
|
- **implementer** — write already-defined solution on allowlisted paths. TDD when spec/plan says tests. Validate + sim after write. Fat dumps via `need:`. Never spawn. Never plan. Id is **implementer** (not Implementor)
|
|
87
|
-
- **thinker** — plan/
|
|
111
|
+
- **thinker** — think/plan/advise/recommend/how/why/explain; unsure→thinker; cheap false+; edge cases. Read-only named-file Read of prompt SSOT; trees/search/exec/web/test → `need:`. Never spawn. No bash. Never implement
|
|
88
112
|
|
|
89
113
|
Children never spawn. Spawn only grunt|implementer|thinker. Omit model on spawn; frontmatter on agent files picks haiku/sonnet/opus vs grok-4.5 / grok-4.6 / other hosts. Voice: `.rulesync/reference/output.md`. Protocol: `.rulesync/reference/cascade.md`. `AGENTS.md`/`CLAUDE.md` spawn-first. `GEMINI.md` → `@AGENTS.md`. Goals: synced configs across grok build claude code codex gemini cli antigravity; max situational speed; superterse token savings; max/min reasoning by role.
|
|
90
114
|
|
|
115
|
+
## Browser
|
|
116
|
+
|
|
117
|
+
Lightpanda-first session CLI: `node scripts/browser.mjs nav|snap|click|fill|shot|pdf|stop|doctor|ensure`. Zero MCP. Zero env knobs. Chromium only for `shot`/`pdf`/`trace`, Windows, missing Lightpanda, one probe-fail replay, or paint hosts. Session: `.tmp/grunt/browser/`. Spec: [`.rulesync/reference/browser.md`](.rulesync/reference/browser.md).
|
|
118
|
+
|
|
119
|
+
`node scripts/browser.mjs doctor` (alias `ensure`) runs the unified doctor (`scripts/doctor.mjs`). Install engines: [Prerequisites](#prerequisites).
|
|
120
|
+
|
|
91
121
|
## Skills
|
|
92
122
|
|
|
93
123
|
Present under `.claude` / `.rulesync` / `.agents`:
|
|
94
124
|
|
|
95
|
-
- `cascade` `commit` `commit-and-push` `commit-push` `commit-push-deploy` `commit-push-release` `explain` `handoff` `parent` `solo`
|
|
96
|
-
- Grok-only: `write-plan` `implement-plan` (`/write-plan` plan-only → `next: /implement-plan`)
|
|
125
|
+
- `browser` `cascade` `commit` `commit-and-push` `commit-push` `commit-push-deploy` `commit-push-release` `explain` `handoff` `pickup` `parent` `solo`
|
|
126
|
+
- Grok-only: `write-plan` `implement-plan` (`/write-plan` plan-only → `next: /implement-plan`; advise unlock = pick **1. Implement**, not `ok`/`yes`/`go`; slash `/implement-plan` still valid alias)
|
|
97
127
|
|
|
98
128
|
## Generate
|
|
99
129
|
|
|
@@ -104,7 +134,7 @@ Pipeline (no `-t geminicli`):
|
|
|
104
134
|
3. `emit-gemini.mjs` — `GEMINI.md` `.gemini/agents/{id}/agent.md` MCP `.gemini/settings.json`
|
|
105
135
|
4. `emit-agent-shell-tools` — Claude grunt body `Bash`; other hosts `run_terminal_command` (hooks-union)
|
|
106
136
|
|
|
107
|
-
`check`
|
|
137
|
+
`check` = rulesync check. `doctor` = unified prereqs. Schema: `npm run rulesync:doctor`.
|
|
108
138
|
|
|
109
139
|
Emit writes other-CLI trees from `.rulesync` for the **next** process of that CLI. Not a live hop into another host.
|
|
110
140
|
|
|
@@ -132,7 +162,7 @@ Same topology as the session recap; every legal edge labeled.
|
|
|
132
162
|
```
|
|
133
163
|
USER
|
|
134
164
|
│
|
|
135
|
-
│ session in/out (parent only; tagged one-line recap)
|
|
165
|
+
│ session in/out (parent only; tagged one-line recap; advise leftover 1./2. own lines)
|
|
136
166
|
│ TUI local; completion tokens from Model API (mixed)
|
|
137
167
|
▼
|
|
138
168
|
┌──────────────────────── CLI HOST (this process) ────────────────────────┐
|
|
@@ -221,7 +251,7 @@ Implementer or thinker that still needs a fat dump **stops** on a `need:` JSON l
|
|
|
221
251
|
| node | inside host? | local vs AI-server | notes |
|
|
222
252
|
| --- | --- | --- | --- |
|
|
223
253
|
| User | no | mixed | Speaks only to parent session. Never a child edge. |
|
|
224
|
-
| Session in/out / TUI | yes | mixed | TUI is local; recap tokens come from the model. Legal `[role]:` one-line
|
|
254
|
+
| Session in/out / TUI | yes | mixed | TUI is local; recap tokens come from the model. Legal `[role]:` one-line recap; advise leftover numbered pick each on own line after (not crammed onto the recap line). |
|
|
225
255
|
| CLI host process | yes (is the box) | local process | Grok Build / Claude Code / Codex / Gemini / Antigravity. Not a peer of another CLI. |
|
|
226
256
|
| Parent orchestrator | yes | AI-server **turn** | Spawn-first low router. No parent Read/Bash/Grep. Lives in this session, not a sidecar. |
|
|
227
257
|
| `grunt` sibling | yes | AI-server **turn** | Facts/tools/mechanical write. Never spawn. Never feature solution. |
|
|
@@ -288,7 +318,7 @@ Repo-relative (repository root):
|
|
|
288
318
|
|
|
289
319
|
## Layout
|
|
290
320
|
|
|
291
|
-
Published (`package.json` `files`): `bin/grunt.js` `cli` `scripts/check-globals.mjs` `scripts/emit-agent-shell-tools.mjs` `scripts/emit-gemini.mjs` `scripts/emit-mcp-policy.mjs` `scripts/gate-fat-tools.mjs` `scripts/hooks-union.mjs` `scripts/grunt-job.mjs` `scripts/parse-need.mjs` `scripts/persist-handoff.mjs` `scripts/persist-plan.mjs` `scripts/purge-global-mcps.mjs` `scripts/scrub-spawn-prompt.mjs` `scripts/scrub-text-lib.mjs` `scripts/sync-global-settings.mjs` `scripts/telemetry.mjs` `scripts/scrub-text` `.rulesync` `.grok` `.codex` `.claude` `.agents` `AGENTS.md` `CLAUDE.md` `.mcp.json` `README.md` `LICENSE`
|
|
321
|
+
Published (`package.json` `files`): `bin/grunt.js` `cli` `scripts/check-globals.mjs` `scripts/emit-agent-shell-tools.mjs` `scripts/emit-gemini.mjs` `scripts/emit-mcp-policy.mjs` `scripts/gate-fat-tools.mjs` `scripts/hooks-union.mjs` `scripts/grunt-job.mjs` `scripts/parse-need.mjs` `scripts/persist-handoff.mjs` `scripts/persist-plan.mjs` `scripts/purge-global-mcps.mjs` `scripts/scrub-spawn-prompt.mjs` `scripts/scrub-text-lib.mjs` `scripts/sync-global-settings.mjs` `scripts/telemetry.mjs` `scripts/browser.mjs` `scripts/doctor.mjs` `scripts/scrub-text` `.rulesync` `.grok` `.codex` `.claude` `.agents` `AGENTS.md` `CLAUDE.md` `.mcp.json` `README.md` `LICENSE`
|
|
292
322
|
|
|
293
323
|
No `scripts/*.test.ts` `scripts/fixtures/` `docs/` `coverage/` `vitest.config.ts` in `files`.
|
|
294
324
|
|
|
@@ -319,7 +349,7 @@ npm i && npm test
|
|
|
319
349
|
|
|
320
350
|
Keep these four flows only:
|
|
321
351
|
|
|
322
|
-
1. “Create me a react weather app” → thinker plan → implementer write →
|
|
352
|
+
1. “Create me a react weather app” → thinker plan/recap-stop + pick 1.Implement 2.Tweak → user picks 1 or `/implement-plan` → implementer write → recap
|
|
323
353
|
2. “What is 2+2” → grunt → `[grunt]:` echo
|
|
324
354
|
3. Marvel theatrical next → grunt `job:web` → recap
|
|
325
355
|
4. `.logs` 3/6/2021 tag `framework bug` → grunt local search → recap
|
package/cli/grunt.mjs
CHANGED
|
@@ -17,7 +17,7 @@ Commands:
|
|
|
17
17
|
check npm run rulesync:check
|
|
18
18
|
sync-globals npm run sync:globals (dry-run; --apply to write)
|
|
19
19
|
purge-mcps npm run purge:global-mcps (dry-run; --apply to write)
|
|
20
|
-
doctor npm run
|
|
20
|
+
doctor npm run doctor
|
|
21
21
|
help Show this help
|
|
22
22
|
version Print package version
|
|
23
23
|
|
|
@@ -163,7 +163,7 @@ async function dispatch(cmd, flags, interactive) {
|
|
|
163
163
|
return
|
|
164
164
|
}
|
|
165
165
|
if (cmd === "doctor") {
|
|
166
|
-
npmRun("
|
|
166
|
+
npmRun("doctor")
|
|
167
167
|
return
|
|
168
168
|
}
|
|
169
169
|
process.stdout.write(USAGE)
|
package/cli/grunt.test.ts
CHANGED
|
@@ -37,7 +37,7 @@ Commands:
|
|
|
37
37
|
check npm run rulesync:check
|
|
38
38
|
sync-globals npm run sync:globals (dry-run; --apply to write)
|
|
39
39
|
purge-mcps npm run purge:global-mcps (dry-run; --apply to write)
|
|
40
|
-
doctor npm run
|
|
40
|
+
doctor npm run doctor
|
|
41
41
|
help Show this help
|
|
42
42
|
version Print package version
|
|
43
43
|
|
|
@@ -236,10 +236,10 @@ describe("start", () => {
|
|
|
236
236
|
);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
-
it("doctor npm-runs
|
|
239
|
+
it("doctor npm-runs doctor", async () => {
|
|
240
240
|
process.argv = ["node", "grunt", "doctor"];
|
|
241
241
|
await start();
|
|
242
|
-
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "
|
|
242
|
+
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "doctor"], {
|
|
243
243
|
cwd: process.cwd(),
|
|
244
244
|
stdio: "inherit",
|
|
245
245
|
});
|
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",
|
|
@@ -21,17 +22,23 @@ const PRODUCT_SCRIPTS = [
|
|
|
21
22
|
"scrub-text-lib.mjs",
|
|
22
23
|
"sync-global-settings.mjs",
|
|
23
24
|
"telemetry.mjs",
|
|
25
|
+
"browser.mjs",
|
|
26
|
+
"doctor.mjs",
|
|
24
27
|
"scrub-text",
|
|
25
28
|
]
|
|
26
29
|
|
|
27
30
|
const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"]
|
|
28
31
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"]
|
|
32
|
+
export const GUARDED_ROOT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]
|
|
29
33
|
// .mcp.json is intentionally not copied here: scripts/emit-mcp-policy.mjs
|
|
30
34
|
// (run via `rulesync:generate`, see mergeClaudeSettings ordering) owns and
|
|
31
35
|
// merges it, so a plain-copy would just be clobbered/redundant.
|
|
32
36
|
const OWNED_HOOK_FILES = ["scrub-spawn-prompt.mjs", "gate-fat-tools.mjs", "orchestrate-parent.js"]
|
|
33
|
-
const SENTINEL_BEGIN = "<!-- grunt:begin -->"
|
|
34
|
-
const SENTINEL_END = "<!-- grunt:end -->"
|
|
37
|
+
export const SENTINEL_BEGIN = "<!-- grunt:begin -->"
|
|
38
|
+
export const SENTINEL_END = "<!-- grunt:end -->"
|
|
39
|
+
export const MAX_GUARDED_MARKDOWN_BYTES = 2 * 1024 * 1024
|
|
40
|
+
const GRUNT_REGION_RE = /<!-- grunt:begin -->\r?\n?[\s\S]*?<!-- grunt:end -->\r?\n?/g
|
|
41
|
+
const GRUNT_INTERIOR_RE = /<!-- grunt:begin -->\r?\n?([\s\S]*?)<!-- grunt:end -->/g
|
|
35
42
|
|
|
36
43
|
function sortKeys(obj) {
|
|
37
44
|
return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]))
|
|
@@ -77,33 +84,199 @@ export function shouldAutoSkipGlobals(dest) {
|
|
|
77
84
|
return destHasGruntSentinel(dest) || fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
function detectNewline(text) {
|
|
88
|
+
return String(text).includes("\r\n") ? "\r\n" : "\n"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toNewline(text, nl) {
|
|
92
|
+
return String(text).replace(/\r\n/g, "\n").replace(/\n/g, nl)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function trimTrailingNewlines(text) {
|
|
96
|
+
return String(text).replace(/(?:\r?\n)+$/, "")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normEq(a, b) {
|
|
100
|
+
return trimTrailingNewlines(String(a).replace(/\r\n/g, "\n")) ===
|
|
101
|
+
trimTrailingNewlines(String(b).replace(/\r\n/g, "\n"))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cloneRe(re) {
|
|
105
|
+
return new RegExp(re.source, re.flags)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function inspectGuardedBuffer(buf) {
|
|
109
|
+
if (buf.length > MAX_GUARDED_MARKDOWN_BYTES) return "huge"
|
|
110
|
+
if (buf.includes(0)) return "binary"
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function extractUserMarkdown(text) {
|
|
115
|
+
return String(text).replace(cloneRe(GRUNT_REGION_RE), "")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function extractGruntBody(text) {
|
|
119
|
+
const parts = []
|
|
120
|
+
const re = cloneRe(GRUNT_INTERIOR_RE)
|
|
121
|
+
let m
|
|
122
|
+
while ((m = re.exec(String(text)))) {
|
|
123
|
+
parts.push(trimTrailingNewlines(m[1]))
|
|
124
|
+
}
|
|
125
|
+
if (!parts.length) return null
|
|
126
|
+
return parts.join("\n")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function userRemainderIsBlank(user) {
|
|
130
|
+
return String(user).replace(/\r\n/g, "\n").replace(/\n/g, "") === ""
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function composeGuardedMarkdown(gruntBody, userRemainder, nl = "\n") {
|
|
134
|
+
const body = toNewline(trimTrailingNewlines(gruntBody), nl)
|
|
135
|
+
const block = `${SENTINEL_BEGIN}${nl}${body}${nl}${SENTINEL_END}${nl}`
|
|
136
|
+
const user = userRemainder == null ? "" : String(userRemainder)
|
|
137
|
+
if (!user || userRemainderIsBlank(user)) return block
|
|
138
|
+
return block + toNewline(user, nl)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function mergeGuardedContent(existingText, gruntBody) {
|
|
142
|
+
const existing = existingText == null ? "" : String(existingText)
|
|
143
|
+
const nl = existing.includes("\r\n") || String(gruntBody).includes("\r\n")
|
|
144
|
+
? detectNewline(existing || gruntBody)
|
|
145
|
+
: "\n"
|
|
146
|
+
if (!existing) return composeGuardedMarkdown(gruntBody, "", nl)
|
|
147
|
+
const hadSentinel = existing.includes(SENTINEL_BEGIN) && existing.includes(SENTINEL_END)
|
|
148
|
+
const user = extractUserMarkdown(existing)
|
|
149
|
+
if (!hadSentinel && normEq(existing, gruntBody)) {
|
|
150
|
+
return composeGuardedMarkdown(gruntBody, "", nl)
|
|
151
|
+
}
|
|
152
|
+
return composeGuardedMarkdown(gruntBody, user, nl)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function writeMergedGuardedFile(destPath, gruntBody) {
|
|
156
|
+
if (!fs.existsSync(destPath)) {
|
|
157
|
+
fs.writeFileSync(destPath, mergeGuardedContent("", gruntBody))
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
const buf = fs.readFileSync(destPath)
|
|
161
|
+
if (inspectGuardedBuffer(buf)) return "aborted-unsafe"
|
|
162
|
+
fs.writeFileSync(destPath, mergeGuardedContent(buf.toString("utf8"), gruntBody))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function guardedMarkdownDrift(destPath, gruntBody) {
|
|
166
|
+
if (!fs.existsSync(destPath)) return true
|
|
167
|
+
const buf = fs.readFileSync(destPath)
|
|
168
|
+
if (inspectGuardedBuffer(buf)) return true
|
|
169
|
+
const text = buf.toString("utf8")
|
|
170
|
+
const interior = extractGruntBody(text)
|
|
171
|
+
if (interior == null) return !normEq(text, gruntBody)
|
|
172
|
+
return !normEq(interior, gruntBody)
|
|
173
|
+
}
|
|
174
|
+
|
|
80
175
|
export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = false } = {}) {
|
|
176
|
+
void alreadyInited
|
|
81
177
|
const src = path.join(pkgRoot, file)
|
|
82
178
|
const d = path.join(dest, file)
|
|
83
179
|
if (samePath(src, d)) return
|
|
84
180
|
const srcText = fs.readFileSync(src, "utf8")
|
|
181
|
+
return writeMergedGuardedFile(d, srcText)
|
|
182
|
+
}
|
|
85
183
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
184
|
+
export function snapshotGuardedRoots(root) {
|
|
185
|
+
const snap = {}
|
|
186
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
187
|
+
const p = path.join(root, file)
|
|
188
|
+
if (!fs.existsSync(p)) continue
|
|
189
|
+
const buf = fs.readFileSync(p)
|
|
190
|
+
const unsafe = inspectGuardedBuffer(buf)
|
|
191
|
+
if (unsafe) {
|
|
192
|
+
snap[file] = { unsafe, rawBuf: buf }
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
const raw = buf.toString("utf8")
|
|
196
|
+
snap[file] = { raw, user: extractUserMarkdown(raw) }
|
|
89
197
|
}
|
|
198
|
+
return snap
|
|
199
|
+
}
|
|
90
200
|
|
|
91
|
-
|
|
92
|
-
const
|
|
201
|
+
export function remergeGuardedRoots(root, snap) {
|
|
202
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
203
|
+
const p = path.join(root, file)
|
|
204
|
+
const rec = snap[file]
|
|
205
|
+
if (rec?.unsafe) {
|
|
206
|
+
fs.writeFileSync(p, rec.rawBuf)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (!fs.existsSync(p)) {
|
|
210
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
const buf = fs.readFileSync(p)
|
|
214
|
+
if (inspectGuardedBuffer(buf)) {
|
|
215
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
const current = buf.toString("utf8")
|
|
219
|
+
if (rec && current === rec.raw) continue
|
|
220
|
+
const gruntBody = extractGruntBody(current) ?? current
|
|
221
|
+
const nl = detectNewline((rec && rec.raw) || current)
|
|
222
|
+
let user = rec ? rec.user : extractUserMarkdown(current)
|
|
223
|
+
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = ""
|
|
224
|
+
if (!rec && extractGruntBody(current) == null && normEq(current, gruntBody)) user = ""
|
|
225
|
+
fs.writeFileSync(p, composeGuardedMarkdown(gruntBody, user, nl))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
93
228
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
229
|
+
export function healGuardedRootFile(root, file, snap) {
|
|
230
|
+
if (!GUARDED_ROOT_FILES.includes(file)) return
|
|
231
|
+
const p = path.join(root, file)
|
|
232
|
+
const rec = snap[file]
|
|
233
|
+
if (!fs.existsSync(p)) return
|
|
234
|
+
if (rec?.unsafe) {
|
|
235
|
+
fs.writeFileSync(p, rec.rawBuf)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
const buf = fs.readFileSync(p)
|
|
239
|
+
if (inspectGuardedBuffer(buf)) {
|
|
240
|
+
if (rec?.raw != null) fs.writeFileSync(p, rec.raw)
|
|
99
241
|
return
|
|
100
242
|
}
|
|
243
|
+
const current = buf.toString("utf8")
|
|
244
|
+
const interior = extractGruntBody(current)
|
|
245
|
+
if (interior != null) {
|
|
246
|
+
snap[file] = { raw: current, user: extractUserMarkdown(current) }
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
if (rec && current === rec.raw) return
|
|
250
|
+
const gruntBody = current
|
|
251
|
+
const nl = detectNewline((rec && rec.raw) || current)
|
|
252
|
+
let user = rec ? rec.user : extractUserMarkdown(current)
|
|
253
|
+
if (rec && !rec.raw.includes(SENTINEL_BEGIN) && normEq(rec.raw, gruntBody)) user = ""
|
|
254
|
+
if (!rec && normEq(current, gruntBody)) user = ""
|
|
255
|
+
const out = composeGuardedMarkdown(gruntBody, user, nl)
|
|
256
|
+
fs.writeFileSync(p, out)
|
|
257
|
+
snap[file] = { raw: out, user }
|
|
258
|
+
}
|
|
101
259
|
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
260
|
+
export function withGuardedCheckInteriors(root, fn) {
|
|
261
|
+
const snap = snapshotGuardedRoots(root)
|
|
262
|
+
try {
|
|
263
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
264
|
+
const rec = snap[file]
|
|
265
|
+
if (!rec || rec.unsafe) continue
|
|
266
|
+
const body = extractGruntBody(rec.raw)
|
|
267
|
+
if (body == null) continue
|
|
268
|
+
const nl = detectNewline(rec.raw)
|
|
269
|
+
fs.writeFileSync(path.join(root, file), `${toNewline(trimTrailingNewlines(body), nl)}${nl}`)
|
|
270
|
+
}
|
|
271
|
+
return fn()
|
|
272
|
+
} finally {
|
|
273
|
+
for (const file of GUARDED_ROOT_FILES) {
|
|
274
|
+
const rec = snap[file]
|
|
275
|
+
if (!rec) continue
|
|
276
|
+
if (rec.unsafe) fs.writeFileSync(path.join(root, file), rec.rawBuf)
|
|
277
|
+
else fs.writeFileSync(path.join(root, file), rec.raw)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
107
280
|
}
|
|
108
281
|
|
|
109
282
|
export function mergeClaudeSettings(destRoot, pkgRoot) {
|
|
@@ -220,7 +393,6 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
220
393
|
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase } = {}) {
|
|
221
394
|
dest = path.resolve(dest)
|
|
222
395
|
const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
|
|
223
|
-
const alreadyInited = destAlreadyInited(dest)
|
|
224
396
|
const skipGlobalsApply =
|
|
225
397
|
applyGlobals === true ? false : skipGlobals || shouldAutoSkipGlobals(dest)
|
|
226
398
|
|
|
@@ -253,14 +425,8 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
253
425
|
}
|
|
254
426
|
}
|
|
255
427
|
|
|
256
|
-
let skippedSideFile = false
|
|
257
428
|
for (const file of GUARDED_MD_FILES) {
|
|
258
|
-
|
|
259
|
-
skippedSideFile = true
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (skippedSideFile) {
|
|
263
|
-
console.log("AGENTS.md/CLAUDE.md lack markers and were left alone")
|
|
429
|
+
mergeGuardedMarkdown(dest, pkgRoot, file)
|
|
264
430
|
}
|
|
265
431
|
|
|
266
432
|
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true })
|
|
@@ -289,9 +455,13 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
289
455
|
exec("npm", args, { cwd: dest, stdio: "inherit" })
|
|
290
456
|
}
|
|
291
457
|
runNpm("install", ["install"])
|
|
458
|
+
const guardedSnap = snapshotGuardedRoots(dest)
|
|
292
459
|
runNpm("generate", ["run", "rulesync:generate"])
|
|
460
|
+
remergeGuardedRoots(dest, guardedSnap)
|
|
293
461
|
if (!skipGlobalsApply) {
|
|
294
462
|
runNpm("sync-globals", ["run", "sync:globals:apply"])
|
|
295
463
|
}
|
|
296
|
-
|
|
464
|
+
withGuardedCheckInteriors(dest, () => {
|
|
465
|
+
runNpm("check", ["run", "rulesync:check"])
|
|
466
|
+
})
|
|
297
467
|
}
|