@kuznai/inception-engine 0.16.0 → 0.18.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.
@@ -1,12 +1,13 @@
1
- import type { AgentRuleEntry, McpServerEntry, PermissionsEntry } from "../../schemas/manifest.ts";
1
+ import type { AgentDefinitionEntry, AgentRuleEntry, McpServerEntry, PermissionsEntry } from "../../schemas/manifest.ts";
2
2
  import type { AgentId, ConfigPatchDeployAction, FileWriteDeployAction, FrontmatterEmitDeployAction, PlanWarning, TomlPatchDeployAction } from "../../types.ts";
3
+ import { compileAgentDefinitionReverts } from "./agent-definitions.ts";
3
4
  import { compileMcpServerReverts } from "./mcp.ts";
4
5
  import { compilePermissionsReverts } from "./permissions.ts";
5
6
  import { compileAgentRuleReverts } from "./rules.ts";
6
- export { compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, };
7
+ export { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, };
7
8
  export type AdapterAction = ConfigPatchDeployAction | FileWriteDeployAction | TomlPatchDeployAction | FrontmatterEmitDeployAction;
8
9
  export interface AdapterResult {
9
10
  actions: AdapterAction[];
10
11
  warnings: PlanWarning[];
11
12
  }
12
- export declare function compileAdapterActions(mcpServers: McpServerEntry[], agentRules: AgentRuleEntry[], permissions: PermissionsEntry[], sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string, repo?: string): Promise<AdapterResult>;
13
+ export declare function compileAdapterActions(mcpServers: McpServerEntry[], agentRules: AgentRuleEntry[], permissions: PermissionsEntry[], sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string, repo?: string, agentDefinitions?: AgentDefinitionEntry[]): Promise<AdapterResult>;
@@ -1,8 +1,9 @@
1
+ import { compileAgentDefinitionActions, compileAgentDefinitionReverts, } from "./agent-definitions.js";
1
2
  import { compileMcpServerActions, compileMcpServerReverts } from "./mcp.js";
2
3
  import { compilePermissionsActions, compilePermissionsReverts, } from "./permissions.js";
3
4
  import { compileAgentRuleActions, compileAgentRuleReverts } from "./rules.js";
4
- export { compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, };
5
- export async function compileAdapterActions(mcpServers, agentRules, permissions, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo) {
5
+ export { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, };
6
+ export async function compileAdapterActions(mcpServers, agentRules, permissions, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, agentDefinitions) {
6
7
  const actions = [];
7
8
  const warnings = [];
8
9
  for (const entry of mcpServers) {
@@ -20,5 +21,10 @@ export async function compileAdapterActions(mcpServers, agentRules, permissions,
20
21
  actions.push(...r.actions);
21
22
  warnings.push(...r.warnings);
22
23
  }
24
+ for (const entry of agentDefinitions ?? []) {
25
+ const r = await compileAgentDefinitionActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo);
26
+ actions.push(...r.actions);
27
+ warnings.push(...r.warnings);
28
+ }
23
29
  return { actions, warnings };
24
30
  }
@@ -62,6 +62,13 @@ export function compileMcpServerActions(entry, detectedAgents, home, repo) {
62
62
  });
63
63
  continue;
64
64
  }
65
+ if (support.status === "planned") {
66
+ warnings.push({
67
+ kind: "confidence",
68
+ message: `mcpServers: agent "${agentId}" MCP support is planned via ${support.plannedSurface} — skipping "${entry.name}" until that surface is implemented`,
69
+ });
70
+ continue;
71
+ }
65
72
  validateMcpServerConfigShape(entry.config, entry.name, agentId);
66
73
  const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home, repo);
67
74
  const resolvedTarget = path.resolve(rawTarget);
@@ -79,7 +86,9 @@ export function compileMcpServerReverts(entry, agentFilter, home, repo) {
79
86
  continue;
80
87
  const agent = AGENT_REGISTRY_BY_ID[agentId];
81
88
  const support = agent?.mcpSupport;
82
- if (!support || support.status === "unsupported")
89
+ if (!support ||
90
+ support.status === "unsupported" ||
91
+ support.status === "planned")
83
92
  continue;
84
93
  const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home, repo);
85
94
  const target = path.resolve(rawTarget);
@@ -21,6 +21,13 @@ export function compilePermissionsActions(entry, detectedAgents, home) {
21
21
  });
22
22
  continue;
23
23
  }
24
+ if (support.status === "planned") {
25
+ warnings.push({
26
+ kind: "confidence",
27
+ message: `permissions: agent "${agentId}" permissions support is planned via ${support.plannedSurface} — skipping "${entry.name}" until that surface is implemented`,
28
+ });
29
+ continue;
30
+ }
24
31
  validatePermissionsConfigShape(entry.config, entry.name, agentId);
25
32
  const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home);
26
33
  const resolvedTarget = path.resolve(rawTarget);
@@ -56,7 +63,9 @@ export function compilePermissionsReverts(entry, agentFilter, home) {
56
63
  continue;
57
64
  const agent = AGENT_REGISTRY_BY_ID[agentId];
58
65
  const support = agent?.permissionsSupport;
59
- if (!support || support.status === "unsupported")
66
+ if (!support ||
67
+ support.status === "unsupported" ||
68
+ support.status === "planned")
60
69
  continue;
61
70
  const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home);
62
71
  const target = path.resolve(rawTarget);
@@ -2,6 +2,40 @@ import path from "node:path";
2
2
  import { AGENT_REGISTRY_BY_ID } from "../../config/agents.js";
3
3
  import { getPlatformKey, resolvePlaceholders } from "../resolve.js";
4
4
  import { validateAgentRuleMarkdownPath, validateSourceFile, validateSourcePath, } from "../validation.js";
5
+ function resolveRulesSupport(agentId, scope) {
6
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
7
+ if (scope === "repo") {
8
+ return agent?.agentRulesRepoSupport ?? agent?.agentRulesSupport;
9
+ }
10
+ return agent?.agentRulesSupport;
11
+ }
12
+ function resolveAgentTarget(agentId, entry, home, repo, platform) {
13
+ const support = resolveRulesSupport(agentId, entry.scope);
14
+ if (!support || support.status === "unsupported") {
15
+ return {
16
+ kind: "confidence",
17
+ message: `agentRules: agent "${agentId}" uses ${support?.schemaLabel ?? "an unsupported instruction schema"} and ${support?.status === "unsupported" ? support.reason : "does not expose a supported rules adapter"} — skipping "${entry.name}"`,
18
+ };
19
+ }
20
+ if (support.status === "planned") {
21
+ return {
22
+ kind: "confidence",
23
+ message: `agentRules: agent "${agentId}" rules support is planned via ${support.plannedSurface} — skipping "${entry.name}" until that surface is implemented`,
24
+ };
25
+ }
26
+ if (entry.scope === "repo" && !repo) {
27
+ return {
28
+ kind: "confidence",
29
+ message: `agentRules: scope "repo" requires a repository path but none was resolved — skipping "${entry.name}" for agent "${agentId}"`,
30
+ };
31
+ }
32
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
33
+ return {
34
+ agentId,
35
+ confidence: agent?.provenance.agentRules ?? "provisional",
36
+ target: resolvePlaceholders(support.path[platform], entry.name, home, repo),
37
+ };
38
+ }
5
39
  export async function compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo) {
6
40
  const actions = [];
7
41
  const warnings = [];
@@ -12,26 +46,19 @@ export async function compileAgentRuleActions(entry, sourceDir, resolvedSourceDi
12
46
  return { actions, warnings };
13
47
  }
14
48
  for (const agentId of targetAgents) {
15
- const agent = AGENT_REGISTRY_BY_ID[agentId];
16
- const support = agent?.agentRulesSupport;
17
- if (!support || support.status === "unsupported") {
18
- warnings.push({
19
- kind: "confidence",
20
- message: `agentRules: agent "${agentId}" uses ${support?.schemaLabel ?? "an unsupported instruction schema"} and ${support?.status === "unsupported" ? support.reason : "does not expose a supported rules adapter"} — skipping "${entry.name}"`,
21
- });
22
- continue;
49
+ const result = resolveAgentTarget(agentId, entry, home, repo, platform);
50
+ if ("kind" in result) {
51
+ warnings.push(result);
52
+ }
53
+ else {
54
+ supportedTargets.push(result);
23
55
  }
24
- supportedTargets.push({
25
- agentId,
26
- confidence: agent.provenance.agentRules ?? "provisional",
27
- target: resolvePlaceholders(support.path[platform], entry.name, home, repo),
28
- });
29
56
  }
30
57
  if (supportedTargets.length === 0) {
31
58
  return { actions, warnings };
32
59
  }
33
60
  // Validate the shared source file only when at least one target uses the
34
- // current global-Markdown adapter surface.
61
+ // current rules adapter surface.
35
62
  const source = path.resolve(sourceDir, entry.path);
36
63
  await validateSourcePath(source, entry.path, resolvedSourceDir, realRoot);
37
64
  await validateSourceFile(source, entry.path);
@@ -55,8 +82,14 @@ export function compileAgentRuleReverts(entry, agentFilter, home, repo) {
55
82
  if (agentFilter && !agentFilter.includes(agentId))
56
83
  continue;
57
84
  const agent = AGENT_REGISTRY_BY_ID[agentId];
58
- const support = agent?.agentRulesSupport;
59
- if (!support || support.status === "unsupported")
85
+ const support = entry.scope === "repo"
86
+ ? (agent?.agentRulesRepoSupport ?? agent?.agentRulesSupport)
87
+ : agent?.agentRulesSupport;
88
+ if (!support ||
89
+ support.status === "unsupported" ||
90
+ support.status === "planned")
91
+ continue;
92
+ if (entry.scope === "repo" && !repo)
60
93
  continue;
61
94
  const target = resolvePlaceholders(support.path[platform], entry.name, home, repo);
62
95
  actions.push({
@@ -91,7 +91,7 @@ function detectAmbiguities(detectedAgents, manifest) {
91
91
  if (bothAgents(entry.agents)) {
92
92
  warnings.push({
93
93
  kind: "ambiguity",
94
- message: `Both "gemini-cli" and "antigravity" are listed in agentRules entry "${entry.name}". They share a GEMINI.md-backed instruction shared surface — verify that deploying to both does not create conflicting behavior.`,
94
+ message: `Both "gemini-cli" and "antigravity" are listed in agentRules entry "${entry.name}". They write to distinct surfaces ("gemini-cli" → ~/.gemini/GEMINI.md, "antigravity" {repo}/.agents/rules/${entry.name}.md) from the same source file — verify that deploying to both produces the intended behavior on each agent.`,
95
95
  });
96
96
  }
97
97
  }
@@ -119,6 +119,8 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
119
119
  const agent = AGENT_REGISTRY_BY_ID[agentId];
120
120
  if (!agent)
121
121
  continue;
122
+ if (!agent.skills)
123
+ continue;
122
124
  actions.push({
123
125
  kind: "skill-dir",
124
126
  skill: skill.name,
@@ -126,7 +128,7 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
126
128
  source,
127
129
  target: resolveAgentSkillPath(agent, skill.name, home),
128
130
  method,
129
- confidence: agent.provenance.skills,
131
+ confidence: agent.provenance.skills ?? "provisional",
130
132
  });
131
133
  }
132
134
  }
@@ -192,7 +194,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
192
194
  ...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
193
195
  ...planConfigPatchActions(manifest, detectedAgents, home),
194
196
  ];
195
- const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
197
+ const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, undefined, manifest.agentDefinitions ?? []);
196
198
  actions.push(...adapterResult.actions);
197
199
  const warnings = [
198
200
  ...detectAmbiguities(detectedAgents, manifest),
package/dist/core/init.js CHANGED
@@ -1,8 +1,8 @@
1
- import { access, readFile, readdir, writeFile } from "node:fs/promises";
1
+ import { access, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
4
  import { dryRunPrefix, logger } from "../logger.js";
5
- import { AGENT_IDS, ConfigEntrySchema, FileEntrySchema, McpServerEntrySchema, } from "../schemas/manifest.js";
5
+ import { AGENT_IDS, AgentDefinitionEntrySchema, ConfigEntrySchema, FileEntrySchema, McpServerEntrySchema, } from "../schemas/manifest.js";
6
6
  const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
7
7
  // Ordered list: first match wins. Catch-all is applied at call site.
8
8
  const AGENT_RULES_FILE_PATTERNS = [
@@ -27,6 +27,16 @@ const AGENT_RULES_SUBDIRS = [
27
27
  ".github",
28
28
  ".agents/rules",
29
29
  ];
30
+ // 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
+ ];
30
40
  async function findSkillDirs(baseDir, dir, found) {
31
41
  let entries;
32
42
  try {
@@ -160,10 +170,121 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
160
170
  }
161
171
  }
162
172
  namesSeen.add(name);
163
- rules.push({ name, path: relPath, agents });
173
+ rules.push({ name, path: relPath, agents, scope: "global" });
164
174
  }
165
175
  return rules;
166
176
  }
177
+ /**
178
+ * Derives the name for an agent definition entry from its file name.
179
+ * For GitHub Copilot's `{name}.agent.md` naming convention, strips the
180
+ * `.agent` infix in addition to the `.md` extension.
181
+ */
182
+ function deriveAgentDefinitionName(relPath, fileName) {
183
+ const ext = path.extname(fileName).toLowerCase();
184
+ // Strip the extension to get the base name, then strip any trailing ".agent"
185
+ // suffix (GitHub Copilot convention: foo.agent.md → foo).
186
+ let baseName = path.basename(fileName, ext);
187
+ if (baseName.endsWith(".agent")) {
188
+ baseName = baseName.slice(0, -".agent".length);
189
+ }
190
+ const rawName = baseName.toLowerCase().replace(/[^a-zA-Z0-9._-]/g, "-");
191
+ if (!SAFE_NAME_RE.test(rawName)) {
192
+ logger.warn("init", `Skipping "${relPath}": could not derive a valid agentDefinitions name`);
193
+ return null;
194
+ }
195
+ return rawName;
196
+ }
197
+ /**
198
+ * 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).
201
+ */
202
+ 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;
216
+ }
217
+ }
218
+ async function scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, agentRulesRelPaths, candidates) {
219
+ const suggestedAgents = agentsForDefinitionSubdir(subdir) ?? [];
220
+ const dir = path.join(baseDir, subdir);
221
+ let entries;
222
+ try {
223
+ entries = await readdir(dir, { withFileTypes: true, encoding: "utf-8" });
224
+ }
225
+ catch {
226
+ return;
227
+ }
228
+ for (const entry of entries) {
229
+ if (!entry.isFile())
230
+ continue;
231
+ const ext = path.extname(entry.name).toLowerCase();
232
+ if (ext !== ".md" && ext !== ".markdown")
233
+ continue;
234
+ const absPath = path.join(dir, entry.name);
235
+ const relPath = path.relative(baseDir, absPath).split(path.sep).join("/");
236
+ if (seen.has(relPath) ||
237
+ isInsideSkillDir(relPath, skillDirRelPaths) ||
238
+ agentRulesRelPaths.has(relPath))
239
+ continue;
240
+ const name = deriveAgentDefinitionName(relPath, entry.name);
241
+ if (name === null)
242
+ continue;
243
+ seen.add(relPath);
244
+ candidates.push({ relPath, name, suggestedAgents });
245
+ }
246
+ }
247
+ async function findAgentDefinitionCandidates(baseDir, skillDirRelPaths, agentRulesRelPaths) {
248
+ const candidates = [];
249
+ const seen = new Set();
250
+ for (const subdir of AGENT_DEFINITION_SUBDIRS) {
251
+ await scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, agentRulesRelPaths, candidates);
252
+ }
253
+ return candidates;
254
+ }
255
+ function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
256
+ const definitions = [];
257
+ const localNamesSeen = new Set(namesSeen);
258
+ const definitionsCapableAgents = activeAgents.filter((id) => AGENT_REGISTRY_BY_ID[id].agentDefinitionsSupport?.status !==
259
+ "unsupported");
260
+ for (const { relPath, name: rawName, suggestedAgents } of candidates) {
261
+ // Use suggested agents (from the dir that was scanned), intersected with
262
+ // active agents that support agentDefinitions. Fall back to all capable
263
+ // agents if the intersection is empty.
264
+ const intersection = suggestedAgents.length > 0
265
+ ? suggestedAgents.filter((a) => definitionsCapableAgents.includes(a))
266
+ : [];
267
+ const agents = intersection.length > 0 ? intersection : definitionsCapableAgents;
268
+ if (agents.length === 0)
269
+ continue;
270
+ // Resolve name collision
271
+ let name = rawName;
272
+ if (localNamesSeen.has(name)) {
273
+ const candidate = `${name}-agent`;
274
+ if (SAFE_NAME_RE.test(candidate)) {
275
+ logger.warn("init", `agentDefinitions name "${name}" collides with an existing name; using "${candidate}"`);
276
+ name = candidate;
277
+ }
278
+ else {
279
+ logger.warn("init", `Skipping "${relPath}": name "${name}" collides and fallback is invalid`);
280
+ continue;
281
+ }
282
+ }
283
+ localNamesSeen.add(name);
284
+ definitions.push({ name, path: relPath, agents });
285
+ }
286
+ return definitions;
287
+ }
167
288
  async function loadMcpServers(baseDir) {
168
289
  const filePath = path.join(baseDir, "mcp-servers.json");
169
290
  let raw;
@@ -263,6 +384,39 @@ async function loadConfigsManifest(baseDir) {
263
384
  }
264
385
  return results;
265
386
  }
387
+ async function loadAgentDefinitionsManifest(baseDir) {
388
+ const filePath = path.join(baseDir, "agent-definitions-manifest.json");
389
+ let raw;
390
+ try {
391
+ raw = await readFile(filePath, "utf-8");
392
+ }
393
+ catch {
394
+ return [];
395
+ }
396
+ let parsed;
397
+ try {
398
+ parsed = JSON.parse(raw);
399
+ }
400
+ catch {
401
+ logger.warn("init", "agent-definitions-manifest.json: invalid JSON, skipping");
402
+ return [];
403
+ }
404
+ if (!Array.isArray(parsed)) {
405
+ logger.warn("init", "agent-definitions-manifest.json: expected a JSON array, skipping");
406
+ return [];
407
+ }
408
+ const results = [];
409
+ for (let i = 0; i < parsed.length; i++) {
410
+ const result = AgentDefinitionEntrySchema.safeParse(parsed[i]);
411
+ if (result.success) {
412
+ results.push(result.data);
413
+ }
414
+ else {
415
+ logger.warn("init", `agent-definitions-manifest.json: entry[${i}] invalid, skipping`);
416
+ }
417
+ }
418
+ return results;
419
+ }
266
420
  async function emitDirectoryHints(baseDir, filesLoaded, configsLoaded) {
267
421
  for (const [dir, section, loaded, sidecar] of [
268
422
  ["files", "files", filesLoaded, "files-manifest.json"],
@@ -288,16 +442,20 @@ async function manifestExists(manifestPath) {
288
442
  return false;
289
443
  }
290
444
  }
291
- function logVerboseManifest(skills, agentRules, mcpServers, files, configs) {
445
+ function logPathAgentEntries(label, entries) {
446
+ if (entries.length === 0)
447
+ return;
448
+ logger.detail(`${label}:`);
449
+ for (const e of entries) {
450
+ logger.detail(` ${e.name} → ${e.path} [${e.agents.join(", ")}]`);
451
+ }
452
+ }
453
+ function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agentDefinitions) {
292
454
  for (const s of skills) {
293
455
  logger.detail(`${s.name} → ${s.path}`);
294
456
  }
295
- if (agentRules.length > 0) {
296
- logger.detail("agentRules:");
297
- for (const r of agentRules) {
298
- logger.detail(` ${r.name} → ${r.path} [${r.agents.join(", ")}]`);
299
- }
300
- }
457
+ logPathAgentEntries("agentRules", agentRules);
458
+ logPathAgentEntries("agentDefinitions", agentDefinitions);
301
459
  if (mcpServers.length > 0) {
302
460
  logger.detail("mcpServers:");
303
461
  for (const m of mcpServers) {
@@ -336,6 +494,16 @@ export async function runInit(options) {
336
494
  const skillDirRelPaths = new Set(found.map((f) => f.relPath));
337
495
  const agentRulesCandidates = await findAgentRulesCandidates(directory, skillDirRelPaths);
338
496
  const agentRules = buildAgentRules(agentRulesCandidates, agentRulesCapableAgents, skillNamesSeen);
497
+ const allNamesSeen = new Set([
498
+ ...skillNamesSeen,
499
+ ...agentRules.map((r) => r.name),
500
+ ]);
501
+ const agentRulesRelPaths = new Set(agentRules.map((r) => r.path));
502
+ const agentDefinitionCandidates = await findAgentDefinitionCandidates(directory, skillDirRelPaths, agentRulesRelPaths);
503
+ const discoveredDefinitions = buildAgentDefinitions(agentDefinitionCandidates, agents, allNamesSeen);
504
+ // Sidecar file overrides take precedence over auto-discovery
505
+ const sidecarDefinitions = await loadAgentDefinitionsManifest(directory);
506
+ const agentDefinitions = sidecarDefinitions.length > 0 ? sidecarDefinitions : discoveredDefinitions;
339
507
  const mcpServers = await loadMcpServers(directory);
340
508
  const files = await loadFilesManifest(directory);
341
509
  const configs = await loadConfigsManifest(directory);
@@ -345,12 +513,14 @@ export async function runInit(options) {
345
513
  configs,
346
514
  mcpServers,
347
515
  agentRules,
516
+ agentDefinitions,
348
517
  };
349
518
  const json = `${JSON.stringify(manifest, null, 2)}\n`;
350
519
  function summarize() {
351
520
  const parts = [
352
521
  `${skills.length} skill(s)`,
353
522
  `${agentRules.length} agentRule(s)`,
523
+ `${agentDefinitions.length} agentDefinition(s)`,
354
524
  `${mcpServers.length} mcpServer(s)`,
355
525
  `${files.length} file(s)`,
356
526
  `${configs.length} config(s)`,
@@ -367,7 +537,7 @@ export async function runInit(options) {
367
537
  await writeFile(manifestPath, json, "utf-8");
368
538
  logger.info(`Generated ${manifestPath} with ${summarize()}.`);
369
539
  if (verbose) {
370
- logVerboseManifest(skills, agentRules, mcpServers, files, configs);
540
+ logVerboseManifest(skills, agentRules, mcpServers, files, configs, agentDefinitions);
371
541
  }
372
542
  await emitDirectoryHints(directory, files.length, configs.length);
373
543
  return 0;
@@ -1,6 +1,6 @@
1
1
  import type { AgentId, CliOptions, Manifest } from "../types.ts";
2
2
  export interface PreflightWarning {
3
- kind: "policy" | "config-authority" | "info";
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[]>;
@@ -1,5 +1,73 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
2
- export async function runPreflight(_options, _manifest, _home, detectedAgents) {
4
+ const BUDGET_WARN_BYTES = 50 * 1024; // 50 KB
5
+ function detectInstructionPrecedence(detectedAgents, manifest) {
6
+ const warnings = [];
7
+ for (const agentId of detectedAgents) {
8
+ const rulesForAgent = (manifest.agentRules ?? []).filter((e) => e.agents.includes(agentId));
9
+ const globalEntries = rulesForAgent.filter((e) => (e.scope ?? "global") === "global");
10
+ const repoEntries = rulesForAgent.filter((e) => e.scope === "repo");
11
+ if (globalEntries.length === 0 || repoEntries.length === 0)
12
+ continue;
13
+ const globalPaths = new Set(globalEntries.map((e) => e.path));
14
+ for (const entry of repoEntries) {
15
+ if (globalPaths.has(entry.path)) {
16
+ warnings.push({
17
+ kind: "precedence",
18
+ message: `Agent "${agentId}" has agentRules entry "${entry.name}" deployed to both global and repo scope from the same source path "${entry.path}". The file will be written to two distinct targets — verify this is intentional and not a copy-paste mistake.`,
19
+ });
20
+ }
21
+ }
22
+ const nonOverlapRepo = repoEntries.filter((e) => !globalPaths.has(e.path));
23
+ if (nonOverlapRepo.length > 0) {
24
+ const globalNames = globalEntries.map((e) => `"${e.name}"`).join(", ");
25
+ const repoNames = nonOverlapRepo.map((e) => `"${e.name}"`).join(", ");
26
+ warnings.push({
27
+ kind: "precedence",
28
+ message: `Agent "${agentId}" will have both global and repo instruction files active simultaneously: global [${globalNames}] and repo [${repoNames}]. Both will be loaded by the agent — ensure the content is intended to stack and does not conflict.`,
29
+ });
30
+ }
31
+ }
32
+ return warnings;
33
+ }
34
+ async function detectInstructionBudgetRisk(detectedAgents, manifest, sourceDir) {
35
+ const warnings = [];
36
+ const checkedPaths = new Set();
37
+ async function checkEntry(entryPath, label, agentIds) {
38
+ if (!agentIds.some((id) => detectedAgents.includes(id)))
39
+ return;
40
+ const sourcePath = path.resolve(sourceDir, entryPath);
41
+ if (checkedPaths.has(sourcePath))
42
+ return;
43
+ checkedPaths.add(sourcePath);
44
+ try {
45
+ const fileStat = await stat(sourcePath);
46
+ if (fileStat.size > BUDGET_WARN_BYTES) {
47
+ const sizeKb = (fileStat.size / 1024).toFixed(1);
48
+ warnings.push({
49
+ kind: "budget",
50
+ 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.`,
51
+ });
52
+ }
53
+ }
54
+ catch (err) {
55
+ const code = err.code;
56
+ if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM")
57
+ throw err;
58
+ // Missing/unreadable files are silently skipped; compileAgentRuleActions
59
+ // will produce the proper UserError during action compilation.
60
+ }
61
+ }
62
+ for (const entry of manifest.agentRules ?? []) {
63
+ await checkEntry(entry.path, "agentRules", entry.agents);
64
+ }
65
+ for (const entry of manifest.agentDefinitions ?? []) {
66
+ await checkEntry(entry.path, "agentDefinitions", entry.agents);
67
+ }
68
+ return warnings;
69
+ }
70
+ export async function runPreflight(options, manifest, _home, detectedAgents) {
3
71
  const warnings = [];
4
72
  for (const agentId of detectedAgents) {
5
73
  const agent = AGENT_REGISTRY_BY_ID[agentId];
@@ -24,5 +92,7 @@ export async function runPreflight(_options, _manifest, _home, detectedAgents) {
24
92
  });
25
93
  }
26
94
  }
95
+ warnings.push(...detectInstructionPrecedence(detectedAgents, manifest));
96
+ warnings.push(...(await detectInstructionBudgetRisk(detectedAgents, manifest, options.directory)));
27
97
  return warnings;
28
98
  }
@@ -85,6 +85,10 @@ export function getDeployMethod() {
85
85
  return process.platform === "win32" ? "copy" : "symlink";
86
86
  }
87
87
  export function resolveAgentSkillPathFor(agent, skillName, home, platform) {
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).`);
91
+ }
88
92
  return resolvePlaceholders(agent.skills[platform], skillName, home);
89
93
  }
90
94
  export function resolveAgentDetectPathFor(agent, home, platform) {
@@ -1,7 +1,7 @@
1
1
  import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
2
2
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
3
3
  import { logger } from "../logger.js";
4
- import { compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
4
+ import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
5
5
  import { revertTomlMcpPatch } from "./adapters/toml.js";
6
6
  import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
7
7
  import { resolveAgentSkillPath } from "./resolve.js";
@@ -15,6 +15,8 @@ function buildSkillDirReverts(manifest, home, agentFilter) {
15
15
  const agent = AGENT_REGISTRY_BY_ID[agentId];
16
16
  if (!agent)
17
17
  continue;
18
+ if (!agent.skills)
19
+ continue;
18
20
  actions.push({
19
21
  kind: "skill-dir",
20
22
  skill: skill.name,
@@ -71,6 +73,7 @@ export function planRevert(manifest, detectedAgents, home, repo) {
71
73
  ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home, repo)),
72
74
  ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home, repo)),
73
75
  ...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, detectedAgents, home)),
76
+ ...(manifest.agentDefinitions ?? []).flatMap((e) => compileAgentDefinitionReverts(e, detectedAgents, home, repo)),
74
77
  ];
75
78
  }
76
79
  export function planRevertAll(manifest, home, repo) {
@@ -81,6 +84,7 @@ export function planRevertAll(manifest, home, repo) {
81
84
  ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home, repo)),
82
85
  ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home, repo)),
83
86
  ...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, null, home)),
87
+ ...(manifest.agentDefinitions ?? []).flatMap((e) => compileAgentDefinitionReverts(e, null, home, repo)),
84
88
  ];
85
89
  }
86
90
  function recordOutcome(result, action, counts, failed) {