@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
|
@@ -4,14 +4,15 @@ import path from "node:path";
|
|
|
4
4
|
import { AGENT_REGISTRY, AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
5
5
|
import { UserError } from "../errors.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
|
+
import * as frontmatterAdapter from "./adapters/frontmatter.js";
|
|
7
8
|
import { compileAdapterActions } from "./adapters/index.js";
|
|
9
|
+
import { applyTomlMcpPatch } from "./adapters/toml.js";
|
|
10
|
+
import { planCapabilityForDeploy } from "./capabilities.js";
|
|
11
|
+
import { applyMergePatch, computeUndoPatch, isPlainObject, } from "./merge-patch.js";
|
|
8
12
|
import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
|
|
9
13
|
import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
|
|
10
14
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
11
15
|
import { sourceAccessError, validateSkillDefinitionFile, validateSourceFile, validateSourcePath, } from "./validation.js";
|
|
12
|
-
function isPlainObject(v) {
|
|
13
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
14
|
-
}
|
|
15
16
|
async function readJsonConfigFile(filePath) {
|
|
16
17
|
let rawContent;
|
|
17
18
|
try {
|
|
@@ -35,34 +36,6 @@ async function readJsonConfigFile(filePath) {
|
|
|
35
36
|
}
|
|
36
37
|
return parsed;
|
|
37
38
|
}
|
|
38
|
-
function computeUndoPatch(original, patch) {
|
|
39
|
-
const undoPatch = {};
|
|
40
|
-
for (const key of Object.keys(patch)) {
|
|
41
|
-
const patchVal = patch[key];
|
|
42
|
-
if (isPlainObject(patchVal) && isPlainObject(original[key])) {
|
|
43
|
-
undoPatch[key] = computeUndoPatch(original[key], patchVal);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
undoPatch[key] = key in original ? original[key] : null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return undoPatch;
|
|
50
|
-
}
|
|
51
|
-
function applyMergePatch(original, patch) {
|
|
52
|
-
const patched = { ...original };
|
|
53
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
54
|
-
if (value === null) {
|
|
55
|
-
delete patched[key];
|
|
56
|
-
}
|
|
57
|
-
else if (isPlainObject(value) && isPlainObject(patched[key])) {
|
|
58
|
-
patched[key] = applyMergePatch(patched[key], value);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
patched[key] = value;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return patched;
|
|
65
|
-
}
|
|
66
39
|
function detectCollisions(actions) {
|
|
67
40
|
const seen = new Map();
|
|
68
41
|
const warnings = [];
|
|
@@ -134,7 +107,7 @@ function checkPairAgentRuleAmbiguities(manifest, primary, rider) {
|
|
|
134
107
|
JSON.stringify(primarySupport.path) === JSON.stringify(riderSupport.path)) {
|
|
135
108
|
warnings.push({
|
|
136
109
|
kind: "ambiguity",
|
|
137
|
-
message: `Both "${primary}" and "${rider}" are listed in agentRules entry "${entry.name}". Both target the same surface
|
|
110
|
+
message: `Both "${primary}" and "${rider}" are listed in agentRules entry "${entry.name}". Both target the same surface - listing both is redundant but harmless; deduplication ensures only one write action is emitted.`,
|
|
138
111
|
});
|
|
139
112
|
}
|
|
140
113
|
}
|
|
@@ -151,7 +124,7 @@ function checkPairMcpAmbiguities(manifest, primary, rider) {
|
|
|
151
124
|
continue;
|
|
152
125
|
warnings.push({
|
|
153
126
|
kind: "ambiguity",
|
|
154
|
-
message: `Both "${primary}" and "${rider}" are listed in mcpServers entry "${entry.name}". "${primary}" writes to a shared MCP surface
|
|
127
|
+
message: `Both "${primary}" and "${rider}" are listed in mcpServers entry "${entry.name}". "${primary}" writes to a shared MCP surface - verify that deploying to both does not produce conflicting MCP server behavior.`,
|
|
155
128
|
});
|
|
156
129
|
}
|
|
157
130
|
return warnings;
|
|
@@ -163,7 +136,7 @@ function checkPairAgentDefinitionAmbiguities(manifest, primary, rider) {
|
|
|
163
136
|
continue;
|
|
164
137
|
warnings.push({
|
|
165
138
|
kind: "ambiguity",
|
|
166
|
-
message: `Both "${primary}" and "${rider}" are listed in agentDefinitions entry "${entry.name}". They write to distinct surfaces
|
|
139
|
+
message: `Both "${primary}" and "${rider}" are listed in agentDefinitions entry "${entry.name}". They write to distinct surfaces - verify that this behavioral divergence is intended.`,
|
|
167
140
|
});
|
|
168
141
|
}
|
|
169
142
|
return warnings;
|
|
@@ -191,28 +164,45 @@ function checkAntigravityPathCollisions(manifest) {
|
|
|
191
164
|
if (defNames.has(entry.name)) {
|
|
192
165
|
warnings.push({
|
|
193
166
|
kind: "collision",
|
|
194
|
-
message: `agentDefinitions entry "${entry.name}" and mcpServers entry "${entry.name}" for agent "antigravity" both resolve to {repo}/.agents/rules/${entry.name}.md
|
|
167
|
+
message: `agentDefinitions entry "${entry.name}" and mcpServers entry "${entry.name}" for agent "antigravity" both resolve to {repo}/.agents/rules/${entry.name}.md - one will silently overwrite the other; use different names or remove one entry`,
|
|
195
168
|
});
|
|
196
169
|
}
|
|
197
170
|
}
|
|
198
171
|
return warnings;
|
|
199
172
|
}
|
|
173
|
+
function assertNoAntigravityPathCollisions(manifest) {
|
|
174
|
+
const collisions = checkAntigravityPathCollisions(manifest);
|
|
175
|
+
if (collisions.length === 0)
|
|
176
|
+
return;
|
|
177
|
+
throw new UserError("MANIFEST_INVALID", collisions.map((warning) => warning.message).join("; "));
|
|
178
|
+
}
|
|
200
179
|
async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
|
|
201
180
|
const method = getDeployMethod();
|
|
202
181
|
const actions = [];
|
|
203
|
-
|
|
182
|
+
const warnings = [];
|
|
183
|
+
async function planSkillEntry(skill) {
|
|
204
184
|
const targetAgents = skill.agents.filter((agentId) => detectedAgents.includes(agentId));
|
|
205
185
|
if (targetAgents.length === 0)
|
|
206
|
-
|
|
186
|
+
return;
|
|
207
187
|
const source = path.resolve(sourceDir, skill.path);
|
|
208
188
|
await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
|
|
209
189
|
await validateSkillContract(source, skill.path);
|
|
210
190
|
for (const agentId of targetAgents) {
|
|
191
|
+
const plan = planCapabilityForDeploy({
|
|
192
|
+
agentId,
|
|
193
|
+
capability: "skills",
|
|
194
|
+
entryName: skill.name,
|
|
195
|
+
targetAgentIds: targetAgents,
|
|
196
|
+
});
|
|
197
|
+
if (plan.outcome === "warn") {
|
|
198
|
+
warnings.push(plan.warning);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (plan.outcome === "native" || plan.outcome === "redundant")
|
|
202
|
+
continue;
|
|
211
203
|
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
212
204
|
if (!agent)
|
|
213
205
|
continue;
|
|
214
|
-
if (!agent.skills)
|
|
215
|
-
continue;
|
|
216
206
|
actions.push({
|
|
217
207
|
kind: "skill-dir",
|
|
218
208
|
skill: skill.name,
|
|
@@ -220,18 +210,19 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
|
|
|
220
210
|
source,
|
|
221
211
|
target: resolveAgentSkillPath(agent, skill.name, home),
|
|
222
212
|
method,
|
|
223
|
-
confidence:
|
|
213
|
+
confidence: plan.confidence ?? "provisional",
|
|
224
214
|
});
|
|
225
215
|
}
|
|
226
216
|
}
|
|
227
|
-
|
|
217
|
+
await Promise.all(manifest.skills.map(planSkillEntry));
|
|
218
|
+
return { actions, warnings };
|
|
228
219
|
}
|
|
229
220
|
async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace) {
|
|
230
221
|
const actions = [];
|
|
231
|
-
|
|
222
|
+
await Promise.all((manifest.files ?? []).map(async (fileEntry) => {
|
|
232
223
|
const targetAgents = fileEntry.agents.filter((agentId) => detectedAgents.includes(agentId));
|
|
233
224
|
if (targetAgents.length === 0)
|
|
234
|
-
|
|
225
|
+
return;
|
|
235
226
|
const source = path.resolve(sourceDir, fileEntry.path);
|
|
236
227
|
await validateSourcePath(source, fileEntry.path, resolvedSourceDir, realRoot);
|
|
237
228
|
await validateSourceFile(source, fileEntry.path);
|
|
@@ -245,10 +236,10 @@ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, real
|
|
|
245
236
|
agent: agentId,
|
|
246
237
|
source,
|
|
247
238
|
target: resolveTargetTemplate(fileEntry.target, home, repo, workspace),
|
|
248
|
-
confidence:
|
|
239
|
+
confidence: "implementation-only",
|
|
249
240
|
});
|
|
250
241
|
}
|
|
251
|
-
}
|
|
242
|
+
}));
|
|
252
243
|
return actions;
|
|
253
244
|
}
|
|
254
245
|
function planConfigPatchActions(manifest, detectedAgents, home, repo, workspace) {
|
|
@@ -273,6 +264,7 @@ function planConfigPatchActions(manifest, detectedAgents, home, repo, workspace)
|
|
|
273
264
|
return actions;
|
|
274
265
|
}
|
|
275
266
|
export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo, workspace) {
|
|
267
|
+
assertNoAntigravityPathCollisions(manifest);
|
|
276
268
|
const resolvedSourceDir = path.resolve(sourceDir);
|
|
277
269
|
let realRoot;
|
|
278
270
|
try {
|
|
@@ -282,14 +274,16 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo
|
|
|
282
274
|
realRoot = resolvedSourceDir;
|
|
283
275
|
}
|
|
284
276
|
const repoDir = repo ?? realRoot;
|
|
277
|
+
const skillPlan = await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
|
|
285
278
|
const actions = [
|
|
286
|
-
...
|
|
279
|
+
...skillPlan.actions,
|
|
287
280
|
...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, workspace)),
|
|
288
281
|
...planConfigPatchActions(manifest, detectedAgents, home, repoDir, workspace),
|
|
289
282
|
];
|
|
290
283
|
const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, manifest.agentDefinitions ?? [], workspace);
|
|
291
284
|
actions.push(...adapterResult.actions);
|
|
292
285
|
const warnings = [
|
|
286
|
+
...skillPlan.warnings,
|
|
293
287
|
...detectAmbiguities(detectedAgents, manifest),
|
|
294
288
|
...checkAntigravityPathCollisions(manifest),
|
|
295
289
|
...detectCollisions(actions),
|
|
@@ -376,7 +370,7 @@ async function backupManagedFileWriteTarget(action, home, deps) {
|
|
|
376
370
|
agent: action.agent,
|
|
377
371
|
}, deps.registry);
|
|
378
372
|
if (!isOwned) {
|
|
379
|
-
throw new Error(`Target "${action.target}" exists but is not managed by inception-engine
|
|
373
|
+
throw new Error(`Target "${action.target}" exists but is not managed by inception-engine - refusing to overwrite`);
|
|
380
374
|
}
|
|
381
375
|
const backupPath = `${action.target}.inception-backup`;
|
|
382
376
|
await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
|
|
@@ -495,6 +489,7 @@ async function deployFileWrite(action, dryRun, verbose, home, planned, deps) {
|
|
|
495
489
|
source: action.source,
|
|
496
490
|
skill: action.skill,
|
|
497
491
|
agent: action.agent,
|
|
492
|
+
migratedFrom: action.migratedFrom,
|
|
498
493
|
}, deps.registry));
|
|
499
494
|
logger.ok(label);
|
|
500
495
|
if (verbose) {
|
|
@@ -533,7 +528,7 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
533
528
|
if (existingEntry &&
|
|
534
529
|
(existingEntry.skill !== action.skill ||
|
|
535
530
|
existingEntry.agent !== action.agent)) {
|
|
536
|
-
throw new Error(`Config "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}"
|
|
531
|
+
throw new Error(`Config "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" - refusing to double-patch`);
|
|
537
532
|
}
|
|
538
533
|
const original = await readJsonConfigFile(action.target);
|
|
539
534
|
const undoPatch = computeUndoPatch(original, patch);
|
|
@@ -581,7 +576,7 @@ async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
581
576
|
try {
|
|
582
577
|
const { previousValue } = await (deps.registry
|
|
583
578
|
? Promise.resolve({ previousValue: null })
|
|
584
|
-
:
|
|
579
|
+
: applyTomlMcpPatch(action.target, action.skill, action.config));
|
|
585
580
|
// Note: To truly support custom deps here we'd need to refactor toml adapter to accept deps.
|
|
586
581
|
// For now we assume standard adapter for TOML.
|
|
587
582
|
await registerDeployment(home, action.target, {
|
|
@@ -605,6 +600,11 @@ async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
605
600
|
}
|
|
606
601
|
async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
|
|
607
602
|
const label = `${action.skill} -> ${action.agent}`;
|
|
603
|
+
if (!isPlainObject(action.frontmatter)) {
|
|
604
|
+
const msg = `Frontmatter patch for skill "${action.skill}" must be a plain object`;
|
|
605
|
+
logger.fail(label, msg);
|
|
606
|
+
return { error: msg };
|
|
607
|
+
}
|
|
608
608
|
if (dryRun) {
|
|
609
609
|
planned.push({
|
|
610
610
|
verb: "emit-frontmatter",
|
|
@@ -617,12 +617,40 @@ async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, dep
|
|
|
617
617
|
return { error: null };
|
|
618
618
|
}
|
|
619
619
|
try {
|
|
620
|
-
await (
|
|
621
|
-
|
|
620
|
+
const existingEntry = await lookupDeployment(home, action.target, deps.registry);
|
|
621
|
+
if (existingEntry &&
|
|
622
|
+
(existingEntry.kind !== "frontmatter-emit" ||
|
|
623
|
+
existingEntry.skill !== action.skill ||
|
|
624
|
+
existingEntry.agent !== action.agent)) {
|
|
625
|
+
throw new Error(`Frontmatter target "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" - refusing to double-patch`);
|
|
626
|
+
}
|
|
627
|
+
const existing = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
|
|
628
|
+
const undoPatch = computeUndoPatch(existing.attributes, action.frontmatter);
|
|
629
|
+
const patchedFrontmatter = applyMergePatch(existing.attributes, action.frontmatter);
|
|
630
|
+
const content = frontmatterAdapter.buildMarkdownDocument(patchedFrontmatter, existing.body, { hasFrontmatter: true });
|
|
631
|
+
await replaceFileAtomically(action.target, deps, (tempPath, fileOps) => fileOps.writeFile(tempPath, content, "utf-8"), async () => {
|
|
632
|
+
try {
|
|
633
|
+
await lstat(action.target);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
const backupPath = `${action.target}.inception-backup`;
|
|
639
|
+
await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
|
|
640
|
+
recursive: true,
|
|
641
|
+
force: true,
|
|
642
|
+
});
|
|
643
|
+
await (deps.fileOps ?? defaultDeployFileOps).rename(action.target, backupPath);
|
|
644
|
+
return backupPath;
|
|
645
|
+
}, () => registerDeployment(home, action.target, {
|
|
622
646
|
kind: "frontmatter-emit",
|
|
647
|
+
patch: action.frontmatter,
|
|
648
|
+
undoPatch,
|
|
649
|
+
created: !existing.exists,
|
|
650
|
+
hadFrontmatter: existing.hasFrontmatter,
|
|
623
651
|
skill: action.skill,
|
|
624
652
|
agent: action.agent,
|
|
625
|
-
}, deps.registry);
|
|
653
|
+
}, deps.registry));
|
|
626
654
|
logger.ok(label);
|
|
627
655
|
if (verbose) {
|
|
628
656
|
logger.detail(`emit-frontmatter: ${action.target}`);
|
|
@@ -683,7 +711,7 @@ async function assertTargetAbsent(targetPath) {
|
|
|
683
711
|
catch (err) {
|
|
684
712
|
if (err instanceof Error && err.message.startsWith("Target path appeared"))
|
|
685
713
|
throw err;
|
|
686
|
-
// ENOENT is expected
|
|
714
|
+
// ENOENT is expected - target should not exist after backup
|
|
687
715
|
}
|
|
688
716
|
}
|
|
689
717
|
async function createDeployTarget(action, home, deps) {
|
|
@@ -746,7 +774,7 @@ async function backupExisting(targetPath, verbose, home, expected, deps) {
|
|
|
746
774
|
skill: expected.skill,
|
|
747
775
|
agent: expected.agent,
|
|
748
776
|
}, deps.registry))) {
|
|
749
|
-
throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine
|
|
777
|
+
throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine - refusing to overwrite`);
|
|
750
778
|
}
|
|
751
779
|
const backupPath = `${targetPath}.inception-backup`;
|
|
752
780
|
if (verbose) {
|
|
@@ -8,13 +8,8 @@ const defaultExecFn = async (cmd, args) => {
|
|
|
8
8
|
await execFileAsync(cmd, args);
|
|
9
9
|
};
|
|
10
10
|
export async function detectInstalledAgents(home) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
if (await isAgentInstalled(agent, home)) {
|
|
14
|
-
detected.push(agent.id);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return detected;
|
|
11
|
+
const results = await Promise.all(AGENT_REGISTRY.map((agent) => isAgentInstalled(agent, home)));
|
|
12
|
+
return AGENT_REGISTRY.filter((_, i) => results[i]).map((a) => a.id);
|
|
18
13
|
}
|
|
19
14
|
async function isAgentInstalled(agent, home) {
|
|
20
15
|
const detectPath = resolveAgentDetectPath(agent, home);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { access, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { AGENT_REGISTRY
|
|
3
|
+
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
4
4
|
import { dryRunPrefix, logger } from "../logger.js";
|
|
5
5
|
import { AGENT_IDS, AgentDefinitionEntrySchema, ConfigEntrySchema, FileEntrySchema, McpServerEntrySchema, } from "../schemas/manifest.js";
|
|
6
6
|
import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
|
|
7
|
+
import { shouldInitIncludeAgent } from "./capabilities.js";
|
|
7
8
|
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
8
9
|
// Ordered list: first match wins. Catch-all is applied at call site.
|
|
9
10
|
// Derived from the registry: group agents by the filename of their global
|
|
@@ -14,13 +15,11 @@ const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
|
14
15
|
const AGENT_RULES_FILE_PATTERNS = (() => {
|
|
15
16
|
const filenameToAgents = new Map();
|
|
16
17
|
for (const agent of AGENT_REGISTRY) {
|
|
18
|
+
if (!shouldInitIncludeAgent(agent.id, "agentRules", "global"))
|
|
19
|
+
continue;
|
|
17
20
|
const support = agent.agentRulesSupport;
|
|
18
21
|
if (support?.status !== "supported")
|
|
19
22
|
continue;
|
|
20
|
-
// Skip riders that require the primary — they cannot be listed independently
|
|
21
|
-
if (support.surfaceKind?.kind === "shared-via" &&
|
|
22
|
-
support.surfaceKind.requiresPrimary)
|
|
23
|
-
continue;
|
|
24
23
|
const filename = support.path.posix[support.path.posix.length - 1]?.toLowerCase();
|
|
25
24
|
if (!filename)
|
|
26
25
|
continue;
|
|
@@ -47,10 +46,11 @@ const AGENT_RULES_SUBDIRS = ["rules", "instructions", ".github"];
|
|
|
47
46
|
// Conventional subdirectories that contain agent definition files.
|
|
48
47
|
// Derived from the registry: for each agent with a supported agentDefinitions
|
|
49
48
|
// surface, extract the directory prefix from its posix path template.
|
|
49
|
+
// Includes the legacy ".github/agents" path for backward compatibility.
|
|
50
50
|
const AGENT_DEFINITION_SUBDIRS = (() => {
|
|
51
51
|
const subdirs = new Set();
|
|
52
52
|
for (const agent of AGENT_REGISTRY) {
|
|
53
|
-
const support = agent.
|
|
53
|
+
const support = agent.agentDefinitionsRepoSupport;
|
|
54
54
|
if (support?.status !== "supported")
|
|
55
55
|
continue;
|
|
56
56
|
const tmpl = support.path.posix;
|
|
@@ -62,6 +62,7 @@ const AGENT_DEFINITION_SUBDIRS = (() => {
|
|
|
62
62
|
if (dirSegs.length > 0)
|
|
63
63
|
subdirs.add(dirSegs.join("/"));
|
|
64
64
|
}
|
|
65
|
+
subdirs.add(".github/agents");
|
|
65
66
|
return Array.from(subdirs);
|
|
66
67
|
})();
|
|
67
68
|
async function hasAntigravityMcpFrontmatter(absPath) {
|
|
@@ -114,6 +115,7 @@ function resolveSkillName(relPath, name, namesSeen) {
|
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
117
|
function buildSkills(found, agents) {
|
|
118
|
+
const initAgents = agents.filter((agentId) => shouldInitIncludeAgent(agentId, "skills"));
|
|
117
119
|
const namesSeen = new Set();
|
|
118
120
|
const skills = [];
|
|
119
121
|
for (const { relPath, name } of found) {
|
|
@@ -121,7 +123,7 @@ function buildSkills(found, agents) {
|
|
|
121
123
|
if (skillName === null)
|
|
122
124
|
continue;
|
|
123
125
|
namesSeen.add(skillName);
|
|
124
|
-
skills.push({ name: skillName, path: relPath, agents });
|
|
126
|
+
skills.push({ name: skillName, path: relPath, agents: initAgents });
|
|
125
127
|
}
|
|
126
128
|
return skills;
|
|
127
129
|
}
|
|
@@ -215,7 +217,7 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
|
|
|
215
217
|
}
|
|
216
218
|
/**
|
|
217
219
|
* Derives the name for an agent definition entry from its file name.
|
|
218
|
-
* For GitHub Copilot's `{name}.agent.md` naming convention, strips the
|
|
220
|
+
* For GitHub Copilot's legacy `{name}.agent.md` naming convention, strips the
|
|
219
221
|
* `.agent` infix in addition to the `.md` extension.
|
|
220
222
|
*/
|
|
221
223
|
function deriveAgentDefinitionName(relPath, fileName) {
|
|
@@ -236,12 +238,15 @@ function deriveAgentDefinitionName(relPath, fileName) {
|
|
|
236
238
|
/**
|
|
237
239
|
* Maps a known agent-definition subdirectory to the agent IDs that own it.
|
|
238
240
|
* Derived from the registry by matching each agent's agentDefinitionsSupport
|
|
239
|
-
* path prefix.
|
|
241
|
+
* path prefix. Includes legacy mapping for ".github/agents".
|
|
242
|
+
* Returns null when the subdir is not recognized.
|
|
240
243
|
*/
|
|
241
244
|
function agentsForDefinitionSubdir(subdir) {
|
|
245
|
+
if (subdir === ".github/agents")
|
|
246
|
+
return ["github-copilot"];
|
|
242
247
|
const matching = [];
|
|
243
248
|
for (const agent of AGENT_REGISTRY) {
|
|
244
|
-
const support = agent.
|
|
249
|
+
const support = agent.agentDefinitionsRepoSupport;
|
|
245
250
|
if (support?.status !== "supported")
|
|
246
251
|
continue;
|
|
247
252
|
const tmpl = support.path.posix;
|
|
@@ -325,8 +330,7 @@ async function findAgentDefinitionCandidates(baseDir, skillDirRelPaths, agentRul
|
|
|
325
330
|
function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
|
|
326
331
|
const definitions = [];
|
|
327
332
|
const localNamesSeen = new Set(namesSeen);
|
|
328
|
-
const definitionsCapableAgents = activeAgents.filter((id) =>
|
|
329
|
-
"unsupported");
|
|
333
|
+
const definitionsCapableAgents = activeAgents.filter((id) => shouldInitIncludeAgent(id, "agentDefinitions", "repo"));
|
|
330
334
|
for (const { relPath, name: rawName, suggestedAgents } of candidates) {
|
|
331
335
|
// Use suggested agents (from the dir that was scanned), intersected with
|
|
332
336
|
// active agents that support agentDefinitions. Fall back to all capable
|
|
@@ -351,7 +355,7 @@ function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
|
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
357
|
localNamesSeen.add(name);
|
|
354
|
-
definitions.push({ name, path: relPath, agents });
|
|
358
|
+
definitions.push({ name, path: relPath, agents, scope: "repo" });
|
|
355
359
|
}
|
|
356
360
|
return definitions;
|
|
357
361
|
}
|
|
@@ -517,7 +521,8 @@ function logPathAgentEntries(label, entries) {
|
|
|
517
521
|
return;
|
|
518
522
|
logger.detail(`${label}:`);
|
|
519
523
|
for (const e of entries) {
|
|
520
|
-
|
|
524
|
+
const entryAgents = e.agents ?? [];
|
|
525
|
+
logger.detail(` ${e.name} → ${e.path} [${entryAgents.join(", ")}]`);
|
|
521
526
|
}
|
|
522
527
|
}
|
|
523
528
|
function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agentDefinitions) {
|
|
@@ -548,17 +553,7 @@ function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agen
|
|
|
548
553
|
export async function runInit(options) {
|
|
549
554
|
const { directory, dryRun, force, verbose } = options;
|
|
550
555
|
const agents = options.agents ?? [...AGENT_IDS];
|
|
551
|
-
const agentRulesCapableAgents = agents.filter((id) =>
|
|
552
|
-
const support = AGENT_REGISTRY_BY_ID[id].agentRulesSupport;
|
|
553
|
-
if (!support || support.status === "unsupported")
|
|
554
|
-
return false;
|
|
555
|
-
// Exclude riders that require the primary — they cannot operate independently
|
|
556
|
-
if (support.status === "supported" &&
|
|
557
|
-
support.surfaceKind?.kind === "shared-via" &&
|
|
558
|
-
support.surfaceKind.requiresPrimary)
|
|
559
|
-
return false;
|
|
560
|
-
return true;
|
|
561
|
-
});
|
|
556
|
+
const agentRulesCapableAgents = agents.filter((id) => shouldInitIncludeAgent(id, "agentRules", "global"));
|
|
562
557
|
const manifestPath = path.join(directory, "inception.json");
|
|
563
558
|
if (!dryRun && (await manifestExists(manifestPath)) && !force) {
|
|
564
559
|
logger.error(`Error: ${manifestPath} already exists. Use --force to overwrite.`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
|
|
2
|
+
export declare function computeUndoPatch(original: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
3
|
+
export declare function applyMergePatch(original: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
4
|
+
export declare function applyUndoPatch(current: Record<string, unknown>, undoPatch: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function isPlainObject(v) {
|
|
2
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
export function computeUndoPatch(original, patch) {
|
|
5
|
+
const undoPatch = {};
|
|
6
|
+
for (const key of Object.keys(patch)) {
|
|
7
|
+
const patchVal = patch[key];
|
|
8
|
+
if (isPlainObject(patchVal) && isPlainObject(original[key])) {
|
|
9
|
+
undoPatch[key] = computeUndoPatch(original[key], patchVal);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
undoPatch[key] = key in original ? original[key] : null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undoPatch;
|
|
16
|
+
}
|
|
17
|
+
export function applyMergePatch(original, patch) {
|
|
18
|
+
const patched = { ...original };
|
|
19
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
20
|
+
if (value === null) {
|
|
21
|
+
delete patched[key];
|
|
22
|
+
}
|
|
23
|
+
else if (isPlainObject(value) && isPlainObject(patched[key])) {
|
|
24
|
+
patched[key] = applyMergePatch(patched[key], value);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
patched[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return patched;
|
|
31
|
+
}
|
|
32
|
+
export function applyUndoPatch(current, undoPatch) {
|
|
33
|
+
const restored = { ...current };
|
|
34
|
+
for (const [key, originalValue] of Object.entries(undoPatch)) {
|
|
35
|
+
if (originalValue === null) {
|
|
36
|
+
delete restored[key];
|
|
37
|
+
}
|
|
38
|
+
else if (isPlainObject(originalValue) && isPlainObject(restored[key])) {
|
|
39
|
+
restored[key] = applyUndoPatch(restored[key], originalValue);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
restored[key] = originalValue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return restored;
|
|
46
|
+
}
|
|
@@ -40,10 +40,23 @@ async function setFilePermissions(filePath) {
|
|
|
40
40
|
function emptyRegistry() {
|
|
41
41
|
return { version: 1, deployments: {} };
|
|
42
42
|
}
|
|
43
|
+
function surfaceIdForEntry(entry) {
|
|
44
|
+
return entry.surfaceId ?? `${entry.kind}:${entry.agent}:${entry.skill}`;
|
|
45
|
+
}
|
|
43
46
|
export async function registerDeployment(home, targetPath, entry, persistence = defaultRegistryPersistence) {
|
|
44
47
|
const registry = await persistence.load(home);
|
|
48
|
+
const surfaceId = surfaceIdForEntry(entry);
|
|
49
|
+
for (const migratedTarget of entry.migratedFrom ?? []) {
|
|
50
|
+
if (migratedTarget === targetPath)
|
|
51
|
+
continue;
|
|
52
|
+
const existing = registry.deployments[migratedTarget];
|
|
53
|
+
if (existing?.surfaceId === surfaceId) {
|
|
54
|
+
delete registry.deployments[migratedTarget];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
registry.deployments[targetPath] = {
|
|
46
58
|
...entry,
|
|
59
|
+
surfaceId,
|
|
47
60
|
deployed: new Date().toISOString(),
|
|
48
61
|
};
|
|
49
62
|
await persistence.save(home, registry);
|