@kuznai/inception-engine 0.22.0 → 0.23.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 +45 -8
- 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 +1 -1
- package/dist/src/core/init.js +69 -13
- package/dist/src/core/preflight.js +28 -0
- package/dist/src/core/revert.js +5 -1
- 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/unit/adapters.test.js +264 -1
- package/dist/test/unit/deploy.test.js +26 -0
- package/dist/test/unit/init-fixture.test.js +85 -1
- package/dist/test/unit/manifest.test.js +72 -0
- package/dist/test/unit/preflight.test.js +86 -0
- package/dist/test/unit/revert.test.js +65 -0
- package/package.json +1 -1
|
@@ -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
|
@@ -2,7 +2,7 @@ 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
4
|
import * as frontmatterAdapter from "./adapters/frontmatter.js";
|
|
5
|
-
import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
|
|
5
|
+
import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileExecutionConfigReverts, compileHookReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
|
|
6
6
|
import { revertTomlMcpPatch } from "./adapters/toml.js";
|
|
7
7
|
import { applyUndoPatch } from "./merge-patch.js";
|
|
8
8
|
import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
|
|
@@ -75,6 +75,8 @@ export function planRevert(manifest, detectedAgents, home, repo) {
|
|
|
75
75
|
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home, repo)),
|
|
76
76
|
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home, repo)),
|
|
77
77
|
...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, detectedAgents, home)),
|
|
78
|
+
...(manifest.hooks ?? []).flatMap((e) => compileHookReverts(e, detectedAgents, home)),
|
|
79
|
+
...(manifest.executionConfigs ?? []).flatMap((e) => compileExecutionConfigReverts(e, detectedAgents, home)),
|
|
78
80
|
...(manifest.agentDefinitions ?? []).flatMap((e) => compileAgentDefinitionReverts(e, detectedAgents, home, repo)),
|
|
79
81
|
];
|
|
80
82
|
}
|
|
@@ -86,6 +88,8 @@ export function planRevertAll(manifest, home, repo) {
|
|
|
86
88
|
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home, repo)),
|
|
87
89
|
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home, repo)),
|
|
88
90
|
...(manifest.permissions ?? []).flatMap((e) => compilePermissionsReverts(e, null, home)),
|
|
91
|
+
...(manifest.hooks ?? []).flatMap((e) => compileHookReverts(e, null, home)),
|
|
92
|
+
...(manifest.executionConfigs ?? []).flatMap((e) => compileExecutionConfigReverts(e, null, home)),
|
|
89
93
|
...(manifest.agentDefinitions ?? []).flatMap((e) => compileAgentDefinitionReverts(e, null, home, repo)),
|
|
90
94
|
];
|
|
91
95
|
}
|
|
@@ -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
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { AgentId } from "./schemas/manifest.ts";
|
|
2
|
-
export type { AgentDefinitionEntry, AgentId, ConfigEntry, FileEntry, HookEntry, Manifest, SkillEntry, } from "./schemas/manifest.ts";
|
|
2
|
+
export type { AgentDefinitionEntry, AgentId, ConfigEntry, ExecutionConfigEntry, FileEntry, HookEntry, Manifest, SkillEntry, } from "./schemas/manifest.ts";
|
|
3
3
|
export interface AgentPaths {
|
|
4
4
|
posix: string[];
|
|
5
5
|
windows: string[];
|
|
6
6
|
}
|
|
7
7
|
export type Confidence = "documented" | "implementation-only" | "provisional";
|
|
8
|
-
export type CapabilityKind = "skills" | "mcpServers" | "agentRules" | "permissions" | "hooks" | "agentDefinitions";
|
|
8
|
+
export type CapabilityKind = "skills" | "mcpServers" | "agentRules" | "permissions" | "hooks" | "executionConfigs" | "agentDefinitions";
|
|
9
9
|
export interface SupportedAgentSurface {
|
|
10
10
|
status: "supported";
|
|
11
11
|
/**
|
|
@@ -68,6 +68,7 @@ export interface AgentProvenance {
|
|
|
68
68
|
agentRules?: Confidence;
|
|
69
69
|
permissions?: Confidence;
|
|
70
70
|
hooks?: Confidence;
|
|
71
|
+
executionConfig?: Confidence;
|
|
71
72
|
agentDefinitions?: Confidence;
|
|
72
73
|
}
|
|
73
74
|
export interface AgentConfig {
|
|
@@ -97,6 +98,8 @@ export interface AgentConfig {
|
|
|
97
98
|
agentRulesSupport?: AgentSurfaceSupport;
|
|
98
99
|
agentRulesRepoSupport?: AgentSurfaceSupport;
|
|
99
100
|
agentRulesWorkspaceSupport?: AgentSurfaceSupport;
|
|
101
|
+
agentRulesCopilotRepoSupport?: AgentSurfaceSupport;
|
|
102
|
+
agentRulesCopilotScopedSupport?: AgentSurfaceSupport;
|
|
100
103
|
permissionsSupport?: AgentSurfaceSupport;
|
|
101
104
|
hooksSupport?: AgentSurfaceSupport;
|
|
102
105
|
hooksRepoSupport?: AgentSurfaceSupport;
|
|
@@ -106,6 +109,7 @@ export interface AgentConfig {
|
|
|
106
109
|
agentDefinitionsWorkspaceSupport?: AgentSurfaceSupport;
|
|
107
110
|
agentDefinitionsTomlSupport?: AgentSurfaceSupport;
|
|
108
111
|
agentDefinitionsTomlRepoSupport?: AgentSurfaceSupport;
|
|
112
|
+
executionConfigSupport?: AgentSurfaceSupport;
|
|
109
113
|
mcpDevcontainerSupport?: AgentSurfaceSupport;
|
|
110
114
|
mcpAgentFrontmatterSupport?: AgentSurfaceSupport;
|
|
111
115
|
policyNote?: string;
|
|
@@ -3,8 +3,9 @@ import { realpath, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
5
|
import { compileMcpServerActions } from "../../src/core/adapters/mcp.js";
|
|
6
|
+
import { compileHookActions } from "../../src/core/adapters/hooks.js";
|
|
6
7
|
import { compilePermissionsActions } from "../../src/core/adapters/permissions.js";
|
|
7
|
-
import { compileAgentRuleActions } from "../../src/core/adapters/rules.js";
|
|
8
|
+
import { compileAgentRuleActions, compileAgentRuleReverts, } from "../../src/core/adapters/rules.js";
|
|
8
9
|
import { makeTmpDir } from "../helpers/fs.js";
|
|
9
10
|
import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
|
|
10
11
|
describe("compileMcpServerActions", () => {
|
|
@@ -354,6 +355,115 @@ describe("compileAgentRuleActions", () => {
|
|
|
354
355
|
await rm(dir, { recursive: true });
|
|
355
356
|
}
|
|
356
357
|
});
|
|
358
|
+
it("scope copilot-repo: returns file-write action targeting {repo}/.github/copilot-instructions.md", async () => {
|
|
359
|
+
const dir = await makeTmpDir();
|
|
360
|
+
try {
|
|
361
|
+
await writeFile(path.join(dir, "copilot.md"), "# Copilot rules");
|
|
362
|
+
const repo = "/repo/myproject";
|
|
363
|
+
const realRoot = await realpath(dir);
|
|
364
|
+
const { actions, warnings } = await compileAgentRuleActions({
|
|
365
|
+
name: "copilot-main",
|
|
366
|
+
agents: ["github-copilot"],
|
|
367
|
+
path: "copilot.md",
|
|
368
|
+
scope: "copilot-repo",
|
|
369
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", repo);
|
|
370
|
+
assert.equal(actions.length, 1);
|
|
371
|
+
assert.equal(warnings.length, 0);
|
|
372
|
+
const action = actions[0];
|
|
373
|
+
assert.equal(action.kind, "file-write");
|
|
374
|
+
assert.equal(action.agent, "github-copilot");
|
|
375
|
+
assert.equal(normalizeSlashes(action.target), `${repo}/.github/copilot-instructions.md`, `expected target at {repo}/.github/copilot-instructions.md, got: ${action.target}`);
|
|
376
|
+
assert.equal(action.confidence, "documented");
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
await rm(dir, { recursive: true });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
it("scope copilot-scoped: returns file-write action with {name} substituted in path", async () => {
|
|
383
|
+
const dir = await makeTmpDir();
|
|
384
|
+
try {
|
|
385
|
+
await writeFile(path.join(dir, "typescript.md"), "# TypeScript rules");
|
|
386
|
+
const repo = "/repo/myproject";
|
|
387
|
+
const realRoot = await realpath(dir);
|
|
388
|
+
const { actions, warnings } = await compileAgentRuleActions({
|
|
389
|
+
name: "typescript",
|
|
390
|
+
agents: ["github-copilot"],
|
|
391
|
+
path: "typescript.md",
|
|
392
|
+
scope: "copilot-scoped",
|
|
393
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", repo);
|
|
394
|
+
assert.equal(actions.length, 1);
|
|
395
|
+
assert.equal(warnings.length, 0);
|
|
396
|
+
const action = actions[0];
|
|
397
|
+
assert.equal(action.kind, "file-write");
|
|
398
|
+
assert.equal(action.agent, "github-copilot");
|
|
399
|
+
assert.equal(normalizeSlashes(action.target), `${repo}/.github/instructions/typescript.instructions.md`, `expected target at {repo}/.github/instructions/typescript.instructions.md, got: ${action.target}`);
|
|
400
|
+
assert.equal(action.confidence, "documented");
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
await rm(dir, { recursive: true });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
it("scope copilot-repo: returns warning when no repo path provided", async () => {
|
|
407
|
+
const dir = await makeTmpDir();
|
|
408
|
+
try {
|
|
409
|
+
await writeFile(path.join(dir, "copilot.md"), "# Copilot rules");
|
|
410
|
+
const realRoot = await realpath(dir);
|
|
411
|
+
const { actions, warnings } = await compileAgentRuleActions({
|
|
412
|
+
name: "copilot-main",
|
|
413
|
+
agents: ["github-copilot"],
|
|
414
|
+
path: "copilot.md",
|
|
415
|
+
scope: "copilot-repo",
|
|
416
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test");
|
|
417
|
+
assert.equal(actions.length, 0);
|
|
418
|
+
assert.equal(warnings.length, 1);
|
|
419
|
+
assert.equal(warnings[0]?.kind, "confidence");
|
|
420
|
+
assert.match(warnings[0]?.message ?? "", /copilot-repo/);
|
|
421
|
+
assert.match(warnings[0]?.message ?? "", /repository path/);
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
await rm(dir, { recursive: true });
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
it("scope copilot-repo: non-github-copilot agents get unsupported warning", async () => {
|
|
428
|
+
const dir = await makeTmpDir();
|
|
429
|
+
try {
|
|
430
|
+
await writeFile(path.join(dir, "copilot.md"), "# Copilot rules");
|
|
431
|
+
const realRoot = await realpath(dir);
|
|
432
|
+
const { actions, warnings } = await compileAgentRuleActions({
|
|
433
|
+
name: "copilot-main",
|
|
434
|
+
agents: ["claude-code"],
|
|
435
|
+
path: "copilot.md",
|
|
436
|
+
scope: "copilot-repo",
|
|
437
|
+
}, dir, dir, realRoot, ["claude-code"], "/home/test", "/repo/test");
|
|
438
|
+
assert.equal(actions.length, 0);
|
|
439
|
+
assert.equal(warnings.length, 1);
|
|
440
|
+
assert.equal(warnings[0]?.kind, "confidence");
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
await rm(dir, { recursive: true });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
it("scope copilot-repo: plain markdown without frontmatter succeeds (no instructionFrontmatterRequired check)", async () => {
|
|
447
|
+
const dir = await makeTmpDir();
|
|
448
|
+
try {
|
|
449
|
+
// Plain markdown with no frontmatter — should NOT throw despite github-copilot
|
|
450
|
+
// having instructionFrontmatterRequired: true (native agentRules scopes skip that check)
|
|
451
|
+
await writeFile(path.join(dir, "plain.md"), "# Plain rules\n\nNo frontmatter.");
|
|
452
|
+
const repo = "/repo/myproject";
|
|
453
|
+
const realRoot = await realpath(dir);
|
|
454
|
+
const { actions, warnings } = await compileAgentRuleActions({
|
|
455
|
+
name: "plain-rules",
|
|
456
|
+
agents: ["github-copilot"],
|
|
457
|
+
path: "plain.md",
|
|
458
|
+
scope: "copilot-repo",
|
|
459
|
+
}, dir, dir, realRoot, ["github-copilot"], "/home/test", repo);
|
|
460
|
+
assert.equal(actions.length, 1);
|
|
461
|
+
assert.equal(warnings.length, 0);
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
await rm(dir, { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
});
|
|
357
467
|
it("throws when rules source file does not exist", async () => {
|
|
358
468
|
const dir = await makeTmpDir();
|
|
359
469
|
try {
|
|
@@ -469,6 +579,29 @@ describe("compileAgentRuleActions", () => {
|
|
|
469
579
|
await rm(dir, { recursive: true });
|
|
470
580
|
}
|
|
471
581
|
});
|
|
582
|
+
it("scope repo with targetDir: injects targetDir into path for claude-code", async () => {
|
|
583
|
+
const dir = await makeTmpDir();
|
|
584
|
+
try {
|
|
585
|
+
const rulesFile = path.join(dir, "CLAUDE.md");
|
|
586
|
+
await writeFile(rulesFile, "# Rules");
|
|
587
|
+
const repo = "/repo/myproject";
|
|
588
|
+
const realRoot = await realpath(dir);
|
|
589
|
+
const { actions, warnings } = await compileAgentRuleActions({
|
|
590
|
+
name: "my-rule",
|
|
591
|
+
agents: ["claude-code"],
|
|
592
|
+
path: "CLAUDE.md",
|
|
593
|
+
scope: "repo",
|
|
594
|
+
targetDir: "apps/frontend",
|
|
595
|
+
}, dir, dir, realRoot, ["claude-code"], "/home/test", repo);
|
|
596
|
+
assert.equal(actions.length, 1);
|
|
597
|
+
assert.equal(warnings.length, 0);
|
|
598
|
+
const action = actions[0];
|
|
599
|
+
assert.equal(normalizeSlashes(action.target), `${repo}/apps/frontend/CLAUDE.md`, `expected target at {repo}/apps/frontend/CLAUDE.md, got: ${action.target}`);
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
await rm(dir, { recursive: true });
|
|
603
|
+
}
|
|
604
|
+
});
|
|
472
605
|
it("scope repo: emits a warning and skips when repo path is not provided", async () => {
|
|
473
606
|
const dir = await makeTmpDir();
|
|
474
607
|
try {
|
|
@@ -552,6 +685,21 @@ describe("compileAgentRuleActions", () => {
|
|
|
552
685
|
}
|
|
553
686
|
});
|
|
554
687
|
});
|
|
688
|
+
describe("compileAgentRuleReverts", () => {
|
|
689
|
+
it("injects targetDir into revert path", () => {
|
|
690
|
+
const home = "/home/test";
|
|
691
|
+
const repo = "/repo/test";
|
|
692
|
+
const actions = compileAgentRuleReverts({
|
|
693
|
+
name: "my-rule",
|
|
694
|
+
agents: ["claude-code"],
|
|
695
|
+
path: "CLAUDE.md",
|
|
696
|
+
scope: "repo",
|
|
697
|
+
targetDir: "apps/frontend",
|
|
698
|
+
}, ["claude-code"], home, repo);
|
|
699
|
+
assert.equal(actions.length, 1);
|
|
700
|
+
assert.equal(normalizeSlashes(actions[0]?.target ?? ""), `${repo}/apps/frontend/CLAUDE.md`);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
555
703
|
describe("compilePermissionsActions", () => {
|
|
556
704
|
it("returns zero actions and warnings when no detected agents overlap", () => {
|
|
557
705
|
const { actions, warnings } = compilePermissionsActions({
|
|
@@ -683,6 +831,121 @@ describe("compilePermissionsActions", () => {
|
|
|
683
831
|
assert.ok(agents.includes("codex"));
|
|
684
832
|
});
|
|
685
833
|
});
|
|
834
|
+
describe("compileHookActions", () => {
|
|
835
|
+
const validClaudeHookConfig = {
|
|
836
|
+
hooks: {
|
|
837
|
+
PreToolUse: [
|
|
838
|
+
{
|
|
839
|
+
matcher: "Bash",
|
|
840
|
+
hooks: [{ type: "command", command: "scripts/check.sh" }],
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
it("returns zero actions and warnings when no detected agents overlap", () => {
|
|
846
|
+
const { actions, warnings } = compileHookActions({ name: "check", agents: ["claude-code"], config: validClaudeHookConfig }, ["codex"], "/home/test");
|
|
847
|
+
assert.equal(actions.length, 0);
|
|
848
|
+
assert.equal(warnings.length, 0);
|
|
849
|
+
});
|
|
850
|
+
it("returns a config-patch action for claude-code targeting settings.json", () => {
|
|
851
|
+
const home = "/home/test";
|
|
852
|
+
const { actions, warnings } = compileHookActions({ name: "check", agents: ["claude-code"], config: validClaudeHookConfig }, ["claude-code"], home);
|
|
853
|
+
assert.equal(warnings.length, 0);
|
|
854
|
+
assert.equal(actions.length, 1);
|
|
855
|
+
const action = actions[0];
|
|
856
|
+
assert.equal(action.kind, "config-patch");
|
|
857
|
+
assert.ok(action.target.endsWith(".claude/settings.json".replace("/", path.sep)));
|
|
858
|
+
assert.equal(action.agent, "claude-code");
|
|
859
|
+
assert.equal(action.confidence, "documented");
|
|
860
|
+
});
|
|
861
|
+
it("accepts a hooks entry with no matcher property on the matcher object", () => {
|
|
862
|
+
const { actions, warnings } = compileHookActions({
|
|
863
|
+
name: "check",
|
|
864
|
+
agents: ["claude-code"],
|
|
865
|
+
config: {
|
|
866
|
+
hooks: {
|
|
867
|
+
Stop: [{ hooks: [{ type: "command", command: "notify.sh" }] }],
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
}, ["claude-code"], "/home/test");
|
|
871
|
+
assert.equal(warnings.length, 0);
|
|
872
|
+
assert.equal(actions.length, 1);
|
|
873
|
+
});
|
|
874
|
+
it("emits a warning and skips for agents without a hooks surface", () => {
|
|
875
|
+
const { actions, warnings } = compileHookActions({ name: "check", agents: ["gemini-cli"], config: {} }, ["gemini-cli"], "/home/test");
|
|
876
|
+
assert.equal(actions.length, 0);
|
|
877
|
+
assert.equal(warnings.length, 1);
|
|
878
|
+
});
|
|
879
|
+
it("throws on unknown top-level key in config for claude-code", () => {
|
|
880
|
+
assert.throws(() => compileHookActions({ name: "bad", agents: ["claude-code"], config: { bad_key: {} } }, ["claude-code"], "/home/test"), /unrecognized keys/);
|
|
881
|
+
});
|
|
882
|
+
it("throws when hooks value is not an object for claude-code", () => {
|
|
883
|
+
assert.throws(() => compileHookActions({ name: "bad", agents: ["claude-code"], config: { hooks: "string" } }, ["claude-code"], "/home/test"), /must define "hooks" as an object/);
|
|
884
|
+
});
|
|
885
|
+
it("throws when an event value is not an array", () => {
|
|
886
|
+
assert.throws(() => compileHookActions({
|
|
887
|
+
name: "bad",
|
|
888
|
+
agents: ["claude-code"],
|
|
889
|
+
config: { hooks: { PreToolUse: "not-array" } },
|
|
890
|
+
}, ["claude-code"], "/home/test"), /must be an array/);
|
|
891
|
+
});
|
|
892
|
+
it("throws when a matcher element is not an object", () => {
|
|
893
|
+
assert.throws(() => compileHookActions({
|
|
894
|
+
name: "bad",
|
|
895
|
+
agents: ["claude-code"],
|
|
896
|
+
config: { hooks: { PreToolUse: ["string"] } },
|
|
897
|
+
}, ["claude-code"], "/home/test"), /must be an object/);
|
|
898
|
+
});
|
|
899
|
+
it("throws when matcher.hooks is missing", () => {
|
|
900
|
+
assert.throws(() => compileHookActions({
|
|
901
|
+
name: "bad",
|
|
902
|
+
agents: ["claude-code"],
|
|
903
|
+
config: { hooks: { PreToolUse: [{ matcher: "Bash" }] } },
|
|
904
|
+
}, ["claude-code"], "/home/test"), /must be an array/);
|
|
905
|
+
});
|
|
906
|
+
it("throws when a hook command type is not 'command'", () => {
|
|
907
|
+
assert.throws(() => compileHookActions({
|
|
908
|
+
name: "bad",
|
|
909
|
+
agents: ["claude-code"],
|
|
910
|
+
config: {
|
|
911
|
+
hooks: {
|
|
912
|
+
PreToolUse: [
|
|
913
|
+
{
|
|
914
|
+
hooks: [{ type: "script", command: "check.sh" }],
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
}, ["claude-code"], "/home/test"), /must be "command"/);
|
|
920
|
+
});
|
|
921
|
+
it("throws when a hook command is not a string", () => {
|
|
922
|
+
assert.throws(() => compileHookActions({
|
|
923
|
+
name: "bad",
|
|
924
|
+
agents: ["claude-code"],
|
|
925
|
+
config: {
|
|
926
|
+
hooks: {
|
|
927
|
+
PreToolUse: [{ hooks: [{ type: "command", command: 123 }] }],
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
}, ["claude-code"], "/home/test"), /must be a string/);
|
|
931
|
+
});
|
|
932
|
+
it("throws when matcher.matcher is not a string", () => {
|
|
933
|
+
assert.throws(() => compileHookActions({
|
|
934
|
+
name: "bad",
|
|
935
|
+
agents: ["claude-code"],
|
|
936
|
+
config: {
|
|
937
|
+
hooks: {
|
|
938
|
+
PreToolUse: [
|
|
939
|
+
{
|
|
940
|
+
matcher: 42,
|
|
941
|
+
hooks: [{ type: "command", command: "check.sh" }],
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
}, ["claude-code"], "/home/test"), /must be a string when present/);
|
|
947
|
+
});
|
|
948
|
+
});
|
|
686
949
|
describe("compileAgentDefinitionActions", () => {
|
|
687
950
|
// Import is added inline to avoid modifying the top-level imports block
|
|
688
951
|
// (the function is async so we do a dynamic import once and reuse).
|