Q35: retired built-in skills come back when the bundle ships them again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 07:00:57 -04:00
1 parent da072f9a9b
commit bbc60cd56a
4 files changed
+47 -9

No files matched your search

+2 -2
View File
@@ -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}`);
}
}
+12 -7
View File
@@ -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);
+18
View File
@@ -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(`
+15
View File
@@ -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);