* test(#2941): add regression for bare-skill-name global: hint When a bare skill name matches an existing global skill, the skip warning must hint at the global: prefix. When no global skill matches, the original 'Skill not found' warning is unchanged. * fix(#2941): hint at global: prefix when a bare skill name matches a global skill When buildAgentSkillsBlock skips a bare skill name that doesn't exist as a project-relative path, check if it matches an existing global skill. If so, append a hint to the warning: 'a global skill named X exists; use global:X to reference it'. When no global skill matches, the warning is unchanged. getGlobalSkillDir and getGlobalSkillsBase are already imported in this module for the global: branch. The hint is guarded on globalSkillsBase being non-null (runtimes without a skills directory don't support the prefix). * chore(#2941): add changeset fragment * chore(#2941): backfill changeset PR number 2973 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/wise-rams-glide.md
Normal file
5
.changeset/wise-rams-glide.md
Normal file
@@ -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)
|
||||
15
src/init.cts
15
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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user