From bbc60cd56a7740fe6bf95046b4709dc20f11778e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 07:00:57 -0400 Subject: [PATCH] Q35: retired built-in skills come back when the bundle ships them again Co-Authored-By: Claude Opus 5.5 --- src/main/index.ts | 4 ++-- src/main/services/builtin-skills.ts | 19 ++++++++++++------- src/main/services/skill-service.ts | 18 ++++++++++++++++++ tests/unit/builtin-skills.test.ts | 15 +++++++++++++++ 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 52e5772e..c2294ec9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -102,8 +102,8 @@ function syncAllOrganizationSkills(connection: DatabaseConnection, artifactRoot: const orgs = connection.db.prepare("select id from organizations").all() as Array<{ id: string }>; for (const org of orgs) { const report = connection.transaction(() => importStagedSkills(skills, org.id, staged)); - if (report.imported > 0 || report.upgraded > 0 || report.retired > 0 || report.failures.length > 0) { - console.info(`[skills] org ${org.id}: imported ${report.imported}, upgraded ${report.upgraded}, skipped ${report.skipped}, retired ${report.retired}, failures ${report.failures.length}`); + if (report.imported > 0 || report.upgraded > 0 || report.restored > 0 || report.retired > 0 || report.failures.length > 0) { + console.info(`[skills] org ${org.id}: imported ${report.imported}, upgraded ${report.upgraded}, restored ${report.restored}, skipped ${report.skipped}, retired ${report.retired}, failures ${report.failures.length}`); for (const failure of report.failures.slice(0, 5)) console.warn(`[skills] ${failure}`); } } diff --git a/src/main/services/builtin-skills.ts b/src/main/services/builtin-skills.ts index abba893d..137c0b2a 100644 --- a/src/main/services/builtin-skills.ts +++ b/src/main/services/builtin-skills.ts @@ -30,6 +30,8 @@ export interface BuiltinSkillSyncReport { imported: number; /** Existing skills that got a new revision because the bundle changed them. */ upgraded: number; + /** Retired skills reactivated because the bundle ships them again. */ + restored: number; skipped: number; /** Active skills retired because the bundle no longer carries their slug. */ retired: number; @@ -75,18 +77,21 @@ export function stageBuiltinSkillsLibrary(sourceRoot: string, libraryRoot: strin /** * Makes the org catalogue mirror the bundle: publishes every staged skill - * the org does not carry yet, upgrades skills whose files changed (moving - * bindings onto the new revision), then retires active skills whose slug - * the bundle no longer ships. Retired skills leave list_skills and stop - * being grantable. + * the org does not carry yet, restores retired skills the bundle ships again, + * upgrades skills whose files changed (moving bindings onto the new + * revision), then retires active skills whose slug the bundle no longer + * ships. Retired skills leave list_skills and stop being grantable. */ export function importStagedSkills(skills: SkillService, orgId: string, staged: StagedBuiltinSkill[]): BuiltinSkillSyncReport { - const report: BuiltinSkillSyncReport = { imported: 0, upgraded: 0, skipped: 0, retired: 0, failures: [] }; + const report: BuiltinSkillSyncReport = { imported: 0, upgraded: 0, restored: 0, skipped: 0, retired: 0, failures: [] }; for (const unit of staged) { try { if (skills.hasSkill(orgId, unit.slug)) { - if (skills.upgradeIfChanged(orgId, unit.directory, unit.manifest, unit.slug)) report.upgraded += 1; - else report.skipped += 1; + const restored = skills.restoreIfRetired(orgId, unit.slug); + if (restored) report.restored += 1; + const upgraded = skills.upgradeIfChanged(orgId, unit.directory, unit.manifest, unit.slug); + if (upgraded) report.upgraded += 1; + if (!restored && !upgraded) report.skipped += 1; continue; } skills.importManifest(orgId, unit.directory, unit.manifest, unit.slug); diff --git a/src/main/services/skill-service.ts b/src/main/services/skill-service.ts index 0a93a66a..0d3c0019 100644 --- a/src/main/services/skill-service.ts +++ b/src/main/services/skill-service.ts @@ -83,6 +83,24 @@ export class SkillService { return retired; } + /** Reactivates a retired skill; returns whether it was retired. */ + restoreIfRetired(orgId: string, slug: string): boolean { + const skill = this.db.prepare("select id from skill_definitions where org_id = ? and slug = ? and status = 'retired'") + .get(orgId, slug) as { id: string } | undefined; + if (!skill) return false; + this.db.prepare("update skill_definitions set status = 'active' where id = ?").run(skill.id); + this.audit.record({ + orgId, + entityType: "skill_definition", + entityId: skill.id, + actorType: "system", + actorId: null, + action: "restored", + payload: { slug, reason: "back_in_builtin_bundle" }, + }); + return true; + } + /** Publishes a new revision when the files changed and moves bindings on the latest revision onto it. */ upgradeIfChanged(orgId: string, directoryPath: string, manifest: SkillManifest, slug: string): boolean { const latest = this.db.prepare(` diff --git a/tests/unit/builtin-skills.test.ts b/tests/unit/builtin-skills.test.ts index dfad3a09..a1ecc900 100644 --- a/tests/unit/builtin-skills.test.ts +++ b/tests/unit/builtin-skills.test.ts @@ -154,6 +154,21 @@ describe("built-in skills", () => { h.connection.close(); }); + test("a skill the bundle drops and later ships again comes back active", () => { + const h = harness(); + const sync = () => h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot))); + sync(); + const architectPath = join(h.sourceRoot, "engineering", "engineering-backend-architect.md"); + rmSync(architectPath); + expect(sync()).toMatchObject({ retired: 1 }); + writeFileSync(architectPath, ARCHITECT_MD, "utf8"); + expect(sync()).toMatchObject({ imported: 0, restored: 1, upgraded: 0, retired: 0, failures: [] }); + expect(h.connection.db.prepare("select status from skill_definitions where org_id = ? and slug = 'engineering-backend-architect'").get(h.orgId)) + .toEqual({ status: "active" }); + expect(sync()).toMatchObject({ restored: 0, skipped: 3 }); + h.connection.close(); + }); + test("legacy catalogue entries retire so every install mirrors the bundle, while their bindings keep working", () => { const h = harness(); const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);