@kuznai/inception-engine 0.20.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 +2 -0
- package/dist/src/config/agents.js +26 -0
- package/dist/src/core/adapters/index.js +15 -20
- package/dist/src/core/deploy.js +17 -18
- package/dist/src/core/detect.js +2 -7
- package/dist/src/core/init.js +2 -1
- package/dist/src/core/preflight.js +35 -19
- package/dist/src/core/revert.js +1 -1
- package/dist/src/core/validation.js +11 -0
- package/dist/src/errors.d.ts +3 -1
- package/dist/src/errors.js +2 -2
- package/dist/src/types.d.ts +2 -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/unit/adapters.test.js +70 -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/preflight.test.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -271,6 +271,8 @@ Global targets where supported:
|
|
|
271
271
|
|
|
272
272
|
For GitHub Copilot, deploy also records migration from the legacy `{repo}/.github/agents/{name}.agent.md` path so older installs can be cleaned up safely.
|
|
273
273
|
|
|
274
|
+
For GitHub Copilot, the `tools` YAML frontmatter field in agent definition files declares which MCP tools and built-in Copilot tools the agent has access to (agent-level tool mapping). Inception-engine validates that `tools`, when present, is an array of strings. Deploy agent-level tool mappings through `agentDefinitions` by including the `tools` field in your source file's frontmatter - no separate manifest section is needed.
|
|
275
|
+
|
|
274
276
|
Revert removes the deployed agent-definition file.
|
|
275
277
|
|
|
276
278
|
## Creating Skills
|
|
@@ -472,6 +472,12 @@ export const AGENT_REGISTRY = [
|
|
|
472
472
|
windows: ["{workspace}", "CLAUDE.md"],
|
|
473
473
|
},
|
|
474
474
|
},
|
|
475
|
+
mcpDevcontainerSupport: {
|
|
476
|
+
status: "planned",
|
|
477
|
+
schemaLabel: "devcontainer.json MCP surface",
|
|
478
|
+
plannedSurface: "devcontainer.json customizations.vscode.mcp.servers",
|
|
479
|
+
reason: 'devcontainer.json MCP support is planned — use scope: "repo" or scope: "workspace" to target .vscode/mcp.json in the meantime',
|
|
480
|
+
},
|
|
475
481
|
permissionsSupport: {
|
|
476
482
|
status: "unsupported",
|
|
477
483
|
schemaLabel: "global permissions surface",
|
|
@@ -485,7 +491,27 @@ export const AGENT_REGISTRY = [
|
|
|
485
491
|
windows: ["{repo}", ".github", "copilot", "agents", "{name}.md"],
|
|
486
492
|
},
|
|
487
493
|
},
|
|
494
|
+
// GitHub Copilot agent definitions declare MCP tool access via a top-level
|
|
495
|
+
// 'tools' array in YAML frontmatter. This surface is deployed through
|
|
496
|
+
// agentDefinitions - no separate deploy action is emitted.
|
|
497
|
+
mcpAgentFrontmatterSupport: {
|
|
498
|
+
status: "supported",
|
|
499
|
+
schemaLabel: "agent definition frontmatter tools field",
|
|
500
|
+
surfaceKind: { kind: "native" },
|
|
501
|
+
path: {
|
|
502
|
+
posix: ["{repo}", ".github", "copilot", "agents", "{name}.md"],
|
|
503
|
+
windows: ["{repo}", ".github", "copilot", "agents", "{name}.md"],
|
|
504
|
+
},
|
|
505
|
+
},
|
|
488
506
|
policyNote: "Organization policies may override locally deployed configuration. Verify with your GitHub org admin if deployed skills or rules are not active.",
|
|
507
|
+
unsupportedSurfaces: [
|
|
508
|
+
{
|
|
509
|
+
status: "planned",
|
|
510
|
+
schemaLabel: "devcontainer.json MCP surface",
|
|
511
|
+
plannedSurface: "devcontainer.json customizations.vscode.mcp.servers",
|
|
512
|
+
reason: 'devcontainer.json MCP support is planned — use scope: "repo" or scope: "workspace" to target .vscode/mcp.json in the meantime',
|
|
513
|
+
},
|
|
514
|
+
],
|
|
489
515
|
instructionFrontmatterRequired: true,
|
|
490
516
|
enterprisePolicyDetection: true,
|
|
491
517
|
},
|
|
@@ -3,28 +3,23 @@ import { compileMcpServerActions, compileMcpServerReverts } from "./mcp.js";
|
|
|
3
3
|
import { compilePermissionsActions, compilePermissionsReverts, } from "./permissions.js";
|
|
4
4
|
import { compileAgentRuleActions, compileAgentRuleReverts } from "./rules.js";
|
|
5
5
|
export { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, };
|
|
6
|
+
async function compileAll(entries, fn) {
|
|
7
|
+
const results = await Promise.all(entries.map(fn));
|
|
8
|
+
return {
|
|
9
|
+
actions: results.flatMap((r) => r.actions),
|
|
10
|
+
warnings: results.flatMap((r) => r.warnings),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
6
13
|
export async function compileAdapterActions(mcpServers, agentRules, permissions, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, agentDefinitions, workspace) {
|
|
7
14
|
const actions = [];
|
|
8
15
|
const warnings = [];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
warnings.push(...r.warnings);
|
|
18
|
-
}
|
|
19
|
-
for (const entry of permissions) {
|
|
20
|
-
const r = compilePermissionsActions(entry, detectedAgents, home);
|
|
21
|
-
actions.push(...r.actions);
|
|
22
|
-
warnings.push(...r.warnings);
|
|
23
|
-
}
|
|
24
|
-
for (const entry of agentDefinitions ?? []) {
|
|
25
|
-
const r = await compileAgentDefinitionActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace);
|
|
26
|
-
actions.push(...r.actions);
|
|
27
|
-
warnings.push(...r.warnings);
|
|
28
|
-
}
|
|
16
|
+
const [mcp, rules, perms, defs] = await Promise.all([
|
|
17
|
+
compileAll(mcpServers, (entry) => compileMcpServerActions(entry, detectedAgents, home, repo, workspace)),
|
|
18
|
+
compileAll(agentRules, (entry) => compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace)),
|
|
19
|
+
compileAll(permissions, (entry) => compilePermissionsActions(entry, detectedAgents, home)),
|
|
20
|
+
compileAll(agentDefinitions ?? [], (entry) => compileAgentDefinitionActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace)),
|
|
21
|
+
]);
|
|
22
|
+
actions.push(...mcp.actions, ...rules.actions, ...perms.actions, ...defs.actions);
|
|
23
|
+
warnings.push(...mcp.warnings, ...rules.warnings, ...perms.warnings, ...defs.warnings);
|
|
29
24
|
return { actions, warnings };
|
|
30
25
|
}
|
package/dist/src/core/deploy.js
CHANGED
|
@@ -4,7 +4,9 @@ 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";
|
|
8
10
|
import { planCapabilityForDeploy } from "./capabilities.js";
|
|
9
11
|
import { applyMergePatch, computeUndoPatch, isPlainObject, } from "./merge-patch.js";
|
|
10
12
|
import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
|
|
@@ -105,7 +107,7 @@ function checkPairAgentRuleAmbiguities(manifest, primary, rider) {
|
|
|
105
107
|
JSON.stringify(primarySupport.path) === JSON.stringify(riderSupport.path)) {
|
|
106
108
|
warnings.push({
|
|
107
109
|
kind: "ambiguity",
|
|
108
|
-
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.`,
|
|
109
111
|
});
|
|
110
112
|
}
|
|
111
113
|
}
|
|
@@ -122,7 +124,7 @@ function checkPairMcpAmbiguities(manifest, primary, rider) {
|
|
|
122
124
|
continue;
|
|
123
125
|
warnings.push({
|
|
124
126
|
kind: "ambiguity",
|
|
125
|
-
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.`,
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
130
|
return warnings;
|
|
@@ -134,7 +136,7 @@ function checkPairAgentDefinitionAmbiguities(manifest, primary, rider) {
|
|
|
134
136
|
continue;
|
|
135
137
|
warnings.push({
|
|
136
138
|
kind: "ambiguity",
|
|
137
|
-
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.`,
|
|
138
140
|
});
|
|
139
141
|
}
|
|
140
142
|
return warnings;
|
|
@@ -162,7 +164,7 @@ function checkAntigravityPathCollisions(manifest) {
|
|
|
162
164
|
if (defNames.has(entry.name)) {
|
|
163
165
|
warnings.push({
|
|
164
166
|
kind: "collision",
|
|
165
|
-
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`,
|
|
166
168
|
});
|
|
167
169
|
}
|
|
168
170
|
}
|
|
@@ -212,17 +214,15 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
|
|
|
212
214
|
});
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
|
-
|
|
216
|
-
await planSkillEntry(skill);
|
|
217
|
-
}
|
|
217
|
+
await Promise.all(manifest.skills.map(planSkillEntry));
|
|
218
218
|
return { actions, warnings };
|
|
219
219
|
}
|
|
220
220
|
async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo, workspace) {
|
|
221
221
|
const actions = [];
|
|
222
|
-
|
|
222
|
+
await Promise.all((manifest.files ?? []).map(async (fileEntry) => {
|
|
223
223
|
const targetAgents = fileEntry.agents.filter((agentId) => detectedAgents.includes(agentId));
|
|
224
224
|
if (targetAgents.length === 0)
|
|
225
|
-
|
|
225
|
+
return;
|
|
226
226
|
const source = path.resolve(sourceDir, fileEntry.path);
|
|
227
227
|
await validateSourcePath(source, fileEntry.path, resolvedSourceDir, realRoot);
|
|
228
228
|
await validateSourceFile(source, fileEntry.path);
|
|
@@ -236,10 +236,10 @@ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, real
|
|
|
236
236
|
agent: agentId,
|
|
237
237
|
source,
|
|
238
238
|
target: resolveTargetTemplate(fileEntry.target, home, repo, workspace),
|
|
239
|
-
confidence:
|
|
239
|
+
confidence: "implementation-only",
|
|
240
240
|
});
|
|
241
241
|
}
|
|
242
|
-
}
|
|
242
|
+
}));
|
|
243
243
|
return actions;
|
|
244
244
|
}
|
|
245
245
|
function planConfigPatchActions(manifest, detectedAgents, home, repo, workspace) {
|
|
@@ -370,7 +370,7 @@ async function backupManagedFileWriteTarget(action, home, deps) {
|
|
|
370
370
|
agent: action.agent,
|
|
371
371
|
}, deps.registry);
|
|
372
372
|
if (!isOwned) {
|
|
373
|
-
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`);
|
|
374
374
|
}
|
|
375
375
|
const backupPath = `${action.target}.inception-backup`;
|
|
376
376
|
await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
|
|
@@ -528,7 +528,7 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
528
528
|
if (existingEntry &&
|
|
529
529
|
(existingEntry.skill !== action.skill ||
|
|
530
530
|
existingEntry.agent !== action.agent)) {
|
|
531
|
-
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`);
|
|
532
532
|
}
|
|
533
533
|
const original = await readJsonConfigFile(action.target);
|
|
534
534
|
const undoPatch = computeUndoPatch(original, patch);
|
|
@@ -576,7 +576,7 @@ async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
576
576
|
try {
|
|
577
577
|
const { previousValue } = await (deps.registry
|
|
578
578
|
? Promise.resolve({ previousValue: null })
|
|
579
|
-
:
|
|
579
|
+
: applyTomlMcpPatch(action.target, action.skill, action.config));
|
|
580
580
|
// Note: To truly support custom deps here we'd need to refactor toml adapter to accept deps.
|
|
581
581
|
// For now we assume standard adapter for TOML.
|
|
582
582
|
await registerDeployment(home, action.target, {
|
|
@@ -622,9 +622,8 @@ async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, dep
|
|
|
622
622
|
(existingEntry.kind !== "frontmatter-emit" ||
|
|
623
623
|
existingEntry.skill !== action.skill ||
|
|
624
624
|
existingEntry.agent !== action.agent)) {
|
|
625
|
-
throw new Error(`Frontmatter target "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.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
626
|
}
|
|
627
|
-
const frontmatterAdapter = await import("./adapters/frontmatter.js");
|
|
628
627
|
const existing = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
|
|
629
628
|
const undoPatch = computeUndoPatch(existing.attributes, action.frontmatter);
|
|
630
629
|
const patchedFrontmatter = applyMergePatch(existing.attributes, action.frontmatter);
|
|
@@ -712,7 +711,7 @@ async function assertTargetAbsent(targetPath) {
|
|
|
712
711
|
catch (err) {
|
|
713
712
|
if (err instanceof Error && err.message.startsWith("Target path appeared"))
|
|
714
713
|
throw err;
|
|
715
|
-
// ENOENT is expected
|
|
714
|
+
// ENOENT is expected - target should not exist after backup
|
|
716
715
|
}
|
|
717
716
|
}
|
|
718
717
|
async function createDeployTarget(action, home, deps) {
|
|
@@ -775,7 +774,7 @@ async function backupExisting(targetPath, verbose, home, expected, deps) {
|
|
|
775
774
|
skill: expected.skill,
|
|
776
775
|
agent: expected.agent,
|
|
777
776
|
}, deps.registry))) {
|
|
778
|
-
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`);
|
|
779
778
|
}
|
|
780
779
|
const backupPath = `${targetPath}.inception-backup`;
|
|
781
780
|
if (verbose) {
|
package/dist/src/core/detect.js
CHANGED
|
@@ -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);
|
package/dist/src/core/init.js
CHANGED
|
@@ -521,7 +521,8 @@ function logPathAgentEntries(label, entries) {
|
|
|
521
521
|
return;
|
|
522
522
|
logger.detail(`${label}:`);
|
|
523
523
|
for (const e of entries) {
|
|
524
|
-
|
|
524
|
+
const entryAgents = e.agents ?? [];
|
|
525
|
+
logger.detail(` ${e.name} → ${e.path} [${entryAgents.join(", ")}]`);
|
|
525
526
|
}
|
|
526
527
|
}
|
|
527
528
|
function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agentDefinitions) {
|
|
@@ -155,12 +155,22 @@ async function detectInstructionBudgetRisk(detectedAgents, manifest, sourceDir)
|
|
|
155
155
|
// will produce the proper UserError during action compilation.
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
const entries = [];
|
|
158
159
|
for (const entry of manifest.agentRules ?? []) {
|
|
159
|
-
|
|
160
|
+
entries.push({
|
|
161
|
+
path: entry.path,
|
|
162
|
+
label: "agentRules",
|
|
163
|
+
agents: entry.agents,
|
|
164
|
+
});
|
|
160
165
|
}
|
|
161
166
|
for (const entry of manifest.agentDefinitions ?? []) {
|
|
162
|
-
|
|
167
|
+
entries.push({
|
|
168
|
+
path: entry.path,
|
|
169
|
+
label: "agentDefinitions",
|
|
170
|
+
agents: entry.agents,
|
|
171
|
+
});
|
|
163
172
|
}
|
|
173
|
+
await Promise.all(entries.map((e) => checkEntry(e.path, e.label, e.agents)));
|
|
164
174
|
return warnings;
|
|
165
175
|
}
|
|
166
176
|
function pushCapabilityWarning(acc, kind, message) {
|
|
@@ -169,6 +179,21 @@ function pushCapabilityWarning(acc, kind, message) {
|
|
|
169
179
|
acc.seen.add(`${kind}:${message}`);
|
|
170
180
|
acc.warnings.push({ kind, message });
|
|
171
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
|
+
}
|
|
172
197
|
function collectCapabilityWarningsForAgent(acc, agentId, capability, entryName, targetAgents, scope) {
|
|
173
198
|
const plan = planCapabilityForDeploy({
|
|
174
199
|
agentId,
|
|
@@ -181,24 +206,13 @@ function collectCapabilityWarningsForAgent(acc, agentId, capability, entryName,
|
|
|
181
206
|
pushCapabilityWarning(acc, "info", plan.warning.message);
|
|
182
207
|
return;
|
|
183
208
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (surface.status === "planned") {
|
|
189
|
-
pushCapabilityWarning(acc, "info", `Agent "${agentId}": ${surface.reason ?? surface.plannedSurface}`);
|
|
190
|
-
}
|
|
191
|
-
else if (surface.status === "unsupported") {
|
|
192
|
-
pushCapabilityWarning(acc, "config-authority", `Agent "${agentId}": ${surface.reason ?? surface.schemaLabel}`);
|
|
193
|
-
}
|
|
209
|
+
if (capability !== "skills") {
|
|
210
|
+
const confidence = describeCapabilityConfidence(agentId, capability, scope);
|
|
211
|
+
if (confidence.message) {
|
|
212
|
+
pushCapabilityWarning(acc, "config-authority", confidence.message);
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
|
-
|
|
197
|
-
return;
|
|
198
|
-
const confidence = describeCapabilityConfidence(agentId, capability, scope);
|
|
199
|
-
if (confidence.message) {
|
|
200
|
-
pushCapabilityWarning(acc, "config-authority", confidence.message);
|
|
201
|
-
}
|
|
215
|
+
collectPlannedSurfaceWarnings(acc, agentId, capability);
|
|
202
216
|
}
|
|
203
217
|
function collectCapabilityWarningsForTargets(acc, targetAgents, capability, entryName, scope) {
|
|
204
218
|
for (const agentId of targetAgents) {
|
|
@@ -252,7 +266,9 @@ async function collectAgentWarnings(agentId, manifest, home) {
|
|
|
252
266
|
});
|
|
253
267
|
}
|
|
254
268
|
else if (agent.policyNote &&
|
|
255
|
-
!agent.policyNote
|
|
269
|
+
!agent.policyNote
|
|
270
|
+
.toLowerCase()
|
|
271
|
+
.includes("organization policies may override")) {
|
|
256
272
|
warnings.push({
|
|
257
273
|
kind: "policy",
|
|
258
274
|
message: `Agent "${agentId}": ${agent.policyNote}`,
|
package/dist/src/core/revert.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
|
+
import * 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";
|
|
6
7
|
import { applyUndoPatch } from "./merge-patch.js";
|
|
@@ -177,7 +178,6 @@ function planFrontmatterRevert(action, frontmatterEntry, planned) {
|
|
|
177
178
|
return { outcome: "ok" };
|
|
178
179
|
}
|
|
179
180
|
async function applyFrontmatterRevert(action, frontmatterEntry) {
|
|
180
|
-
const frontmatterAdapter = await import("./adapters/frontmatter.js");
|
|
181
181
|
const current = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
|
|
182
182
|
const restoredFrontmatter = applyUndoPatch(current.attributes, frontmatterEntry.undoPatch ?? {});
|
|
183
183
|
const shouldDeleteFile = frontmatterEntry.created === true &&
|
|
@@ -218,6 +218,17 @@ function validateGithubCopilotRequirements(attributes, manifestPath) {
|
|
|
218
218
|
if (!(hasTools || hasInstructions)) {
|
|
219
219
|
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "github-copilot" must define "tools" or "instructions" in frontmatter`);
|
|
220
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
|
+
}
|
|
221
232
|
}
|
|
222
233
|
function validateAntigravityRequirements(attributes, manifestPath) {
|
|
223
234
|
const mcpServers = attributes["mcp-servers"] ?? attributes.mcpServers;
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/src/errors.js
CHANGED
package/dist/src/types.d.ts
CHANGED
|
@@ -102,6 +102,8 @@ export interface AgentConfig {
|
|
|
102
102
|
agentDefinitionsWorkspaceSupport?: AgentSurfaceSupport;
|
|
103
103
|
agentDefinitionsTomlSupport?: AgentSurfaceSupport;
|
|
104
104
|
agentDefinitionsTomlRepoSupport?: AgentSurfaceSupport;
|
|
105
|
+
mcpDevcontainerSupport?: AgentSurfaceSupport;
|
|
106
|
+
mcpAgentFrontmatterSupport?: AgentSurfaceSupport;
|
|
105
107
|
policyNote?: string;
|
|
106
108
|
/**
|
|
107
109
|
* A list of documented surfaces that are currently not safe to support directly.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import { executeDeploy } from "../../../src/core/deploy.js";
|
|
6
|
+
import { lookupDeployment, registerDeployment, } from "../../../src/core/ownership.js";
|
|
7
|
+
import { executeRevert } from "../../../src/core/revert.js";
|
|
8
|
+
import { exists, makeTmpDir } from "../../helpers/fs.js";
|
|
9
|
+
describe("agentRules repo/workspace file-write (Windows)", {
|
|
10
|
+
skip: process.platform !== "win32",
|
|
11
|
+
}, () => {
|
|
12
|
+
it("deploys agentRules with scope repo and registers it", async () => {
|
|
13
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
14
|
+
const repo = await makeTmpDir("ie-rules-repo");
|
|
15
|
+
const sourceDir = await makeTmpDir("ie-rules-source");
|
|
16
|
+
try {
|
|
17
|
+
const source = path.join(sourceDir, "CLAUDE.md");
|
|
18
|
+
await writeFile(source, "# My Rules\n");
|
|
19
|
+
const target = path.join(repo, "CLAUDE.md");
|
|
20
|
+
const action = {
|
|
21
|
+
kind: "file-write",
|
|
22
|
+
skill: "my-rules",
|
|
23
|
+
agent: "claude-code",
|
|
24
|
+
source,
|
|
25
|
+
target,
|
|
26
|
+
};
|
|
27
|
+
const { succeeded, failed } = await executeDeploy([action], false, false, home);
|
|
28
|
+
assert.equal(succeeded, 1);
|
|
29
|
+
assert.equal(failed.length, 0);
|
|
30
|
+
assert.ok(await exists(target));
|
|
31
|
+
const entry = await lookupDeployment(home, target);
|
|
32
|
+
assert.ok(entry !== null);
|
|
33
|
+
assert.equal(entry?.kind, "file-write");
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await rm(home, { recursive: true, force: true });
|
|
37
|
+
await rm(repo, { recursive: true, force: true });
|
|
38
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
it("reverts agentRules with scope repo and unregisters it", async () => {
|
|
42
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
43
|
+
const repo = await makeTmpDir("ie-rules-repo");
|
|
44
|
+
try {
|
|
45
|
+
const source = path.join(repo, "source-CLAUDE.md");
|
|
46
|
+
const target = path.join(repo, "CLAUDE.md");
|
|
47
|
+
await writeFile(target, "# My Rules\n");
|
|
48
|
+
await registerDeployment(home, target, {
|
|
49
|
+
kind: "file-write",
|
|
50
|
+
source,
|
|
51
|
+
skill: "my-rules",
|
|
52
|
+
agent: "claude-code",
|
|
53
|
+
});
|
|
54
|
+
const action = {
|
|
55
|
+
kind: "file-write",
|
|
56
|
+
skill: "my-rules",
|
|
57
|
+
agent: "claude-code",
|
|
58
|
+
target,
|
|
59
|
+
};
|
|
60
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
61
|
+
assert.equal(succeeded, 1);
|
|
62
|
+
assert.equal(failed.length, 0);
|
|
63
|
+
assert.ok(!(await exists(target)));
|
|
64
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(home, { recursive: true, force: true });
|
|
68
|
+
await rm(repo, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
it("deploys agentRules with scope workspace and registers it", async () => {
|
|
72
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
73
|
+
const workspace = await makeTmpDir("ie-rules-workspace");
|
|
74
|
+
const sourceDir = await makeTmpDir("ie-rules-source");
|
|
75
|
+
try {
|
|
76
|
+
const source = path.join(sourceDir, "CLAUDE.md");
|
|
77
|
+
await writeFile(source, "# Workspace Rules\n");
|
|
78
|
+
const target = path.join(workspace, "CLAUDE.md");
|
|
79
|
+
const action = {
|
|
80
|
+
kind: "file-write",
|
|
81
|
+
skill: "my-rules",
|
|
82
|
+
agent: "claude-code",
|
|
83
|
+
source,
|
|
84
|
+
target,
|
|
85
|
+
};
|
|
86
|
+
const { succeeded, failed } = await executeDeploy([action], false, false, home);
|
|
87
|
+
assert.equal(succeeded, 1);
|
|
88
|
+
assert.equal(failed.length, 0);
|
|
89
|
+
assert.ok(await exists(target));
|
|
90
|
+
const entry = await lookupDeployment(home, target);
|
|
91
|
+
assert.ok(entry !== null);
|
|
92
|
+
assert.equal(entry?.kind, "file-write");
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await rm(home, { recursive: true, force: true });
|
|
96
|
+
await rm(workspace, { recursive: true, force: true });
|
|
97
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
it("reverts agentRules with scope workspace and unregisters it", async () => {
|
|
101
|
+
const home = await makeTmpDir("ie-rules-home");
|
|
102
|
+
const workspace = await makeTmpDir("ie-rules-workspace");
|
|
103
|
+
try {
|
|
104
|
+
const source = path.join(workspace, "source-CLAUDE.md");
|
|
105
|
+
const target = path.join(workspace, "CLAUDE.md");
|
|
106
|
+
await writeFile(target, "# Workspace Rules\n");
|
|
107
|
+
await registerDeployment(home, target, {
|
|
108
|
+
kind: "file-write",
|
|
109
|
+
source,
|
|
110
|
+
skill: "my-rules",
|
|
111
|
+
agent: "claude-code",
|
|
112
|
+
});
|
|
113
|
+
const action = {
|
|
114
|
+
kind: "file-write",
|
|
115
|
+
skill: "my-rules",
|
|
116
|
+
agent: "claude-code",
|
|
117
|
+
target,
|
|
118
|
+
};
|
|
119
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
120
|
+
assert.equal(succeeded, 1);
|
|
121
|
+
assert.equal(failed.length, 0);
|
|
122
|
+
assert.ok(!(await exists(target)));
|
|
123
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await rm(home, { recursive: true, force: true });
|
|
127
|
+
await rm(workspace, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("Antigravity frontmatter-emit (Windows)", {
|
|
132
|
+
skip: process.platform !== "win32",
|
|
133
|
+
}, () => {
|
|
134
|
+
it("deploys frontmatter-emit for Antigravity and registers it", async () => {
|
|
135
|
+
const home = await makeTmpDir("ie-fm-home");
|
|
136
|
+
const repo = await makeTmpDir("ie-fm-repo");
|
|
137
|
+
try {
|
|
138
|
+
const target = path.join(repo, ".agents", "rules", "my-mcp.md");
|
|
139
|
+
const frontmatter = {
|
|
140
|
+
"mcp-servers": {
|
|
141
|
+
"my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const action = {
|
|
145
|
+
kind: "frontmatter-emit",
|
|
146
|
+
skill: "my-mcp",
|
|
147
|
+
agent: "antigravity",
|
|
148
|
+
target,
|
|
149
|
+
frontmatter,
|
|
150
|
+
};
|
|
151
|
+
const { succeeded, failed } = await executeDeploy([action], false, false, home);
|
|
152
|
+
assert.equal(succeeded, 1);
|
|
153
|
+
assert.equal(failed.length, 0);
|
|
154
|
+
assert.ok(await exists(target));
|
|
155
|
+
const content = await readFile(target, "utf-8");
|
|
156
|
+
assert.ok(content.includes("mcp-servers:"));
|
|
157
|
+
const entry = await lookupDeployment(home, target);
|
|
158
|
+
assert.ok(entry !== null);
|
|
159
|
+
assert.equal(entry?.kind, "frontmatter-emit");
|
|
160
|
+
assert.equal(entry.created, true);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await rm(home, { recursive: true, force: true });
|
|
164
|
+
await rm(repo, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
it("reverts frontmatter-emit for Antigravity and removes file when no body", async () => {
|
|
168
|
+
const home = await makeTmpDir("ie-fm-home");
|
|
169
|
+
const repo = await makeTmpDir("ie-fm-repo");
|
|
170
|
+
try {
|
|
171
|
+
const target = path.join(repo, ".agents", "rules", "my-mcp.md");
|
|
172
|
+
const frontmatter = {
|
|
173
|
+
"mcp-servers": {
|
|
174
|
+
"my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
// Deploy first so the file and registry entry are created.
|
|
178
|
+
const deployAction = {
|
|
179
|
+
kind: "frontmatter-emit",
|
|
180
|
+
skill: "my-mcp",
|
|
181
|
+
agent: "antigravity",
|
|
182
|
+
target,
|
|
183
|
+
frontmatter,
|
|
184
|
+
};
|
|
185
|
+
await executeDeploy([deployAction], false, false, home);
|
|
186
|
+
const action = {
|
|
187
|
+
kind: "frontmatter-emit",
|
|
188
|
+
skill: "my-mcp",
|
|
189
|
+
agent: "antigravity",
|
|
190
|
+
target,
|
|
191
|
+
};
|
|
192
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
193
|
+
assert.equal(succeeded, 1);
|
|
194
|
+
assert.equal(failed.length, 0);
|
|
195
|
+
assert.ok(!(await exists(target)));
|
|
196
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await rm(home, { recursive: true, force: true });
|
|
200
|
+
await rm(repo, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
it("reverts frontmatter-emit for Antigravity preserving body when file had prior content", async () => {
|
|
204
|
+
const home = await makeTmpDir("ie-fm-home");
|
|
205
|
+
const repo = await makeTmpDir("ie-fm-repo");
|
|
206
|
+
try {
|
|
207
|
+
const target = path.join(repo, ".agents", "rules", "my-mcp.md");
|
|
208
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
209
|
+
await writeFile(target, "# My Rules\n");
|
|
210
|
+
const undoPatch = { "mcp-servers": null };
|
|
211
|
+
await registerDeployment(home, target, {
|
|
212
|
+
kind: "frontmatter-emit",
|
|
213
|
+
patch: {
|
|
214
|
+
"mcp-servers": {
|
|
215
|
+
"my-mcp": { command: "npx", args: ["-y", "my-mcp-server"] },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
undoPatch,
|
|
219
|
+
created: false,
|
|
220
|
+
hadFrontmatter: false,
|
|
221
|
+
skill: "my-mcp",
|
|
222
|
+
agent: "antigravity",
|
|
223
|
+
});
|
|
224
|
+
const action = {
|
|
225
|
+
kind: "frontmatter-emit",
|
|
226
|
+
skill: "my-mcp",
|
|
227
|
+
agent: "antigravity",
|
|
228
|
+
target,
|
|
229
|
+
};
|
|
230
|
+
const { succeeded, failed } = await executeRevert([action], false, false, home);
|
|
231
|
+
assert.equal(succeeded, 1);
|
|
232
|
+
assert.equal(failed.length, 0);
|
|
233
|
+
// File should still exist because it had body content.
|
|
234
|
+
assert.ok(await exists(target));
|
|
235
|
+
const content = await readFile(target, "utf-8");
|
|
236
|
+
assert.ok(!content.includes("mcp-servers:"));
|
|
237
|
+
assert.ok(content.includes("My Rules"));
|
|
238
|
+
assert.equal(await lookupDeployment(home, target), null);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await rm(home, { recursive: true, force: true });
|
|
242
|
+
await rm(repo, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -910,6 +910,76 @@ describe("compileAgentDefinitionActions", () => {
|
|
|
910
910
|
await rm(dir, { recursive: true });
|
|
911
911
|
}
|
|
912
912
|
});
|
|
913
|
+
it("accepts github-copilot tools as empty array", async () => {
|
|
914
|
+
const compile = await getAdapter();
|
|
915
|
+
const dir = await makeTmpDir();
|
|
916
|
+
try {
|
|
917
|
+
await writeFile(path.join(dir, "agent.md"), "---\nname: test\ndescription: test\ntools: []\n---\n# Agent");
|
|
918
|
+
const realRoot = await realpath(dir);
|
|
919
|
+
const result = await compile({
|
|
920
|
+
name: "agent",
|
|
921
|
+
agents: ["github-copilot"],
|
|
922
|
+
path: "agent.md",
|
|
923
|
+
scope: "repo",
|
|
924
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test");
|
|
925
|
+
assert.ok(result.actions.length > 0, "expected at least one action");
|
|
926
|
+
}
|
|
927
|
+
finally {
|
|
928
|
+
await rm(dir, { recursive: true });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
it("accepts github-copilot tools as array of strings", async () => {
|
|
932
|
+
const compile = await getAdapter();
|
|
933
|
+
const dir = await makeTmpDir();
|
|
934
|
+
try {
|
|
935
|
+
await writeFile(path.join(dir, "agent.md"), "---\nname: test\ndescription: test\ntools:\n - github\n - codebase\n---\n# Agent");
|
|
936
|
+
const realRoot = await realpath(dir);
|
|
937
|
+
const result = await compile({
|
|
938
|
+
name: "agent",
|
|
939
|
+
agents: ["github-copilot"],
|
|
940
|
+
path: "agent.md",
|
|
941
|
+
scope: "repo",
|
|
942
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test");
|
|
943
|
+
assert.ok(result.actions.length > 0, "expected at least one action");
|
|
944
|
+
}
|
|
945
|
+
finally {
|
|
946
|
+
await rm(dir, { recursive: true });
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
it("throws for github-copilot when tools is not an array", async () => {
|
|
950
|
+
const compile = await getAdapter();
|
|
951
|
+
const dir = await makeTmpDir();
|
|
952
|
+
try {
|
|
953
|
+
await writeFile(path.join(dir, "bad-tools.md"), "---\nname: test\ndescription: test\ntools: not-an-array\n---\n# Bad");
|
|
954
|
+
const realRoot = await realpath(dir);
|
|
955
|
+
await assert.rejects(compile({
|
|
956
|
+
name: "bad",
|
|
957
|
+
agents: ["github-copilot"],
|
|
958
|
+
path: "bad-tools.md",
|
|
959
|
+
scope: "repo",
|
|
960
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test"), /"tools" field that must be an array/);
|
|
961
|
+
}
|
|
962
|
+
finally {
|
|
963
|
+
await rm(dir, { recursive: true });
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
it("throws for github-copilot when tools contains a non-string entry", async () => {
|
|
967
|
+
const compile = await getAdapter();
|
|
968
|
+
const dir = await makeTmpDir();
|
|
969
|
+
try {
|
|
970
|
+
await writeFile(path.join(dir, "bad-tools.md"), "---\nname: test\ndescription: test\ntools:\n - github\n - 123\n---\n# Bad");
|
|
971
|
+
const realRoot = await realpath(dir);
|
|
972
|
+
await assert.rejects(compile({
|
|
973
|
+
name: "bad",
|
|
974
|
+
agents: ["github-copilot"],
|
|
975
|
+
path: "bad-tools.md",
|
|
976
|
+
scope: "repo",
|
|
977
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", "/repo/test"), /"tools" entry that must be a string/);
|
|
978
|
+
}
|
|
979
|
+
finally {
|
|
980
|
+
await rm(dir, { recursive: true });
|
|
981
|
+
}
|
|
982
|
+
});
|
|
913
983
|
it("throws for antigravity when mcp-servers is malformed", async () => {
|
|
914
984
|
const compile = await getAdapter();
|
|
915
985
|
const dir = await makeTmpDir();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { cp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { makeTmpDir } from "../helpers/fs.js";
|
|
7
|
+
import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
|
|
8
|
+
const PROJECT_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
9
|
+
const FIXTURE_DIR = path.join(PROJECT_ROOT, "test", "fixtures", "readme-sample");
|
|
10
|
+
function run(args, env) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const child = spawn(process.execPath, [path.join(PROJECT_ROOT, "src", "index.ts"), ...args], {
|
|
13
|
+
cwd: PROJECT_ROOT,
|
|
14
|
+
env: { ...process.env, ...env },
|
|
15
|
+
});
|
|
16
|
+
let stdout = "";
|
|
17
|
+
let stderr = "";
|
|
18
|
+
child.stdout.on("data", (d) => {
|
|
19
|
+
stdout += d.toString();
|
|
20
|
+
});
|
|
21
|
+
child.stderr.on("data", (d) => {
|
|
22
|
+
stderr += d.toString();
|
|
23
|
+
});
|
|
24
|
+
child.on("close", (code) => {
|
|
25
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract the JSON manifest object from `init --plan` stdout.
|
|
31
|
+
* The output format is:
|
|
32
|
+
* [plan] Would write /path/inception.json with N skill(s), ...:
|
|
33
|
+
* <blank line>
|
|
34
|
+
* { ...json... }
|
|
35
|
+
* The [plan] prefix may include ANSI escape codes; the first `{` is
|
|
36
|
+
* reliably the start of the JSON object.
|
|
37
|
+
*/
|
|
38
|
+
function extractPlanJson(stdout) {
|
|
39
|
+
const jsonStart = stdout.indexOf("{");
|
|
40
|
+
assert.ok(jsonStart !== -1, `Could not find JSON object in --plan stdout:\n${stdout}`);
|
|
41
|
+
return JSON.parse(stdout.slice(jsonStart));
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Block 1: init --plan against the real limbo/ tree (read-only)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
describe("init --plan against real limbo/ tree", () => {
|
|
47
|
+
it("emits the 4 limbo skills with all 5 agents and empty other sections", async () => {
|
|
48
|
+
const limboDir = path.join(PROJECT_ROOT, "limbo");
|
|
49
|
+
const { stdout, code } = await run(["init", limboDir, "--plan"]);
|
|
50
|
+
assert.equal(code, 0, `init --plan exited non-zero.\nstdout: ${stdout}`);
|
|
51
|
+
assert.ok(stdout.includes("[plan]"), `missing [plan] prefix:\n${stdout}`);
|
|
52
|
+
const manifest = extractPlanJson(stdout);
|
|
53
|
+
// Exactly 4 skills
|
|
54
|
+
assert.equal(manifest.skills.length, 4, `expected 4 skills, got ${manifest.skills.length}: ${JSON.stringify(manifest.skills.map((s) => s.name))}`);
|
|
55
|
+
// Names sorted alphabetically (sort before comparing - filesystem order not guaranteed)
|
|
56
|
+
const names = manifest.skills.map((s) => s.name).sort();
|
|
57
|
+
assert.deepEqual(names, [
|
|
58
|
+
"inception",
|
|
59
|
+
"interstellar",
|
|
60
|
+
"tenet",
|
|
61
|
+
"the-prestige",
|
|
62
|
+
]);
|
|
63
|
+
// Each skill has exactly the 5 portability agents (github-copilot excluded)
|
|
64
|
+
const expectedAgents = [
|
|
65
|
+
"antigravity",
|
|
66
|
+
"claude-code",
|
|
67
|
+
"codex",
|
|
68
|
+
"gemini-cli",
|
|
69
|
+
"opencode",
|
|
70
|
+
];
|
|
71
|
+
for (const skill of manifest.skills) {
|
|
72
|
+
assert.deepEqual(skill.agents.slice().sort(), expectedAgents, `skill "${skill.name}" agents mismatch: ${JSON.stringify(skill.agents)}`);
|
|
73
|
+
assertPathEndsWith(skill.path, `skills/${skill.name}`, `skill "${skill.name}" path should end with skills/${skill.name}`);
|
|
74
|
+
}
|
|
75
|
+
// All other sections are empty arrays
|
|
76
|
+
assert.deepEqual(manifest.mcpServers, [], "mcpServers should be []");
|
|
77
|
+
assert.deepEqual(manifest.agentRules, [], "agentRules should be []");
|
|
78
|
+
assert.deepEqual(manifest.agentDefinitions, [], "agentDefinitions should be []");
|
|
79
|
+
assert.deepEqual(manifest.files, [], "files should be []");
|
|
80
|
+
assert.deepEqual(manifest.configs, [], "configs should be []");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Block 2: init against README-shaped fixture (copied to temp dir)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
describe("init against readme-sample fixture", () => {
|
|
87
|
+
it("generates a manifest covering all README-documented init discovery paths", async () => {
|
|
88
|
+
const tmpDir = await makeTmpDir("ie-fixture-readme");
|
|
89
|
+
try {
|
|
90
|
+
// Copy the static fixture tree (including .claude/ hidden dir) into tmpDir
|
|
91
|
+
await cp(FIXTURE_DIR, tmpDir, { recursive: true });
|
|
92
|
+
const { stdout, code } = await run(["init", tmpDir]);
|
|
93
|
+
assert.equal(code, 0, `init exited non-zero.\nstdout: ${stdout}`);
|
|
94
|
+
const manifest = JSON.parse(await readFile(path.join(tmpDir, "inception.json"), "utf-8"));
|
|
95
|
+
// --- skills: 2 entries, github-copilot excluded per portability rules ---
|
|
96
|
+
assert.equal(manifest.skills.length, 2, `expected 2 skills, got ${manifest.skills.length}: ${JSON.stringify(manifest.skills.map((s) => s.name))}`);
|
|
97
|
+
const skillNames = manifest.skills.map((s) => s.name).sort();
|
|
98
|
+
assert.deepEqual(skillNames, ["my-skill", "other-skill"]);
|
|
99
|
+
const expectedSkillAgents = [
|
|
100
|
+
"antigravity",
|
|
101
|
+
"claude-code",
|
|
102
|
+
"codex",
|
|
103
|
+
"gemini-cli",
|
|
104
|
+
"opencode",
|
|
105
|
+
];
|
|
106
|
+
for (const skill of manifest.skills) {
|
|
107
|
+
assert.deepEqual(skill.agents.slice().sort(), expectedSkillAgents, `skill "${skill.name}" should have all 5 agents (not github-copilot): ${JSON.stringify(skill.agents)}`);
|
|
108
|
+
assertPathEndsWith(skill.path, `skills/${skill.name}`, `skill "${skill.name}" path should end with skills/${skill.name}`);
|
|
109
|
+
}
|
|
110
|
+
// --- agentRules: 3 entries for CLAUDE.md, AGENTS.md, GEMINI.md ---
|
|
111
|
+
assert.equal(manifest.agentRules.length, 3, `expected 3 agentRules, got ${manifest.agentRules.length}: ${JSON.stringify(manifest.agentRules.map((r) => r.name))}`);
|
|
112
|
+
const claudeRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("CLAUDE.md"));
|
|
113
|
+
assert.ok(claudeRule, "should have an agentRules entry for CLAUDE.md");
|
|
114
|
+
assert.deepEqual(claudeRule?.agents.slice().sort(), ["claude-code"], `CLAUDE.md agents: ${JSON.stringify(claudeRule?.agents)}`);
|
|
115
|
+
assert.equal(claudeRule?.scope, "global");
|
|
116
|
+
const agentsRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("AGENTS.md"));
|
|
117
|
+
assert.ok(agentsRule, "should have an agentRules entry for AGENTS.md");
|
|
118
|
+
assert.deepEqual(agentsRule?.agents.slice().sort(), ["codex", "opencode"], `AGENTS.md agents: ${JSON.stringify(agentsRule?.agents)}`);
|
|
119
|
+
assert.equal(agentsRule?.scope, "global");
|
|
120
|
+
const geminiRule = manifest.agentRules.find((r) => normalizeSlashes(r.path).endsWith("GEMINI.md"));
|
|
121
|
+
assert.ok(geminiRule, "should have an agentRules entry for GEMINI.md");
|
|
122
|
+
assert.deepEqual(geminiRule?.agents.slice().sort(), ["gemini-cli"],
|
|
123
|
+
// antigravity is shared-via gemini-cli and excluded from init defaults
|
|
124
|
+
`GEMINI.md agents should be [gemini-cli] only: ${JSON.stringify(geminiRule?.agents)}`);
|
|
125
|
+
assert.equal(geminiRule?.scope, "global");
|
|
126
|
+
// --- mcpServers: 1 entry round-tripped from mcp-servers.json sidecar ---
|
|
127
|
+
assert.equal(manifest.mcpServers.length, 1, `expected 1 mcpServer, got ${manifest.mcpServers.length}`);
|
|
128
|
+
const mcp = manifest.mcpServers[0];
|
|
129
|
+
assert.equal(mcp.name, "my-mcp-server");
|
|
130
|
+
assert.deepEqual(mcp.agents.slice().sort(), [
|
|
131
|
+
"claude-code",
|
|
132
|
+
"codex",
|
|
133
|
+
"gemini-cli",
|
|
134
|
+
"opencode",
|
|
135
|
+
]);
|
|
136
|
+
assert.equal(mcp.config.command, "npx");
|
|
137
|
+
assert.deepEqual(mcp.config.args, ["-y", "@example/mcp-server"]);
|
|
138
|
+
// --- agentDefinitions: 1 entry from .claude/agents/ -> claude-code ---
|
|
139
|
+
assert.equal(manifest.agentDefinitions.length, 1, `expected 1 agentDefinition, got ${manifest.agentDefinitions.length}`);
|
|
140
|
+
const def = manifest.agentDefinitions[0];
|
|
141
|
+
assert.equal(def.name, "code-reviewer");
|
|
142
|
+
assert.deepEqual(def.agents, ["claude-code"]);
|
|
143
|
+
assert.equal(def.scope, "repo");
|
|
144
|
+
assertPathEndsWith(def.path, ".claude/agents/code-reviewer.md", "code-reviewer path should end with .claude/agents/code-reviewer.md");
|
|
145
|
+
// --- files and configs stay empty (no sidecar manifests for them) ---
|
|
146
|
+
assert.deepEqual(manifest.files, [], "files should be []");
|
|
147
|
+
assert.deepEqual(manifest.configs, [], "configs should be []");
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await rm(tmpDir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -125,6 +125,26 @@ describe("runPreflight", () => {
|
|
|
125
125
|
assert.equal(implementationOnlyWarning, undefined, `expected no implementation-only warning, got: ${implementationOnlyWarning?.message}`);
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
describe("github-copilot planned surfaces", () => {
|
|
129
|
+
it("emits a planned surface info notice for devcontainer MCP when Copilot is detected and MCP is targeted", async () => {
|
|
130
|
+
const manifestWithMcp = {
|
|
131
|
+
...emptyManifest,
|
|
132
|
+
mcpServers: [
|
|
133
|
+
{
|
|
134
|
+
name: "test-mcp",
|
|
135
|
+
scope: "repo",
|
|
136
|
+
agents: ["github-copilot"],
|
|
137
|
+
config: { command: "node", args: ["server.js"] },
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
const warnings = await runPreflight(baseOptions, manifestWithMcp, "/home/test", ["github-copilot"]);
|
|
142
|
+
const plannedWarning = warnings.find((w) => w.kind === "info" &&
|
|
143
|
+
/devcontainer\.json MCP support is planned/.test(w.message));
|
|
144
|
+
assert.ok(plannedWarning, "expected a planned surface warning for Copilot devcontainer when MCP is targeted");
|
|
145
|
+
assert.match(plannedWarning.message, /planned/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
128
148
|
describe("instruction precedence warnings", () => {
|
|
129
149
|
it("emits duplicate-content precedence warning when same source used in both global and repo scope for an agent", async () => {
|
|
130
150
|
const sourceDir = await makeTmpDir();
|