@kuznai/inception-engine 0.22.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.
@@ -68,6 +68,32 @@ describe("planDeploy", () => {
68
68
  await rm(sourceDir, { recursive: true });
69
69
  }
70
70
  });
71
+ it("includes executionConfigs actions for supported detected agents", async () => {
72
+ const sourceDir = await makeTmpDir();
73
+ try {
74
+ const manifest = {
75
+ ...testManifest,
76
+ executionConfigs: [
77
+ {
78
+ name: "gemini-safety",
79
+ agents: ["gemini-cli"],
80
+ config: { safeMode: true },
81
+ },
82
+ ],
83
+ };
84
+ await createSkillSource(sourceDir, "skills/test-skill");
85
+ const { actions } = await planDeploy(manifest, sourceDir, ["gemini-cli"], "/home/test");
86
+ const executionAction = actions.find((action) => action.kind === "config-patch" &&
87
+ action.agent === "gemini-cli" &&
88
+ action.skill === "gemini-safety");
89
+ assert.ok(executionAction, "expected planDeploy to include the gemini executionConfigs patch");
90
+ assert.deepEqual(executionAction.patch, { safeMode: true });
91
+ assertPathEndsWith(executionAction.target, ".gemini/settings.json", `expected Gemini execution config target under .gemini/settings.json, got: ${executionAction.target}`);
92
+ }
93
+ finally {
94
+ await rm(sourceDir, { recursive: true });
95
+ }
96
+ });
71
97
  it("throws when skill source path does not exist", async () => {
72
98
  const sourceDir = await makeTmpDir();
73
99
  try {
@@ -1,10 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
- import { cp, readFile, rm } from "node:fs/promises";
2
+ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { spawn } from "node:child_process";
4
4
  import path from "node:path";
5
5
  import { describe, it } from "node:test";
6
+ import { runInit } from "../../src/core/init.js";
7
+ import { logger } from "../../src/logger.js";
6
8
  import { makeTmpDir } from "../helpers/fs.js";
7
9
  import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
10
+ logger.silence();
8
11
  const PROJECT_ROOT = path.resolve(import.meta.dirname, "..", "..");
9
12
  const FIXTURE_DIR = path.join(PROJECT_ROOT, "test", "fixtures", "readme-sample");
10
13
  function run(args, env) {
@@ -151,3 +154,84 @@ describe("init against readme-sample fixture", () => {
151
154
  }
152
155
  });
153
156
  });
157
+ // ---------------------------------------------------------------------------
158
+ // Block 3: init discovery of GitHub Copilot native instruction surfaces
159
+ // ---------------------------------------------------------------------------
160
+ describe("init Copilot native instruction discovery", () => {
161
+ it("runInit dryRun succeeds when .github/copilot-instructions.md is present", async () => {
162
+ const dir = await makeTmpDir();
163
+ try {
164
+ await mkdir(path.join(dir, ".github"), { recursive: true });
165
+ await writeFile(path.join(dir, ".github", "copilot-instructions.md"), "# Copilot instructions");
166
+ // Use dryRun to avoid writing inception.json
167
+ const result = await runInit({
168
+ directory: dir,
169
+ agents: null,
170
+ dryRun: true,
171
+ force: false,
172
+ verbose: false,
173
+ });
174
+ assert.equal(result, 0);
175
+ }
176
+ finally {
177
+ await rm(dir, { recursive: true });
178
+ }
179
+ });
180
+ it("init --plan output includes copilot-repo scope for .github/copilot-instructions.md", async () => {
181
+ const dir = await makeTmpDir();
182
+ try {
183
+ await mkdir(path.join(dir, ".github"), { recursive: true });
184
+ await writeFile(path.join(dir, ".github", "copilot-instructions.md"), "# Copilot instructions");
185
+ const { stdout, code } = await run(["init", dir, "--plan"]);
186
+ assert.equal(code, 0, `init --plan failed:\n${stdout}`);
187
+ const manifest = extractPlanJson(stdout);
188
+ const copilotEntry = manifest.agentRules.find((r) => r.scope === "copilot-repo");
189
+ assert.ok(copilotEntry, `expected a copilot-repo entry in agentRules, got: ${JSON.stringify(manifest.agentRules)}`);
190
+ assert.deepEqual(copilotEntry.agents, ["github-copilot"]);
191
+ assertPathEndsWith(copilotEntry.path, ".github/copilot-instructions.md", `copilot-repo entry path should end with .github/copilot-instructions.md`);
192
+ }
193
+ finally {
194
+ await rm(dir, { recursive: true });
195
+ }
196
+ });
197
+ it("init --plan output includes copilot-scoped scope for .github/instructions/*.instructions.md", async () => {
198
+ const dir = await makeTmpDir();
199
+ try {
200
+ await mkdir(path.join(dir, ".github", "instructions"), {
201
+ recursive: true,
202
+ });
203
+ await writeFile(path.join(dir, ".github", "instructions", "typescript.instructions.md"), "# TypeScript scoped instructions");
204
+ await writeFile(path.join(dir, ".github", "instructions", "python.instructions.md"), "# Python scoped instructions");
205
+ const { stdout, code } = await run(["init", dir, "--plan"]);
206
+ assert.equal(code, 0, `init --plan failed:\n${stdout}`);
207
+ const manifest = extractPlanJson(stdout);
208
+ const scopedEntries = manifest.agentRules.filter((r) => r.scope === "copilot-scoped");
209
+ assert.equal(scopedEntries.length, 2, `expected 2 copilot-scoped entries, got ${scopedEntries.length}: ${JSON.stringify(manifest.agentRules)}`);
210
+ for (const entry of scopedEntries) {
211
+ assert.deepEqual(entry.agents, ["github-copilot"]);
212
+ }
213
+ const names = scopedEntries.map((e) => e.name).sort();
214
+ assert.deepEqual(names, ["python", "typescript"]);
215
+ }
216
+ finally {
217
+ await rm(dir, { recursive: true });
218
+ }
219
+ });
220
+ it("non-.github/ copilot-instructions.md still maps to claude-code (backward compat)", async () => {
221
+ const dir = await makeTmpDir();
222
+ try {
223
+ await mkdir(path.join(dir, "rules"), { recursive: true });
224
+ await writeFile(path.join(dir, "rules", "copilot-instructions.md"), "# Copilot instructions");
225
+ const { stdout, code } = await run(["init", dir, "--plan"]);
226
+ assert.equal(code, 0, `init --plan failed:\n${stdout}`);
227
+ const manifest = extractPlanJson(stdout);
228
+ const entry = manifest.agentRules.find((r) => r.path.endsWith("copilot-instructions.md"));
229
+ assert.ok(entry, `expected a copilot-instructions.md entry, got: ${JSON.stringify(manifest.agentRules)}`);
230
+ assert.ok(entry.agents.includes("claude-code"), `expected agents to include claude-code, got: ${JSON.stringify(entry.agents)}`);
231
+ assert.notEqual(entry.scope, "copilot-repo", "rules/ copilot-instructions.md should not use copilot-repo scope");
232
+ }
233
+ finally {
234
+ await rm(dir, { recursive: true });
235
+ }
236
+ });
237
+ });
@@ -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 {
@@ -279,6 +279,92 @@ describe("instruction precedence warnings", () => {
279
279
  await rm(sourceDir, { recursive: true });
280
280
  }
281
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
+ });
282
368
  });
283
369
  describe("instruction budget warnings", () => {
284
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", () => {
@@ -87,6 +122,36 @@ describe("planRevertAll", () => {
87
122
  assert.match(targets.get("antigravity") ?? "", /\.gemini[\\/]antigravity[\\/]skills[\\/]test-skill$/);
88
123
  assert.match(targets.get("opencode") ?? "", /opencode[\\/]skills[\\/]test-skill$/);
89
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
+ });
90
155
  });
91
156
  describe("executeRevert", { skip: process.platform === "win32" }, () => {
92
157
  it("removes a symlink registered in the deployment registry", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.22.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",