@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.
Files changed (109) hide show
  1. package/README.md +72 -43
  2. package/dist/{config → src/config}/agents.js +174 -24
  3. package/dist/src/core/adapters/agent-definitions.d.ts +8 -0
  4. package/dist/src/core/adapters/agent-definitions.js +162 -0
  5. package/dist/{core → src/core}/adapters/frontmatter.d.ts +10 -0
  6. package/dist/{core → src/core}/adapters/frontmatter.js +23 -6
  7. package/dist/{core → src/core}/adapters/index.d.ts +1 -1
  8. package/dist/{core → src/core}/adapters/index.js +4 -4
  9. package/dist/{core → src/core}/adapters/mcp.d.ts +2 -2
  10. package/dist/{core → src/core}/adapters/mcp.js +21 -24
  11. package/dist/{core → src/core}/adapters/permissions.js +16 -20
  12. package/dist/{core → src/core}/adapters/rules.d.ts +2 -2
  13. package/dist/src/core/adapters/rules.js +126 -0
  14. package/dist/src/core/capabilities.d.ts +47 -0
  15. package/dist/src/core/capabilities.js +280 -0
  16. package/dist/{core → src/core}/deploy.d.ts +1 -1
  17. package/dist/{core → src/core}/deploy.js +273 -92
  18. package/dist/{core → src/core}/init.js +126 -52
  19. package/dist/src/core/merge-patch.d.ts +4 -0
  20. package/dist/src/core/merge-patch.js +46 -0
  21. package/dist/{core → src/core}/ownership.d.ts +2 -2
  22. package/dist/{core → src/core}/ownership.js +13 -0
  23. package/dist/{core → src/core}/preflight.d.ts +1 -1
  24. package/dist/src/core/preflight.js +280 -0
  25. package/dist/{core → src/core}/resolve.d.ts +1 -1
  26. package/dist/{core → src/core}/resolve.js +7 -4
  27. package/dist/{core → src/core}/revert.js +95 -33
  28. package/dist/{core → src/core}/runtime-paths.d.ts +2 -1
  29. package/dist/{core → src/core}/runtime-paths.js +30 -23
  30. package/dist/{core → src/core}/validation.d.ts +10 -1
  31. package/dist/{core → src/core}/validation.js +75 -0
  32. package/dist/src/formatters.d.ts +5 -0
  33. package/dist/src/formatters.js +58 -0
  34. package/dist/{index.js → src/index.js} +8 -24
  35. package/dist/src/logger.js +62 -0
  36. package/dist/{schemas → src/schemas}/manifest.d.ts +22 -0
  37. package/dist/{schemas → src/schemas}/manifest.js +19 -5
  38. package/dist/{schemas → src/schemas}/registry.d.ts +74 -0
  39. package/dist/{schemas → src/schemas}/registry.js +18 -3
  40. package/dist/{types.d.ts → src/types.d.ts} +60 -0
  41. package/dist/test/helpers/fs.d.ts +2 -0
  42. package/dist/test/helpers/fs.js +17 -0
  43. package/dist/test/helpers/path.d.ts +10 -0
  44. package/dist/test/helpers/path.js +15 -0
  45. package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
  46. package/dist/test/helpers/skill-dir-scenarios.js +206 -0
  47. package/dist/test/helpers/skill-dir.d.ts +7 -0
  48. package/dist/test/helpers/skill-dir.js +46 -0
  49. package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
  50. package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
  51. package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
  52. package/dist/test/os/posix/skill-dir.test.js +19 -0
  53. package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
  54. package/dist/test/os/windows/revert-integration.test.js +72 -0
  55. package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
  56. package/dist/test/os/windows/skill-dir.test.js +19 -0
  57. package/dist/test/unit/adapters.test.d.ts +1 -0
  58. package/dist/test/unit/adapters.test.js +947 -0
  59. package/dist/test/unit/capabilities.test.d.ts +1 -0
  60. package/dist/test/unit/capabilities.test.js +71 -0
  61. package/dist/test/unit/cli.test.d.ts +1 -0
  62. package/dist/test/unit/cli.test.js +834 -0
  63. package/dist/test/unit/deploy.test.d.ts +1 -0
  64. package/dist/test/unit/deploy.test.js +2481 -0
  65. package/dist/test/unit/detect.test.d.ts +1 -0
  66. package/dist/test/unit/detect.test.js +192 -0
  67. package/dist/test/unit/enterprise.test.d.ts +1 -0
  68. package/dist/test/unit/enterprise.test.js +106 -0
  69. package/dist/test/unit/frontmatter.test.d.ts +1 -0
  70. package/dist/test/unit/frontmatter.test.js +102 -0
  71. package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
  72. package/dist/test/unit/gemini-north-star.test.js +243 -0
  73. package/dist/test/unit/manifest.test.d.ts +1 -0
  74. package/dist/test/unit/manifest.test.js +737 -0
  75. package/dist/test/unit/migration-golden.test.d.ts +1 -0
  76. package/dist/test/unit/migration-golden.test.js +197 -0
  77. package/dist/test/unit/ownership.test.d.ts +1 -0
  78. package/dist/test/unit/ownership.test.js +587 -0
  79. package/dist/test/unit/preflight.test.d.ts +1 -0
  80. package/dist/test/unit/preflight.test.js +501 -0
  81. package/dist/test/unit/resolve.test.d.ts +1 -0
  82. package/dist/test/unit/resolve.test.js +119 -0
  83. package/dist/test/unit/revert.test.d.ts +1 -0
  84. package/dist/test/unit/revert.test.js +1004 -0
  85. package/dist/test/unit/toml.test.d.ts +1 -0
  86. package/dist/test/unit/toml.test.js +55 -0
  87. package/package.json +1 -1
  88. package/dist/core/adapters/agent-definitions.d.ts +0 -20
  89. package/dist/core/adapters/agent-definitions.js +0 -90
  90. package/dist/core/adapters/rules.js +0 -103
  91. package/dist/core/preflight.js +0 -98
  92. package/dist/logger.js +0 -61
  93. /package/dist/{config → src/config}/agents.d.ts +0 -0
  94. /package/dist/{config → src/config}/manifest.d.ts +0 -0
  95. /package/dist/{config → src/config}/manifest.js +0 -0
  96. /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
  97. /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
  98. /package/dist/{core → src/core}/adapters/toml.js +0 -0
  99. /package/dist/{core → src/core}/detect.d.ts +0 -0
  100. /package/dist/{core → src/core}/detect.js +0 -0
  101. /package/dist/{core → src/core}/init.d.ts +0 -0
  102. /package/dist/{core → src/core}/revert.d.ts +0 -0
  103. /package/dist/{errors.d.ts → src/errors.d.ts} +0 -0
  104. /package/dist/{errors.js → src/errors.js} +0 -0
  105. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  106. /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
  107. /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
  108. /package/dist/{schemas → src/schemas}/errors.js +0 -0
  109. /package/dist/{types.js → src/types.js} +0 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,192 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { describe, it } from "node:test";
5
+ import { detectInstalledAgents, isBinaryInPath, isBinaryViaCommandV, isBinaryViaWhereExe, isBinaryViaWhich, } from "../../src/core/detect.js";
6
+ import { makeTmpDir } from "../helpers/fs.js";
7
+ describe("detectInstalledAgents", () => {
8
+ it("returns empty array when no agents are installed", async () => {
9
+ const home = await makeTmpDir();
10
+ try {
11
+ const agents = await detectInstalledAgents(home);
12
+ // May detect agents via binary-in-PATH, but directory check should find nothing
13
+ // Filter to only directory-based detections by checking what we know
14
+ assert.ok(Array.isArray(agents));
15
+ }
16
+ finally {
17
+ await rm(home, { recursive: true });
18
+ }
19
+ });
20
+ it("detects claude-code when .claude directory exists", async () => {
21
+ const home = await makeTmpDir();
22
+ try {
23
+ await mkdir(path.join(home, ".claude"), { recursive: true });
24
+ const agents = await detectInstalledAgents(home);
25
+ assert.ok(agents.includes("claude-code"));
26
+ }
27
+ finally {
28
+ await rm(home, { recursive: true });
29
+ }
30
+ });
31
+ it("detects gemini-cli when .gemini directory exists", async () => {
32
+ const home = await makeTmpDir();
33
+ try {
34
+ await mkdir(path.join(home, ".gemini"), { recursive: true });
35
+ const agents = await detectInstalledAgents(home);
36
+ assert.ok(agents.includes("gemini-cli"));
37
+ }
38
+ finally {
39
+ await rm(home, { recursive: true });
40
+ }
41
+ });
42
+ it("detects multiple agents", async () => {
43
+ const home = await makeTmpDir();
44
+ try {
45
+ await mkdir(path.join(home, ".claude"), { recursive: true });
46
+ await mkdir(path.join(home, ".codex"), { recursive: true });
47
+ await mkdir(path.join(home, ".copilot"), { recursive: true });
48
+ const agents = await detectInstalledAgents(home);
49
+ assert.ok(agents.includes("claude-code"));
50
+ assert.ok(agents.includes("codex"));
51
+ assert.ok(agents.includes("github-copilot"));
52
+ }
53
+ finally {
54
+ await rm(home, { recursive: true });
55
+ }
56
+ });
57
+ });
58
+ const NONEXISTENT_BINARY = "__nonexistent_binary_inception_test__";
59
+ describe("isBinaryViaCommandV", { skip: process.platform === "win32" }, () => {
60
+ it("returns true for node (known to be in PATH)", async () => {
61
+ const result = await isBinaryViaCommandV("node");
62
+ assert.equal(result, true);
63
+ });
64
+ it("returns false for a nonexistent binary", async () => {
65
+ const result = await isBinaryViaCommandV(NONEXISTENT_BINARY);
66
+ assert.equal(result, false);
67
+ });
68
+ });
69
+ describe("isBinaryViaWhich", { skip: process.platform === "win32" }, () => {
70
+ it("returns true for node (known to be in PATH)", async () => {
71
+ const result = await isBinaryViaWhich("node");
72
+ assert.equal(result, true);
73
+ });
74
+ it("returns false for a nonexistent binary", async () => {
75
+ const result = await isBinaryViaWhich(NONEXISTENT_BINARY);
76
+ assert.equal(result, false);
77
+ });
78
+ });
79
+ describe("isBinaryViaWhereExe", {
80
+ skip: process.platform !== "win32",
81
+ }, () => {
82
+ it("returns true for node.exe (known to be in PATH)", async () => {
83
+ const result = await isBinaryViaWhereExe("node.exe");
84
+ assert.equal(result, true);
85
+ });
86
+ it("returns false for a nonexistent binary", async () => {
87
+ const result = await isBinaryViaWhereExe(NONEXISTENT_BINARY);
88
+ assert.equal(result, false);
89
+ });
90
+ });
91
+ describe("mocked resolution", () => {
92
+ it("isBinaryViaWhereExe returns true on success", async () => {
93
+ let calledCmd = "";
94
+ const mockSuccess = async (cmd) => {
95
+ calledCmd = cmd;
96
+ };
97
+ const result = await isBinaryViaWhereExe("foo", mockSuccess);
98
+ assert.equal(result, true);
99
+ assert.equal(calledCmd, "where.exe");
100
+ });
101
+ it("isBinaryViaWhereExe returns false on failure", async () => {
102
+ const mockFail = async () => {
103
+ throw new Error("fail");
104
+ };
105
+ const result = await isBinaryViaWhereExe("foo", mockFail);
106
+ assert.equal(result, false);
107
+ });
108
+ it("isBinaryViaWhich returns true on success", async () => {
109
+ let calledCmd = "";
110
+ const mockSuccess = async (cmd) => {
111
+ calledCmd = cmd;
112
+ };
113
+ const result = await isBinaryViaWhich("foo", mockSuccess);
114
+ assert.equal(result, true);
115
+ assert.equal(calledCmd, "which");
116
+ });
117
+ it("isBinaryViaWhich returns false on failure", async () => {
118
+ const mockFail = async () => {
119
+ throw new Error("fail");
120
+ };
121
+ const result = await isBinaryViaWhich("foo", mockFail);
122
+ assert.equal(result, false);
123
+ });
124
+ it("isBinaryViaCommandV returns true on success", async () => {
125
+ let calledCmd = "";
126
+ const mockSuccess = async (cmd) => {
127
+ calledCmd = cmd;
128
+ };
129
+ const result = await isBinaryViaCommandV("foo", mockSuccess);
130
+ assert.equal(result, true);
131
+ assert.equal(calledCmd, "sh");
132
+ });
133
+ it("isBinaryViaCommandV returns false on failure", async () => {
134
+ const mockFail = async () => {
135
+ throw new Error("fail");
136
+ };
137
+ const result = await isBinaryViaCommandV("foo", mockFail);
138
+ assert.equal(result, false);
139
+ });
140
+ it("isBinaryInPath routes to where.exe on win32", async () => {
141
+ let calledCmd = "";
142
+ const mockSuccess = async (cmd) => {
143
+ calledCmd = cmd;
144
+ };
145
+ const result = await isBinaryInPath("foo", mockSuccess, "win32");
146
+ assert.equal(result, true);
147
+ assert.equal(calledCmd, "where.exe");
148
+ });
149
+ describe("isBinaryInPath ENOENT fallback", () => {
150
+ it("falls back to command -v when which throws ENOENT and succeeds", async () => {
151
+ const calls = [];
152
+ const enoentFn = async (cmd) => {
153
+ calls.push(cmd);
154
+ if (cmd === "which") {
155
+ const err = Object.assign(new Error("spawn which ENOENT"), {
156
+ code: "ENOENT",
157
+ });
158
+ throw err;
159
+ }
160
+ // "sh" (command -v) succeeds
161
+ };
162
+ const result = await isBinaryInPath("foo", enoentFn, "linux");
163
+ assert.equal(result, true);
164
+ assert.deepEqual(calls, ["which", "sh"]);
165
+ });
166
+ it("returns false when which throws ENOENT and command -v fails", async () => {
167
+ const enoentFn = async (cmd) => {
168
+ if (cmd === "which") {
169
+ const err = Object.assign(new Error("spawn which ENOENT"), {
170
+ code: "ENOENT",
171
+ });
172
+ throw err;
173
+ }
174
+ if (cmd === "sh") {
175
+ throw new Error("sh failed");
176
+ }
177
+ };
178
+ const result = await isBinaryInPath("foo", enoentFn, "linux");
179
+ assert.equal(result, false);
180
+ });
181
+ it("returns false when which fails with non-ENOENT", async () => {
182
+ let calledCmd = "";
183
+ const otherErrFn = async (cmd) => {
184
+ calledCmd = cmd;
185
+ throw new Error("some other error");
186
+ };
187
+ const result = await isBinaryInPath("foo", otherErrFn, "linux");
188
+ assert.equal(result, false);
189
+ assert.equal(calledCmd, "which");
190
+ });
191
+ });
192
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, 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
+ logger.silence();
9
+ const baseOptions = {
10
+ command: "deploy",
11
+ directory: "/tmp",
12
+ dryRun: false,
13
+ agents: null,
14
+ verbose: false,
15
+ debug: false,
16
+ force: false,
17
+ };
18
+ const emptyManifest = {
19
+ skills: [],
20
+ files: [],
21
+ configs: [],
22
+ mcpServers: [],
23
+ agentRules: [],
24
+ permissions: [],
25
+ agentDefinitions: [],
26
+ };
27
+ describe("enterprise detection", () => {
28
+ let tmpHome;
29
+ const originalEnv = { ...process.env };
30
+ beforeEach(async () => {
31
+ tmpHome = await makeTmpDir();
32
+ // Reset environment variables that might interfere
33
+ delete process.env.GITHUB_ENTERPRISE_URL;
34
+ delete process.env.GH_ENTERPRISE_TOKEN;
35
+ delete process.env.GITHUB_TOKEN_TYPE;
36
+ delete process.env.XDG_CONFIG_HOME;
37
+ delete process.env.LOCALAPPDATA;
38
+ delete process.env.APPDATA;
39
+ });
40
+ afterEach(async () => {
41
+ process.env = { ...originalEnv };
42
+ await rm(tmpHome, { recursive: true });
43
+ });
44
+ it("detects enterprise via environment variables", async () => {
45
+ process.env.GITHUB_ENTERPRISE_URL = "https://github.enterprise.com";
46
+ const warnings = await runPreflight(baseOptions, emptyManifest, tmpHome, [
47
+ "github-copilot",
48
+ ]);
49
+ const policy = warnings.find((w) => w.kind === "policy");
50
+ assert.ok(policy, "Expected a policy warning");
51
+ assert.match(policy.message, /Enterprise environment variables detected/);
52
+ });
53
+ it("detects enterprise via hosts.json", async () => {
54
+ const configDir = process.platform === "win32"
55
+ ? path.join(tmpHome, "AppData", "Local", "github-copilot")
56
+ : path.join(tmpHome, ".config", "github-copilot");
57
+ await mkdir(configDir, { recursive: true });
58
+ await writeFile(path.join(configDir, "hosts.json"), JSON.stringify({
59
+ "github.com": { user: "alice" },
60
+ "ghe.acme.com": { user: "bob" },
61
+ }));
62
+ // Ensure we're using the mock home for config resolution
63
+ if (process.platform === "win32") {
64
+ process.env.LOCALAPPDATA = path.join(tmpHome, "AppData", "Local");
65
+ }
66
+ else {
67
+ process.env.XDG_CONFIG_HOME = path.join(tmpHome, ".config");
68
+ }
69
+ const warnings = await runPreflight(baseOptions, emptyManifest, tmpHome, [
70
+ "github-copilot",
71
+ ]);
72
+ const policy = warnings.find((w) => w.kind === "policy");
73
+ assert.ok(policy, "Expected a policy warning");
74
+ assert.match(policy.message, /authenticated against enterprise host\(s\): ghe.acme.com/);
75
+ });
76
+ it("emits no warning for standard github.com hosts.json", async () => {
77
+ const configDir = process.platform === "win32"
78
+ ? path.join(tmpHome, "AppData", "Local", "github-copilot")
79
+ : path.join(tmpHome, ".config", "github-copilot");
80
+ await mkdir(configDir, { recursive: true });
81
+ await writeFile(path.join(configDir, "hosts.json"), JSON.stringify({
82
+ "github.com": { user: "alice" },
83
+ }));
84
+ if (process.platform === "win32") {
85
+ process.env.LOCALAPPDATA = path.join(tmpHome, "AppData", "Local");
86
+ }
87
+ else {
88
+ process.env.XDG_CONFIG_HOME = path.join(tmpHome, ".config");
89
+ }
90
+ const warnings = await runPreflight(baseOptions, emptyManifest, tmpHome, [
91
+ "github-copilot",
92
+ ]);
93
+ const policy = warnings.find((w) => w.kind === "policy");
94
+ assert.ok(!policy, "Expected no policy warning for standard host");
95
+ });
96
+ it("falls back to generic warning if no enterprise detected but policyNote exists and is not redundant", async () => {
97
+ // We already have a "p provisional" warning for antigravity in the registry,
98
+ // but Let's just use github-copilot and ensure it DOES NOT emit the
99
+ // redundant note if no enterprise host is found.
100
+ const warnings = await runPreflight(baseOptions, emptyManifest, tmpHome, [
101
+ "github-copilot",
102
+ ]);
103
+ const policy = warnings.find((w) => w.kind === "policy");
104
+ assert.ok(!policy, "Expected no policy warning for standard user with redundant note");
105
+ });
106
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { afterEach, beforeEach, describe, it } from "node:test";
6
+ import { parseFrontmatterDocument, writeFrontmatterFile, } from "../../src/core/adapters/frontmatter.js";
7
+ describe("Frontmatter Adapter", () => {
8
+ let tmpDir;
9
+ beforeEach(async () => {
10
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "inception-frontmatter-test-"));
11
+ });
12
+ afterEach(async () => {
13
+ await fs.rm(tmpDir, { recursive: true, force: true });
14
+ });
15
+ it("writes a new frontmatter file", async () => {
16
+ const target = path.join(tmpDir, "rule.md");
17
+ const frontmatter = {
18
+ "mcp-servers": {
19
+ "my-skill": { command: "npx", args: ["serve"] },
20
+ },
21
+ };
22
+ await writeFrontmatterFile(target, frontmatter);
23
+ const content = await fs.readFile(target, "utf-8");
24
+ assert.match(content, /^---\nmcp-servers:/);
25
+ assert.match(content, /my-skill:/);
26
+ assert.match(content, /command: npx/);
27
+ // Standard YAML with 2 space increments:
28
+ // mcp-servers:
29
+ // my-skill:
30
+ // command: npx
31
+ // args:
32
+ // - serve
33
+ // args is at 4 spaces. Item is at 6 spaces.
34
+ assert.match(content, /args:\n {6}- serve/);
35
+ });
36
+ it("updates an existing frontmatter file preserving body", async () => {
37
+ const target = path.join(tmpDir, "rule.md");
38
+ const original = "---\ntitle: Old Title\n---\n\n# Body content\nExisting text here.";
39
+ await fs.writeFile(target, original);
40
+ const frontmatter = {
41
+ "mcp-servers": {
42
+ "my-skill": { command: "npx" },
43
+ },
44
+ };
45
+ await writeFrontmatterFile(target, frontmatter, { preserveBody: true });
46
+ const content = await fs.readFile(target, "utf-8");
47
+ assert.match(content, /^---\nmcp-servers:/);
48
+ assert.match(content, /# Body content/);
49
+ assert.match(content, /Existing text here\./);
50
+ });
51
+ it("serializes nested objects correctly", async () => {
52
+ const target = path.join(tmpDir, "rule.md");
53
+ const frontmatter = {
54
+ obj: {
55
+ nested: {
56
+ key: "value",
57
+ },
58
+ },
59
+ };
60
+ await writeFrontmatterFile(target, frontmatter);
61
+ const content = await fs.readFile(target, "utf-8");
62
+ assert.match(content, /obj:\n {2}nested:\n {4}key: value/);
63
+ });
64
+ it("quotes string values with special characters", async () => {
65
+ const target = path.join(tmpDir, "rule.md");
66
+ const frontmatter = {
67
+ special: "v:a#l[u]e",
68
+ plain: "normal",
69
+ };
70
+ await writeFrontmatterFile(target, frontmatter);
71
+ const content = await fs.readFile(target, "utf-8");
72
+ // YAML package usually quotes special characters automatically if required.
73
+ // In this case, v:a#l[u]e is valid unquoted in YAML 1.2 plain scalars.
74
+ assert.match(content, /special: v:a#l\[u\]e/);
75
+ assert.match(content, /plain: normal/);
76
+ });
77
+ it("parses valid frontmatter correctly", () => {
78
+ const raw = "---\nname: my-skill\ndescription: A cool skill\n---\n\n# Body\nHere.";
79
+ const parsed = parseFrontmatterDocument(raw);
80
+ assert.deepEqual(parsed.attributes, {
81
+ name: "my-skill",
82
+ description: "A cool skill",
83
+ });
84
+ assert.equal(parsed.body.trim(), "# Body\nHere.");
85
+ });
86
+ it("parses empty YAML frontmatter block", () => {
87
+ const raw = "---\n---\n# Body";
88
+ const parsed = parseFrontmatterDocument(raw);
89
+ assert.deepEqual(parsed.attributes, {});
90
+ assert.equal(parsed.body.trim(), "# Body");
91
+ });
92
+ it("returns raw content as body when missing frontmatter delimiters", () => {
93
+ const raw = "# Just Markdown\nNo frontmatter.";
94
+ const parsed = parseFrontmatterDocument(raw);
95
+ assert.deepEqual(parsed.attributes, {});
96
+ assert.equal(parsed.body, raw);
97
+ });
98
+ it("throws on malformed YAML in frontmatter", () => {
99
+ const raw = "---\nkey: : bad\n---";
100
+ assert.throws(() => parseFrontmatterDocument(raw), /YAML/);
101
+ });
102
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,243 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, realpath, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { describe, it } from "node:test";
5
+ import { compileAgentDefinitionActions } from "../../src/core/adapters/agent-definitions.js";
6
+ import { runPreflight } from "../../src/core/preflight.js";
7
+ import { makeTmpDir } from "../helpers/fs.js";
8
+ import { assertPathEndsWith } from "../helpers/path.js";
9
+ describe("Gemini CLI North Star", () => {
10
+ it("compiles a global file-write action for gemini-cli when scope is global (.md)", async () => {
11
+ const dir = await makeTmpDir();
12
+ try {
13
+ await writeFile(path.join(dir, "my-agent.md"), "---\nname: test-agent\ndescription: test\ntools: []\n---\n# Agent");
14
+ const realRoot = await realpath(dir);
15
+ const { actions, warnings } = await compileAgentDefinitionActions({
16
+ name: "my-agent",
17
+ agents: ["gemini-cli"],
18
+ path: "my-agent.md",
19
+ scope: "global",
20
+ }, dir, dir, realRoot, ["gemini-cli"], "/home/test", "/repo/test");
21
+ assert.equal(warnings.length, 0);
22
+ assert.equal(actions.length, 1);
23
+ const action = actions[0];
24
+ assert.equal(action.agent, "gemini-cli");
25
+ assertPathEndsWith(action.target, ".gemini/agents/my-agent.md", `expected target under .gemini/agents/my-agent.md, got: ${action.target}`);
26
+ assert.ok(action.target.startsWith("/home/test"), `expected target to start with /home/test, got: ${action.target}`);
27
+ }
28
+ finally {
29
+ await rm(dir, { recursive: true });
30
+ }
31
+ });
32
+ it("deploys .toml source to .gemini/agents/{name}.toml for gemini-cli (global scope)", async () => {
33
+ const dir = await makeTmpDir();
34
+ try {
35
+ await writeFile(path.join(dir, "my-agent.toml"), '[agent]\nname = "my-agent"\ndescription = "test"\n');
36
+ const realRoot = await realpath(dir);
37
+ const { actions, warnings } = await compileAgentDefinitionActions({
38
+ name: "my-agent",
39
+ agents: ["gemini-cli"],
40
+ path: "my-agent.toml",
41
+ scope: "global",
42
+ }, dir, dir, realRoot, ["gemini-cli"], "/home/test", "/repo/test");
43
+ assert.equal(warnings.length, 0);
44
+ assert.equal(actions.length, 1);
45
+ const action = actions[0];
46
+ assert.equal(action.agent, "gemini-cli");
47
+ assertPathEndsWith(action.target, ".gemini/agents/my-agent.toml", `expected target under .gemini/agents/my-agent.toml, got: ${action.target}`);
48
+ assert.ok(action.target.startsWith("/home/test"), `expected target to start with /home/test, got: ${action.target}`);
49
+ }
50
+ finally {
51
+ await rm(dir, { recursive: true });
52
+ }
53
+ });
54
+ it("deploys .toml source to .gemini/agents/{name}.toml for gemini-cli (repo scope)", async () => {
55
+ const dir = await makeTmpDir();
56
+ try {
57
+ await writeFile(path.join(dir, "my-agent.toml"), '[agent]\nname = "my-agent"\ndescription = "test"\n');
58
+ const realRoot = await realpath(dir);
59
+ const { actions, warnings } = await compileAgentDefinitionActions({
60
+ name: "my-agent",
61
+ agents: ["gemini-cli"],
62
+ path: "my-agent.toml",
63
+ scope: "repo",
64
+ }, dir, dir, realRoot, ["gemini-cli"], "/home/test", "/repo/test");
65
+ assert.equal(warnings.length, 0);
66
+ assert.equal(actions.length, 1);
67
+ const action = actions[0];
68
+ assert.equal(action.agent, "gemini-cli");
69
+ assertPathEndsWith(action.target, ".gemini/agents/my-agent.toml", `expected target under .gemini/agents/my-agent.toml, got: ${action.target}`);
70
+ assert.ok(action.target.startsWith("/repo/test"), `expected target to start with /repo/test, got: ${action.target}`);
71
+ }
72
+ finally {
73
+ await rm(dir, { recursive: true });
74
+ }
75
+ });
76
+ it("produces no action for .toml agentDefinition targeting an agent without a TOML surface", async () => {
77
+ const dir = await makeTmpDir();
78
+ try {
79
+ await writeFile(path.join(dir, "my-agent.toml"), '[agent]\nname = "my-agent"\ndescription = "test"\n');
80
+ const realRoot = await realpath(dir);
81
+ // claude-code has no TOML surface — should silently produce no action
82
+ const { actions, warnings } = await compileAgentDefinitionActions({
83
+ name: "my-agent",
84
+ agents: ["claude-code"],
85
+ path: "my-agent.toml",
86
+ scope: "repo",
87
+ }, dir, dir, realRoot, ["claude-code"], "/home/test", "/repo/test");
88
+ assert.equal(actions.length, 0, "expected no action for agent without TOML surface");
89
+ assert.equal(warnings.length, 0, "expected no warnings");
90
+ }
91
+ finally {
92
+ await rm(dir, { recursive: true });
93
+ }
94
+ });
95
+ it("emits no planned-surface warnings for Gemini CLI after alignment", async () => {
96
+ const warnings = await runPreflight({
97
+ directory: "/test",
98
+ command: "deploy",
99
+ dryRun: false,
100
+ agents: ["gemini-cli"],
101
+ verbose: false,
102
+ debug: false,
103
+ force: false,
104
+ }, {
105
+ skills: [],
106
+ mcpServers: [],
107
+ agentRules: [
108
+ {
109
+ name: "rules",
110
+ agents: ["gemini-cli"],
111
+ path: "rules.md",
112
+ scope: "global",
113
+ },
114
+ ],
115
+ agentDefinitions: [
116
+ {
117
+ name: "agent",
118
+ agents: ["gemini-cli"],
119
+ path: "agent.md",
120
+ scope: "global",
121
+ },
122
+ ],
123
+ files: [],
124
+ configs: [],
125
+ permissions: [],
126
+ }, "/home/test", ["gemini-cli"]);
127
+ // None of the old "planned" surface messages should appear after alignment
128
+ assert.ok(!warnings.some((w) => w.message.includes("TOML subagent definitions are documented")), "unexpected warning for TOML subagent definitions");
129
+ assert.ok(!warnings.some((w) => w.message.includes("Instruction filename overrides are documented")), "unexpected warning for configurable instruction filenames");
130
+ assert.ok(!warnings.some((w) => w.message.includes("Gemini CLI loads AGENTS.md natively")), "unexpected warning for AGENTS.md instructions fallback");
131
+ });
132
+ it("emits config-authority warning when settings.json sets a non-default instructionFilename", async () => {
133
+ const home = await makeTmpDir();
134
+ try {
135
+ await mkdir(path.join(home, ".gemini"), { recursive: true });
136
+ await writeFile(path.join(home, ".gemini", "settings.json"), JSON.stringify({ instructionFilename: "CUSTOM.md" }));
137
+ const warnings = await runPreflight({
138
+ directory: "/test",
139
+ command: "deploy",
140
+ dryRun: false,
141
+ agents: ["gemini-cli"],
142
+ verbose: false,
143
+ debug: false,
144
+ force: false,
145
+ }, {
146
+ skills: [],
147
+ mcpServers: [],
148
+ agentRules: [
149
+ {
150
+ name: "rules",
151
+ agents: ["gemini-cli"],
152
+ path: "rules.md",
153
+ scope: "global",
154
+ },
155
+ ],
156
+ agentDefinitions: [],
157
+ files: [],
158
+ configs: [],
159
+ permissions: [],
160
+ }, home, ["gemini-cli"]);
161
+ const filenameWarning = warnings.find((w) => w.kind === "config-authority" && w.message.includes("CUSTOM.md"));
162
+ assert.ok(filenameWarning, `expected config-authority warning about CUSTOM.md, got: ${JSON.stringify(warnings)}`);
163
+ assert.match(filenameWarning.message, /instructionFilename/);
164
+ assert.match(filenameWarning.message, /GEMINI\.md/);
165
+ }
166
+ finally {
167
+ await rm(home, { recursive: true });
168
+ }
169
+ });
170
+ it("emits no warning when settings.json sets instructionFilename to GEMINI.md", async () => {
171
+ const home = await makeTmpDir();
172
+ try {
173
+ await mkdir(path.join(home, ".gemini"), { recursive: true });
174
+ await writeFile(path.join(home, ".gemini", "settings.json"), JSON.stringify({ instructionFilename: "GEMINI.md" }));
175
+ const warnings = await runPreflight({
176
+ directory: "/test",
177
+ command: "deploy",
178
+ dryRun: false,
179
+ agents: ["gemini-cli"],
180
+ verbose: false,
181
+ debug: false,
182
+ force: false,
183
+ }, {
184
+ skills: [],
185
+ mcpServers: [],
186
+ agentRules: [
187
+ {
188
+ name: "rules",
189
+ agents: ["gemini-cli"],
190
+ path: "rules.md",
191
+ scope: "global",
192
+ },
193
+ ],
194
+ agentDefinitions: [],
195
+ files: [],
196
+ configs: [],
197
+ permissions: [],
198
+ }, home, ["gemini-cli"]);
199
+ const filenameWarning = warnings.find((w) => w.kind === "config-authority" &&
200
+ w.message.includes("instructionFilename"));
201
+ assert.equal(filenameWarning, undefined, `expected no filename warning when override matches deploy target, got: ${filenameWarning?.message}`);
202
+ }
203
+ finally {
204
+ await rm(home, { recursive: true });
205
+ }
206
+ });
207
+ it("emits no warning when settings.json is absent", async () => {
208
+ const home = await makeTmpDir();
209
+ try {
210
+ // No .gemini/settings.json file — detection should silently return null
211
+ const warnings = await runPreflight({
212
+ directory: "/test",
213
+ command: "deploy",
214
+ dryRun: false,
215
+ agents: ["gemini-cli"],
216
+ verbose: false,
217
+ debug: false,
218
+ force: false,
219
+ }, {
220
+ skills: [],
221
+ mcpServers: [],
222
+ agentRules: [
223
+ {
224
+ name: "rules",
225
+ agents: ["gemini-cli"],
226
+ path: "rules.md",
227
+ scope: "global",
228
+ },
229
+ ],
230
+ agentDefinitions: [],
231
+ files: [],
232
+ configs: [],
233
+ permissions: [],
234
+ }, home, ["gemini-cli"]);
235
+ const filenameWarning = warnings.find((w) => w.kind === "config-authority" &&
236
+ w.message.includes("instructionFilename"));
237
+ assert.equal(filenameWarning, undefined, `expected no filename warning when settings.json is absent, got: ${filenameWarning?.message}`);
238
+ }
239
+ finally {
240
+ await rm(home, { recursive: true });
241
+ }
242
+ });
243
+ });
@@ -0,0 +1 @@
1
+ export {};