@kuznai/inception-engine 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -22
- package/dist/{config → src/config}/agents.js +113 -14
- package/dist/{core → src/core}/adapters/agent-definitions.d.ts +0 -12
- package/dist/src/core/adapters/agent-definitions.js +162 -0
- package/dist/{core → src/core}/adapters/frontmatter.d.ts +10 -0
- package/dist/{core → src/core}/adapters/frontmatter.js +23 -6
- package/dist/src/core/adapters/index.js +25 -0
- package/dist/{core → src/core}/adapters/mcp.js +17 -20
- package/dist/{core → src/core}/adapters/permissions.js +16 -20
- package/dist/{core → src/core}/adapters/rules.js +26 -50
- package/dist/src/core/capabilities.d.ts +47 -0
- package/dist/src/core/capabilities.js +280 -0
- package/dist/{core → src/core}/deploy.js +82 -54
- package/dist/{core → src/core}/detect.js +2 -7
- package/dist/{core → src/core}/init.js +20 -25
- package/dist/src/core/merge-patch.d.ts +4 -0
- package/dist/src/core/merge-patch.js +46 -0
- package/dist/{core → src/core}/ownership.js +13 -0
- package/dist/src/core/preflight.js +296 -0
- package/dist/{core → src/core}/revert.js +86 -26
- package/dist/{core → src/core}/validation.js +31 -0
- package/dist/{errors.d.ts → src/errors.d.ts} +3 -1
- package/dist/{errors.js → src/errors.js} +2 -2
- package/dist/{schemas → src/schemas}/manifest.d.ts +20 -0
- package/dist/{schemas → src/schemas}/manifest.js +11 -0
- package/dist/{schemas → src/schemas}/registry.d.ts +36 -0
- package/dist/{schemas → src/schemas}/registry.js +12 -4
- package/dist/{types.d.ts → src/types.d.ts} +15 -0
- package/dist/test/helpers/fs.d.ts +2 -0
- package/dist/test/helpers/fs.js +17 -0
- package/dist/test/helpers/path.d.ts +10 -0
- package/dist/test/helpers/path.js +15 -0
- package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
- package/dist/test/helpers/skill-dir-scenarios.js +206 -0
- package/dist/test/helpers/skill-dir.d.ts +7 -0
- package/dist/test/helpers/skill-dir.js +46 -0
- package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
- package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
- package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
- package/dist/test/os/posix/skill-dir.test.js +19 -0
- package/dist/test/os/windows/agentRules-integration.test.d.ts +1 -0
- package/dist/test/os/windows/agentRules-integration.test.js +245 -0
- package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
- package/dist/test/os/windows/revert-integration.test.js +72 -0
- package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
- package/dist/test/os/windows/skill-dir.test.js +19 -0
- package/dist/test/unit/adapters.test.d.ts +1 -0
- package/dist/test/unit/adapters.test.js +1017 -0
- package/dist/test/unit/capabilities.test.d.ts +1 -0
- package/dist/test/unit/capabilities.test.js +71 -0
- package/dist/test/unit/cli.test.d.ts +1 -0
- package/dist/test/unit/cli.test.js +834 -0
- package/dist/test/unit/deploy.test.d.ts +1 -0
- package/dist/test/unit/deploy.test.js +2481 -0
- package/dist/test/unit/detect.test.d.ts +1 -0
- package/dist/test/unit/detect.test.js +192 -0
- package/dist/test/unit/enterprise.test.d.ts +1 -0
- package/dist/test/unit/enterprise.test.js +106 -0
- package/dist/test/unit/frontmatter.test.d.ts +1 -0
- package/dist/test/unit/frontmatter.test.js +102 -0
- package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
- package/dist/test/unit/gemini-north-star.test.js +243 -0
- package/dist/test/unit/init-fixture.test.d.ts +1 -0
- package/dist/test/unit/init-fixture.test.js +153 -0
- package/dist/test/unit/manifest.test.d.ts +1 -0
- package/dist/test/unit/manifest.test.js +737 -0
- package/dist/test/unit/migration-golden.test.d.ts +1 -0
- package/dist/test/unit/migration-golden.test.js +197 -0
- package/dist/test/unit/ownership.test.d.ts +1 -0
- package/dist/test/unit/ownership.test.js +587 -0
- package/dist/test/unit/preflight.test.d.ts +1 -0
- package/dist/test/unit/preflight.test.js +521 -0
- package/dist/test/unit/resolve.test.d.ts +1 -0
- package/dist/test/unit/resolve.test.js +119 -0
- package/dist/test/unit/revert.test.d.ts +1 -0
- package/dist/test/unit/revert.test.js +1004 -0
- package/dist/test/unit/toml.test.d.ts +1 -0
- package/dist/test/unit/toml.test.js +55 -0
- package/package.json +1 -1
- package/dist/core/adapters/agent-definitions.js +0 -91
- package/dist/core/adapters/index.js +0 -30
- package/dist/core/preflight.js +0 -168
- /package/dist/{config → src/config}/agents.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.js +0 -0
- /package/dist/{core → src/core}/adapters/index.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/mcp.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/rules.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.js +0 -0
- /package/dist/{core → src/core}/deploy.d.ts +0 -0
- /package/dist/{core → src/core}/detect.d.ts +0 -0
- /package/dist/{core → src/core}/init.d.ts +0 -0
- /package/dist/{core → src/core}/ownership.d.ts +0 -0
- /package/dist/{core → src/core}/preflight.d.ts +0 -0
- /package/dist/{core → src/core}/resolve.d.ts +0 -0
- /package/dist/{core → src/core}/resolve.js +0 -0
- /package/dist/{core → src/core}/revert.d.ts +0 -0
- /package/dist/{core → src/core}/runtime-paths.d.ts +0 -0
- /package/dist/{core → src/core}/runtime-paths.js +0 -0
- /package/dist/{core → src/core}/validation.d.ts +0 -0
- /package/dist/{formatters.d.ts → src/formatters.d.ts} +0 -0
- /package/dist/{formatters.js → src/formatters.js} +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
- /package/dist/{logger.js → src/logger.js} +0 -0
- /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
- /package/dist/{schemas → src/schemas}/errors.js +0 -0
- /package/dist/{types.js → src/types.js} +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
4
|
+
import { describeCapabilityConfidence, planCapabilityForDeploy, } from "./capabilities.js";
|
|
5
|
+
import { resolveRuntimePaths } from "./runtime-paths.js";
|
|
6
|
+
const BUDGET_WARN_BYTES = 50 * 1024; // 50 KB
|
|
7
|
+
async function detectEnterpriseManagement(agentId, home) {
|
|
8
|
+
if (!AGENT_REGISTRY_BY_ID[agentId]?.enterprisePolicyDetection)
|
|
9
|
+
return null;
|
|
10
|
+
// Check for common GitHub Enterprise environment variables
|
|
11
|
+
if (process.env.GITHUB_ENTERPRISE_URL ||
|
|
12
|
+
process.env.GH_ENTERPRISE_TOKEN ||
|
|
13
|
+
process.env.GITHUB_TOKEN_TYPE === "enterprise") {
|
|
14
|
+
return "GitHub Enterprise environment variables detected. Enterprise policies may override local configurations.";
|
|
15
|
+
}
|
|
16
|
+
const { xdgConfig, localAppdata } = resolveRuntimePaths(home);
|
|
17
|
+
const configPath = process.platform === "win32"
|
|
18
|
+
? path.join(localAppdata, "github-copilot", "hosts.json")
|
|
19
|
+
: path.join(xdgConfig, "github-copilot", "hosts.json");
|
|
20
|
+
try {
|
|
21
|
+
const content = await readFile(configPath, "utf8");
|
|
22
|
+
const hosts = JSON.parse(content);
|
|
23
|
+
const hostNames = Object.keys(hosts);
|
|
24
|
+
const enterpriseHosts = hostNames.filter((h) => h !== "github.com" && h !== "localhost");
|
|
25
|
+
if (enterpriseHosts.length > 0) {
|
|
26
|
+
return `GitHub Copilot is authenticated against enterprise host(s): ${enterpriseHosts.join(", ")}. Enterprise policies may override local configurations.`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const code = err.code;
|
|
31
|
+
if (code !== "ENOENT" &&
|
|
32
|
+
code !== "EACCES" &&
|
|
33
|
+
code !== "EPERM" &&
|
|
34
|
+
code !== undefined) {
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads ~/.gemini/settings.json and warns when `instructionFilename` is set
|
|
42
|
+
* to a value other than "GEMINI.md". If the override is in effect, inception-
|
|
43
|
+
* engine's agentRules deployment to GEMINI.md will be silently ignored by the
|
|
44
|
+
* agent at runtime.
|
|
45
|
+
*/
|
|
46
|
+
async function detectGeminiCustomInstructionFilename(home) {
|
|
47
|
+
const settingsPath = path.join(home, ".gemini", "settings.json");
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readFile(settingsPath, "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (typeof parsed === "object" &&
|
|
52
|
+
parsed !== null &&
|
|
53
|
+
typeof parsed.instructionFilename ===
|
|
54
|
+
"string") {
|
|
55
|
+
const configured = parsed
|
|
56
|
+
.instructionFilename;
|
|
57
|
+
if (configured !== "GEMINI.md") {
|
|
58
|
+
return {
|
|
59
|
+
kind: "config-authority",
|
|
60
|
+
message: `Agent "gemini-cli": settings.json sets instructionFilename to "${configured}" but inception-engine deploys agentRules to "GEMINI.md". The deployed rules file may not be loaded by the agent. Remove the override or align the deploy target.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const code = err.code;
|
|
67
|
+
if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM") {
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function groupRulesByPath(rulesForAgent) {
|
|
74
|
+
const entriesByPath = new Map();
|
|
75
|
+
for (const entry of rulesForAgent ?? []) {
|
|
76
|
+
const list = entriesByPath.get(entry.path) ?? [];
|
|
77
|
+
list.push({ name: entry.name, scope: entry.scope });
|
|
78
|
+
entriesByPath.set(entry.path, list);
|
|
79
|
+
}
|
|
80
|
+
return entriesByPath;
|
|
81
|
+
}
|
|
82
|
+
function detectScopeOverlaps(agentId, rulesForAgent) {
|
|
83
|
+
const warnings = [];
|
|
84
|
+
const entriesByPath = groupRulesByPath(rulesForAgent);
|
|
85
|
+
for (const [path, list] of entriesByPath) {
|
|
86
|
+
const scopes = new Set(list.map((l) => l.scope));
|
|
87
|
+
if (scopes.size < 2)
|
|
88
|
+
continue;
|
|
89
|
+
const scopeList = Array.from(scopes).sort();
|
|
90
|
+
for (let i = 0; i < scopeList.length; i++) {
|
|
91
|
+
for (let j = i + 1; j < scopeList.length; j++) {
|
|
92
|
+
const entryB = list.find((l) => l.scope === scopeList[j]);
|
|
93
|
+
if (entryB) {
|
|
94
|
+
warnings.push({
|
|
95
|
+
kind: "precedence",
|
|
96
|
+
message: `Agent "${agentId}" has agentRules entry "${entryB.name}" deployed to both ${scopeList[i]} and ${scopeList[j]} scope from the same source path "${path}". The file will be written to two distinct targets — verify this is intentional and not a copy-paste mistake.`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return warnings;
|
|
103
|
+
}
|
|
104
|
+
function detectMultipleActiveInstructionScopes(agentId, rulesForAgent) {
|
|
105
|
+
const activeScopes = Array.from(new Set((rulesForAgent ?? []).map((e) => e.scope))).sort();
|
|
106
|
+
if (activeScopes.length <= 1)
|
|
107
|
+
return [];
|
|
108
|
+
const scopeDescriptions = activeScopes
|
|
109
|
+
.map((s) => {
|
|
110
|
+
const entries = (rulesForAgent ?? []).filter((e) => e.scope === s);
|
|
111
|
+
return `${s} [${entries.map((e) => `"${e.name}"`).join(", ")}]`;
|
|
112
|
+
})
|
|
113
|
+
.join(" and ");
|
|
114
|
+
return [
|
|
115
|
+
{
|
|
116
|
+
kind: "precedence",
|
|
117
|
+
message: `Agent "${agentId}" will have multiple instruction files active simultaneously across ${activeScopes.length} scopes: ${scopeDescriptions}. All will be loaded by the agent — ensure the content is intended to stack and does not conflict.`,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
function detectInstructionPrecedence(detectedAgents, manifest) {
|
|
122
|
+
const warnings = [];
|
|
123
|
+
for (const agentId of detectedAgents) {
|
|
124
|
+
const rulesForAgent = (manifest.agentRules ?? []).filter((e) => e.agents.includes(agentId));
|
|
125
|
+
warnings.push(...detectScopeOverlaps(agentId, rulesForAgent));
|
|
126
|
+
warnings.push(...detectMultipleActiveInstructionScopes(agentId, rulesForAgent));
|
|
127
|
+
}
|
|
128
|
+
return warnings;
|
|
129
|
+
}
|
|
130
|
+
async function detectInstructionBudgetRisk(detectedAgents, manifest, sourceDir) {
|
|
131
|
+
const warnings = [];
|
|
132
|
+
const checkedPaths = new Set();
|
|
133
|
+
async function checkEntry(entryPath, label, agentIds) {
|
|
134
|
+
if (!agentIds.some((id) => detectedAgents.includes(id)))
|
|
135
|
+
return;
|
|
136
|
+
const sourcePath = path.resolve(sourceDir, entryPath);
|
|
137
|
+
if (checkedPaths.has(sourcePath))
|
|
138
|
+
return;
|
|
139
|
+
checkedPaths.add(sourcePath);
|
|
140
|
+
try {
|
|
141
|
+
const fileStat = await stat(sourcePath);
|
|
142
|
+
if (fileStat.size > BUDGET_WARN_BYTES) {
|
|
143
|
+
const sizeKb = (fileStat.size / 1024).toFixed(1);
|
|
144
|
+
warnings.push({
|
|
145
|
+
kind: "budget",
|
|
146
|
+
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.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const code = err.code;
|
|
152
|
+
if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM")
|
|
153
|
+
throw err;
|
|
154
|
+
// Missing/unreadable files are silently skipped; compileAgentRuleActions
|
|
155
|
+
// will produce the proper UserError during action compilation.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const entries = [];
|
|
159
|
+
for (const entry of manifest.agentRules ?? []) {
|
|
160
|
+
entries.push({
|
|
161
|
+
path: entry.path,
|
|
162
|
+
label: "agentRules",
|
|
163
|
+
agents: entry.agents,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
for (const entry of manifest.agentDefinitions ?? []) {
|
|
167
|
+
entries.push({
|
|
168
|
+
path: entry.path,
|
|
169
|
+
label: "agentDefinitions",
|
|
170
|
+
agents: entry.agents,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
await Promise.all(entries.map((e) => checkEntry(e.path, e.label, e.agents)));
|
|
174
|
+
return warnings;
|
|
175
|
+
}
|
|
176
|
+
function pushCapabilityWarning(acc, kind, message) {
|
|
177
|
+
if (acc.seen.has(`${kind}:${message}`))
|
|
178
|
+
return;
|
|
179
|
+
acc.seen.add(`${kind}:${message}`);
|
|
180
|
+
acc.warnings.push({ kind, message });
|
|
181
|
+
}
|
|
182
|
+
function collectPlannedSurfaceWarnings(acc, agentId, capability) {
|
|
183
|
+
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
184
|
+
if (!agent?.unsupportedSurfaces ||
|
|
185
|
+
(capability !== "mcpServers" && capability !== "permissions")) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const surface of agent.unsupportedSurfaces) {
|
|
189
|
+
if (surface.status === "planned") {
|
|
190
|
+
pushCapabilityWarning(acc, "info", `Agent "${agentId}": ${surface.reason ?? surface.plannedSurface}`);
|
|
191
|
+
}
|
|
192
|
+
else if (surface.status === "unsupported") {
|
|
193
|
+
pushCapabilityWarning(acc, "config-authority", `Agent "${agentId}": ${surface.reason ?? surface.schemaLabel}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function collectCapabilityWarningsForAgent(acc, agentId, capability, entryName, targetAgents, scope) {
|
|
198
|
+
const plan = planCapabilityForDeploy({
|
|
199
|
+
agentId,
|
|
200
|
+
capability,
|
|
201
|
+
entryName,
|
|
202
|
+
targetAgentIds: targetAgents,
|
|
203
|
+
scope,
|
|
204
|
+
});
|
|
205
|
+
if (plan.outcome === "warn") {
|
|
206
|
+
pushCapabilityWarning(acc, "info", plan.warning.message);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (capability !== "skills") {
|
|
210
|
+
const confidence = describeCapabilityConfidence(agentId, capability, scope);
|
|
211
|
+
if (confidence.message) {
|
|
212
|
+
pushCapabilityWarning(acc, "config-authority", confidence.message);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
collectPlannedSurfaceWarnings(acc, agentId, capability);
|
|
216
|
+
}
|
|
217
|
+
function collectCapabilityWarningsForTargets(acc, targetAgents, capability, entryName, scope) {
|
|
218
|
+
for (const agentId of targetAgents) {
|
|
219
|
+
collectCapabilityWarningsForAgent(acc, agentId, capability, entryName, targetAgents, scope);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function collectManifestCapabilityWarnings(manifest, detectedAgents) {
|
|
223
|
+
const acc = {
|
|
224
|
+
warnings: [],
|
|
225
|
+
seen: new Set(),
|
|
226
|
+
};
|
|
227
|
+
for (const entry of manifest.skills) {
|
|
228
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "skills", entry.name);
|
|
229
|
+
}
|
|
230
|
+
for (const entry of manifest.mcpServers ?? []) {
|
|
231
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "mcpServers", entry.name, entry.scope);
|
|
232
|
+
}
|
|
233
|
+
for (const entry of manifest.agentRules ?? []) {
|
|
234
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "agentRules", entry.name, entry.scope);
|
|
235
|
+
}
|
|
236
|
+
for (const entry of manifest.permissions ?? []) {
|
|
237
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "permissions", entry.name);
|
|
238
|
+
}
|
|
239
|
+
for (const entry of manifest.agentDefinitions ?? []) {
|
|
240
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "agentDefinitions", entry.name, entry.scope);
|
|
241
|
+
}
|
|
242
|
+
return acc.warnings;
|
|
243
|
+
}
|
|
244
|
+
function detectCapabilityPlanningWarnings(manifest, detectedAgents) {
|
|
245
|
+
return collectManifestCapabilityWarnings(manifest, detectedAgents);
|
|
246
|
+
}
|
|
247
|
+
async function collectAgentWarnings(agentId, manifest, home) {
|
|
248
|
+
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
249
|
+
if (!agent)
|
|
250
|
+
return [];
|
|
251
|
+
const warnings = [];
|
|
252
|
+
const skillConfidence = describeCapabilityConfidence(agentId, "skills");
|
|
253
|
+
if (skillConfidence.confidence === "implementation-only" ||
|
|
254
|
+
skillConfidence.confidence === "provisional") {
|
|
255
|
+
warnings.push({
|
|
256
|
+
kind: "config-authority",
|
|
257
|
+
message: skillConfidence.message ??
|
|
258
|
+
`Agent "${agentId}" skill support is ${skillConfidence.confidence}.`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const enterpriseWarning = await detectEnterpriseManagement(agentId, home);
|
|
262
|
+
if (enterpriseWarning) {
|
|
263
|
+
warnings.push({
|
|
264
|
+
kind: "policy",
|
|
265
|
+
message: `Agent "${agentId}": ${enterpriseWarning}`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
else if (agent.policyNote &&
|
|
269
|
+
!agent.policyNote
|
|
270
|
+
.toLowerCase()
|
|
271
|
+
.includes("organization policies may override")) {
|
|
272
|
+
warnings.push({
|
|
273
|
+
kind: "policy",
|
|
274
|
+
message: `Agent "${agentId}": ${agent.policyNote}`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (agentId === "gemini-cli") {
|
|
278
|
+
const hasGeminiRules = (manifest.agentRules ?? []).some((e) => e.agents.includes("gemini-cli"));
|
|
279
|
+
if (hasGeminiRules) {
|
|
280
|
+
const filenameWarning = await detectGeminiCustomInstructionFilename(home);
|
|
281
|
+
if (filenameWarning)
|
|
282
|
+
warnings.push(filenameWarning);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return warnings;
|
|
286
|
+
}
|
|
287
|
+
export async function runPreflight(options, manifest, home, detectedAgents) {
|
|
288
|
+
const warnings = [];
|
|
289
|
+
for (const agentId of detectedAgents) {
|
|
290
|
+
warnings.push(...(await collectAgentWarnings(agentId, manifest, home)));
|
|
291
|
+
}
|
|
292
|
+
warnings.push(...detectCapabilityPlanningWarnings(manifest, detectedAgents));
|
|
293
|
+
warnings.push(...detectInstructionPrecedence(detectedAgents, manifest));
|
|
294
|
+
warnings.push(...(await detectInstructionBudgetRisk(detectedAgents, manifest, options.directory)));
|
|
295
|
+
return warnings;
|
|
296
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
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 * as frontmatterAdapter from "./adapters/frontmatter.js";
|
|
4
5
|
import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
|
|
5
6
|
import { revertTomlMcpPatch } from "./adapters/toml.js";
|
|
7
|
+
import { applyUndoPatch } from "./merge-patch.js";
|
|
6
8
|
import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
|
|
7
9
|
import { resolveAgentSkillPath } from "./resolve.js";
|
|
8
10
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
@@ -112,24 +114,6 @@ async function readJsonConfig(filePath) {
|
|
|
112
114
|
}
|
|
113
115
|
return parsed;
|
|
114
116
|
}
|
|
115
|
-
function isPlainObject(v) {
|
|
116
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
117
|
-
}
|
|
118
|
-
function applyUndoPatch(current, undoPatch) {
|
|
119
|
-
const restored = { ...current };
|
|
120
|
-
for (const [key, originalValue] of Object.entries(undoPatch)) {
|
|
121
|
-
if (originalValue === null) {
|
|
122
|
-
delete restored[key];
|
|
123
|
-
}
|
|
124
|
-
else if (isPlainObject(originalValue) && isPlainObject(restored[key])) {
|
|
125
|
-
restored[key] = applyUndoPatch(restored[key], originalValue);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
restored[key] = originalValue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return restored;
|
|
132
|
-
}
|
|
133
117
|
function lstatOutcome(err) {
|
|
134
118
|
if (err.code === "ENOENT") {
|
|
135
119
|
return { outcome: "skip" };
|
|
@@ -157,14 +141,7 @@ export async function executeRevert(actions, dryRun, verbose, home, deps = {}) {
|
|
|
157
141
|
result = await revertTomlPatch(action, dryRun, verbose, home, planned, deps);
|
|
158
142
|
break;
|
|
159
143
|
case "frontmatter-emit":
|
|
160
|
-
|
|
161
|
-
// deletes the file, same as a file-write revert.
|
|
162
|
-
result = await revertFileWrite({
|
|
163
|
-
kind: "file-write",
|
|
164
|
-
skill: action.skill,
|
|
165
|
-
agent: action.agent,
|
|
166
|
-
target: action.target,
|
|
167
|
-
}, dryRun, verbose, home, planned, deps);
|
|
144
|
+
result = await revertFrontmatterEmit(action, dryRun, verbose, home, planned, deps);
|
|
168
145
|
break;
|
|
169
146
|
default: {
|
|
170
147
|
throw new Error(`Unhandled revert action kind: ${action.kind}`);
|
|
@@ -179,6 +156,89 @@ export async function executeRevert(actions, dryRun, verbose, home, deps = {}) {
|
|
|
179
156
|
planned,
|
|
180
157
|
};
|
|
181
158
|
}
|
|
159
|
+
function isManagedFrontmatterEntry(entry, action) {
|
|
160
|
+
return (entry !== null &&
|
|
161
|
+
entry.kind === "frontmatter-emit" &&
|
|
162
|
+
entry.skill === action.skill &&
|
|
163
|
+
entry.agent === action.agent);
|
|
164
|
+
}
|
|
165
|
+
async function loadManagedFrontmatterEntry(action, home, deps) {
|
|
166
|
+
const entry = await lookupDeployment(home, action.target, deps.registry);
|
|
167
|
+
return isManagedFrontmatterEntry(entry, action) ? entry : null;
|
|
168
|
+
}
|
|
169
|
+
function planFrontmatterRevert(action, frontmatterEntry, planned) {
|
|
170
|
+
planned.push({
|
|
171
|
+
verb: "unapply-patch",
|
|
172
|
+
kind: "frontmatter-emit",
|
|
173
|
+
skill: action.skill,
|
|
174
|
+
agent: action.agent,
|
|
175
|
+
target: action.target,
|
|
176
|
+
patch: frontmatterEntry.undoPatch,
|
|
177
|
+
});
|
|
178
|
+
return { outcome: "ok" };
|
|
179
|
+
}
|
|
180
|
+
async function applyFrontmatterRevert(action, frontmatterEntry) {
|
|
181
|
+
const current = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
|
|
182
|
+
const restoredFrontmatter = applyUndoPatch(current.attributes, frontmatterEntry.undoPatch ?? {});
|
|
183
|
+
const shouldDeleteFile = frontmatterEntry.created === true &&
|
|
184
|
+
!frontmatterEntry.hadFrontmatter &&
|
|
185
|
+
Object.keys(restoredFrontmatter).length === 0 &&
|
|
186
|
+
current.body.trim() === "";
|
|
187
|
+
if (shouldDeleteFile) {
|
|
188
|
+
await unlink(action.target);
|
|
189
|
+
return { shouldDeleteFile };
|
|
190
|
+
}
|
|
191
|
+
const restoredContent = frontmatterAdapter.buildMarkdownDocument(restoredFrontmatter, current.body, { hasFrontmatter: frontmatterEntry.hadFrontmatter ?? false });
|
|
192
|
+
await writeFile(action.target, restoredContent, "utf-8");
|
|
193
|
+
return { shouldDeleteFile };
|
|
194
|
+
}
|
|
195
|
+
async function revertFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
|
|
196
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
197
|
+
try {
|
|
198
|
+
await lstat(action.target);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
const result = lstatOutcome(err);
|
|
202
|
+
if (result.outcome === "skip") {
|
|
203
|
+
logger.skip(label, "(not found, skipping)");
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
logger.fail(label, result.error);
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
const frontmatterEntry = await loadManagedFrontmatterEntry(action, home, deps);
|
|
210
|
+
if (!frontmatterEntry) {
|
|
211
|
+
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
212
|
+
return { outcome: "skip" };
|
|
213
|
+
}
|
|
214
|
+
if (!frontmatterEntry.undoPatch) {
|
|
215
|
+
return revertFileWrite({
|
|
216
|
+
kind: "file-write",
|
|
217
|
+
skill: action.skill,
|
|
218
|
+
agent: action.agent,
|
|
219
|
+
target: action.target,
|
|
220
|
+
}, dryRun, verbose, home, planned, deps);
|
|
221
|
+
}
|
|
222
|
+
if (dryRun) {
|
|
223
|
+
return planFrontmatterRevert(action, frontmatterEntry, planned);
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const { shouldDeleteFile } = await applyFrontmatterRevert(action, frontmatterEntry);
|
|
227
|
+
await unregisterDeployment(home, action.target, deps.registry);
|
|
228
|
+
logger.ok(label);
|
|
229
|
+
if (verbose) {
|
|
230
|
+
logger.detail(shouldDeleteFile
|
|
231
|
+
? `removed: ${action.target}`
|
|
232
|
+
: `unapplied frontmatter patch from: ${action.target}`);
|
|
233
|
+
}
|
|
234
|
+
return { outcome: "ok" };
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
logger.fail(label, msg);
|
|
239
|
+
return { outcome: "fail", error: msg };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
182
242
|
async function executeRevertAction(action, dryRun, verbose, home, planned, deps) {
|
|
183
243
|
const label = `${action.skill} -> ${action.agent}`;
|
|
184
244
|
try {
|
|
@@ -135,6 +135,23 @@ function validateCodexPermissions(config, entryName) {
|
|
|
135
135
|
throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "codex" must define "approval_policy" as one of: ${CODEX_APPROVAL_POLICY_VALUES.join(", ")}`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
function validateOpenCodePermissions(config, entryName) {
|
|
139
|
+
rejectUnknownKeys(config, ["permissions"], entryName, "opencode");
|
|
140
|
+
const perms = config.permissions;
|
|
141
|
+
if (perms === undefined)
|
|
142
|
+
return;
|
|
143
|
+
if (typeof perms !== "object" || perms === null || Array.isArray(perms)) {
|
|
144
|
+
throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "opencode" must define "permissions" as an object`);
|
|
145
|
+
}
|
|
146
|
+
const permsObj = perms;
|
|
147
|
+
for (const key of ["allow", "ask", "deny"]) {
|
|
148
|
+
const val = permsObj[key];
|
|
149
|
+
if (val !== undefined &&
|
|
150
|
+
(!Array.isArray(val) || val.some((item) => typeof item !== "string"))) {
|
|
151
|
+
throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "opencode" must define "permissions.${key}" as an array of strings when present`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
138
155
|
export function validatePermissionsConfigShape(config, entryName, agentId) {
|
|
139
156
|
if (agentId === "claude-code") {
|
|
140
157
|
validateClaudeCodePermissions(config, entryName);
|
|
@@ -142,6 +159,9 @@ export function validatePermissionsConfigShape(config, entryName, agentId) {
|
|
|
142
159
|
else if (agentId === "codex") {
|
|
143
160
|
validateCodexPermissions(config, entryName);
|
|
144
161
|
}
|
|
162
|
+
else if (agentId === "opencode") {
|
|
163
|
+
validateOpenCodePermissions(config, entryName);
|
|
164
|
+
}
|
|
145
165
|
}
|
|
146
166
|
export function validateAgentRuleMarkdownPath(manifestPath, agentId) {
|
|
147
167
|
const extension = path.extname(manifestPath).toLowerCase();
|
|
@@ -198,6 +218,17 @@ function validateGithubCopilotRequirements(attributes, manifestPath) {
|
|
|
198
218
|
if (!(hasTools || hasInstructions)) {
|
|
199
219
|
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "github-copilot" must define "tools" or "instructions" in frontmatter`);
|
|
200
220
|
}
|
|
221
|
+
if (hasTools) {
|
|
222
|
+
const tools = attributes.tools;
|
|
223
|
+
if (!Array.isArray(tools)) {
|
|
224
|
+
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "github-copilot" has a "tools" field that must be an array`);
|
|
225
|
+
}
|
|
226
|
+
for (const tool of tools) {
|
|
227
|
+
if (typeof tool !== "string") {
|
|
228
|
+
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "github-copilot" has a "tools" entry that must be a string`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
201
232
|
}
|
|
202
233
|
function validateAntigravityRequirements(attributes, manifestPath) {
|
|
203
234
|
const mcpServers = attributes["mcp-servers"] ?? attributes.mcpServers;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type ErrorCode = "INVALID_ARGS" | "MANIFEST_INVALID" | "DEPLOY_FAILED" | "RESOLVE_FAILED";
|
|
2
2
|
export declare class UserError extends Error {
|
|
3
3
|
readonly code: ErrorCode;
|
|
4
|
-
constructor(code: ErrorCode, message: string
|
|
4
|
+
constructor(code: ErrorCode, message: string, options?: {
|
|
5
|
+
cause?: unknown;
|
|
6
|
+
});
|
|
5
7
|
}
|
|
@@ -59,6 +59,11 @@ export declare const McpServerEntrySchema: z.ZodObject<{
|
|
|
59
59
|
"github-copilot": "github-copilot";
|
|
60
60
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
61
61
|
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
62
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
63
|
+
global: "global";
|
|
64
|
+
repo: "repo";
|
|
65
|
+
workspace: "workspace";
|
|
66
|
+
}>>;
|
|
62
67
|
}, z.core.$strip>;
|
|
63
68
|
export declare const AgentRuleEntrySchema: z.ZodObject<{
|
|
64
69
|
name: z.ZodString;
|
|
@@ -100,6 +105,11 @@ export declare const AgentDefinitionEntrySchema: z.ZodObject<{
|
|
|
100
105
|
"github-copilot": "github-copilot";
|
|
101
106
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
102
107
|
path: z.ZodString;
|
|
108
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
109
|
+
global: "global";
|
|
110
|
+
repo: "repo";
|
|
111
|
+
workspace: "workspace";
|
|
112
|
+
}>>;
|
|
103
113
|
}, z.core.$strip>;
|
|
104
114
|
export declare const ManifestSchema: z.ZodObject<{
|
|
105
115
|
skills: z.ZodArray<z.ZodObject<{
|
|
@@ -151,6 +161,11 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
151
161
|
"github-copilot": "github-copilot";
|
|
152
162
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
153
163
|
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
164
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
165
|
+
global: "global";
|
|
166
|
+
repo: "repo";
|
|
167
|
+
workspace: "workspace";
|
|
168
|
+
}>>;
|
|
154
169
|
}, z.core.$strip>>>;
|
|
155
170
|
agentRules: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
156
171
|
name: z.ZodString;
|
|
@@ -192,6 +207,11 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
192
207
|
"github-copilot": "github-copilot";
|
|
193
208
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
194
209
|
path: z.ZodString;
|
|
210
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
211
|
+
global: "global";
|
|
212
|
+
repo: "repo";
|
|
213
|
+
workspace: "workspace";
|
|
214
|
+
}>>;
|
|
195
215
|
}, z.core.$strip>>>;
|
|
196
216
|
}, z.core.$strip>;
|
|
197
217
|
export type SkillEntry = z.infer<typeof SkillEntrySchema>;
|
|
@@ -83,6 +83,12 @@ export const McpServerEntrySchema = z.object({
|
|
|
83
83
|
// Raw server descriptor passed verbatim to each agent adapter.
|
|
84
84
|
// Adapters are responsible for validating the shape they need.
|
|
85
85
|
config: z.record(z.string(), z.unknown()),
|
|
86
|
+
// Deployment scope: "global" (default) targets the agent's user-level MCP
|
|
87
|
+
// config file. "repo" and "workspace" target repo- or workspace-relative
|
|
88
|
+
// config files for agents that support them (e.g. GitHub Copilot's
|
|
89
|
+
// .vscode/mcp.json). For agents without scope-specific surfaces the
|
|
90
|
+
// adapter falls back to the global surface or emits a warning.
|
|
91
|
+
scope: z.enum(["global", "repo", "workspace"]).default("global"),
|
|
86
92
|
});
|
|
87
93
|
export const AgentRuleEntrySchema = z.object({
|
|
88
94
|
name: nameField,
|
|
@@ -103,6 +109,7 @@ export const PermissionsEntrySchema = z.object({
|
|
|
103
109
|
// Raw permission config payload validated per agent by the permissions adapter.
|
|
104
110
|
// For claude-code: { permissions: { allow?: string[], deny?: string[] } }
|
|
105
111
|
// For codex: { approval_policy?: "auto" | "manual" | "suggest" | "on-failure" }
|
|
112
|
+
// For opencode: { permissions: { allow?: string[], ask?: string[], deny?: string[] } }
|
|
106
113
|
config: z.record(z.string(), z.unknown()),
|
|
107
114
|
});
|
|
108
115
|
export const AgentDefinitionEntrySchema = z.object({
|
|
@@ -112,6 +119,10 @@ export const AgentDefinitionEntrySchema = z.object({
|
|
|
112
119
|
// Must be a .md or .markdown file. Structural requirements (e.g. YAML
|
|
113
120
|
// frontmatter) are validated per agent requirements by the adapter.
|
|
114
121
|
path: sourcePathField,
|
|
122
|
+
// Deployment scope: "global" targets the agent's home-directory definition
|
|
123
|
+
// directory (if supported), "repo" targets the project-root definition directory
|
|
124
|
+
// within the deployed repository (default).
|
|
125
|
+
scope: z.enum(["global", "repo", "workspace"]).default("repo"),
|
|
115
126
|
});
|
|
116
127
|
export const ManifestSchema = z.object({
|
|
117
128
|
skills: z.array(SkillEntrySchema).superRefine((skills, ctx) => {
|