diff --git a/.changeset/wise-rams-glide.md b/.changeset/wise-rams-glide.md new file mode 100644 index 000000000..60d45085d --- /dev/null +++ b/.changeset/wise-rams-glide.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2973 +--- +**Agent-skills warnings now suggest the `global:` prefix when a bare name matches a global skill** — configuring a skill by bare name (e.g. `patch-coverage-check`) that exists as a global skill was silently skipped with no hint that the fix is `global:patch-coverage-check`. The skip warning now appends a hint when the bare name matches an existing global skill. (#2941) diff --git a/src/init.cts b/src/init.cts index 9d1d14aa7..3778e6c40 100644 --- a/src/init.cts +++ b/src/init.cts @@ -2201,8 +2201,21 @@ function buildAgentSkillsBlock( const skillMdPath = path.join(projectRoot, skillPath, 'SKILL.md'); if (!fs.existsSync(skillMdPath)) { + // #2941: if the bare name matches a global skill, hint at the global: prefix. + // The bare name resolves as project-relative (which doesn't exist), but the + // user likely meant to reference a global skill. getGlobalSkillDir is already + // imported for the global: branch above; guard on globalSkillsBase being non-null + // since runtimes without a skills directory don't support the prefix. + let hint = ''; + if (globalSkillsBase !== null) { + const baseName = path.basename(skillPath); + const globalDir = getGlobalSkillDir(runtime, baseName) as string; + if (globalDir && fs.existsSync(path.join(globalDir, 'SKILL.md'))) { + hint = ` — a global skill named "${baseName}" exists; use "global:${baseName}" to reference it`; + } + } warn( - `[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`, + `[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md"${hint} — skipping\n`, ); continue; } diff --git a/tests/agent-skills.test.cjs b/tests/agent-skills.test.cjs index 27ac96d82..f1c4ea679 100644 --- a/tests/agent-skills.test.cjs +++ b/tests/agent-skills.test.cjs @@ -558,6 +558,49 @@ describe('agent-skills global: prefix', () => { assert.strictEqual(r.ir.block, '', 'block must be empty when skill is missing'); }); + // ─── #2941: bare skill name matching a global skill must hint at global: prefix ── + + test('#2941 — bare name matching a global skill hints at the global: prefix', () => { + // Create a global skill so it exists on disk under ~/.claude/skills/ + createGlobalSkill('patch-coverage-check'); + // Reference it by BARE name (no global: prefix) — this resolves as + // project-relative, which doesn't exist, so it's skipped. The warning + // must hint that the name matches a global skill. + writeConfig(tmpDir, { + agent_skills: { 'gsd-executor': ['patch-coverage-check'] }, + }); + + const r = runAgentSkillsJson( + ['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome } + ); + assert.ok(r.success, `Command failed: ${r.error}`); + assert.strictEqual(r.ir.block, '', 'block must be empty — bare name does not resolve as project-relative'); + assert.ok(Array.isArray(r.ir.warnings), 'IR must include warnings'); + const hintWarning = r.ir.warnings.find((w) => /patch-coverage-check/.test(w) && /global:/.test(w)); + assert.ok(hintWarning, + `warning must hint at the global: prefix when a bare name matches a global skill, got: ${JSON.stringify(r.ir.warnings)}`); + }); + + test('#2941 — bare name with NO global match keeps the original warning (no false hint)', () => { + // No global skill of this name exists. The warning must be the original + // "Skill not found" message without a global: hint. + writeConfig(tmpDir, { + agent_skills: { 'gsd-executor': ['totally-nonexistent-skill'] }, + }); + + const r = runAgentSkillsJson( + ['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome } + ); + assert.ok(r.success, `Command failed: ${r.error}`); + assert.strictEqual(r.ir.block, '', 'block must be empty'); + assert.ok(Array.isArray(r.ir.warnings), 'IR must include warnings'); + const notFoundWarning = r.ir.warnings.find((w) => /Skill not found/.test(w) && /totally-nonexistent-skill/.test(w)); + assert.ok(notFoundWarning, `must have the standard "not found" warning, got: ${JSON.stringify(r.ir.warnings)}`); + // Must NOT contain a global: hint — there is no global skill of this name. + assert.ok(!notFoundWarning.includes('global:'), + `warning must not hint at global: when no global skill matches, got: ${notFoundWarning}`); + }); + test('mix of global: and project-relative paths both resolve correctly', () => { createGlobalSkill('shadcn');