@lovrozagar/grunt 0.2.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.
Files changed (49) hide show
  1. package/.agents/agents/grunt.md +4 -2
  2. package/.agents/hooks.json +11 -0
  3. package/.agents/skills/commit/SKILL.md +21 -0
  4. package/.agents/skills/commit-and-push/SKILL.md +15 -0
  5. package/.agents/skills/explain/SKILL.md +14 -0
  6. package/.agents/skills/handoff/SKILL.md +84 -0
  7. package/.agents/skills/parent/SKILL.md +11 -0
  8. package/.agents/skills/solo/SKILL.md +21 -0
  9. package/.claude/agents/grunt.md +2 -0
  10. package/.claude/settings.json +33 -0
  11. package/.claude/skills/commit/SKILL.md +21 -0
  12. package/.claude/skills/commit-and-push/SKILL.md +15 -0
  13. package/.claude/skills/explain/SKILL.md +14 -0
  14. package/.claude/skills/handoff/SKILL.md +84 -0
  15. package/.claude/skills/parent/SKILL.md +11 -0
  16. package/.claude/skills/solo/SKILL.md +21 -0
  17. package/.codex/agents/grunt.toml +4 -2
  18. package/.codex/hooks.json +33 -0
  19. package/.grok/agents/grunt.md +4 -2
  20. package/.grok/hooks/orchestrate-parent.js +109 -19
  21. package/.grok/skills/commit/SKILL.md +21 -0
  22. package/.grok/skills/commit-and-push/SKILL.md +15 -0
  23. package/.grok/skills/handoff/SKILL.md +84 -0
  24. package/.grok/skills/solo/SKILL.md +21 -0
  25. package/.rulesync/hooks.jsonc +8 -1
  26. package/.rulesync/reference/cascade.md +3 -2
  27. package/.rulesync/reference/hooks.md +5 -1
  28. package/.rulesync/reference/map.md +18 -3
  29. package/.rulesync/rules/CLAUDE.md +2 -1
  30. package/.rulesync/rules/overview.md +2 -1
  31. package/.rulesync/skills/commit/SKILL.md +22 -0
  32. package/.rulesync/skills/commit-and-push/SKILL.md +16 -0
  33. package/.rulesync/skills/handoff/SKILL.md +84 -0
  34. package/.rulesync/skills/solo/SKILL.md +21 -0
  35. package/.rulesync/subagents/grunt.md +6 -2
  36. package/.rulesync/subagents/implementer.md +2 -0
  37. package/.rulesync/subagents/orchestrator.md +2 -0
  38. package/.rulesync/subagents/thinker.md +2 -0
  39. package/AGENTS.md +2 -1
  40. package/CLAUDE.md +2 -1
  41. package/README.md +2 -2
  42. package/cli/init.mjs +105 -6
  43. package/cli/init.test.ts +303 -6
  44. package/package.json +12 -6
  45. package/scripts/emit-agent-shell-tools.mjs +118 -0
  46. package/scripts/emit-gemini.mjs +219 -0
  47. package/scripts/emit-mcp-policy.mjs +23 -0
  48. package/scripts/hooks-union.mjs +391 -0
  49. package/scripts/persist-handoff.mjs +231 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lovrozagar/grunt",
3
- "version": "0.2.0",
4
- "description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
3
+ "version": "0.3.0",
4
+ "description": "Multi-agent orchestrator SoT \u2014 deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
5
5
  "keywords": [
6
6
  "agents",
7
7
  "grunt",
@@ -25,10 +25,14 @@
25
25
  "bin/grunt.js",
26
26
  "cli",
27
27
  "scripts/check-globals.mjs",
28
+ "scripts/emit-agent-shell-tools.mjs",
29
+ "scripts/emit-gemini.mjs",
28
30
  "scripts/emit-mcp-policy.mjs",
29
31
  "scripts/gate-fat-tools.mjs",
32
+ "scripts/hooks-union.mjs",
30
33
  "scripts/grunt-job.mjs",
31
34
  "scripts/parse-need.mjs",
35
+ "scripts/persist-handoff.mjs",
32
36
  "scripts/persist-plan.mjs",
33
37
  "scripts/purge-global-mcps.mjs",
34
38
  "scripts/scrub-spawn-prompt.mjs",
@@ -52,9 +56,11 @@
52
56
  "access": "public"
53
57
  },
54
58
  "scripts": {
55
- "rulesync:generate": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks && node scripts/emit-mcp-policy.mjs",
56
- "rulesync:watch": "node scripts/emit-mcp-policy.mjs && rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents --watch",
57
- "rulesync:check": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents --check && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks --check && node scripts/emit-mcp-policy.mjs --check && node scripts/check-globals.mjs",
59
+ "rulesync:generate": "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",
60
+ "rulesync:watch": "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",
61
+ "rulesync:check": "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",
62
+ "hooks:union": "node scripts/hooks-union.mjs",
63
+ "hooks:check": "node scripts/hooks-union.mjs --check",
58
64
  "rulesync:doctor": "rulesync doctor",
59
65
  "sync:globals": "node scripts/sync-global-settings.mjs",
60
66
  "sync:globals:check": "node scripts/check-globals.mjs",
@@ -66,7 +72,7 @@
66
72
  "devDependencies": {
67
73
  "@types/node": "^24.13.3",
68
74
  "@vitest/coverage-v8": "^3.2.7",
69
- "rulesync": "latest",
75
+ "rulesync": "16.17.0",
70
76
  "smol-toml": "^1.8.0",
71
77
  "typescript": "^5.9.3",
72
78
  "vitest": "^3.2.7"
@@ -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,6 +11,7 @@ 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__*";
16
17
  const AGENT_ORCHESTRATOR_DENY = "Agent(orchestrator)";
@@ -276,6 +277,17 @@ function desiredAgentsMcp(policy) {
276
277
  return { mcpServers: pickServers(policy.servers, policy.allow) };
277
278
  }
278
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
+
279
291
  function grokCheckOk(disk, desired, policy) {
280
292
  if (!isPlainObject(disk)) return false;
281
293
  if (Object.prototype.hasOwnProperty.call(disk, "features")) return false;
@@ -337,24 +349,28 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
337
349
  const mcpAbs = path.join(ws, MCP_JSON_REL);
338
350
  const codexAbs = path.join(ws, CODEX_REL);
339
351
  const agentsAbs = path.join(ws, AGENTS_MCP_REL);
352
+ const geminiAbs = path.join(ws, GEMINI_SETTINGS_REL);
340
353
 
341
354
  const grokRaw = readText(grokAbs);
342
355
  const claudeRaw = readText(claudeAbs);
343
356
  const mcpRaw = readText(mcpAbs);
344
357
  const codexRaw = readText(codexAbs);
345
358
  const agentsRaw = readText(agentsAbs);
359
+ const geminiRaw = readText(geminiAbs);
346
360
 
347
361
  const grokDisk = parseTomlFile(grokRaw, {});
348
362
  const claudeDisk = parseJsonFile(claudeRaw, {});
349
363
  const mcpDisk = mcpRaw == null ? null : parseJsonFile(mcpRaw, {});
350
364
  const codexDisk = parseTomlFile(codexRaw, {});
351
365
  const agentsDisk = agentsRaw == null ? null : parseJsonFile(agentsRaw, {});
366
+ const geminiDisk = geminiRaw == null ? null : parseJsonFile(geminiRaw, {});
352
367
 
353
368
  const grokDesired = desiredGrok(grokDisk, policy);
354
369
  const claudeDesired = desiredClaudeSettings(claudeDisk, policy);
355
370
  const mcpDesired = desiredMcpJson(policy);
356
371
  const codexDesired = desiredCodex(codexDisk, policy);
357
372
  const agentsDesired = desiredAgentsMcp(policy);
373
+ const geminiDesired = desiredGeminiSettings(geminiDisk, policy);
358
374
 
359
375
  if (check) {
360
376
  const drift = [];
@@ -371,6 +387,7 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
371
387
  : !Object.prototype.hasOwnProperty.call(codexDisk, "mcp_servers"));
372
388
  if (!codexOk) drift.push(CODEX_REL);
373
389
  if (agentsDisk == null || !deepEqual(agentsDisk, agentsDesired)) drift.push(AGENTS_MCP_REL);
390
+ if (geminiDisk == null || !geminiCheckOk(geminiDisk, geminiDesired)) drift.push(GEMINI_SETTINGS_REL);
374
391
  if (drift.length) {
375
392
  return { ok: false, check: true, error: `drift: ${drift.join(", ")}`, drift };
376
393
  }
@@ -409,6 +426,12 @@ export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
409
426
  nextText: jsonOut(agentsDesired),
410
427
  diskRaw: agentsRaw,
411
428
  });
429
+ writeOrCheck({
430
+ check: false,
431
+ abs: geminiAbs,
432
+ nextText: jsonOut(geminiDesired),
433
+ diskRaw: geminiRaw,
434
+ });
412
435
  return { ok: true, check: false };
413
436
  }
414
437