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/.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/.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..67216c88e 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: 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#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'); + 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..1b19555db 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,19 @@ ${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#1126 (canonical issue; PR #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 +1295,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..8a728de07 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: { @@ -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') { @@ -1161,7 +1171,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..8c89bc170 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: #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-#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'); + 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: #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 + // 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..34b43427b 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 () => { @@ -265,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"'); @@ -292,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); 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); 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'); });