fix(skills): prune skills removed from GitHub/sks_sh sources on re-scan

When re-scanning existing sources, diff skills in the DB against the
current source manifest. Skills no longer in the source are either:
- Added to conflicts (if still used by agents)
- Deleted via deleteSkill (if orphaned), added to skipped

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 10:21:56 -04:00
committed by Pawla Abdul (Bot)
parent 268806ff1c
commit 3144df70d6
+23 -1
View File
@@ -2016,7 +2016,7 @@ export function companySkillService(db: Db) {
}
}
// Re-scan GitHub/sks_sh sources to pick up newly added skills
// Re-scan GitHub/sks_sh sources to pick up newly added skills and prune removed ones
const sourceLocators = new Set<string>();
for (const skill of acceptedSkills) {
if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") continue;
@@ -2026,6 +2026,9 @@ export function companySkillService(db: Db) {
for (const sourceLocator of sourceLocators) {
try {
const result = await readUrlSkillImports(companyId, sourceLocator, null);
const currentSlugs = new Set(result.skills.map((s) => s.slug));
// Upsert any new skills found in the source
for (const nextSkill of result.skills) {
if (acceptedSkills.some((s) => s.slug === nextSkill.slug)) continue;
const persisted = (await upsertImportedSkills(companyId, [nextSkill]))[0];
@@ -2034,6 +2037,25 @@ export function companySkillService(db: Db) {
upsertAcceptedSkill(persisted);
}
}
// Prune skills that are no longer in the source
const skillsAtSource = acceptedSkills.filter((s) => s.sourceLocator === sourceLocator);
for (const skill of skillsAtSource) {
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.`,
});
} else {
const deleted = await deleteSkill(companyId, skill.id);
if (deleted) skipped.push(deleted);
}
}
} catch {
// Best-effort: don't fail the whole scan if one source fails
warnings.push(`Could not re-scan source ${sourceLocator} — skipping.`);