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 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 10:56:59 -04:00
committed by Pawla Abdul (Bot)
parent ba44667ae3
commit 2e522c9b37
2 changed files with 16 additions and 5 deletions
@@ -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.',
],
});
+15 -4
View File
@@ -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<string, unknown>;
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