@lovrozagar/grunt 0.1.0 → 0.3.0
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 +14 -15
- package/.agents/agents/implementer.md +9 -21
- package/.agents/agents/orchestrator.md +10 -38
- package/.agents/agents/thinker.md +7 -11
- package/.agents/hooks.json +11 -0
- package/.agents/skills/commit/SKILL.md +21 -0
- package/.agents/skills/commit-and-push/SKILL.md +15 -0
- package/.agents/skills/explain/SKILL.md +14 -0
- package/.agents/skills/handoff/SKILL.md +84 -0
- package/.agents/skills/parent/SKILL.md +11 -0
- package/.agents/skills/solo/SKILL.md +21 -0
- package/.claude/agents/grunt.md +15 -18
- package/.claude/agents/implementer.md +9 -24
- package/.claude/agents/orchestrator.md +11 -41
- package/.claude/agents/thinker.md +7 -14
- package/.claude/settings.json +34 -0
- package/.claude/skills/commit/SKILL.md +21 -0
- package/.claude/skills/commit-and-push/SKILL.md +15 -0
- package/.claude/skills/explain/SKILL.md +14 -0
- package/.claude/skills/handoff/SKILL.md +84 -0
- package/.claude/skills/parent/SKILL.md +11 -0
- package/.claude/skills/solo/SKILL.md +21 -0
- package/.codex/agents/grunt.toml +15 -16
- package/.codex/agents/implementer.toml +10 -22
- package/.codex/agents/orchestrator.toml +11 -39
- package/.codex/agents/thinker.toml +8 -12
- package/.codex/hooks.json +33 -0
- package/.grok/agents/grunt.md +14 -15
- package/.grok/agents/implementer.md +9 -21
- package/.grok/agents/orchestrator.md +10 -39
- package/.grok/agents/thinker.md +7 -12
- package/.grok/hooks/orchestrate-parent.js +126 -73
- package/.grok/roles/thinker.toml +1 -1
- package/.grok/skills/commit/SKILL.md +21 -0
- package/.grok/skills/commit-and-push/SKILL.md +15 -0
- package/.grok/skills/handoff/SKILL.md +84 -0
- package/.grok/skills/solo/SKILL.md +21 -0
- package/.rulesync/hooks.jsonc +23 -0
- package/.rulesync/reference/cascade.md +13 -19
- package/.rulesync/reference/hooks.md +5 -1
- package/.rulesync/reference/map.md +20 -4
- package/.rulesync/reference/output.md +7 -0
- package/.rulesync/rules/CLAUDE.md +11 -8
- package/.rulesync/rules/overview.md +11 -13
- package/.rulesync/skills/commit/SKILL.md +22 -0
- package/.rulesync/skills/commit-and-push/SKILL.md +16 -0
- package/.rulesync/skills/handoff/SKILL.md +84 -0
- package/.rulesync/skills/solo/SKILL.md +21 -0
- package/.rulesync/subagents/grunt.md +20 -18
- package/.rulesync/subagents/implementer.md +15 -24
- package/.rulesync/subagents/orchestrator.md +16 -42
- package/.rulesync/subagents/thinker.md +13 -15
- package/AGENTS.md +11 -12
- package/CLAUDE.md +11 -7
- package/README.md +11 -9
- package/cli/init.mjs +105 -6
- package/cli/init.test.ts +303 -6
- package/package.json +12 -6
- package/scripts/emit-agent-shell-tools.mjs +118 -0
- package/scripts/emit-gemini.mjs +219 -0
- package/scripts/emit-mcp-policy.mjs +30 -0
- package/scripts/hooks-union.mjs +391 -0
- package/scripts/persist-handoff.mjs +231 -0
- package/.rulesync/skills/terse/SKILL.md +0 -8
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** After rulesync subagent emit: Claude grunt body uses Bash; other targets keep run_terminal_command. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const SSOT_REL = ".rulesync/subagents/grunt.md";
|
|
8
|
+
export const CLAUDE_GRUNT_REL = ".claude/agents/grunt.md";
|
|
9
|
+
|
|
10
|
+
export const GENERIC_TOOL_LIST =
|
|
11
|
+
"read_file, grep, list_dir, run_terminal_command, write, search_replace, web_search, web_fetch";
|
|
12
|
+
export const CLAUDE_TOOL_LIST = "Read, Grep, Glob, Bash, Write, Edit, Web";
|
|
13
|
+
|
|
14
|
+
export function parseArgv(argv) {
|
|
15
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
16
|
+
let check = false;
|
|
17
|
+
for (const a of args) {
|
|
18
|
+
if (a === "--check") {
|
|
19
|
+
check = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
return { ok: false, error: `unknown flag: ${a}` };
|
|
23
|
+
}
|
|
24
|
+
return { ok: true, check };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function splitFrontmatter(text) {
|
|
28
|
+
const src = String(text ?? "");
|
|
29
|
+
if (!src.startsWith("---")) return { ok: false, error: "missing frontmatter" };
|
|
30
|
+
const rest = src.slice(3).replace(/^\r?\n/, "");
|
|
31
|
+
const m = rest.match(/\r?\n---\s*(?:\r?\n|$)/);
|
|
32
|
+
if (!m || m.index == null) return { ok: false, error: "missing frontmatter close" };
|
|
33
|
+
return { ok: true, fm: rest.slice(0, m.index), body: rest.slice(m.index + m[0].length) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toClaudeShellBody(body) {
|
|
37
|
+
return String(body)
|
|
38
|
+
.split(GENERIC_TOOL_LIST)
|
|
39
|
+
.join(CLAUDE_TOOL_LIST)
|
|
40
|
+
.split("web_search/web_fetch")
|
|
41
|
+
.join("WebSearch/WebFetch")
|
|
42
|
+
.split("run_terminal_command")
|
|
43
|
+
.join("Bash");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readText(abs) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readFileSync(abs, "utf8");
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err && err.code === "ENOENT") return null;
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function joinMarkdown(fm, body) {
|
|
56
|
+
const b = body.endsWith("\n") || body === "" ? body : `${body}\n`;
|
|
57
|
+
return `---\n${fm}\n---\n${b}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function emitAgentShellTools({ workspaceRoot, check = false } = {}) {
|
|
61
|
+
const ws = workspaceRoot || process.cwd();
|
|
62
|
+
const ssotAbs = path.join(ws, SSOT_REL);
|
|
63
|
+
const claudeAbs = path.join(ws, CLAUDE_GRUNT_REL);
|
|
64
|
+
const ssotRaw = readText(ssotAbs);
|
|
65
|
+
if (ssotRaw == null) {
|
|
66
|
+
return { ok: false, check, error: `missing ${SSOT_REL}` };
|
|
67
|
+
}
|
|
68
|
+
const ssot = splitFrontmatter(ssotRaw);
|
|
69
|
+
if (!ssot.ok) {
|
|
70
|
+
return { ok: false, check, error: `ssot: ${ssot.error}` };
|
|
71
|
+
}
|
|
72
|
+
const claudeRaw = readText(claudeAbs);
|
|
73
|
+
if (claudeRaw == null) {
|
|
74
|
+
return { ok: false, check, error: `missing ${CLAUDE_GRUNT_REL}` };
|
|
75
|
+
}
|
|
76
|
+
const claude = splitFrontmatter(claudeRaw);
|
|
77
|
+
if (!claude.ok) {
|
|
78
|
+
return { ok: false, check, error: `claude grunt: ${claude.error}` };
|
|
79
|
+
}
|
|
80
|
+
const next = joinMarkdown(claude.fm, toClaudeShellBody(ssot.body));
|
|
81
|
+
if (check) {
|
|
82
|
+
if (claudeRaw !== next) {
|
|
83
|
+
return { ok: false, check: true, error: `drift: ${CLAUDE_GRUNT_REL}`, drift: [CLAUDE_GRUNT_REL] };
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, check: true, drift: [] };
|
|
86
|
+
}
|
|
87
|
+
fs.mkdirSync(path.dirname(claudeAbs), { recursive: true });
|
|
88
|
+
fs.writeFileSync(claudeAbs, next);
|
|
89
|
+
return { ok: true, check: false };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function main() {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
95
|
+
if (!parsed.ok) {
|
|
96
|
+
process.stderr.write((parsed.error || "invalid argv") + "\n");
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
const result = emitAgentShellTools({
|
|
100
|
+
workspaceRoot: process.cwd(),
|
|
101
|
+
check: parsed.check,
|
|
102
|
+
});
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
process.stderr.write((result.error || "emit failed") + "\n");
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
process.stderr.write(String(err && err.message ? err.message : err) + "\n");
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
115
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
116
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
117
|
+
process.exit(main());
|
|
118
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Emit Gemini CLI GEMINI.md + .gemini/agents from .rulesync/subagents. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const AGENT_IDS = ["orchestrator", "implementer", "thinker", "grunt"];
|
|
8
|
+
const SSOT_REL = ".rulesync/subagents";
|
|
9
|
+
const GEMINI_MD_REL = "GEMINI.md";
|
|
10
|
+
const AGENTS_REL = ".gemini/agents";
|
|
11
|
+
const GEMINI_MD_BODY = "@AGENTS.md\n";
|
|
12
|
+
|
|
13
|
+
function isPlainObject(v) {
|
|
14
|
+
return v != null && typeof v === "object" && !Array.isArray(v);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseArgv(argv) {
|
|
18
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
19
|
+
let check = false;
|
|
20
|
+
for (const a of args) {
|
|
21
|
+
if (a === "--check") {
|
|
22
|
+
check = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
return { ok: false, error: `unknown flag: ${a}` };
|
|
26
|
+
}
|
|
27
|
+
return { ok: true, check };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function unquoteYaml(raw) {
|
|
31
|
+
const s = String(raw).trim();
|
|
32
|
+
if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(s);
|
|
35
|
+
} catch {
|
|
36
|
+
return s.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (s.length >= 2 && s[0] === "'" && s[s.length - 1] === "'") {
|
|
40
|
+
return s.slice(1, -1).replace(/''/g, "'");
|
|
41
|
+
}
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function yamlScalar(v) {
|
|
46
|
+
const s = String(v);
|
|
47
|
+
if (s === "") return '""';
|
|
48
|
+
if (/^[A-Za-z0-9_./+-]+$/.test(s)) return s;
|
|
49
|
+
return JSON.stringify(s);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function splitFrontmatter(text) {
|
|
53
|
+
const src = String(text ?? "");
|
|
54
|
+
if (!src.startsWith("---")) return { ok: false, error: "missing frontmatter" };
|
|
55
|
+
const rest = src.slice(3).replace(/^\r?\n/, "");
|
|
56
|
+
const m = rest.match(/\r?\n---\s*(?:\r?\n|$)/);
|
|
57
|
+
if (!m || m.index == null) return { ok: false, error: "missing frontmatter close" };
|
|
58
|
+
return { ok: true, fm: rest.slice(0, m.index), body: rest.slice(m.index + m[0].length) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseYamlBlock(fm) {
|
|
62
|
+
const top = {};
|
|
63
|
+
let cur = null;
|
|
64
|
+
for (const line of String(fm).split(/\r?\n/)) {
|
|
65
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
66
|
+
const nested = line.match(/^([ \t]+)([^:#\s][^:]*):\s*(.*?)\s*$/);
|
|
67
|
+
if (nested && cur) {
|
|
68
|
+
const key = nested[2].trim();
|
|
69
|
+
const val = nested[3];
|
|
70
|
+
if (!isPlainObject(top[cur])) top[cur] = {};
|
|
71
|
+
if (val !== "") top[cur][key] = unquoteYaml(val);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const topm = line.match(/^([^:#\s][^:]*):\s*(.*?)\s*$/);
|
|
75
|
+
if (topm && !/^[ \t]/.test(line)) {
|
|
76
|
+
const key = topm[1].trim();
|
|
77
|
+
const val = topm[2];
|
|
78
|
+
cur = key;
|
|
79
|
+
top[key] = val === "" ? {} : unquoteYaml(val);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return top;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function geminiModel(parsed) {
|
|
86
|
+
if (parsed["geminicli.model"] != null && parsed["geminicli.model"] !== "") {
|
|
87
|
+
return String(parsed["geminicli.model"]);
|
|
88
|
+
}
|
|
89
|
+
if (isPlainObject(parsed.geminicli) && parsed.geminicli.model != null && parsed.geminicli.model !== "") {
|
|
90
|
+
return String(parsed.geminicli.model);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readText(abs) {
|
|
96
|
+
try {
|
|
97
|
+
return fs.readFileSync(abs, "utf8");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err && err.code === "ENOENT") return null;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function lstat(abs) {
|
|
105
|
+
try {
|
|
106
|
+
return fs.lstatSync(abs);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err && err.code === "ENOENT") return null;
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function agentMarkdown(name, description, model, body) {
|
|
114
|
+
const fm = ["---", `name: ${yamlScalar(name)}`, `description: ${yamlScalar(description)}`, `model: ${yamlScalar(model)}`, "---"].join(
|
|
115
|
+
"\n",
|
|
116
|
+
);
|
|
117
|
+
const b = body.endsWith("\n") || body === "" ? body : `${body}\n`;
|
|
118
|
+
return `${fm}\n${b}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function collisionRel(id) {
|
|
122
|
+
return path.join(AGENTS_REL, `${id}.md`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ownedRel(id) {
|
|
126
|
+
return path.join(AGENTS_REL, id, "agent.md");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function emitGemini({ workspaceRoot, check = false } = {}) {
|
|
130
|
+
const ws = workspaceRoot || process.cwd();
|
|
131
|
+
const collisions = [];
|
|
132
|
+
for (const id of AGENT_IDS) {
|
|
133
|
+
const flat = path.join(ws, collisionRel(id));
|
|
134
|
+
const dir = path.join(ws, AGENTS_REL, id);
|
|
135
|
+
const stFlat = lstat(flat);
|
|
136
|
+
if (stFlat && stFlat.isFile()) collisions.push(collisionRel(id));
|
|
137
|
+
const stDir = lstat(dir);
|
|
138
|
+
if (stDir && stDir.isFile()) collisions.push(path.join(AGENTS_REL, id));
|
|
139
|
+
}
|
|
140
|
+
if (collisions.length) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
check,
|
|
144
|
+
error: `collision: ${collisions.join(", ")}`,
|
|
145
|
+
collisions,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const files = [{ rel: GEMINI_MD_REL, text: GEMINI_MD_BODY }];
|
|
150
|
+
for (const id of AGENT_IDS) {
|
|
151
|
+
const ssotAbs = path.join(ws, SSOT_REL, `${id}.md`);
|
|
152
|
+
const raw = readText(ssotAbs);
|
|
153
|
+
if (raw == null) {
|
|
154
|
+
return { ok: false, check, error: `missing ${path.join(SSOT_REL, `${id}.md`)}` };
|
|
155
|
+
}
|
|
156
|
+
const split = splitFrontmatter(raw);
|
|
157
|
+
if (!split.ok) {
|
|
158
|
+
return { ok: false, check, error: `${id}: ${split.error}` };
|
|
159
|
+
}
|
|
160
|
+
const parsed = parseYamlBlock(split.fm);
|
|
161
|
+
const model = geminiModel(parsed);
|
|
162
|
+
if (!model) {
|
|
163
|
+
return { ok: false, check, error: `${id}: missing geminicli.model` };
|
|
164
|
+
}
|
|
165
|
+
const name = parsed.name != null && parsed.name !== "" ? String(parsed.name) : id;
|
|
166
|
+
const description = parsed.description != null ? String(parsed.description) : "";
|
|
167
|
+
files.push({
|
|
168
|
+
rel: ownedRel(id),
|
|
169
|
+
text: agentMarkdown(name, description, model, split.body),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (check) {
|
|
174
|
+
const drift = [];
|
|
175
|
+
for (const f of files) {
|
|
176
|
+
const disk = readText(path.join(ws, f.rel));
|
|
177
|
+
if (disk !== f.text) drift.push(f.rel);
|
|
178
|
+
}
|
|
179
|
+
if (drift.length) {
|
|
180
|
+
return { ok: false, check: true, error: `drift: ${drift.join(", ")}`, drift };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, check: true, drift: [] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const f of files) {
|
|
186
|
+
const abs = path.join(ws, f.rel);
|
|
187
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
188
|
+
fs.writeFileSync(abs, f.text);
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, check: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function main() {
|
|
194
|
+
try {
|
|
195
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
196
|
+
if (!parsed.ok) {
|
|
197
|
+
process.stderr.write((parsed.error || "invalid argv") + "\n");
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
const result = emitGemini({
|
|
201
|
+
workspaceRoot: process.cwd(),
|
|
202
|
+
check: parsed.check,
|
|
203
|
+
});
|
|
204
|
+
if (!result.ok) {
|
|
205
|
+
process.stderr.write((result.error || "emit failed") + "\n");
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
return 0;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
process.stderr.write(String(err && err.message ? err.message : err) + "\n");
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
216
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
217
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
218
|
+
process.exit(main());
|
|
219
|
+
}
|
|
@@ -11,8 +11,10 @@ const CLAUDE_SETTINGS_REL = ".claude/settings.json";
|
|
|
11
11
|
const MCP_JSON_REL = ".mcp.json";
|
|
12
12
|
const CODEX_REL = ".codex/config.toml";
|
|
13
13
|
const AGENTS_MCP_REL = ".agents/mcp_config.json";
|
|
14
|
+
const GEMINI_SETTINGS_REL = ".gemini/settings.json";
|
|
14
15
|
const AGENT_IDS = ["orchestrator", "implementer", "thinker", "grunt"];
|
|
15
16
|
const MCP_DENY = "mcp__*";
|
|
17
|
+
const AGENT_ORCHESTRATOR_DENY = "Agent(orchestrator)";
|
|
16
18
|
const GROK_HEADER =
|
|
17
19
|
"# Generated by emit-mcp-policy.mjs from .rulesync/mcp-policy.jsonc.\n";
|
|
18
20
|
|
|
@@ -239,6 +241,12 @@ function desiredClaudeSettings(disk, policy) {
|
|
|
239
241
|
obj.enabledMcpjsonServers = [...policy.allow];
|
|
240
242
|
const permissions = isPlainObject(obj.permissions) ? { ...obj.permissions } : {};
|
|
241
243
|
const deny = Array.isArray(permissions.deny) ? [...permissions.deny] : [];
|
|
244
|
+
const hasAgentDeny = deny.some(
|
|
245
|
+
(s) => typeof s === "string" && s.startsWith("Agent("),
|
|
246
|
+
);
|
|
247
|
+
if (hasAgentDeny && !deny.includes(AGENT_ORCHESTRATOR_DENY)) {
|
|
248
|
+
deny.push(AGENT_ORCHESTRATOR_DENY);
|
|
249
|
+
}
|
|
242
250
|
if (!deny.includes(MCP_DENY)) deny.push(MCP_DENY);
|
|
243
251
|
permissions.deny = deny;
|
|
244
252
|
obj.permissions = permissions;
|
|
@@ -269,6 +277,17 @@ function desiredAgentsMcp(policy) {
|
|
|
269
277
|
return { mcpServers: pickServers(policy.servers, policy.allow) };
|
|
270
278
|
}
|
|
271
279
|
|
|
280
|
+
function desiredGeminiSettings(disk, policy) {
|
|
281
|
+
const obj = isPlainObject(disk) ? { ...disk } : {};
|
|
282
|
+
obj.mcpServers = pickServers(policy.servers, policy.allow);
|
|
283
|
+
return obj;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function geminiCheckOk(disk, desired) {
|
|
287
|
+
if (!isPlainObject(disk)) return false;
|
|
288
|
+
return deepEqual(disk.mcpServers, desired.mcpServers);
|
|
289
|
+
}
|
|
290
|
+
|
|
272
291
|
function grokCheckOk(disk, desired, policy) {
|
|
273
292
|
if (!isPlainObject(disk)) return false;
|
|
274
293
|
if (Object.prototype.hasOwnProperty.call(disk, "features")) return false;
|
|
@@ -330,24 +349,28 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
|
|
|
330
349
|
const mcpAbs = path.join(ws, MCP_JSON_REL);
|
|
331
350
|
const codexAbs = path.join(ws, CODEX_REL);
|
|
332
351
|
const agentsAbs = path.join(ws, AGENTS_MCP_REL);
|
|
352
|
+
const geminiAbs = path.join(ws, GEMINI_SETTINGS_REL);
|
|
333
353
|
|
|
334
354
|
const grokRaw = readText(grokAbs);
|
|
335
355
|
const claudeRaw = readText(claudeAbs);
|
|
336
356
|
const mcpRaw = readText(mcpAbs);
|
|
337
357
|
const codexRaw = readText(codexAbs);
|
|
338
358
|
const agentsRaw = readText(agentsAbs);
|
|
359
|
+
const geminiRaw = readText(geminiAbs);
|
|
339
360
|
|
|
340
361
|
const grokDisk = parseTomlFile(grokRaw, {});
|
|
341
362
|
const claudeDisk = parseJsonFile(claudeRaw, {});
|
|
342
363
|
const mcpDisk = mcpRaw == null ? null : parseJsonFile(mcpRaw, {});
|
|
343
364
|
const codexDisk = parseTomlFile(codexRaw, {});
|
|
344
365
|
const agentsDisk = agentsRaw == null ? null : parseJsonFile(agentsRaw, {});
|
|
366
|
+
const geminiDisk = geminiRaw == null ? null : parseJsonFile(geminiRaw, {});
|
|
345
367
|
|
|
346
368
|
const grokDesired = desiredGrok(grokDisk, policy);
|
|
347
369
|
const claudeDesired = desiredClaudeSettings(claudeDisk, policy);
|
|
348
370
|
const mcpDesired = desiredMcpJson(policy);
|
|
349
371
|
const codexDesired = desiredCodex(codexDisk, policy);
|
|
350
372
|
const agentsDesired = desiredAgentsMcp(policy);
|
|
373
|
+
const geminiDesired = desiredGeminiSettings(geminiDisk, policy);
|
|
351
374
|
|
|
352
375
|
if (check) {
|
|
353
376
|
const drift = [];
|
|
@@ -364,6 +387,7 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
|
|
|
364
387
|
: !Object.prototype.hasOwnProperty.call(codexDisk, "mcp_servers"));
|
|
365
388
|
if (!codexOk) drift.push(CODEX_REL);
|
|
366
389
|
if (agentsDisk == null || !deepEqual(agentsDisk, agentsDesired)) drift.push(AGENTS_MCP_REL);
|
|
390
|
+
if (geminiDisk == null || !geminiCheckOk(geminiDisk, geminiDesired)) drift.push(GEMINI_SETTINGS_REL);
|
|
367
391
|
if (drift.length) {
|
|
368
392
|
return { ok: false, check: true, error: `drift: ${drift.join(", ")}`, drift };
|
|
369
393
|
}
|
|
@@ -402,6 +426,12 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
|
|
|
402
426
|
nextText: jsonOut(agentsDesired),
|
|
403
427
|
diskRaw: agentsRaw,
|
|
404
428
|
});
|
|
429
|
+
writeOrCheck({
|
|
430
|
+
check: false,
|
|
431
|
+
abs: geminiAbs,
|
|
432
|
+
nextText: jsonOut(geminiDesired),
|
|
433
|
+
diskRaw: geminiRaw,
|
|
434
|
+
});
|
|
405
435
|
return { ok: true, check: false };
|
|
406
436
|
}
|
|
407
437
|
|