@kuznai/inception-engine 0.19.0 → 0.21.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 (111) hide show
  1. package/README.md +50 -22
  2. package/dist/{config → src/config}/agents.js +113 -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/src/core/adapters/index.js +25 -0
  8. package/dist/{core → src/core}/adapters/mcp.js +17 -20
  9. package/dist/{core → src/core}/adapters/permissions.js +16 -20
  10. package/dist/{core → src/core}/adapters/rules.js +26 -50
  11. package/dist/src/core/capabilities.d.ts +47 -0
  12. package/dist/src/core/capabilities.js +280 -0
  13. package/dist/{core → src/core}/deploy.js +82 -54
  14. package/dist/{core → src/core}/detect.js +2 -7
  15. package/dist/{core → src/core}/init.js +20 -25
  16. package/dist/src/core/merge-patch.d.ts +4 -0
  17. package/dist/src/core/merge-patch.js +46 -0
  18. package/dist/{core → src/core}/ownership.js +13 -0
  19. package/dist/src/core/preflight.js +296 -0
  20. package/dist/{core → src/core}/revert.js +86 -26
  21. package/dist/{core → src/core}/validation.js +31 -0
  22. package/dist/{errors.d.ts → src/errors.d.ts} +3 -1
  23. package/dist/{errors.js → src/errors.js} +2 -2
  24. package/dist/{schemas → src/schemas}/manifest.d.ts +20 -0
  25. package/dist/{schemas → src/schemas}/manifest.js +11 -0
  26. package/dist/{schemas → src/schemas}/registry.d.ts +36 -0
  27. package/dist/{schemas → src/schemas}/registry.js +12 -4
  28. package/dist/{types.d.ts → src/types.d.ts} +15 -0
  29. package/dist/test/helpers/fs.d.ts +2 -0
  30. package/dist/test/helpers/fs.js +17 -0
  31. package/dist/test/helpers/path.d.ts +10 -0
  32. package/dist/test/helpers/path.js +15 -0
  33. package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
  34. package/dist/test/helpers/skill-dir-scenarios.js +206 -0
  35. package/dist/test/helpers/skill-dir.d.ts +7 -0
  36. package/dist/test/helpers/skill-dir.js +46 -0
  37. package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
  38. package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
  39. package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
  40. package/dist/test/os/posix/skill-dir.test.js +19 -0
  41. package/dist/test/os/windows/agentRules-integration.test.d.ts +1 -0
  42. package/dist/test/os/windows/agentRules-integration.test.js +245 -0
  43. package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
  44. package/dist/test/os/windows/revert-integration.test.js +72 -0
  45. package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
  46. package/dist/test/os/windows/skill-dir.test.js +19 -0
  47. package/dist/test/unit/adapters.test.d.ts +1 -0
  48. package/dist/test/unit/adapters.test.js +1017 -0
  49. package/dist/test/unit/capabilities.test.d.ts +1 -0
  50. package/dist/test/unit/capabilities.test.js +71 -0
  51. package/dist/test/unit/cli.test.d.ts +1 -0
  52. package/dist/test/unit/cli.test.js +834 -0
  53. package/dist/test/unit/deploy.test.d.ts +1 -0
  54. package/dist/test/unit/deploy.test.js +2481 -0
  55. package/dist/test/unit/detect.test.d.ts +1 -0
  56. package/dist/test/unit/detect.test.js +192 -0
  57. package/dist/test/unit/enterprise.test.d.ts +1 -0
  58. package/dist/test/unit/enterprise.test.js +106 -0
  59. package/dist/test/unit/frontmatter.test.d.ts +1 -0
  60. package/dist/test/unit/frontmatter.test.js +102 -0
  61. package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
  62. package/dist/test/unit/gemini-north-star.test.js +243 -0
  63. package/dist/test/unit/init-fixture.test.d.ts +1 -0
  64. package/dist/test/unit/init-fixture.test.js +153 -0
  65. package/dist/test/unit/manifest.test.d.ts +1 -0
  66. package/dist/test/unit/manifest.test.js +737 -0
  67. package/dist/test/unit/migration-golden.test.d.ts +1 -0
  68. package/dist/test/unit/migration-golden.test.js +197 -0
  69. package/dist/test/unit/ownership.test.d.ts +1 -0
  70. package/dist/test/unit/ownership.test.js +587 -0
  71. package/dist/test/unit/preflight.test.d.ts +1 -0
  72. package/dist/test/unit/preflight.test.js +521 -0
  73. package/dist/test/unit/resolve.test.d.ts +1 -0
  74. package/dist/test/unit/resolve.test.js +119 -0
  75. package/dist/test/unit/revert.test.d.ts +1 -0
  76. package/dist/test/unit/revert.test.js +1004 -0
  77. package/dist/test/unit/toml.test.d.ts +1 -0
  78. package/dist/test/unit/toml.test.js +55 -0
  79. package/package.json +1 -1
  80. package/dist/core/adapters/agent-definitions.js +0 -91
  81. package/dist/core/adapters/index.js +0 -30
  82. package/dist/core/preflight.js +0 -168
  83. /package/dist/{config → src/config}/agents.d.ts +0 -0
  84. /package/dist/{config → src/config}/manifest.d.ts +0 -0
  85. /package/dist/{config → src/config}/manifest.js +0 -0
  86. /package/dist/{core → src/core}/adapters/index.d.ts +0 -0
  87. /package/dist/{core → src/core}/adapters/mcp.d.ts +0 -0
  88. /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
  89. /package/dist/{core → src/core}/adapters/rules.d.ts +0 -0
  90. /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
  91. /package/dist/{core → src/core}/adapters/toml.js +0 -0
  92. /package/dist/{core → src/core}/deploy.d.ts +0 -0
  93. /package/dist/{core → src/core}/detect.d.ts +0 -0
  94. /package/dist/{core → src/core}/init.d.ts +0 -0
  95. /package/dist/{core → src/core}/ownership.d.ts +0 -0
  96. /package/dist/{core → src/core}/preflight.d.ts +0 -0
  97. /package/dist/{core → src/core}/resolve.d.ts +0 -0
  98. /package/dist/{core → src/core}/resolve.js +0 -0
  99. /package/dist/{core → src/core}/revert.d.ts +0 -0
  100. /package/dist/{core → src/core}/runtime-paths.d.ts +0 -0
  101. /package/dist/{core → src/core}/runtime-paths.js +0 -0
  102. /package/dist/{core → src/core}/validation.d.ts +0 -0
  103. /package/dist/{formatters.d.ts → src/formatters.d.ts} +0 -0
  104. /package/dist/{formatters.js → src/formatters.js} +0 -0
  105. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  106. /package/dist/{index.js → src/index.js} +0 -0
  107. /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
  108. /package/dist/{logger.js → src/logger.js} +0 -0
  109. /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
  110. /package/dist/{schemas → src/schemas}/errors.js +0 -0
  111. /package/dist/{types.js → src/types.js} +0 -0
@@ -4,14 +4,15 @@ import path from "node:path";
4
4
  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
+ import * as frontmatterAdapter from "./adapters/frontmatter.js";
7
8
  import { compileAdapterActions } from "./adapters/index.js";
9
+ import { applyTomlMcpPatch } from "./adapters/toml.js";
10
+ import { planCapabilityForDeploy } from "./capabilities.js";
11
+ import { applyMergePatch, computeUndoPatch, isPlainObject, } from "./merge-patch.js";
8
12
  import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
9
13
  import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
10
14
  import { resolveTargetTemplate } from "./runtime-paths.js";
11
15
  import { sourceAccessError, validateSkillDefinitionFile, validateSourceFile, validateSourcePath, } from "./validation.js";
12
- function isPlainObject(v) {
13
- return typeof v === "object" && v !== null && !Array.isArray(v);
14
- }
15
16
  async function readJsonConfigFile(filePath) {
16
17
  let rawContent;
17
18
  try {
@@ -35,34 +36,6 @@ async function readJsonConfigFile(filePath) {
35
36
  }
36
37
  return parsed;
37
38
  }
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
39
  function detectCollisions(actions) {
67
40
  const seen = new Map();
68
41
  const warnings = [];
@@ -134,7 +107,7 @@ function checkPairAgentRuleAmbiguities(manifest, primary, rider) {
134
107
  JSON.stringify(primarySupport.path) === JSON.stringify(riderSupport.path)) {
135
108
  warnings.push({
136
109
  kind: "ambiguity",
137
- message: `Both "${primary}" and "${rider}" are listed in agentRules entry "${entry.name}". Both target the same surface listing both is redundant but harmless; deduplication ensures only one write action is emitted.`,
110
+ message: `Both "${primary}" and "${rider}" are listed in agentRules entry "${entry.name}". Both target the same surface - listing both is redundant but harmless; deduplication ensures only one write action is emitted.`,
138
111
  });
139
112
  }
140
113
  }
@@ -151,7 +124,7 @@ function checkPairMcpAmbiguities(manifest, primary, rider) {
151
124
  continue;
152
125
  warnings.push({
153
126
  kind: "ambiguity",
154
- message: `Both "${primary}" and "${rider}" are listed in mcpServers entry "${entry.name}". "${primary}" writes to a shared MCP surface verify that deploying to both does not produce conflicting MCP server behavior.`,
127
+ message: `Both "${primary}" and "${rider}" are listed in mcpServers entry "${entry.name}". "${primary}" writes to a shared MCP surface - verify that deploying to both does not produce conflicting MCP server behavior.`,
155
128
  });
156
129
  }
157
130
  return warnings;
@@ -163,7 +136,7 @@ function checkPairAgentDefinitionAmbiguities(manifest, primary, rider) {
163
136
  continue;
164
137
  warnings.push({
165
138
  kind: "ambiguity",
166
- message: `Both "${primary}" and "${rider}" are listed in agentDefinitions entry "${entry.name}". They write to distinct surfaces verify that this behavioral divergence is intended.`,
139
+ message: `Both "${primary}" and "${rider}" are listed in agentDefinitions entry "${entry.name}". They write to distinct surfaces - verify that this behavioral divergence is intended.`,
167
140
  });
168
141
  }
169
142
  return warnings;
@@ -191,28 +164,45 @@ function checkAntigravityPathCollisions(manifest) {
191
164
  if (defNames.has(entry.name)) {
192
165
  warnings.push({
193
166
  kind: "collision",
194
- message: `agentDefinitions entry "${entry.name}" and mcpServers entry "${entry.name}" for agent "antigravity" both resolve to {repo}/.agents/rules/${entry.name}.md one will silently overwrite the other; use different names or remove one entry`,
167
+ message: `agentDefinitions entry "${entry.name}" and mcpServers entry "${entry.name}" for agent "antigravity" both resolve to {repo}/.agents/rules/${entry.name}.md - one will silently overwrite the other; use different names or remove one entry`,
195
168
  });
196
169
  }
197
170
  }
198
171
  return warnings;
199
172
  }
173
+ function assertNoAntigravityPathCollisions(manifest) {
174
+ const collisions = checkAntigravityPathCollisions(manifest);
175
+ if (collisions.length === 0)
176
+ return;
177
+ throw new UserError("MANIFEST_INVALID", collisions.map((warning) => warning.message).join("; "));
178
+ }
200
179
  async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
201
180
  const method = getDeployMethod();
202
181
  const actions = [];
203
- for (const skill of manifest.skills) {
182
+ const warnings = [];
183
+ async function planSkillEntry(skill) {
204
184
  const targetAgents = skill.agents.filter((agentId) => detectedAgents.includes(agentId));
205
185
  if (targetAgents.length === 0)
206
- continue;
186
+ return;
207
187
  const source = path.resolve(sourceDir, skill.path);
208
188
  await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
209
189
  await validateSkillContract(source, skill.path);
210
190
  for (const agentId of targetAgents) {
191
+ const plan = planCapabilityForDeploy({
192
+ agentId,
193
+ capability: "skills",
194
+ entryName: skill.name,
195
+ targetAgentIds: targetAgents,
196
+ });
197
+ if (plan.outcome === "warn") {
198
+ warnings.push(plan.warning);
199
+ continue;
200
+ }
201
+ if (plan.outcome === "native" || plan.outcome === "redundant")
202
+ continue;
211
203
  const agent = AGENT_REGISTRY_BY_ID[agentId];
212
204
  if (!agent)
213
205
  continue;
214
- if (!agent.skills)
215
- continue;
216
206
  actions.push({
217
207
  kind: "skill-dir",
218
208
  skill: skill.name,
@@ -220,18 +210,19 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
220
210
  source,
221
211
  target: resolveAgentSkillPath(agent, skill.name, home),
222
212
  method,
223
- confidence: agent.provenance.skills ?? "provisional",
213
+ confidence: plan.confidence ?? "provisional",
224
214
  });
225
215
  }
226
216
  }
227
- return actions;
217
+ await Promise.all(manifest.skills.map(planSkillEntry));
218
+ return { actions, warnings };
228
219
  }
229
220
  async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace) {
230
221
  const actions = [];
231
- for (const fileEntry of manifest.files ?? []) {
222
+ await Promise.all((manifest.files ?? []).map(async (fileEntry) => {
232
223
  const targetAgents = fileEntry.agents.filter((agentId) => detectedAgents.includes(agentId));
233
224
  if (targetAgents.length === 0)
234
- continue;
225
+ return;
235
226
  const source = path.resolve(sourceDir, fileEntry.path);
236
227
  await validateSourcePath(source, fileEntry.path, resolvedSourceDir, realRoot);
237
228
  await validateSourceFile(source, fileEntry.path);
@@ -245,10 +236,10 @@ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, real
245
236
  agent: agentId,
246
237
  source,
247
238
  target: resolveTargetTemplate(fileEntry.target, home, repo, workspace),
248
- confidence: agent.provenance.skills,
239
+ confidence: "implementation-only",
249
240
  });
250
241
  }
251
- }
242
+ }));
252
243
  return actions;
253
244
  }
254
245
  function planConfigPatchActions(manifest, detectedAgents, home, repo, workspace) {
@@ -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),
@@ -376,7 +370,7 @@ async function backupManagedFileWriteTarget(action, home, deps) {
376
370
  agent: action.agent,
377
371
  }, deps.registry);
378
372
  if (!isOwned) {
379
- throw new Error(`Target "${action.target}" exists but is not managed by inception-engine refusing to overwrite`);
373
+ throw new Error(`Target "${action.target}" exists but is not managed by inception-engine - refusing to overwrite`);
380
374
  }
381
375
  const backupPath = `${action.target}.inception-backup`;
382
376
  await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
@@ -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) {
@@ -533,7 +528,7 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
533
528
  if (existingEntry &&
534
529
  (existingEntry.skill !== action.skill ||
535
530
  existingEntry.agent !== action.agent)) {
536
- throw new Error(`Config "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" refusing to double-patch`);
531
+ throw new Error(`Config "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" - refusing to double-patch`);
537
532
  }
538
533
  const original = await readJsonConfigFile(action.target);
539
534
  const undoPatch = computeUndoPatch(original, patch);
@@ -581,7 +576,7 @@ async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
581
576
  try {
582
577
  const { previousValue } = await (deps.registry
583
578
  ? Promise.resolve({ previousValue: null })
584
- : (await import("./adapters/toml.js")).applyTomlMcpPatch(action.target, action.skill, action.config));
579
+ : applyTomlMcpPatch(action.target, action.skill, action.config));
585
580
  // Note: To truly support custom deps here we'd need to refactor toml adapter to accept deps.
586
581
  // For now we assume standard adapter for TOML.
587
582
  await registerDeployment(home, action.target, {
@@ -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,40 @@ 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 existing = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
628
+ const undoPatch = computeUndoPatch(existing.attributes, action.frontmatter);
629
+ const patchedFrontmatter = applyMergePatch(existing.attributes, action.frontmatter);
630
+ const content = frontmatterAdapter.buildMarkdownDocument(patchedFrontmatter, existing.body, { hasFrontmatter: true });
631
+ await replaceFileAtomically(action.target, deps, (tempPath, fileOps) => fileOps.writeFile(tempPath, content, "utf-8"), async () => {
632
+ try {
633
+ await lstat(action.target);
634
+ }
635
+ catch {
636
+ return null;
637
+ }
638
+ const backupPath = `${action.target}.inception-backup`;
639
+ await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
640
+ recursive: true,
641
+ force: true,
642
+ });
643
+ await (deps.fileOps ?? defaultDeployFileOps).rename(action.target, backupPath);
644
+ return backupPath;
645
+ }, () => registerDeployment(home, action.target, {
622
646
  kind: "frontmatter-emit",
647
+ patch: action.frontmatter,
648
+ undoPatch,
649
+ created: !existing.exists,
650
+ hadFrontmatter: existing.hasFrontmatter,
623
651
  skill: action.skill,
624
652
  agent: action.agent,
625
- }, deps.registry);
653
+ }, deps.registry));
626
654
  logger.ok(label);
627
655
  if (verbose) {
628
656
  logger.detail(`emit-frontmatter: ${action.target}`);
@@ -683,7 +711,7 @@ async function assertTargetAbsent(targetPath) {
683
711
  catch (err) {
684
712
  if (err instanceof Error && err.message.startsWith("Target path appeared"))
685
713
  throw err;
686
- // ENOENT is expected target should not exist after backup
714
+ // ENOENT is expected - target should not exist after backup
687
715
  }
688
716
  }
689
717
  async function createDeployTarget(action, home, deps) {
@@ -746,7 +774,7 @@ async function backupExisting(targetPath, verbose, home, expected, deps) {
746
774
  skill: expected.skill,
747
775
  agent: expected.agent,
748
776
  }, deps.registry))) {
749
- throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine refusing to overwrite`);
777
+ throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine - refusing to overwrite`);
750
778
  }
751
779
  const backupPath = `${targetPath}.inception-backup`;
752
780
  if (verbose) {
@@ -8,13 +8,8 @@ const defaultExecFn = async (cmd, args) => {
8
8
  await execFileAsync(cmd, args);
9
9
  };
10
10
  export async function detectInstalledAgents(home) {
11
- const detected = [];
12
- for (const agent of AGENT_REGISTRY) {
13
- if (await isAgentInstalled(agent, home)) {
14
- detected.push(agent.id);
15
- }
16
- }
17
- return detected;
11
+ const results = await Promise.all(AGENT_REGISTRY.map((agent) => isAgentInstalled(agent, home)));
12
+ return AGENT_REGISTRY.filter((_, i) => results[i]).map((a) => a.id);
18
13
  }
19
14
  async function isAgentInstalled(agent, home) {
20
15
  const detectPath = resolveAgentDetectPath(agent, home);
@@ -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
  }
@@ -517,7 +521,8 @@ function logPathAgentEntries(label, entries) {
517
521
  return;
518
522
  logger.detail(`${label}:`);
519
523
  for (const e of entries) {
520
- logger.detail(` ${e.name} → ${e.path} [${e.agents.join(", ")}]`);
524
+ const entryAgents = e.agents ?? [];
525
+ logger.detail(` ${e.name} → ${e.path} [${entryAgents.join(", ")}]`);
521
526
  }
522
527
  }
523
528
  function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agentDefinitions) {
@@ -548,17 +553,7 @@ function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agen
548
553
  export async function runInit(options) {
549
554
  const { directory, dryRun, force, verbose } = options;
550
555
  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
- });
556
+ const agentRulesCapableAgents = agents.filter((id) => shouldInitIncludeAgent(id, "agentRules", "global"));
562
557
  const manifestPath = path.join(directory, "inception.json");
563
558
  if (!dryRun && (await manifestExists(manifestPath)) && !force) {
564
559
  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);