@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.
- package/README.md +72 -43
- package/dist/{config → src/config}/agents.js +174 -24
- package/dist/src/core/adapters/agent-definitions.d.ts +8 -0
- package/dist/src/core/adapters/agent-definitions.js +162 -0
- package/dist/{core → src/core}/adapters/frontmatter.d.ts +10 -0
- package/dist/{core → src/core}/adapters/frontmatter.js +23 -6
- package/dist/{core → src/core}/adapters/index.d.ts +1 -1
- package/dist/{core → src/core}/adapters/index.js +4 -4
- package/dist/{core → src/core}/adapters/mcp.d.ts +2 -2
- package/dist/{core → src/core}/adapters/mcp.js +21 -24
- package/dist/{core → src/core}/adapters/permissions.js +16 -20
- package/dist/{core → src/core}/adapters/rules.d.ts +2 -2
- package/dist/src/core/adapters/rules.js +126 -0
- package/dist/src/core/capabilities.d.ts +47 -0
- package/dist/src/core/capabilities.js +280 -0
- package/dist/{core → src/core}/deploy.d.ts +1 -1
- package/dist/{core → src/core}/deploy.js +273 -92
- package/dist/{core → src/core}/init.js +126 -52
- package/dist/src/core/merge-patch.d.ts +4 -0
- package/dist/src/core/merge-patch.js +46 -0
- package/dist/{core → src/core}/ownership.d.ts +2 -2
- package/dist/{core → src/core}/ownership.js +13 -0
- package/dist/{core → src/core}/preflight.d.ts +1 -1
- package/dist/src/core/preflight.js +280 -0
- package/dist/{core → src/core}/resolve.d.ts +1 -1
- package/dist/{core → src/core}/resolve.js +7 -4
- package/dist/{core → src/core}/revert.js +95 -33
- package/dist/{core → src/core}/runtime-paths.d.ts +2 -1
- package/dist/{core → src/core}/runtime-paths.js +30 -23
- package/dist/{core → src/core}/validation.d.ts +10 -1
- package/dist/{core → src/core}/validation.js +75 -0
- package/dist/src/formatters.d.ts +5 -0
- package/dist/src/formatters.js +58 -0
- package/dist/{index.js → src/index.js} +8 -24
- package/dist/src/logger.js +62 -0
- package/dist/{schemas → src/schemas}/manifest.d.ts +22 -0
- package/dist/{schemas → src/schemas}/manifest.js +19 -5
- package/dist/{schemas → src/schemas}/registry.d.ts +74 -0
- package/dist/{schemas → src/schemas}/registry.js +18 -3
- package/dist/{types.d.ts → src/types.d.ts} +60 -0
- package/dist/test/helpers/fs.d.ts +2 -0
- package/dist/test/helpers/fs.js +17 -0
- package/dist/test/helpers/path.d.ts +10 -0
- package/dist/test/helpers/path.js +15 -0
- package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
- package/dist/test/helpers/skill-dir-scenarios.js +206 -0
- package/dist/test/helpers/skill-dir.d.ts +7 -0
- package/dist/test/helpers/skill-dir.js +46 -0
- package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
- package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
- package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
- package/dist/test/os/posix/skill-dir.test.js +19 -0
- package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
- package/dist/test/os/windows/revert-integration.test.js +72 -0
- package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
- package/dist/test/os/windows/skill-dir.test.js +19 -0
- package/dist/test/unit/adapters.test.d.ts +1 -0
- package/dist/test/unit/adapters.test.js +947 -0
- package/dist/test/unit/capabilities.test.d.ts +1 -0
- package/dist/test/unit/capabilities.test.js +71 -0
- package/dist/test/unit/cli.test.d.ts +1 -0
- package/dist/test/unit/cli.test.js +834 -0
- package/dist/test/unit/deploy.test.d.ts +1 -0
- package/dist/test/unit/deploy.test.js +2481 -0
- package/dist/test/unit/detect.test.d.ts +1 -0
- package/dist/test/unit/detect.test.js +192 -0
- package/dist/test/unit/enterprise.test.d.ts +1 -0
- package/dist/test/unit/enterprise.test.js +106 -0
- package/dist/test/unit/frontmatter.test.d.ts +1 -0
- package/dist/test/unit/frontmatter.test.js +102 -0
- package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
- package/dist/test/unit/gemini-north-star.test.js +243 -0
- package/dist/test/unit/manifest.test.d.ts +1 -0
- package/dist/test/unit/manifest.test.js +737 -0
- package/dist/test/unit/migration-golden.test.d.ts +1 -0
- package/dist/test/unit/migration-golden.test.js +197 -0
- package/dist/test/unit/ownership.test.d.ts +1 -0
- package/dist/test/unit/ownership.test.js +587 -0
- package/dist/test/unit/preflight.test.d.ts +1 -0
- package/dist/test/unit/preflight.test.js +501 -0
- package/dist/test/unit/resolve.test.d.ts +1 -0
- package/dist/test/unit/resolve.test.js +119 -0
- package/dist/test/unit/revert.test.d.ts +1 -0
- package/dist/test/unit/revert.test.js +1004 -0
- package/dist/test/unit/toml.test.d.ts +1 -0
- package/dist/test/unit/toml.test.js +55 -0
- package/package.json +1 -1
- package/dist/core/adapters/agent-definitions.d.ts +0 -20
- package/dist/core/adapters/agent-definitions.js +0 -90
- package/dist/core/adapters/rules.js +0 -103
- package/dist/core/preflight.js +0 -98
- package/dist/logger.js +0 -61
- /package/dist/{config → src/config}/agents.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.js +0 -0
- /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.js +0 -0
- /package/dist/{core → src/core}/detect.d.ts +0 -0
- /package/dist/{core → src/core}/detect.js +0 -0
- /package/dist/{core → src/core}/init.d.ts +0 -0
- /package/dist/{core → src/core}/revert.d.ts +0 -0
- /package/dist/{errors.d.ts → src/errors.d.ts} +0 -0
- /package/dist/{errors.js → src/errors.js} +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
- /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
- /package/dist/{schemas → src/schemas}/errors.js +0 -0
- /package/dist/{types.js → src/types.js} +0 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { runPreflight } from "../../src/core/preflight.js";
|
|
6
|
+
import { logger } from "../../src/logger.js";
|
|
7
|
+
import { makeTmpDir } from "../helpers/fs.js";
|
|
8
|
+
import { normalizeSlashes } from "../helpers/path.js";
|
|
9
|
+
logger.silence();
|
|
10
|
+
const baseOptions = {
|
|
11
|
+
command: "deploy",
|
|
12
|
+
directory: "/tmp",
|
|
13
|
+
dryRun: false,
|
|
14
|
+
agents: null,
|
|
15
|
+
verbose: false,
|
|
16
|
+
debug: false,
|
|
17
|
+
force: false,
|
|
18
|
+
};
|
|
19
|
+
const emptyManifest = {
|
|
20
|
+
skills: [],
|
|
21
|
+
files: [],
|
|
22
|
+
configs: [],
|
|
23
|
+
mcpServers: [],
|
|
24
|
+
agentRules: [],
|
|
25
|
+
permissions: [],
|
|
26
|
+
agentDefinitions: [],
|
|
27
|
+
};
|
|
28
|
+
describe("runPreflight", () => {
|
|
29
|
+
it("returns empty for empty detected agents list", async () => {
|
|
30
|
+
const warnings = await runPreflight(baseOptions, emptyManifest, "/home/test", []);
|
|
31
|
+
assert.equal(warnings.length, 0);
|
|
32
|
+
});
|
|
33
|
+
it("returns empty for documented agents without policy notes", async () => {
|
|
34
|
+
const warnings = await runPreflight(baseOptions, emptyManifest, "/home/test", ["claude-code", "codex", "gemini-cli", "opencode"]);
|
|
35
|
+
assert.equal(warnings.length, 0);
|
|
36
|
+
});
|
|
37
|
+
it("returns empty for standard github-copilot without enterprise markers", async () => {
|
|
38
|
+
const warnings = await runPreflight(baseOptions, emptyManifest, "/home/test", ["github-copilot"]);
|
|
39
|
+
assert.equal(warnings.length, 0);
|
|
40
|
+
});
|
|
41
|
+
it("emits config-authority warning for antigravity (implementation-only skills)", async () => {
|
|
42
|
+
const warnings = await runPreflight(baseOptions, emptyManifest, "/home/test", ["antigravity"]);
|
|
43
|
+
assert.equal(warnings.length, 1);
|
|
44
|
+
assert.equal(warnings[0]?.kind, "config-authority");
|
|
45
|
+
assert.match(warnings[0]?.message ?? "", /antigravity/);
|
|
46
|
+
assert.match(warnings[0]?.message ?? "", /implementation-only/);
|
|
47
|
+
});
|
|
48
|
+
it("emits one warning per non-documented agent", async () => {
|
|
49
|
+
const warnings = await runPreflight(baseOptions, emptyManifest, "/home/test", ["claude-code", "antigravity"]);
|
|
50
|
+
assert.equal(warnings.length, 1);
|
|
51
|
+
assert.equal(warnings[0]?.kind, "config-authority");
|
|
52
|
+
assert.match(warnings[0]?.message ?? "", /antigravity/);
|
|
53
|
+
});
|
|
54
|
+
it("emits shared-surface guidance when a skill targets github-copilot without claude-code", async () => {
|
|
55
|
+
const manifest = {
|
|
56
|
+
...emptyManifest,
|
|
57
|
+
skills: [
|
|
58
|
+
{
|
|
59
|
+
name: "my-skill",
|
|
60
|
+
path: "skills/my-skill",
|
|
61
|
+
agents: ["github-copilot"],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
66
|
+
"github-copilot",
|
|
67
|
+
]);
|
|
68
|
+
const info = warnings.find((w) => w.kind === "info");
|
|
69
|
+
assert.ok(info, "expected a shared-surface info warning");
|
|
70
|
+
assert.match(info?.message ?? "", /via "claude-code"/);
|
|
71
|
+
});
|
|
72
|
+
it("emits unsupported warning when manifest uses github-copilot MCP with global scope", async () => {
|
|
73
|
+
const manifest = {
|
|
74
|
+
...emptyManifest,
|
|
75
|
+
mcpServers: [
|
|
76
|
+
{
|
|
77
|
+
name: "my-mcp",
|
|
78
|
+
agents: ["github-copilot"],
|
|
79
|
+
config: { command: "npx", args: ["-y", "my-mcp"] },
|
|
80
|
+
scope: "global",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
85
|
+
"github-copilot",
|
|
86
|
+
]);
|
|
87
|
+
const info = warnings.find((w) => w.kind === "info");
|
|
88
|
+
assert.ok(info, "expected an unsupported-surface info warning");
|
|
89
|
+
assert.match(info?.message ?? "", /unsupported/);
|
|
90
|
+
});
|
|
91
|
+
it("emits no capability warning when manifest uses github-copilot MCP with scope: repo", async () => {
|
|
92
|
+
const manifest = {
|
|
93
|
+
...emptyManifest,
|
|
94
|
+
mcpServers: [
|
|
95
|
+
{
|
|
96
|
+
name: "my-mcp",
|
|
97
|
+
agents: ["github-copilot"],
|
|
98
|
+
config: { command: "npx", args: ["-y", "my-mcp"] },
|
|
99
|
+
scope: "repo",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
104
|
+
"github-copilot",
|
|
105
|
+
]);
|
|
106
|
+
const capabilityWarning = warnings.find((w) => w.kind === "info" && /mcpServers/.test(w.message));
|
|
107
|
+
assert.equal(capabilityWarning, undefined, `expected no MCP capability warning, got: ${capabilityWarning?.message}`);
|
|
108
|
+
});
|
|
109
|
+
it("emits no config-authority warning for gemini-cli agentDefinitions (surface is now documented)", async () => {
|
|
110
|
+
const manifest = {
|
|
111
|
+
...emptyManifest,
|
|
112
|
+
agentDefinitions: [
|
|
113
|
+
{
|
|
114
|
+
name: "my-agent",
|
|
115
|
+
path: "agents/my-agent.md",
|
|
116
|
+
agents: ["gemini-cli"],
|
|
117
|
+
scope: "repo",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
122
|
+
"gemini-cli",
|
|
123
|
+
]);
|
|
124
|
+
const implementationOnlyWarning = warnings.find((w) => w.kind === "config-authority" && /implementation-only/.test(w.message));
|
|
125
|
+
assert.equal(implementationOnlyWarning, undefined, `expected no implementation-only warning, got: ${implementationOnlyWarning?.message}`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe("instruction precedence warnings", () => {
|
|
129
|
+
it("emits duplicate-content precedence warning when same source used in both global and repo scope for an agent", async () => {
|
|
130
|
+
const sourceDir = await makeTmpDir();
|
|
131
|
+
try {
|
|
132
|
+
await writeFile(path.join(sourceDir, "CLAUDE.md"), "# Rules");
|
|
133
|
+
const manifest = {
|
|
134
|
+
...emptyManifest,
|
|
135
|
+
agentRules: [
|
|
136
|
+
{
|
|
137
|
+
name: "global-rules",
|
|
138
|
+
path: "CLAUDE.md",
|
|
139
|
+
agents: ["claude-code"],
|
|
140
|
+
scope: "global",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "repo-rules",
|
|
144
|
+
path: "CLAUDE.md",
|
|
145
|
+
agents: ["claude-code"],
|
|
146
|
+
scope: "repo",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
151
|
+
const precedenceWarnings = warnings.filter((w) => w.kind === "precedence");
|
|
152
|
+
assert.ok(precedenceWarnings.length > 0, "expected a precedence warning");
|
|
153
|
+
assert.ok(precedenceWarnings.some((w) => w.message.includes("claude-code") &&
|
|
154
|
+
w.message.includes("CLAUDE.md")));
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await rm(sourceDir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
it("emits stacking advisory when different source files in global and repo scope for an agent", async () => {
|
|
161
|
+
const sourceDir = await makeTmpDir();
|
|
162
|
+
try {
|
|
163
|
+
await writeFile(path.join(sourceDir, "global.md"), "# Global");
|
|
164
|
+
await writeFile(path.join(sourceDir, "repo.md"), "# Repo");
|
|
165
|
+
const manifest = {
|
|
166
|
+
...emptyManifest,
|
|
167
|
+
agentRules: [
|
|
168
|
+
{
|
|
169
|
+
name: "global-rules",
|
|
170
|
+
path: "global.md",
|
|
171
|
+
agents: ["claude-code"],
|
|
172
|
+
scope: "global",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "repo-rules",
|
|
176
|
+
path: "repo.md",
|
|
177
|
+
agents: ["claude-code"],
|
|
178
|
+
scope: "repo",
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
183
|
+
const precedenceWarnings = warnings.filter((w) => w.kind === "precedence");
|
|
184
|
+
assert.ok(precedenceWarnings.length > 0, "expected a precedence warning");
|
|
185
|
+
assert.ok(precedenceWarnings.some((w) => w.message.includes("claude-code") &&
|
|
186
|
+
w.message.includes("simultaneously")));
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await rm(sourceDir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
it("emits no precedence warning when agent only has global entries", async () => {
|
|
193
|
+
const sourceDir = await makeTmpDir();
|
|
194
|
+
try {
|
|
195
|
+
await writeFile(path.join(sourceDir, "CLAUDE.md"), "# Rules");
|
|
196
|
+
const manifest = {
|
|
197
|
+
...emptyManifest,
|
|
198
|
+
agentRules: [
|
|
199
|
+
{
|
|
200
|
+
name: "global-rules",
|
|
201
|
+
path: "CLAUDE.md",
|
|
202
|
+
agents: ["claude-code"],
|
|
203
|
+
scope: "global",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
208
|
+
assert.equal(warnings.filter((w) => w.kind === "precedence").length, 0);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
await rm(sourceDir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
it("emits no precedence warning when agent only has repo entries", async () => {
|
|
215
|
+
const sourceDir = await makeTmpDir();
|
|
216
|
+
try {
|
|
217
|
+
await writeFile(path.join(sourceDir, "CLAUDE.md"), "# Rules");
|
|
218
|
+
const manifest = {
|
|
219
|
+
...emptyManifest,
|
|
220
|
+
agentRules: [
|
|
221
|
+
{
|
|
222
|
+
name: "repo-rules",
|
|
223
|
+
path: "CLAUDE.md",
|
|
224
|
+
agents: ["claude-code"],
|
|
225
|
+
scope: "repo",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
230
|
+
assert.equal(warnings.filter((w) => w.kind === "precedence").length, 0);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await rm(sourceDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
it("emits no precedence warning for agents not in detectedAgents", async () => {
|
|
237
|
+
const sourceDir = await makeTmpDir();
|
|
238
|
+
try {
|
|
239
|
+
await writeFile(path.join(sourceDir, "CLAUDE.md"), "# Rules");
|
|
240
|
+
const manifest = {
|
|
241
|
+
...emptyManifest,
|
|
242
|
+
agentRules: [
|
|
243
|
+
{
|
|
244
|
+
name: "global-rules",
|
|
245
|
+
path: "CLAUDE.md",
|
|
246
|
+
agents: ["claude-code"],
|
|
247
|
+
scope: "global",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "repo-rules",
|
|
251
|
+
path: "CLAUDE.md",
|
|
252
|
+
agents: ["claude-code"],
|
|
253
|
+
scope: "repo",
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["codex"]);
|
|
258
|
+
assert.equal(warnings.filter((w) => w.kind === "precedence").length, 0);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await rm(sourceDir, { recursive: true });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
describe("instruction budget warnings", () => {
|
|
266
|
+
it("emits budget warning for agentRules source file exceeding 50 KB", async () => {
|
|
267
|
+
const sourceDir = await makeTmpDir();
|
|
268
|
+
try {
|
|
269
|
+
await writeFile(path.join(sourceDir, "big.md"), Buffer.alloc(51 * 1024, "x"));
|
|
270
|
+
const manifest = {
|
|
271
|
+
...emptyManifest,
|
|
272
|
+
agentRules: [
|
|
273
|
+
{
|
|
274
|
+
name: "big-rules",
|
|
275
|
+
path: "big.md",
|
|
276
|
+
agents: ["claude-code"],
|
|
277
|
+
scope: "global",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
282
|
+
const budgetWarnings = warnings.filter((w) => w.kind === "budget");
|
|
283
|
+
assert.ok(budgetWarnings.length > 0, "expected a budget warning");
|
|
284
|
+
assert.ok(budgetWarnings.some((w) => w.message.includes("big.md") && w.message.includes("KB")));
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await rm(sourceDir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
it("emits budget warning for agentDefinitions source file exceeding 50 KB", async () => {
|
|
291
|
+
const sourceDir = await makeTmpDir();
|
|
292
|
+
try {
|
|
293
|
+
await mkdir(path.join(sourceDir, "agents"), { recursive: true });
|
|
294
|
+
await writeFile(path.join(sourceDir, "agents", "big-agent.md"), Buffer.alloc(51 * 1024, "x"));
|
|
295
|
+
const manifest = {
|
|
296
|
+
...emptyManifest,
|
|
297
|
+
agentDefinitions: [
|
|
298
|
+
{
|
|
299
|
+
name: "big-agent",
|
|
300
|
+
path: "agents/big-agent.md",
|
|
301
|
+
agents: ["claude-code"],
|
|
302
|
+
scope: "repo",
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
307
|
+
const budgetWarnings = warnings.filter((w) => w.kind === "budget");
|
|
308
|
+
assert.ok(budgetWarnings.length > 0, "expected a budget warning");
|
|
309
|
+
assert.ok(budgetWarnings.some((w) => normalizeSlashes(w.message).includes("agents/big-agent.md")));
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await rm(sourceDir, { recursive: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
it("emits no budget warning for files at or below 50 KB", async () => {
|
|
316
|
+
const sourceDir = await makeTmpDir();
|
|
317
|
+
try {
|
|
318
|
+
await writeFile(path.join(sourceDir, "small.md"), Buffer.alloc(49 * 1024, "x"));
|
|
319
|
+
const manifest = {
|
|
320
|
+
...emptyManifest,
|
|
321
|
+
agentRules: [
|
|
322
|
+
{
|
|
323
|
+
name: "small-rules",
|
|
324
|
+
path: "small.md",
|
|
325
|
+
agents: ["claude-code"],
|
|
326
|
+
scope: "global",
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
331
|
+
assert.equal(warnings.filter((w) => w.kind === "budget").length, 0);
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
await rm(sourceDir, { recursive: true });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
it("emits no budget warning when entry's agents are not in detectedAgents", async () => {
|
|
338
|
+
const sourceDir = await makeTmpDir();
|
|
339
|
+
try {
|
|
340
|
+
await writeFile(path.join(sourceDir, "big.md"), Buffer.alloc(51 * 1024, "x"));
|
|
341
|
+
const manifest = {
|
|
342
|
+
...emptyManifest,
|
|
343
|
+
agentRules: [
|
|
344
|
+
{
|
|
345
|
+
name: "big-rules",
|
|
346
|
+
path: "big.md",
|
|
347
|
+
agents: ["codex"],
|
|
348
|
+
scope: "global",
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
353
|
+
assert.equal(warnings.filter((w) => w.kind === "budget").length, 0);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
await rm(sourceDir, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
it("deduplicates budget warnings when the same source path appears in two entries", async () => {
|
|
360
|
+
const sourceDir = await makeTmpDir();
|
|
361
|
+
try {
|
|
362
|
+
await writeFile(path.join(sourceDir, "big.md"), Buffer.alloc(51 * 1024, "x"));
|
|
363
|
+
const manifest = {
|
|
364
|
+
...emptyManifest,
|
|
365
|
+
agentRules: [
|
|
366
|
+
{
|
|
367
|
+
name: "rules-a",
|
|
368
|
+
path: "big.md",
|
|
369
|
+
agents: ["claude-code"],
|
|
370
|
+
scope: "global",
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "rules-b",
|
|
374
|
+
path: "big.md",
|
|
375
|
+
agents: ["claude-code"],
|
|
376
|
+
scope: "global",
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
381
|
+
assert.equal(warnings.filter((w) => w.kind === "budget").length, 1);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
await rm(sourceDir, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
it("silently skips missing source files without emitting a budget warning", async () => {
|
|
388
|
+
const sourceDir = await makeTmpDir();
|
|
389
|
+
try {
|
|
390
|
+
const manifest = {
|
|
391
|
+
...emptyManifest,
|
|
392
|
+
agentRules: [
|
|
393
|
+
{
|
|
394
|
+
name: "missing-rules",
|
|
395
|
+
path: "does-not-exist.md",
|
|
396
|
+
agents: ["claude-code"],
|
|
397
|
+
scope: "global",
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
};
|
|
401
|
+
const warnings = await runPreflight({ ...baseOptions, directory: sourceDir }, manifest, "/home/test", ["claude-code"]);
|
|
402
|
+
assert.equal(warnings.filter((w) => w.kind === "budget").length, 0);
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
await rm(sourceDir, { recursive: true });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
it("emits precedence warnings for triple overlap (global, repo, workspace) for an agent", async () => {
|
|
409
|
+
const sourceDir = await makeTmpDir();
|
|
410
|
+
try {
|
|
411
|
+
const manifest = {
|
|
412
|
+
...emptyManifest,
|
|
413
|
+
agentRules: [
|
|
414
|
+
{ name: "g", path: "g.md", agents: ["claude-code"], scope: "global" },
|
|
415
|
+
{ name: "r", path: "r.md", agents: ["claude-code"], scope: "repo" },
|
|
416
|
+
{
|
|
417
|
+
name: "w",
|
|
418
|
+
path: "w.md",
|
|
419
|
+
agents: ["claude-code"],
|
|
420
|
+
scope: "workspace",
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
await writeFile(path.join(sourceDir, "g.md"), "# G");
|
|
425
|
+
await writeFile(path.join(sourceDir, "r.md"), "# R");
|
|
426
|
+
await writeFile(path.join(sourceDir, "w.md"), "# W");
|
|
427
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
428
|
+
"claude-code",
|
|
429
|
+
]);
|
|
430
|
+
const precedence = warnings.filter((w) => w.kind === "precedence");
|
|
431
|
+
assert.equal(precedence.length, 1);
|
|
432
|
+
const msg = precedence[0]?.message ?? "";
|
|
433
|
+
assert.match(msg, /global/);
|
|
434
|
+
assert.match(msg, /repo/);
|
|
435
|
+
assert.match(msg, /workspace/);
|
|
436
|
+
assert.match(msg, /active simultaneously/);
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
await rm(sourceDir, { recursive: true });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
it("emits duplicate-content warning for workspace overlap (global vs workspace)", async () => {
|
|
443
|
+
const sourceDir = await makeTmpDir();
|
|
444
|
+
try {
|
|
445
|
+
const manifest = {
|
|
446
|
+
...emptyManifest,
|
|
447
|
+
agentRules: [
|
|
448
|
+
{
|
|
449
|
+
name: "global",
|
|
450
|
+
path: "SHARED.md",
|
|
451
|
+
agents: ["claude-code"],
|
|
452
|
+
scope: "global",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "workspace",
|
|
456
|
+
path: "SHARED.md",
|
|
457
|
+
agents: ["claude-code"],
|
|
458
|
+
scope: "workspace",
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
};
|
|
462
|
+
await writeFile(path.join(sourceDir, "SHARED.md"), "# Shared");
|
|
463
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
464
|
+
"claude-code",
|
|
465
|
+
]);
|
|
466
|
+
const duplicate = warnings.find((w) => w.kind === "precedence" && w.message.includes("distinct targets"));
|
|
467
|
+
assert.ok(duplicate, "expected duplicate target warning");
|
|
468
|
+
assert.match(duplicate.message, /global and workspace/);
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
await rm(sourceDir, { recursive: true });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
it("detects precedence overlaps for Gemini CLI combinations", async () => {
|
|
475
|
+
const sourceDir = await makeTmpDir();
|
|
476
|
+
try {
|
|
477
|
+
const manifest = {
|
|
478
|
+
skills: [],
|
|
479
|
+
files: [],
|
|
480
|
+
configs: [],
|
|
481
|
+
mcpServers: [],
|
|
482
|
+
agentRules: [
|
|
483
|
+
{ name: "g", path: "r1.md", agents: ["gemini-cli"], scope: "global" },
|
|
484
|
+
{ name: "r", path: "r1.md", agents: ["gemini-cli"], scope: "repo" },
|
|
485
|
+
],
|
|
486
|
+
permissions: [],
|
|
487
|
+
agentDefinitions: [],
|
|
488
|
+
};
|
|
489
|
+
await writeFile(path.join(sourceDir, "r1.md"), "# R1");
|
|
490
|
+
const warnings = await runPreflight(baseOptions, manifest, "/home/test", [
|
|
491
|
+
"gemini-cli",
|
|
492
|
+
]);
|
|
493
|
+
const precedence = warnings.filter((w) => w.kind === "precedence");
|
|
494
|
+
// Expect 2: one for duplicate path, one for simultaneous load
|
|
495
|
+
assert.equal(precedence.length, 2);
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await rm(sourceDir, { recursive: true });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { AGENT_REGISTRY } from "../../src/config/agents.js";
|
|
6
|
+
import { resolveAgentDetectPath, resolveAgentSkillPath, resolveHome, } from "../../src/core/resolve.js";
|
|
7
|
+
import { resolveTargetTemplate } from "../../src/core/runtime-paths.js";
|
|
8
|
+
import { UserError } from "../../src/errors.js";
|
|
9
|
+
const posixJoin = path.posix.join;
|
|
10
|
+
function getAgent(id) {
|
|
11
|
+
const agent = AGENT_REGISTRY.find((a) => a.id === id);
|
|
12
|
+
assert.ok(agent, `agent "${id}" not found in AGENT_REGISTRY`);
|
|
13
|
+
return agent;
|
|
14
|
+
}
|
|
15
|
+
describe("resolveAgentSkillPath", () => {
|
|
16
|
+
it("resolves Claude Code skill path", () => {
|
|
17
|
+
const claude = getAgent("claude-code");
|
|
18
|
+
const result = resolveAgentSkillPath(claude, "my-skill", "/Users/test");
|
|
19
|
+
assert.equal(result, posixJoin("/Users/test", ".claude", "skills", "my-skill"));
|
|
20
|
+
});
|
|
21
|
+
it("resolves OpenCode skill path with home", () => {
|
|
22
|
+
const opencode = getAgent("opencode");
|
|
23
|
+
const result = resolveAgentSkillPath(opencode, "test-skill", "/home/user");
|
|
24
|
+
// On non-Windows the posix template is used; cross-platform template tests live in cross-platform.test.ts
|
|
25
|
+
assert.ok(result.includes("opencode") && result.includes("test-skill"));
|
|
26
|
+
});
|
|
27
|
+
it("resolves Antigravity nested path", () => {
|
|
28
|
+
const antigravity = getAgent("antigravity");
|
|
29
|
+
const result = resolveAgentSkillPath(antigravity, "my-skill", "/home/user");
|
|
30
|
+
assert.equal(result, posixJoin("/home/user", ".gemini", "antigravity", "skills", "my-skill"));
|
|
31
|
+
});
|
|
32
|
+
it("throws with a shared-via hint for github-copilot (no skills field)", () => {
|
|
33
|
+
const copilot = getAgent("github-copilot");
|
|
34
|
+
assert.throws(() => resolveAgentSkillPath(copilot, "my-skill", "/home/user"), (err) => {
|
|
35
|
+
assert.ok(err.message.includes("claude-code"), `expected hint referencing claude-code, got: ${err.message}`);
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe("resolveAgentDetectPath", () => {
|
|
41
|
+
it("resolves detect path for each agent", () => {
|
|
42
|
+
for (const agent of AGENT_REGISTRY) {
|
|
43
|
+
const result = resolveAgentDetectPath(agent, "/home/user");
|
|
44
|
+
assert.ok(path.isAbsolute(result), `expected absolute path, got: ${result}`);
|
|
45
|
+
assert.ok(!result.includes("{home}"));
|
|
46
|
+
assert.ok(!result.includes("{name}"));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("resolveHome", () => {
|
|
51
|
+
it("returns os.homedir() when SUDO_USER is not set", () => {
|
|
52
|
+
const saved = process.env.SUDO_USER;
|
|
53
|
+
try {
|
|
54
|
+
delete process.env.SUDO_USER;
|
|
55
|
+
assert.equal(resolveHome(), os.homedir());
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (saved === undefined) {
|
|
59
|
+
delete process.env.SUDO_USER;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
process.env.SUDO_USER = saved;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
it("ignores SUDO_USER when not running as root and returns os.homedir()", {
|
|
67
|
+
skip: process.getuid?.() === 0,
|
|
68
|
+
}, () => {
|
|
69
|
+
const currentUser = process.env.USER ?? os.userInfo().username;
|
|
70
|
+
if (!currentUser)
|
|
71
|
+
return;
|
|
72
|
+
const saved = process.env.SUDO_USER;
|
|
73
|
+
try {
|
|
74
|
+
process.env.SUDO_USER = currentUser;
|
|
75
|
+
const result = resolveHome();
|
|
76
|
+
assert.equal(result, os.homedir());
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (saved === undefined) {
|
|
80
|
+
delete process.env.SUDO_USER;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
process.env.SUDO_USER = saved;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it("throws UserError when SUDO_USER is a non-existent user (only when running as root)", {
|
|
88
|
+
skip: process.getuid?.() !== 0,
|
|
89
|
+
}, () => {
|
|
90
|
+
const saved = process.env.SUDO_USER;
|
|
91
|
+
try {
|
|
92
|
+
process.env.SUDO_USER = "__nonexistent_user_inception_engine_test__";
|
|
93
|
+
assert.throws(() => resolveHome(), (err) => {
|
|
94
|
+
assert.ok(err instanceof UserError);
|
|
95
|
+
assert.equal(err.code, "RESOLVE_FAILED");
|
|
96
|
+
assert.match(err.message, /Cannot determine home directory/);
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (saved === undefined) {
|
|
102
|
+
delete process.env.SUDO_USER;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
process.env.SUDO_USER = saved;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe("resolveTargetTemplate", () => {
|
|
111
|
+
it("resolves a descendant path under the placeholder root", () => {
|
|
112
|
+
const home = path.join(path.sep, "home", "user");
|
|
113
|
+
const result = resolveTargetTemplate("{home}/.claude/settings.json", home);
|
|
114
|
+
assert.equal(result, `${home}/.claude/settings.json`);
|
|
115
|
+
});
|
|
116
|
+
it("throws when the template escapes the placeholder root", () => {
|
|
117
|
+
assert.throws(() => resolveTargetTemplate("{home}/../.ssh/config", "/home/user"), /outside its placeholder root/);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|