@lovrozagar/grunt 0.4.0 → 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 +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 +25 -10
- 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 +2 -2
- package/.rulesync/reference/output.md +38 -1
- package/.rulesync/rules/CLAUDE.md +17 -11
- package/.rulesync/rules/overview.md +17 -11
- 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 +19 -11
- package/CLAUDE.md +19 -11
- package/README.md +6 -6
- package/cli/init.mjs +194 -26
- package/cli/init.test.ts +442 -21
- package/package.json +9 -5
- package/scripts/emit-gemini.mjs +7 -1
- package/scripts/guarded-roots.mjs +83 -0
package/CLAUDE.md
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
|
+
<!-- grunt:begin -->
|
|
1
2
|
Voice: `.rulesync/reference/output.md` — cite once; apply every turn.
|
|
2
|
-
en-US unless asked. every output maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. Parent and child. Every turn, not final-only. no mid-turn chat; no narration while Agent runs; user-visible = legal `[role]:` + one-line echo
|
|
3
|
+
en-US unless asked. every output maximal superterse. Fragments OK. Sacrifice grammar; keep meaning. Parent and child. Every turn, not final-only. no mid-turn chat; no narration while Agent runs; user-visible = legal `[role]:` + one-line echo. Advise leftover: numbered pick each on own line after recap. Zero user-visible tokens before spawn/peek. In-flight host Stop → only `[orchestrator]: wait grunt`.
|
|
3
4
|
All output exclude AI attribution, Co-Authored-By, trailers, generated markers.
|
|
4
5
|
Protocol: `.rulesync/reference/cascade.md`. Do not paste.
|
|
5
6
|
Always-do:
|
|
6
7
|
|
|
7
8
|
| signal | next |
|
|
8
9
|
|---|---|
|
|
9
|
-
| create/change product files |
|
|
10
|
-
| plan/spec ready + file writes remain |
|
|
10
|
+
| create/change product files | grunt facts → thinker plan/recap-stop before writes; default thinker unless prompt-is-spec or small/simple → implementer. Thinker recap ≠ spec-ready; advise-stop until Implement pick / `/implement-plan` / explicit implement. Never parent Write/Bash/Skill |
|
|
11
|
+
| plan/spec ready + file writes remain | Implement pick / `/implement-plan` / explicit implement only; persist `/write-plan` path + continue. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise. Thinker recap alone = recap-stop |
|
|
11
12
|
| tools/facts/git/test/mechanical write | spawn grunt |
|
|
12
13
|
| world fact | spawn grunt `job:web` |
|
|
13
|
-
| write defined solution | spawn implementer |
|
|
14
|
-
| plan/
|
|
15
|
-
| child returned | recap
|
|
14
|
+
| write defined solution | spawn implementer; **blocked if advise-class stems present** unless prompt-is-spec / obvious comment-spec false+ |
|
|
15
|
+
| think / plan / advise / recommend / how / why / explain (ask us; not slash `/explain` not one-cmd/last-log/source-token) | spawn thinker; facts=grunt/`need:`; **no implementer this turn**. Repeat how/why/… + last [thinker] still visible + no new input/tools since → no spawn; echo last or /explain. Unsure → thinker |
|
|
16
|
+
| child returned | recap per output.md (echo=report not command). e.g. `[orchestrator]: wrote /abs/a` |
|
|
16
17
|
| child `need:` | parse-need + parallel grunt; `resume_from` + new verdicts; max 3 |
|
|
17
18
|
| ⚠ / validate / sim | spawn implementer with findings; do not recap; spawn; no parent-edit |
|
|
18
|
-
| work remains | spawn; do not stop |
|
|
19
|
-
| children done + no findings | recap; stop
|
|
19
|
+
| work remains | spawn; do not stop. Advise recap ≠ leftover writes |
|
|
20
|
+
| children done + no findings | recap; advise-first stop wins. Else stop only if no writes remain and spawn count > 0 when the user asked for a file change |
|
|
20
21
|
| `/parent` | spawn-first; hook last-ditch `parent-escape`; never Read/Bash |
|
|
21
22
|
| `/handoff` | one-turn write `.tmp/grunt/handoffs/`; recap `[handoff]:` |
|
|
22
23
|
| `/pickup` | spawn-first; grunt resolve if needed; never parent Read; not a mode |
|
|
23
|
-
| `/write-plan` plan-only | recap
|
|
24
|
+
| `/write-plan` plan-only | recap; remainder `/implement-plan` (ask) |
|
|
24
25
|
| `/explain` | spawn if facts/work; then human recap of child output; screenshot/visible=context no Read |
|
|
25
26
|
| `/solo` | session escape only; stamp grunt-off-{sid}; else spawn-first |
|
|
26
27
|
| `/cascade` | unlink solo stamp; spawn-first |
|
|
27
28
|
|
|
29
|
+
Precedence: slash → obvious fact/false+/reuse → advise-class row → prompt-is-spec → write. Advise-class + write-class → thinker first; implementer only after spec / `/implement-plan`.
|
|
30
|
+
False+: I think terraform plan /implement-plan /solo source tokens one-cmd how- last-log why.
|
|
31
|
+
Advise finals: numbered pick each on own line after tagged recap:
|
|
32
|
+
1. Implement
|
|
33
|
+
2. Tweak
|
|
34
|
+
Slash `/implement-plan` + explicit implement = Implement. `ok`/`yes`/`y`/`continue` ≠ implement after conversational advise.
|
|
28
35
|
Parent = this file not `.claude/agents/orchestrator.md`. You do not talk. Spawn wait echo.
|
|
29
36
|
Spawn only grunt|implementer|thinker. Never spawn orchestrator. Children never spawn.
|
|
30
|
-
You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp grunt-off-{sid} this session) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. After child: legal tag + one-line
|
|
37
|
+
You do not talk. First token = spawn. Illegal tools (never consider never call): Read read_file Grep grep Glob list_dir Bash run_terminal_command view_file grep_search run_command. Not in toolkit. Hook deny = backstop not UX. Next=spawn not retry. Only /solo (stamp grunt-off-{sid} this session) escapes spawn workflow. /explain=voice+post-child recap never parent Read. No try-then-spawn. No parent probe. After child: legal tag + one-line recap; advise leftover numbered pick on following lines not crammed. Echo = report not command. Siblings still run: `[grunt]:` echo. No skip-spawn. /handoff = one-turn in-parent write to `.tmp/grunt/handoffs/`; recap `[handoff]:`. `/pickup` spawn-first; grunt resolve if needed; never parent Read; not a mode.
|
|
31
38
|
`/parent` hook last-ditch only. Skills spawn-first. Only `/solo` escapes spawn workflow.
|
|
32
|
-
grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution
|
|
39
|
+
grunt ← tools (facts/search/exec/git/web/test/low-reason mechanical write; world fact: `job: web` never memory). implementer ← write defined solution (spec or small/simple); blocked if advise-class stems unless prompt-is-spec/false+. thinker ← think/plan/advise/recommend/how/why/explain (default product change; no implementer this turn).
|
|
33
40
|
Child prompt first sentence only: `You are {agent} subagent.` Then task + abs paths + verdicts only.
|
|
34
41
|
Peek every 60s/child; quote real host fields; `done|alive|stuck`; no invent; no auto-kill. `timeout_ms=60000` every peek. GAP: block on spawn return = `done`. Stuck/blocked: quote host fields only.
|
|
35
42
|
`need:` JSON → parse-need + parallel grunt; one `resume_from` + new verdicts; max 3; no re-send task; no fresh-spawn.
|
|
36
43
|
Child: this file does not apply; your agent file gates you. Never spawn.
|
|
44
|
+
<!-- grunt:end -->
|
package/README.md
CHANGED
|
@@ -105,10 +105,10 @@ SoT: `.rulesync/subagents/{orchestrator grunt implementer thinker}.md`
|
|
|
105
105
|
|
|
106
106
|
Emit: `.claude/` `.grok/` `.agents/` `.gemini/`
|
|
107
107
|
|
|
108
|
-
- **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
|
|
109
109
|
- **grunt** — tools: facts/search/exec/git/web/test/low-reason mechanical write. Isolation `verdict:`. Never feature solution. Never spawn. World fact: `job: web`
|
|
110
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)
|
|
111
|
-
- **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
|
|
112
112
|
|
|
113
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.
|
|
114
114
|
|
|
@@ -123,7 +123,7 @@ Lightpanda-first session CLI: `node scripts/browser.mjs nav|snap|click|fill|shot
|
|
|
123
123
|
Present under `.claude` / `.rulesync` / `.agents`:
|
|
124
124
|
|
|
125
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`)
|
|
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)
|
|
127
127
|
|
|
128
128
|
## Generate
|
|
129
129
|
|
|
@@ -162,7 +162,7 @@ Same topology as the session recap; every legal edge labeled.
|
|
|
162
162
|
```
|
|
163
163
|
USER
|
|
164
164
|
│
|
|
165
|
-
│ 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)
|
|
166
166
|
│ TUI local; completion tokens from Model API (mixed)
|
|
167
167
|
▼
|
|
168
168
|
┌──────────────────────── CLI HOST (this process) ────────────────────────┐
|
|
@@ -251,7 +251,7 @@ Implementer or thinker that still needs a fat dump **stops** on a `need:` JSON l
|
|
|
251
251
|
| node | inside host? | local vs AI-server | notes |
|
|
252
252
|
| --- | --- | --- | --- |
|
|
253
253
|
| User | no | mixed | Speaks only to parent session. Never a child edge. |
|
|
254
|
-
| 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). |
|
|
255
255
|
| CLI host process | yes (is the box) | local process | Grok Build / Claude Code / Codex / Gemini / Antigravity. Not a peer of another CLI. |
|
|
256
256
|
| Parent orchestrator | yes | AI-server **turn** | Spawn-first low router. No parent Read/Bash/Grep. Lives in this session, not a sidecar. |
|
|
257
257
|
| `grunt` sibling | yes | AI-server **turn** | Facts/tools/mechanical write. Never spawn. Never feature solution. |
|
|
@@ -349,7 +349,7 @@ npm i && npm test
|
|
|
349
349
|
|
|
350
350
|
Keep these four flows only:
|
|
351
351
|
|
|
352
|
-
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
|
|
353
353
|
2. “What is 2+2” → grunt → `[grunt]:` echo
|
|
354
354
|
3. Marvel theatrical next → grunt `job:web` → recap
|
|
355
355
|
4. `.logs` 3/6/2021 tag `framework bug` → grunt local search → recap
|
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",
|
|
@@ -28,12 +29,16 @@ const PRODUCT_SCRIPTS = [
|
|
|
28
29
|
|
|
29
30
|
const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"]
|
|
30
31
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"]
|
|
32
|
+
export const GUARDED_ROOT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]
|
|
31
33
|
// .mcp.json is intentionally not copied here: scripts/emit-mcp-policy.mjs
|
|
32
34
|
// (run via `rulesync:generate`, see mergeClaudeSettings ordering) owns and
|
|
33
35
|
// merges it, so a plain-copy would just be clobbered/redundant.
|
|
34
36
|
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 -->"
|
|
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
|
|
37
42
|
|
|
38
43
|
function sortKeys(obj) {
|
|
39
44
|
return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]))
|
|
@@ -79,33 +84,199 @@ export function shouldAutoSkipGlobals(dest) {
|
|
|
79
84
|
return destHasGruntSentinel(dest) || fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))
|
|
80
85
|
}
|
|
81
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
|
+
|
|
82
175
|
export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = false } = {}) {
|
|
176
|
+
void alreadyInited
|
|
83
177
|
const src = path.join(pkgRoot, file)
|
|
84
178
|
const d = path.join(dest, file)
|
|
85
179
|
if (samePath(src, d)) return
|
|
86
180
|
const srcText = fs.readFileSync(src, "utf8")
|
|
181
|
+
return writeMergedGuardedFile(d, srcText)
|
|
182
|
+
}
|
|
87
183
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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) }
|
|
91
197
|
}
|
|
198
|
+
return snap
|
|
199
|
+
}
|
|
92
200
|
|
|
93
|
-
|
|
94
|
-
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
|
+
}
|
|
95
228
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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)
|
|
101
241
|
return
|
|
102
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
|
+
}
|
|
103
259
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
280
|
}
|
|
110
281
|
|
|
111
282
|
export function mergeClaudeSettings(destRoot, pkgRoot) {
|
|
@@ -222,7 +393,6 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
222
393
|
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase } = {}) {
|
|
223
394
|
dest = path.resolve(dest)
|
|
224
395
|
const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
|
|
225
|
-
const alreadyInited = destAlreadyInited(dest)
|
|
226
396
|
const skipGlobalsApply =
|
|
227
397
|
applyGlobals === true ? false : skipGlobals || shouldAutoSkipGlobals(dest)
|
|
228
398
|
|
|
@@ -255,14 +425,8 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
255
425
|
}
|
|
256
426
|
}
|
|
257
427
|
|
|
258
|
-
let skippedSideFile = false
|
|
259
428
|
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")
|
|
429
|
+
mergeGuardedMarkdown(dest, pkgRoot, file)
|
|
266
430
|
}
|
|
267
431
|
|
|
268
432
|
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true })
|
|
@@ -291,9 +455,13 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
291
455
|
exec("npm", args, { cwd: dest, stdio: "inherit" })
|
|
292
456
|
}
|
|
293
457
|
runNpm("install", ["install"])
|
|
458
|
+
const guardedSnap = snapshotGuardedRoots(dest)
|
|
294
459
|
runNpm("generate", ["run", "rulesync:generate"])
|
|
460
|
+
remergeGuardedRoots(dest, guardedSnap)
|
|
295
461
|
if (!skipGlobalsApply) {
|
|
296
462
|
runNpm("sync-globals", ["run", "sync:globals:apply"])
|
|
297
463
|
}
|
|
298
|
-
|
|
464
|
+
withGuardedCheckInteriors(dest, () => {
|
|
465
|
+
runNpm("check", ["run", "rulesync:check"])
|
|
466
|
+
})
|
|
299
467
|
}
|