@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 +29 -5
- package/dist/config/agents.js +45 -0
- package/dist/core/adapters/frontmatter.d.ts +17 -5
- package/dist/core/adapters/frontmatter.js +36 -57
- package/dist/core/adapters/rules.js +45 -21
- package/dist/core/init.js +2 -2
- package/dist/core/preflight.d.ts +2 -2
- package/dist/core/preflight.js +71 -1
- package/dist/core/validation.js +21 -38
- package/dist/schemas/manifest.d.ts +8 -0
- package/dist/schemas/manifest.js +6 -2
- package/dist/types.d.ts +1 -0
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
package/dist/config/agents.js
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
*
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
31
|
+
const attributes = YAML.parse(frontmatter);
|
|
32
|
+
return { attributes: (attributes || {}), body };
|
|
54
33
|
}
|
|
55
34
|
/**
|
|
56
|
-
* Serializes
|
|
57
|
-
* `---` frontmatter delimiters.
|
|
35
|
+
* Serializes an object to a YAML block.
|
|
58
36
|
*/
|
|
59
37
|
export function serializeFrontmatter(data) {
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
}
|
package/dist/core/preflight.d.ts
CHANGED
|
@@ -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(
|
|
6
|
+
export declare function runPreflight(options: CliOptions, manifest: Manifest, _home: string, detectedAgents: AgentId[]): Promise<PreflightWarning[]>;
|
package/dist/core/preflight.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/core/validation.js
CHANGED
|
@@ -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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
205
|
-
throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md frontmatter
|
|
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;
|
package/dist/schemas/manifest.js
CHANGED
|
@@ -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
|
|
106
|
-
//
|
|
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.
|
|
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": {
|