fix(skills): handle prune type mismatch by using warnings for agent-attached skills

GitHub/sks_sh pruned skills don't have project/workspace context needed for
the CompanySkillProjectScanConflict/Skipped types. Orphaned skills are
silently deleted; skills still used by agents emit a warning instead of
a conflict entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 10:32:16 -04:00
committed by Pawla Abdul (Bot)
parent 9f15436e05
commit ba44667ae3
2 changed files with 11 additions and 28 deletions
@@ -278,31 +278,24 @@ describe("company skill mutation permissions", () => {
expect(mockCompanySkillService.scanProjectWorkspaces).toHaveBeenCalledWith("company-1", {});
});
it("returns skipped and conflicts from scan including pruned skill conflicts", async () => {
it("returns warnings from scan when removed skills are still used by agents", async () => {
mockAgentService.getById.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
permissions: { canCreateAgents: true },
});
const skippedSkill = { id: "skill-1", slug: "removed-skill", name: "Removed Skill" };
const conflictEntry = {
path: "https://github.com/vercel-labs/agent-browser",
existingSkillId: "skill-2",
existingSkillKey: "vercel-labs/agent-browser/ghost-skill",
existingSourceLocator: "https://github.com/vercel-labs/agent-browser",
reason: 'Skill "ghost-skill" was removed from https://github.com/vercel-labs/agent-browser but is still used by Builder. Detach it from those agents first.',
};
mockCompanySkillService.scanProjectWorkspaces.mockResolvedValueOnce({
scannedProjects: 1,
scannedWorkspaces: 1,
discovered: [],
imported: [],
updated: [],
skipped: [skippedSkill],
conflicts: [conflictEntry],
warnings: [],
skipped: [],
conflicts: [],
warnings: [
'Skill "ghost-skill" was removed from https://github.com/vercel-labs/agent-browser but is still used by Builder. It will not be automatically removed.',
],
});
const res = await request(await createApp({
@@ -316,12 +309,7 @@ describe("company skill mutation permissions", () => {
expect(res.status, JSON.stringify(res.body)).toBe(200);
expect(res.body).toMatchObject({
skipped: [{ id: "skill-1", slug: "removed-skill" }],
conflicts: [{
path: "https://github.com/vercel-labs/agent-browser",
existingSkillId: "skill-2",
reason: expect.stringContaining("was removed from"),
}],
warnings: [expect.stringContaining("was removed from")],
});
});
+4 -9
View File
@@ -2044,16 +2044,11 @@ export function companySkillService(db: Db) {
if (currentSlugs.has(skill.slug)) continue;
const usedByAgents = await usage(companyId, skill.key);
if (usedByAgents.length > 0) {
conflicts.push({
path: sourceLocator,
existingSkillId: skill.id,
existingSkillKey: skill.key,
existingSourceLocator: sourceLocator,
reason: `Skill "${skill.slug}" was removed from ${sourceLocator} but is still used by ${usedByAgents.map((a) => a.name).join(", ")}. Detach it from those agents first.`,
});
warnings.push(
`Skill "${skill.slug}" was removed from ${sourceLocator} but is still used by ${usedByAgents.map((a) => a.name).join(", ")}. It will not be automatically removed.`,
);
} else {
const deleted = await deleteSkill(companyId, skill.id);
if (deleted) skipped.push(deleted);
await deleteSkill(companyId, skill.id);
}
}
} catch {