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:
1 parent
da072f9a9b
commit
bbc60cd56a
4 files changed
+47
-9
No files matched your search
+2
-2
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user