@kuznai/inception-engine 0.20.0 → 0.22.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.
@@ -0,0 +1,245 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { describe, it } from "node:test";
5
+ import { executeDeploy } from "../../../src/core/deploy.js";
6
+ import { lookupDeployment, registerDeployment, } from "../../../src/core/ownership.js";
7
+ import { executeRevert } from "../../../src/core/revert.js";
8
+ import { exists, makeTmpDir } from "../../helpers/fs.js";
9
+ describe("agentRules repo/workspace file-write (Windows)", {
10
+ skip: process.platform !== "win32",
11
+ }, () => {
12
+ it("deploys agentRules with scope repo and registers it", async () => {
13
+ const home = await makeTmpDir("ie-rules-home");
14
+ const repo = await makeTmpDir("ie-rules-repo");
15
+ const sourceDir = await makeTmpDir("ie-rules-source");
16
+ try {
17
+ const source = path.join(sourceDir, "CLAUDE.md");
18
+ await writeFile(source, "# My Rules\n");
19
+ const target = path.join(repo, "CLAUDE.md");
20
+ const action = {
21
+ kind: "file-write",
22
+ skill: "my-rules",
23
+ agent: "claude-code",
24
+ source,
25
+ target,
26
+ };
27
+ const { succeeded, failed } = await executeDeploy([action], false, false, home);
28
+ assert.equal(succeeded, 1);
29
+ assert.equal(failed.length, 0);
30
+ assert.ok(await exists(target));
31
+ const entry = await lookupDeployment(home, target);
32
+ assert.ok(entry !== null);
33
+ assert.equal(entry?.kind, "file-write");
34
+ }
35
+ finally {
36
+ await rm(home, { recursive: true, force: true });
37
+ await rm(repo, { recursive: true, force: true });
38
+ await rm(sourceDir, { recursive: true, force: true });
39
+ }
40
+ });
41
+ it("reverts agentRules with scope repo and unregisters it", async () => {
42
+ const home = await makeTmpDir("ie-rules-home");
43
+ const repo = await makeTmpDir("ie-rules-repo");
44
+ try {
45
+ const source = path.join(repo, "source-CLAUDE.md");
46
+ const target = path.join(repo, "CLAUDE.md");
47
+ await writeFile(target, "# My Rules\n");
48
+ await registerDeployment(home, target, {
49
+ kind: "file-write",
50
+ source,
51
+ skill: "my-rules",
52
+ agent: "claude-code",
53
+ });
54
+ const action = {
55
+ kind: "file-write",
56
+ skill: "my-rules",
57
+ agent: "claude-code",
58
+ target,
59
+ };
60
+ const { succeeded, failed } = await executeRevert([action], false, false, home);
61
+ assert.equal(succeeded, 1);
62
+ assert.equal(failed.length, 0);
63
+ assert.ok(!(await exists(target)));
64
+ assert.equal(await lookupDeployment(home, target), null);
65
+ }
66
+ finally {
67
+ await rm(home, { recursive: true, force: true });
68
+ await rm(repo, { recursive: true, force: true });
69
+ }
70
+ });
71
+ it("deploys agentRules with scope workspace and registers it", async () => {
72
+ const home = await makeTmpDir("ie-rules-home");
73
+ const workspace = await makeTmpDir("ie-rules-workspace");
74
+ const sourceDir = await makeTmpDir("ie-rules-source");
75
+ try {
76
+ const source = path.join(sourceDir, "CLAUDE.md");
77
+ await writeFile(source, "# Workspace Rules\n");
78
+ const target = path.join(workspace, "CLAUDE.md");
79
+ const action = {
80
+ kind: "file-write",
81
+ skill: "my-rules",
82
+ agent: "claude-code",
83
+ source,
84
+ target,
85
+ };
86
+ const { succeeded, failed } = await executeDeploy([action], false, false, home);
87
+ assert.equal(succeeded, 1);
88
+ assert.equal(failed.length, 0);
89
+ assert.ok(await exists(target));
90
+ const entry = await lookupDeployment(home, target);
91
+ assert.ok(entry !== null);
92
+ assert.equal(entry?.kind, "file-write");
93
+ }
94
+ finally {
95
+ await rm(home, { recursive: true, force: true });
96
+ await rm(workspace, { recursive: true, force: true });
97
+ await rm(sourceDir, { recursive: true, force: true });
98
+ }
99
+ });
100
+ it("reverts agentRules with scope workspace and unregisters it", async () => {
101
+ const home = await makeTmpDir("ie-rules-home");
102
+ const workspace = await makeTmpDir("ie-rules-workspace");
103
+ try {
104
+ const source = path.join(workspace, "source-CLAUDE.md");
105
+ const target = path.join(workspace, "CLAUDE.md");
106
+ await writeFile(target, "# Workspace Rules\n");
107
+ await registerDeployment(home, target, {
108
+ kind: "file-write",
109
+ source,
110
+ skill: "my-rules",
111
+ agent: "claude-code",
112
+ });
113
+ const action = {
114
+ kind: "file-write",
115
+ skill: "my-rules",
116
+ agent: "claude-code",
117
+ target,
118
+ };
119
+ const { succeeded, failed } = await executeRevert([action], false, false, home);
120
+ assert.equal(succeeded, 1);
121
+ assert.equal(failed.length, 0);
122
+ assert.ok(!(await exists(target)));
123
+ assert.equal(await lookupDeployment(home, target), null);
124
+ }
125
+ finally {
126
+ await rm(home, { recursive: true, force: true });
127
+ await rm(workspace, { recursive: true, force: true });
128
+ }
129
+ });
130
+ });
131
+ describe("Antigravity frontmatter-emit (Windows)", {
132
+ skip: process.platform !== "win32",
133
+ }, () => {
134
+ it("deploys frontmatter-emit for Antigravity and registers it", async () => {
135
+ const home = await makeTmpDir("ie-fm-home");
136
+ const repo = await makeTmpDir("ie-fm-repo");
137
+ try {
138
+ const target = path.join(repo, ".agents", "rules", "my-mcp.md");
139
+ const frontmatter = {
140
+ "mcp-servers": {
141
+ "my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
142
+ },
143
+ };
144
+ const action = {
145
+ kind: "frontmatter-emit",
146
+ skill: "my-mcp",
147
+ agent: "antigravity",
148
+ target,
149
+ frontmatter,
150
+ };
151
+ const { succeeded, failed } = await executeDeploy([action], false, false, home);
152
+ assert.equal(succeeded, 1);
153
+ assert.equal(failed.length, 0);
154
+ assert.ok(await exists(target));
155
+ const content = await readFile(target, "utf-8");
156
+ assert.ok(content.includes("mcp-servers:"));
157
+ const entry = await lookupDeployment(home, target);
158
+ assert.ok(entry !== null);
159
+ assert.equal(entry?.kind, "frontmatter-emit");
160
+ assert.equal(entry.created, true);
161
+ }
162
+ finally {
163
+ await rm(home, { recursive: true, force: true });
164
+ await rm(repo, { recursive: true, force: true });
165
+ }
166
+ });
167
+ it("reverts frontmatter-emit for Antigravity and removes file when no body", async () => {
168
+ const home = await makeTmpDir("ie-fm-home");
169
+ const repo = await makeTmpDir("ie-fm-repo");
170
+ try {
171
+ const target = path.join(repo, ".agents", "rules", "my-mcp.md");
172
+ const frontmatter = {
173
+ "mcp-servers": {
174
+ "my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
175
+ },
176
+ };
177
+ // Deploy first so the file and registry entry are created.
178
+ const deployAction = {
179
+ kind: "frontmatter-emit",
180
+ skill: "my-mcp",
181
+ agent: "antigravity",
182
+ target,
183
+ frontmatter,
184
+ };
185
+ await executeDeploy([deployAction], false, false, home);
186
+ const action = {
187
+ kind: "frontmatter-emit",
188
+ skill: "my-mcp",
189
+ agent: "antigravity",
190
+ target,
191
+ };
192
+ const { succeeded, failed } = await executeRevert([action], false, false, home);
193
+ assert.equal(succeeded, 1);
194
+ assert.equal(failed.length, 0);
195
+ assert.ok(!(await exists(target)));
196
+ assert.equal(await lookupDeployment(home, target), null);
197
+ }
198
+ finally {
199
+ await rm(home, { recursive: true, force: true });
200
+ await rm(repo, { recursive: true, force: true });
201
+ }
202
+ });
203
+ it("reverts frontmatter-emit for Antigravity preserving body when file had prior content", async () => {
204
+ const home = await makeTmpDir("ie-fm-home");
205
+ const repo = await makeTmpDir("ie-fm-repo");
206
+ try {
207
+ const target = path.join(repo, ".agents", "rules", "my-mcp.md");
208
+ await mkdir(path.dirname(target), { recursive: true });
209
+ await writeFile(target, "# My Rules\n");
210
+ const undoPatch = { "mcp-servers": null };
211
+ await registerDeployment(home, target, {
212
+ kind: "frontmatter-emit",
213
+ patch: {
214
+ "mcp-servers": {
215
+ "my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
216
+ },
217
+ },
218
+ undoPatch,
219
+ created: false,
220
+ hadFrontmatter: false,
221
+ skill: "my-mcp",
222
+ agent: "antigravity",
223
+ });
224
+ const action = {
225
+ kind: "frontmatter-emit",
226
+ skill: "my-mcp",
227
+ agent: "antigravity",
228
+ target,
229
+ };
230
+ const { succeeded, failed } = await executeRevert([action], false, false, home);
231
+ assert.equal(succeeded, 1);
232
+ assert.equal(failed.length, 0);
233
+ // File should still exist because it had body content.
234
+ assert.ok(await exists(target));
235
+ const content = await readFile(target, "utf-8");
236
+ assert.ok(!content.includes("mcp-servers:"));
237
+ assert.ok(content.includes("My Rules"));
238
+ assert.equal(await lookupDeployment(home, target), null);
239
+ }
240
+ finally {
241
+ await rm(home, { recursive: true, force: true });
242
+ await rm(repo, { recursive: true, force: true });
243
+ }
244
+ });
245
+ });
@@ -137,13 +137,13 @@ describe("compileMcpServerActions", () => {
137
137
  servers: { "my-mcp": { command: "s" } },
138
138
  });
139
139
  });
140
- it("returns a frontmatter-emit action for antigravity MCP", () => {
140
+ it("returns a frontmatter-emit action for antigravity MCP with scope: repo", () => {
141
141
  const home = "/home/test";
142
142
  const { actions, warnings } = compileMcpServerActions({
143
143
  name: "my-mcp",
144
144
  agents: ["antigravity"],
145
145
  config: { command: "s" },
146
- scope: "global",
146
+ scope: "repo",
147
147
  }, ["antigravity"], home, "/repo/test");
148
148
  assert.equal(actions.length, 1);
149
149
  assert.equal(warnings.length, 0);
@@ -151,6 +151,20 @@ describe("compileMcpServerActions", () => {
151
151
  assert.equal(actions[0]?.agent, "antigravity");
152
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
153
  });
154
+ it("returns a config-patch action for antigravity MCP with scope: global", () => {
155
+ const home = "/home/test";
156
+ const { actions, warnings } = compileMcpServerActions({
157
+ name: "my-mcp",
158
+ agents: ["antigravity"],
159
+ config: { command: "s" },
160
+ scope: "global",
161
+ }, ["antigravity"], home);
162
+ assert.equal(actions.length, 1);
163
+ assert.equal(warnings.length, 0);
164
+ assert.equal(actions[0]?.kind, "config-patch");
165
+ assert.equal(actions[0]?.agent, "antigravity");
166
+ assertPathEndsWith(actions[0]?.target ?? "", ".gemini/antigravity/mcp_config.json", `expected target to end with .gemini/antigravity/mcp_config.json, got ${actions[0]?.target}`);
167
+ });
154
168
  it("throws when a supported MCP target is missing both command and url", () => {
155
169
  assert.throws(() => compileMcpServerActions({
156
170
  name: "my-mcp",
@@ -910,6 +924,76 @@ describe("compileAgentDefinitionActions", () => {
910
924
  await rm(dir, { recursive: true });
911
925
  }
912
926
  });
927
+ it("accepts github-copilot tools as empty array", async () => {
928
+ const compile = await getAdapter();
929
+ const dir = await makeTmpDir();
930
+ try {
931
+ await writeFile(path.join(dir, "agent.md"), "---\nname: test\ndescription: test\ntools: []\n---\n# Agent");
932
+ const realRoot = await realpath(dir);
933
+ const result = await compile({
934
+ name: "agent",
935
+ agents: ["github-copilot"],
936
+ path: "agent.md",
937
+ scope: "repo",
938
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test");
939
+ assert.ok(result.actions.length > 0, "expected at least one action");
940
+ }
941
+ finally {
942
+ await rm(dir, { recursive: true });
943
+ }
944
+ });
945
+ it("accepts github-copilot tools as array of strings", async () => {
946
+ const compile = await getAdapter();
947
+ const dir = await makeTmpDir();
948
+ try {
949
+ await writeFile(path.join(dir, "agent.md"), "---\nname: test\ndescription: test\ntools:\n - github\n - codebase\n---\n# Agent");
950
+ const realRoot = await realpath(dir);
951
+ const result = await compile({
952
+ name: "agent",
953
+ agents: ["github-copilot"],
954
+ path: "agent.md",
955
+ scope: "repo",
956
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test");
957
+ assert.ok(result.actions.length > 0, "expected at least one action");
958
+ }
959
+ finally {
960
+ await rm(dir, { recursive: true });
961
+ }
962
+ });
963
+ it("throws for github-copilot when tools is not an array", async () => {
964
+ const compile = await getAdapter();
965
+ const dir = await makeTmpDir();
966
+ try {
967
+ await writeFile(path.join(dir, "bad-tools.md"), "---\nname: test\ndescription: test\ntools: not-an-array\n---\n# Bad");
968
+ const realRoot = await realpath(dir);
969
+ await assert.rejects(compile({
970
+ name: "bad",
971
+ agents: ["github-copilot"],
972
+ path: "bad-tools.md",
973
+ scope: "repo",
974
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test"), /"tools" field that must be an array/);
975
+ }
976
+ finally {
977
+ await rm(dir, { recursive: true });
978
+ }
979
+ });
980
+ it("throws for github-copilot when tools contains a non-string entry", async () => {
981
+ const compile = await getAdapter();
982
+ const dir = await makeTmpDir();
983
+ try {
984
+ await writeFile(path.join(dir, "bad-tools.md"), "---\nname: test\ndescription: test\ntools:\n - github\n - 123\n---\n# Bad");
985
+ const realRoot = await realpath(dir);
986
+ await assert.rejects(compile({
987
+ name: "bad",
988
+ agents: ["github-copilot"],
989
+ path: "bad-tools.md",
990
+ scope: "repo",
991
+ }, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test"), /"tools" entry that must be a string/);
992
+ }
993
+ finally {
994
+ await rm(dir, { recursive: true });
995
+ }
996
+ });
913
997
  it("throws for antigravity when mcp-servers is malformed", async () => {
914
998
  const compile = await getAdapter();
915
999
  const dir = await makeTmpDir();
@@ -30,6 +30,7 @@ const testManifest = {
30
30
  agentRules: [],
31
31
  permissions: [],
32
32
  agentDefinitions: [],
33
+ hooks: [],
33
34
  };
34
35
  describe("planDeploy", () => {
35
36
  it("creates actions for detected agents only", async () => {
@@ -647,8 +648,9 @@ describe("planDeploy", () => {
647
648
  assert.ok(geminiAction, "expected a gemini-cli action");
648
649
  assert.match(geminiAction.target, /[\\/]\.gemini[\\/]settings\.json$/);
649
650
  assert.ok(antigravityAction, "expected an antigravity action");
650
- // Antigravity's target for mcpServer is .agents/rules/{name}.md
651
- assert.match(antigravityAction.target, /[\\/]\.agents[\\/]rules[\\/]my-mcp\.md$/);
651
+ // Antigravity's target for global mcpServer is .gemini/antigravity/mcp_config.json
652
+ assert.equal(antigravityAction.kind, "config-patch");
653
+ assert.match(antigravityAction.target, /[\\/]\.gemini[\\/]antigravity[\\/]mcp_config\.json$/);
652
654
  }
653
655
  finally {
654
656
  await rm(sourceDir, { recursive: true });
@@ -802,7 +804,7 @@ describe("planDeploy", () => {
802
804
  name: "my-tool",
803
805
  agents: ["antigravity"],
804
806
  config: { command: "npx", args: ["-y", "my-tool"] },
805
- scope: "global",
807
+ scope: "repo",
806
808
  },
807
809
  ],
808
810
  agentRules: [],
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,153 @@
1
+ import assert from "node:assert/strict";
2
+ import { cp, readFile, rm } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ import path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { makeTmpDir } from "../helpers/fs.js";
7
+ import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
8
+ const PROJECT_ROOT = path.resolve(import.meta.dirname, "..", "..");
9
+ const FIXTURE_DIR = path.join(PROJECT_ROOT, "test", "fixtures", "readme-sample");
10
+ function run(args, env) {
11
+ return new Promise((resolve) => {
12
+ const child = spawn(process.execPath, [path.join(PROJECT_ROOT, "src", "index.ts"), ...args], {
13
+ cwd: PROJECT_ROOT,
14
+ env: { ...process.env, ...env },
15
+ });
16
+ let stdout = "";
17
+ let stderr = "";
18
+ child.stdout.on("data", (d) => {
19
+ stdout += d.toString();
20
+ });
21
+ child.stderr.on("data", (d) => {
22
+ stderr += d.toString();
23
+ });
24
+ child.on("close", (code) => {
25
+ resolve({ stdout, stderr, code: code ?? 1 });
26
+ });
27
+ });
28
+ }
29
+ /**
30
+ * Extract the JSON manifest object from `init --plan` stdout.
31
+ * The output format is:
32
+ * [plan] Would write /path/inception.json with N skill(s), ...:
33
+ * <blank line>
34
+ * { ...json... }
35
+ * The [plan] prefix may include ANSI escape codes; the first `{` is
36
+ * reliably the start of the JSON object.
37
+ */
38
+ function extractPlanJson(stdout) {
39
+ const jsonStart = stdout.indexOf("{");
40
+ assert.ok(jsonStart !== -1, `Could not find JSON object in --plan stdout:\n${stdout}`);
41
+ return JSON.parse(stdout.slice(jsonStart));
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Block 1: init --plan against the real limbo/ tree (read-only)
45
+ // ---------------------------------------------------------------------------
46
+ describe("init --plan against real limbo/ tree", () => {
47
+ it("emits the 4 limbo skills with all 5 agents and empty other sections", async () => {
48
+ const limboDir = path.join(PROJECT_ROOT, "limbo");
49
+ const { stdout, code } = await run(["init", limboDir, "--plan"]);
50
+ assert.equal(code, 0, `init --plan exited non-zero.\nstdout: ${stdout}`);
51
+ assert.ok(stdout.includes("[plan]"), `missing [plan] prefix:\n${stdout}`);
52
+ const manifest = extractPlanJson(stdout);
53
+ // Exactly 4 skills
54
+ assert.equal(manifest.skills.length, 4, `expected 4 skills, got ${manifest.skills.length}: ${JSON.stringify(manifest.skills.map((s) => s.name))}`);
55
+ // Names sorted alphabetically (sort before comparing - filesystem order not guaranteed)
56
+ const names = manifest.skills.map((s) => s.name).sort();
57
+ assert.deepEqual(names, [
58
+ "inception",
59
+ "interstellar",
60
+ "tenet",
61
+ "the-prestige",
62
+ ]);
63
+ // Each skill has exactly the 5 portability agents (github-copilot excluded)
64
+ const expectedAgents = [
65
+ "antigravity",
66
+ "claude-code",
67
+ "codex",
68
+ "gemini-cli",
69
+ "opencode",
70
+ ];
71
+ for (const skill of manifest.skills) {
72
+ assert.deepEqual(skill.agents.slice().sort(), expectedAgents, `skill "${skill.name}" agents mismatch: ${JSON.stringify(skill.agents)}`);
73
+ assertPathEndsWith(skill.path, `skills/${skill.name}`, `skill "${skill.name}" path should end with skills/${skill.name}`);
74
+ }
75
+ // All other sections are empty arrays
76
+ assert.deepEqual(manifest.mcpServers, [], "mcpServers should be []");
77
+ assert.deepEqual(manifest.agentRules, [], "agentRules should be []");
78
+ assert.deepEqual(manifest.agentDefinitions, [], "agentDefinitions should be []");
79
+ assert.deepEqual(manifest.files, [], "files should be []");
80
+ assert.deepEqual(manifest.configs, [], "configs should be []");
81
+ });
82
+ });
83
+ // ---------------------------------------------------------------------------
84
+ // Block 2: init against README-shaped fixture (copied to temp dir)
85
+ // ---------------------------------------------------------------------------
86
+ describe("init against readme-sample fixture", () => {
87
+ it("generates a manifest covering all README-documented init discovery paths", async () => {
88
+ const tmpDir = await makeTmpDir("ie-fixture-readme");
89
+ try {
90
+ // Copy the static fixture tree (including .claude/ hidden dir) into tmpDir
91
+ await cp(FIXTURE_DIR, tmpDir, { recursive: true });
92
+ const { stdout, code } = await run(["init", tmpDir]);
93
+ assert.equal(code, 0, `init exited non-zero.\nstdout: ${stdout}`);
94
+ const manifest = JSON.parse(await readFile(path.join(tmpDir, "inception.json"), "utf-8"));
95
+ // --- skills: 2 entries, github-copilot excluded per portability rules ---
96
+ assert.equal(manifest.skills.length, 2, `expected 2 skills, got ${manifest.skills.length}: ${JSON.stringify(manifest.skills.map((s) => s.name))}`);
97
+ const skillNames = manifest.skills.map((s) => s.name).sort();
98
+ assert.deepEqual(skillNames, ["my-skill", "other-skill"]);
99
+ const expectedSkillAgents = [
100
+ "antigravity",
101
+ "claude-code",
102
+ "codex",
103
+ "gemini-cli",
104
+ "opencode",
105
+ ];
106
+ for (const skill of manifest.skills) {
107
+ assert.deepEqual(skill.agents.slice().sort(), expectedSkillAgents, `skill "${skill.name}" should have all 5 agents (not github-copilot): ${JSON.stringify(skill.agents)}`);
108
+ assertPathEndsWith(skill.path, `skills/${skill.name}`, `skill "${skill.name}" path should end with skills/${skill.name}`);
109
+ }
110
+ // --- agentRules: 3 entries for CLAUDE.md, AGENTS.md, GEMINI.md ---
111
+ assert.equal(manifest.agentRules.length, 3, `expected 3 agentRules, got ${manifest.agentRules.length}: ${JSON.stringify(manifest.agentRules.map((r) => r.name))}`);
112
+ const claudeRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("CLAUDE.md"));
113
+ assert.ok(claudeRule, "should have an agentRules entry for CLAUDE.md");
114
+ assert.deepEqual(claudeRule?.agents.slice().sort(), ["claude-code"], `CLAUDE.md agents: ${JSON.stringify(claudeRule?.agents)}`);
115
+ assert.equal(claudeRule?.scope, "global");
116
+ const agentsRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("AGENTS.md"));
117
+ assert.ok(agentsRule, "should have an agentRules entry for AGENTS.md");
118
+ assert.deepEqual(agentsRule?.agents.slice().sort(), ["codex", "opencode"], `AGENTS.md agents: ${JSON.stringify(agentsRule?.agents)}`);
119
+ assert.equal(agentsRule?.scope, "global");
120
+ const geminiRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("GEMINI.md"));
121
+ assert.ok(geminiRule, "should have an agentRules entry for GEMINI.md");
122
+ assert.deepEqual(geminiRule?.agents.slice().sort(), ["gemini-cli"],
123
+ // antigravity is shared-via gemini-cli and excluded from init defaults
124
+ `GEMINI.md agents should be [gemini-cli] only: ${JSON.stringify(geminiRule?.agents)}`);
125
+ assert.equal(geminiRule?.scope, "global");
126
+ // --- mcpServers: 1 entry round-tripped from mcp-servers.json sidecar ---
127
+ assert.equal(manifest.mcpServers.length, 1, `expected 1 mcpServer, got ${manifest.mcpServers.length}`);
128
+ const mcp = manifest.mcpServers[0];
129
+ assert.equal(mcp.name, "my-mcp-server");
130
+ assert.deepEqual(mcp.agents.slice().sort(), [
131
+ "claude-code",
132
+ "codex",
133
+ "gemini-cli",
134
+ "opencode",
135
+ ]);
136
+ assert.equal(mcp.config.command, "npx");
137
+ assert.deepEqual(mcp.config.args, ["-y", "@example/mcp-server"]);
138
+ // --- agentDefinitions: 1 entry from .claude/agents/ -> claude-code ---
139
+ assert.equal(manifest.agentDefinitions.length, 1, `expected 1 agentDefinition, got ${manifest.agentDefinitions.length}`);
140
+ const def = manifest.agentDefinitions[0];
141
+ assert.equal(def.name, "code-reviewer");
142
+ assert.deepEqual(def.agents, ["claude-code"]);
143
+ assert.equal(def.scope, "repo");
144
+ assertPathEndsWith(def.path, ".claude/agents/code-reviewer.md", "code-reviewer path should end with .claude/agents/code-reviewer.md");
145
+ // --- files and configs stay empty (no sidecar manifests for them) ---
146
+ assert.deepEqual(manifest.files, [], "files should be []");
147
+ assert.deepEqual(manifest.configs, [], "configs should be []");
148
+ }
149
+ finally {
150
+ await rm(tmpDir, { recursive: true });
151
+ }
152
+ });
153
+ });
@@ -125,6 +125,24 @@ describe("runPreflight", () => {
125
125
  assert.equal(implementationOnlyWarning, undefined, `expected no implementation-only warning, got: ${implementationOnlyWarning?.message}`);
126
126
  });
127
127
  });
128
+ describe("github-copilot devcontainer support", () => {
129
+ it("emits no capability warning for devcontainer MCP when Copilot is detected and devcontainer scope is targeted", async () => {
130
+ const manifestWithMcp = {
131
+ ...emptyManifest,
132
+ mcpServers: [
133
+ {
134
+ name: "test-mcp",
135
+ scope: "devcontainer",
136
+ agents: ["github-copilot"],
137
+ config: { command: "node", args: ["server.js"] },
138
+ },
139
+ ],
140
+ };
141
+ const warnings = await runPreflight(baseOptions, manifestWithMcp, "/home/test", ["github-copilot"]);
142
+ const capabilityWarning = warnings.find((w) => w.kind === "info" && /mcpServers/.test(w.message));
143
+ assert.equal(capabilityWarning, undefined, `expected no MCP capability warning for devcontainer scope, got: ${capabilityWarning?.message}`);
144
+ });
145
+ });
128
146
  describe("instruction precedence warnings", () => {
129
147
  it("emits duplicate-content precedence warning when same source used in both global and repo scope for an agent", async () => {
130
148
  const sourceDir = await makeTmpDir();
@@ -46,6 +46,7 @@ describe("planRevertAll", () => {
46
46
  agentRules: [],
47
47
  permissions: [],
48
48
  agentDefinitions: [],
49
+ hooks: [],
49
50
  };
50
51
  const actions = planRevertAll(multiAgentManifest, "/home/test");
51
52
  assert.equal(actions.length, 3);
@@ -75,6 +76,7 @@ describe("planRevertAll", () => {
75
76
  agentRules: [],
76
77
  permissions: [],
77
78
  agentDefinitions: [],
79
+ hooks: [],
78
80
  };
79
81
  const actions = planRevertAll(manifest, "/home/test");
80
82
  assert.equal(actions.length, 5);
@@ -712,6 +714,7 @@ describe("planRevert — mcpServers and agentRules", () => {
712
714
  agentRules: [],
713
715
  permissions: [],
714
716
  agentDefinitions: [],
717
+ hooks: [],
715
718
  };
716
719
  const actions = planRevert(manifest, ["claude-code"], home);
717
720
  assert.equal(actions.length, 1);
@@ -735,6 +738,7 @@ describe("planRevert — mcpServers and agentRules", () => {
735
738
  agentRules: [],
736
739
  permissions: [],
737
740
  agentDefinitions: [],
741
+ hooks: [],
738
742
  };
739
743
  const actions = planRevert(manifest, ["codex"], home);
740
744
  assert.equal(actions.length, 0);
@@ -755,6 +759,7 @@ describe("planRevert — mcpServers and agentRules", () => {
755
759
  ],
756
760
  permissions: [],
757
761
  agentDefinitions: [],
762
+ hooks: [],
758
763
  };
759
764
  const actions = planRevert(manifest, ["claude-code"], home);
760
765
  assert.equal(actions.length, 1);
@@ -778,6 +783,7 @@ describe("planRevert — mcpServers and agentRules", () => {
778
783
  ],
779
784
  permissions: [],
780
785
  agentDefinitions: [],
786
+ hooks: [],
781
787
  };
782
788
  const actions = planRevert(manifest, ["codex"], home);
783
789
  assert.equal(actions.length, 0);
@@ -801,6 +807,7 @@ describe("planRevertAll — mcpServers and agentRules", () => {
801
807
  agentRules: [],
802
808
  permissions: [],
803
809
  agentDefinitions: [],
810
+ hooks: [],
804
811
  };
805
812
  const actions = planRevertAll(manifest, home);
806
813
  assert.equal(actions.length, 2);
@@ -824,6 +831,7 @@ describe("planRevertAll — mcpServers and agentRules", () => {
824
831
  ],
825
832
  permissions: [],
826
833
  agentDefinitions: [],
834
+ hooks: [],
827
835
  };
828
836
  const actions = planRevertAll(manifest, home);
829
837
  assert.equal(actions.length, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Deploy AI agent skills from a git repo to user home directories",
5
5
  "license": "MIT",
6
6
  "author": "Damian Piątkowski",