@kuznai/inception-engine 0.21.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 +83 -8
- package/dist/src/config/agents.js +94 -11
- 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/hooks.d.ts +8 -0
- package/dist/src/core/adapters/hooks.js +86 -0
- package/dist/src/core/adapters/index.d.ts +5 -3
- package/dist/src/core/adapters/index.js +9 -5
- package/dist/src/core/adapters/mcp.js +15 -1
- package/dist/src/core/adapters/rules.js +50 -19
- package/dist/src/core/capabilities.d.ts +1 -1
- package/dist/src/core/capabilities.js +33 -2
- package/dist/src/core/deploy.js +1 -1
- package/dist/src/core/init.js +87 -23
- package/dist/src/core/preflight.js +28 -0
- package/dist/src/core/revert.js +5 -1
- package/dist/src/core/validation.d.ts +1 -0
- package/dist/src/core/validation.js +57 -0
- package/dist/src/schemas/manifest.d.ts +58 -0
- package/dist/src/schemas/manifest.js +49 -3
- package/dist/src/types.d.ts +10 -2
- package/dist/test/unit/adapters.test.js +280 -3
- package/dist/test/unit/deploy.test.js +31 -3
- 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 +91 -7
- package/dist/test/unit/revert.test.js +73 -0
- package/package.json +1 -1
|
@@ -88,9 +88,12 @@ export const McpServerEntrySchema = z.object({
|
|
|
88
88
|
// config files for agents that support them (e.g. GitHub Copilot's
|
|
89
89
|
// .vscode/mcp.json). For agents without scope-specific surfaces the
|
|
90
90
|
// adapter falls back to the global surface or emits a warning.
|
|
91
|
-
scope: z
|
|
91
|
+
scope: z
|
|
92
|
+
.enum(["global", "repo", "workspace", "devcontainer"])
|
|
93
|
+
.default("global"),
|
|
92
94
|
});
|
|
93
|
-
export const AgentRuleEntrySchema = z
|
|
95
|
+
export const AgentRuleEntrySchema = z
|
|
96
|
+
.object({
|
|
94
97
|
name: nameField,
|
|
95
98
|
agents: agentsField,
|
|
96
99
|
// Relative path to the rules/instruction file within the source bundle.
|
|
@@ -101,7 +104,33 @@ export const AgentRuleEntrySchema = z.object({
|
|
|
101
104
|
// file (default), "repo" targets the project-root instruction file within the
|
|
102
105
|
// deployed repository (e.g. {repo}/CLAUDE.md for claude-code), and "workspace"
|
|
103
106
|
// targets the agent's workspace-local instruction surface.
|
|
104
|
-
|
|
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
|
+
}
|
|
105
134
|
});
|
|
106
135
|
export const PermissionsEntrySchema = z.object({
|
|
107
136
|
name: nameField,
|
|
@@ -112,6 +141,13 @@ export const PermissionsEntrySchema = z.object({
|
|
|
112
141
|
// For opencode: { permissions: { allow?: string[], ask?: string[], deny?: string[] } }
|
|
113
142
|
config: z.record(z.string(), z.unknown()),
|
|
114
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
|
+
});
|
|
115
151
|
export const AgentDefinitionEntrySchema = z.object({
|
|
116
152
|
name: nameField,
|
|
117
153
|
agents: agentsField,
|
|
@@ -124,6 +160,14 @@ export const AgentDefinitionEntrySchema = z.object({
|
|
|
124
160
|
// within the deployed repository (default).
|
|
125
161
|
scope: z.enum(["global", "repo", "workspace"]).default("repo"),
|
|
126
162
|
});
|
|
163
|
+
export const HookEntrySchema = z.object({
|
|
164
|
+
name: nameField,
|
|
165
|
+
agents: agentsField,
|
|
166
|
+
// Raw hook config payload validated per agent by the hooks adapter.
|
|
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).
|
|
169
|
+
config: z.record(z.string(), z.unknown()),
|
|
170
|
+
});
|
|
127
171
|
export const ManifestSchema = z.object({
|
|
128
172
|
skills: z.array(SkillEntrySchema).superRefine((skills, ctx) => {
|
|
129
173
|
const seen = new Set();
|
|
@@ -144,6 +188,8 @@ export const ManifestSchema = z.object({
|
|
|
144
188
|
agentRules: z.array(AgentRuleEntrySchema).default([]),
|
|
145
189
|
permissions: z.array(PermissionsEntrySchema).default([]),
|
|
146
190
|
agentDefinitions: z.array(AgentDefinitionEntrySchema).default([]),
|
|
191
|
+
hooks: z.array(HookEntrySchema).optional(),
|
|
192
|
+
executionConfigs: z.array(ExecutionConfigEntrySchema).optional(),
|
|
147
193
|
});
|
|
148
194
|
// Parses the --agents CLI flag: comma-separated agent IDs → AgentId[]
|
|
149
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, 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" | "agentDefinitions";
|
|
8
|
+
export type CapabilityKind = "skills" | "mcpServers" | "agentRules" | "permissions" | "hooks" | "executionConfigs" | "agentDefinitions";
|
|
9
9
|
export interface SupportedAgentSurface {
|
|
10
10
|
status: "supported";
|
|
11
11
|
/**
|
|
@@ -67,6 +67,8 @@ export interface AgentProvenance {
|
|
|
67
67
|
mcpConfig?: Confidence;
|
|
68
68
|
agentRules?: Confidence;
|
|
69
69
|
permissions?: Confidence;
|
|
70
|
+
hooks?: Confidence;
|
|
71
|
+
executionConfig?: Confidence;
|
|
70
72
|
agentDefinitions?: Confidence;
|
|
71
73
|
}
|
|
72
74
|
export interface AgentConfig {
|
|
@@ -96,12 +98,18 @@ export interface AgentConfig {
|
|
|
96
98
|
agentRulesSupport?: AgentSurfaceSupport;
|
|
97
99
|
agentRulesRepoSupport?: AgentSurfaceSupport;
|
|
98
100
|
agentRulesWorkspaceSupport?: AgentSurfaceSupport;
|
|
101
|
+
agentRulesCopilotRepoSupport?: AgentSurfaceSupport;
|
|
102
|
+
agentRulesCopilotScopedSupport?: AgentSurfaceSupport;
|
|
99
103
|
permissionsSupport?: AgentSurfaceSupport;
|
|
104
|
+
hooksSupport?: AgentSurfaceSupport;
|
|
105
|
+
hooksRepoSupport?: AgentSurfaceSupport;
|
|
106
|
+
hooksWorkspaceSupport?: AgentSurfaceSupport;
|
|
100
107
|
agentDefinitionsSupport?: AgentSurfaceSupport;
|
|
101
108
|
agentDefinitionsRepoSupport?: AgentSurfaceSupport;
|
|
102
109
|
agentDefinitionsWorkspaceSupport?: AgentSurfaceSupport;
|
|
103
110
|
agentDefinitionsTomlSupport?: AgentSurfaceSupport;
|
|
104
111
|
agentDefinitionsTomlRepoSupport?: AgentSurfaceSupport;
|
|
112
|
+
executionConfigSupport?: AgentSurfaceSupport;
|
|
105
113
|
mcpDevcontainerSupport?: AgentSurfaceSupport;
|
|
106
114
|
mcpAgentFrontmatterSupport?: AgentSurfaceSupport;
|
|
107
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", () => {
|
|
@@ -137,13 +138,13 @@ describe("compileMcpServerActions", () => {
|
|
|
137
138
|
servers: { "my-mcp": { command: "s" } },
|
|
138
139
|
});
|
|
139
140
|
});
|
|
140
|
-
it("returns a frontmatter-emit action for antigravity MCP", () => {
|
|
141
|
+
it("returns a frontmatter-emit action for antigravity MCP with scope: repo", () => {
|
|
141
142
|
const home = "/home/test";
|
|
142
143
|
const { actions, warnings } = compileMcpServerActions({
|
|
143
144
|
name: "my-mcp",
|
|
144
145
|
agents: ["antigravity"],
|
|
145
146
|
config: { command: "s" },
|
|
146
|
-
scope: "
|
|
147
|
+
scope: "repo",
|
|
147
148
|
}, ["antigravity"], home, "/repo/test");
|
|
148
149
|
assert.equal(actions.length, 1);
|
|
149
150
|
assert.equal(warnings.length, 0);
|
|
@@ -151,6 +152,20 @@ describe("compileMcpServerActions", () => {
|
|
|
151
152
|
assert.equal(actions[0]?.agent, "antigravity");
|
|
152
153
|
assertPathEndsWith(actions[0]?.target ?? "", ".agents/rules/my-mcp.md", `expected target to end with .agents/rules/my-mcp.md, got ${actions[0]?.target}`);
|
|
153
154
|
});
|
|
155
|
+
it("returns a config-patch action for antigravity MCP with scope: global", () => {
|
|
156
|
+
const home = "/home/test";
|
|
157
|
+
const { actions, warnings } = compileMcpServerActions({
|
|
158
|
+
name: "my-mcp",
|
|
159
|
+
agents: ["antigravity"],
|
|
160
|
+
config: { command: "s" },
|
|
161
|
+
scope: "global",
|
|
162
|
+
}, ["antigravity"], home);
|
|
163
|
+
assert.equal(actions.length, 1);
|
|
164
|
+
assert.equal(warnings.length, 0);
|
|
165
|
+
assert.equal(actions[0]?.kind, "config-patch");
|
|
166
|
+
assert.equal(actions[0]?.agent, "antigravity");
|
|
167
|
+
assertPathEndsWith(actions[0]?.target ?? "", ".gemini/antigravity/mcp_config.json", `expected target to end with .gemini/antigravity/mcp_config.json, got ${actions[0]?.target}`);
|
|
168
|
+
});
|
|
154
169
|
it("throws when a supported MCP target is missing both command and url", () => {
|
|
155
170
|
assert.throws(() => compileMcpServerActions({
|
|
156
171
|
name: "my-mcp",
|
|
@@ -340,6 +355,115 @@ describe("compileAgentRuleActions", () => {
|
|
|
340
355
|
await rm(dir, { recursive: true });
|
|
341
356
|
}
|
|
342
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
|
+
});
|
|
343
467
|
it("throws when rules source file does not exist", async () => {
|
|
344
468
|
const dir = await makeTmpDir();
|
|
345
469
|
try {
|
|
@@ -455,6 +579,29 @@ describe("compileAgentRuleActions", () => {
|
|
|
455
579
|
await rm(dir, { recursive: true });
|
|
456
580
|
}
|
|
457
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
|
+
});
|
|
458
605
|
it("scope repo: emits a warning and skips when repo path is not provided", async () => {
|
|
459
606
|
const dir = await makeTmpDir();
|
|
460
607
|
try {
|
|
@@ -538,6 +685,21 @@ describe("compileAgentRuleActions", () => {
|
|
|
538
685
|
}
|
|
539
686
|
});
|
|
540
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
|
+
});
|
|
541
703
|
describe("compilePermissionsActions", () => {
|
|
542
704
|
it("returns zero actions and warnings when no detected agents overlap", () => {
|
|
543
705
|
const { actions, warnings } = compilePermissionsActions({
|
|
@@ -669,6 +831,121 @@ describe("compilePermissionsActions", () => {
|
|
|
669
831
|
assert.ok(agents.includes("codex"));
|
|
670
832
|
});
|
|
671
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
|
+
});
|
|
672
949
|
describe("compileAgentDefinitionActions", () => {
|
|
673
950
|
// Import is added inline to avoid modifying the top-level imports block
|
|
674
951
|
// (the function is async so we do a dynamic import once and reuse).
|
|
@@ -30,6 +30,7 @@ const testManifest = {
|
|
|
30
30
|
agentRules: [],
|
|
31
31
|
permissions: [],
|
|
32
32
|
agentDefinitions: [],
|
|
33
|
+
hooks: [],
|
|
33
34
|
};
|
|
34
35
|
describe("planDeploy", () => {
|
|
35
36
|
it("creates actions for detected agents only", async () => {
|
|
@@ -67,6 +68,32 @@ describe("planDeploy", () => {
|
|
|
67
68
|
await rm(sourceDir, { recursive: true });
|
|
68
69
|
}
|
|
69
70
|
});
|
|
71
|
+
it("includes executionConfigs actions for supported detected agents", async () => {
|
|
72
|
+
const sourceDir = await makeTmpDir();
|
|
73
|
+
try {
|
|
74
|
+
const manifest = {
|
|
75
|
+
...testManifest,
|
|
76
|
+
executionConfigs: [
|
|
77
|
+
{
|
|
78
|
+
name: "gemini-safety",
|
|
79
|
+
agents: ["gemini-cli"],
|
|
80
|
+
config: { safeMode: true },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
await createSkillSource(sourceDir, "skills/test-skill");
|
|
85
|
+
const { actions } = await planDeploy(manifest, sourceDir, ["gemini-cli"], "/home/test");
|
|
86
|
+
const executionAction = actions.find((action) => action.kind === "config-patch" &&
|
|
87
|
+
action.agent === "gemini-cli" &&
|
|
88
|
+
action.skill === "gemini-safety");
|
|
89
|
+
assert.ok(executionAction, "expected planDeploy to include the gemini executionConfigs patch");
|
|
90
|
+
assert.deepEqual(executionAction.patch, { safeMode: true });
|
|
91
|
+
assertPathEndsWith(executionAction.target, ".gemini/settings.json", `expected Gemini execution config target under .gemini/settings.json, got: ${executionAction.target}`);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await rm(sourceDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
70
97
|
it("throws when skill source path does not exist", async () => {
|
|
71
98
|
const sourceDir = await makeTmpDir();
|
|
72
99
|
try {
|
|
@@ -647,8 +674,9 @@ describe("planDeploy", () => {
|
|
|
647
674
|
assert.ok(geminiAction, "expected a gemini-cli action");
|
|
648
675
|
assert.match(geminiAction.target, /[\\/]\.gemini[\\/]settings\.json$/);
|
|
649
676
|
assert.ok(antigravityAction, "expected an antigravity action");
|
|
650
|
-
// Antigravity's target for mcpServer is .
|
|
651
|
-
assert.
|
|
677
|
+
// Antigravity's target for global mcpServer is .gemini/antigravity/mcp_config.json
|
|
678
|
+
assert.equal(antigravityAction.kind, "config-patch");
|
|
679
|
+
assert.match(antigravityAction.target, /[\\/]\.gemini[\\/]antigravity[\\/]mcp_config\.json$/);
|
|
652
680
|
}
|
|
653
681
|
finally {
|
|
654
682
|
await rm(sourceDir, { recursive: true });
|
|
@@ -802,7 +830,7 @@ describe("planDeploy", () => {
|
|
|
802
830
|
name: "my-tool",
|
|
803
831
|
agents: ["antigravity"],
|
|
804
832
|
config: { command: "npx", args: ["-y", "my-tool"] },
|
|
805
|
-
scope: "
|
|
833
|
+
scope: "repo",
|
|
806
834
|
},
|
|
807
835
|
],
|
|
808
836
|
agentRules: [],
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { cp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
|
+
import { runInit } from "../../src/core/init.js";
|
|
7
|
+
import { logger } from "../../src/logger.js";
|
|
6
8
|
import { makeTmpDir } from "../helpers/fs.js";
|
|
7
9
|
import { assertPathEndsWith, normalizeSlashes } from "../helpers/path.js";
|
|
10
|
+
logger.silence();
|
|
8
11
|
const PROJECT_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
9
12
|
const FIXTURE_DIR = path.join(PROJECT_ROOT, "test", "fixtures", "readme-sample");
|
|
10
13
|
function run(args, env) {
|
|
@@ -151,3 +154,84 @@ describe("init against readme-sample fixture", () => {
|
|
|
151
154
|
}
|
|
152
155
|
});
|
|
153
156
|
});
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Block 3: init discovery of GitHub Copilot native instruction surfaces
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
describe("init Copilot native instruction discovery", () => {
|
|
161
|
+
it("runInit dryRun succeeds when .github/copilot-instructions.md is present", async () => {
|
|
162
|
+
const dir = await makeTmpDir();
|
|
163
|
+
try {
|
|
164
|
+
await mkdir(path.join(dir, ".github"), { recursive: true });
|
|
165
|
+
await writeFile(path.join(dir, ".github", "copilot-instructions.md"), "# Copilot instructions");
|
|
166
|
+
// Use dryRun to avoid writing inception.json
|
|
167
|
+
const result = await runInit({
|
|
168
|
+
directory: dir,
|
|
169
|
+
agents: null,
|
|
170
|
+
dryRun: true,
|
|
171
|
+
force: false,
|
|
172
|
+
verbose: false,
|
|
173
|
+
});
|
|
174
|
+
assert.equal(result, 0);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await rm(dir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
it("init --plan output includes copilot-repo scope for .github/copilot-instructions.md", async () => {
|
|
181
|
+
const dir = await makeTmpDir();
|
|
182
|
+
try {
|
|
183
|
+
await mkdir(path.join(dir, ".github"), { recursive: true });
|
|
184
|
+
await writeFile(path.join(dir, ".github", "copilot-instructions.md"), "# Copilot instructions");
|
|
185
|
+
const { stdout, code } = await run(["init", dir, "--plan"]);
|
|
186
|
+
assert.equal(code, 0, `init --plan failed:\n${stdout}`);
|
|
187
|
+
const manifest = extractPlanJson(stdout);
|
|
188
|
+
const copilotEntry = manifest.agentRules.find((r) => r.scope === "copilot-repo");
|
|
189
|
+
assert.ok(copilotEntry, `expected a copilot-repo entry in agentRules, got: ${JSON.stringify(manifest.agentRules)}`);
|
|
190
|
+
assert.deepEqual(copilotEntry.agents, ["github-copilot"]);
|
|
191
|
+
assertPathEndsWith(copilotEntry.path, ".github/copilot-instructions.md", `copilot-repo entry path should end with .github/copilot-instructions.md`);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await rm(dir, { recursive: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
it("init --plan output includes copilot-scoped scope for .github/instructions/*.instructions.md", async () => {
|
|
198
|
+
const dir = await makeTmpDir();
|
|
199
|
+
try {
|
|
200
|
+
await mkdir(path.join(dir, ".github", "instructions"), {
|
|
201
|
+
recursive: true,
|
|
202
|
+
});
|
|
203
|
+
await writeFile(path.join(dir, ".github", "instructions", "typescript.instructions.md"), "# TypeScript scoped instructions");
|
|
204
|
+
await writeFile(path.join(dir, ".github", "instructions", "python.instructions.md"), "# Python scoped instructions");
|
|
205
|
+
const { stdout, code } = await run(["init", dir, "--plan"]);
|
|
206
|
+
assert.equal(code, 0, `init --plan failed:\n${stdout}`);
|
|
207
|
+
const manifest = extractPlanJson(stdout);
|
|
208
|
+
const scopedEntries = manifest.agentRules.filter((r) => r.scope === "copilot-scoped");
|
|
209
|
+
assert.equal(scopedEntries.length, 2, `expected 2 copilot-scoped entries, got ${scopedEntries.length}: ${JSON.stringify(manifest.agentRules)}`);
|
|
210
|
+
for (const entry of scopedEntries) {
|
|
211
|
+
assert.deepEqual(entry.agents, ["github-copilot"]);
|
|
212
|
+
}
|
|
213
|
+
const names = scopedEntries.map((e) => e.name).sort();
|
|
214
|
+
assert.deepEqual(names, ["python", "typescript"]);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await rm(dir, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
it("non-.github/ copilot-instructions.md still maps to claude-code (backward compat)", async () => {
|
|
221
|
+
const dir = await makeTmpDir();
|
|
222
|
+
try {
|
|
223
|
+
await mkdir(path.join(dir, "rules"), { recursive: true });
|
|
224
|
+
await writeFile(path.join(dir, "rules", "copilot-instructions.md"), "# Copilot instructions");
|
|
225
|
+
const { stdout, code } = await run(["init", dir, "--plan"]);
|
|
226
|
+
assert.equal(code, 0, `init --plan failed:\n${stdout}`);
|
|
227
|
+
const manifest = extractPlanJson(stdout);
|
|
228
|
+
const entry = manifest.agentRules.find((r) => r.path.endsWith("copilot-instructions.md"));
|
|
229
|
+
assert.ok(entry, `expected a copilot-instructions.md entry, got: ${JSON.stringify(manifest.agentRules)}`);
|
|
230
|
+
assert.ok(entry.agents.includes("claude-code"), `expected agents to include claude-code, got: ${JSON.stringify(entry.agents)}`);
|
|
231
|
+
assert.notEqual(entry.scope, "copilot-repo", "rules/ copilot-instructions.md should not use copilot-repo scope");
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
await rm(dir, { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|