@kuznai/inception-engine 0.17.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.
package/README.md CHANGED
@@ -27,6 +27,8 @@ inception-engine reads a manifest file (`inception.json`) from the target direct
27
27
 
28
28
  Managed skills overwrite their previous version. If a target exists but was not created by inception-engine, deployment refuses to replace it. On POSIX systems, symlinks mean updates to the source repo are reflected immediately.
29
29
 
30
+ Before executing, the deploy command runs preflight analysis on instruction files: it warns when the same agent will have both global and repo `agentRules` active simultaneously, when the same source file is deployed to both scopes (duplicate-content risk), and when `agentRules` or `agentDefinitions` source files exceed 50 KB (context-budget risk). Warnings are printed but do not block deployment.
31
+
30
32
  ## Agent Compatibility Matrix
31
33
 
32
34
  | Agent | ID | Skills | macOS | Linux | Windows |
@@ -49,10 +51,11 @@ Managed skills overwrite their previous version. If a target exists but was not
49
51
  | File write | All agents via manifest and CLI | All agents |
50
52
  | Config patch (JSON merge) | All agents via manifest and CLI | All agents |
51
53
  | MCP Servers | claude-code, gemini-cli, codex, antigravity, opencode; github-copilot repo-scoped surfaces are warned and skipped | claude-code, gemini-cli, codex, antigravity, opencode |
52
- | Global/Repo Rules Files | All agents (antigravity uses repo-local `.agents/rules/`); github-copilot reads CLAUDE.md natively (deploy via claude-code) | All agents |
54
+ | Global/Repo Rules Files | All agents via `scope: "global"` (home-dir) or `scope: "repo"` (project-root); antigravity always uses repo-local `.agents/rules/`; github-copilot reads CLAUDE.md natively (deploy via claude-code) | All agents |
53
55
  | Permissions / Approval Config | claude-code (`~/.claude/settings.json`), codex (`~/.codex/config.toml`); other agents are warned and skipped | claude-code, codex |
54
56
  | Agent Definitions | claude-code (`{repo}/.claude/agents/{name}.md`), gemini-cli (`{repo}/.gemini/agents/{name}.md`), antigravity (`{repo}/.agents/rules/{name}.md`), opencode (`{repo}/.opencode/agents/{name}.md`), github-copilot (`{repo}/.github/agents/{name}.agent.md`); codex is warned and skipped | All supported agents |
55
57
  | `init` manifest generation | Scans `SKILL.md` directories (`skills`), `.md` files with Claude-first agent mapping (`agentRules`), `mcp-servers.json` (`mcpServers`), and agent-definition Markdown files (`agentDefinitions`); emits hints for `files/` and `configs/` directories | N/A |
58
+ | Instruction preflight analysis | Emits `precedence` warnings when an agent has both global and repo `agentRules` active simultaneously (stacking advisory) or the same source file deployed to both scopes (duplicate-content warning); emits `budget` warnings when `agentRules` or `agentDefinitions` source files exceed 50 KB | N/A |
56
59
 
57
60
  Features that depend on agent-specific config surfaces are intentionally conservative: if a target path or schema is not implemented with enough confidence, inception-engine warns and skips it rather than guessing.
58
61
 
@@ -98,7 +101,14 @@ Create an `inception.json` file at the root of your skills directory:
98
101
  {
99
102
  "name": "my-rules",
100
103
  "path": "rules/CLAUDE.md",
101
- "agents": ["claude-code"]
104
+ "agents": ["claude-code"],
105
+ "scope": "global"
106
+ },
107
+ {
108
+ "name": "project-rules",
109
+ "path": "rules/CLAUDE.md",
110
+ "agents": ["claude-code", "codex", "gemini-cli", "opencode"],
111
+ "scope": "repo"
102
112
  }
103
113
  ],
104
114
  "permissions": [
@@ -158,13 +168,27 @@ MCP server registration is supported for all agents except GitHub Copilot. Incep
158
168
 
159
169
  Revert removes the registered server entry from the respective configuration file. GitHub Copilot, which uses repo-scoped MCP surfaces not yet implemented by inception-engine, continues to emit a schema-aware warning and is skipped.
160
170
 
161
- Each **agentRules** entry deploys a Markdown instruction file to an agent's supported global rules file location:
171
+ Each **agentRules** entry deploys a Markdown instruction file to an agent's supported instruction file location:
162
172
 
163
173
  - **name** - Unique identifier (same format as skill names)
164
- - **path** - Relative path to the source Markdown file within the repo; supported global rules adapters require a `.md` or `.markdown` source path
174
+ - **path** - Relative path to the source Markdown file within the repo; supported rules adapters require a `.md` or `.markdown` source path
165
175
  - **agents** - Array of agent IDs to deploy this file to
176
+ - **scope** - `"global"` (default) or `"repo"`. Controls which instruction surface is targeted:
177
+ - `"global"` — deploys to the agent's home-directory instruction file (e.g., `~/.claude/CLAUDE.md` for `claude-code`)
178
+ - `"repo"` — deploys to the project-root instruction file inside the deployed repository (e.g., `{repo}/CLAUDE.md` for `claude-code`)
179
+
180
+ Instruction rule deployment is supported for all agents. The target path depends on the agent and the `scope`:
166
181
 
167
- Instruction rule deployment is supported for all agents. For most agents, this targets a single global rules file (e.g., `~/.claude/CLAUDE.md` for `claude-code`, `~/.codex/AGENTS.md` for `codex`, `~/.gemini/GEMINI.md` for `gemini-cli`, and `~/.config/opencode/AGENTS.md` for `opencode`). For `antigravity`, inception-engine deploys to repo-local instruction surfaces at `{repo}/.agents/rules/{name}.md`. For `github-copilot`, no separate deployment is needed because Copilot reads `CLAUDE.md` natively — target it via the `claude-code` agentRules entry and it reaches Copilot automatically. Revert removes the deployed rules file.
182
+ | Agent | `scope: "global"` | `scope: "repo"` |
183
+ |---|---|---|
184
+ | `claude-code` | `~/.claude/CLAUDE.md` | `{repo}/CLAUDE.md` |
185
+ | `codex` | `~/.codex/AGENTS.md` | `{repo}/AGENTS.md` |
186
+ | `gemini-cli` | `~/.gemini/GEMINI.md` | `{repo}/GEMINI.md` |
187
+ | `antigravity` | `{repo}/.agents/rules/{name}.md` | `{repo}/.agents/rules/{name}.md` |
188
+ | `opencode` | `~/.config/opencode/AGENTS.md` | `{repo}/AGENTS.md` |
189
+ | `github-copilot` | unsupported — reads `CLAUDE.md` natively | unsupported — deploy via `claude-code` |
190
+
191
+ For `antigravity`, both scopes target the same repo-local surface (`{repo}/.agents/rules/{name}.md`) since Antigravity has no global home-directory instruction file. For `github-copilot`, no separate deployment is needed for either scope — target it via the `claude-code` agentRules entry and it reaches Copilot automatically. Revert removes the deployed rules file.
168
192
 
169
193
  Each **permissions** entry deploys execution and safety-oriented configuration to an agent's permission or approval surface:
170
194
 
@@ -36,6 +36,14 @@ export const AGENT_REGISTRY = [
36
36
  windows: ["{home}", ".claude", "CLAUDE.md"],
37
37
  },
38
38
  },
39
+ agentRulesRepoSupport: {
40
+ status: "supported",
41
+ schemaLabel: "repo-local CLAUDE.md",
42
+ path: {
43
+ posix: ["{repo}", "CLAUDE.md"],
44
+ windows: ["{repo}", "CLAUDE.md"],
45
+ },
46
+ },
39
47
  permissionsSupport: {
40
48
  status: "supported",
41
49
  schemaLabel: "JSON permissions config",
@@ -88,6 +96,14 @@ export const AGENT_REGISTRY = [
88
96
  windows: ["{home}", ".codex", "AGENTS.md"],
89
97
  },
90
98
  },
99
+ agentRulesRepoSupport: {
100
+ status: "supported",
101
+ schemaLabel: "repo-local AGENTS.md",
102
+ path: {
103
+ posix: ["{repo}", "AGENTS.md"],
104
+ windows: ["{repo}", "AGENTS.md"],
105
+ },
106
+ },
91
107
  permissionsSupport: {
92
108
  status: "supported",
93
109
  schemaLabel: "TOML approval policy config",
@@ -138,6 +154,14 @@ export const AGENT_REGISTRY = [
138
154
  windows: ["{home}", ".gemini", "GEMINI.md"],
139
155
  },
140
156
  },
157
+ agentRulesRepoSupport: {
158
+ status: "supported",
159
+ schemaLabel: "repo-local GEMINI.md",
160
+ path: {
161
+ posix: ["{repo}", "GEMINI.md"],
162
+ windows: ["{repo}", "GEMINI.md"],
163
+ },
164
+ },
141
165
  permissionsSupport: {
142
166
  status: "unsupported",
143
167
  schemaLabel: "global permissions surface",
@@ -187,6 +211,14 @@ export const AGENT_REGISTRY = [
187
211
  windows: ["{repo}", ".agents", "rules", "{name}.md"],
188
212
  },
189
213
  },
214
+ agentRulesRepoSupport: {
215
+ status: "supported",
216
+ schemaLabel: "repo-local Markdown rules file",
217
+ path: {
218
+ posix: ["{repo}", ".agents", "rules", "{name}.md"],
219
+ windows: ["{repo}", ".agents", "rules", "{name}.md"],
220
+ },
221
+ },
190
222
  permissionsSupport: {
191
223
  status: "unsupported",
192
224
  schemaLabel: "global permissions surface",
@@ -237,6 +269,14 @@ export const AGENT_REGISTRY = [
237
269
  windows: ["{appdata}", "opencode", "AGENTS.md"],
238
270
  },
239
271
  },
272
+ agentRulesRepoSupport: {
273
+ status: "supported",
274
+ schemaLabel: "repo-local AGENTS.md",
275
+ path: {
276
+ posix: ["{repo}", "AGENTS.md"],
277
+ windows: ["{repo}", "AGENTS.md"],
278
+ },
279
+ },
240
280
  permissionsSupport: {
241
281
  status: "unsupported",
242
282
  schemaLabel: "global permissions surface",
@@ -279,6 +319,11 @@ export const AGENT_REGISTRY = [
279
319
  schemaLabel: "Claude-native shared instructions",
280
320
  reason: 'GitHub Copilot reads CLAUDE.md natively, so deploy via the "claude-code" agentRules target instead of a separate rules surface',
281
321
  },
322
+ agentRulesRepoSupport: {
323
+ status: "unsupported",
324
+ schemaLabel: "repo-local CLAUDE.md",
325
+ reason: 'GitHub Copilot reads CLAUDE.md natively, so deploy via the "claude-code" agentRules target with scope: "repo" instead of a separate rules surface',
326
+ },
282
327
  permissionsSupport: {
283
328
  status: "unsupported",
284
329
  schemaLabel: "global permissions surface",
@@ -1,16 +1,28 @@
1
1
  /**
2
- * Serializes a flat (one-level) object to a YAML block suitable for use inside
3
- * `---` frontmatter delimiters.
2
+ * Splits a Markdown string into YAML frontmatter and the remaining body.
3
+ * Expects the file to start with --- delimiter.
4
+ */
5
+ export declare function splitFrontmatter(raw: string): {
6
+ frontmatter: string;
7
+ body: string;
8
+ };
9
+ /**
10
+ * Parses a Markdown string with YAML frontmatter.
11
+ */
12
+ export declare function parseFrontmatterDocument<T = Record<string, unknown>>(raw: string): {
13
+ attributes: T;
14
+ body: string;
15
+ };
16
+ /**
17
+ * Serializes an object to a YAML block.
4
18
  */
5
19
  export declare function serializeFrontmatter(data: Record<string, unknown>): string;
6
20
  /**
7
21
  * Builds the full content of a frontmatter-bearing Markdown file.
8
- * If `body` is provided it is appended after the closing delimiter.
9
22
  */
10
23
  export declare function buildFrontmatterDocument(frontmatter: Record<string, unknown>, body?: string): string;
11
24
  /**
12
- * Reads an existing `.md` file and parses its frontmatter (using the
13
- * `front-matter` package). Returns the parsed attributes and raw body.
25
+ * Reads an existing `.md` file and parses its frontmatter.
14
26
  * Returns `{ attributes: {}, body: "" }` if the file does not exist.
15
27
  */
16
28
  export declare function readFrontmatterFile(filePath: string): Promise<{
@@ -1,71 +1,52 @@
1
1
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
- import { createRequire } from "node:module";
3
2
  import path from "node:path";
4
- const _require = createRequire(import.meta.url);
5
- // front-matter is a CommonJS module whose export is the callable parse function.
6
- const parseFrontmatter = _require("front-matter");
7
- // ---------------------------------------------------------------------------
8
- // Hand-rolled flat-YAML serializer
9
- // Handles the shapes found in MCP server descriptors: strings, numbers,
10
- // booleans, string arrays, and one-level nested objects (e.g. "env").
11
- // ---------------------------------------------------------------------------
12
- function serializeScalar(value) {
13
- if (typeof value === "string") {
14
- const needsQuotes = /[:#[\]{},|>&*!'"@`]|^\s|\s$/.test(value);
15
- return needsQuotes
16
- ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
17
- : value;
3
+ import YAML from "yaml";
4
+ /**
5
+ * Splits a Markdown string into YAML frontmatter and the remaining body.
6
+ * Expects the file to start with --- delimiter.
7
+ */
8
+ export function splitFrontmatter(raw) {
9
+ const lines = raw.split(/\r?\n/);
10
+ if (lines[0]?.trim() !== "---") {
11
+ return { frontmatter: "", body: raw };
18
12
  }
19
- return String(value);
20
- }
21
- function serializeArray(key, arr, pad) {
22
- if (arr.length === 0)
23
- return `${pad}${key}: []`;
24
- const items = arr.map((item) => {
25
- const scalar = typeof item === "string" ||
26
- typeof item === "number" ||
27
- typeof item === "boolean"
28
- ? serializeScalar(item)
29
- : JSON.stringify(item);
30
- return `${pad} - ${scalar}`;
31
- });
32
- return [`${pad}${key}:`, ...items].join("\n");
33
- }
34
- function serializeObject(key, obj, pad) {
35
- const inner = serializeFrontmatterAtDepth(obj, `${pad} `);
36
- return inner ? `${pad}${key}:\n${inner}` : `${pad}${key}: {}`;
13
+ const closingIndex = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
14
+ if (closingIndex === -1) {
15
+ return { frontmatter: "", body: raw };
16
+ }
17
+ // Preserve the actual lines for the body to avoid normalizing line endings if not needed
18
+ // but for frontmatter block, we join with \n for the YAML parser
19
+ const frontmatter = lines.slice(1, closingIndex).join("\n");
20
+ const body = lines.slice(closingIndex + 1).join("\n");
21
+ return { frontmatter, body };
37
22
  }
38
- function serializeFrontmatterAtDepth(data, pad) {
39
- const lines = [];
40
- for (const [key, value] of Object.entries(data)) {
41
- if (value === null || value === undefined)
42
- continue;
43
- if (Array.isArray(value)) {
44
- lines.push(serializeArray(key, value, pad));
45
- }
46
- else if (typeof value === "object") {
47
- lines.push(serializeObject(key, value, pad));
48
- }
49
- else {
50
- lines.push(`${pad}${key}: ${serializeScalar(value)}`);
51
- }
23
+ /**
24
+ * Parses a Markdown string with YAML frontmatter.
25
+ */
26
+ export function parseFrontmatterDocument(raw) {
27
+ const { frontmatter, body } = splitFrontmatter(raw);
28
+ if (!(frontmatter || raw.startsWith("---"))) {
29
+ return { attributes: {}, body };
52
30
  }
53
- return lines.join("\n");
31
+ const attributes = YAML.parse(frontmatter);
32
+ return { attributes: (attributes || {}), body };
54
33
  }
55
34
  /**
56
- * Serializes a flat (one-level) object to a YAML block suitable for use inside
57
- * `---` frontmatter delimiters.
35
+ * Serializes an object to a YAML block.
58
36
  */
59
37
  export function serializeFrontmatter(data) {
60
- return serializeFrontmatterAtDepth(data, "");
38
+ // Using a consistent indentation that matches standard YAML
39
+ return YAML.stringify(data, { indent: 2 }).trim();
61
40
  }
62
41
  /**
63
42
  * Builds the full content of a frontmatter-bearing Markdown file.
64
- * If `body` is provided it is appended after the closing delimiter.
65
43
  */
66
44
  export function buildFrontmatterDocument(frontmatter, body = "") {
67
45
  const serialized = serializeFrontmatter(frontmatter);
68
- return `---\n${serialized}\n---\n${body}`;
46
+ // Ensure exactly one newline after the closing delimiter before the body
47
+ const cleanBody = body.trimStart();
48
+ const separator = cleanBody ? "\n\n" : "\n";
49
+ return `---\n${serialized}\n---\n${separator}${cleanBody}`;
69
50
  }
70
51
  function createAtomicTempPath(targetPath) {
71
52
  return `${targetPath}.inception-tmp-${process.pid}-${Date.now()}-${Math.random()
@@ -73,8 +54,7 @@ function createAtomicTempPath(targetPath) {
73
54
  .slice(2)}`;
74
55
  }
75
56
  /**
76
- * Reads an existing `.md` file and parses its frontmatter (using the
77
- * `front-matter` package). Returns the parsed attributes and raw body.
57
+ * Reads an existing `.md` file and parses its frontmatter.
78
58
  * Returns `{ attributes: {}, body: "" }` if the file does not exist.
79
59
  */
80
60
  export async function readFrontmatterFile(filePath) {
@@ -88,8 +68,7 @@ export async function readFrontmatterFile(filePath) {
88
68
  return { attributes: {}, body: "" };
89
69
  throw err;
90
70
  }
91
- const parsed = parseFrontmatter(raw);
92
- return { attributes: parsed.attributes, body: parsed.body };
71
+ return parseFrontmatterDocument(raw);
93
72
  }
94
73
  /**
95
74
  * Atomically writes a Markdown file with the given frontmatter and optional
@@ -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,33 +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);
23
52
  }
24
- if (support.status === "planned") {
25
- warnings.push({
26
- kind: "confidence",
27
- message: `agentRules: agent "${agentId}" rules support is planned via ${support.plannedSurface} — skipping "${entry.name}" until that surface is implemented`,
28
- });
29
- continue;
53
+ else {
54
+ supportedTargets.push(result);
30
55
  }
31
- supportedTargets.push({
32
- agentId,
33
- confidence: agent.provenance.agentRules ?? "provisional",
34
- target: resolvePlaceholders(support.path[platform], entry.name, home, repo),
35
- });
36
56
  }
37
57
  if (supportedTargets.length === 0) {
38
58
  return { actions, warnings };
39
59
  }
40
60
  // Validate the shared source file only when at least one target uses the
41
- // current global-Markdown adapter surface.
61
+ // current rules adapter surface.
42
62
  const source = path.resolve(sourceDir, entry.path);
43
63
  await validateSourcePath(source, entry.path, resolvedSourceDir, realRoot);
44
64
  await validateSourceFile(source, entry.path);
@@ -62,11 +82,15 @@ export function compileAgentRuleReverts(entry, agentFilter, home, repo) {
62
82
  if (agentFilter && !agentFilter.includes(agentId))
63
83
  continue;
64
84
  const agent = AGENT_REGISTRY_BY_ID[agentId];
65
- const support = agent?.agentRulesSupport;
85
+ const support = entry.scope === "repo"
86
+ ? (agent?.agentRulesRepoSupport ?? agent?.agentRulesSupport)
87
+ : agent?.agentRulesSupport;
66
88
  if (!support ||
67
89
  support.status === "unsupported" ||
68
90
  support.status === "planned")
69
91
  continue;
92
+ if (entry.scope === "repo" && !repo)
93
+ continue;
70
94
  const target = resolvePlaceholders(support.path[platform], entry.name, home, repo);
71
95
  actions.push({
72
96
  kind: "file-write",
package/dist/core/init.js CHANGED
@@ -1,4 +1,4 @@
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";
@@ -170,7 +170,7 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
170
170
  }
171
171
  }
172
172
  namesSeen.add(name);
173
- rules.push({ name, path: relPath, agents });
173
+ rules.push({ name, path: relPath, agents, scope: "global" });
174
174
  }
175
175
  return rules;
176
176
  }
@@ -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
  }
@@ -1,6 +1,7 @@
1
1
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { UserError } from "../errors.js";
4
+ import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
4
5
  export function sourceAccessError(err, sourcePath) {
5
6
  const code = err.code;
6
7
  if (code === "ENOENT")
@@ -147,23 +148,6 @@ export function validateAgentRuleMarkdownPath(manifestPath, agentId) {
147
148
  throw new UserError("DEPLOY_FAILED", `agentRules entry "${manifestPath}" for agent "${agentId}" must point to a Markdown source file`);
148
149
  }
149
150
  }
150
- function trimMatchingQuotes(value) {
151
- if ((value.startsWith('"') && value.endsWith('"')) ||
152
- (value.startsWith("'") && value.endsWith("'"))) {
153
- return value.slice(1, -1).trim();
154
- }
155
- return value;
156
- }
157
- function parseSimpleFrontmatterValue(field, rawValue, manifestPath) {
158
- const value = trimMatchingQuotes(rawValue.trim());
159
- if (value.length === 0) {
160
- throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter field "${field}" must be a non-empty string`);
161
- }
162
- if (rawValue.trim() === "|" || rawValue.trim() === ">") {
163
- throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter field "${field}" must be a single-line string`);
164
- }
165
- return value;
166
- }
167
151
  export async function validateSkillDefinitionFile(sourcePath, manifestPath) {
168
152
  await validateSourceFile(sourcePath, `${manifestPath}/SKILL.md`);
169
153
  let raw;
@@ -181,27 +165,26 @@ export async function validateSkillDefinitionFile(sourcePath, manifestPath) {
181
165
  if (closingIndex === -1) {
182
166
  throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md is missing the closing --- frontmatter delimiter`);
183
167
  }
184
- let name = null;
185
- let description = null;
186
- for (const line of lines.slice(1, closingIndex)) {
187
- const trimmed = line.trim();
188
- if (trimmed.length === 0 || trimmed.startsWith("#"))
189
- continue;
190
- const match = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
191
- if (!match)
192
- continue;
193
- const [, key, rawValue] = match;
194
- if (key === "name") {
195
- name = parseSimpleFrontmatterValue("name", rawValue, manifestPath);
196
- }
197
- else if (key === "description") {
198
- description = parseSimpleFrontmatterValue("description", rawValue, manifestPath);
199
- }
200
- }
201
- if (name === null) {
202
- throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter must include a non-empty "name" field`);
168
+ let attributes;
169
+ try {
170
+ const parsed = parseFrontmatterDocument(raw);
171
+ attributes = parsed.attributes;
203
172
  }
204
- if (description === null) {
205
- throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter must include a non-empty "description" field`);
173
+ catch (err) {
174
+ throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md has malformed YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`);
206
175
  }
176
+ const validateField = (field) => {
177
+ const value = attributes[field];
178
+ if (value === undefined || value === null) {
179
+ throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter must include a non-empty "${field}" field`);
180
+ }
181
+ if (typeof value !== "string" || value.trim().length === 0) {
182
+ throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter field "${field}" must be a non-empty string`);
183
+ }
184
+ if (value.includes("\n") || value.includes("\r")) {
185
+ throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter field "${field}" must be a single-line string`);
186
+ }
187
+ };
188
+ validateField("name");
189
+ validateField("description");
207
190
  }
@@ -71,6 +71,10 @@ export declare const AgentRuleEntrySchema: z.ZodObject<{
71
71
  "github-copilot": "github-copilot";
72
72
  }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
73
73
  path: z.ZodString;
74
+ scope: z.ZodDefault<z.ZodEnum<{
75
+ global: "global";
76
+ repo: "repo";
77
+ }>>;
74
78
  }, z.core.$strip>;
75
79
  export declare const PermissionsEntrySchema: z.ZodObject<{
76
80
  name: z.ZodString;
@@ -158,6 +162,10 @@ export declare const ManifestSchema: z.ZodObject<{
158
162
  "github-copilot": "github-copilot";
159
163
  }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
160
164
  path: z.ZodString;
165
+ scope: z.ZodDefault<z.ZodEnum<{
166
+ global: "global";
167
+ repo: "repo";
168
+ }>>;
161
169
  }, z.core.$strip>>>;
162
170
  permissions: z.ZodDefault<z.ZodArray<z.ZodObject<{
163
171
  name: z.ZodString;
@@ -89,6 +89,10 @@ export const AgentRuleEntrySchema = z.object({
89
89
  agents: agentsField,
90
90
  // Relative path to the rules/instruction file within the source bundle.
91
91
  path: sourcePathField,
92
+ // Deployment scope: "global" targets the agent's home-directory instruction
93
+ // file (default), "repo" targets the project-root instruction file within the
94
+ // deployed repository (e.g. {repo}/CLAUDE.md for claude-code).
95
+ scope: z.enum(["global", "repo"]).default("global"),
92
96
  });
93
97
  export const PermissionsEntrySchema = z.object({
94
98
  name: nameField,
@@ -102,8 +106,8 @@ export const AgentDefinitionEntrySchema = z.object({
102
106
  name: nameField,
103
107
  agents: agentsField,
104
108
  // Relative path to the agent definition Markdown file within the source bundle.
105
- // Must be a .md or .markdown file containing YAML frontmatter that describes
106
- // the agent's persona, instructions, and any tool configuration.
109
+ // Must be a .md or .markdown file. Content and frontmatter structure are not
110
+ // validated by inception-engine format requirements are agent-specific.
107
111
  path: sourcePathField,
108
112
  });
109
113
  export const ManifestSchema = z.object({
package/dist/types.d.ts CHANGED
@@ -55,6 +55,7 @@ export interface AgentConfig {
55
55
  provenance: AgentProvenance;
56
56
  mcpSupport?: AgentSurfaceSupport;
57
57
  agentRulesSupport?: AgentSurfaceSupport;
58
+ agentRulesRepoSupport?: AgentSurfaceSupport;
58
59
  permissionsSupport?: AgentSurfaceSupport;
59
60
  agentDefinitionsSupport?: AgentSurfaceSupport;
60
61
  policyNote?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Deploy AI agent skills from a git repo to user home directories",
5
5
  "license": "MIT",
6
6
  "author": "Damian Piątkowski",
@@ -47,8 +47,8 @@
47
47
  "test:posix": "node --test --test-isolation=none test/unit/*.test.ts test/os/cross-platform/*.test.ts test/os/posix/*.test.ts"
48
48
  },
49
49
  "dependencies": {
50
- "front-matter": "^4.0.2",
51
50
  "smol-toml": "^1.6.1",
51
+ "yaml": "^2.8.3",
52
52
  "zod": "^4.0.0"
53
53
  },
54
54
  "devDependencies": {