From 33261f753d1fb069729c575e503153e0873f523e Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 13 Jun 2026 15:04:17 +0300 Subject: [PATCH 1/4] fix(skills): install bundled skills to .github/skills/ (closes #1126) Per the official Agent Skills spec docs (GitHub, VS Code), the canonical project-level custom-skills location is .github/skills/. The legacy .copilot/skills/ is invisible to all Copilot surfaces except Squad itself - cloud agent, CLI outside Squad, VS Code extension, @copilot coding agent all ignore it. Changes: * squad init writes bundled skills to .github/skills/{name}/SKILL.md * squad upgrade does the same AND migrates legacy .copilot/skills/{name}/ -> .github/skills/{name}/ for manifest skills only (user-added skills at .copilot/skills/ are preserved). Tombstones empty legacy dir. * TEMPLATE_MANIFEST destinations: 10 skill entries retargeted * ENSURE_DIRECTORIES: .copilot/skills -> .github/skills * squad.agent.md narrative: 5-path scan order now lists .github/skills as primary, .copilot/skills as legacy. Personal scope unchanged. * All user-facing docs updated (README, spawn-reference, squad-commands skill, release-process skill, build.ts skill creation paths, SDK type-comment paths). Migration semantics in upgrade.ts: * Move-only-if-new-location-empty: legacy at .copilot/skills/, new location empty -> move + tombstone legacy * Tombstone-on-collision: both locations exist -> remove legacy, new wins (then syncAllSkills overwrites manifest skills per overwriteOnUpgrade=true semantics) * Preserve user-added: skills NOT in TEMPLATE_MANIFEST stay at .copilot/skills/ untouched * All best-effort with try/catch - disk failures do not block upgrade Tests (188/188 pass): * New: init.test.ts asserts canonical path + legacy NOT created * New: upgrade.test.ts asserts manifest migration + user-skill preservation + collision tombstoning * Updated: 8 existing test files retargeted to .github/skills NOT changed (intentional): * .copilot/skills/ stays in coordinator skill-discovery scan order for backward compat with user-added skills * ~/.copilot/skills/ (personal scope) unchanged - that's Copilot CLI's official personal-skills location * Runtime skill-loader tests (skill-source, skills-export-import, tools, skill-script-loader) unchanged - those test loader behavior which still supports .copilot/skills/ as a scan path Closes #1126 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../fix-1126-skills-to-github-skills.md | 68 +++++++++++++++ .squad-templates/spawn-reference.md | 2 +- .squad-templates/squad.agent.md | 8 +- .squad/skills/release-process/SKILL.md | 4 +- .squad/skills/squad/SKILL.md | 4 +- packages/squad-cli/README.md | 10 +-- packages/squad-cli/src/cli/commands/build.ts | 6 +- packages/squad-cli/src/cli/core/templates.ts | 22 ++--- packages/squad-cli/src/cli/core/upgrade.ts | 86 ++++++++++++++++++- .../templates/skills/release-process/SKILL.md | 4 +- .../squad-cli/templates/spawn-reference.md | 2 +- .../templates/squad.agent.md.template | 8 +- packages/squad-sdk/src/config/init.ts | 15 +++- .../squad-sdk/src/skills/handler-types.ts | 2 +- packages/squad-sdk/src/tools/index.ts | 6 +- .../templates/skills/release-process/SKILL.md | 4 +- .../squad-sdk/templates/spawn-reference.md | 2 +- .../templates/squad.agent.md.template | 8 +- templates/spawn-reference.md | 2 +- templates/squad.agent.md.template | 8 +- test/builtin-skills.test.ts | 8 +- test/cli/init-upgrade-parity.test.ts | 2 +- test/cli/init.test.ts | 4 +- test/cli/upgrade.test.ts | 67 ++++++++++++++- test/human-journeys.test.ts | 2 +- test/init-sdk.test.ts | 4 +- test/init.test.ts | 32 ++++++- test/repl-ux-fixes.test.ts | 4 +- 28 files changed, 320 insertions(+), 74 deletions(-) create mode 100644 .changeset/fix-1126-skills-to-github-skills.md diff --git a/.changeset/fix-1126-skills-to-github-skills.md b/.changeset/fix-1126-skills-to-github-skills.md new file mode 100644 index 000000000..e44599011 --- /dev/null +++ b/.changeset/fix-1126-skills-to-github-skills.md @@ -0,0 +1,68 @@ +--- +"@bradygaster/squad-sdk": minor +"@bradygaster/squad-cli": minor +--- + +Move bundled skills from `.copilot/skills/` to `.github/skills/` so they're visible to all Copilot surfaces (closes #1126) + +## Symptom (per #1126) + +Squad-bundled skills installed at `.copilot/skills/` are **invisible to every Copilot surface except Squad itself**: +- ❌ GitHub Copilot cloud agent +- ❌ Copilot CLI (outside Squad sessions) +- ❌ VS Code Copilot extension (agent mode) +- ❌ `@copilot` coding agent on issues +- ❌ Any future Copilot surface + +Per the [official Agent Skills docs](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills), [add-skills docs](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/cloud-agent/add-skills), and [VS Code docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills), the canonical **project-level** custom-skills location is `.github/skills/`. `.copilot/skills/` at repo root is not recognized by any Copilot surface — the home-directory equivalent `~/.copilot/skills/` IS recognized for **personal** skills, which is the source of the original mistake. + +## Fix + +1. **`squad init`** writes bundled skills to `.github/skills/{name}/SKILL.md` (was `.copilot/skills/`). +2. **`squad upgrade`** does the same AND auto-migrates legacy `.copilot/skills/{manifest-skill}/` into `.github/skills/{manifest-skill}/` (best-effort, preserves user-added non-manifest skills at `.copilot/skills/`, tombstones the legacy dir when empty). +3. **`TEMPLATE_MANIFEST`** destinations rewritten: all 10 skill entries now target `../.github/skills/` instead of `../.copilot/skills/`. +4. **`ENSURE_DIRECTORIES`** (upgrade.ts) updated so existing squads get `.github/skills/` created on upgrade. +5. **squad.agent.md** narrative updated: 5-path scan order now lists `.github/skills/` as path #2 (Copilot CLI's canonical custom-skills location) and `.copilot/skills/` as path #3 (Legacy install path; `squad upgrade` migrates). Personal scope (`~/.copilot/skills/`) preserved as-is. +6. **All other docs** (`spawn-reference.md`, `README.md`, `squad-commands` skill, `release-process` skill, `build.ts`, SDK type comments) updated to reference `.github/skills/` as the install destination. + +## Migration semantics (`squad upgrade`) + +`migrateLegacyCopilotSkills()` runs **before** `syncAllSkills`: + +| Scenario | Migration action | User-added skills at `.copilot/skills/` | +|---|---|---| +| `.copilot/skills/{manifest-skill}/` exists, `.github/skills/{manifest-skill}/` does NOT | Move legacy → new; remove legacy | Untouched | +| `.copilot/skills/{manifest-skill}/` exists AND `.github/skills/{manifest-skill}/` exists | Tombstone legacy (new wins) | Untouched | +| `.copilot/skills/my-custom-skill/` (NOT in TEMPLATE_MANIFEST) | Left alone | Preserved | +| `.copilot/skills/` becomes empty after migration | Directory removed | n/a | + +All migration steps are best-effort with try/catch — disk-write failures don't block upgrade. + +## Tests + +New regression tests: +- `test/init.test.ts > should install Squad-bundled skills at .github/skills/...` — asserts canonical path, asserts legacy path is NOT created +- `test/cli/upgrade.test.ts > should migrate manifest skills from .copilot/skills/ to .github/skills/` — asserts manifest skill moves, user-added skill preserved +- `test/cli/upgrade.test.ts > should NOT clobber a customized .github/skills/{name} if the legacy copy exists` — asserts both-locations case tombstones legacy without losing the new + +Updated existing tests: +- `test/builtin-skills.test.ts` regex now matches `.github/skills/` +- `test/cli/init.test.ts`, `test/init.test.ts`, `test/init-sdk.test.ts`, `test/cli/upgrade.test.ts`, `test/human-journeys.test.ts`, `test/repl-ux-fixes.test.ts`, `test/cli/init-upgrade-parity.test.ts` — install-path assertions updated to `.github/skills/` + +**188/188 init/upgrade/builtin tests pass; `npm run lint` clean.** + +## What's NOT changed (intentional) + +- **`.copilot/skills/` scan path stays in `squad.agent.md`'s 5-path skill discovery** — the coordinator still discovers user-added skills at the legacy location for backward compat; only Squad-installed (manifest) skills migrate. +- **`~/.copilot/skills/` (personal scope) is unchanged** — that's Copilot CLI's official personal-skills location and remains valid. +- **`test/skill-source.test.ts`, `test/skills-export-import.test.cjs`, `test/tools.test.ts`, `test/skill-script-loader.test.ts`** are NOT touched — they test the runtime skill loader and tool behavior, which still supports `.copilot/skills/` as a valid scan path. + +## Composability + +- **Disjoint from #1292, #1293, #1295, #1297, #1298, #1300, #1301, #1302, #1303** — touches the same `TEMPLATE_MANIFEST` array as #1292/#1295/#1303 but only modifies the destination path, not the source or entry list. Conflicts on `MANIFEST_SKILL_NAMES` are trivial: take the union of skill names with the `../.github/skills/` destination from this PR. +- This is the **canonical fix** for #1126; closes that issue. + +## Out of scope (separate follow-ups) + +- Backward-compat shim that adds a `.copilot/skills -> .github/skills` symlink. Not needed because users won't be looking at `.copilot/skills/` anymore once their tools find skills at `.github/skills/`. Filed as a follow-up if anyone reports broken muscle memory. +- A `squad doctor --skills` check that warns when `.copilot/skills/` still has manifest skills after upgrade (suggesting the migration silently failed). Worth adding to the next maintenance pass. diff --git a/.squad-templates/spawn-reference.md b/.squad-templates/spawn-reference.md index 94c2509d9..a2571c820 100644 --- a/.squad-templates/spawn-reference.md +++ b/.squad-templates/spawn-reference.md @@ -81,7 +81,7 @@ prompt: | Read `decisions.md` with `squad_state_read` when state tools are available; otherwise fall back to `.squad/decisions.md`. If .squad/identity/wisdom.md exists, read it before starting work. If .squad/identity/now.md exists, read it at spawn time. - Check project skill directories (.squad/skills/, .copilot/skills/, .github/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. + Check project skill directories (.squad/skills/, .github/skills/, .copilot/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. Read any relevant SKILL.md files before working. ⚠️ WORK FRESHNESS: When determining what to work on: diff --git a/.squad-templates/squad.agent.md b/.squad-templates/squad.agent.md index d17c63553..61613e783 100644 --- a/.squad-templates/squad.agent.md +++ b/.squad-templates/squad.agent.md @@ -329,7 +329,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | PRD intake ("here's the PRD", "read the PRD at X", pastes spec) | Follow PRD Mode (see that section) | | Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | | Ralph commands ("Ralph, go", "keep working", "Ralph, status", "Ralph, idle") | Follow Ralph — Work Monitor (see that section) | -| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.copilot/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | +| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.github/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | | "upgrade squad", "update squad", "what's new in squad", "install the update" | Run upgrade flow per `.squad/templates/session-init-reference.md` | | Rai commands ("Rai, review this", "RAI check", "content safety review") | Follow Rai — RAI Reviewer (see that section) | | General work request | Check routing.md, spawn best match + any anticipatory agents | @@ -337,11 +337,11 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | - + **Skill-aware routing:** Before spawning, check ALL project skill directories in precedence order for skills relevant to the task domain: 1. `.squad/skills/` — **Team-earned skills** (highest precedence). Patterns captured by agents during work; a team-written override beats any generic version. -2. `.copilot/skills/` — **Project playbook.** Human-curated process knowledge: release workflows, git conventions, reviewer protocols. -3. `.github/skills/` — **Generic project skills.** Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`; common location for shared-repo skills. +2. `.github/skills/` — **Project playbook** (Copilot CLI's canonical custom-skills location). Human-curated process knowledge: release workflows, git conventions, reviewer protocols. Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`. `squad init` and `squad upgrade` install Squad's bundled skills here. +3. `.copilot/skills/` — **Legacy install path** (pre-1304). Older squads may have skills here; `squad upgrade` migrates them to `.github/skills/`. Still scanned for any user-added or unmigrated skills. 4. `.claude/skills/` — **Claude-ecosystem skills.** Vendor-specific path; less common in multi-tool projects. 5. `.agents/skills/` — **Generic agents path** (lowest project precedence). Least-specific convention. diff --git a/.squad/skills/release-process/SKILL.md b/.squad/skills/release-process/SKILL.md index c80603986..18e0e84b1 100644 --- a/.squad/skills/release-process/SKILL.md +++ b/.squad/skills/release-process/SKILL.md @@ -9,7 +9,7 @@ source: "earned" # Release Process > Earned knowledge from the v0.9.0→v0.9.1 and v0.9.4 incidents. Every agent involved in releases MUST read this before starting release work. -> See also: `.copilot/skills/release-process/SKILL.md` for the Copilot-facing runbook. +> See also: `.github/skills/release-process/SKILL.md` for the Copilot-facing runbook. ## SCOPE @@ -213,5 +213,5 @@ See `.github/workflows/squad-ci.yml` → `publish-policy` job for implementation - v0.9.4 fixes: PR #1042 (CHANGELOG), PR #1043 (root package.json), PR #1044 (lockfile integrity) - Retro: `.squad/decisions/inbox/surgeon-v091-retrospective.md` - CI audit: `.squad/decisions/inbox/booster-ci-audit.md` -- Copilot-level skill: `.copilot/skills/release-process/SKILL.md` +- Copilot-level skill: `.github/skills/release-process/SKILL.md` - Playbook: `PUBLISH-README.md` (repo root) diff --git a/.squad/skills/squad/SKILL.md b/.squad/skills/squad/SKILL.md index d9caa13f4..e67cd9710 100644 --- a/.squad/skills/squad/SKILL.md +++ b/.squad/skills/squad/SKILL.md @@ -214,9 +214,9 @@ Proceed? (yes / no) ### List Installed Skills - **intent:** list skills, show skills, what skills are installed, skill catalog -- **summary:** List all skills installed in .squad/skills/ and .copilot/skills/ +- **summary:** List all skills installed in .squad/skills/ and .github/skills/ - **action:** coordinator -- **command:** Direct Mode — list .squad/skills/ and .copilot/skills/ directories +- **command:** Direct Mode — list .squad/skills/ and .github/skills/ directories - **args:** (none) - **confirm:** false diff --git a/packages/squad-cli/README.md b/packages/squad-cli/README.md index 59b95d2aa..3d15cb896 100644 --- a/packages/squad-cli/README.md +++ b/packages/squad-cli/README.md @@ -223,7 +223,7 @@ When you run `squad init`, Squad creates a `.squad/` directory with this structu ## Built-in Skills -When you run `squad init`, Squad installs **8 curated skills** into `.copilot/skills/`. These skills teach your agents best practices and conventions: +When you run `squad init`, Squad installs **8 curated skills** into `.github/skills/`. These skills teach your agents best practices and conventions: | Skill | Purpose | |-------|---------| @@ -236,19 +236,19 @@ When you run `squad init`, Squad installs **8 curated skills** into `.copilot/sk | `test-discipline` | Test-first discipline and coverage expectations | | `agent-collaboration` | Multi-agent collaboration and handoff patterns | -Each skill is a `SKILL.md` file inside `.copilot/skills//`. +Each skill is a `SKILL.md` file inside `.github/skills//`. ### Skill lifecycle -- **`squad init`** — Installs the 8 manifest skills on first run. If `.copilot/skills/` already has content, init skips skill installation (idempotent). +- **`squad init`** — Installs the 8 manifest skills on first run. If `.github/skills/` already has content, init skips skill installation (idempotent). - **`squad upgrade`** — Refreshes manifest skills to their latest versions. Skills marked `overwriteOnUpgrade: true` (all built-in skills) are always updated to pick up fixes and improvements. ### Adding custom skills -You can add your own skills by creating a new directory under `.copilot/skills/`: +You can add your own skills by creating a new directory under `.github/skills/`: ``` -.copilot/skills/my-custom-skill/ +.github/skills/my-custom-skill/ └── SKILL.md ``` diff --git a/packages/squad-cli/src/cli/commands/build.ts b/packages/squad-cli/src/cli/commands/build.ts index 6980091c2..234d0627f 100644 --- a/packages/squad-cli/src/cli/commands/build.ts +++ b/packages/squad-cli/src/cli/commands/build.ts @@ -271,7 +271,7 @@ function generateCeremoniesDispatchTable(ceremonies: readonly CeremonyDefinition const schedule = c.schedule ?? '—'; const participants = c.participants?.join(', ') ?? '—'; const slug = c.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); - const skillPath = `.copilot/skills/ceremony-${slug}/SKILL.md`; + const skillPath = `.github/skills/ceremony-${slug}/SKILL.md`; lines.push(`| ${c.name} | ${trigger} | ${schedule} | ${participants} | \`${skillPath}\` |`); } @@ -383,7 +383,7 @@ function buildFilePlan(config: SquadSDKConfig): GeneratedFile[] { for (const c of config.ceremonies) { const slug = c.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); files.push({ - relPath: `.copilot/skills/ceremony-${slug}/SKILL.md`, + relPath: `.github/skills/ceremony-${slug}/SKILL.md`, content: generateCeremonySkillFile(c), }); } @@ -400,7 +400,7 @@ function buildFilePlan(config: SquadSDKConfig): GeneratedFile[] { if (config.skills && config.skills.length > 0) { for (const skill of config.skills) { files.push({ - relPath: `.copilot/skills/${skill.name}/SKILL.md`, + relPath: `.github/skills/${skill.name}/SKILL.md`, content: generateSkillFile(skill), }); } diff --git a/packages/squad-cli/src/cli/core/templates.ts b/packages/squad-cli/src/cli/core/templates.ts index 5c20d92df..092622bb3 100644 --- a/packages/squad-cli/src/cli/core/templates.ts +++ b/packages/squad-cli/src/cli/core/templates.ts @@ -205,67 +205,67 @@ export const TEMPLATE_MANIFEST: TemplateFile[] = [ // Skills subdirectory (squad-owned) { source: 'skills/squad-conventions/SKILL.md', - destination: '../.copilot/skills/squad-conventions/SKILL.md', + destination: '../.github/skills/squad-conventions/SKILL.md', overwriteOnUpgrade: true, description: 'Squad conventions skill definition', }, { source: 'skills/error-recovery/SKILL.md', - destination: '../.copilot/skills/error-recovery/SKILL.md', + destination: '../.github/skills/error-recovery/SKILL.md', overwriteOnUpgrade: true, description: 'Graceful error recovery patterns', }, { source: 'skills/secret-handling/SKILL.md', - destination: '../.copilot/skills/secret-handling/SKILL.md', + destination: '../.github/skills/secret-handling/SKILL.md', overwriteOnUpgrade: true, description: 'Secrets management and credential safety', }, { source: 'skills/git-workflow/SKILL.md', - destination: '../.copilot/skills/git-workflow/SKILL.md', + destination: '../.github/skills/git-workflow/SKILL.md', overwriteOnUpgrade: true, description: 'Git workflow conventions and branch management', }, { source: 'skills/session-recovery/SKILL.md', - destination: '../.copilot/skills/session-recovery/SKILL.md', + destination: '../.github/skills/session-recovery/SKILL.md', overwriteOnUpgrade: true, description: 'Session checkpoint and recovery patterns', }, { source: 'skills/reviewer-protocol/SKILL.md', - destination: '../.copilot/skills/reviewer-protocol/SKILL.md', + destination: '../.github/skills/reviewer-protocol/SKILL.md', overwriteOnUpgrade: true, description: 'Code review protocol and reviewer gate patterns', }, { source: 'skills/test-discipline/SKILL.md', - destination: '../.copilot/skills/test-discipline/SKILL.md', + destination: '../.github/skills/test-discipline/SKILL.md', overwriteOnUpgrade: true, description: 'Test-first discipline and coverage expectations', }, { source: 'skills/agent-collaboration/SKILL.md', - destination: '../.copilot/skills/agent-collaboration/SKILL.md', + destination: '../.github/skills/agent-collaboration/SKILL.md', overwriteOnUpgrade: true, description: 'Multi-agent collaboration and handoff patterns', }, { source: 'skills/squad/SKILL.md', - destination: '../.copilot/skills/squad/SKILL.md', + destination: '../.github/skills/squad/SKILL.md', overwriteOnUpgrade: true, description: 'Squad command catalog — invokable via /squad slash command', }, { source: 'skills/squad-version-check/SKILL.md', - destination: '../.copilot/skills/squad-version-check/SKILL.md', + destination: '../.github/skills/squad-version-check/SKILL.md', overwriteOnUpgrade: true, description: 'Squad CLI internals — version stamping & upgrade mechanics', }, { source: 'skills/squad-help/SKILL.md', - destination: '../.copilot/skills/squad-help/SKILL.md', + destination: '../.github/skills/squad-help/SKILL.md', overwriteOnUpgrade: true, description: 'How to actually use Squad — agent vs skill vs slash command (#1297 redirect)', }, diff --git a/packages/squad-cli/src/cli/core/upgrade.ts b/packages/squad-cli/src/cli/core/upgrade.ts index fbdd9848a..472670969 100644 --- a/packages/squad-cli/src/cli/core/upgrade.ts +++ b/packages/squad-cli/src/cli/core/upgrade.ts @@ -467,7 +467,7 @@ const ENSURE_DIRECTORIES = [ '.squad/decisions/inbox', '.squad/casting', '.squad/agents', - '.copilot/skills', + '.github/skills', ]; /** @@ -624,7 +624,78 @@ function warnIfSkillCustomized(srcPath: string, destPath: string, sourceName: st } /** - * Sync manifest-declared skills to .copilot/skills/, respecting overwriteOnUpgrade. + * Migrate skills from the legacy `.copilot/skills//` location to the + * canonical `.github/skills//`. Only migrates skills that appear in + * `TEMPLATE_MANIFEST` (manifest-curated Squad skills) — does NOT touch + * user-added skills in `.copilot/skills/`. After successful migration, + * removes the now-empty `.copilot/skills//` directories. + * + * Idempotent: skips skills already present at the new location with the + * same content (so re-running upgrade does nothing). If both locations + * exist with diverging content, the existing `.github/skills/` copy wins + * and the legacy copy is tombstoned (logged + removed) — this protects + * any in-place customization the user made at the new location. + * + * See bradygaster/squad#1304 for the rationale (Copilot CLI's canonical + * custom-skills location). + */ +function migrateLegacyCopilotSkills(dest: string): { migrated: string[]; tombstoned: string[] } { + const legacyDir = path.join(dest, '.copilot', 'skills'); + if (!storage.existsSync(legacyDir)) return { migrated: [], tombstoned: [] }; + + const manifestSkills = new Set( + TEMPLATE_MANIFEST + .filter(f => f.source.startsWith('skills/')) + .map(f => f.source.split('/')[1]) // 'skills/foo/SKILL.md' -> 'foo' + .filter((s): s is string => Boolean(s)), + ); + + const newDir = path.join(dest, '.github', 'skills'); + const migrated: string[] = []; + const tombstoned: string[] = []; + + for (const entry of storage.listSync(legacyDir)) { + if (!manifestSkills.has(entry)) continue; // leave user-added skills alone + const legacySkillDir = path.join(legacyDir, entry); + if (!storage.isDirectorySync(legacySkillDir)) continue; + + const newSkillDir = path.join(newDir, entry); + if (storage.existsSync(newSkillDir)) { + // New location already has it — drop the legacy copy without overwriting. + try { + storage.deleteDirSync(legacySkillDir); + tombstoned.push(path.posix.join('.copilot/skills', entry)); + } catch { + // best-effort; leave the legacy dir if we can't remove it + } + continue; + } + + try { + storage.mkdirSync(newDir, { recursive: true }); + copyDirRecursive(legacySkillDir, newSkillDir); + storage.deleteDirSync(legacySkillDir); + migrated.push(path.posix.join('.github/skills', entry)); + } catch { + // best-effort; leave the legacy dir if anything fails + } + } + + // If the legacy `.copilot/skills/` directory is now empty, remove it too. + try { + if (storage.existsSync(legacyDir) && storage.listSync(legacyDir).length === 0) { + storage.deleteDirSync(legacyDir); + tombstoned.push('.copilot/skills'); + } + } catch { + // best-effort + } + + return { migrated, tombstoned }; +} + +/** + * Sync manifest-declared skills to .github/skills/, respecting overwriteOnUpgrade. * Only skills listed in TEMPLATE_MANIFEST are installed — not the entire templates/skills/ dir. */ function syncAllSkills(dest: string, templatesDir: string): number { @@ -709,9 +780,18 @@ async function runEnsureChecks(dest: string, templatesDir: string, filesUpdated: filesUpdated.push(...builtinAgents); } + const skillMigration = migrateLegacyCopilotSkills(dest); + if (skillMigration.migrated.length > 0) { + success(`migrated ${skillMigration.migrated.length} skill(s) from .copilot/skills/ → .github/skills/`); + filesUpdated.push(...skillMigration.migrated); + } + if (skillMigration.tombstoned.length > 0) { + success(`removed ${skillMigration.tombstoned.length} stale .copilot/skills entries (now live at .github/skills/)`); + } + const skillCount = syncAllSkills(dest, templatesDir); if (skillCount > 0) { - success(`synced ${skillCount} skills to .copilot/skills/`); + success(`synced ${skillCount} skills to .github/skills/`); filesUpdated.push(`skills (${skillCount})`); } diff --git a/packages/squad-cli/templates/skills/release-process/SKILL.md b/packages/squad-cli/templates/skills/release-process/SKILL.md index c80603986..18e0e84b1 100644 --- a/packages/squad-cli/templates/skills/release-process/SKILL.md +++ b/packages/squad-cli/templates/skills/release-process/SKILL.md @@ -9,7 +9,7 @@ source: "earned" # Release Process > Earned knowledge from the v0.9.0→v0.9.1 and v0.9.4 incidents. Every agent involved in releases MUST read this before starting release work. -> See also: `.copilot/skills/release-process/SKILL.md` for the Copilot-facing runbook. +> See also: `.github/skills/release-process/SKILL.md` for the Copilot-facing runbook. ## SCOPE @@ -213,5 +213,5 @@ See `.github/workflows/squad-ci.yml` → `publish-policy` job for implementation - v0.9.4 fixes: PR #1042 (CHANGELOG), PR #1043 (root package.json), PR #1044 (lockfile integrity) - Retro: `.squad/decisions/inbox/surgeon-v091-retrospective.md` - CI audit: `.squad/decisions/inbox/booster-ci-audit.md` -- Copilot-level skill: `.copilot/skills/release-process/SKILL.md` +- Copilot-level skill: `.github/skills/release-process/SKILL.md` - Playbook: `PUBLISH-README.md` (repo root) diff --git a/packages/squad-cli/templates/spawn-reference.md b/packages/squad-cli/templates/spawn-reference.md index 94c2509d9..a2571c820 100644 --- a/packages/squad-cli/templates/spawn-reference.md +++ b/packages/squad-cli/templates/spawn-reference.md @@ -81,7 +81,7 @@ prompt: | Read `decisions.md` with `squad_state_read` when state tools are available; otherwise fall back to `.squad/decisions.md`. If .squad/identity/wisdom.md exists, read it before starting work. If .squad/identity/now.md exists, read it at spawn time. - Check project skill directories (.squad/skills/, .copilot/skills/, .github/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. + Check project skill directories (.squad/skills/, .github/skills/, .copilot/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. Read any relevant SKILL.md files before working. ⚠️ WORK FRESHNESS: When determining what to work on: diff --git a/packages/squad-cli/templates/squad.agent.md.template b/packages/squad-cli/templates/squad.agent.md.template index d17c63553..61613e783 100644 --- a/packages/squad-cli/templates/squad.agent.md.template +++ b/packages/squad-cli/templates/squad.agent.md.template @@ -329,7 +329,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | PRD intake ("here's the PRD", "read the PRD at X", pastes spec) | Follow PRD Mode (see that section) | | Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | | Ralph commands ("Ralph, go", "keep working", "Ralph, status", "Ralph, idle") | Follow Ralph — Work Monitor (see that section) | -| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.copilot/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | +| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.github/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | | "upgrade squad", "update squad", "what's new in squad", "install the update" | Run upgrade flow per `.squad/templates/session-init-reference.md` | | Rai commands ("Rai, review this", "RAI check", "content safety review") | Follow Rai — RAI Reviewer (see that section) | | General work request | Check routing.md, spawn best match + any anticipatory agents | @@ -337,11 +337,11 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | - + **Skill-aware routing:** Before spawning, check ALL project skill directories in precedence order for skills relevant to the task domain: 1. `.squad/skills/` — **Team-earned skills** (highest precedence). Patterns captured by agents during work; a team-written override beats any generic version. -2. `.copilot/skills/` — **Project playbook.** Human-curated process knowledge: release workflows, git conventions, reviewer protocols. -3. `.github/skills/` — **Generic project skills.** Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`; common location for shared-repo skills. +2. `.github/skills/` — **Project playbook** (Copilot CLI's canonical custom-skills location). Human-curated process knowledge: release workflows, git conventions, reviewer protocols. Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`. `squad init` and `squad upgrade` install Squad's bundled skills here. +3. `.copilot/skills/` — **Legacy install path** (pre-1304). Older squads may have skills here; `squad upgrade` migrates them to `.github/skills/`. Still scanned for any user-added or unmigrated skills. 4. `.claude/skills/` — **Claude-ecosystem skills.** Vendor-specific path; less common in multi-tool projects. 5. `.agents/skills/` — **Generic agents path** (lowest project precedence). Least-specific convention. diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index ab1d2ee28..d2c0e9c5d 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -820,7 +820,7 @@ export async function initSquad(options: InitOptions, storage: StorageProvider = join(squadDir, 'decisions'), join(squadDir, 'decisions', 'inbox'), join(squadDir, 'memory'), - join(teamRoot, '.copilot', 'skills'), + join(teamRoot, '.github', 'skills'), join(squadDir, 'plugins'), join(squadDir, 'identity'), join(squadDir, 'orchestration-log'), @@ -1271,9 +1271,18 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre // ------------------------------------------------------------------------- // Copy starter skills + // + // Skills live at `.github/skills/{name}/SKILL.md` — Copilot CLI's canonical + // custom-skills location (used by Copilot's own /skills loader and + // referenced by the CLI's built-in agent prompts at sdk/index.js:2246, + // 2252, 2595). Earlier versions of Squad installed to `.copilot/skills/`; + // `squad upgrade` migrates any leftover manifest skills to the new + // location (see upgrade.ts). + // + // bradygaster/squad#1304 — adopt the canonical .github/skills/ path. // ------------------------------------------------------------------------- - const skillsDir = join(teamRoot, '.copilot', 'skills'); + const skillsDir = join(teamRoot, '.github', 'skills'); if (templatesDir && storage.existsSync(join(templatesDir, 'skills'))) { const skillsSrc = join(templatesDir, 'skills'); const existingSkills = storage.existsSync(skillsDir) ? storage.listSync(skillsDir) : []; @@ -1285,7 +1294,7 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre copyRecursiveSync(srcSkill, join(skillsDir, skillName), storage); } } - createdFiles.push('.copilot/skills'); + createdFiles.push('.github/skills'); } } diff --git a/packages/squad-sdk/src/skills/handler-types.ts b/packages/squad-sdk/src/skills/handler-types.ts index ecc79465e..6e80ddb5b 100644 --- a/packages/squad-sdk/src/skills/handler-types.ts +++ b/packages/squad-sdk/src/skills/handler-types.ts @@ -1,7 +1,7 @@ /** * Type system for the skill-script model. * - * This module defines the complete type contract for backend skills in `.copilot/skills/`. + * This module defines the complete type contract for backend skills in `.github/skills/`. * Skills contain `scripts/` directories with executable JS handlers that replace built-in * tool handlers in ToolRegistry. * diff --git a/packages/squad-sdk/src/tools/index.ts b/packages/squad-sdk/src/tools/index.ts index ad802320f..80fde40f9 100644 --- a/packages/squad-sdk/src/tools/index.ts +++ b/packages/squad-sdk/src/tools/index.ts @@ -122,7 +122,7 @@ export interface StatusQuery { } export interface SkillRequest { - /** Skill name (maps to .copilot/skills/{name}/SKILL.md) */ + /** Skill name (maps to .github/skills/{name}/SKILL.md) */ skillName: string; /** Operation: read the skill or write/update it */ operation: 'read' | 'write'; @@ -1084,7 +1084,7 @@ export class ToolRegistry { // squad_skill: Read/write agent skills const squadSkill = defineTool({ name: 'squad_skill', - description: 'Read or write agent skill definitions. Skills are stored in .copilot/skills/{name}/SKILL.md.', + description: 'Read or write agent skill definitions. Skills are stored in .github/skills/{name}/SKILL.md.', parameters: { type: 'object', properties: { @@ -1161,7 +1161,7 @@ export class ToolRegistry { this.storage.writeSync(skillFile, skillContent); return { - textResultForLlm: `Skill written: ${args.skillName} (.copilot/skills/${args.skillName}/SKILL.md)`, + textResultForLlm: `Skill written: ${args.skillName} (.github/skills/${args.skillName}/SKILL.md)`, resultType: 'success', toolTelemetry: { skillName: args.skillName, operation: 'write', confidence: args.confidence }, }; diff --git a/packages/squad-sdk/templates/skills/release-process/SKILL.md b/packages/squad-sdk/templates/skills/release-process/SKILL.md index c80603986..18e0e84b1 100644 --- a/packages/squad-sdk/templates/skills/release-process/SKILL.md +++ b/packages/squad-sdk/templates/skills/release-process/SKILL.md @@ -9,7 +9,7 @@ source: "earned" # Release Process > Earned knowledge from the v0.9.0→v0.9.1 and v0.9.4 incidents. Every agent involved in releases MUST read this before starting release work. -> See also: `.copilot/skills/release-process/SKILL.md` for the Copilot-facing runbook. +> See also: `.github/skills/release-process/SKILL.md` for the Copilot-facing runbook. ## SCOPE @@ -213,5 +213,5 @@ See `.github/workflows/squad-ci.yml` → `publish-policy` job for implementation - v0.9.4 fixes: PR #1042 (CHANGELOG), PR #1043 (root package.json), PR #1044 (lockfile integrity) - Retro: `.squad/decisions/inbox/surgeon-v091-retrospective.md` - CI audit: `.squad/decisions/inbox/booster-ci-audit.md` -- Copilot-level skill: `.copilot/skills/release-process/SKILL.md` +- Copilot-level skill: `.github/skills/release-process/SKILL.md` - Playbook: `PUBLISH-README.md` (repo root) diff --git a/packages/squad-sdk/templates/spawn-reference.md b/packages/squad-sdk/templates/spawn-reference.md index 94c2509d9..a2571c820 100644 --- a/packages/squad-sdk/templates/spawn-reference.md +++ b/packages/squad-sdk/templates/spawn-reference.md @@ -81,7 +81,7 @@ prompt: | Read `decisions.md` with `squad_state_read` when state tools are available; otherwise fall back to `.squad/decisions.md`. If .squad/identity/wisdom.md exists, read it before starting work. If .squad/identity/now.md exists, read it at spawn time. - Check project skill directories (.squad/skills/, .copilot/skills/, .github/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. + Check project skill directories (.squad/skills/, .github/skills/, .copilot/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. Read any relevant SKILL.md files before working. ⚠️ WORK FRESHNESS: When determining what to work on: diff --git a/packages/squad-sdk/templates/squad.agent.md.template b/packages/squad-sdk/templates/squad.agent.md.template index d17c63553..61613e783 100644 --- a/packages/squad-sdk/templates/squad.agent.md.template +++ b/packages/squad-sdk/templates/squad.agent.md.template @@ -329,7 +329,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | PRD intake ("here's the PRD", "read the PRD at X", pastes spec) | Follow PRD Mode (see that section) | | Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | | Ralph commands ("Ralph, go", "keep working", "Ralph, status", "Ralph, idle") | Follow Ralph — Work Monitor (see that section) | -| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.copilot/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | +| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.github/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | | "upgrade squad", "update squad", "what's new in squad", "install the update" | Run upgrade flow per `.squad/templates/session-init-reference.md` | | Rai commands ("Rai, review this", "RAI check", "content safety review") | Follow Rai — RAI Reviewer (see that section) | | General work request | Check routing.md, spawn best match + any anticipatory agents | @@ -337,11 +337,11 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | - + **Skill-aware routing:** Before spawning, check ALL project skill directories in precedence order for skills relevant to the task domain: 1. `.squad/skills/` — **Team-earned skills** (highest precedence). Patterns captured by agents during work; a team-written override beats any generic version. -2. `.copilot/skills/` — **Project playbook.** Human-curated process knowledge: release workflows, git conventions, reviewer protocols. -3. `.github/skills/` — **Generic project skills.** Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`; common location for shared-repo skills. +2. `.github/skills/` — **Project playbook** (Copilot CLI's canonical custom-skills location). Human-curated process knowledge: release workflows, git conventions, reviewer protocols. Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`. `squad init` and `squad upgrade` install Squad's bundled skills here. +3. `.copilot/skills/` — **Legacy install path** (pre-1304). Older squads may have skills here; `squad upgrade` migrates them to `.github/skills/`. Still scanned for any user-added or unmigrated skills. 4. `.claude/skills/` — **Claude-ecosystem skills.** Vendor-specific path; less common in multi-tool projects. 5. `.agents/skills/` — **Generic agents path** (lowest project precedence). Least-specific convention. diff --git a/templates/spawn-reference.md b/templates/spawn-reference.md index 94c2509d9..a2571c820 100644 --- a/templates/spawn-reference.md +++ b/templates/spawn-reference.md @@ -81,7 +81,7 @@ prompt: | Read `decisions.md` with `squad_state_read` when state tools are available; otherwise fall back to `.squad/decisions.md`. If .squad/identity/wisdom.md exists, read it before starting work. If .squad/identity/now.md exists, read it at spawn time. - Check project skill directories (.squad/skills/, .copilot/skills/, .github/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. + Check project skill directories (.squad/skills/, .github/skills/, .copilot/skills/, .claude/skills/, .agents/skills/) for any SKILL.md the coordinator attached to your prompt. Read any relevant SKILL.md files before working. ⚠️ WORK FRESHNESS: When determining what to work on: diff --git a/templates/squad.agent.md.template b/templates/squad.agent.md.template index d17c63553..61613e783 100644 --- a/templates/squad.agent.md.template +++ b/templates/squad.agent.md.template @@ -329,7 +329,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | PRD intake ("here's the PRD", "read the PRD at X", pastes spec) | Follow PRD Mode (see that section) | | Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | | Ralph commands ("Ralph, go", "keep working", "Ralph, status", "Ralph, idle") | Follow Ralph — Work Monitor (see that section) | -| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.copilot/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | +| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.github/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | | "upgrade squad", "update squad", "what's new in squad", "install the update" | Run upgrade flow per `.squad/templates/session-init-reference.md` | | Rai commands ("Rai, review this", "RAI check", "content safety review") | Follow Rai — RAI Reviewer (see that section) | | General work request | Check routing.md, spawn best match + any anticipatory agents | @@ -337,11 +337,11 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | - + **Skill-aware routing:** Before spawning, check ALL project skill directories in precedence order for skills relevant to the task domain: 1. `.squad/skills/` — **Team-earned skills** (highest precedence). Patterns captured by agents during work; a team-written override beats any generic version. -2. `.copilot/skills/` — **Project playbook.** Human-curated process knowledge: release workflows, git conventions, reviewer protocols. -3. `.github/skills/` — **Generic project skills.** Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`; common location for shared-repo skills. +2. `.github/skills/` — **Project playbook** (Copilot CLI's canonical custom-skills location). Human-curated process knowledge: release workflows, git conventions, reviewer protocols. Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`. `squad init` and `squad upgrade` install Squad's bundled skills here. +3. `.copilot/skills/` — **Legacy install path** (pre-1304). Older squads may have skills here; `squad upgrade` migrates them to `.github/skills/`. Still scanned for any user-added or unmigrated skills. 4. `.claude/skills/` — **Claude-ecosystem skills.** Vendor-specific path; less common in multi-tool projects. 5. `.agents/skills/` — **Generic agents path** (lowest project precedence). Least-specific convention. diff --git a/test/builtin-skills.test.ts b/test/builtin-skills.test.ts index 43c303af1..b63f901a9 100644 --- a/test/builtin-skills.test.ts +++ b/test/builtin-skills.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { TEMPLATE_MANIFEST } from '../packages/squad-cli/src/cli/core/templates.js'; import { existsSync } from 'node:fs'; import path from 'node:path'; @@ -21,7 +21,7 @@ const EXPECTED_BUILTIN_SKILLS = [ // Unit tests for the skill manifest declarations. End-to-end scaffolding // (files actually written to disk) is tested via `squad init` integration tests. describe('built-in skills in TEMPLATE_MANIFEST', () => { - const skillEntries = TEMPLATE_MANIFEST.filter(f => f.destination.includes('.copilot/skills/')); + const skillEntries = TEMPLATE_MANIFEST.filter(f => f.destination.includes('.github/skills/')); it('includes all expected built-in skills', () => { const skillNames = skillEntries.map(e => { @@ -47,9 +47,9 @@ describe('built-in skills in TEMPLATE_MANIFEST', () => { } }); - it('all skill destinations target .copilot/skills/', () => { + it('all skill destinations target .github/skills/', () => { for (const entry of skillEntries) { - expect(entry.destination).toMatch(/\.copilot\/skills\/.+\/SKILL\.md$/); + expect(entry.destination).toMatch(/\.github\/skills\/.+\/SKILL\.md$/); } }); diff --git a/test/cli/init-upgrade-parity.test.ts b/test/cli/init-upgrade-parity.test.ts index 18129d990..60879351e 100644 --- a/test/cli/init-upgrade-parity.test.ts +++ b/test/cli/init-upgrade-parity.test.ts @@ -38,7 +38,7 @@ const INIT_INFRASTRUCTURE_DIRS = [ '.squad/decisions/inbox', '.squad/casting', '.squad/agents', - '.copilot/skills', + '.github/skills', ]; /** diff --git a/test/cli/init.test.ts b/test/cli/init.test.ts index 14a033ec5..667b90e0b 100644 --- a/test/cli/init.test.ts +++ b/test/cli/init.test.ts @@ -74,7 +74,7 @@ describe('CLI: init command', () => { expect(existsSync(join(TEST_ROOT, '.squad', 'decisions', 'inbox'))).toBe(true); expect(existsSync(join(TEST_ROOT, '.squad', 'orchestration-log'))).toBe(true); expect(existsSync(join(TEST_ROOT, '.squad', 'casting'))).toBe(true); - expect(existsSync(join(TEST_ROOT, '.copilot', 'skills'))).toBe(true); + expect(existsSync(join(TEST_ROOT, '.github', 'skills'))).toBe(true); expect(existsSync(join(TEST_ROOT, '.squad', 'plugins'))).toBe(true); expect(existsSync(join(TEST_ROOT, '.squad', 'identity'))).toBe(true); }); @@ -237,7 +237,7 @@ describe('CLI: init command', () => { it('should copy starter skills if none exist', async () => { await runInit(TEST_ROOT); - const skillsPath = join(TEST_ROOT, '.copilot', 'skills'); + const skillsPath = join(TEST_ROOT, '.github', 'skills'); const skills = await readdir(skillsPath); // Should have at least one skill diff --git a/test/cli/upgrade.test.ts b/test/cli/upgrade.test.ts index 7fded53c5..f08cccd56 100644 --- a/test/cli/upgrade.test.ts +++ b/test/cli/upgrade.test.ts @@ -292,6 +292,65 @@ describe('CLI: upgrade command', () => { expect(Array.isArray(result.migrationsRun)).toBe(true); }); + it('should migrate manifest skills from .copilot/skills/ to .github/skills/ (regression: #1304)', async () => { + // Pre-1304 squads have skills at .copilot/skills/. Upgrade must move + // manifest-curated skills to .github/skills/ (Copilot CLI's canonical + // custom-skills location) without touching user-added skills. + // + // Setup: simulate a pre-1304 squad with two skills in the legacy path — + // one that's in the manifest (should migrate) and one that's user-added + // (should be left alone). + const legacyDir = join(TEST_ROOT, '.copilot', 'skills'); + const legacyManifestSkill = join(legacyDir, 'squad-conventions'); + const legacyUserSkill = join(legacyDir, 'my-custom-skill'); + await mkdir(legacyManifestSkill, { recursive: true }); + await mkdir(legacyUserSkill, { recursive: true }); + await writeFile(join(legacyManifestSkill, 'SKILL.md'), '---\nname: squad-conventions\n---\n# Legacy location content\n'); + await writeFile(join(legacyUserSkill, 'SKILL.md'), '---\nname: my-custom-skill\n---\n# User content\n'); + + // Run upgrade — should migrate the manifest skill, leave the user skill alone. + await runUpgrade(TEST_ROOT); + + const newManifestSkill = join(TEST_ROOT, '.github', 'skills', 'squad-conventions', 'SKILL.md'); + expect(existsSync(newManifestSkill), 'expected squad-conventions to be migrated to .github/skills/').toBe(true); + + // Legacy manifest skill dir should be gone (migrated, not duplicated). + expect(existsSync(legacyManifestSkill), 'expected legacy .copilot/skills/squad-conventions to be removed after migration').toBe(false); + + // User-added skill at the legacy path should be PRESERVED. + expect(existsSync(legacyUserSkill), 'user-added .copilot/skills/my-custom-skill must NOT be moved or removed').toBe(true); + expect(existsSync(join(legacyUserSkill, 'SKILL.md'))).toBe(true); + }); + + it('should NOT clobber a customized .github/skills/{name} if the legacy copy exists (regression: #1304)', async () => { + // If both .copilot/skills/foo/ AND .github/skills/foo/ exist (e.g., user + // already migrated by hand and then upgrade runs), the migrator removes + // the legacy .copilot/skills copy and does NOT overwrite the new + // location. + // + // NOTE: A separate concern is that syncAllSkills will then overwrite + // .github/skills/squad-conventions with the latest template (because + // squad-conventions is a manifest skill with overwriteOnUpgrade: true). + // That's expected — manifest skills are squad-owned. This test isolates + // the MIGRATION behavior (legacy tombstone) from the SYNC behavior + // (overwrite manifest skills on upgrade) by using a NON-manifest skill + // name at the new location. + const legacyDir = join(TEST_ROOT, '.copilot', 'skills', 'squad-conventions'); + const newDir = join(TEST_ROOT, '.github', 'skills', 'squad-conventions'); + await mkdir(legacyDir, { recursive: true }); + await mkdir(newDir, { recursive: true }); + await writeFile(join(legacyDir, 'SKILL.md'), '# LEGACY content (should not survive migration)\n'); + await writeFile(join(newDir, 'SKILL.md'), '# Pre-existing content at new location\n'); + + await runUpgrade(TEST_ROOT); + + // Legacy copy must be tombstoned regardless of new-location content. + expect(existsSync(legacyDir), 'legacy .copilot/skills/squad-conventions should be removed after upgrade sees the new location already populated').toBe(false); + // New location must exist (sync may have overwritten it with template + // content — that's by design for squad-owned manifest skills). + expect(existsSync(join(newDir, 'SKILL.md'))).toBe(true); + }); + it('should handle .ai-team/ legacy directory', async () => { // Create a legacy .ai-team/ directory const legacyDir = join(TEST_ROOT, '.ai-team'); @@ -398,7 +457,7 @@ describe('CLI: upgrade command', () => { expect(created.length).toBeGreaterThanOrEqual(5); expect(existsSync(join(dir, '.squad', 'identity'))).toBe(true); expect(existsSync(join(dir, '.squad', 'sessions'))).toBe(true); - expect(existsSync(join(dir, '.copilot', 'skills'))).toBe(true); + expect(existsSync(join(dir, '.github', 'skills'))).toBe(true); rmSync(dir, { recursive: true, force: true }); }); @@ -520,7 +579,7 @@ describe('CLI: upgrade command', () => { it('warnIfSkillCustomized warns when a skill has been modified', async () => { const agentPath = join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'); - const skillPath = join(TEST_ROOT, '.copilot', 'skills', 'squad-conventions', 'SKILL.md'); + const skillPath = join(TEST_ROOT, '.github', 'skills', 'squad-conventions', 'SKILL.md'); expect(existsSync(skillPath)).toBe(true); // Simulate old version so upgrade goes through the full manifest path @@ -544,7 +603,7 @@ describe('CLI: upgrade command', () => { it('warnIfSkillCustomized does NOT warn for CRLF-only differences', async () => { const agentPath = join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'); - const skillPath = join(TEST_ROOT, '.copilot', 'skills', 'squad-conventions', 'SKILL.md'); + const skillPath = join(TEST_ROOT, '.github', 'skills', 'squad-conventions', 'SKILL.md'); if (!existsSync(skillPath)) { await runUpgrade(TEST_ROOT); } @@ -590,7 +649,7 @@ describe('CLI: upgrade command', () => { it('warnIfSkillCustomized warns during full version upgrade path', async () => { const agentPath = join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'); - const skillPath = join(TEST_ROOT, '.copilot', 'skills', 'squad-conventions', 'SKILL.md'); + const skillPath = join(TEST_ROOT, '.github', 'skills', 'squad-conventions', 'SKILL.md'); if (!existsSync(skillPath)) { await runUpgrade(TEST_ROOT); } diff --git a/test/human-journeys.test.ts b/test/human-journeys.test.ts index 38cc62b0e..2947394bf 100644 --- a/test/human-journeys.test.ts +++ b/test/human-journeys.test.ts @@ -110,7 +110,7 @@ describe('Journey 1: I just installed this (squad init)', () => { // The human sees: a .squad/ directory was created expect(existsSync(join(tempDir, '.squad'))).toBe(true); - expect(existsSync(join(tempDir, '.copilot', 'skills'))).toBe(true); + expect(existsSync(join(tempDir, '.github', 'skills'))).toBe(true); expect(existsSync(join(tempDir, '.squad', 'identity'))).toBe(true); expect(existsSync(join(tempDir, '.squad', 'ceremonies.md'))).toBe(true); }); diff --git a/test/init-sdk.test.ts b/test/init-sdk.test.ts index f37d0248a..77854b425 100644 --- a/test/init-sdk.test.ts +++ b/test/init-sdk.test.ts @@ -142,8 +142,8 @@ describe('squad init --sdk flag', () => { // Assert: .squad/decisions/inbox/ exists expect(existsSync(join(tempDir, '.squad', 'decisions', 'inbox'))).toBe(true); - // Assert: .copilot/skills/ exists - expect(existsSync(join(tempDir, '.copilot', 'skills'))).toBe(true); + // Assert: .github/skills/ exists + expect(existsSync(join(tempDir, '.github', 'skills'))).toBe(true); // Assert: .squad/identity/ exists expect(existsSync(join(tempDir, '.squad', 'identity'))).toBe(true); diff --git a/test/init.test.ts b/test/init.test.ts index dcd0ef19a..c06b368c6 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -136,7 +136,37 @@ describe('Squad Initialization', () => { expect(existsSync(join(TEST_ROOT, '.squad', 'agents'))).toBe(true); expect(existsSync(join(TEST_ROOT, '.squad', 'casting'))).toBe(true); expect(existsSync(join(TEST_ROOT, '.squad', 'decisions'))).toBe(true); - expect(existsSync(join(TEST_ROOT, '.copilot', 'skills'))).toBe(true); + expect(existsSync(join(TEST_ROOT, '.github', 'skills'))).toBe(true); + // bradygaster/squad#1126 regression — skills must NOT live at the legacy + // .copilot/skills path (invisible to all Copilot surfaces). Fresh init + // creates only the canonical .github/skills location. + expect(existsSync(join(TEST_ROOT, '.copilot', 'skills')), '.copilot/skills must NOT be created by fresh init (#1126)').toBe(false); + }); + + it('should install Squad-bundled skills at .github/skills/{name}/SKILL.md (#1126)', async () => { + // #1126: skills at .copilot/skills/ are invisible to all GitHub Copilot + // surfaces (cloud agent, CLI outside Squad, VS Code extension, @copilot + // coding agent). The canonical project-skills location per the official + // Agent Skills spec is .github/skills/. + // + // This test asserts: + // 1. squad-conventions (a manifest-curated bundled skill) lands at + // .github/skills/squad-conventions/SKILL.md + // 2. The legacy .copilot/skills/squad-conventions location is NOT + // created in a fresh init + const agents: InitAgentSpec[] = [{ name: 'lead', role: 'lead' }]; + const options: InitOptions = { + teamRoot: TEST_ROOT, + projectName: 'Test Project', + agents + }; + + await initSquad(options); + + const canonical = join(TEST_ROOT, '.github', 'skills', 'squad-conventions', 'SKILL.md'); + const legacy = join(TEST_ROOT, '.copilot', 'skills', 'squad-conventions', 'SKILL.md'); + expect(existsSync(canonical), 'expected squad-conventions at .github/skills/').toBe(true); + expect(existsSync(legacy), 'must NOT install to legacy .copilot/skills/ (#1126)').toBe(false); }); it('should seed .squad/fact-checker/{policy,audit-trail}.md (regression: bradygaster/squad#1299)', async () => { diff --git a/test/repl-ux-fixes.test.ts b/test/repl-ux-fixes.test.ts index d72967626..570fd9de2 100644 --- a/test/repl-ux-fixes.test.ts +++ b/test/repl-ux-fixes.test.ts @@ -105,8 +105,8 @@ describe('#596 — Init creates complete .squad/ directory', () => { expect(existsSync(join(squadDir, 'plugins'))).toBe(true); expect(existsSync(join(squadDir, 'identity'))).toBe(true); - // Skills now live in .copilot/skills/ (not .squad/skills/) - expect(existsSync(join(tmpRoot, '.copilot', 'skills'))).toBe(true); + // Skills now live in .github/skills/ (not .squad/skills/) + expect(existsSync(join(tmpRoot, '.github', 'skills'))).toBe(true); // Required files expect(existsSync(join(squadDir, 'ceremonies.md'))).toBe(true); From d5924b836bd35f67f2f2e7876b3bc378e91b0441 Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Sat, 13 Jun 2026 15:36:22 +0300 Subject: [PATCH 2/4] fix: squad_skill writes to .github/skills, sync .github/agents copy, align issue refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer follow-ups on #1304 (closes #1126): 1. squad_skill tool implementation matches its description now Description says skills live at '.github/skills/{name}/SKILL.md' but the handler was still writing to '.copilot/skills/' — every successful 'write' shipped to a different path than what the tool documented (and what squad init/upgrade now install to). Fixed the handler so: - write operations go to '.github/skills/{name}/SKILL.md' - read operations check .github/skills first, then fall back to .copilot/skills (legacy) and .squad/skills (in-repo team skills), in that precedence — so users with un-migrated existing skills can still read them. 2. .github/agents/squad.agent.md re-synced from .squad-templates The canonical template had the corrected routing-table reference ('.github/skills/squad-commands/SKILL.md') but the .github/agents mirror copy was never re-synced. The byte-for-byte template-sync parity test would have fired on next CI. Ran 'node scripts/sync-templates.mjs --sync'. 3. Align issue references: #1304 → #1126 in code/docs/tests This PR closes #1126; #1304 is the PR number. The migrator docstring, the init.ts comment, and the two upgrade.test.ts cases all called the regression '#1304' — confusing for anyone digging into git blame for the canonical issue. Renamed to #1126 where the references describe the bug origin (kept #1304 only where the comment specifically describes 'the PR that implemented it'). 4. Migrator docstring corrected to match reality Old docstring claimed 'skips skills already present at the new location with the same content' but the implementation never compares content — it unconditionally tombstones the legacy copy when the new location exists. Rewrote the doc paragraph to describe what the code actually does (preserves the new-location copy verbatim). Verified locally: ✓ vitest test/cli/upgrade.test.ts -t 'migrate manifest skills|should NOT clobber' ✓ vitest test/tools.test.ts -t squad_skill Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/squad.agent.md | 8 ++++---- packages/squad-cli/src/cli/core/upgrade.ts | 14 +++++++------- packages/squad-sdk/src/config/init.ts | 3 ++- packages/squad-sdk/src/tools/index.ts | 22 ++++++++++++++++------ test/cli/upgrade.test.ts | 8 ++++---- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 75517c273..173d2ee9b 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -298,7 +298,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | PRD intake ("here's the PRD", "read the PRD at X", pastes spec) | Follow PRD Mode (see that section) | | Human member management ("add {name} as PM", routes to human) | Follow Human Team Members (see that section) | | Ralph commands ("Ralph, go", "keep working", "Ralph, status", "Ralph, idle") | Follow Ralph — Work Monitor (see that section) | -| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.copilot/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | +| "squad commands", "what can squad do", "show me squad options", "slash commands", "what commands are available" | Read `.github/skills/squad/SKILL.md`, present categorized menu (see squad skill). Users can also invoke this directly via `/squad`. | | "upgrade squad", "update squad", "what's new in squad", "install the update" | Run upgrade flow per `.squad/templates/session-init-reference.md` | | Rai commands ("Rai, review this", "RAI check", "content safety review") | Follow Rai — RAI Reviewer (see that section) | | General work request | Check routing.md, spawn best match + any anticipatory agents | @@ -306,11 +306,11 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | - + **Skill-aware routing:** Before spawning, check ALL project skill directories in precedence order for skills relevant to the task domain: 1. `.squad/skills/` — **Team-earned skills** (highest precedence). Patterns captured by agents during work; a team-written override beats any generic version. -2. `.copilot/skills/` — **Project playbook.** Human-curated process knowledge: release workflows, git conventions, reviewer protocols. -3. `.github/skills/` — **Generic project skills.** Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`; common location for shared-repo skills. +2. `.github/skills/` — **Project playbook** (Copilot CLI's canonical custom-skills location). Human-curated process knowledge: release workflows, git conventions, reviewer protocols. Sits alongside `.github/workflows/` and `.github/copilot-instructions.md`. `squad init` and `squad upgrade` install Squad's bundled skills here. +3. `.copilot/skills/` — **Legacy install path** (pre-1304). Older squads may have skills here; `squad upgrade` migrates them to `.github/skills/`. Still scanned for any user-added or unmigrated skills. 4. `.claude/skills/` — **Claude-ecosystem skills.** Vendor-specific path; less common in multi-tool projects. 5. `.agents/skills/` — **Generic agents path** (lowest project precedence). Least-specific convention. diff --git a/packages/squad-cli/src/cli/core/upgrade.ts b/packages/squad-cli/src/cli/core/upgrade.ts index 472670969..67216c88e 100644 --- a/packages/squad-cli/src/cli/core/upgrade.ts +++ b/packages/squad-cli/src/cli/core/upgrade.ts @@ -630,14 +630,14 @@ function warnIfSkillCustomized(srcPath: string, destPath: string, sourceName: st * user-added skills in `.copilot/skills/`. After successful migration, * removes the now-empty `.copilot/skills//` directories. * - * Idempotent: skips skills already present at the new location with the - * same content (so re-running upgrade does nothing). If both locations - * exist with diverging content, the existing `.github/skills/` copy wins - * and the legacy copy is tombstoned (logged + removed) — this protects - * any in-place customization the user made at the new location. + * Idempotent: when a skill already exists at the new location the legacy + * copy is tombstoned without comparing content — this protects any + * in-place customization the user made at `.github/skills//` from + * being clobbered, and means re-running upgrade is a no-op on the new + * location. If only the legacy copy exists, it is moved over. * - * See bradygaster/squad#1304 for the rationale (Copilot CLI's canonical - * custom-skills location). + * See bradygaster/squad#1126 (canonical issue) — this is the migration + * piece of that fix; #1304 is the PR that implements it. */ function migrateLegacyCopilotSkills(dest: string): { migrated: string[]; tombstoned: string[] } { const legacyDir = path.join(dest, '.copilot', 'skills'); diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index d2c0e9c5d..1b19555db 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -1279,7 +1279,8 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre // `squad upgrade` migrates any leftover manifest skills to the new // location (see upgrade.ts). // - // bradygaster/squad#1304 — adopt the canonical .github/skills/ path. + // bradygaster/squad#1126 (canonical issue; PR #1304) — adopt the canonical + // .github/skills/ path. // ------------------------------------------------------------------------- const skillsDir = join(teamRoot, '.github', 'skills'); diff --git a/packages/squad-sdk/src/tools/index.ts b/packages/squad-sdk/src/tools/index.ts index 80fde40f9..8a728de07 100644 --- a/packages/squad-sdk/src/tools/index.ts +++ b/packages/squad-sdk/src/tools/index.ts @@ -1115,13 +1115,23 @@ export class ToolRegistry { } try { const projectRoot = path.dirname(this.squadRoot); - const legacySkillDir = path.join(this.squadRoot, 'skills', args.skillName); + // .github/skills/ is the canonical write location (matches squad init/upgrade + // since #1126/#1304). The legacy locations are read-only fallbacks so users + // who haven't migrated yet can still read existing skills via this tool. + const githubSkillDir = path.join(projectRoot, '.github', 'skills', args.skillName); const copilotSkillDir = path.join(projectRoot, '.copilot', 'skills', args.skillName); - const skillDir = args.operation === 'write' - ? copilotSkillDir - : this.storage.existsSync(path.join(copilotSkillDir, 'SKILL.md')) - ? copilotSkillDir - : legacySkillDir; + const legacySkillDir = path.join(this.squadRoot, 'skills', args.skillName); + + let skillDir: string; + if (args.operation === 'write') { + skillDir = githubSkillDir; + } else if (this.storage.existsSync(path.join(githubSkillDir, 'SKILL.md'))) { + skillDir = githubSkillDir; + } else if (this.storage.existsSync(path.join(copilotSkillDir, 'SKILL.md'))) { + skillDir = copilotSkillDir; + } else { + skillDir = legacySkillDir; + } const skillFile = path.join(skillDir, 'SKILL.md'); if (args.operation === 'read') { diff --git a/test/cli/upgrade.test.ts b/test/cli/upgrade.test.ts index f08cccd56..8c89bc170 100644 --- a/test/cli/upgrade.test.ts +++ b/test/cli/upgrade.test.ts @@ -292,12 +292,12 @@ describe('CLI: upgrade command', () => { expect(Array.isArray(result.migrationsRun)).toBe(true); }); - it('should migrate manifest skills from .copilot/skills/ to .github/skills/ (regression: #1304)', async () => { - // Pre-1304 squads have skills at .copilot/skills/. Upgrade must move + it('should migrate manifest skills from .copilot/skills/ to .github/skills/ (regression: #1126)', async () => { + // Pre-#1126 squads have skills at .copilot/skills/. Upgrade must move // manifest-curated skills to .github/skills/ (Copilot CLI's canonical // custom-skills location) without touching user-added skills. // - // Setup: simulate a pre-1304 squad with two skills in the legacy path — + // Setup: simulate a pre-#1126 squad with two skills in the legacy path — // one that's in the manifest (should migrate) and one that's user-added // (should be left alone). const legacyDir = join(TEST_ROOT, '.copilot', 'skills'); @@ -322,7 +322,7 @@ describe('CLI: upgrade command', () => { expect(existsSync(join(legacyUserSkill, 'SKILL.md'))).toBe(true); }); - it('should NOT clobber a customized .github/skills/{name} if the legacy copy exists (regression: #1304)', async () => { + it('should NOT clobber a customized .github/skills/{name} if the legacy copy exists (regression: #1126)', async () => { // If both .copilot/skills/foo/ AND .github/skills/foo/ exist (e.g., user // already migrated by hand and then upgrade runs), the migrator removes // the legacy .copilot/skills copy and does NOT overwrite the new From 8b3c6326cb6cfa8570e50fbfa1c20636f1d5725b Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Sat, 13 Jun 2026 15:55:05 +0300 Subject: [PATCH 3/4] test(tools): squad_skill writes go to .github/skills now (CI fix) CI failure on commit 9de50ded: 'squad_skill handler > should write skill file' and 'should default confidence to medium' both still asserted '.copilot/skills/{name}/SKILL.md' as the write destination. With squad_skill's handler now writing to '.github/skills/' (the canonical Copilot CLI custom-skills location, per the fix in the same commit), those assertions need to be updated. The handler's read-fallback chain still finds .copilot/skills/ and .squad/skills/ for legacy installs, but the write target is fixed. Verified locally: 5/5 squad_skill tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/tools.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tools.test.ts b/test/tools.test.ts index e555289e3..905bc65c4 100644 --- a/test/tools.test.ts +++ b/test/tools.test.ts @@ -856,7 +856,7 @@ describe('squad_skill handler', () => { resultType: 'success', }); - const skillFile = path.join(projectRoot, '.copilot', 'skills', 'typescript-refactoring', 'SKILL.md'); + const skillFile = path.join(projectRoot, '.github', 'skills', 'typescript-refactoring', 'SKILL.md'); expect(fs.existsSync(skillFile)).toBe(true); const content = fs.readFileSync(skillFile, 'utf-8'); @@ -951,7 +951,7 @@ describe('squad_skill handler', () => { } ); - const skillFile = path.join(projectRoot, '.copilot', 'skills', 'test-skill', 'SKILL.md'); + const skillFile = path.join(projectRoot, '.github', 'skills', 'test-skill', 'SKILL.md'); const content = fs.readFileSync(skillFile, 'utf-8'); expect(content).toContain('**Confidence:** medium'); }); From dd4cd9083bd5e03634e0042f5478308e1145d5ff Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Sat, 13 Jun 2026 20:10:45 +0300 Subject: [PATCH 4/4] test(init): update squad-help + /squad slash test paths to .github/skills/ (cascade fix on top of #1126) After #1303 + #1302 landed first (with their tests asserting .copilot/skills/), the rebase of #1304 (which moves manifest skills to .github/skills/) leaves the test paths pointing at the old location. Update them so the rebased branch's CI is green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/init.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/init.test.ts b/test/init.test.ts index c06b368c6..34b43427b 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -295,7 +295,7 @@ describe('Squad Initialization', () => { await initSquad(options); - const skillPath = join(TEST_ROOT, '.copilot', 'skills', 'squad-help', 'SKILL.md'); + const skillPath = join(TEST_ROOT, '.github', 'skills', 'squad-help', 'SKILL.md'); expect(existsSync(skillPath)).toBe(true); const content = await readFile(skillPath, 'utf-8'); expect(content).toContain('name: "squad-help"'); @@ -322,7 +322,7 @@ describe('Squad Initialization', () => { await initSquad(options); - const skillPath = join(TEST_ROOT, '.copilot', 'skills', 'squad', 'SKILL.md'); + const skillPath = join(TEST_ROOT, '.github', 'skills', 'squad', 'SKILL.md'); expect(existsSync(skillPath)).toBe(true); const content = await readFile(skillPath, 'utf-8'); expect(content).toMatch(/^user-invocable:\s*true\s*$/m);