@kuznai/inception-engine 0.18.0 → 0.20.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 (109) hide show
  1. package/README.md +72 -43
  2. package/dist/{config → src/config}/agents.js +174 -24
  3. package/dist/src/core/adapters/agent-definitions.d.ts +8 -0
  4. package/dist/src/core/adapters/agent-definitions.js +162 -0
  5. package/dist/{core → src/core}/adapters/frontmatter.d.ts +10 -0
  6. package/dist/{core → src/core}/adapters/frontmatter.js +23 -6
  7. package/dist/{core → src/core}/adapters/index.d.ts +1 -1
  8. package/dist/{core → src/core}/adapters/index.js +4 -4
  9. package/dist/{core → src/core}/adapters/mcp.d.ts +2 -2
  10. package/dist/{core → src/core}/adapters/mcp.js +21 -24
  11. package/dist/{core → src/core}/adapters/permissions.js +16 -20
  12. package/dist/{core → src/core}/adapters/rules.d.ts +2 -2
  13. package/dist/src/core/adapters/rules.js +126 -0
  14. package/dist/src/core/capabilities.d.ts +47 -0
  15. package/dist/src/core/capabilities.js +280 -0
  16. package/dist/{core → src/core}/deploy.d.ts +1 -1
  17. package/dist/{core → src/core}/deploy.js +273 -92
  18. package/dist/{core → src/core}/init.js +126 -52
  19. package/dist/src/core/merge-patch.d.ts +4 -0
  20. package/dist/src/core/merge-patch.js +46 -0
  21. package/dist/{core → src/core}/ownership.d.ts +2 -2
  22. package/dist/{core → src/core}/ownership.js +13 -0
  23. package/dist/{core → src/core}/preflight.d.ts +1 -1
  24. package/dist/src/core/preflight.js +280 -0
  25. package/dist/{core → src/core}/resolve.d.ts +1 -1
  26. package/dist/{core → src/core}/resolve.js +7 -4
  27. package/dist/{core → src/core}/revert.js +95 -33
  28. package/dist/{core → src/core}/runtime-paths.d.ts +2 -1
  29. package/dist/{core → src/core}/runtime-paths.js +30 -23
  30. package/dist/{core → src/core}/validation.d.ts +10 -1
  31. package/dist/{core → src/core}/validation.js +75 -0
  32. package/dist/src/formatters.d.ts +5 -0
  33. package/dist/src/formatters.js +58 -0
  34. package/dist/{index.js → src/index.js} +8 -24
  35. package/dist/src/logger.js +62 -0
  36. package/dist/{schemas → src/schemas}/manifest.d.ts +22 -0
  37. package/dist/{schemas → src/schemas}/manifest.js +19 -5
  38. package/dist/{schemas → src/schemas}/registry.d.ts +74 -0
  39. package/dist/{schemas → src/schemas}/registry.js +18 -3
  40. package/dist/{types.d.ts → src/types.d.ts} +60 -0
  41. package/dist/test/helpers/fs.d.ts +2 -0
  42. package/dist/test/helpers/fs.js +17 -0
  43. package/dist/test/helpers/path.d.ts +10 -0
  44. package/dist/test/helpers/path.js +15 -0
  45. package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
  46. package/dist/test/helpers/skill-dir-scenarios.js +206 -0
  47. package/dist/test/helpers/skill-dir.d.ts +7 -0
  48. package/dist/test/helpers/skill-dir.js +46 -0
  49. package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
  50. package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
  51. package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
  52. package/dist/test/os/posix/skill-dir.test.js +19 -0
  53. package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
  54. package/dist/test/os/windows/revert-integration.test.js +72 -0
  55. package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
  56. package/dist/test/os/windows/skill-dir.test.js +19 -0
  57. package/dist/test/unit/adapters.test.d.ts +1 -0
  58. package/dist/test/unit/adapters.test.js +947 -0
  59. package/dist/test/unit/capabilities.test.d.ts +1 -0
  60. package/dist/test/unit/capabilities.test.js +71 -0
  61. package/dist/test/unit/cli.test.d.ts +1 -0
  62. package/dist/test/unit/cli.test.js +834 -0
  63. package/dist/test/unit/deploy.test.d.ts +1 -0
  64. package/dist/test/unit/deploy.test.js +2481 -0
  65. package/dist/test/unit/detect.test.d.ts +1 -0
  66. package/dist/test/unit/detect.test.js +192 -0
  67. package/dist/test/unit/enterprise.test.d.ts +1 -0
  68. package/dist/test/unit/enterprise.test.js +106 -0
  69. package/dist/test/unit/frontmatter.test.d.ts +1 -0
  70. package/dist/test/unit/frontmatter.test.js +102 -0
  71. package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
  72. package/dist/test/unit/gemini-north-star.test.js +243 -0
  73. package/dist/test/unit/manifest.test.d.ts +1 -0
  74. package/dist/test/unit/manifest.test.js +737 -0
  75. package/dist/test/unit/migration-golden.test.d.ts +1 -0
  76. package/dist/test/unit/migration-golden.test.js +197 -0
  77. package/dist/test/unit/ownership.test.d.ts +1 -0
  78. package/dist/test/unit/ownership.test.js +587 -0
  79. package/dist/test/unit/preflight.test.d.ts +1 -0
  80. package/dist/test/unit/preflight.test.js +501 -0
  81. package/dist/test/unit/resolve.test.d.ts +1 -0
  82. package/dist/test/unit/resolve.test.js +119 -0
  83. package/dist/test/unit/revert.test.d.ts +1 -0
  84. package/dist/test/unit/revert.test.js +1004 -0
  85. package/dist/test/unit/toml.test.d.ts +1 -0
  86. package/dist/test/unit/toml.test.js +55 -0
  87. package/package.json +1 -1
  88. package/dist/core/adapters/agent-definitions.d.ts +0 -20
  89. package/dist/core/adapters/agent-definitions.js +0 -90
  90. package/dist/core/adapters/rules.js +0 -103
  91. package/dist/core/preflight.js +0 -98
  92. package/dist/logger.js +0 -61
  93. /package/dist/{config → src/config}/agents.d.ts +0 -0
  94. /package/dist/{config → src/config}/manifest.d.ts +0 -0
  95. /package/dist/{config → src/config}/manifest.js +0 -0
  96. /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
  97. /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
  98. /package/dist/{core → src/core}/adapters/toml.js +0 -0
  99. /package/dist/{core → src/core}/detect.d.ts +0 -0
  100. /package/dist/{core → src/core}/detect.js +0 -0
  101. /package/dist/{core → src/core}/init.d.ts +0 -0
  102. /package/dist/{core → src/core}/revert.d.ts +0 -0
  103. /package/dist/{errors.d.ts → src/errors.d.ts} +0 -0
  104. /package/dist/{errors.js → src/errors.js} +0 -0
  105. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  106. /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
  107. /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
  108. /package/dist/{schemas → src/schemas}/errors.js +0 -0
  109. /package/dist/{types.js → src/types.js} +0 -0
@@ -0,0 +1,947 @@
1
+ import assert from "node:assert/strict";
2
+ import { realpath, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { describe, it } from "node:test";
5
+ import { compileMcpServerActions } from "../../src/core/adapters/mcp.js";
6
+ import { compilePermissionsActions } from "../../src/core/adapters/permissions.js";
7
+ import { compileAgentRuleActions } from "../../src/core/adapters/rules.js";
8
+ import { makeTmpDir } from "../helpers/fs.js";
9
+ import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
10
+ describe("compileMcpServerActions", () => {
11
+ it("returns zero actions and warnings when no detectedAgents overlap", () => {
12
+ const { actions, warnings } = compileMcpServerActions({
13
+ name: "my-mcp",
14
+ agents: ["claude-code"],
15
+ config: { command: "s" },
16
+ scope: "global",
17
+ }, ["codex"], "/home/test");
18
+ assert.equal(actions.length, 0);
19
+ assert.equal(warnings.length, 0);
20
+ });
21
+ it("returns a config-patch action for claude-code with correct target and patch", () => {
22
+ const home = "/home/test";
23
+ const { actions, warnings } = compileMcpServerActions({
24
+ name: "my-mcp",
25
+ agents: ["claude-code"],
26
+ config: { command: "my-server", args: ["--verbose"] },
27
+ scope: "global",
28
+ }, ["claude-code"], home);
29
+ assert.equal(actions.length, 1);
30
+ assert.equal(warnings.length, 0);
31
+ const action = actions[0];
32
+ assert.equal(action.kind, "config-patch");
33
+ assert.equal(action.skill, "my-mcp");
34
+ assert.equal(action.agent, "claude-code");
35
+ assert.ok(action.target.endsWith(".claude.json"), `expected target to end with .claude.json, got: ${action.target}`);
36
+ assert.deepEqual(action.patch, {
37
+ mcpServers: { "my-mcp": { command: "my-server", args: ["--verbose"] } },
38
+ });
39
+ assert.equal(action.confidence, "documented");
40
+ });
41
+ it("returns a config-patch action for claude-code MCP with scope: repo", () => {
42
+ const repo = "/repo/test";
43
+ const { actions, warnings } = compileMcpServerActions({
44
+ name: "my-mcp",
45
+ agents: ["claude-code"],
46
+ config: { command: "s" },
47
+ scope: "repo",
48
+ }, ["claude-code"], "/home/test", repo);
49
+ assert.equal(actions.length, 1);
50
+ assert.equal(warnings.length, 0);
51
+ const action = actions[0];
52
+ assert.equal(action.kind, "config-patch");
53
+ assert.equal(action.agent, "claude-code");
54
+ assertPathEndsWith(action.target, ".claude/mcp.json", `expected target to end with .claude/mcp.json, got: ${action.target}`);
55
+ assert.deepEqual(action.patch, {
56
+ mcpServers: { "my-mcp": { command: "s" } },
57
+ });
58
+ assert.equal(action.confidence, "documented");
59
+ });
60
+ it("returns a config-patch action for claude-code MCP with scope: workspace", () => {
61
+ const workspace = "/workspace/test";
62
+ const { actions, warnings } = compileMcpServerActions({
63
+ name: "my-mcp",
64
+ agents: ["claude-code"],
65
+ config: { command: "s" },
66
+ scope: "workspace",
67
+ }, ["claude-code"], "/home/test", undefined, workspace);
68
+ assert.equal(actions.length, 1);
69
+ assert.equal(warnings.length, 0);
70
+ const action = actions[0];
71
+ assert.equal(action.kind, "config-patch");
72
+ assert.equal(action.agent, "claude-code");
73
+ assertPathEndsWith(action.target, ".claude/mcp.json", `expected target to end with .claude/mcp.json, got: ${action.target}`);
74
+ assert.deepEqual(action.patch, {
75
+ mcpServers: { "my-mcp": { command: "s" } },
76
+ });
77
+ });
78
+ it("returns a config-patch action for gemini-cli with correct target", () => {
79
+ const home = "/home/test";
80
+ const { actions } = compileMcpServerActions({
81
+ name: "my-mcp",
82
+ agents: ["gemini-cli"],
83
+ config: { url: "http://x" },
84
+ scope: "global",
85
+ }, ["gemini-cli"], home);
86
+ assert.equal(actions.length, 1);
87
+ const action = actions[0];
88
+ assertPathEndsWith(action.target, ".gemini/settings.json", `expected target under .gemini/settings.json, got: ${action.target}`);
89
+ });
90
+ it("returns an unsupported warning and no action for github-copilot MCP with default (global) scope", () => {
91
+ const { actions, warnings } = compileMcpServerActions({
92
+ name: "my-mcp",
93
+ agents: ["github-copilot"],
94
+ config: { command: "s" },
95
+ scope: "global",
96
+ }, ["github-copilot"], "/home/test");
97
+ assert.equal(actions.length, 0);
98
+ assert.equal(warnings.length, 1);
99
+ assert.equal(warnings[0]?.kind, "confidence");
100
+ assert.match(warnings[0]?.message ?? "", /github-copilot/);
101
+ assert.match(warnings[0]?.message ?? "", /unsupported/);
102
+ });
103
+ it("returns a config-patch action for github-copilot MCP with scope: repo", () => {
104
+ const repo = "/repo/test";
105
+ const { actions, warnings } = compileMcpServerActions({
106
+ name: "my-mcp",
107
+ agents: ["github-copilot"],
108
+ config: { command: "s" },
109
+ scope: "repo",
110
+ }, ["github-copilot"], "/home/test", repo);
111
+ assert.equal(actions.length, 1);
112
+ assert.equal(warnings.length, 0);
113
+ const action = actions[0];
114
+ assert.equal(action.kind, "config-patch");
115
+ assert.equal(action.agent, "github-copilot");
116
+ assertPathEndsWith(action.target, ".vscode/mcp.json", `expected target to end with .vscode/mcp.json, got: ${action.target}`);
117
+ assert.deepEqual(action.patch, {
118
+ servers: { "my-mcp": { command: "s" } },
119
+ });
120
+ assert.equal(action.confidence, "documented");
121
+ });
122
+ it("returns a config-patch action for github-copilot MCP with scope: workspace", () => {
123
+ const workspace = "/workspace/test";
124
+ const { actions, warnings } = compileMcpServerActions({
125
+ name: "my-mcp",
126
+ agents: ["github-copilot"],
127
+ config: { command: "s" },
128
+ scope: "workspace",
129
+ }, ["github-copilot"], "/home/test", undefined, workspace);
130
+ assert.equal(actions.length, 1);
131
+ assert.equal(warnings.length, 0);
132
+ const action = actions[0];
133
+ assert.equal(action.kind, "config-patch");
134
+ assert.equal(action.agent, "github-copilot");
135
+ assertPathEndsWith(action.target, ".vscode/mcp.json", `expected target to end with .vscode/mcp.json, got: ${action.target}`);
136
+ assert.deepEqual(action.patch, {
137
+ servers: { "my-mcp": { command: "s" } },
138
+ });
139
+ });
140
+ it("returns a frontmatter-emit action for antigravity MCP", () => {
141
+ const home = "/home/test";
142
+ const { actions, warnings } = compileMcpServerActions({
143
+ name: "my-mcp",
144
+ agents: ["antigravity"],
145
+ config: { command: "s" },
146
+ scope: "global",
147
+ }, ["antigravity"], home, "/repo/test");
148
+ assert.equal(actions.length, 1);
149
+ assert.equal(warnings.length, 0);
150
+ assert.equal(actions[0]?.kind, "frontmatter-emit");
151
+ assert.equal(actions[0]?.agent, "antigravity");
152
+ assertPathEndsWith(actions[0]?.target ?? "", ".agents/rules/my-mcp.md", `expected target to end with .agents/rules/my-mcp.md, got ${actions[0]?.target}`);
153
+ });
154
+ it("throws when a supported MCP target is missing both command and url", () => {
155
+ assert.throws(() => compileMcpServerActions({
156
+ name: "my-mcp",
157
+ agents: ["claude-code"],
158
+ config: { args: ["--verbose"] },
159
+ scope: "global",
160
+ }, ["claude-code"], "/home/test"), /must define either a non-empty "command" or "url"/);
161
+ });
162
+ it("produces actions for each detected agent that has supported MCP config", () => {
163
+ const { actions, warnings } = compileMcpServerActions({
164
+ name: "my-mcp",
165
+ agents: ["claude-code", "gemini-cli"],
166
+ config: { command: "s" },
167
+ scope: "global",
168
+ }, ["claude-code", "gemini-cli"], "/home/test");
169
+ assert.equal(actions.length, 2);
170
+ assert.equal(warnings.length, 0);
171
+ const agents = actions.map((a) => a.agent);
172
+ assert.ok(agents.includes("claude-code"));
173
+ assert.ok(agents.includes("gemini-cli"));
174
+ });
175
+ });
176
+ describe("compileAgentRuleActions", () => {
177
+ it("returns zero actions and warnings when no detectedAgents overlap", async () => {
178
+ const dir = await makeTmpDir();
179
+ try {
180
+ const rulesFile = path.join(dir, "CLAUDE.md");
181
+ await writeFile(rulesFile, "# Rules");
182
+ const realRoot = await realpath(dir);
183
+ const { actions, warnings } = await compileAgentRuleActions({
184
+ name: "my-rule",
185
+ agents: ["claude-code"],
186
+ path: "CLAUDE.md",
187
+ scope: "global",
188
+ }, dir, dir, realRoot, ["codex"], "/home/test");
189
+ assert.equal(actions.length, 0);
190
+ assert.equal(warnings.length, 0);
191
+ }
192
+ finally {
193
+ await rm(dir, { recursive: true });
194
+ }
195
+ });
196
+ it("does not validate the rules source when no targeted agents overlap", async () => {
197
+ const dir = await makeTmpDir();
198
+ try {
199
+ const { actions, warnings } = await compileAgentRuleActions({
200
+ name: "my-rule",
201
+ agents: ["claude-code"],
202
+ path: "missing.md",
203
+ scope: "global",
204
+ }, dir, dir, dir, ["codex"], "/home/test");
205
+ assert.equal(actions.length, 0);
206
+ assert.equal(warnings.length, 0);
207
+ }
208
+ finally {
209
+ await rm(dir, { recursive: true });
210
+ }
211
+ });
212
+ it("returns a file-write action for claude-code with correct source and target", async () => {
213
+ const dir = await makeTmpDir();
214
+ try {
215
+ const rulesFile = path.join(dir, "CLAUDE.md");
216
+ await writeFile(rulesFile, "# Rules");
217
+ const home = "/home/test";
218
+ const realRoot = await realpath(dir);
219
+ const { actions, warnings } = await compileAgentRuleActions({
220
+ name: "my-rule",
221
+ agents: ["claude-code"],
222
+ path: "CLAUDE.md",
223
+ scope: "global",
224
+ }, dir, dir, realRoot, ["claude-code"], home);
225
+ assert.equal(actions.length, 1);
226
+ assert.equal(warnings.length, 0);
227
+ const action = actions[0];
228
+ assert.equal(action.kind, "file-write");
229
+ assert.equal(action.skill, "my-rule");
230
+ assert.equal(action.agent, "claude-code");
231
+ assert.equal(action.source, rulesFile);
232
+ assertPathEndsWith(action.target, ".claude/CLAUDE.md", `expected target under .claude/CLAUDE.md, got: ${action.target}`);
233
+ assert.equal(action.confidence, "documented");
234
+ }
235
+ finally {
236
+ await rm(dir, { recursive: true });
237
+ }
238
+ });
239
+ it("returns a schema-aware warning and no action for github-copilot rules", async () => {
240
+ const dir = await makeTmpDir();
241
+ try {
242
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
243
+ const realRoot = await realpath(dir);
244
+ const { actions, warnings } = await compileAgentRuleActions({
245
+ name: "my-rule",
246
+ agents: ["github-copilot"],
247
+ path: "rules.md",
248
+ scope: "global",
249
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test");
250
+ assert.equal(actions.length, 0);
251
+ assert.equal(warnings.length, 1);
252
+ assert.equal(warnings[0]?.kind, "confidence");
253
+ // github-copilot is shared-via claude-code with requiresPrimary, so
254
+ // when claude-code is absent a guidance warning is emitted
255
+ assert.match(warnings[0]?.message ?? "", /reads this surface via "claude-code"/);
256
+ assert.match(warnings[0]?.message ?? "", /add "claude-code" to the entry's agents list/);
257
+ }
258
+ finally {
259
+ await rm(dir, { recursive: true });
260
+ }
261
+ });
262
+ it("returns a file-write action for antigravity rules", async () => {
263
+ const dir = await makeTmpDir();
264
+ try {
265
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
266
+ const realRoot = await realpath(dir);
267
+ const { actions, warnings } = await compileAgentRuleActions({
268
+ name: "my-rule",
269
+ agents: ["antigravity"],
270
+ path: "rules.md",
271
+ scope: "global",
272
+ }, dir, dir, realRoot, ["antigravity"], "/home/test", "/repo/test");
273
+ assert.equal(actions.length, 1);
274
+ assert.equal(warnings.length, 0);
275
+ assert.equal(actions[0]?.kind, "file-write");
276
+ assert.equal(actions[0]?.agent, "antigravity");
277
+ assertPathEndsWith(actions[0]?.target ?? "", ".gemini/GEMINI.md", `expected target to end with .gemini/GEMINI.md, got ${actions[0]?.target}`);
278
+ }
279
+ finally {
280
+ await rm(dir, { recursive: true });
281
+ }
282
+ });
283
+ it("gemini-cli and antigravity in the same agentRules entry (global scope) produce ONE deduplicated action", async () => {
284
+ const dir = await makeTmpDir();
285
+ try {
286
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
287
+ const realRoot = await realpath(dir);
288
+ const { actions, warnings } = await compileAgentRuleActions({
289
+ name: "my-rule",
290
+ agents: ["gemini-cli", "antigravity"],
291
+ path: "rules.md",
292
+ scope: "global",
293
+ }, dir, dir, realRoot, ["gemini-cli", "antigravity"], "/home/test", "/repo/test");
294
+ // Both agents target ~/.gemini/GEMINI.md — deduplication emits only one action.
295
+ assert.equal(actions.length, 1, "expected one deduplicated action");
296
+ assert.equal(warnings.length, 0);
297
+ // First agent in list wins.
298
+ assert.equal(actions[0]?.agent, "gemini-cli");
299
+ assertPathEndsWith(actions[0]?.target ?? "", ".gemini/GEMINI.md", `expected target to end with .gemini/GEMINI.md, got: ${actions[0]?.target}`);
300
+ assert.equal(actions[0]?.confidence, "documented");
301
+ }
302
+ finally {
303
+ await rm(dir, { recursive: true });
304
+ }
305
+ });
306
+ it("gemini-cli and antigravity together (repo scope) emit ONE action targeting {repo}/GEMINI.md", async () => {
307
+ const dir = await makeTmpDir();
308
+ try {
309
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
310
+ const repo = "/repo/myproject";
311
+ const realRoot = await realpath(dir);
312
+ const { actions, warnings } = await compileAgentRuleActions({
313
+ name: "my-rule",
314
+ agents: ["gemini-cli", "antigravity"],
315
+ path: "rules.md",
316
+ scope: "repo",
317
+ }, dir, dir, realRoot, ["gemini-cli", "antigravity"], "/home/test", repo);
318
+ assert.equal(actions.length, 1, "expected one deduplicated action");
319
+ assert.equal(warnings.length, 0);
320
+ assert.equal(actions[0]?.agent, "gemini-cli");
321
+ assert.equal(normalizeSlashes(actions[0]?.target ?? ""), `${repo}/GEMINI.md`, "expected target to be {repo}/GEMINI.md");
322
+ }
323
+ finally {
324
+ await rm(dir, { recursive: true });
325
+ }
326
+ });
327
+ it("throws when a supported rules target is given a non-Markdown source path", async () => {
328
+ const dir = await makeTmpDir();
329
+ try {
330
+ await writeFile(path.join(dir, "rules.txt"), "plain text");
331
+ const realRoot = await realpath(dir);
332
+ await assert.rejects(compileAgentRuleActions({
333
+ name: "my-rule",
334
+ agents: ["codex"],
335
+ path: "rules.txt",
336
+ scope: "global",
337
+ }, dir, dir, realRoot, ["codex"], "/home/test"), /must point to a Markdown source file/);
338
+ }
339
+ finally {
340
+ await rm(dir, { recursive: true });
341
+ }
342
+ });
343
+ it("throws when rules source file does not exist", async () => {
344
+ const dir = await makeTmpDir();
345
+ try {
346
+ await assert.rejects(compileAgentRuleActions({
347
+ name: "my-rule",
348
+ agents: ["claude-code"],
349
+ path: "nonexistent.md",
350
+ scope: "global",
351
+ }, dir, dir, dir, ["claude-code"], "/home/test"), (err) => {
352
+ assert.ok(err instanceof Error);
353
+ assert.match(err.message, /nonexistent\.md/);
354
+ return true;
355
+ });
356
+ }
357
+ finally {
358
+ await rm(dir, { recursive: true });
359
+ }
360
+ });
361
+ it("produces actions for each detected agent that has supported rules config", async () => {
362
+ const dir = await makeTmpDir();
363
+ try {
364
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
365
+ const realRoot = await realpath(dir);
366
+ const { actions, warnings } = await compileAgentRuleActions({
367
+ name: "my-rule",
368
+ agents: ["claude-code", "codex"],
369
+ path: "rules.md",
370
+ scope: "global",
371
+ }, dir, dir, realRoot, ["claude-code", "codex"], "/home/test");
372
+ assert.equal(actions.length, 2);
373
+ assert.equal(warnings.length, 0);
374
+ const agents = actions.map((a) => a.agent);
375
+ assert.ok(agents.includes("claude-code"));
376
+ assert.ok(agents.includes("codex"));
377
+ }
378
+ finally {
379
+ await rm(dir, { recursive: true });
380
+ }
381
+ });
382
+ it("scope repo: returns a file-write action targeting {repo}/CLAUDE.md for claude-code", async () => {
383
+ const dir = await makeTmpDir();
384
+ try {
385
+ const rulesFile = path.join(dir, "CLAUDE.md");
386
+ await writeFile(rulesFile, "# Rules");
387
+ const repo = "/repo/myproject";
388
+ const realRoot = await realpath(dir);
389
+ const { actions, warnings } = await compileAgentRuleActions({
390
+ name: "my-rule",
391
+ agents: ["claude-code"],
392
+ path: "CLAUDE.md",
393
+ scope: "repo",
394
+ }, dir, dir, realRoot, ["claude-code"], "/home/test", repo);
395
+ assert.equal(actions.length, 1);
396
+ assert.equal(warnings.length, 0);
397
+ const action = actions[0];
398
+ assert.equal(action.kind, "file-write");
399
+ assert.equal(action.agent, "claude-code");
400
+ assert.equal(normalizeSlashes(action.target), `${repo}/CLAUDE.md`, `expected target at {repo}/CLAUDE.md, got: ${action.target}`);
401
+ }
402
+ finally {
403
+ await rm(dir, { recursive: true });
404
+ }
405
+ });
406
+ it("scope repo: target differs from scope global for claude-code", async () => {
407
+ const dir = await makeTmpDir();
408
+ try {
409
+ const rulesFile = path.join(dir, "CLAUDE.md");
410
+ await writeFile(rulesFile, "# Rules");
411
+ const home = "/home/test";
412
+ const repo = "/repo/myproject";
413
+ const realRoot = await realpath(dir);
414
+ const [globalResult, repoResult] = await Promise.all([
415
+ compileAgentRuleActions({
416
+ name: "my-rule",
417
+ agents: ["claude-code"],
418
+ path: "CLAUDE.md",
419
+ scope: "global",
420
+ }, dir, dir, realRoot, ["claude-code"], home, repo),
421
+ compileAgentRuleActions({
422
+ name: "my-rule",
423
+ agents: ["claude-code"],
424
+ path: "CLAUDE.md",
425
+ scope: "repo",
426
+ }, dir, dir, realRoot, ["claude-code"], home, repo),
427
+ ]);
428
+ assert.equal(globalResult.actions.length, 1);
429
+ assert.equal(repoResult.actions.length, 1);
430
+ assert.notEqual(globalResult.actions[0]?.target, repoResult.actions[0]?.target, "global and repo scopes must produce distinct targets");
431
+ assert.ok(normalizeSlashes(globalResult.actions[0]?.target ?? "").includes(".claude/CLAUDE.md"), "global scope must target ~/.claude/CLAUDE.md");
432
+ assert.equal(normalizeSlashes(repoResult.actions[0]?.target ?? ""), `${repo}/CLAUDE.md`, "repo scope must target {repo}/CLAUDE.md");
433
+ }
434
+ finally {
435
+ await rm(dir, { recursive: true });
436
+ }
437
+ });
438
+ it("scope repo: antigravity targets {repo}/GEMINI.md", async () => {
439
+ const dir = await makeTmpDir();
440
+ try {
441
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
442
+ const repo = "/repo/myproject";
443
+ const realRoot = await realpath(dir);
444
+ const { actions, warnings } = await compileAgentRuleActions({
445
+ name: "my-rule",
446
+ agents: ["antigravity"],
447
+ path: "rules.md",
448
+ scope: "repo",
449
+ }, dir, dir, realRoot, ["antigravity"], "/home/test", repo);
450
+ assert.equal(actions.length, 1);
451
+ assert.equal(warnings.length, 0);
452
+ assert.equal(normalizeSlashes(actions[0]?.target ?? ""), `${repo}/GEMINI.md`, "expected target to be {repo}/GEMINI.md");
453
+ }
454
+ finally {
455
+ await rm(dir, { recursive: true });
456
+ }
457
+ });
458
+ it("scope repo: emits a warning and skips when repo path is not provided", async () => {
459
+ const dir = await makeTmpDir();
460
+ try {
461
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
462
+ const realRoot = await realpath(dir);
463
+ const { actions, warnings } = await compileAgentRuleActions({
464
+ name: "my-rule",
465
+ agents: ["claude-code"],
466
+ path: "rules.md",
467
+ scope: "repo",
468
+ }, dir, dir, realRoot, ["claude-code"], "/home/test");
469
+ assert.equal(actions.length, 0);
470
+ assert.equal(warnings.length, 1);
471
+ assert.equal(warnings[0]?.kind, "confidence");
472
+ assert.match(warnings[0]?.message ?? "", /scope "repo" requires a repository path/);
473
+ }
474
+ finally {
475
+ await rm(dir, { recursive: true });
476
+ }
477
+ });
478
+ it("scope repo: github-copilot emits unsupported warning", async () => {
479
+ const dir = await makeTmpDir();
480
+ try {
481
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
482
+ const realRoot = await realpath(dir);
483
+ const { actions, warnings } = await compileAgentRuleActions({
484
+ name: "my-rule",
485
+ agents: ["github-copilot"],
486
+ path: "rules.md",
487
+ scope: "repo",
488
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/myproject");
489
+ assert.equal(actions.length, 0);
490
+ assert.equal(warnings.length, 1);
491
+ assert.equal(warnings[0]?.kind, "confidence");
492
+ assert.match(warnings[0]?.message ?? "", /claude-code/);
493
+ }
494
+ finally {
495
+ await rm(dir, { recursive: true });
496
+ }
497
+ });
498
+ it("github-copilot + claude-code: emits ONE action for claude-code only (shared-via dedup)", async () => {
499
+ const dir = await makeTmpDir();
500
+ try {
501
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
502
+ const realRoot = await realpath(dir);
503
+ const { actions, warnings } = await compileAgentRuleActions({
504
+ name: "my-rule",
505
+ agents: ["claude-code", "github-copilot"],
506
+ path: "rules.md",
507
+ scope: "global",
508
+ }, dir, dir, realRoot, ["claude-code", "github-copilot"], "/home/test");
509
+ // github-copilot rides claude-code's CLAUDE.md deployment; only one
510
+ // action should be emitted for claude-code.
511
+ assert.equal(actions.length, 1, "expected exactly one action");
512
+ assert.equal(warnings.length, 0);
513
+ assert.equal(actions[0]?.agent, "claude-code");
514
+ assertPathEndsWith(actions[0]?.target ?? "", ".claude/CLAUDE.md", `expected target to end with .claude/CLAUDE.md, got: ${actions[0]?.target}`);
515
+ }
516
+ finally {
517
+ await rm(dir, { recursive: true });
518
+ }
519
+ });
520
+ it("github-copilot without claude-code: emits guidance confidence warning, no action", async () => {
521
+ const dir = await makeTmpDir();
522
+ try {
523
+ await writeFile(path.join(dir, "rules.md"), "---\nname: test-rule\ndescription: test\n---\n# Rules");
524
+ const realRoot = await realpath(dir);
525
+ const { actions, warnings } = await compileAgentRuleActions({
526
+ name: "my-rule",
527
+ agents: ["github-copilot"],
528
+ path: "rules.md",
529
+ scope: "global",
530
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test");
531
+ assert.equal(actions.length, 0);
532
+ assert.equal(warnings.length, 1);
533
+ assert.equal(warnings[0]?.kind, "confidence");
534
+ assert.match(warnings[0]?.message ?? "", /reads this surface via "claude-code"/);
535
+ }
536
+ finally {
537
+ await rm(dir, { recursive: true });
538
+ }
539
+ });
540
+ });
541
+ describe("compilePermissionsActions", () => {
542
+ it("returns zero actions and warnings when no detected agents overlap", () => {
543
+ const { actions, warnings } = compilePermissionsActions({
544
+ name: "safety",
545
+ agents: ["claude-code"],
546
+ config: { permissions: { allow: ["Read"] } },
547
+ }, ["codex"], "/home/test");
548
+ assert.equal(actions.length, 0);
549
+ assert.equal(warnings.length, 0);
550
+ });
551
+ it("returns a config-patch action for claude-code targeting settings.json", () => {
552
+ const home = "/home/test";
553
+ const { actions, warnings } = compilePermissionsActions({
554
+ name: "safety",
555
+ agents: ["claude-code"],
556
+ config: {
557
+ permissions: { allow: ["Read", "Glob"], deny: ["Bash(rm:*)"] },
558
+ },
559
+ }, ["claude-code"], home);
560
+ assert.equal(actions.length, 1);
561
+ assert.equal(warnings.length, 0);
562
+ const action = actions[0];
563
+ assert.equal(action.kind, "config-patch");
564
+ assert.equal(action.skill, "safety");
565
+ assert.equal(action.agent, "claude-code");
566
+ assertPathEndsWith(action.target, ".claude/settings.json", `expected target to end with .claude/settings.json, got: ${action.target}`);
567
+ assert.deepEqual(action.patch, {
568
+ permissions: { allow: ["Read", "Glob"], deny: ["Bash(rm:*)"] },
569
+ });
570
+ assert.equal(action.confidence, "documented");
571
+ });
572
+ it("returns a toml-patch action for codex targeting config.toml", () => {
573
+ const home = "/home/test";
574
+ const { actions, warnings } = compilePermissionsActions({
575
+ name: "codex-approval",
576
+ agents: ["codex"],
577
+ config: { approval_policy: "suggest" },
578
+ }, ["codex"], home);
579
+ assert.equal(actions.length, 1);
580
+ assert.equal(warnings.length, 0);
581
+ const action = actions[0];
582
+ assert.equal(action.kind, "toml-patch");
583
+ assert.equal(action.skill, "codex-approval");
584
+ assert.equal(action.agent, "codex");
585
+ assertPathEndsWith(action.target, ".codex/config.toml", `expected target to end with .codex/config.toml, got: ${action.target}`);
586
+ assert.deepEqual(action.config, { approval_policy: "suggest" });
587
+ assert.equal(action.confidence, "documented");
588
+ });
589
+ it("emits a warning and skips for agents without a permissions surface", () => {
590
+ for (const agentId of ["gemini-cli", "antigravity"]) {
591
+ const { actions, warnings } = compilePermissionsActions({ name: "safety", agents: [agentId], config: {} }, [agentId], "/home/test");
592
+ assert.equal(actions.length, 0, `expected no actions for ${agentId}`);
593
+ assert.equal(warnings.length, 1, `expected one warning for ${agentId}`);
594
+ assert.equal(warnings[0]?.kind, "confidence");
595
+ }
596
+ });
597
+ it("throws for claude-code config with unrecognized top-level keys", () => {
598
+ assert.throws(() => compilePermissionsActions({
599
+ name: "bad",
600
+ agents: ["claude-code"],
601
+ config: { unknown_key: true },
602
+ }, ["claude-code"], "/home/test"), /unrecognized keys/);
603
+ });
604
+ it("throws for claude-code config with non-array allow field", () => {
605
+ assert.throws(() => compilePermissionsActions({
606
+ name: "bad",
607
+ agents: ["claude-code"],
608
+ config: { permissions: { allow: "Read" } },
609
+ }, ["claude-code"], "/home/test"), /array of strings/);
610
+ });
611
+ it("throws for codex config with unrecognized keys", () => {
612
+ assert.throws(() => compilePermissionsActions({
613
+ name: "bad",
614
+ agents: ["codex"],
615
+ config: { unknown_policy: "auto" },
616
+ }, ["codex"], "/home/test"), /unrecognized keys/);
617
+ });
618
+ it("throws for codex config with invalid approval_policy value", () => {
619
+ assert.throws(() => compilePermissionsActions({
620
+ name: "bad",
621
+ agents: ["codex"],
622
+ config: { approval_policy: "always" },
623
+ }, ["codex"], "/home/test"), /approval_policy/);
624
+ });
625
+ it("returns a config-patch action for opencode permissions", () => {
626
+ const home = "/home/test";
627
+ const { actions, warnings } = compilePermissionsActions({
628
+ name: "opencode-perms",
629
+ agents: ["opencode"],
630
+ config: { permissions: { allow: ["Read", "Glob"], ask: ["*"] } },
631
+ }, ["opencode"], home);
632
+ assert.equal(actions.length, 1);
633
+ assert.equal(warnings.length, 0);
634
+ const action = actions[0];
635
+ assert.equal(action.kind, "config-patch");
636
+ assert.equal(action.skill, "opencode-perms");
637
+ assert.equal(action.agent, "opencode");
638
+ assertPathEndsWith(action.target, "opencode/opencode.json", `expected target to end with opencode/opencode.json, got: ${action.target}`);
639
+ assert.deepEqual(action.patch, {
640
+ permissions: { allow: ["Read", "Glob"], ask: ["*"] },
641
+ });
642
+ assert.equal(action.confidence, "documented");
643
+ });
644
+ it("throws for opencode config with unrecognized top-level keys", () => {
645
+ assert.throws(() => compilePermissionsActions({
646
+ name: "bad",
647
+ agents: ["opencode"],
648
+ config: { unknown_key: true },
649
+ }, ["opencode"], "/home/test"), /unrecognized keys/);
650
+ });
651
+ it("throws for opencode config with non-array deny field", () => {
652
+ assert.throws(() => compilePermissionsActions({
653
+ name: "bad",
654
+ agents: ["opencode"],
655
+ config: { permissions: { deny: "Rm" } },
656
+ }, ["opencode"], "/home/test"), /array of strings/);
657
+ });
658
+ it("produces actions for both claude-code and codex when both are detected", () => {
659
+ const home = "/home/test";
660
+ const { actions, warnings } = compilePermissionsActions({
661
+ name: "multi",
662
+ agents: ["claude-code", "codex"],
663
+ config: {},
664
+ }, ["claude-code", "codex"], home);
665
+ assert.equal(warnings.length, 0);
666
+ assert.equal(actions.length, 2);
667
+ const agents = actions.map((a) => a.agent);
668
+ assert.ok(agents.includes("claude-code"));
669
+ assert.ok(agents.includes("codex"));
670
+ });
671
+ });
672
+ describe("compileAgentDefinitionActions", () => {
673
+ // Import is added inline to avoid modifying the top-level imports block
674
+ // (the function is async so we do a dynamic import once and reuse).
675
+ async function getAdapter() {
676
+ const { compileAgentDefinitionActions } = await import("../../src/core/adapters/agent-definitions.js");
677
+ return compileAgentDefinitionActions;
678
+ }
679
+ it("returns zero actions and warnings when no detectedAgents overlap", async () => {
680
+ const compile = await getAdapter();
681
+ const dir = await makeTmpDir();
682
+ try {
683
+ await writeFile(path.join(dir, "agent.md"), "---\nname: test-agent\ndescription: test\n---\n# Agent");
684
+ const realRoot = await realpath(dir);
685
+ const { actions, warnings } = await compile({
686
+ name: "my-agent",
687
+ agents: ["claude-code"],
688
+ path: "agent.md",
689
+ scope: "repo",
690
+ }, dir, dir, realRoot, ["codex"], "/home/test", "/repo/test");
691
+ assert.equal(actions.length, 0);
692
+ assert.equal(warnings.length, 0);
693
+ }
694
+ finally {
695
+ await rm(dir, { recursive: true });
696
+ }
697
+ });
698
+ it("does not validate source when no targeted agents overlap", async () => {
699
+ const compile = await getAdapter();
700
+ const dir = await makeTmpDir();
701
+ try {
702
+ // No file created — validation must not run
703
+ const { actions, warnings } = await compile({
704
+ name: "my-agent",
705
+ agents: ["claude-code"],
706
+ path: "missing.md",
707
+ scope: "repo",
708
+ }, dir, dir, dir, ["codex"], "/home/test", "/repo/test");
709
+ assert.equal(actions.length, 0);
710
+ assert.equal(warnings.length, 0);
711
+ }
712
+ finally {
713
+ await rm(dir, { recursive: true });
714
+ }
715
+ });
716
+ it("returns a file-write action for claude-code with correct target", async () => {
717
+ const compile = await getAdapter();
718
+ const dir = await makeTmpDir();
719
+ try {
720
+ await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
721
+ const realRoot = await realpath(dir);
722
+ const { actions, warnings } = await compile({
723
+ name: "my-agent",
724
+ agents: ["claude-code"],
725
+ path: "my-agent.md",
726
+ scope: "repo",
727
+ }, dir, dir, realRoot, ["claude-code"], "/home/test", "/repo/test");
728
+ assert.equal(warnings.length, 0);
729
+ assert.equal(actions.length, 1);
730
+ const action = actions[0];
731
+ assert.equal(action.kind, "file-write");
732
+ assert.equal(action.skill, "my-agent");
733
+ assert.equal(action.agent, "claude-code");
734
+ assertPathEndsWith(action.target, ".claude/agents/my-agent.md", `expected target under .claude/agents/my-agent.md, got: ${action.target}`);
735
+ assert.equal(action.confidence, "documented");
736
+ }
737
+ finally {
738
+ await rm(dir, { recursive: true });
739
+ }
740
+ });
741
+ it("returns a file-write action for opencode with correct target", async () => {
742
+ const compile = await getAdapter();
743
+ const dir = await makeTmpDir();
744
+ try {
745
+ await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
746
+ const realRoot = await realpath(dir);
747
+ const { actions, warnings } = await compile({
748
+ name: "my-agent",
749
+ agents: ["opencode"],
750
+ path: "my-agent.md",
751
+ scope: "repo",
752
+ }, dir, dir, realRoot, ["opencode"], "/home/test", "/repo/test");
753
+ assert.equal(warnings.length, 0);
754
+ assert.equal(actions.length, 1);
755
+ const action = actions[0];
756
+ assert.equal(action.kind, "file-write");
757
+ assert.equal(action.agent, "opencode");
758
+ assertPathEndsWith(action.target, ".opencode/agents/my-agent.md", `expected target under .opencode/agents/my-agent.md, got: ${action.target}`);
759
+ assert.equal(action.confidence, "documented");
760
+ }
761
+ finally {
762
+ await rm(dir, { recursive: true });
763
+ }
764
+ });
765
+ it("returns a file-write action for github-copilot with the new documented target and migration path", async () => {
766
+ const compile = await getAdapter();
767
+ const dir = await makeTmpDir();
768
+ try {
769
+ await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
770
+ const realRoot = await realpath(dir);
771
+ const { actions, warnings } = await compile({
772
+ name: "my-agent",
773
+ agents: ["github-copilot"],
774
+ path: "my-agent.md",
775
+ scope: "repo",
776
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test");
777
+ assert.equal(warnings.length, 0);
778
+ assert.equal(actions.length, 1);
779
+ const action = actions[0];
780
+ assert.equal(action.kind, "file-write");
781
+ assert.equal(action.agent, "github-copilot");
782
+ assertPathEndsWith(action.target, ".github/copilot/agents/my-agent.md", `expected target under .github/copilot/agents/my-agent.md, got: ${action.target}`);
783
+ assert.deepEqual(action.migratedFrom, [
784
+ "/repo/test/.github/agents/my-agent.agent.md",
785
+ ]);
786
+ assert.equal(action.confidence, "documented");
787
+ }
788
+ finally {
789
+ await rm(dir, { recursive: true });
790
+ }
791
+ });
792
+ it("returns a file-write action for antigravity with correct target", async () => {
793
+ const compile = await getAdapter();
794
+ const dir = await makeTmpDir();
795
+ try {
796
+ await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
797
+ const realRoot = await realpath(dir);
798
+ const { actions, warnings } = await compile({
799
+ name: "my-agent",
800
+ agents: ["antigravity"],
801
+ path: "my-agent.md",
802
+ scope: "repo",
803
+ }, dir, dir, realRoot, ["antigravity"], "/home/test", "/repo/test");
804
+ assert.equal(warnings.length, 0);
805
+ assert.equal(actions.length, 1);
806
+ const action = actions[0];
807
+ assert.equal(action.kind, "file-write");
808
+ assert.equal(action.agent, "antigravity");
809
+ assertPathEndsWith(action.target, ".agents/rules/my-agent.md", `expected target under .agents/rules/my-agent.md, got: ${action.target}`);
810
+ assert.equal(action.confidence, "documented");
811
+ }
812
+ finally {
813
+ await rm(dir, { recursive: true });
814
+ }
815
+ });
816
+ it("emits a warning and no action for codex (unsupported)", async () => {
817
+ const compile = await getAdapter();
818
+ const dir = await makeTmpDir();
819
+ try {
820
+ await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
821
+ const realRoot = await realpath(dir);
822
+ const { actions, warnings } = await compile({
823
+ name: "my-agent",
824
+ agents: ["codex"],
825
+ path: "my-agent.md",
826
+ scope: "repo",
827
+ }, dir, dir, realRoot, ["codex"], "/home/test", "/repo/test");
828
+ assert.equal(actions.length, 0);
829
+ assert.equal(warnings.length, 1);
830
+ assert.equal(warnings[0]?.kind, "confidence");
831
+ assert.match(warnings[0]?.message ?? "", /codex/);
832
+ }
833
+ finally {
834
+ await rm(dir, { recursive: true });
835
+ }
836
+ });
837
+ it("throws when source is a non-Markdown path", async () => {
838
+ const compile = await getAdapter();
839
+ const dir = await makeTmpDir();
840
+ try {
841
+ await writeFile(path.join(dir, "agent.txt"), "plain text");
842
+ const realRoot = await realpath(dir);
843
+ await assert.rejects(compile({
844
+ name: "my-agent",
845
+ agents: ["claude-code"],
846
+ path: "agent.txt",
847
+ scope: "repo",
848
+ }, dir, dir, realRoot, ["claude-code"], "/home/test", "/repo/test"), /must point to a Markdown source file/);
849
+ }
850
+ finally {
851
+ await rm(dir, { recursive: true });
852
+ }
853
+ });
854
+ it("throws when source file does not exist", async () => {
855
+ const compile = await getAdapter();
856
+ const dir = await makeTmpDir();
857
+ try {
858
+ await assert.rejects(compile({
859
+ name: "my-agent",
860
+ agents: ["claude-code"],
861
+ path: "nonexistent.md",
862
+ scope: "repo",
863
+ }, dir, dir, dir, ["claude-code"], "/home/test", "/repo/test"), (err) => {
864
+ assert.ok(err instanceof Error);
865
+ assert.match(err.message, /nonexistent\.md/);
866
+ return true;
867
+ });
868
+ }
869
+ finally {
870
+ await rm(dir, { recursive: true });
871
+ }
872
+ });
873
+ it("produces actions for multiple detected agents", async () => {
874
+ const compile = await getAdapter();
875
+ const dir = await makeTmpDir();
876
+ try {
877
+ await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
878
+ const realRoot = await realpath(dir);
879
+ const { actions, warnings } = await compile({
880
+ name: "my-agent",
881
+ agents: ["claude-code", "opencode", "github-copilot"],
882
+ path: "my-agent.md",
883
+ scope: "repo",
884
+ }, dir, dir, realRoot, ["claude-code", "opencode", "github-copilot"], "/home/test", "/repo/test");
885
+ assert.equal(warnings.length, 0);
886
+ assert.equal(actions.length, 3);
887
+ const agentIds = actions.map((a) => a.agent);
888
+ assert.ok(agentIds.includes("claude-code"));
889
+ assert.ok(agentIds.includes("opencode"));
890
+ assert.ok(agentIds.includes("github-copilot"));
891
+ }
892
+ finally {
893
+ await rm(dir, { recursive: true });
894
+ }
895
+ });
896
+ it("throws for github-copilot when missing both tools and instructions", async () => {
897
+ const compile = await getAdapter();
898
+ const dir = await makeTmpDir();
899
+ try {
900
+ await writeFile(path.join(dir, "bad-copilot.md"), "---\nname: test\ndescription: test\n---\n# Bad");
901
+ const realRoot = await realpath(dir);
902
+ await assert.rejects(compile({
903
+ name: "bad",
904
+ agents: ["github-copilot"],
905
+ path: "bad-copilot.md",
906
+ scope: "repo",
907
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test"), /must define "tools" or "instructions" in frontmatter/);
908
+ }
909
+ finally {
910
+ await rm(dir, { recursive: true });
911
+ }
912
+ });
913
+ it("throws for antigravity when mcp-servers is malformed", async () => {
914
+ const compile = await getAdapter();
915
+ const dir = await makeTmpDir();
916
+ try {
917
+ await writeFile(path.join(dir, "bad-antigravity.md"), "---\nname: test\ndescription: test\nmcp-servers:\n - not-an-object\n---\n# Bad");
918
+ const realRoot = await realpath(dir);
919
+ await assert.rejects(compile({
920
+ name: "bad",
921
+ agents: ["antigravity"],
922
+ path: "bad-antigravity.md",
923
+ scope: "repo",
924
+ }, dir, dir, realRoot, ["antigravity"], "/home/test", "/repo/test"), /must define "mcp-servers" as an object/);
925
+ }
926
+ finally {
927
+ await rm(dir, { recursive: true });
928
+ }
929
+ });
930
+ it("throws for antigravity when individual MCP config is missing required fields", async () => {
931
+ const compile = await getAdapter();
932
+ const dir = await makeTmpDir();
933
+ try {
934
+ await writeFile(path.join(dir, "bad-mcp-config.md"), "---\nname: test\ndescription: test\nmcp-servers:\n my-server: { args: [] }\n---\n# Bad");
935
+ const realRoot = await realpath(dir);
936
+ await assert.rejects(compile({
937
+ name: "bad",
938
+ agents: ["antigravity"],
939
+ path: "bad-mcp-config.md",
940
+ scope: "repo",
941
+ }, dir, dir, realRoot, ["antigravity"], "/home/test", "/repo/test"), /must define either a non-empty "command" or "url"/);
942
+ }
943
+ finally {
944
+ await rm(dir, { recursive: true });
945
+ }
946
+ });
947
+ });