@kuznai/inception-engine 0.19.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 (105) hide show
  1. package/README.md +48 -22
  2. package/dist/{config → src/config}/agents.js +87 -14
  3. package/dist/{core → src/core}/adapters/agent-definitions.d.ts +0 -12
  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/mcp.js +17 -20
  8. package/dist/{core → src/core}/adapters/permissions.js +16 -20
  9. package/dist/{core → src/core}/adapters/rules.js +26 -50
  10. package/dist/src/core/capabilities.d.ts +47 -0
  11. package/dist/src/core/capabilities.js +280 -0
  12. package/dist/{core → src/core}/deploy.js +70 -41
  13. package/dist/{core → src/core}/init.js +18 -24
  14. package/dist/src/core/merge-patch.d.ts +4 -0
  15. package/dist/src/core/merge-patch.js +46 -0
  16. package/dist/{core → src/core}/ownership.js +13 -0
  17. package/dist/{core → src/core}/preflight.js +141 -29
  18. package/dist/{core → src/core}/revert.js +86 -26
  19. package/dist/{core → src/core}/validation.js +20 -0
  20. package/dist/{schemas → src/schemas}/manifest.d.ts +20 -0
  21. package/dist/{schemas → src/schemas}/manifest.js +11 -0
  22. package/dist/{schemas → src/schemas}/registry.d.ts +36 -0
  23. package/dist/{schemas → src/schemas}/registry.js +12 -4
  24. package/dist/{types.d.ts → src/types.d.ts} +13 -0
  25. package/dist/test/helpers/fs.d.ts +2 -0
  26. package/dist/test/helpers/fs.js +17 -0
  27. package/dist/test/helpers/path.d.ts +10 -0
  28. package/dist/test/helpers/path.js +15 -0
  29. package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
  30. package/dist/test/helpers/skill-dir-scenarios.js +206 -0
  31. package/dist/test/helpers/skill-dir.d.ts +7 -0
  32. package/dist/test/helpers/skill-dir.js +46 -0
  33. package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
  34. package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
  35. package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
  36. package/dist/test/os/posix/skill-dir.test.js +19 -0
  37. package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
  38. package/dist/test/os/windows/revert-integration.test.js +72 -0
  39. package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
  40. package/dist/test/os/windows/skill-dir.test.js +19 -0
  41. package/dist/test/unit/adapters.test.d.ts +1 -0
  42. package/dist/test/unit/adapters.test.js +947 -0
  43. package/dist/test/unit/capabilities.test.d.ts +1 -0
  44. package/dist/test/unit/capabilities.test.js +71 -0
  45. package/dist/test/unit/cli.test.d.ts +1 -0
  46. package/dist/test/unit/cli.test.js +834 -0
  47. package/dist/test/unit/deploy.test.d.ts +1 -0
  48. package/dist/test/unit/deploy.test.js +2481 -0
  49. package/dist/test/unit/detect.test.d.ts +1 -0
  50. package/dist/test/unit/detect.test.js +192 -0
  51. package/dist/test/unit/enterprise.test.d.ts +1 -0
  52. package/dist/test/unit/enterprise.test.js +106 -0
  53. package/dist/test/unit/frontmatter.test.d.ts +1 -0
  54. package/dist/test/unit/frontmatter.test.js +102 -0
  55. package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
  56. package/dist/test/unit/gemini-north-star.test.js +243 -0
  57. package/dist/test/unit/manifest.test.d.ts +1 -0
  58. package/dist/test/unit/manifest.test.js +737 -0
  59. package/dist/test/unit/migration-golden.test.d.ts +1 -0
  60. package/dist/test/unit/migration-golden.test.js +197 -0
  61. package/dist/test/unit/ownership.test.d.ts +1 -0
  62. package/dist/test/unit/ownership.test.js +587 -0
  63. package/dist/test/unit/preflight.test.d.ts +1 -0
  64. package/dist/test/unit/preflight.test.js +501 -0
  65. package/dist/test/unit/resolve.test.d.ts +1 -0
  66. package/dist/test/unit/resolve.test.js +119 -0
  67. package/dist/test/unit/revert.test.d.ts +1 -0
  68. package/dist/test/unit/revert.test.js +1004 -0
  69. package/dist/test/unit/toml.test.d.ts +1 -0
  70. package/dist/test/unit/toml.test.js +55 -0
  71. package/package.json +1 -1
  72. package/dist/core/adapters/agent-definitions.js +0 -91
  73. /package/dist/{config → src/config}/agents.d.ts +0 -0
  74. /package/dist/{config → src/config}/manifest.d.ts +0 -0
  75. /package/dist/{config → src/config}/manifest.js +0 -0
  76. /package/dist/{core → src/core}/adapters/index.d.ts +0 -0
  77. /package/dist/{core → src/core}/adapters/index.js +0 -0
  78. /package/dist/{core → src/core}/adapters/mcp.d.ts +0 -0
  79. /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
  80. /package/dist/{core → src/core}/adapters/rules.d.ts +0 -0
  81. /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
  82. /package/dist/{core → src/core}/adapters/toml.js +0 -0
  83. /package/dist/{core → src/core}/deploy.d.ts +0 -0
  84. /package/dist/{core → src/core}/detect.d.ts +0 -0
  85. /package/dist/{core → src/core}/detect.js +0 -0
  86. /package/dist/{core → src/core}/init.d.ts +0 -0
  87. /package/dist/{core → src/core}/ownership.d.ts +0 -0
  88. /package/dist/{core → src/core}/preflight.d.ts +0 -0
  89. /package/dist/{core → src/core}/resolve.d.ts +0 -0
  90. /package/dist/{core → src/core}/resolve.js +0 -0
  91. /package/dist/{core → src/core}/revert.d.ts +0 -0
  92. /package/dist/{core → src/core}/runtime-paths.d.ts +0 -0
  93. /package/dist/{core → src/core}/runtime-paths.js +0 -0
  94. /package/dist/{core → src/core}/validation.d.ts +0 -0
  95. /package/dist/{errors.d.ts → src/errors.d.ts} +0 -0
  96. /package/dist/{errors.js → src/errors.js} +0 -0
  97. /package/dist/{formatters.d.ts → src/formatters.d.ts} +0 -0
  98. /package/dist/{formatters.js → src/formatters.js} +0 -0
  99. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  100. /package/dist/{index.js → src/index.js} +0 -0
  101. /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
  102. /package/dist/{logger.js → src/logger.js} +0 -0
  103. /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
  104. /package/dist/{schemas → src/schemas}/errors.js +0 -0
  105. /package/dist/{types.js → src/types.js} +0 -0
@@ -5,13 +5,12 @@ import { AGENT_REGISTRY, AGENT_REGISTRY_BY_ID } from "../config/agents.js";
5
5
  import { UserError } from "../errors.js";
6
6
  import { logger } from "../logger.js";
7
7
  import { compileAdapterActions } from "./adapters/index.js";
8
+ import { planCapabilityForDeploy } from "./capabilities.js";
9
+ import { applyMergePatch, computeUndoPatch, isPlainObject, } from "./merge-patch.js";
8
10
  import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
9
11
  import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
10
12
  import { resolveTargetTemplate } from "./runtime-paths.js";
11
13
  import { sourceAccessError, validateSkillDefinitionFile, validateSourceFile, validateSourcePath, } from "./validation.js";
12
- function isPlainObject(v) {
13
- return typeof v === "object" && v !== null && !Array.isArray(v);
14
- }
15
14
  async function readJsonConfigFile(filePath) {
16
15
  let rawContent;
17
16
  try {
@@ -35,34 +34,6 @@ async function readJsonConfigFile(filePath) {
35
34
  }
36
35
  return parsed;
37
36
  }
38
- function computeUndoPatch(original, patch) {
39
- const undoPatch = {};
40
- for (const key of Object.keys(patch)) {
41
- const patchVal = patch[key];
42
- if (isPlainObject(patchVal) && isPlainObject(original[key])) {
43
- undoPatch[key] = computeUndoPatch(original[key], patchVal);
44
- }
45
- else {
46
- undoPatch[key] = key in original ? original[key] : null;
47
- }
48
- }
49
- return undoPatch;
50
- }
51
- function applyMergePatch(original, patch) {
52
- const patched = { ...original };
53
- for (const [key, value] of Object.entries(patch)) {
54
- if (value === null) {
55
- delete patched[key];
56
- }
57
- else if (isPlainObject(value) && isPlainObject(patched[key])) {
58
- patched[key] = applyMergePatch(patched[key], value);
59
- }
60
- else {
61
- patched[key] = value;
62
- }
63
- }
64
- return patched;
65
- }
66
37
  function detectCollisions(actions) {
67
38
  const seen = new Map();
68
39
  const warnings = [];
@@ -197,22 +168,39 @@ function checkAntigravityPathCollisions(manifest) {
197
168
  }
198
169
  return warnings;
199
170
  }
171
+ function assertNoAntigravityPathCollisions(manifest) {
172
+ const collisions = checkAntigravityPathCollisions(manifest);
173
+ if (collisions.length === 0)
174
+ return;
175
+ throw new UserError("MANIFEST_INVALID", collisions.map((warning) => warning.message).join("; "));
176
+ }
200
177
  async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
201
178
  const method = getDeployMethod();
202
179
  const actions = [];
203
- for (const skill of manifest.skills) {
180
+ const warnings = [];
181
+ async function planSkillEntry(skill) {
204
182
  const targetAgents = skill.agents.filter((agentId) => detectedAgents.includes(agentId));
205
183
  if (targetAgents.length === 0)
206
- continue;
184
+ return;
207
185
  const source = path.resolve(sourceDir, skill.path);
208
186
  await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
209
187
  await validateSkillContract(source, skill.path);
210
188
  for (const agentId of targetAgents) {
189
+ const plan = planCapabilityForDeploy({
190
+ agentId,
191
+ capability: "skills",
192
+ entryName: skill.name,
193
+ targetAgentIds: targetAgents,
194
+ });
195
+ if (plan.outcome === "warn") {
196
+ warnings.push(plan.warning);
197
+ continue;
198
+ }
199
+ if (plan.outcome === "native" || plan.outcome === "redundant")
200
+ continue;
211
201
  const agent = AGENT_REGISTRY_BY_ID[agentId];
212
202
  if (!agent)
213
203
  continue;
214
- if (!agent.skills)
215
- continue;
216
204
  actions.push({
217
205
  kind: "skill-dir",
218
206
  skill: skill.name,
@@ -220,11 +208,14 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
220
208
  source,
221
209
  target: resolveAgentSkillPath(agent, skill.name, home),
222
210
  method,
223
- confidence: agent.provenance.skills ?? "provisional",
211
+ confidence: plan.confidence ?? "provisional",
224
212
  });
225
213
  }
226
214
  }
227
- return actions;
215
+ for (const skill of manifest.skills) {
216
+ await planSkillEntry(skill);
217
+ }
218
+ return { actions, warnings };
228
219
  }
229
220
  async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace) {
230
221
  const actions = [];
@@ -273,6 +264,7 @@ function planConfigPatchActions(manifest, detectedAgents, home, repo, workspace)
273
264
  return actions;
274
265
  }
275
266
  export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo, workspace) {
267
+ assertNoAntigravityPathCollisions(manifest);
276
268
  const resolvedSourceDir = path.resolve(sourceDir);
277
269
  let realRoot;
278
270
  try {
@@ -282,14 +274,16 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo
282
274
  realRoot = resolvedSourceDir;
283
275
  }
284
276
  const repoDir = repo ?? realRoot;
277
+ const skillPlan = await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
285
278
  const actions = [
286
- ...(await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
279
+ ...skillPlan.actions,
287
280
  ...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, workspace)),
288
281
  ...planConfigPatchActions(manifest, detectedAgents, home, repoDir, workspace),
289
282
  ];
290
283
  const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, manifest.agentDefinitions ?? [], workspace);
291
284
  actions.push(...adapterResult.actions);
292
285
  const warnings = [
286
+ ...skillPlan.warnings,
293
287
  ...detectAmbiguities(detectedAgents, manifest),
294
288
  ...checkAntigravityPathCollisions(manifest),
295
289
  ...detectCollisions(actions),
@@ -495,6 +489,7 @@ async function deployFileWrite(action, dryRun, verbose, home, planned, deps) {
495
489
  source: action.source,
496
490
  skill: action.skill,
497
491
  agent: action.agent,
492
+ migratedFrom: action.migratedFrom,
498
493
  }, deps.registry));
499
494
  logger.ok(label);
500
495
  if (verbose) {
@@ -605,6 +600,11 @@ async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
605
600
  }
606
601
  async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
607
602
  const label = `${action.skill} -> ${action.agent}`;
603
+ if (!isPlainObject(action.frontmatter)) {
604
+ const msg = `Frontmatter patch for skill "${action.skill}" must be a plain object`;
605
+ logger.fail(label, msg);
606
+ return { error: msg };
607
+ }
608
608
  if (dryRun) {
609
609
  planned.push({
610
610
  verb: "emit-frontmatter",
@@ -617,12 +617,41 @@ async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, dep
617
617
  return { error: null };
618
618
  }
619
619
  try {
620
- await (await import("./adapters/frontmatter.js")).writeFrontmatterFile(action.target, action.frontmatter, { preserveBody: true });
621
- await registerDeployment(home, action.target, {
620
+ const existingEntry = await lookupDeployment(home, action.target, deps.registry);
621
+ if (existingEntry &&
622
+ (existingEntry.kind !== "frontmatter-emit" ||
623
+ existingEntry.skill !== action.skill ||
624
+ existingEntry.agent !== action.agent)) {
625
+ throw new Error(`Frontmatter target "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" — refusing to double-patch`);
626
+ }
627
+ const frontmatterAdapter = await import("./adapters/frontmatter.js");
628
+ const existing = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
629
+ const undoPatch = computeUndoPatch(existing.attributes, action.frontmatter);
630
+ const patchedFrontmatter = applyMergePatch(existing.attributes, action.frontmatter);
631
+ const content = frontmatterAdapter.buildMarkdownDocument(patchedFrontmatter, existing.body, { hasFrontmatter: true });
632
+ await replaceFileAtomically(action.target, deps, (tempPath, fileOps) => fileOps.writeFile(tempPath, content, "utf-8"), async () => {
633
+ try {
634
+ await lstat(action.target);
635
+ }
636
+ catch {
637
+ return null;
638
+ }
639
+ const backupPath = `${action.target}.inception-backup`;
640
+ await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
641
+ recursive: true,
642
+ force: true,
643
+ });
644
+ await (deps.fileOps ?? defaultDeployFileOps).rename(action.target, backupPath);
645
+ return backupPath;
646
+ }, () => registerDeployment(home, action.target, {
622
647
  kind: "frontmatter-emit",
648
+ patch: action.frontmatter,
649
+ undoPatch,
650
+ created: !existing.exists,
651
+ hadFrontmatter: existing.hasFrontmatter,
623
652
  skill: action.skill,
624
653
  agent: action.agent,
625
- }, deps.registry);
654
+ }, deps.registry));
626
655
  logger.ok(label);
627
656
  if (verbose) {
628
657
  logger.detail(`emit-frontmatter: ${action.target}`);
@@ -1,9 +1,10 @@
1
1
  import { access, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { AGENT_REGISTRY, 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
6
  import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
7
+ import { shouldInitIncludeAgent } from "./capabilities.js";
7
8
  const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
8
9
  // Ordered list: first match wins. Catch-all is applied at call site.
9
10
  // Derived from the registry: group agents by the filename of their global
@@ -14,13 +15,11 @@ const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
14
15
  const AGENT_RULES_FILE_PATTERNS = (() => {
15
16
  const filenameToAgents = new Map();
16
17
  for (const agent of AGENT_REGISTRY) {
18
+ if (!shouldInitIncludeAgent(agent.id, "agentRules", "global"))
19
+ continue;
17
20
  const support = agent.agentRulesSupport;
18
21
  if (support?.status !== "supported")
19
22
  continue;
20
- // Skip riders that require the primary — they cannot be listed independently
21
- if (support.surfaceKind?.kind === "shared-via" &&
22
- support.surfaceKind.requiresPrimary)
23
- continue;
24
23
  const filename = support.path.posix[support.path.posix.length - 1]?.toLowerCase();
25
24
  if (!filename)
26
25
  continue;
@@ -47,10 +46,11 @@ const AGENT_RULES_SUBDIRS = ["rules", "instructions", ".github"];
47
46
  // Conventional subdirectories that contain agent definition files.
48
47
  // Derived from the registry: for each agent with a supported agentDefinitions
49
48
  // surface, extract the directory prefix from its posix path template.
49
+ // Includes the legacy ".github/agents" path for backward compatibility.
50
50
  const AGENT_DEFINITION_SUBDIRS = (() => {
51
51
  const subdirs = new Set();
52
52
  for (const agent of AGENT_REGISTRY) {
53
- const support = agent.agentDefinitionsSupport;
53
+ const support = agent.agentDefinitionsRepoSupport;
54
54
  if (support?.status !== "supported")
55
55
  continue;
56
56
  const tmpl = support.path.posix;
@@ -62,6 +62,7 @@ const AGENT_DEFINITION_SUBDIRS = (() => {
62
62
  if (dirSegs.length > 0)
63
63
  subdirs.add(dirSegs.join("/"));
64
64
  }
65
+ subdirs.add(".github/agents");
65
66
  return Array.from(subdirs);
66
67
  })();
67
68
  async function hasAntigravityMcpFrontmatter(absPath) {
@@ -114,6 +115,7 @@ function resolveSkillName(relPath, name, namesSeen) {
114
115
  return null;
115
116
  }
116
117
  function buildSkills(found, agents) {
118
+ const initAgents = agents.filter((agentId) => shouldInitIncludeAgent(agentId, "skills"));
117
119
  const namesSeen = new Set();
118
120
  const skills = [];
119
121
  for (const { relPath, name } of found) {
@@ -121,7 +123,7 @@ function buildSkills(found, agents) {
121
123
  if (skillName === null)
122
124
  continue;
123
125
  namesSeen.add(skillName);
124
- skills.push({ name: skillName, path: relPath, agents });
126
+ skills.push({ name: skillName, path: relPath, agents: initAgents });
125
127
  }
126
128
  return skills;
127
129
  }
@@ -215,7 +217,7 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
215
217
  }
216
218
  /**
217
219
  * Derives the name for an agent definition entry from its file name.
218
- * For GitHub Copilot's `{name}.agent.md` naming convention, strips the
220
+ * For GitHub Copilot's legacy `{name}.agent.md` naming convention, strips the
219
221
  * `.agent` infix in addition to the `.md` extension.
220
222
  */
221
223
  function deriveAgentDefinitionName(relPath, fileName) {
@@ -236,12 +238,15 @@ function deriveAgentDefinitionName(relPath, fileName) {
236
238
  /**
237
239
  * Maps a known agent-definition subdirectory to the agent IDs that own it.
238
240
  * Derived from the registry by matching each agent's agentDefinitionsSupport
239
- * path prefix. Returns null when the subdir is not recognized.
241
+ * path prefix. Includes legacy mapping for ".github/agents".
242
+ * Returns null when the subdir is not recognized.
240
243
  */
241
244
  function agentsForDefinitionSubdir(subdir) {
245
+ if (subdir === ".github/agents")
246
+ return ["github-copilot"];
242
247
  const matching = [];
243
248
  for (const agent of AGENT_REGISTRY) {
244
- const support = agent.agentDefinitionsSupport;
249
+ const support = agent.agentDefinitionsRepoSupport;
245
250
  if (support?.status !== "supported")
246
251
  continue;
247
252
  const tmpl = support.path.posix;
@@ -325,8 +330,7 @@ async function findAgentDefinitionCandidates(baseDir, skillDirRelPaths, agentRul
325
330
  function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
326
331
  const definitions = [];
327
332
  const localNamesSeen = new Set(namesSeen);
328
- const definitionsCapableAgents = activeAgents.filter((id) => AGENT_REGISTRY_BY_ID[id].agentDefinitionsSupport?.status !==
329
- "unsupported");
333
+ const definitionsCapableAgents = activeAgents.filter((id) => shouldInitIncludeAgent(id, "agentDefinitions", "repo"));
330
334
  for (const { relPath, name: rawName, suggestedAgents } of candidates) {
331
335
  // Use suggested agents (from the dir that was scanned), intersected with
332
336
  // active agents that support agentDefinitions. Fall back to all capable
@@ -351,7 +355,7 @@ function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
351
355
  }
352
356
  }
353
357
  localNamesSeen.add(name);
354
- definitions.push({ name, path: relPath, agents });
358
+ definitions.push({ name, path: relPath, agents, scope: "repo" });
355
359
  }
356
360
  return definitions;
357
361
  }
@@ -548,17 +552,7 @@ function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agen
548
552
  export async function runInit(options) {
549
553
  const { directory, dryRun, force, verbose } = options;
550
554
  const agents = options.agents ?? [...AGENT_IDS];
551
- const agentRulesCapableAgents = agents.filter((id) => {
552
- const support = AGENT_REGISTRY_BY_ID[id].agentRulesSupport;
553
- if (!support || support.status === "unsupported")
554
- return false;
555
- // Exclude riders that require the primary — they cannot operate independently
556
- if (support.status === "supported" &&
557
- support.surfaceKind?.kind === "shared-via" &&
558
- support.surfaceKind.requiresPrimary)
559
- return false;
560
- return true;
561
- });
555
+ const agentRulesCapableAgents = agents.filter((id) => shouldInitIncludeAgent(id, "agentRules", "global"));
562
556
  const manifestPath = path.join(directory, "inception.json");
563
557
  if (!dryRun && (await manifestExists(manifestPath)) && !force) {
564
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
+ }
@@ -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);
@@ -1,6 +1,7 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
+ import { describeCapabilityConfidence, planCapabilityForDeploy, } from "./capabilities.js";
4
5
  import { resolveRuntimePaths } from "./runtime-paths.js";
5
6
  const BUDGET_WARN_BYTES = 50 * 1024; // 50 KB
6
7
  async function detectEnterpriseManagement(agentId, home) {
@@ -36,6 +37,39 @@ async function detectEnterpriseManagement(agentId, home) {
36
37
  }
37
38
  return null;
38
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
+ }
39
73
  function groupRulesByPath(rulesForAgent) {
40
74
  const entriesByPath = new Map();
41
75
  for (const entry of rulesForAgent ?? []) {
@@ -129,39 +163,117 @@ async function detectInstructionBudgetRisk(detectedAgents, manifest, sourceDir)
129
163
  }
130
164
  return warnings;
131
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
+ }
132
271
  export async function runPreflight(options, manifest, home, detectedAgents) {
133
272
  const warnings = [];
134
273
  for (const agentId of detectedAgents) {
135
- const agent = AGENT_REGISTRY_BY_ID[agentId];
136
- if (!agent)
137
- continue;
138
- if (agent.provenance.skills === "implementation-only") {
139
- warnings.push({
140
- kind: "config-authority",
141
- message: `Agent "${agentId}" skill support is implementation-only: paths are derived from source inspection, not published documentation.`,
142
- });
143
- }
144
- else if (agent.provenance.skills === "provisional") {
145
- warnings.push({
146
- kind: "config-authority",
147
- message: `Agent "${agentId}" skill support is provisional: behavior has not been independently verified.`,
148
- });
149
- }
150
- const enterpriseWarning = await detectEnterpriseManagement(agentId, home);
151
- if (enterpriseWarning) {
152
- warnings.push({
153
- kind: "policy",
154
- message: `Agent "${agentId}": ${enterpriseWarning}`,
155
- });
156
- }
157
- else if (agent.policyNote &&
158
- !agent.policyNote.includes("Organization policies may override")) {
159
- warnings.push({
160
- kind: "policy",
161
- message: `Agent "${agentId}": ${agent.policyNote}`,
162
- });
163
- }
274
+ warnings.push(...(await collectAgentWarnings(agentId, manifest, home)));
164
275
  }
276
+ warnings.push(...detectCapabilityPlanningWarnings(manifest, detectedAgents));
165
277
  warnings.push(...detectInstructionPrecedence(detectedAgents, manifest));
166
278
  warnings.push(...(await detectInstructionBudgetRisk(detectedAgents, manifest, options.directory)));
167
279
  return warnings;