@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.
Files changed (43) hide show
  1. package/README.md +50 -9
  2. package/dist/src/config/agents.js +39 -0
  3. package/dist/src/core/adapters/execution-config.d.ts +14 -0
  4. package/dist/src/core/adapters/execution-config.js +60 -0
  5. package/dist/src/core/adapters/index.d.ts +4 -3
  6. package/dist/src/core/adapters/index.js +7 -5
  7. package/dist/src/core/adapters/rules.js +50 -19
  8. package/dist/src/core/capabilities.d.ts +1 -1
  9. package/dist/src/core/capabilities.js +18 -2
  10. package/dist/src/core/deploy.js +66 -2
  11. package/dist/src/core/init.js +69 -13
  12. package/dist/src/core/ownership.d.ts +1 -0
  13. package/dist/src/core/ownership.js +34 -4
  14. package/dist/src/core/preflight.js +28 -0
  15. package/dist/src/core/revert.js +86 -18
  16. package/dist/src/core/validation.d.ts +1 -1
  17. package/dist/src/core/validation.js +56 -3
  18. package/dist/src/schemas/manifest.d.ts +31 -0
  19. package/dist/src/schemas/manifest.js +39 -3
  20. package/dist/src/types.d.ts +6 -2
  21. package/dist/test/helpers/path.d.ts +2 -2
  22. package/dist/test/helpers/path.js +3 -3
  23. package/dist/test/os/posix/deploy.test.js +395 -0
  24. package/dist/test/os/posix/revert.test.js +188 -0
  25. package/dist/test/os/windows/deploy.test.d.ts +1 -0
  26. package/dist/test/os/windows/deploy.test.js +238 -0
  27. package/dist/test/unit/adapters.test.js +402 -2
  28. package/dist/test/unit/capabilities.test.js +44 -1
  29. package/dist/test/unit/cli.test.js +162 -0
  30. package/dist/test/unit/deploy.test.js +88 -647
  31. package/dist/test/unit/formatters.test.d.ts +1 -0
  32. package/dist/test/unit/formatters.test.js +74 -0
  33. package/dist/test/unit/init-fixture.test.js +85 -1
  34. package/dist/test/unit/manifest.test.js +72 -0
  35. package/dist/test/unit/ownership.test.js +61 -4
  36. package/dist/test/unit/preflight.test.js +145 -0
  37. package/dist/test/unit/resolve.test.js +67 -1
  38. package/dist/test/unit/revert.test.js +81 -182
  39. package/package.json +1 -1
  40. package/dist/test/os/windows/agentRules-integration.test.js +0 -245
  41. package/dist/test/os/windows/revert-integration.test.js +0 -72
  42. /package/dist/test/os/{windows/agentRules-integration.test.d.ts → posix/deploy.test.d.ts} +0 -0
  43. /package/dist/test/os/{windows/revert-integration.test.d.ts → posix/revert.test.d.ts} +0 -0
@@ -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: copilot-instructions.md claude-code. Copilot reads
36
- // CLAUDE.md natively; this filename is a well-known convention that cannot
37
- // be derived from any agent path template.
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
- const fileName = path.basename(relPath);
193
- const defaultAgents = defaultAgentsForFile(fileName, activeAgents);
194
- // Intersect with active agents; fall back to full active list if empty
195
- const intersection = defaultAgents.filter((a) => activeAgents.includes(a));
196
- const agents = intersection.length > 0 ? intersection : activeAgents;
197
- // Skip if no capable agents remain (e.g. --agents github-copilot only)
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 = path.join(home, REGISTRY_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
- * Ensure the file is not world-writable regardless of umask.
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, 0o644);
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) {
@@ -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
- try {
245
- await lstat(action.target);
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(_config: Record<string, unknown>, _entryName: string, _agentId: string): void;
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
- export function validateHookConfigShape(_config, _entryName, _agentId) {
167
- // Placeholder for agent-specific hook validation logic.
168
- // Currently allows any record as a hook payload.
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.object({
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
- scope: z.enum(["global", "repo", "workspace"]).default("global"),
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
- // Supports lifecycle-binding hooks (e.g. pre-exec, post-exec) for various agents.
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