@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
@@ -1,42 +1,82 @@
1
1
  import { access, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
3
+ import { AGENT_REGISTRY } from "../config/agents.js";
4
4
  import { dryRunPrefix, logger } from "../logger.js";
5
5
  import { AGENT_IDS, AgentDefinitionEntrySchema, ConfigEntrySchema, FileEntrySchema, McpServerEntrySchema, } from "../schemas/manifest.js";
6
+ import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
7
+ import { shouldInitIncludeAgent } from "./capabilities.js";
6
8
  const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
7
9
  // Ordered list: first match wins. Catch-all is applied at call site.
8
- const AGENT_RULES_FILE_PATTERNS = [
9
- {
10
- fileNames: ["claude.md", "claude-instructions.md"],
11
- agents: ["claude-code"],
12
- },
13
- {
14
- fileNames: ["agents.md", "agents-instructions.md"],
15
- agents: ["codex", "opencode"],
16
- },
17
- {
18
- fileNames: ["gemini.md", "gemini-instructions.md"],
19
- agents: ["gemini-cli", "antigravity"],
20
- },
21
- { fileNames: ["copilot-instructions.md"], agents: ["claude-code"] },
22
- ];
10
+ // Derived from the registry: group agents by the filename of their global
11
+ // agentRulesSupport path. Agents with requiresPrimary (e.g. github-copilot)
12
+ // are excluded because they cannot be deployed independently. The
13
+ // copilot-instructions.md convention mapping is appended explicitly since it
14
+ // is a well-known filename convention, not derivable from a path template.
15
+ const AGENT_RULES_FILE_PATTERNS = (() => {
16
+ const filenameToAgents = new Map();
17
+ for (const agent of AGENT_REGISTRY) {
18
+ if (!shouldInitIncludeAgent(agent.id, "agentRules", "global"))
19
+ continue;
20
+ const support = agent.agentRulesSupport;
21
+ if (support?.status !== "supported")
22
+ continue;
23
+ const filename = support.path.posix[support.path.posix.length - 1]?.toLowerCase();
24
+ if (!filename)
25
+ continue;
26
+ const list = filenameToAgents.get(filename) ?? [];
27
+ list.push(agent.id);
28
+ filenameToAgents.set(filename, list);
29
+ }
30
+ return [
31
+ ...Array.from(filenameToAgents.entries()).map(([filename, agents]) => ({
32
+ fileNames: [filename, filename.replace(".md", "-instructions.md")],
33
+ agents,
34
+ })),
35
+ // Convention mapping: copilot-instructions.md → claude-code. Copilot reads
36
+ // CLAUDE.md natively; this filename is a well-known convention that cannot
37
+ // be derived from any agent path template.
38
+ {
39
+ fileNames: ["copilot-instructions.md"],
40
+ agents: ["claude-code"],
41
+ },
42
+ ];
43
+ })();
23
44
  // Conventional subdirectory names to scan one level deep for .md files.
24
- const AGENT_RULES_SUBDIRS = [
25
- "rules",
26
- "instructions",
27
- ".github",
28
- ".agents/rules",
29
- ];
45
+ const AGENT_RULES_SUBDIRS = ["rules", "instructions", ".github"];
30
46
  // Conventional subdirectories that contain agent definition files.
31
- // These are the agent-specific directories that each agent scans for
32
- // subagent/persona definitions at runtime.
33
- const AGENT_DEFINITION_SUBDIRS = [
34
- ".claude/agents",
35
- ".gemini/agents",
36
- ".agents/rules",
37
- ".opencode/agents",
38
- ".github/agents",
39
- ];
47
+ // Derived from the registry: for each agent with a supported agentDefinitions
48
+ // surface, extract the directory prefix from its posix path template.
49
+ // Includes the legacy ".github/agents" path for backward compatibility.
50
+ const AGENT_DEFINITION_SUBDIRS = (() => {
51
+ const subdirs = new Set();
52
+ for (const agent of AGENT_REGISTRY) {
53
+ const support = agent.agentDefinitionsRepoSupport;
54
+ if (support?.status !== "supported")
55
+ continue;
56
+ const tmpl = support.path.posix;
57
+ const repoIdx = tmpl.indexOf("{repo}");
58
+ const nameIdx = tmpl.findIndex((s) => s.includes("{name}"));
59
+ if (repoIdx === -1 || nameIdx === -1 || nameIdx <= repoIdx)
60
+ continue;
61
+ const dirSegs = tmpl.slice(repoIdx + 1, nameIdx);
62
+ if (dirSegs.length > 0)
63
+ subdirs.add(dirSegs.join("/"));
64
+ }
65
+ subdirs.add(".github/agents");
66
+ return Array.from(subdirs);
67
+ })();
68
+ async function hasAntigravityMcpFrontmatter(absPath) {
69
+ let raw;
70
+ try {
71
+ raw = await readFile(absPath, "utf-8");
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ const { attributes } = parseFrontmatterDocument(raw);
77
+ return (Object.hasOwn(attributes, "mcp-servers") ||
78
+ Object.hasOwn(attributes, "mcpServers"));
79
+ }
40
80
  async function findSkillDirs(baseDir, dir, found) {
41
81
  let entries;
42
82
  try {
@@ -75,6 +115,7 @@ function resolveSkillName(relPath, name, namesSeen) {
75
115
  return null;
76
116
  }
77
117
  function buildSkills(found, agents) {
118
+ const initAgents = agents.filter((agentId) => shouldInitIncludeAgent(agentId, "skills"));
78
119
  const namesSeen = new Set();
79
120
  const skills = [];
80
121
  for (const { relPath, name } of found) {
@@ -82,7 +123,7 @@ function buildSkills(found, agents) {
82
123
  if (skillName === null)
83
124
  continue;
84
125
  namesSeen.add(skillName);
85
- skills.push({ name: skillName, path: relPath, agents });
126
+ skills.push({ name: skillName, path: relPath, agents: initAgents });
86
127
  }
87
128
  return skills;
88
129
  }
@@ -176,7 +217,7 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
176
217
  }
177
218
  /**
178
219
  * Derives the name for an agent definition entry from its file name.
179
- * For GitHub Copilot's `{name}.agent.md` naming convention, strips the
220
+ * For GitHub Copilot's legacy `{name}.agent.md` naming convention, strips the
180
221
  * `.agent` infix in addition to the `.md` extension.
181
222
  */
182
223
  function deriveAgentDefinitionName(relPath, fileName) {
@@ -196,24 +237,56 @@ function deriveAgentDefinitionName(relPath, fileName) {
196
237
  }
197
238
  /**
198
239
  * Maps a known agent-definition subdirectory to the agent IDs that own it.
199
- * Returns null when the subdir is not agent-specific (fall back to all agents
200
- * that support agentDefinitions).
240
+ * Derived from the registry by matching each agent's agentDefinitionsSupport
241
+ * path prefix. Includes legacy mapping for ".github/agents".
242
+ * Returns null when the subdir is not recognized.
201
243
  */
202
244
  function agentsForDefinitionSubdir(subdir) {
203
- switch (subdir) {
204
- case ".claude/agents":
205
- return ["claude-code"];
206
- case ".gemini/agents":
207
- return ["gemini-cli"];
208
- case ".agents/rules":
209
- return ["antigravity"];
210
- case ".opencode/agents":
211
- return ["opencode"];
212
- case ".github/agents":
213
- return ["github-copilot"];
214
- default:
215
- return null;
245
+ if (subdir === ".github/agents")
246
+ return ["github-copilot"];
247
+ const matching = [];
248
+ for (const agent of AGENT_REGISTRY) {
249
+ const support = agent.agentDefinitionsRepoSupport;
250
+ if (support?.status !== "supported")
251
+ continue;
252
+ const tmpl = support.path.posix;
253
+ const repoIdx = tmpl.indexOf("{repo}");
254
+ const nameIdx = tmpl.findIndex((s) => s.includes("{name}"));
255
+ if (repoIdx === -1 || nameIdx === -1 || nameIdx <= repoIdx)
256
+ continue;
257
+ const agentSubdir = tmpl.slice(repoIdx + 1, nameIdx).join("/");
258
+ if (agentSubdir === subdir)
259
+ matching.push(agent.id);
260
+ }
261
+ return matching.length > 0 ? matching : null;
262
+ }
263
+ /**
264
+ * Returns true when a file in a definition subdir contains MCP-specific
265
+ * frontmatter and should be excluded from agentDefinitions discovery. Detects
266
+ * this by checking whether any registered agent uses the same directory as
267
+ * both its definition surface and its MCP surface (currently: Antigravity's
268
+ * .agents/rules/).
269
+ */
270
+ async function isSkippedMcpFile(subdir, absPath, relPath) {
271
+ const hasMcpSurfaceHere = AGENT_REGISTRY.some((agent) => {
272
+ const support = agent.mcpSupport;
273
+ if (support?.status !== "supported")
274
+ return false;
275
+ const tmpl = support.path.posix;
276
+ const repoIdx = tmpl.indexOf("{repo}");
277
+ const nameIdx = tmpl.findIndex((s) => s.includes("{name}"));
278
+ if (repoIdx === -1 || nameIdx === -1 || nameIdx <= repoIdx)
279
+ return false;
280
+ const prefix = tmpl.slice(repoIdx + 1, nameIdx).join("/");
281
+ return prefix === subdir;
282
+ });
283
+ if (!hasMcpSurfaceHere)
284
+ return false;
285
+ const isMcp = await hasAntigravityMcpFrontmatter(absPath);
286
+ if (isMcp) {
287
+ logger.warn("init", `Skipping "${relPath}" as agentDefinitions: frontmatter contains "mcp-servers" — file is an MCP surface, not an agent definition`);
216
288
  }
289
+ return isMcp;
217
290
  }
218
291
  async function scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, agentRulesRelPaths, candidates) {
219
292
  const suggestedAgents = agentsForDefinitionSubdir(subdir) ?? [];
@@ -237,6 +310,8 @@ async function scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, age
237
310
  isInsideSkillDir(relPath, skillDirRelPaths) ||
238
311
  agentRulesRelPaths.has(relPath))
239
312
  continue;
313
+ if (await isSkippedMcpFile(subdir, absPath, relPath))
314
+ continue;
240
315
  const name = deriveAgentDefinitionName(relPath, entry.name);
241
316
  if (name === null)
242
317
  continue;
@@ -255,8 +330,7 @@ async function findAgentDefinitionCandidates(baseDir, skillDirRelPaths, agentRul
255
330
  function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
256
331
  const definitions = [];
257
332
  const localNamesSeen = new Set(namesSeen);
258
- const definitionsCapableAgents = activeAgents.filter((id) => AGENT_REGISTRY_BY_ID[id].agentDefinitionsSupport?.status !==
259
- "unsupported");
333
+ const definitionsCapableAgents = activeAgents.filter((id) => shouldInitIncludeAgent(id, "agentDefinitions", "repo"));
260
334
  for (const { relPath, name: rawName, suggestedAgents } of candidates) {
261
335
  // Use suggested agents (from the dir that was scanned), intersected with
262
336
  // active agents that support agentDefinitions. Fall back to all capable
@@ -281,7 +355,7 @@ function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
281
355
  }
282
356
  }
283
357
  localNamesSeen.add(name);
284
- definitions.push({ name, path: relPath, agents });
358
+ definitions.push({ name, path: relPath, agents, scope: "repo" });
285
359
  }
286
360
  return definitions;
287
361
  }
@@ -478,7 +552,7 @@ function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agen
478
552
  export async function runInit(options) {
479
553
  const { directory, dryRun, force, verbose } = options;
480
554
  const agents = options.agents ?? [...AGENT_IDS];
481
- const agentRulesCapableAgents = agents.filter((id) => AGENT_REGISTRY_BY_ID[id].agentRulesSupport?.status !== "unsupported");
555
+ const agentRulesCapableAgents = agents.filter((id) => shouldInitIncludeAgent(id, "agentRules", "global"));
482
556
  const manifestPath = path.join(directory, "inception.json");
483
557
  if (!dryRun && (await manifestExists(manifestPath)) && !force) {
484
558
  logger.error(`Error: ${manifestPath} already exists. Use --force to overwrite.`);
@@ -0,0 +1,4 @@
1
+ export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
2
+ export declare function computeUndoPatch(original: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
3
+ export declare function applyMergePatch(original: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
4
+ export declare function applyUndoPatch(current: Record<string, unknown>, undoPatch: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,46 @@
1
+ export function isPlainObject(v) {
2
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3
+ }
4
+ export function computeUndoPatch(original, patch) {
5
+ const undoPatch = {};
6
+ for (const key of Object.keys(patch)) {
7
+ const patchVal = patch[key];
8
+ if (isPlainObject(patchVal) && isPlainObject(original[key])) {
9
+ undoPatch[key] = computeUndoPatch(original[key], patchVal);
10
+ }
11
+ else {
12
+ undoPatch[key] = key in original ? original[key] : null;
13
+ }
14
+ }
15
+ return undoPatch;
16
+ }
17
+ export function applyMergePatch(original, patch) {
18
+ const patched = { ...original };
19
+ for (const [key, value] of Object.entries(patch)) {
20
+ if (value === null) {
21
+ delete patched[key];
22
+ }
23
+ else if (isPlainObject(value) && isPlainObject(patched[key])) {
24
+ patched[key] = applyMergePatch(patched[key], value);
25
+ }
26
+ else {
27
+ patched[key] = value;
28
+ }
29
+ }
30
+ return patched;
31
+ }
32
+ export function applyUndoPatch(current, undoPatch) {
33
+ const restored = { ...current };
34
+ for (const [key, originalValue] of Object.entries(undoPatch)) {
35
+ if (originalValue === null) {
36
+ delete restored[key];
37
+ }
38
+ else if (isPlainObject(originalValue) && isPlainObject(restored[key])) {
39
+ restored[key] = applyUndoPatch(restored[key], originalValue);
40
+ }
41
+ else {
42
+ restored[key] = originalValue;
43
+ }
44
+ }
45
+ return restored;
46
+ }
@@ -1,4 +1,4 @@
1
- import { type ConfigPatchRegistryEntry, type FileWriteRegistryEntry, type Registry, type RegistryEntry, type SkillDirRegistryEntry } from "../schemas/registry.ts";
1
+ import { type ConfigPatchRegistryEntry, type FileWriteRegistryEntry, type FrontmatterEmitRegistryEntry, type Registry, type RegistryEntry, type SkillDirRegistryEntry } from "../schemas/registry.ts";
2
2
  import type { AgentId } from "../types.ts";
3
3
  export type { RegistryEntry } from "../schemas/registry.ts";
4
4
  export interface RegistryPersistence {
@@ -22,7 +22,7 @@ export type VerifyExpected = {
22
22
  };
23
23
  export declare function registryPath(home: string): string;
24
24
  export declare const defaultRegistryPersistence: RegistryPersistence;
25
- export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed">;
25
+ export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed"> | Omit<FrontmatterEmitRegistryEntry, "deployed">;
26
26
  export declare function registerDeployment(home: string, targetPath: string, entry: RegisterEntry, persistence?: RegistryPersistence): Promise<void>;
27
27
  export declare function unregisterDeployment(home: string, targetPath: string, persistence?: RegistryPersistence): Promise<void>;
28
28
  export declare function lookupDeployment(home: string, targetPath: string, persistence?: RegistryPersistence): Promise<RegistryEntry | null>;
@@ -40,10 +40,23 @@ async function setFilePermissions(filePath) {
40
40
  function emptyRegistry() {
41
41
  return { version: 1, deployments: {} };
42
42
  }
43
+ function surfaceIdForEntry(entry) {
44
+ return entry.surfaceId ?? `${entry.kind}:${entry.agent}:${entry.skill}`;
45
+ }
43
46
  export async function registerDeployment(home, targetPath, entry, persistence = defaultRegistryPersistence) {
44
47
  const registry = await persistence.load(home);
48
+ const surfaceId = surfaceIdForEntry(entry);
49
+ for (const migratedTarget of entry.migratedFrom ?? []) {
50
+ if (migratedTarget === targetPath)
51
+ continue;
52
+ const existing = registry.deployments[migratedTarget];
53
+ if (existing?.surfaceId === surfaceId) {
54
+ delete registry.deployments[migratedTarget];
55
+ }
56
+ }
45
57
  registry.deployments[targetPath] = {
46
58
  ...entry,
59
+ surfaceId,
47
60
  deployed: new Date().toISOString(),
48
61
  };
49
62
  await persistence.save(home, registry);
@@ -3,4 +3,4 @@ export interface PreflightWarning {
3
3
  kind: "policy" | "config-authority" | "info" | "precedence" | "budget";
4
4
  message: string;
5
5
  }
6
- export declare function runPreflight(options: CliOptions, manifest: Manifest, _home: string, detectedAgents: AgentId[]): Promise<PreflightWarning[]>;
6
+ export declare function runPreflight(options: CliOptions, manifest: Manifest, home: string, detectedAgents: AgentId[]): Promise<PreflightWarning[]>;
@@ -0,0 +1,280 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
+ import { describeCapabilityConfidence, planCapabilityForDeploy, } from "./capabilities.js";
5
+ import { resolveRuntimePaths } from "./runtime-paths.js";
6
+ const BUDGET_WARN_BYTES = 50 * 1024; // 50 KB
7
+ async function detectEnterpriseManagement(agentId, home) {
8
+ if (!AGENT_REGISTRY_BY_ID[agentId]?.enterprisePolicyDetection)
9
+ return null;
10
+ // Check for common GitHub Enterprise environment variables
11
+ if (process.env.GITHUB_ENTERPRISE_URL ||
12
+ process.env.GH_ENTERPRISE_TOKEN ||
13
+ process.env.GITHUB_TOKEN_TYPE === "enterprise") {
14
+ return "GitHub Enterprise environment variables detected. Enterprise policies may override local configurations.";
15
+ }
16
+ const { xdgConfig, localAppdata } = resolveRuntimePaths(home);
17
+ const configPath = process.platform === "win32"
18
+ ? path.join(localAppdata, "github-copilot", "hosts.json")
19
+ : path.join(xdgConfig, "github-copilot", "hosts.json");
20
+ try {
21
+ const content = await readFile(configPath, "utf8");
22
+ const hosts = JSON.parse(content);
23
+ const hostNames = Object.keys(hosts);
24
+ const enterpriseHosts = hostNames.filter((h) => h !== "github.com" && h !== "localhost");
25
+ if (enterpriseHosts.length > 0) {
26
+ return `GitHub Copilot is authenticated against enterprise host(s): ${enterpriseHosts.join(", ")}. Enterprise policies may override local configurations.`;
27
+ }
28
+ }
29
+ catch (err) {
30
+ const code = err.code;
31
+ if (code !== "ENOENT" &&
32
+ code !== "EACCES" &&
33
+ code !== "EPERM" &&
34
+ code !== undefined) {
35
+ throw err;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * Reads ~/.gemini/settings.json and warns when `instructionFilename` is set
42
+ * to a value other than "GEMINI.md". If the override is in effect, inception-
43
+ * engine's agentRules deployment to GEMINI.md will be silently ignored by the
44
+ * agent at runtime.
45
+ */
46
+ async function detectGeminiCustomInstructionFilename(home) {
47
+ const settingsPath = path.join(home, ".gemini", "settings.json");
48
+ try {
49
+ const raw = await readFile(settingsPath, "utf8");
50
+ const parsed = JSON.parse(raw);
51
+ if (typeof parsed === "object" &&
52
+ parsed !== null &&
53
+ typeof parsed.instructionFilename ===
54
+ "string") {
55
+ const configured = parsed
56
+ .instructionFilename;
57
+ if (configured !== "GEMINI.md") {
58
+ return {
59
+ kind: "config-authority",
60
+ message: `Agent "gemini-cli": settings.json sets instructionFilename to "${configured}" but inception-engine deploys agentRules to "GEMINI.md". The deployed rules file may not be loaded by the agent. Remove the override or align the deploy target.`,
61
+ };
62
+ }
63
+ }
64
+ }
65
+ catch (err) {
66
+ const code = err.code;
67
+ if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM") {
68
+ throw err;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ function groupRulesByPath(rulesForAgent) {
74
+ const entriesByPath = new Map();
75
+ for (const entry of rulesForAgent ?? []) {
76
+ const list = entriesByPath.get(entry.path) ?? [];
77
+ list.push({ name: entry.name, scope: entry.scope });
78
+ entriesByPath.set(entry.path, list);
79
+ }
80
+ return entriesByPath;
81
+ }
82
+ function detectScopeOverlaps(agentId, rulesForAgent) {
83
+ const warnings = [];
84
+ const entriesByPath = groupRulesByPath(rulesForAgent);
85
+ for (const [path, list] of entriesByPath) {
86
+ const scopes = new Set(list.map((l) => l.scope));
87
+ if (scopes.size < 2)
88
+ continue;
89
+ const scopeList = Array.from(scopes).sort();
90
+ for (let i = 0; i < scopeList.length; i++) {
91
+ for (let j = i + 1; j < scopeList.length; j++) {
92
+ const entryB = list.find((l) => l.scope === scopeList[j]);
93
+ if (entryB) {
94
+ warnings.push({
95
+ kind: "precedence",
96
+ message: `Agent "${agentId}" has agentRules entry "${entryB.name}" deployed to both ${scopeList[i]} and ${scopeList[j]} scope from the same source path "${path}". The file will be written to two distinct targets — verify this is intentional and not a copy-paste mistake.`,
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+ return warnings;
103
+ }
104
+ function detectMultipleActiveInstructionScopes(agentId, rulesForAgent) {
105
+ const activeScopes = Array.from(new Set((rulesForAgent ?? []).map((e) => e.scope))).sort();
106
+ if (activeScopes.length <= 1)
107
+ return [];
108
+ const scopeDescriptions = activeScopes
109
+ .map((s) => {
110
+ const entries = (rulesForAgent ?? []).filter((e) => e.scope === s);
111
+ return `${s} [${entries.map((e) => `"${e.name}"`).join(", ")}]`;
112
+ })
113
+ .join(" and ");
114
+ return [
115
+ {
116
+ kind: "precedence",
117
+ message: `Agent "${agentId}" will have multiple instruction files active simultaneously across ${activeScopes.length} scopes: ${scopeDescriptions}. All will be loaded by the agent — ensure the content is intended to stack and does not conflict.`,
118
+ },
119
+ ];
120
+ }
121
+ function detectInstructionPrecedence(detectedAgents, manifest) {
122
+ const warnings = [];
123
+ for (const agentId of detectedAgents) {
124
+ const rulesForAgent = (manifest.agentRules ?? []).filter((e) => e.agents.includes(agentId));
125
+ warnings.push(...detectScopeOverlaps(agentId, rulesForAgent));
126
+ warnings.push(...detectMultipleActiveInstructionScopes(agentId, rulesForAgent));
127
+ }
128
+ return warnings;
129
+ }
130
+ async function detectInstructionBudgetRisk(detectedAgents, manifest, sourceDir) {
131
+ const warnings = [];
132
+ const checkedPaths = new Set();
133
+ async function checkEntry(entryPath, label, agentIds) {
134
+ if (!agentIds.some((id) => detectedAgents.includes(id)))
135
+ return;
136
+ const sourcePath = path.resolve(sourceDir, entryPath);
137
+ if (checkedPaths.has(sourcePath))
138
+ return;
139
+ checkedPaths.add(sourcePath);
140
+ try {
141
+ const fileStat = await stat(sourcePath);
142
+ if (fileStat.size > BUDGET_WARN_BYTES) {
143
+ const sizeKb = (fileStat.size / 1024).toFixed(1);
144
+ warnings.push({
145
+ kind: "budget",
146
+ message: `${label} source "${entryPath}" is ${sizeKb} KB — large instruction files risk crowding out code context in the agent's context window. Consider splitting into smaller focused files.`,
147
+ });
148
+ }
149
+ }
150
+ catch (err) {
151
+ const code = err.code;
152
+ if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM")
153
+ throw err;
154
+ // Missing/unreadable files are silently skipped; compileAgentRuleActions
155
+ // will produce the proper UserError during action compilation.
156
+ }
157
+ }
158
+ for (const entry of manifest.agentRules ?? []) {
159
+ await checkEntry(entry.path, "agentRules", entry.agents);
160
+ }
161
+ for (const entry of manifest.agentDefinitions ?? []) {
162
+ await checkEntry(entry.path, "agentDefinitions", entry.agents);
163
+ }
164
+ return warnings;
165
+ }
166
+ function pushCapabilityWarning(acc, kind, message) {
167
+ if (acc.seen.has(`${kind}:${message}`))
168
+ return;
169
+ acc.seen.add(`${kind}:${message}`);
170
+ acc.warnings.push({ kind, message });
171
+ }
172
+ function collectCapabilityWarningsForAgent(acc, agentId, capability, entryName, targetAgents, scope) {
173
+ const plan = planCapabilityForDeploy({
174
+ agentId,
175
+ capability,
176
+ entryName,
177
+ targetAgentIds: targetAgents,
178
+ scope,
179
+ });
180
+ if (plan.outcome === "warn") {
181
+ pushCapabilityWarning(acc, "info", plan.warning.message);
182
+ return;
183
+ }
184
+ // Evaluate undocumented/planned surfaces warning
185
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
186
+ if (agent?.unsupportedSurfaces) {
187
+ for (const surface of agent.unsupportedSurfaces) {
188
+ if (surface.status === "planned") {
189
+ pushCapabilityWarning(acc, "info", `Agent "${agentId}": ${surface.reason ?? surface.plannedSurface}`);
190
+ }
191
+ else if (surface.status === "unsupported") {
192
+ pushCapabilityWarning(acc, "config-authority", `Agent "${agentId}": ${surface.reason ?? surface.schemaLabel}`);
193
+ }
194
+ }
195
+ }
196
+ if (capability === "skills")
197
+ return;
198
+ const confidence = describeCapabilityConfidence(agentId, capability, scope);
199
+ if (confidence.message) {
200
+ pushCapabilityWarning(acc, "config-authority", confidence.message);
201
+ }
202
+ }
203
+ function collectCapabilityWarningsForTargets(acc, targetAgents, capability, entryName, scope) {
204
+ for (const agentId of targetAgents) {
205
+ collectCapabilityWarningsForAgent(acc, agentId, capability, entryName, targetAgents, scope);
206
+ }
207
+ }
208
+ function collectManifestCapabilityWarnings(manifest, detectedAgents) {
209
+ const acc = {
210
+ warnings: [],
211
+ seen: new Set(),
212
+ };
213
+ for (const entry of manifest.skills) {
214
+ collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "skills", entry.name);
215
+ }
216
+ for (const entry of manifest.mcpServers ?? []) {
217
+ collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "mcpServers", entry.name, entry.scope);
218
+ }
219
+ for (const entry of manifest.agentRules ?? []) {
220
+ collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "agentRules", entry.name, entry.scope);
221
+ }
222
+ for (const entry of manifest.permissions ?? []) {
223
+ collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "permissions", entry.name);
224
+ }
225
+ for (const entry of manifest.agentDefinitions ?? []) {
226
+ collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "agentDefinitions", entry.name, entry.scope);
227
+ }
228
+ return acc.warnings;
229
+ }
230
+ function detectCapabilityPlanningWarnings(manifest, detectedAgents) {
231
+ return collectManifestCapabilityWarnings(manifest, detectedAgents);
232
+ }
233
+ async function collectAgentWarnings(agentId, manifest, home) {
234
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
235
+ if (!agent)
236
+ return [];
237
+ const warnings = [];
238
+ const skillConfidence = describeCapabilityConfidence(agentId, "skills");
239
+ if (skillConfidence.confidence === "implementation-only" ||
240
+ skillConfidence.confidence === "provisional") {
241
+ warnings.push({
242
+ kind: "config-authority",
243
+ message: skillConfidence.message ??
244
+ `Agent "${agentId}" skill support is ${skillConfidence.confidence}.`,
245
+ });
246
+ }
247
+ const enterpriseWarning = await detectEnterpriseManagement(agentId, home);
248
+ if (enterpriseWarning) {
249
+ warnings.push({
250
+ kind: "policy",
251
+ message: `Agent "${agentId}": ${enterpriseWarning}`,
252
+ });
253
+ }
254
+ else if (agent.policyNote &&
255
+ !agent.policyNote.includes("Organization policies may override")) {
256
+ warnings.push({
257
+ kind: "policy",
258
+ message: `Agent "${agentId}": ${agent.policyNote}`,
259
+ });
260
+ }
261
+ if (agentId === "gemini-cli") {
262
+ const hasGeminiRules = (manifest.agentRules ?? []).some((e) => e.agents.includes("gemini-cli"));
263
+ if (hasGeminiRules) {
264
+ const filenameWarning = await detectGeminiCustomInstructionFilename(home);
265
+ if (filenameWarning)
266
+ warnings.push(filenameWarning);
267
+ }
268
+ }
269
+ return warnings;
270
+ }
271
+ export async function runPreflight(options, manifest, home, detectedAgents) {
272
+ const warnings = [];
273
+ for (const agentId of detectedAgents) {
274
+ warnings.push(...(await collectAgentWarnings(agentId, manifest, home)));
275
+ }
276
+ warnings.push(...detectCapabilityPlanningWarnings(manifest, detectedAgents));
277
+ warnings.push(...detectInstructionPrecedence(detectedAgents, manifest));
278
+ warnings.push(...(await detectInstructionBudgetRisk(detectedAgents, manifest, options.directory)));
279
+ return warnings;
280
+ }
@@ -9,4 +9,4 @@ export declare function resolveAgentSkillPathFor(agent: AgentConfig, skillName:
9
9
  export declare function resolveAgentDetectPathFor(agent: AgentConfig, home: string, platform: "posix" | "windows"): string;
10
10
  export declare function resolveAgentSkillPath(agent: AgentConfig, skillName: string, home: string): string;
11
11
  export declare function resolveAgentDetectPath(agent: AgentConfig, home: string): string;
12
- export declare function resolvePlaceholders(segments: string[], skillName: string, home: string, repo?: string): string;
12
+ export declare function resolvePlaceholders(segments: string[], skillName: string, home: string, repo?: string, workspace?: string): string;
@@ -86,8 +86,10 @@ export function getDeployMethod() {
86
86
  }
87
87
  export function resolveAgentSkillPathFor(agent, skillName, home, platform) {
88
88
  if (!agent.skills) {
89
- throw new Error(`Agent "${agent.id}" does not have a skills deployment path. ` +
90
- `Deploy skills via another agent target that covers this agent natively (e.g. claude-code for github-copilot).`);
89
+ const hint = agent.skillsSurfaceKind?.kind === "shared-via"
90
+ ? ` Deploy skills via the "${agent.skillsSurfaceKind.via}" target, which covers this agent natively.`
91
+ : "";
92
+ throw new Error(`Agent "${agent.id}" does not have a skills deployment path.${hint}`);
91
93
  }
92
94
  return resolvePlaceholders(agent.skills[platform], skillName, home);
93
95
  }
@@ -100,14 +102,15 @@ export function resolveAgentSkillPath(agent, skillName, home) {
100
102
  export function resolveAgentDetectPath(agent, home) {
101
103
  return resolveAgentDetectPathFor(agent, home, getPlatformKey());
102
104
  }
103
- export function resolvePlaceholders(segments, skillName, home, repo) {
105
+ export function resolvePlaceholders(segments, skillName, home, repo, workspace) {
104
106
  const { appdata, xdgConfig } = resolveRuntimePaths(home);
105
107
  const resolved = segments.map((seg) => seg
106
108
  .replace("{home}", home)
107
109
  .replace("{name}", skillName)
108
110
  .replace("{appdata}", appdata)
109
111
  .replace("{xdg_config}", xdgConfig)
110
- .replace("{repo}", repo ?? ""));
112
+ .replace("{repo}", repo ?? "")
113
+ .replace("{workspace}", workspace ?? repo ?? ""));
111
114
  const root = resolved.find((segment) => segment.length > 0) ?? home;
112
115
  return getPathApi(root).join(...resolved);
113
116
  }