@kuznai/inception-engine 0.19.0 → 0.21.0

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