Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,20 +273,30 @@ receive the skills as files. Add `--json` for a machine-readable report, and
| Cursor | copied files | `~/.cursor/skills`, or `.agents/skills` with `--project` | Cursor 3.14.27, cursor-agent 2026.08.04-aaa8809 | a new session (a new chat) |
| Windsurf (ships as Devin) | copied files | `~/.codeium/windsurf/skills`, or `.windsurf/skills` with `--project` | Devin 3.6.27, devin CLI 3000.3.27 | a new session (the next `devin` run) |
| Antigravity | copied files | `~/.gemini/config/skills`, or `.agents/skills` with `--project` | Antigravity 2.0.1 | nothing — it re-reads its skills every turn, including in a conversation already open |
| GitHub Copilot | copied files | `~/.copilot/skills`, or `.agents/skills` with `--project` | not yet | — |
| GitHub Copilot | copied files | `~/.copilot/skills`, or `.agents/skills` with `--project` | Copilot CLI 1.0.80 | a new session, or `/skills reload` in the open one |

The project directories overlap in a way the rows above do not show: Windsurf
also reads `.agents/skills`, which is where `--project` puts things for Cursor,
Codex and Antigravity. A project-scope install aimed at one of them therefore
makes those skills visible in the others, and removing them for one changes what
the others see. Use user scope when you want a host's skills to itself.

"Not yet" means the skills directory for that assistant comes from its
documentation and has not been confirmed by a verification run of our own: the
install works, but we cannot promise the assistant reads from where we put the
files. GitHub Copilot is marked `(paths not yet verified)` in the report and
carries `"verified": false` in `--json`. Cursor and Windsurf were verified with
reply-cli 0.5.1, Antigravity with the release that adds it.
Codex, Antigravity and GitHub Copilot. A project-scope install aimed at one of
them therefore makes those skills visible in the others, and removing them for
one changes what the others see. Use user scope when you want a host's skills to
itself. GitHub Copilot reaches furthest: it also reads `.claude/skills` and
`.github/skills` in a repository, and `~/.claude/skills` for user scope, so a
Comment thread
CaptainReply marked this conversation as resolved.
Outdated
Claude Code install shows up in it as well.

Every assistant in the table has been verified against the version named. A host
added later may arrive with its skills directory taken from the vendor's
documentation alone; until someone confirms it by installing and using a skill,
the report marks that host `(paths not yet verified)` and `--json` carries
`"verified": false`. Cursor and Windsurf were verified with reply-cli 0.5.1,
Antigravity and GitHub Copilot with the release that adds them.

GitHub Copilot is two products over one directory: the `copilot` CLI and the
VS Code agent host, which ships `~/.copilot/skills` among the defaults of its
`chat.agentSkillsLocations` setting. The CLI is what the row above was verified
against. VS Code scans those directories once per window and does not watch them,
so an install made while it is open needs `Developer: Reload Window` rather than
just a new chat.

Antigravity is the one host that needs nothing to pick up new skills — it
re-reads them every turn, so an install lands in a conversation you already have
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/skills/hosts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('host registry', ()=>{

it('marks only the hosts we actually verified', ()=>{
expect(HOSTS.filter(h=>h.verified).map(h=>h.id))
.toEqual(['claude-code', 'codex', 'antigravity', 'cursor', 'windsurf']);
.toEqual(['claude-code', 'codex', 'antigravity', 'cursor', 'github-copilot', 'windsurf']);
});

it('marks only the hosts that pick up skills without a new session', ()=>{
Expand Down
37 changes: 15 additions & 22 deletions src/__tests__/skills/orchestrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,10 @@ describe('run_skills', ()=>{
expect(human_lines(report).join('\n')).toMatch(/new session/i);
});

// I5 (final review): some hosts still ship with paths taken from
// documentation rather than a verification run, and one of them is detected
// here alongside the verified hosts so this asserts both halves at once —
// the flag travels from the registry into a real report, and the warning
// still reaches a host that needs it.
//
// Which host that is comes from the registry rather than a name, because two
// PRs in a row had to edit this test after verifying one. When the list
// finally empties, the guard below fails loudly and someone decides what
// this should assert, instead of the assertion quietly passing on nothing.
const unverified = HOSTS.find(h=>!h.verified);
// Asserted with `verified` because it is the only mixed flag now that every
// host is verified, so a hardcoded stamp still fails.
const in_session = HOSTS.find(h=>!h.needs_new_session);
const flags_guard = 'every host needs a new session — decide what this should assert now';
// Derived from which directories exist rather than from a list of ids, in
// registry order, because creating one host's config directory can reveal
// another whose own directory is an ancestor of it — a fixed list silently
Expand All @@ -288,21 +281,21 @@ describe('run_skills', ()=>{
HOSTS.filter(h=>h.config_dirs.some(dir=>
made.some(m=>m === dir || m.startsWith(`${dir}${path.sep}`))));

it('carries each host\'s verified flag into the report', async()=>{
expect(unverified, 'every host is verified — decide what this should assert now').toBeDefined();
const made = ['.claude', '.cursor', unverified!.config_dirs[0]];
fs.mkdirSync(path.join(home, unverified!.config_dirs[0]), {recursive: true});
it('carries each host\'s registry flags into the report', async()=>{
expect(in_session, flags_guard).toBeDefined();
const made = ['.claude', '.cursor', in_session!.config_dirs[0]];
fs.mkdirSync(path.join(home, in_session!.config_dirs[0]), {recursive: true});
const report = await run_skills(opts());
expect(report.hosts.map(h=>[h.host, h.verified])).toEqual(
detected_defs(made).map(h=>[h.id, h.verified]),
expect(report.hosts.map(h=>[h.host, h.verified, h.needs_new_session])).toEqual(
detected_defs(made).map(h=>[h.id, h.verified, h.needs_new_session]),
);
expect(human_lines(report).join('\n')).toContain('paths not yet verified');
});

it('carries the verified flag on a host that was requested but not installed', async()=>{
expect(unverified, 'every host is verified — decide what this should assert now').toBeDefined();
const report = await run_skills(opts({agents: [unverified!.id]}));
expect(report.hosts[0].verified).toBe(false);
it('carries the registry flags on a host that was requested but not installed', async()=>{
expect(in_session, flags_guard).toBeDefined();
const report = await run_skills(opts({agents: [in_session!.id]}));
expect([report.hosts[0].verified, report.hosts[0].needs_new_session])
.toEqual([in_session!.verified, in_session!.needs_new_session]);
});

// Retiring a host leaves its journal branch behind, and nothing else in this
Expand Down
4 changes: 3 additions & 1 deletion src/skills/hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,11 @@ const HOSTS: Host_def[] = [
binaries: [],
binary_paths: [],
user_skills_dir: path.join('.copilot', 'skills'),
// It also reads .github/skills and .claude/skills for project scope.
project_skills_dir: path.join('.agents', 'skills'),
// `/skills reload` picks them up too, without a restart.
needs_new_session: true,
verified: false,
verified: true,
},
{
id: 'windsurf',
Expand Down