mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-16 13:15:36 +02:00
merge: preserve tested Copilot install sources 2960 and 2963
This commit is contained in:
@@ -408,8 +408,9 @@ cd ECC
|
||||
| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | Project-local `.kimi-code/` install · [Get Kimi Code](https://www.kimi.ai/code?aff=ecc) |
|
||||
| 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 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.
|
||||
|
||||
For a harness without a native ECC target, use the [manual adaptation guide](docs/MANUAL-ADAPTATION-GUIDE.md). It explains how to carry a small set of ECC skills and workflow instructions into chat-style tools without pretending hooks or native skill discovery are available.
|
||||
|
||||
@@ -1291,7 +1292,8 @@ Treat `stable`, `beta`, `experimental`, and `instruction-only` below as capabili
|
||||
| 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 | Instruction-only | Checked-in instructions and prompt files | No ECC hooks, runtime agents, delegation, or native skill discovery. |
|
||||
| GitHub Copilot Chat | Instruction-only | Checked-in instructions and prompt files | ECC hooks are not adapted. |
|
||||
| GitHub Copilot CLI | Beta project adapter | `--target copilot` places skills and adapted agents in `.github/` | ECC hooks, rules, and command shims are not installed by this 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. |
|
||||
|
||||
<details>
|
||||
@@ -1400,16 +1402,16 @@ See [affaan-m/ECC#2065](https://github.com/affaan-m/ECC/issues/2065).
|
||||
| Capability | Claude Code | Codex | Cursor | OpenCode | GitHub Copilot |
|
||||
|---|---|---|---|---|---|
|
||||
| Instructions | Native | Native `AGENTS.md` | Project rules | Plugin instructions | Native instruction file |
|
||||
| Skills | Native installed set | Native plugin set | Build-dependent/project set | Built subset | Prompt/instruction references only |
|
||||
| Agents/delegation | Native agents | Codex multi-agent roles; Claude agent files are not installed as roles | Build-dependent project agents | Plugin agents | Not supported |
|
||||
| ECC hooks | Native plugin hooks | Native reviewed subset with explicit trust | Cursor hook adapter; install-path differences remain | Plugin events | Not supported |
|
||||
| Skills | Native installed set | Native plugin set | Build-dependent/project set | Built subset | Native discovery (Copilot CLI) from `.github/skills/`, `.agents/skills/`, `.claude/skills/` |
|
||||
| Agents/delegation | Native agents | Codex multi-agent roles; Claude agent files are not installed as roles | Build-dependent project agents | Plugin agents | Copilot CLI custom agents via `.github/agents/` and `--agent` |
|
||||
| ECC hooks | Native plugin hooks | Native reviewed subset with explicit trust | Cursor hook adapter; install-path differences remain | Plugin events | Not ported; Copilot CLI has its own plugin hook system |
|
||||
| MCP configuration | Available, explicit activation | Native plugin manifest; legacy sync can merge TOML | Explicit project/user config | Provider/plugin config | Not supplied by ECC |
|
||||
| Parity with Claude Code | Primary reference | Partial | Partial | Partial | Not a parity target |
|
||||
| Parity with Claude Code | Primary reference | Partial | Partial | Partial | Partial |
|
||||
|
||||
**Key architectural decisions:**
|
||||
- **AGENTS.md** at root is the universal cross-tool file (read by Claude Code, Cursor, Codex, and OpenCode; GitHub Copilot uses `.github/copilot-instructions.md` instead)
|
||||
- **DRY adapter pattern** lets Cursor reuse Claude Code's hook scripts without duplication
|
||||
- **Skills format** (SKILL.md with YAML frontmatter) works across Claude Code, Codex, and OpenCode
|
||||
- **Skills format** (SKILL.md with YAML frontmatter) works across Claude Code, Codex, OpenCode, and GitHub Copilot CLI
|
||||
- Codex's narrower native hook set is supplemented by `AGENTS.md`, optional `model_instructions_file` overrides, and sandbox permissions
|
||||
|
||||
<details>
|
||||
@@ -1653,7 +1655,7 @@ For the full ECC OpenCode setup, either:
|
||||
<details>
|
||||
<summary><strong>GitHub Copilot support in depth</strong></summary>
|
||||
|
||||
ECC provides **GitHub Copilot support** for VS Code via Copilot Chat's native instruction and prompt file system. No extra tooling required.
|
||||
ECC supports **GitHub Copilot** on two surfaces: Copilot Chat in VS Code, via its native instruction and prompt file system, and **GitHub Copilot CLI**, which discovers the shared skills already present in this repository. The optional `copilot` install target places canonical skills and adapted agents in the CLI's native project directories.
|
||||
|
||||
#### What's included for GitHub Copilot
|
||||
|
||||
@@ -1674,6 +1676,99 @@ To use the workflow prompts in Copilot Chat:
|
||||
2. Click the **paperclip / attach** icon and select **Prompt...**, or type `/` and choose a prompt.
|
||||
3. Select the prompt (e.g. `plan`, `tdd`, `security-review`).
|
||||
|
||||
#### Copilot CLI: skills and agents
|
||||
|
||||
GitHub Copilot CLI discovers `SKILL.md` skills from `.github/skills/`,
|
||||
`.agents/skills/`, `.claude/skills/`, `~/.copilot/skills/`, and
|
||||
`~/.agents/skills/`, and custom agents from `.github/agents/`. ECC's skill and
|
||||
skill frontmatter is compatible. The managed `copilot` target adapts agent
|
||||
frontmatter to `name` and `description`, removing Claude-specific model and
|
||||
tool keys. Raw manual agent copies can retain unsupported metadata.
|
||||
|
||||
Because ECC ships a skill subset in `.agents/skills/`, a plain clone of this
|
||||
repository is picked up by Copilot CLI with no configuration at all:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/affaan-m/ECC.git && cd ECC
|
||||
copilot skill list
|
||||
# The inventory depends on the checkout and CLI version.
|
||||
```
|
||||
|
||||
In the contributor's September 2026 checkout, Copilot CLI 1.0.83 reported all 42 under `Project skills` with `source:
|
||||
"project"` and `enabled: true`. The three from `.claude/commands/` are Claude
|
||||
command files that Copilot surfaces as skills; they are reported with `path`
|
||||
set to the `.claude/commands` directory rather than to a per-skill subdirectory.
|
||||
|
||||
To check this with a temporary npm-distributed CLI, run the pinned build
|
||||
against a throwaway clone:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://github.com/affaan-m/ECC.git && cd ECC
|
||||
npx -y @github/copilot@1.0.82 skill list --json | jq '[.[] | select(.source=="project")] | length'
|
||||
# Count the current inventory rather than assuming a fixed total.
|
||||
```
|
||||
|
||||
That checkout's 39 + 3 split was reported by 1.0.82 from npm and by 1.0.83 installed
|
||||
locally, so the behaviour is not specific to one build or install method.
|
||||
|
||||
To expose the full catalog, place `skills/` where Copilot CLI looks:
|
||||
|
||||
```bash
|
||||
mkdir -p .github/skills
|
||||
cp -r skills/. .github/skills/
|
||||
copilot skill list --json > copilot-skills.json
|
||||
jq '[.[] | select(.path | contains(".github/skills"))] | length' copilot-skills.json
|
||||
# Compare with the current canonical skill catalog.
|
||||
```
|
||||
|
||||
Filter on the path rather than on `source`: the CLI can also discover skills
|
||||
from other project locations and de-duplicates entries with the same skill
|
||||
name. Counts depend on the checkout and CLI version. The file redirection also
|
||||
avoids truncated large JSON output observed with Copilot CLI 1.0.82 on macOS.
|
||||
|
||||
Copilot registers a skill under the `name` in its `SKILL.md` frontmatter, not
|
||||
its directory name, so catalog folders whose names differ from their frontmatter
|
||||
(for example `scientific-db-pubmed-database`, which declares
|
||||
`name: pubmed-database`) still load — they are just listed under the frontmatter
|
||||
name.
|
||||
|
||||
Agents work the same way:
|
||||
|
||||
```bash
|
||||
./install.sh --profile full --target copilot
|
||||
copilot --agent architect -p "Review this module's boundaries"
|
||||
```
|
||||
|
||||
The contributor verified native discovery with Copilot CLI 1.0.83. Manually
|
||||
copied agent files that declare a Claude model
|
||||
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.
|
||||
|
||||
#### 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 selects the modules declared for Copilot. It can place fewer
|
||||
skills than a manual copy of the entire canonical catalog because modules
|
||||
that require another host's runtime are excluded. List the resulting native
|
||||
inventory to see what this checkout installs; a count is not a capability grant.
|
||||
|
||||
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
|
||||
|
||||
| ECC Feature | Copilot equivalent |
|
||||
@@ -1686,12 +1781,24 @@ To use the workflow prompts in Copilot Chat:
|
||||
| Build error resolution | `build-fix` prompt |
|
||||
| Refactoring | `refactor` prompt |
|
||||
| Commit message format | Per-task instruction in `settings.json` |
|
||||
| Hooks / automation | Not supported (Copilot has no hook system) |
|
||||
| Agents / delegation | Not supported (Copilot has no subagent API) |
|
||||
| Skills | Native discovery in Copilot CLI (`.github/skills/`, `.agents/skills/`) |
|
||||
| Agents / delegation | Copilot CLI custom agents (`.github/agents/`, `--agent`) |
|
||||
| Hooks / automation | ECC's hooks are not ported (Copilot CLI has its own plugin hook system) |
|
||||
|
||||
#### Limitations
|
||||
|
||||
GitHub Copilot does not have a hook system or a subagent API, so ECC's hook automations (auto-format, TypeScript check, session persistence, dev-server guard) and agent delegation are unavailable. The instruction and prompt layer still brings the full ECC coding philosophy (standards, security, TDD, and workflow) into every Copilot Chat session.
|
||||
ECC's hook automations (auto-format, TypeScript check, session persistence,
|
||||
dev-server guard) are written against Claude Code's hook event model and are
|
||||
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 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>
|
||||
|
||||
@@ -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",
|
||||
@@ -232,6 +236,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -268,6 +273,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -294,6 +300,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"gemini",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
@@ -371,6 +378,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -438,6 +446,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -484,6 +493,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -518,6 +528,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -557,6 +568,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -601,6 +613,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -629,6 +642,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -686,6 +700,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"gemini",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
@@ -717,6 +732,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"gemini",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
@@ -749,6 +765,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -784,6 +801,7 @@
|
||||
"claude-project",
|
||||
"cursor",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -842,6 +860,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -903,6 +922,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -945,6 +965,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -974,6 +995,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -1011,6 +1033,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
@@ -1038,6 +1061,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
"joycode",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"gemini",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"copilot",
|
||||
"gemini",
|
||||
"opencode",
|
||||
"codebuddy",
|
||||
|
||||
@@ -39,6 +39,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/
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -42,6 +42,7 @@ const {
|
||||
validateManagedHooks,
|
||||
} = require('./install/claude-settings');
|
||||
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');
|
||||
@@ -245,6 +246,9 @@ function transformCopyFileContent(operation, content) {
|
||||
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
|
||||
return getDisabledOpenCodePluginContent();
|
||||
}
|
||||
if (operation.contentTransform === 'copilot-agent-frontmatter') {
|
||||
return adaptCopilotAgent(content, operation.sourceRelativePath);
|
||||
}
|
||||
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
|
||||
}
|
||||
|
||||
@@ -1919,9 +1923,11 @@ function prepareRepairMigration(plan, record) {
|
||||
statePreview: buildAdapterDerivedStatePreview(plan.statePreview, record),
|
||||
};
|
||||
const initialMigration = prepareClaudeSkillMigration(trustedPlan);
|
||||
const guardedMigration = record.adapter.id === 'codex-home'
|
||||
? prepareUserOwnedFileGuard(trustedPlan, initialMigration)
|
||||
: initialMigration;
|
||||
// Repair normalizes recorded target metadata to the adapter-derived roots.
|
||||
// Preserve its recorded operations as the ownership evidence.
|
||||
const guardedMigration = prepareUserOwnedFileGuard(
|
||||
trustedPlan, initialMigration, buildAdapterDerivedStatePreview(record.state, record)
|
||||
);
|
||||
const migration = trustedPlan.target === 'opencode'
|
||||
? require('./install/apply').prepareHookConsentMigration(trustedPlan, guardedMigration)
|
||||
: guardedMigration;
|
||||
@@ -2240,9 +2246,7 @@ function repairInstalledStates(options = {}) {
|
||||
const { assertOpenCodeActivationUnchanged } = require('./install/apply');
|
||||
assertOpenCodeActivationUnchanged(desiredPlan, operation, activationSnapshot);
|
||||
}
|
||||
if (record.adapter.id === 'codex-home') {
|
||||
assertNoNewUserOwnedFile(migration, operation, desiredPlan);
|
||||
}
|
||||
assertNoNewUserOwnedFile(migration, operation, desiredPlan);
|
||||
const repairedPath = executeRepairOperation(
|
||||
context.repoRoot,
|
||||
operation,
|
||||
|
||||
@@ -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,97 @@
|
||||
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 hasPrefix(normalizedPath, prefix) {
|
||||
return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
|
||||
}
|
||||
|
||||
function supportsCopilotSourcePath(sourceRelativePath) {
|
||||
const normalizedPath = normalizeRelativePath(sourceRelativePath);
|
||||
if (normalizedPath.split('/').includes('..')) return false;
|
||||
return SUPPORTED_SOURCE_PREFIXES.some(prefix => hasPrefix(normalizedPath, prefix));
|
||||
}
|
||||
|
||||
function stripPrefix(normalizedPath, prefix) {
|
||||
return normalizedPath === prefix ? '' : normalizedPath.slice(`${prefix}/`.length);
|
||||
}
|
||||
|
||||
function planSourcePathOperations(module, sourceRelativePath, targetRoot) {
|
||||
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
||||
|
||||
if (hasPrefix(normalizedSourcePath, '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 (hasPrefix(normalizedSourcePath, 'skills')) {
|
||||
return [
|
||||
createManagedScaffoldOperation(
|
||||
module.id,
|
||||
normalizedSourcePath,
|
||||
path.join(targetRoot, 'skills', stripPrefix(normalizedSourcePath, 'skills')),
|
||||
'preserve-relative-path'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
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 => planSourcePathOperations(
|
||||
module,
|
||||
sourceRelativePath,
|
||||
targetRoot
|
||||
));
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -52,6 +52,7 @@ const {
|
||||
} = require('./excluded-paths-reconciliation');
|
||||
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 || ''));
|
||||
@@ -70,6 +71,9 @@ function transformInstallContent(operation, content) {
|
||||
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
|
||||
return getDisabledOpenCodePluginContent();
|
||||
}
|
||||
if (operation.contentTransform === 'copilot-agent-frontmatter') {
|
||||
return adaptCopilotAgent(content, operation.sourceRelativePath);
|
||||
}
|
||||
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -37,10 +37,10 @@ function comparablePath(filePath) {
|
||||
*
|
||||
* All managed targets share this ownership boundary (#2964).
|
||||
*/
|
||||
function prepareUserOwnedFileGuard(plan, migration) {
|
||||
const previousState = pathExists(plan.installStatePath)
|
||||
? readInstallState(plan.installStatePath)
|
||||
: null;
|
||||
function prepareUserOwnedFileGuard(plan, migration, recordedState) {
|
||||
const previousState = recordedState === undefined
|
||||
? (pathExists(plan.installStatePath) ? readInstallState(plan.installStatePath) : null)
|
||||
: recordedState;
|
||||
if (previousState && (
|
||||
previousState.target.id !== plan.adapter.id
|
||||
|| comparablePath(previousState.target.root) !== comparablePath(plan.targetRoot)
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
|
||||
test('traversal segments produce no planned operations', () => {
|
||||
for (const value of ['agents/../../escape.md', 'skills/../escape.md', 'agents\\..\\..\\escape.md']) {
|
||||
assert.deepStrictEqual(planFor([{ id: 'agents-core', paths: [value] }]), []);
|
||||
}
|
||||
});
|
||||
|
||||
test('repair and uninstall preserve user-owned agents across project adapters', () => {
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
|
||||
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
||||
for (const [target, segments] of [['copilot', ['.github', 'agents']], ['antigravity', ['.agents', 'agents']]]) {
|
||||
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-ownership-'));
|
||||
try {
|
||||
const agent = path.join(project, ...segments, 'architect.md');
|
||||
fs.mkdirSync(path.dirname(agent), { recursive: true });
|
||||
fs.writeFileSync(agent, 'USER OWNED SENTINEL\n');
|
||||
const homeDir = path.join(project, 'home');
|
||||
const plan = createManifestInstallPlan({ target, moduleIds: ['agents-core'], projectRoot: project, homeDir, env: {} });
|
||||
applyInstallPlan(plan);
|
||||
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'install must preserve user file');
|
||||
const options = { repoRoot: REPO_ROOT, projectRoot: project, homeDir, targets: [target], env: {} };
|
||||
const repair = repairInstalledStates(options);
|
||||
assert.strictEqual(repair.summary.errorCount, 0, JSON.stringify(repair));
|
||||
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'repair must preserve user file');
|
||||
const state = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
|
||||
assert.ok(!state.operations.some(operation => operation.destinationPath === agent), 'repair must not claim ownership');
|
||||
uninstallInstalledStates(options);
|
||||
assert.strictEqual(fs.readFileSync(agent, 'utf8'), 'USER OWNED SENTINEL\n', 'uninstall must preserve user file');
|
||||
} finally {
|
||||
fs.rmSync(project, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user