From 2e522c9b37b912e955a3775dbdd0f06d3e9089da Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Sat, 11 Apr 2026 10:56:59 -0400 Subject: [PATCH] fix(skills): auto-detach agents when pruned skill is in use during repo sync During a repo sync (scan button), if a skill was removed from the source and is still assigned to agents, the skill is now automatically detached from those agents before deletion, rather than leaving it attached. Manual delete-by-source is unchanged (still blocks if in use). Co-Authored-By: Claude Opus 4.6 --- .../__tests__/company-skills-routes.test.ts | 2 +- server/src/services/company-skills.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/server/src/__tests__/company-skills-routes.test.ts b/server/src/__tests__/company-skills-routes.test.ts index fdc74a54..32656226 100644 --- a/server/src/__tests__/company-skills-routes.test.ts +++ b/server/src/__tests__/company-skills-routes.test.ts @@ -294,7 +294,7 @@ describe("company skill mutation permissions", () => { 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.', + 'Skill "ghost-skill" was removed from https://github.com/vercel-labs/agent-browser and detached from Builder.', ], }); diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index a95b9ebb..b0343c51 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url"; import { and, asc, eq } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { companySkills } from "@paperclipai/db"; -import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; +import { readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; import type { PaperclipSkillEntry } from "@paperclipai/adapter-utils/server-utils"; import type { CompanySkill, @@ -2044,12 +2044,23 @@ export function companySkillService(db: Db) { if (currentSlugs.has(skill.slug)) continue; const usedByAgents = await usage(companyId, skill.key); if (usedByAgents.length > 0) { + // Detach the skill from all agents that have it, then delete + for (const agent of usedByAgents) { + const currentConfig = (agent.adapterConfig ?? {}) as Record; + const preference = readPaperclipSkillSyncPreference(currentConfig); + if (preference.desiredSkills.includes(skill.key)) { + const updatedConfig = writePaperclipSkillSyncPreference( + currentConfig, + preference.desiredSkills.filter((k) => k !== skill.key), + ); + await agents.updateAgent(agent.id, { adapterConfig: updatedConfig }); + } + } 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.`, + `Skill "${skill.slug}" was removed from ${sourceLocator} and detached from ${usedByAgents.map((a) => a.name).join(", ")}.`, ); - } else { - await deleteSkill(companyId, skill.id); } + await deleteSkill(companyId, skill.id); } } catch { // Best-effort: don't fail the whole scan if one source fails