diff --git a/config/scripts/generate-skill-bundle-manifest.mjs b/config/scripts/generate-skill-bundle-manifest.mjs index d4ba49c9b..9dd15e2bc 100644 --- a/config/scripts/generate-skill-bundle-manifest.mjs +++ b/config/scripts/generate-skill-bundle-manifest.mjs @@ -423,20 +423,38 @@ async function buildArtifacts() { // Why: released snapshots are the detection ground truth for existing installs, // so a generation-logic change must not rewrite them silently. Only the one // unreleased working-tree append per skill may change between runs. -function assertReleasedHistoryPreserved(committedRegistry, artifacts) { +function releasedSnapshotCountsFromMapping(releaseMapping) { + if (!releaseMapping || releaseMapping.schemaVersion !== RELEASE_MAPPING_SCHEMA_VERSION) { + return null + } + const counts = {} + for (const release of releaseMapping.releases ?? []) { + for (const [name, revision] of Object.entries(release.skills ?? {})) { + counts[name] = Math.max(counts[name] ?? 0, revision) + } + } + return counts +} + +function assertReleasedHistoryPreserved(committedRegistry, artifacts, committedReleaseMapping) { if (!committedRegistry || committedRegistry.schemaVersion !== SNAPSHOT_REGISTRY_SCHEMA_VERSION) { return } + const committedReleasedCounts = releasedSnapshotCountsFromMapping(committedReleaseMapping) for (const [name, committedSnapshots] of Object.entries(committedRegistry.skills ?? {})) { const releasedCount = artifacts.releasedSnapshotCounts[name] ?? 0 const regenerated = artifacts.snapshotRegistry.skills[name] ?? [] - if (releasedCount < Math.max(0, committedSnapshots.length - 1)) { + const minimumReleasedCount = + committedReleasedCounts?.[name] ?? Math.max(0, committedSnapshots.length - 1) + if (releasedCount < minimumReleasedCount) { throw new Error( `Released snapshot history is incomplete for ${name}. ` + 'Fetch all release tags before regenerating skill artifacts.' ) } - const protectedCount = Math.min(committedSnapshots.length, releasedCount) + const protectedCount = committedReleasedCounts + ? (committedReleasedCounts[name] ?? 0) + : Math.min(committedSnapshots.length, releasedCount) for (let index = 0; index < protectedCount; index += 1) { const committed = committedSnapshots[index] const rebuilt = regenerated[index] @@ -458,6 +476,14 @@ async function readCommittedRegistry() { } } +async function readCommittedReleaseMapping() { + try { + return JSON.parse(await readFile(RELEASE_MAPPING_PATH, 'utf8')) + } catch { + return null + } +} + function serialized(value) { return `${JSON.stringify(value, null, 2)}\n` } @@ -532,7 +558,11 @@ async function verifyArtifacts(artifacts) { async function main() { const artifacts = await buildArtifacts() - assertReleasedHistoryPreserved(await readCommittedRegistry(), artifacts) + assertReleasedHistoryPreserved( + await readCommittedRegistry(), + artifacts, + await readCommittedReleaseMapping() + ) await (process.argv.includes('--write') ? writeArtifacts : verifyArtifacts)(artifacts) } diff --git a/config/scripts/generate-skill-bundle-manifest.test.mjs b/config/scripts/generate-skill-bundle-manifest.test.mjs index a6539f800..1552cb047 100644 --- a/config/scripts/generate-skill-bundle-manifest.test.mjs +++ b/config/scripts/generate-skill-bundle-manifest.test.mjs @@ -137,6 +137,36 @@ describe('skill bundle manifest generator', () => { expect(() => assertReleasedHistoryPreserved(null, artifacts)).not.toThrow() }) + it('protects only revisions named by the committed release mapping', () => { + const snapshot = (releaseRevision, packageDigest) => ({ releaseRevision, packageDigest }) + const committedRegistry = { + schemaVersion: 1, + skills: { + 'linear-tickets': [snapshot(1, 'released'), snapshot(2, 'unreleased-tail')] + } + } + const artifacts = { + releasedSnapshotCounts: { 'linear-tickets': 2 }, + snapshotRegistry: { + schemaVersion: 1, + skills: { 'linear-tickets': [snapshot(1, 'released'), snapshot(2, 'new-release')] } + } + } + + expect(() => + assertReleasedHistoryPreserved(committedRegistry, artifacts, { + schemaVersion: 1, + releases: [{ appVersion: '1.0.0', skills: { 'linear-tickets': 1 } }] + }) + ).not.toThrow() + expect(() => + assertReleasedHistoryPreserved(committedRegistry, artifacts, { + schemaVersion: 1, + releases: [{ appVersion: '1.0.0', skills: { 'linear-tickets': 2 } }] + }) + ).toThrow('Released snapshot history changed for linear-tickets at revision 2') + }) + it('tolerates only redundant trailing release-mapping rows', () => { const serialized = (value) => `${JSON.stringify(value, null, 2)}\n` const rows = [ diff --git a/resources/skills/current-manifest.json b/resources/skills/current-manifest.json index 2662ae48a..362fc24cf 100644 --- a/resources/skills/current-manifest.json +++ b/resources/skills/current-manifest.json @@ -22,7 +22,7 @@ { "name": "linear-tickets", "sourcePath": "skills/linear-tickets", - "releaseRevision": 6, + "releaseRevision": 7, "packageDigest": "ff9f085631f753f059c631d874177ddd4fa847c5eca85a420dc85fb2bece6ff6", "gitTreeSha": "e35ac3c0c583661983d3fc1352ff3aec74e67e8c", "files": [ @@ -94,7 +94,7 @@ { "name": "orca-linear", "sourcePath": "skills/orca-linear", - "releaseRevision": 4, + "releaseRevision": 5, "packageDigest": "5e9622bd3883c0f53e6bd349758096deafceebd2fa260d3e90d677e64d06416d", "gitTreeSha": "f3727995a4719fd522119eca6d1b57542cb5fe23", "files": [ diff --git a/resources/skills/release-mapping.json b/resources/skills/release-mapping.json index b83af3545..a98533ed0 100644 --- a/resources/skills/release-mapping.json +++ b/resources/skills/release-mapping.json @@ -561,6 +561,19 @@ "orca-per-workspace-env": 2, "orchestration": 25 } + }, + { + "appVersion": "1.4.149-rc.2", + "skills": { + "computer-use": 5, + "linear-tickets": 6, + "orca-cli": 35, + "orca-emulator": 4, + "orca-emulator-android": 2, + "orca-linear": 4, + "orca-per-workspace-env": 2, + "orchestration": 25 + } } ] } diff --git a/resources/skills/snapshot-registry.json b/resources/skills/snapshot-registry.json index eb0d14051..c8d7fab3c 100644 --- a/resources/skills/snapshot-registry.json +++ b/resources/skills/snapshot-registry.json @@ -1214,6 +1214,22 @@ }, { "releaseRevision": 6, + "packageDigest": "f198d7b22e5ee1673dac403f9cca0553b124e0a90e4fdd05d2c23b7344e32d2b", + "gitTreeSha": "de9fc106bbb4e313a90ff9a9513a720909bbd176", + "files": [ + { + "path": "SKILL.md", + "size": 10596, + "executable": false, + "classification": "text", + "exactSha256": "0c3077c93328b9965430cd6951f1a35889b8c0775037870ea3a8bcf303d9d2c5", + "textNormalizedSha256": "0c3077c93328b9965430cd6951f1a35889b8c0775037870ea3a8bcf303d9d2c5", + "identitySha256": "0c3077c93328b9965430cd6951f1a35889b8c0775037870ea3a8bcf303d9d2c5" + } + ] + }, + { + "releaseRevision": 7, "packageDigest": "ff9f085631f753f059c631d874177ddd4fa847c5eca85a420dc85fb2bece6ff6", "gitTreeSha": "e35ac3c0c583661983d3fc1352ff3aec74e67e8c", "files": [ @@ -1280,6 +1296,22 @@ }, { "releaseRevision": 4, + "packageDigest": "d44d09e6ecb6a64da177083aad26a95f031cd1cf26ba059fdc888c2628aef64f", + "gitTreeSha": "c34f42030f43e5a85737996fa375bbd79cb5bea8", + "files": [ + { + "path": "SKILL.md", + "size": 10320, + "executable": false, + "classification": "text", + "exactSha256": "48ded55ec3842ce65105e6db7adf9bc9ed263ece08555cec056e68e90321c3d5", + "textNormalizedSha256": "48ded55ec3842ce65105e6db7adf9bc9ed263ece08555cec056e68e90321c3d5", + "identitySha256": "48ded55ec3842ce65105e6db7adf9bc9ed263ece08555cec056e68e90321c3d5" + } + ] + }, + { + "releaseRevision": 5, "packageDigest": "5e9622bd3883c0f53e6bd349758096deafceebd2fa260d3e90d677e64d06416d", "gitTreeSha": "f3727995a4719fd522119eca6d1b57542cb5fe23", "files": [