feat(install): add a copilot project target for skills and agents

GitHub Copilot CLI discovers SKILL.md skills from .github/skills/ and custom
agents from .github/agents/, so ECC's catalog can be installed there without a
conversion step. This adds a managed-project install target that places both.

Scope is deliberately limited to agents and skills. ECC hooks are written
against Claude Code's event model, and commands rely on slash-command argument
substitution Copilot does not provide, so neither is installed.

Agent frontmatter goes through an allowlist transform rather than a passthrough:
Copilot resolves the session model from user configuration, so a Claude model id
makes it warn and fall back on every invocation; Claude tool names are not
Copilot tool names, and Copilot governs tool access per session via
--allow-tool/--deny-tool. Unknown keys added to ECC agents later therefore
cannot leak into the Copilot copy.

Target selection stays permissive in supportsModule so dependency-anchor modules
still resolve; planOperations is what narrows the install, matching the
convention used by the other adapters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Brian SwigerandCopilot committed 2026-09-04 20:58:17 +01:00
1 parent c24e2d8f81
commit d58441072d
14 files changed
+451 -15

No files matched your search

+33 -8
View File
@@ -340,6 +340,7 @@ cd ECC
| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | Project-local `.kimi-code/` install |
| CodeBuddy | `./install.sh --profile minimal --target codebuddy` | Project-local `.codebuddy/` install |
| JoyCode | `./install.sh --profile minimal --target joycode` | Project-local `.joycode/` install |
| GitHub Copilot | `./install.sh --profile full --target copilot` | Installs skills to `.github/skills/` and agents to `.github/agents/` for Copilot CLI |
GitHub Copilot support is already included in this repository. `.github/copilot-instructions.md` provides the instruction layer, `.github/prompts/` contains the reusable `/plan`, `/tdd`, `/security-review`, `/build-fix`, and `/refactor` prompts, and `.vscode/settings.json` enables `chat.promptFiles`. GitHub Copilot CLI additionally discovers the `.agents/skills/` subset from a plain clone with no setup; see [Platform Support](#platform-support) for the full catalog and agents.
@@ -1588,7 +1589,7 @@ See [affaan-m/ECC#2065](https://github.com/affaan-m/ECC/issues/2065).
| Codex | Supported native plugin | Codex marketplace plugin or repo config | Native hooks require an explicit trust decision and do not use Claude's hook profiles. The legacy sync is compatibility-only. |
| Cursor | Beta project adapter | Selective installer into `.cursor/` | Agent discovery varies by Cursor build, and ECC's installer paths do not yet expose identical hook sets ([#2419](https://github.com/affaan-m/ECC/issues/2419)). |
| OpenCode | Beta built plugin | Build plugin, then selective installer | ECC ships a subset of the catalog; connect a provider and select a model in OpenCode ([#2617](https://github.com/affaan-m/ECC/issues/2617)). |
| GitHub Copilot | Instructions, prompts, and native skill/agent discovery (Copilot CLI) | Checked-in instructions and prompt files; skills and agents are discovered by Copilot CLI from existing paths | ECC's hook automations do not run. There is no `copilot` installer target, so placing the full skill and agent catalog is a manual step. |
| GitHub Copilot | Instructions, prompts, and native skill/agent discovery (Copilot CLI) | `./install.sh --profile full --target copilot`, or checked-in instructions and prompt files | ECC's hook automations do not run, and rules and commands are not installed by the target. |
| Gemini, Zed, Antigravity, Qwen, Hermes, OpenClaw, Kimi, CodeBuddy, JoyCode | Experimental/minimal adapters | Harness-specific selective target | File placement and instruction portability are tested; full Claude feature parity is not claimed. |
### Cross-tool capability map
@@ -1933,7 +1934,31 @@ Verified against Copilot CLI 1.0.83. All 286 skills load and report
name (`model: opus`, `model: sonnet`) emit a warning and fall back to the
session's default model, so remove or remap `model:` if you want to pin one.
There is no `copilot` target in `install.sh` yet — the steps above are manual.
#### Installing with the `copilot` target
The steps above copy files by hand. The `copilot` install target does the same
placement and additionally rewrites agent frontmatter so no warning is emitted:
```bash
./install.sh --profile full --target copilot
# Windows: ./install.ps1 --profile full --target copilot
```
That writes skills to `.github/skills/`, agents to `.github/agents/`, and an
install-state file to `.github/ecc-install-state.json`. Nothing is written
outside `.github/`.
The `full` profile places 285 skills and 68 agents. That is one skill fewer
than the 286 a manual `cp -r skills .github/skills` produces: `dmux-workflows`
belongs to the `orchestration` module, which ships tmux and shell worker
scripts and does not target Copilot. Copy `skills/dmux-workflows` by hand if
you want it.
Claude-only agent frontmatter (`model`, `tools`, `color`) is dropped during the
copy: Copilot resolves the model from your own configuration, and it governs
tool access per session through `--allow-tool`/`--deny-tool` rather than through
agent frontmatter. ECC's rules, commands, and hooks are not installed by this
target.
#### Feature coverage
@@ -1959,12 +1984,12 @@ not ported to Copilot. Copilot CLI does expose a hook system through its plugin
format, but ECC does not currently ship a Copilot plugin, so none of ECC's hooks
run there.
ECC also does not install a `copilot` target, so skill and agent placement is
manual (see above). Skills that shell out to bundled scripts assume the same
interpreters as on Claude Code. Copilot Chat in VS Code does not read
`.github/skills/` or `.github/agents/`; those paths are Copilot CLI surfaces.
The instruction and prompt layer is what carries ECC's coding philosophy into
Copilot Chat sessions.
ECC also does not install rules, commands, or hooks for Copilot. The `copilot`
target installs skills and agents only. Skills that shell out to bundled
scripts assume the same interpreters as on Claude Code. Copilot Chat in VS Code
does not read `.github/skills/` or `.github/agents/`; those paths are Copilot
CLI surfaces. The instruction and prompt layer is what carries ECC's coding
philosophy into Copilot Chat sessions.
</details>
<details>
+24
View File
@@ -12,6 +12,7 @@
"claude",
"claude-project",
"cursor",
"copilot",
"antigravity",
"codebuddy",
"joycode",
@@ -42,6 +43,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"codebuddy",
"joycode",
"qwen",
@@ -69,6 +71,7 @@
"claude",
"claude-project",
"cursor",
"copilot",
"antigravity",
"opencode",
"codebuddy",
@@ -131,6 +134,7 @@
"claude",
"claude-project",
"cursor",
"copilot",
"antigravity",
"codex",
"gemini",
@@ -230,6 +234,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -265,6 +270,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -291,6 +297,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"gemini",
"opencode",
"codebuddy",
@@ -368,6 +375,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -405,6 +413,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -451,6 +460,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -485,6 +495,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -524,6 +535,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -568,6 +580,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -596,6 +609,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -626,6 +640,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"gemini",
"opencode",
"codebuddy",
@@ -657,6 +672,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"gemini",
"opencode",
"codebuddy",
@@ -689,6 +705,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -722,6 +739,7 @@
"claude-project",
"cursor",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -780,6 +798,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -841,6 +860,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -883,6 +903,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -912,6 +933,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -949,6 +971,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
@@ -976,6 +999,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"opencode",
"codebuddy",
"joycode",
+1
View File
@@ -23,6 +23,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"gemini",
"opencode",
"codebuddy",
+1
View File
@@ -53,6 +53,7 @@
"cursor",
"antigravity",
"codex",
"copilot",
"gemini",
"opencode",
"codebuddy",
+1
View File
@@ -38,6 +38,7 @@ Targets:
cursor - Install rules, hooks, and bundled Cursor configs to ./.cursor/
antigravity - Install rules, workflows, skills, and agents to ./.agents/
codex - Install shared agents/config into ~/.codex/
copilot - Install skills and agents into ./.github/ for GitHub Copilot CLI (ECC hooks not configured)
gemini - Install project-local Gemini config into ./.gemini/
opencode - Install into OPENCODE_CONFIG_DIR, XDG_CONFIG_HOME/opencode, or ~/.config/opencode/
codebuddy - Install commands, agents, skills, and flattened rules into ./.codebuddy/
+17
View File
@@ -114,6 +114,23 @@ const HARNESS_CAPABILITIES = deepFreeze([
hooks: hooks('not-configured', false, 'ECC hooks are not configured by this adapter.'),
aliases: ['google-antigravity'],
},
{
id: 'copilot',
label: 'GitHub Copilot',
targetIds: ['copilot'],
channel: 'managed-project',
installMode: 'managed-project',
guidedReady: false,
availability: 'advanced',
destination: './.github',
scopes: [scope('project', 'copilot', './.github')],
hooks: hooks(
'not-configured',
false,
'ECC hooks target Claude Code events and are not installed for Copilot.'
),
aliases: ['github-copilot', 'copilot-cli'],
},
{
id: 'gemini',
label: 'Gemini CLI',
+4
View File
@@ -21,6 +21,7 @@ const {
inspectLegacyOpencodeState,
} = require('./install/opencode-legacy-migration');
const { adaptAntigravityAgent } = require('./install/antigravity-agent');
const { adaptCopilotAgent } = require('./install/copilot-agent');
const { buildInstallIndex, rewriteRelativeLinks } = require('./install/link-rewrite');
const { getInstallTargetAdapter, listInstallTargetAdapters } = require('./install-targets/registry');
const { resolveInvocationEnvironment } = require('./invocation-environment');
@@ -217,6 +218,9 @@ function transformCopyFileContent(operation, content) {
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
return adaptAntigravityAgent(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'copilot-agent-frontmatter') {
return adaptCopilotAgent(content, operation.sourceRelativePath);
}
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
}
+1 -1
View File
@@ -5,7 +5,7 @@ const { getInstallTargetAdapter, planInstallTargetScaffold } = require('./instal
const { resolveInvocationEnvironment } = require('./invocation-environment');
const DEFAULT_REPO_ROOT = path.join(__dirname, '../..');
const SUPPORTED_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed', 'hermes', 'openclaw', 'kimi', 'adal'];
const SUPPORTED_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity', 'codex', 'copilot', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed', 'hermes', 'openclaw', 'kimi', 'adal'];
const COMPONENT_FAMILY_PREFIXES = {
baseline: 'baseline:',
language: 'lang:',
@@ -0,0 +1,98 @@
const path = require('path');
const {
createInstallTargetAdapter,
createManagedOperation,
createManagedScaffoldOperation,
normalizeRelativePath,
} = require('./helpers');
// GitHub Copilot CLI discovers skills from `.github/skills/` and custom agents
// from `.github/agents/`. Those two directories are the entire supported
// surface for this target: ECC's hooks target Claude Code's event model, and
// commands rely on slash-command argument substitution Copilot does not
// provide, so neither is installed here.
const SUPPORTED_SOURCE_PREFIXES = ['agents', 'skills'];
function supportsCopilotSourcePath(sourceRelativePath) {
const normalizedPath = normalizeRelativePath(sourceRelativePath);
return SUPPORTED_SOURCE_PREFIXES.some(prefix => (
normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`)
));
}
function stripPrefix(normalizedPath, prefix) {
return normalizedPath === prefix ? '' : normalizedPath.slice(`${prefix}/`.length);
}
module.exports = createInstallTargetAdapter({
id: 'copilot-project',
target: 'copilot',
kind: 'project',
rootSegments: ['.github'],
installStatePathSegments: ['ecc-install-state.json'],
supportsModule(module) {
// Selection gating stays permissive so modules that only act as dependency
// anchors (rules-core, commands-core, platform-configs) still resolve.
// planOperations() is what narrows the install to agents and skills, so an
// unsupported path contributes zero operations rather than skipping the
// module and every module that depends on it.
const paths = Array.isArray(module && module.paths) ? module.paths : [];
return paths.length > 0;
},
planOperations(input, adapter) {
const modules = Array.isArray(input.modules)
? input.modules
: (input.module ? [input.module] : []);
const {
repoRoot,
projectRoot,
homeDir,
} = input;
const targetRoot = adapter.resolveRoot({ repoRoot, projectRoot, homeDir });
return modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths
.filter(supportsCopilotSourcePath)
.flatMap(sourceRelativePath => {
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
if (
normalizedSourcePath === 'agents'
|| normalizedSourcePath.startsWith('agents/')
) {
return [
createManagedOperation({
moduleId: module.id,
sourceRelativePath: normalizedSourcePath,
destinationPath: path.join(
targetRoot,
'agents',
stripPrefix(normalizedSourcePath, 'agents')
),
strategy: 'preserve-relative-path',
contentTransform: 'copilot-agent-frontmatter',
}),
];
}
if (
normalizedSourcePath === 'skills'
|| normalizedSourcePath.startsWith('skills/')
) {
return [
createManagedScaffoldOperation(
module.id,
normalizedSourcePath,
path.join(targetRoot, 'skills', stripPrefix(normalizedSourcePath, 'skills')),
'preserve-relative-path'
),
];
}
return [];
});
});
},
});
+2
View File
@@ -4,6 +4,7 @@ const claudeHome = require('./claude-home');
const claudeProject = require('./claude-project');
const codebuddyProject = require('./codebuddy-project');
const codexHome = require('./codex-home');
const copilotProject = require('./copilot-project');
const cursorProject = require('./cursor-project');
const geminiProject = require('./gemini-project');
const hermesHome = require('./hermes-home');
@@ -21,6 +22,7 @@ const ADAPTERS = Object.freeze([
cursorProject,
antigravityProject,
codexHome,
copilotProject,
geminiProject,
hermesHome,
opencodeHome,
+4
View File
@@ -21,6 +21,7 @@ const { cleanupLegacyAntigravityInstall } = require('./antigravity-legacy-migrat
const { cleanupLegacyOpencodeInstall } = require('./opencode-legacy-migration');
const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite');
const { adaptAntigravityAgent } = require('./antigravity-agent');
const { adaptCopilotAgent } = require('./copilot-agent');
function isMarkdownPath(filePath) {
return /\.(md|mdx|markdown)$/i.test(String(filePath || ''));
@@ -33,6 +34,9 @@ function transformInstallContent(operation, content) {
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
return adaptAntigravityAgent(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'copilot-agent-frontmatter') {
return adaptCopilotAgent(content, operation.sourceRelativePath);
}
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
}
+70
View File
@@ -0,0 +1,70 @@
'use strict';
// GitHub Copilot CLI custom agents are markdown files with `name` and
// `description` frontmatter. Claude Code's agent files additionally carry
// `model`, `tools`, and `color`, none of which transfer cleanly:
//
// model Copilot CLI resolves the session model from user configuration and
// plan entitlement. Carrying a Claude model id ("opus", "sonnet")
// makes Copilot emit "specifies model ... which is not available" on
// every invocation and silently fall back. Dropping the key selects
// the session default with no warning.
// tools Claude tool names (Read, Grep, Bash, ...) are not Copilot CLI tool
// names. Copilot governs tool access at the session level through
// --allow-tool/--deny-tool, so carrying the Claude list would assert
// a restriction Copilot does not actually apply.
// color Claude Code presentation only.
//
// The transform is therefore an allowlist rather than a denylist: unknown keys
// added to ECC agents later cannot silently leak into the Copilot copy.
const SUPPORTED_FRONTMATTER_KEYS = Object.freeze(['name', 'description']);
const REQUIRED_FRONTMATTER_KEYS = Object.freeze(['name', 'description']);
function splitFrontmatter(source, label) {
const match = String(source || '').match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
if (!match) {
throw new Error(`Cannot adapt Copilot agent ${label}: missing YAML frontmatter`);
}
// Keep YAML loading behind the transform boundary. Public help commands load
// the installer graph without executing a transform, including in hermetic
// packed-artifact checks where runtime dependencies are intentionally absent.
const frontmatter = require('js-yaml').load(match[1]);
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
throw new Error(`Cannot adapt Copilot agent ${label}: frontmatter must be an object`);
}
return {
frontmatter,
body: source.slice(match[0].length),
};
}
function adaptCopilotAgent(source, label = '<unknown>') {
const { frontmatter, body } = splitFrontmatter(source, label);
for (const key of REQUIRED_FRONTMATTER_KEYS) {
const value = frontmatter[key];
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`Cannot adapt Copilot agent ${label}: missing required frontmatter "${key}"`);
}
}
const adapted = {};
for (const key of SUPPORTED_FRONTMATTER_KEYS) {
if (Object.hasOwn(frontmatter, key)) {
adapted[key] = frontmatter[key];
}
}
const serialized = require('js-yaml')
.dump(adapted, { lineWidth: -1, noRefs: true })
.trimEnd();
return `---\n${serialized}\n---\n${body}`;
}
module.exports = {
adaptCopilotAgent,
SUPPORTED_FRONTMATTER_KEYS,
};
+171
View File
@@ -0,0 +1,171 @@
/**
* Contract tests for the GitHub Copilot install target.
*
* Covers the frontmatter transform in isolation and the adapter's operation
* planning, including the containment guarantee that nothing is written
* outside the project's .github/ directory.
*/
'use strict';
const assert = require('assert');
const path = require('path');
const REPO_ROOT = path.resolve(__dirname, '..', '..');
const { adaptCopilotAgent } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install', 'copilot-agent'));
const copilotAdapter = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-targets', 'copilot-project'));
const { getInstallTargetAdapter } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-targets', 'registry'));
const { SUPPORTED_INSTALL_TARGETS } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'install-manifests'));
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
passed += 1;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
const CLAUDE_AGENT = [
'---',
'name: architect',
'description: Software architecture specialist.',
'tools: Read, Grep, Glob',
'model: opus',
'color: teal',
'---',
'',
'## Role',
'',
'You are an architect.',
'',
].join('\n');
function planFor(modules) {
return copilotAdapter.planOperations({
repoRoot: '/repo',
projectRoot: '/project',
homeDir: '/home/user',
modules,
});
}
console.log('=== Testing Copilot install target ===\n');
test('registry resolves the copilot target and adapter id', () => {
assert.strictEqual(getInstallTargetAdapter('copilot').id, 'copilot-project');
assert.strictEqual(getInstallTargetAdapter('copilot-project').id, 'copilot-project');
assert.strictEqual(copilotAdapter.target, 'copilot');
assert.strictEqual(copilotAdapter.kind, 'project');
});
test('copilot is a supported install target', () => {
assert.ok(SUPPORTED_INSTALL_TARGETS.includes('copilot'));
});
test('transform keeps only name and description', () => {
const adapted = adaptCopilotAgent(CLAUDE_AGENT, 'agents/architect.md');
assert.match(adapted, /^---\n/);
assert.match(adapted, /name: architect/);
assert.match(adapted, /description: Software architecture specialist\./);
assert.doesNotMatch(adapted, /^model:/m);
assert.doesNotMatch(adapted, /^tools:/m);
assert.doesNotMatch(adapted, /^color:/m);
});
test('transform preserves the agent body verbatim', () => {
const adapted = adaptCopilotAgent(CLAUDE_AGENT, 'agents/architect.md');
assert.ok(adapted.includes('## Role'));
assert.ok(adapted.includes('You are an architect.'));
});
test('transform rejects a file without frontmatter', () => {
assert.throws(
() => adaptCopilotAgent('# no frontmatter\n', 'agents/broken.md'),
/missing YAML frontmatter/
);
});
test('transform rejects frontmatter that is not an object', () => {
assert.throws(
() => adaptCopilotAgent('---\n- a\n- b\n---\nbody\n', 'agents/broken.md'),
/frontmatter must be an object/
);
});
test('transform requires name and description', () => {
assert.throws(
() => adaptCopilotAgent('---\nname: only-name\n---\nbody\n', 'agents/broken.md'),
/missing required frontmatter "description"/
);
assert.throws(
() => adaptCopilotAgent('---\ndescription: only description\n---\nbody\n', 'agents/broken.md'),
/missing required frontmatter "name"/
);
});
test('agents are planned into .github/agents with the frontmatter transform', () => {
const operations = planFor([{ id: 'agents-core', paths: ['agents'] }]);
assert.strictEqual(operations.length, 1);
assert.strictEqual(operations[0].contentTransform, 'copilot-agent-frontmatter');
assert.strictEqual(operations[0].destinationPath, path.join('/project', '.github', 'agents'));
});
test('skills are planned into .github/skills without a transform', () => {
const operations = planFor([{ id: 'workflow-quality', paths: ['skills/tdd-workflow'] }]);
assert.strictEqual(operations.length, 1);
assert.strictEqual(operations[0].contentTransform, undefined);
assert.strictEqual(
operations[0].destinationPath,
path.join('/project', '.github', 'skills', 'tdd-workflow')
);
});
test('unsupported source paths produce no operations', () => {
const operations = planFor([
{ id: 'rules-core', paths: ['rules'] },
{ id: 'commands-core', paths: ['commands', 'scripts/harness-audit.js'] },
{ id: 'hooks-runtime', paths: ['hooks', 'scripts/hooks'] },
{ id: 'platform-configs', paths: ['.claude-plugin', '.codex', 'mcp-configs'] },
]);
assert.strictEqual(operations.length, 0);
});
test('supportsModule stays permissive so dependency anchors still resolve', () => {
// rules-core, commands-core and platform-configs ship no agents or skills but
// are dependencies of the skill modules. Gating them out here would cascade
// and skip every module that depends on them.
assert.strictEqual(copilotAdapter.supportsModule({ id: 'rules-core', paths: ['rules'] }), true);
assert.strictEqual(copilotAdapter.supportsModule({ id: 'empty', paths: [] }), false);
});
test('every planned operation is contained within .github', () => {
const operations = planFor([
{ id: 'agents-core', paths: ['agents', '.agents', 'AGENTS.md'] },
{ id: 'framework-language', paths: ['skills/api-design', 'skills/react-patterns'] },
{ id: 'rules-core', paths: ['rules'] },
]);
const root = path.join('/project', '.github');
assert.ok(operations.length > 0);
for (const operation of operations) {
const relative = path.relative(root, operation.destinationPath);
assert.ok(
relative && !relative.startsWith('..') && !path.isAbsolute(relative),
`operation escapes .github: ${operation.destinationPath}`
);
}
});
test('AGENTS.md and .agents are not installed by this target', () => {
const operations = planFor([{ id: 'agents-core', paths: ['.agents', 'AGENTS.md'] }]);
assert.strictEqual(operations.length, 0);
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed === 0 ? 0 : 1);
+24 -6
View File
@@ -33,12 +33,12 @@ function runTests() {
let passed = 0;
let failed = 0;
if (test('represents all 15 registered targets exactly once across 14 harnesses', () => {
if (test('represents all 16 registered targets exactly once across 15 harnesses', () => {
const catalogTargetIds = HARNESS_CAPABILITIES.flatMap(harness => harness.targetIds);
const adapterTargetIds = listInstallTargetAdapters().map(adapter => adapter.target);
assert.strictEqual(HARNESS_CAPABILITIES.length, 14);
assert.strictEqual(new Set(catalogTargetIds).size, 15);
assert.strictEqual(HARNESS_CAPABILITIES.length, 15);
assert.strictEqual(new Set(catalogTargetIds).size, 16);
assert.deepStrictEqual([...catalogTargetIds].sort(), [...SUPPORTED_INSTALL_TARGETS].sort());
assert.deepStrictEqual([...catalogTargetIds].sort(), [...adapterTargetIds].sort());
})) passed++; else failed++;
@@ -128,8 +128,26 @@ function runTests() {
assert.doesNotMatch(kimiHooks.note, /provider.*unsupported|Kimi.*unsupported/i);
})) passed++; else failed++;
if (test('does not advertise unregistered Copilot, Kiro, or Pi harnesses', () => {
for (const id of ['copilot', 'kiro', 'pi']) {
if (test('registers Copilot as an advanced project harness', () => {
const copilot = getHarnessCapability('copilot');
assert.ok(copilot);
assert.deepStrictEqual(copilot.targetIds, ['copilot']);
assert.strictEqual(copilot.installMode, 'managed-project');
assert.strictEqual(copilot.destination, './.github');
assert.strictEqual(copilot.guidedReady, false);
assert.strictEqual(copilot.availability, 'advanced');
assert.strictEqual(copilot.hooks.mode, 'not-configured');
assert.strictEqual(copilot.hooks.eccConfigured, false);
assert.match(copilot.hooks.note, /ECC hooks .*not installed for Copilot/i);
// Copilot stays out of the guided wizard, which is Claude/Codex/Kimi only.
assert.throws(
() => normalizeHarnessSelection('copilot'),
/Unknown guided harness selection|advanced/i
);
})) passed++; else failed++;
if (test('does not advertise unregistered Kiro or Pi harnesses', () => {
for (const id of ['kiro', 'pi']) {
assert.strictEqual(getHarnessCapability(id), null);
assert.throws(
() => normalizeHarnessSelection(id),
@@ -171,7 +189,7 @@ function runTests() {
const first = listHarnessCapabilities();
first.pop();
assert.strictEqual(listHarnessCapabilities().length, 14);
assert.strictEqual(listHarnessCapabilities().length, 15);
const guided = listGuidedHarnesses();
guided.reverse();