@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.
- package/README.md +45 -8
- package/dist/src/config/agents.js +39 -0
- package/dist/src/core/adapters/execution-config.d.ts +14 -0
- package/dist/src/core/adapters/execution-config.js +60 -0
- package/dist/src/core/adapters/index.d.ts +4 -3
- package/dist/src/core/adapters/index.js +7 -5
- package/dist/src/core/adapters/rules.js +50 -19
- package/dist/src/core/capabilities.d.ts +1 -1
- package/dist/src/core/capabilities.js +18 -2
- package/dist/src/core/deploy.js +1 -1
- package/dist/src/core/init.js +69 -13
- package/dist/src/core/preflight.js +28 -0
- package/dist/src/core/revert.js +5 -1
- package/dist/src/core/validation.d.ts +1 -1
- package/dist/src/core/validation.js +56 -3
- package/dist/src/schemas/manifest.d.ts +31 -0
- package/dist/src/schemas/manifest.js +39 -3
- package/dist/src/types.d.ts +6 -2
- package/dist/test/unit/adapters.test.js +264 -1
- package/dist/test/unit/deploy.test.js +26 -0
- package/dist/test/unit/init-fixture.test.js +85 -1
- package/dist/test/unit/manifest.test.js +72 -0
- package/dist/test/unit/preflight.test.js +86 -0
- package/dist/test/unit/revert.test.js +65 -0
- package/package.json +1 -1
|
@@ -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 () => {
|