@kuznai/inception-engine 0.22.0 → 0.24.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 -9
- package/dist/src/config/agents.js +39 -0
- package/dist/src/core/adapters/execution-config.d.ts +14 -0
- package/dist/src/core/adapters/execution-config.js +60 -0
- package/dist/src/core/adapters/index.d.ts +4 -3
- package/dist/src/core/adapters/index.js +7 -5
- package/dist/src/core/adapters/rules.js +50 -19
- package/dist/src/core/capabilities.d.ts +1 -1
- package/dist/src/core/capabilities.js +18 -2
- package/dist/src/core/deploy.js +66 -2
- package/dist/src/core/init.js +69 -13
- package/dist/src/core/ownership.d.ts +1 -0
- package/dist/src/core/ownership.js +34 -4
- package/dist/src/core/preflight.js +28 -0
- package/dist/src/core/revert.js +86 -18
- package/dist/src/core/validation.d.ts +1 -1
- package/dist/src/core/validation.js +56 -3
- package/dist/src/schemas/manifest.d.ts +31 -0
- package/dist/src/schemas/manifest.js +39 -3
- package/dist/src/types.d.ts +6 -2
- package/dist/test/helpers/path.d.ts +2 -2
- package/dist/test/helpers/path.js +3 -3
- package/dist/test/os/posix/deploy.test.js +395 -0
- package/dist/test/os/posix/revert.test.js +188 -0
- package/dist/test/os/windows/deploy.test.d.ts +1 -0
- package/dist/test/os/windows/deploy.test.js +238 -0
- package/dist/test/unit/adapters.test.js +402 -2
- package/dist/test/unit/capabilities.test.js +44 -1
- package/dist/test/unit/cli.test.js +162 -0
- package/dist/test/unit/deploy.test.js +88 -647
- package/dist/test/unit/formatters.test.d.ts +1 -0
- package/dist/test/unit/formatters.test.js +74 -0
- package/dist/test/unit/init-fixture.test.js +85 -1
- package/dist/test/unit/manifest.test.js +72 -0
- package/dist/test/unit/ownership.test.js +61 -4
- package/dist/test/unit/preflight.test.js +145 -0
- package/dist/test/unit/resolve.test.js +67 -1
- package/dist/test/unit/revert.test.js +81 -182
- package/package.json +1 -1
- package/dist/test/os/windows/agentRules-integration.test.js +0 -245
- package/dist/test/os/windows/revert-integration.test.js +0 -72
- /package/dist/test/os/{windows/agentRules-integration.test.d.ts → posix/deploy.test.d.ts} +0 -0
- /package/dist/test/os/{windows/revert-integration.test.d.ts → posix/revert.test.d.ts} +0 -0
package/dist/src/core/init.js
CHANGED
|
@@ -32,9 +32,10 @@ const AGENT_RULES_FILE_PATTERNS = (() => {
|
|
|
32
32
|
fileNames: [filename, filename.replace(".md", "-instructions.md")],
|
|
33
33
|
agents,
|
|
34
34
|
})),
|
|
35
|
-
// Convention mapping
|
|
36
|
-
//
|
|
37
|
-
//
|
|
35
|
+
// Convention mapping for copilot-instructions.md found outside .github/:
|
|
36
|
+
// map to claude-code since Copilot reads CLAUDE.md natively. Files at
|
|
37
|
+
// .github/copilot-instructions.md are handled separately as a native
|
|
38
|
+
// Copilot surface via the copilot-repo scope.
|
|
38
39
|
{
|
|
39
40
|
fileNames: ["copilot-instructions.md"],
|
|
40
41
|
agents: ["claude-code"],
|
|
@@ -183,18 +184,73 @@ async function findAgentRulesCandidates(baseDir, skillDirRelPaths) {
|
|
|
183
184
|
for (const subdir of AGENT_RULES_SUBDIRS) {
|
|
184
185
|
await scanDirForMarkdown(path.join(baseDir, subdir), baseDir, skillDirRelPaths, seen, candidates);
|
|
185
186
|
}
|
|
187
|
+
// Promote .github/copilot-instructions.md to the native copilot-repo scope.
|
|
188
|
+
// The general scanDirForMarkdown pass above picks it up via AGENT_RULES_SUBDIRS
|
|
189
|
+
// (".github"), so we just patch the candidate that was already added.
|
|
190
|
+
const copilotRepoRelPath = ".github/copilot-instructions.md";
|
|
191
|
+
const copilotRepoCandidate = candidates.find((c) => c.relPath === copilotRepoRelPath);
|
|
192
|
+
if (copilotRepoCandidate) {
|
|
193
|
+
copilotRepoCandidate.defaultAgents = ["github-copilot"];
|
|
194
|
+
copilotRepoCandidate.scope = "copilot-repo";
|
|
195
|
+
}
|
|
196
|
+
// Discover .github/instructions/*.instructions.md as copilot-scoped entries.
|
|
197
|
+
// These are not covered by the general scan (it only looks for .md/.markdown
|
|
198
|
+
// and the stem derivation would lose the .instructions suffix).
|
|
199
|
+
const instructionsDir = path.join(baseDir, ".github", "instructions");
|
|
200
|
+
let instrEntries;
|
|
201
|
+
try {
|
|
202
|
+
instrEntries = await readdir(instructionsDir, {
|
|
203
|
+
withFileTypes: true,
|
|
204
|
+
encoding: "utf-8",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
instrEntries = [];
|
|
209
|
+
}
|
|
210
|
+
for (const entry of instrEntries) {
|
|
211
|
+
if (!entry.isFile())
|
|
212
|
+
continue;
|
|
213
|
+
const lower = entry.name.toLowerCase();
|
|
214
|
+
if (!lower.endsWith(".instructions.md"))
|
|
215
|
+
continue;
|
|
216
|
+
const absPath = path.join(instructionsDir, entry.name);
|
|
217
|
+
const relPath = path.relative(baseDir, absPath).split(path.sep).join("/");
|
|
218
|
+
if (seen.has(relPath) || isInsideSkillDir(relPath, skillDirRelPaths))
|
|
219
|
+
continue;
|
|
220
|
+
// Derive name by stripping ".instructions.md" suffix
|
|
221
|
+
const baseStem = entry.name.slice(0, -".instructions.md".length);
|
|
222
|
+
const rawName = baseStem.toLowerCase().replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
223
|
+
if (!SAFE_NAME_RE.test(rawName)) {
|
|
224
|
+
logger.warn("init", `Skipping "${relPath}": could not derive a valid agentRules name`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
seen.add(relPath);
|
|
228
|
+
candidates.push({
|
|
229
|
+
relPath,
|
|
230
|
+
name: rawName,
|
|
231
|
+
defaultAgents: ["github-copilot"],
|
|
232
|
+
scope: "copilot-scoped",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
186
235
|
return candidates;
|
|
187
236
|
}
|
|
188
|
-
function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
|
|
237
|
+
function buildAgentRules(candidates, activeAgents, allAgents, skillNamesSeen) {
|
|
189
238
|
const rules = [];
|
|
190
239
|
const namesSeen = new Set(skillNamesSeen);
|
|
191
|
-
for (const { relPath, name: rawName } of candidates) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
240
|
+
for (const { relPath, name: rawName, defaultAgents: presetAgents, scope: presetScope, } of candidates) {
|
|
241
|
+
let agents;
|
|
242
|
+
if (presetAgents.length > 0) {
|
|
243
|
+
// Candidates with preset agents (e.g. copilot-repo, copilot-scoped) use
|
|
244
|
+
// those agents directly, intersected with the full agents list.
|
|
245
|
+
agents = presetAgents.filter((a) => allAgents.includes(a));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Derive from filename pattern and intersect with active (capable) agents.
|
|
249
|
+
const defaults = defaultAgentsForFile(path.basename(relPath), activeAgents);
|
|
250
|
+
const ix = defaults.filter((a) => activeAgents.includes(a));
|
|
251
|
+
agents = ix.length > 0 ? ix : activeAgents;
|
|
252
|
+
}
|
|
253
|
+
// Skip if excluded by --agents or no capable agents remain
|
|
198
254
|
if (agents.length === 0)
|
|
199
255
|
continue;
|
|
200
256
|
// Resolve name collision with skill names
|
|
@@ -211,7 +267,7 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
|
|
|
211
267
|
}
|
|
212
268
|
}
|
|
213
269
|
namesSeen.add(name);
|
|
214
|
-
rules.push({ name, path: relPath, agents, scope: "global" });
|
|
270
|
+
rules.push({ name, path: relPath, agents, scope: presetScope ?? "global" });
|
|
215
271
|
}
|
|
216
272
|
return rules;
|
|
217
273
|
}
|
|
@@ -576,7 +632,7 @@ export async function runInit(options) {
|
|
|
576
632
|
const skillNamesSeen = new Set(skills.map((s) => s.name));
|
|
577
633
|
const skillDirRelPaths = new Set(found.map((f) => f.relPath));
|
|
578
634
|
const agentRulesCandidates = await findAgentRulesCandidates(directory, skillDirRelPaths);
|
|
579
|
-
const agentRules = buildAgentRules(agentRulesCandidates, agentRulesCapableAgents, skillNamesSeen);
|
|
635
|
+
const agentRules = buildAgentRules(agentRulesCandidates, agentRulesCapableAgents, agents, skillNamesSeen);
|
|
580
636
|
const allNamesSeen = new Set([
|
|
581
637
|
...skillNamesSeen,
|
|
582
638
|
...agentRules.map((r) => r.name),
|
|
@@ -21,6 +21,7 @@ export type VerifyExpected = {
|
|
|
21
21
|
agent: AgentId;
|
|
22
22
|
};
|
|
23
23
|
export declare function registryPath(home: string): string;
|
|
24
|
+
export declare function registryDirPath(home: string): string;
|
|
24
25
|
export declare const defaultRegistryPersistence: RegistryPersistence;
|
|
25
26
|
export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed"> | Omit<FrontmatterEmitRegistryEntry, "deployed">;
|
|
26
27
|
export declare function registerDeployment(home: string, targetPath: string, entry: RegisterEntry, persistence?: RegistryPersistence): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RegistrySchema, } from "../schemas/registry.js";
|
|
4
4
|
const REGISTRY_DIR = ".inception-engine";
|
|
@@ -6,8 +6,26 @@ const REGISTRY_FILE = "registry.json";
|
|
|
6
6
|
export function registryPath(home) {
|
|
7
7
|
return path.join(home, REGISTRY_DIR, REGISTRY_FILE);
|
|
8
8
|
}
|
|
9
|
+
export function registryDirPath(home) {
|
|
10
|
+
return path.join(home, REGISTRY_DIR);
|
|
11
|
+
}
|
|
12
|
+
async function assertSafeRegistryStoragePath(targetPath, label) {
|
|
13
|
+
try {
|
|
14
|
+
const stat = await lstat(targetPath);
|
|
15
|
+
if (stat.isSymbolicLink()) {
|
|
16
|
+
throw new Error(`Refusing to use ${label} symlink: ${targetPath}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err.code === "ENOENT")
|
|
21
|
+
return;
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
async function loadRegistry(home) {
|
|
10
26
|
try {
|
|
27
|
+
await assertSafeRegistryStoragePath(registryDirPath(home), "registry directory");
|
|
28
|
+
await assertSafeRegistryStoragePath(registryPath(home), "registry file");
|
|
11
29
|
const content = await readFile(registryPath(home), "utf-8");
|
|
12
30
|
const parsed = JSON.parse(content);
|
|
13
31
|
const result = RegistrySchema.safeParse(parsed);
|
|
@@ -18,9 +36,12 @@ async function loadRegistry(home) {
|
|
|
18
36
|
}
|
|
19
37
|
}
|
|
20
38
|
async function saveRegistry(home, registry) {
|
|
21
|
-
const dir =
|
|
39
|
+
const dir = registryDirPath(home);
|
|
40
|
+
await assertSafeRegistryStoragePath(dir, "registry directory");
|
|
22
41
|
await mkdir(dir, { recursive: true });
|
|
42
|
+
await setDirectoryPermissions(dir);
|
|
23
43
|
const filePath = registryPath(home);
|
|
44
|
+
await assertSafeRegistryStoragePath(filePath, "registry file");
|
|
24
45
|
await writeFile(filePath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
25
46
|
await setFilePermissions(filePath);
|
|
26
47
|
}
|
|
@@ -29,12 +50,21 @@ export const defaultRegistryPersistence = {
|
|
|
29
50
|
save: saveRegistry,
|
|
30
51
|
};
|
|
31
52
|
/**
|
|
32
|
-
*
|
|
53
|
+
* Restrict access to the state directory regardless of umask.
|
|
54
|
+
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
55
|
+
*/
|
|
56
|
+
async function setDirectoryPermissions(dirPath) {
|
|
57
|
+
if (process.platform !== "win32") {
|
|
58
|
+
await chmod(dirPath, 0o700);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Restrict the registry file to the current user regardless of umask.
|
|
33
63
|
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
34
64
|
*/
|
|
35
65
|
async function setFilePermissions(filePath) {
|
|
36
66
|
if (process.platform !== "win32") {
|
|
37
|
-
await chmod(filePath,
|
|
67
|
+
await chmod(filePath, 0o600);
|
|
38
68
|
}
|
|
39
69
|
}
|
|
40
70
|
function emptyRegistry() {
|
|
@@ -118,12 +118,37 @@ function detectMultipleActiveInstructionScopes(agentId, rulesForAgent) {
|
|
|
118
118
|
},
|
|
119
119
|
];
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Warns when github-copilot has both a shared-via CLAUDE.md agentRules entry
|
|
123
|
+
* (scope: "repo" or "global") AND a native Copilot instruction entry
|
|
124
|
+
* (scope: "copilot-repo" or "copilot-scoped"). GitHub Copilot merges all
|
|
125
|
+
* active instruction sources at runtime, so duplicate or conflicting rules
|
|
126
|
+
* across these surfaces may cause unexpected agent behavior.
|
|
127
|
+
*/
|
|
128
|
+
function detectCopilotInstructionPrecedence(rulesForAgent) {
|
|
129
|
+
const hasSharedVia = (rulesForAgent ?? []).some((e) => e.scope === "repo" || e.scope === "global");
|
|
130
|
+
const hasNative = (rulesForAgent ?? []).some((e) => e.scope === "copilot-repo" || e.scope === "copilot-scoped");
|
|
131
|
+
if (!(hasSharedVia && hasNative))
|
|
132
|
+
return [];
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
kind: "precedence",
|
|
136
|
+
message: `Agent "github-copilot" will load both a CLAUDE.md-shared instruction file` +
|
|
137
|
+
` and a native Copilot instruction file (.github/copilot-instructions.md or` +
|
|
138
|
+
` .github/instructions/). GitHub Copilot merges all active instruction` +
|
|
139
|
+
` sources - ensure content is non-conflicting and does not duplicate rules.`,
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
121
143
|
function detectInstructionPrecedence(detectedAgents, manifest) {
|
|
122
144
|
const warnings = [];
|
|
123
145
|
for (const agentId of detectedAgents) {
|
|
124
146
|
const rulesForAgent = (manifest.agentRules ?? []).filter((e) => e.agents.includes(agentId));
|
|
125
147
|
warnings.push(...detectScopeOverlaps(agentId, rulesForAgent));
|
|
126
148
|
warnings.push(...detectMultipleActiveInstructionScopes(agentId, rulesForAgent));
|
|
149
|
+
if (agentId === "github-copilot") {
|
|
150
|
+
warnings.push(...detectCopilotInstructionPrecedence(rulesForAgent));
|
|
151
|
+
}
|
|
127
152
|
}
|
|
128
153
|
return warnings;
|
|
129
154
|
}
|
|
@@ -239,6 +264,9 @@ function collectManifestCapabilityWarnings(manifest, detectedAgents) {
|
|
|
239
264
|
for (const entry of manifest.agentDefinitions ?? []) {
|
|
240
265
|
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "agentDefinitions", entry.name, entry.scope);
|
|
241
266
|
}
|
|
267
|
+
for (const entry of manifest.executionConfigs ?? []) {
|
|
268
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "executionConfigs", entry.name);
|
|
269
|
+
}
|
|
242
270
|
return acc.warnings;
|
|
243
271
|
}
|
|
244
272
|
function detectCapabilityPlanningWarnings(manifest, detectedAgents) {
|
package/dist/src/core/revert.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
3
4
|
import { logger } from "../logger.js";
|
|
4
5
|
import * as frontmatterAdapter from "./adapters/frontmatter.js";
|
|
5
|
-
import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
|
|
6
|
+
import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileExecutionConfigReverts, compileHookReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
|
|
6
7
|
import { revertTomlMcpPatch } from "./adapters/toml.js";
|
|
7
8
|
import { applyUndoPatch } from "./merge-patch.js";
|
|
8
|
-
import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
|
|
9
|
+
import { lookupDeployment, registryDirPath, unregisterDeployment, } from "./ownership.js";
|
|
9
10
|
import { resolveAgentSkillPath } from "./resolve.js";
|
|
10
11
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
11
12
|
function buildSkillDirReverts(manifest, home, agentFilter) {
|
|
@@ -75,6 +76,8 @@ export function planRevert(manifest, detectedAgents, home, repo) {
|
|
|
75
76
|
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home, repo)),
|
|
76
77
|
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home, repo)),
|
|
77
78
|
...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, detectedAgents, home)),
|
|
79
|
+
...(manifest.hooks ?? []).flatMap((e) => compileHookReverts(e, detectedAgents, home)),
|
|
80
|
+
...(manifest.executionConfigs ?? []).flatMap((e) => compileExecutionConfigReverts(e, detectedAgents, home)),
|
|
78
81
|
...(manifest.agentDefinitions ?? []).flatMap((e) => compileAgentDefinitionReverts(e, detectedAgents, home, repo)),
|
|
79
82
|
];
|
|
80
83
|
}
|
|
@@ -86,6 +89,8 @@ export function planRevertAll(manifest, home, repo) {
|
|
|
86
89
|
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home, repo)),
|
|
87
90
|
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home, repo)),
|
|
88
91
|
...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, null, home)),
|
|
92
|
+
...(manifest.hooks ?? []).flatMap((e) => compileHookReverts(e, null, home)),
|
|
93
|
+
...(manifest.executionConfigs ?? []).flatMap((e) => compileExecutionConfigReverts(e, null, home)),
|
|
89
94
|
...(manifest.agentDefinitions ?? []).flatMap((e) => compileAgentDefinitionReverts(e, null, home, repo)),
|
|
90
95
|
];
|
|
91
96
|
}
|
|
@@ -121,6 +126,54 @@ function lstatOutcome(err) {
|
|
|
121
126
|
const msg = err instanceof Error ? err.message : String(err);
|
|
122
127
|
return { outcome: "fail", error: msg };
|
|
123
128
|
}
|
|
129
|
+
function normalizePathForComparison(candidate) {
|
|
130
|
+
const normalized = path.normalize(candidate);
|
|
131
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
132
|
+
}
|
|
133
|
+
function isSameOrDescendantPath(candidate, root) {
|
|
134
|
+
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
135
|
+
const normalizedRoot = normalizePathForComparison(root);
|
|
136
|
+
return (normalizedCandidate === normalizedRoot ||
|
|
137
|
+
normalizedCandidate.startsWith(normalizedRoot + path.sep));
|
|
138
|
+
}
|
|
139
|
+
function failReservedStateTarget(action, home) {
|
|
140
|
+
const reservedDir = registryDirPath(home);
|
|
141
|
+
if (!isSameOrDescendantPath(action.target, reservedDir)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
outcome: "fail",
|
|
146
|
+
error: `Refusing to modify inception-engine state directory "${reservedDir}" ` +
|
|
147
|
+
`via manifest-managed revert target "${action.target}"`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function preflightManagedSkillDirRevert(action, label, home, deps) {
|
|
151
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
152
|
+
if (reservedTargetFailure) {
|
|
153
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
154
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
155
|
+
}
|
|
156
|
+
return reservedTargetFailure;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await lstat(action.target);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const result = lstatOutcome(err);
|
|
163
|
+
if (result.outcome === "skip") {
|
|
164
|
+
logger.skip(label, "(not found, skipping)");
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
logger.fail(label, result.error);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
const entry = await lookupDeployment(home, action.target, deps.registry);
|
|
171
|
+
if (!entry || entry.skill !== action.skill || entry.agent !== action.agent) {
|
|
172
|
+
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
173
|
+
return { outcome: "skip" };
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
124
177
|
export async function executeRevert(actions, dryRun, verbose, home, deps = {}) {
|
|
125
178
|
const failed = [];
|
|
126
179
|
const planned = [];
|
|
@@ -194,6 +247,13 @@ async function applyFrontmatterRevert(action, frontmatterEntry) {
|
|
|
194
247
|
}
|
|
195
248
|
async function revertFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
|
|
196
249
|
const label = `${action.skill} -> ${action.agent}`;
|
|
250
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
251
|
+
if (reservedTargetFailure) {
|
|
252
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
253
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
254
|
+
}
|
|
255
|
+
return reservedTargetFailure;
|
|
256
|
+
}
|
|
197
257
|
try {
|
|
198
258
|
await lstat(action.target);
|
|
199
259
|
}
|
|
@@ -241,22 +301,9 @@ async function revertFrontmatterEmit(action, dryRun, verbose, home, planned, dep
|
|
|
241
301
|
}
|
|
242
302
|
async function executeRevertAction(action, dryRun, verbose, home, planned, deps) {
|
|
243
303
|
const label = `${action.skill} -> ${action.agent}`;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
catch (err) {
|
|
248
|
-
const result = lstatOutcome(err);
|
|
249
|
-
if (result.outcome === "skip") {
|
|
250
|
-
logger.skip(label, "(not found, skipping)");
|
|
251
|
-
return result;
|
|
252
|
-
}
|
|
253
|
-
logger.fail(label, result.error);
|
|
254
|
-
return result;
|
|
255
|
-
}
|
|
256
|
-
const entry = await lookupDeployment(home, action.target, deps.registry);
|
|
257
|
-
if (!entry || entry.skill !== action.skill || entry.agent !== action.agent) {
|
|
258
|
-
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
259
|
-
return { outcome: "skip" };
|
|
304
|
+
const preflight = await preflightManagedSkillDirRevert(action, label, home, deps);
|
|
305
|
+
if (preflight) {
|
|
306
|
+
return preflight;
|
|
260
307
|
}
|
|
261
308
|
if (dryRun) {
|
|
262
309
|
planned.push({
|
|
@@ -298,6 +345,13 @@ async function executeRevertAction(action, dryRun, verbose, home, planned, deps)
|
|
|
298
345
|
}
|
|
299
346
|
async function revertFileWrite(action, dryRun, verbose, home, planned, deps) {
|
|
300
347
|
const label = `${action.skill} -> ${action.agent}`;
|
|
348
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
349
|
+
if (reservedTargetFailure) {
|
|
350
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
351
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
352
|
+
}
|
|
353
|
+
return reservedTargetFailure;
|
|
354
|
+
}
|
|
301
355
|
try {
|
|
302
356
|
await lstat(action.target);
|
|
303
357
|
}
|
|
@@ -349,6 +403,13 @@ async function revertFileWrite(action, dryRun, verbose, home, planned, deps) {
|
|
|
349
403
|
}
|
|
350
404
|
async function revertConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
351
405
|
const label = `${action.skill} -> ${action.agent}`;
|
|
406
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
407
|
+
if (reservedTargetFailure) {
|
|
408
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
409
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
410
|
+
}
|
|
411
|
+
return reservedTargetFailure;
|
|
412
|
+
}
|
|
352
413
|
try {
|
|
353
414
|
await lstat(action.target);
|
|
354
415
|
}
|
|
@@ -400,6 +461,13 @@ async function revertConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
400
461
|
}
|
|
401
462
|
async function revertTomlPatch(action, dryRun, verbose, home, planned, deps) {
|
|
402
463
|
const label = `${action.skill} -> ${action.agent}`;
|
|
464
|
+
const reservedTargetFailure = failReservedStateTarget(action, home);
|
|
465
|
+
if (reservedTargetFailure) {
|
|
466
|
+
if (reservedTargetFailure.outcome === "fail") {
|
|
467
|
+
logger.fail(label, reservedTargetFailure.error);
|
|
468
|
+
}
|
|
469
|
+
return reservedTargetFailure;
|
|
470
|
+
}
|
|
403
471
|
try {
|
|
404
472
|
await lstat(action.target);
|
|
405
473
|
}
|
|
@@ -4,7 +4,7 @@ export declare function validateSourcePath(source: string, skillPath: string, re
|
|
|
4
4
|
export declare function validateSourceFile(sourcePath: string, manifestPath: string): Promise<void>;
|
|
5
5
|
export declare function validateMcpServerConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
|
|
6
6
|
export declare function validatePermissionsConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
|
|
7
|
-
export declare function validateHookConfigShape(
|
|
7
|
+
export declare function validateHookConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
|
|
8
8
|
export declare function validateAgentRuleMarkdownPath(manifestPath: string, agentId: string): void;
|
|
9
9
|
export declare function validateSkillDefinitionFile(sourcePath: string, manifestPath: string): Promise<{
|
|
10
10
|
attributes: Record<string, unknown>;
|
|
@@ -163,9 +163,62 @@ export function validatePermissionsConfigShape(config, entryName, agentId) {
|
|
|
163
163
|
validateOpenCodePermissions(config, entryName);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
function validateClaudeHookCommand(cmd, path) {
|
|
167
|
+
if (typeof cmd !== "object" || cmd === null || Array.isArray(cmd)) {
|
|
168
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry ${path} must be an object`);
|
|
169
|
+
}
|
|
170
|
+
const cmdObj = cmd;
|
|
171
|
+
if (cmdObj.type !== "command") {
|
|
172
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry ${path}.type must be "command"`);
|
|
173
|
+
}
|
|
174
|
+
if (typeof cmdObj.command !== "string") {
|
|
175
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry ${path}.command must be a string`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function validateClaudeHookMatcher(matcher, path) {
|
|
179
|
+
if (typeof matcher !== "object" ||
|
|
180
|
+
matcher === null ||
|
|
181
|
+
Array.isArray(matcher)) {
|
|
182
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry ${path} must be an object`);
|
|
183
|
+
}
|
|
184
|
+
const matcherObj = matcher;
|
|
185
|
+
if (matcherObj.matcher !== undefined &&
|
|
186
|
+
typeof matcherObj.matcher !== "string") {
|
|
187
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry ${path}.matcher must be a string when present`);
|
|
188
|
+
}
|
|
189
|
+
const matcherHooks = matcherObj.hooks;
|
|
190
|
+
if (!Array.isArray(matcherHooks)) {
|
|
191
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry ${path}.hooks must be an array`);
|
|
192
|
+
}
|
|
193
|
+
for (const [cmdIdx, cmd] of matcherHooks.entries()) {
|
|
194
|
+
validateClaudeHookCommand(cmd, `${path}.hooks[${cmdIdx}]`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function validateClaudeCodeHooks(config, entryName) {
|
|
198
|
+
const unknownKeys = Object.keys(config).filter((k) => k !== "hooks");
|
|
199
|
+
if (unknownKeys.length > 0) {
|
|
200
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry "${entryName}" for agent "claude-code" contains unrecognized keys: ${unknownKeys.join(", ")}. Only "hooks" is allowed.`);
|
|
201
|
+
}
|
|
202
|
+
const hooks = config.hooks;
|
|
203
|
+
if (hooks === undefined)
|
|
204
|
+
return;
|
|
205
|
+
if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
|
|
206
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry "${entryName}" for agent "claude-code" must define "hooks" as an object`);
|
|
207
|
+
}
|
|
208
|
+
const hooksObj = hooks;
|
|
209
|
+
for (const [eventName, matchers] of Object.entries(hooksObj)) {
|
|
210
|
+
if (!Array.isArray(matchers)) {
|
|
211
|
+
throw new UserError("DEPLOY_FAILED", `hooks entry "${entryName}" for agent "claude-code": "hooks.${eventName}" must be an array`);
|
|
212
|
+
}
|
|
213
|
+
for (const [idx, matcher] of matchers.entries()) {
|
|
214
|
+
validateClaudeHookMatcher(matcher, `"${entryName}" for agent "claude-code": "hooks.${eventName}[${idx}]"`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function validateHookConfigShape(config, entryName, agentId) {
|
|
219
|
+
if (agentId === "claude-code") {
|
|
220
|
+
validateClaudeCodeHooks(config, entryName);
|
|
221
|
+
}
|
|
169
222
|
}
|
|
170
223
|
export function validateAgentRuleMarkdownPath(manifestPath, agentId) {
|
|
171
224
|
const extension = path.extname(manifestPath).toLowerCase();
|
|
@@ -81,7 +81,10 @@ export declare const AgentRuleEntrySchema: z.ZodObject<{
|
|
|
81
81
|
global: "global";
|
|
82
82
|
repo: "repo";
|
|
83
83
|
workspace: "workspace";
|
|
84
|
+
"copilot-repo": "copilot-repo";
|
|
85
|
+
"copilot-scoped": "copilot-scoped";
|
|
84
86
|
}>>;
|
|
87
|
+
targetDir: z.ZodOptional<z.ZodString>;
|
|
85
88
|
}, z.core.$strip>;
|
|
86
89
|
export declare const PermissionsEntrySchema: z.ZodObject<{
|
|
87
90
|
name: z.ZodString;
|
|
@@ -95,6 +98,18 @@ export declare const PermissionsEntrySchema: z.ZodObject<{
|
|
|
95
98
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
96
99
|
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
97
100
|
}, z.core.$strip>;
|
|
101
|
+
export declare const ExecutionConfigEntrySchema: z.ZodObject<{
|
|
102
|
+
name: z.ZodString;
|
|
103
|
+
agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
104
|
+
"claude-code": "claude-code";
|
|
105
|
+
codex: "codex";
|
|
106
|
+
"gemini-cli": "gemini-cli";
|
|
107
|
+
antigravity: "antigravity";
|
|
108
|
+
opencode: "opencode";
|
|
109
|
+
"github-copilot": "github-copilot";
|
|
110
|
+
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
111
|
+
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
112
|
+
}, z.core.$strip>;
|
|
98
113
|
export declare const AgentDefinitionEntrySchema: z.ZodObject<{
|
|
99
114
|
name: z.ZodString;
|
|
100
115
|
agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
@@ -196,7 +211,10 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
196
211
|
global: "global";
|
|
197
212
|
repo: "repo";
|
|
198
213
|
workspace: "workspace";
|
|
214
|
+
"copilot-repo": "copilot-repo";
|
|
215
|
+
"copilot-scoped": "copilot-scoped";
|
|
199
216
|
}>>;
|
|
217
|
+
targetDir: z.ZodOptional<z.ZodString>;
|
|
200
218
|
}, z.core.$strip>>>;
|
|
201
219
|
permissions: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
202
220
|
name: z.ZodString;
|
|
@@ -239,6 +257,18 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
239
257
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
240
258
|
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
241
259
|
}, z.core.$strip>>>;
|
|
260
|
+
executionConfigs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
261
|
+
name: z.ZodString;
|
|
262
|
+
agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
263
|
+
"claude-code": "claude-code";
|
|
264
|
+
codex: "codex";
|
|
265
|
+
"gemini-cli": "gemini-cli";
|
|
266
|
+
antigravity: "antigravity";
|
|
267
|
+
opencode: "opencode";
|
|
268
|
+
"github-copilot": "github-copilot";
|
|
269
|
+
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
270
|
+
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
271
|
+
}, z.core.$strip>>>;
|
|
242
272
|
}, z.core.$strip>;
|
|
243
273
|
export type SkillEntry = z.infer<typeof SkillEntrySchema>;
|
|
244
274
|
export type FileEntry = z.infer<typeof FileEntrySchema>;
|
|
@@ -248,6 +278,7 @@ export type AgentRuleEntry = z.infer<typeof AgentRuleEntrySchema>;
|
|
|
248
278
|
export type PermissionsEntry = z.infer<typeof PermissionsEntrySchema>;
|
|
249
279
|
export type AgentDefinitionEntry = z.infer<typeof AgentDefinitionEntrySchema>;
|
|
250
280
|
export type HookEntry = z.infer<typeof HookEntrySchema>;
|
|
281
|
+
export type ExecutionConfigEntry = z.infer<typeof ExecutionConfigEntrySchema>;
|
|
251
282
|
export type Manifest = z.infer<typeof ManifestSchema>;
|
|
252
283
|
export declare const AgentListSchema: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
253
284
|
"claude-code": "claude-code";
|
|
@@ -92,7 +92,8 @@ export const McpServerEntrySchema = z.object({
|
|
|
92
92
|
.enum(["global", "repo", "workspace", "devcontainer"])
|
|
93
93
|
.default("global"),
|
|
94
94
|
});
|
|
95
|
-
export const AgentRuleEntrySchema = z
|
|
95
|
+
export const AgentRuleEntrySchema = z
|
|
96
|
+
.object({
|
|
96
97
|
name: nameField,
|
|
97
98
|
agents: agentsField,
|
|
98
99
|
// Relative path to the rules/instruction file within the source bundle.
|
|
@@ -103,7 +104,33 @@ export const AgentRuleEntrySchema = z.object({
|
|
|
103
104
|
// file (default), "repo" targets the project-root instruction file within the
|
|
104
105
|
// deployed repository (e.g. {repo}/CLAUDE.md for claude-code), and "workspace"
|
|
105
106
|
// targets the agent's workspace-local instruction surface.
|
|
106
|
-
|
|
107
|
+
// "copilot-repo" targets GitHub Copilot's native repo-level instruction file
|
|
108
|
+
// at {repo}/.github/copilot-instructions.md (github-copilot only).
|
|
109
|
+
// "copilot-scoped" targets {repo}/.github/instructions/{name}.instructions.md
|
|
110
|
+
// where {name} is the manifest entry name (github-copilot only).
|
|
111
|
+
scope: z
|
|
112
|
+
.enum(["global", "repo", "workspace", "copilot-repo", "copilot-scoped"])
|
|
113
|
+
.default("global"),
|
|
114
|
+
// Optional relative directory within the repo/workspace where the rule
|
|
115
|
+
// should be deployed. Only supported for scope: "repo" and scope: "workspace".
|
|
116
|
+
targetDir: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.refine((p) => !(p && nodePath.isAbsolute(p)), {
|
|
120
|
+
message: "targetDir must be a relative path",
|
|
121
|
+
})
|
|
122
|
+
.refine((p) => !(p && nodePath.normalize(p).startsWith("..")), {
|
|
123
|
+
message: "targetDir must not escape the target root",
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
126
|
+
.superRefine((data, ctx) => {
|
|
127
|
+
if (data.targetDir && data.scope !== "repo" && data.scope !== "workspace") {
|
|
128
|
+
ctx.addIssue({
|
|
129
|
+
code: "custom",
|
|
130
|
+
path: ["targetDir"],
|
|
131
|
+
message: 'targetDir is only supported for scope "repo" or "workspace"',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
107
134
|
});
|
|
108
135
|
export const PermissionsEntrySchema = z.object({
|
|
109
136
|
name: nameField,
|
|
@@ -114,6 +141,13 @@ export const PermissionsEntrySchema = z.object({
|
|
|
114
141
|
// For opencode: { permissions: { allow?: string[], ask?: string[], deny?: string[] } }
|
|
115
142
|
config: z.record(z.string(), z.unknown()),
|
|
116
143
|
});
|
|
144
|
+
export const ExecutionConfigEntrySchema = z.object({
|
|
145
|
+
name: nameField,
|
|
146
|
+
agents: agentsField,
|
|
147
|
+
// Raw execution config payload validated per agent by the execution-config adapter.
|
|
148
|
+
// For gemini-cli: { safeMode?: boolean, ... }
|
|
149
|
+
config: z.record(z.string(), z.unknown()),
|
|
150
|
+
});
|
|
117
151
|
export const AgentDefinitionEntrySchema = z.object({
|
|
118
152
|
name: nameField,
|
|
119
153
|
agents: agentsField,
|
|
@@ -130,7 +164,8 @@ export const HookEntrySchema = z.object({
|
|
|
130
164
|
name: nameField,
|
|
131
165
|
agents: agentsField,
|
|
132
166
|
// Raw hook config payload validated per agent by the hooks adapter.
|
|
133
|
-
//
|
|
167
|
+
// For claude-code: { hooks: { "<EventName>": [{ matcher?: string, hooks: [{ type: "command", command: string }] }] } }
|
|
168
|
+
// Event names follow Claude Code's settings.json hooks surface (e.g. PreToolUse, PostToolUse, Notification, Stop, SubagentStop).
|
|
134
169
|
config: z.record(z.string(), z.unknown()),
|
|
135
170
|
});
|
|
136
171
|
export const ManifestSchema = z.object({
|
|
@@ -154,6 +189,7 @@ export const ManifestSchema = z.object({
|
|
|
154
189
|
permissions: z.array(PermissionsEntrySchema).default([]),
|
|
155
190
|
agentDefinitions: z.array(AgentDefinitionEntrySchema).default([]),
|
|
156
191
|
hooks: z.array(HookEntrySchema).optional(),
|
|
192
|
+
executionConfigs: z.array(ExecutionConfigEntrySchema).optional(),
|
|
157
193
|
});
|
|
158
194
|
// Parses the --agents CLI flag: comma-separated agent IDs → AgentId[]
|
|
159
195
|
export const AgentListSchema = z
|