@kuznai/inception-engine 0.21.0 → 0.23.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.
@@ -554,6 +554,78 @@ describe("loadManifest", () => {
554
554
  await rm(dir, { recursive: true });
555
555
  }
556
556
  });
557
+ it("agentRules accepts targetDir with scope: 'repo'", async () => {
558
+ const dir = await makeTmpDir();
559
+ try {
560
+ await writeFile(path.join(dir, "inception.json"), JSON.stringify({
561
+ skills: [],
562
+ agentRules: [
563
+ {
564
+ name: "my-rule",
565
+ agents: ["claude-code"],
566
+ path: "rules/CLAUDE.md",
567
+ scope: "repo",
568
+ targetDir: "apps/frontend",
569
+ },
570
+ ],
571
+ }));
572
+ const manifest = await loadManifest(dir);
573
+ assert.equal(manifest.agentRules[0]?.targetDir, "apps/frontend");
574
+ }
575
+ finally {
576
+ await rm(dir, { recursive: true });
577
+ }
578
+ });
579
+ it("agentRules rejects targetDir with scope: 'global'", async () => {
580
+ const dir = await makeTmpDir();
581
+ try {
582
+ await writeFile(path.join(dir, "inception.json"), JSON.stringify({
583
+ skills: [],
584
+ agentRules: [
585
+ {
586
+ name: "my-rule",
587
+ agents: ["claude-code"],
588
+ path: "rules/CLAUDE.md",
589
+ scope: "global",
590
+ targetDir: "apps/frontend",
591
+ },
592
+ ],
593
+ }));
594
+ await assert.rejects(loadManifest(dir), (err) => {
595
+ assert.ok(err instanceof UserError);
596
+ assert.match(err.message, /targetDir is only supported for scope "repo" or "workspace"/);
597
+ return true;
598
+ });
599
+ }
600
+ finally {
601
+ await rm(dir, { recursive: true });
602
+ }
603
+ });
604
+ it("throws when agentRules targetDir is absolute", async () => {
605
+ const dir = await makeTmpDir();
606
+ try {
607
+ await writeFile(path.join(dir, "inception.json"), JSON.stringify({
608
+ skills: [],
609
+ agentRules: [
610
+ {
611
+ name: "my-rule",
612
+ agents: ["claude-code"],
613
+ path: "rules/CLAUDE.md",
614
+ scope: "repo",
615
+ targetDir: "/absolute/path",
616
+ },
617
+ ],
618
+ }));
619
+ await assert.rejects(loadManifest(dir), (err) => {
620
+ assert.ok(err instanceof UserError);
621
+ assert.match(err.message, /targetDir must be a relative path/);
622
+ return true;
623
+ });
624
+ }
625
+ finally {
626
+ await rm(dir, { recursive: true });
627
+ }
628
+ });
557
629
  it("defaults mcpServers and agentRules to [] when omitted", async () => {
558
630
  const dir = await makeTmpDir();
559
631
  try {
@@ -125,24 +125,22 @@ describe("runPreflight", () => {
125
125
  assert.equal(implementationOnlyWarning, undefined, `expected no implementation-only warning, got: ${implementationOnlyWarning?.message}`);
126
126
  });
127
127
  });
128
- describe("github-copilot planned surfaces", () => {
129
- it("emits a planned surface info notice for devcontainer MCP when Copilot is detected and MCP is targeted", async () => {
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
130
  const manifestWithMcp = {
131
131
  ...emptyManifest,
132
132
  mcpServers: [
133
133
  {
134
134
  name: "test-mcp",
135
- scope: "repo",
135
+ scope: "devcontainer",
136
136
  agents: ["github-copilot"],
137
137
  config: { command: "node", args: ["server.js"] },
138
138
  },
139
139
  ],
140
140
  };
141
141
  const warnings = await runPreflight(baseOptions, manifestWithMcp, "/home/test", ["github-copilot"]);
142
- const plannedWarning = warnings.find((w) => w.kind === "info" &&
143
- /devcontainer\.json MCP support is planned/.test(w.message));
144
- assert.ok(plannedWarning, "expected a planned surface warning for Copilot devcontainer when MCP is targeted");
145
- assert.match(plannedWarning.message, /planned/);
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}`);
146
144
  });
147
145
  });
148
146
  describe("instruction precedence warnings", () => {
@@ -281,6 +279,92 @@ describe("instruction precedence warnings", () => {
281
279
  await rm(sourceDir, { recursive: true });
282
280
  }
283
281
  });
282
+ it("emits Copilot precedence warning when github-copilot has both shared-via (scope: repo) and native (scope: copilot-repo) entries", async () => {
283
+ const sourceDir = await makeTmpDir();
284
+ try {
285
+ await writeFile(path.join(sourceDir, "shared.md"), "# Shared rules");
286
+ await writeFile(path.join(sourceDir, "native.md"), "# Native rules");
287
+ const manifest = {
288
+ ...emptyManifest,
289
+ agentRules: [
290
+ {
291
+ name: "shared-rules",
292
+ path: "shared.md",
293
+ agents: ["claude-code", "github-copilot"],
294
+ scope: "repo",
295
+ },
296
+ {
297
+ name: "native-rules",
298
+ path: "native.md",
299
+ agents: ["github-copilot"],
300
+ scope: "copilot-repo",
301
+ },
302
+ ],
303
+ };
304
+ const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["github-copilot"]);
305
+ const precedenceWarnings = warnings.filter((w) => w.kind === "precedence");
306
+ assert.ok(precedenceWarnings.length > 0, "expected a precedence warning");
307
+ assert.ok(precedenceWarnings.some((w) => w.message.includes("github-copilot") &&
308
+ w.message.includes("CLAUDE.md-shared") &&
309
+ w.message.includes("native Copilot")), `expected Copilot precedence warning, got: ${JSON.stringify(precedenceWarnings)}`);
310
+ }
311
+ finally {
312
+ await rm(sourceDir, { recursive: true });
313
+ }
314
+ });
315
+ it("emits Copilot precedence warning when github-copilot has both global (scope: global) and copilot-scoped entries", async () => {
316
+ const sourceDir = await makeTmpDir();
317
+ try {
318
+ await writeFile(path.join(sourceDir, "shared.md"), "# Shared rules");
319
+ await writeFile(path.join(sourceDir, "scoped.md"), "# Scoped rules");
320
+ const manifest = {
321
+ ...emptyManifest,
322
+ agentRules: [
323
+ {
324
+ name: "shared-rules",
325
+ path: "shared.md",
326
+ agents: ["claude-code", "github-copilot"],
327
+ scope: "global",
328
+ },
329
+ {
330
+ name: "typescript",
331
+ path: "scoped.md",
332
+ agents: ["github-copilot"],
333
+ scope: "copilot-scoped",
334
+ },
335
+ ],
336
+ };
337
+ const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["github-copilot"]);
338
+ const precedenceWarnings = warnings.filter((w) => w.kind === "precedence");
339
+ assert.ok(precedenceWarnings.some((w) => w.message.includes("github-copilot") &&
340
+ w.message.includes("native Copilot")), `expected Copilot precedence warning, got: ${JSON.stringify(precedenceWarnings)}`);
341
+ }
342
+ finally {
343
+ await rm(sourceDir, { recursive: true });
344
+ }
345
+ });
346
+ it("emits no Copilot precedence warning when github-copilot only has native entries", async () => {
347
+ const sourceDir = await makeTmpDir();
348
+ try {
349
+ await writeFile(path.join(sourceDir, "native.md"), "# Native rules");
350
+ const manifest = {
351
+ ...emptyManifest,
352
+ agentRules: [
353
+ {
354
+ name: "native-rules",
355
+ path: "native.md",
356
+ agents: ["github-copilot"],
357
+ scope: "copilot-repo",
358
+ },
359
+ ],
360
+ };
361
+ const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["github-copilot"]);
362
+ assert.equal(warnings.filter((w) => w.kind === "precedence").length, 0);
363
+ }
364
+ finally {
365
+ await rm(sourceDir, { recursive: true });
366
+ }
367
+ });
284
368
  });
285
369
  describe("instruction budget warnings", () => {
286
370
  it("emits budget warning for agentRules source file exceeding 50 KB", async () => {
@@ -29,6 +29,41 @@ describe("planRevert", () => {
29
29
  const actions = planRevert(testManifest, ["codex"], "/home/test");
30
30
  assert.equal(actions.length, 0);
31
31
  });
32
+ it("includes hook and execution-config reverts for detected agents", () => {
33
+ const manifest = {
34
+ ...testManifest,
35
+ hooks: [
36
+ {
37
+ name: "pre-exec-check",
38
+ agents: ["claude-code"],
39
+ config: {
40
+ hooks: {
41
+ PreToolUse: [
42
+ {
43
+ matcher: "Bash",
44
+ hooks: [{ type: "command", command: "scripts/check.sh" }],
45
+ },
46
+ ],
47
+ },
48
+ },
49
+ },
50
+ ],
51
+ executionConfigs: [
52
+ {
53
+ name: "gemini-safety",
54
+ agents: ["gemini-cli"],
55
+ config: { safeMode: true },
56
+ },
57
+ ],
58
+ };
59
+ const actions = planRevert(manifest, ["claude-code", "gemini-cli"], "/home/test");
60
+ assert.ok(actions.some((action) => action.kind === "config-patch" &&
61
+ action.agent === "claude-code" &&
62
+ action.skill === "pre-exec-check"), "expected hook revert action for Claude Code");
63
+ assert.ok(actions.some((action) => action.kind === "config-patch" &&
64
+ action.agent === "gemini-cli" &&
65
+ action.skill === "gemini-safety"), "expected execution config revert action for Gemini CLI");
66
+ });
32
67
  });
33
68
  describe("planRevertAll", () => {
34
69
  it("creates revert actions for all agents in manifest regardless of detection", () => {
@@ -46,6 +81,7 @@ describe("planRevertAll", () => {
46
81
  agentRules: [],
47
82
  permissions: [],
48
83
  agentDefinitions: [],
84
+ hooks: [],
49
85
  };
50
86
  const actions = planRevertAll(multiAgentManifest, "/home/test");
51
87
  assert.equal(actions.length, 3);
@@ -75,6 +111,7 @@ describe("planRevertAll", () => {
75
111
  agentRules: [],
76
112
  permissions: [],
77
113
  agentDefinitions: [],
114
+ hooks: [],
78
115
  };
79
116
  const actions = planRevertAll(manifest, "/home/test");
80
117
  assert.equal(actions.length, 5);
@@ -85,6 +122,36 @@ describe("planRevertAll", () => {
85
122
  assert.match(targets.get("antigravity") ?? "", /\.gemini[\\/]antigravity[\\/]skills[\\/]test-skill$/);
86
123
  assert.match(targets.get("opencode") ?? "", /opencode[\\/]skills[\\/]test-skill$/);
87
124
  });
125
+ it("includes hook and execution-config reverts for all manifest agents", () => {
126
+ const manifest = {
127
+ ...testManifest,
128
+ hooks: [
129
+ {
130
+ name: "pre-exec-check",
131
+ agents: ["claude-code"],
132
+ config: {
133
+ hooks: {
134
+ Stop: [{ hooks: [{ type: "command", command: "notify.sh" }] }],
135
+ },
136
+ },
137
+ },
138
+ ],
139
+ executionConfigs: [
140
+ {
141
+ name: "gemini-safety",
142
+ agents: ["gemini-cli"],
143
+ config: { safeMode: true },
144
+ },
145
+ ],
146
+ };
147
+ const actions = planRevertAll(manifest, "/home/test");
148
+ assert.ok(actions.some((action) => action.kind === "config-patch" &&
149
+ action.agent === "claude-code" &&
150
+ action.skill === "pre-exec-check"), "expected planRevertAll to include Claude hook revert action");
151
+ assert.ok(actions.some((action) => action.kind === "config-patch" &&
152
+ action.agent === "gemini-cli" &&
153
+ action.skill === "gemini-safety"), "expected planRevertAll to include Gemini execution config revert action");
154
+ });
88
155
  });
89
156
  describe("executeRevert", { skip: process.platform === "win32" }, () => {
90
157
  it("removes a symlink registered in the deployment registry", async () => {
@@ -712,6 +779,7 @@ describe("planRevert — mcpServers and agentRules", () => {
712
779
  agentRules: [],
713
780
  permissions: [],
714
781
  agentDefinitions: [],
782
+ hooks: [],
715
783
  };
716
784
  const actions = planRevert(manifest, ["claude-code"], home);
717
785
  assert.equal(actions.length, 1);
@@ -735,6 +803,7 @@ describe("planRevert — mcpServers and agentRules", () => {
735
803
  agentRules: [],
736
804
  permissions: [],
737
805
  agentDefinitions: [],
806
+ hooks: [],
738
807
  };
739
808
  const actions = planRevert(manifest, ["codex"], home);
740
809
  assert.equal(actions.length, 0);
@@ -755,6 +824,7 @@ describe("planRevert — mcpServers and agentRules", () => {
755
824
  ],
756
825
  permissions: [],
757
826
  agentDefinitions: [],
827
+ hooks: [],
758
828
  };
759
829
  const actions = planRevert(manifest, ["claude-code"], home);
760
830
  assert.equal(actions.length, 1);
@@ -778,6 +848,7 @@ describe("planRevert — mcpServers and agentRules", () => {
778
848
  ],
779
849
  permissions: [],
780
850
  agentDefinitions: [],
851
+ hooks: [],
781
852
  };
782
853
  const actions = planRevert(manifest, ["codex"], home);
783
854
  assert.equal(actions.length, 0);
@@ -801,6 +872,7 @@ describe("planRevertAll — mcpServers and agentRules", () => {
801
872
  agentRules: [],
802
873
  permissions: [],
803
874
  agentDefinitions: [],
875
+ hooks: [],
804
876
  };
805
877
  const actions = planRevertAll(manifest, home);
806
878
  assert.equal(actions.length, 2);
@@ -824,6 +896,7 @@ describe("planRevertAll — mcpServers and agentRules", () => {
824
896
  ],
825
897
  permissions: [],
826
898
  agentDefinitions: [],
899
+ hooks: [],
827
900
  };
828
901
  const actions = planRevertAll(manifest, home);
829
902
  assert.equal(actions.length, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.21.0",
3
+ "version": "0.23.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",