release-skill 0.9.36 → 0.9.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.qoder-plugin/plugin.json +1 -1
  7. package/CHANGELOG.md +56 -0
  8. package/INSTALL.md +2 -2
  9. package/INSTALL.zh-CN.md +2 -2
  10. package/README.md +48 -14
  11. package/README.zh-CN.md +42 -14
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +2020 -1457
  14. package/adapters/claude/schemas/release-plan.schema.json +1 -0
  15. package/adapters/claude/schemas/test-intake.schema.json +1 -0
  16. package/adapters/claude/skills/release-config/SKILL.md +36 -41
  17. package/adapters/claude/skills/release-docs/SKILL.md +38 -35
  18. package/adapters/claude/skills/release-help/SKILL.md +59 -54
  19. package/adapters/claude/skills/release-marketplace/SKILL.md +38 -46
  20. package/adapters/claude/skills/release-prepare/SKILL.md +38 -27
  21. package/adapters/claude/skills/release-publish/SKILL.md +38 -44
  22. package/adapters/claude/skills/release-reconcile/SKILL.md +14 -0
  23. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  24. package/adapters/codex/bin/release-skill.bundle.mjs +2020 -1457
  25. package/adapters/codex/schemas/release-plan.schema.json +1 -0
  26. package/adapters/codex/schemas/test-intake.schema.json +1 -0
  27. package/adapters/codex/skills/release-config/SKILL.md +36 -41
  28. package/adapters/codex/skills/release-docs/SKILL.md +38 -35
  29. package/adapters/codex/skills/release-help/SKILL.md +59 -54
  30. package/adapters/codex/skills/release-marketplace/SKILL.md +38 -46
  31. package/adapters/codex/skills/release-prepare/SKILL.md +38 -27
  32. package/adapters/codex/skills/release-publish/SKILL.md +38 -44
  33. package/adapters/codex/skills/release-reconcile/SKILL.md +14 -0
  34. package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
  35. package/adapters/cursor/bin/release-skill.bundle.mjs +2020 -1457
  36. package/adapters/cursor/schemas/release-plan.schema.json +1 -0
  37. package/adapters/cursor/schemas/test-intake.schema.json +1 -0
  38. package/adapters/cursor/skills/release-config/SKILL.md +36 -41
  39. package/adapters/cursor/skills/release-docs/SKILL.md +38 -35
  40. package/adapters/cursor/skills/release-help/SKILL.md +59 -54
  41. package/adapters/cursor/skills/release-marketplace/SKILL.md +38 -46
  42. package/adapters/cursor/skills/release-prepare/SKILL.md +38 -27
  43. package/adapters/cursor/skills/release-publish/SKILL.md +38 -44
  44. package/adapters/cursor/skills/release-reconcile/SKILL.md +14 -0
  45. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  46. package/adapters/kimi/bin/release-skill.bundle.mjs +2020 -1457
  47. package/adapters/kimi/schemas/release-plan.schema.json +1 -0
  48. package/adapters/kimi/schemas/test-intake.schema.json +1 -0
  49. package/adapters/kimi/skills/release-config/SKILL.md +36 -41
  50. package/adapters/kimi/skills/release-docs/SKILL.md +38 -35
  51. package/adapters/kimi/skills/release-help/SKILL.md +59 -54
  52. package/adapters/kimi/skills/release-marketplace/SKILL.md +38 -46
  53. package/adapters/kimi/skills/release-prepare/SKILL.md +38 -27
  54. package/adapters/kimi/skills/release-publish/SKILL.md +38 -44
  55. package/adapters/kimi/skills/release-reconcile/SKILL.md +14 -0
  56. package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
  57. package/adapters/qoder/bin/release-skill.bundle.mjs +2020 -1457
  58. package/adapters/qoder/schemas/release-plan.schema.json +1 -0
  59. package/adapters/qoder/schemas/test-intake.schema.json +1 -0
  60. package/adapters/qoder/skills/release-config/SKILL.md +36 -41
  61. package/adapters/qoder/skills/release-docs/SKILL.md +38 -35
  62. package/adapters/qoder/skills/release-help/SKILL.md +59 -54
  63. package/adapters/qoder/skills/release-marketplace/SKILL.md +38 -46
  64. package/adapters/qoder/skills/release-prepare/SKILL.md +38 -27
  65. package/adapters/qoder/skills/release-publish/SKILL.md +38 -44
  66. package/adapters/qoder/skills/release-reconcile/SKILL.md +14 -0
  67. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  68. package/adapters/workbuddy/bin/release-skill.bundle.mjs +2020 -1457
  69. package/adapters/workbuddy/schemas/release-plan.schema.json +1 -0
  70. package/adapters/workbuddy/schemas/test-intake.schema.json +1 -0
  71. package/adapters/workbuddy/skills/release-config/SKILL.md +36 -41
  72. package/adapters/workbuddy/skills/release-docs/SKILL.md +38 -35
  73. package/adapters/workbuddy/skills/release-help/SKILL.md +59 -54
  74. package/adapters/workbuddy/skills/release-marketplace/SKILL.md +38 -46
  75. package/adapters/workbuddy/skills/release-prepare/SKILL.md +38 -27
  76. package/adapters/workbuddy/skills/release-publish/SKILL.md +38 -44
  77. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +14 -0
  78. package/bin/release-skill-cli.mjs +5 -0
  79. package/bin/release-skill.bundle.mjs +2020 -1457
  80. package/package.json +1 -1
  81. package/platform-manifest.json +4 -4
  82. package/references/01-state-machine.md +26 -0
  83. package/references/02-project-config.md +108 -7
  84. package/schemas/release-plan.schema.json +1 -0
  85. package/schemas/test-intake.schema.json +1 -0
  86. package/skills/release-config/SKILL.md +36 -41
  87. package/skills/release-docs/SKILL.md +38 -35
  88. package/skills/release-help/SKILL.md +59 -54
  89. package/skills/release-marketplace/SKILL.md +38 -46
  90. package/skills/release-prepare/SKILL.md +38 -27
  91. package/skills/release-publish/SKILL.md +38 -44
  92. package/skills/release-reconcile/SKILL.md +14 -0
  93. package/skills-src/release-config/SKILL.md +36 -41
  94. package/skills-src/release-docs/SKILL.md +38 -35
  95. package/skills-src/release-help/SKILL.md +59 -54
  96. package/skills-src/release-marketplace/SKILL.md +38 -46
  97. package/skills-src/release-prepare/SKILL.md +38 -27
  98. package/skills-src/release-publish/SKILL.md +38 -44
  99. package/skills-src/release-reconcile/SKILL.md +14 -0
  100. package/src/commands/approve.mjs +33 -24
  101. package/src/commands/prepare.mjs +9 -2
  102. package/src/commands/publish.mjs +10 -2
  103. package/src/commands/reconcile.mjs +2 -0
  104. package/src/commands/route.mjs +2 -159
  105. package/src/commands/verify.mjs +123 -17
  106. package/src/core/plan-replacement.mjs +371 -0
  107. package/src/core/pre-authority-failure.mjs +241 -0
  108. package/src/core/skill-resource-closure.mjs +1 -1
  109. package/src/core/test-candidate.mjs +15 -67
@@ -1,3 +1,4 @@
1
+ import { isProvenPreAuthorityFailure } from '../core/pre-authority-failure.mjs';
1
2
  /**
2
3
  * release-route command: Deterministic quickstart routing for workflow profiles.
3
4
  *
@@ -46,7 +47,6 @@ import {
46
47
  validateRunCheckpointMapping,
47
48
  } from '../core/run.mjs';
48
49
  import { computePlanDigest, validatePlan } from '../core/plan.mjs';
49
- import { resolveProducerVersion } from '../core/evidence.mjs';
50
50
  import { normalizePostPublishView, postPublishActionId } from '../core/postpublish.mjs';
51
51
 
52
52
  const WORKFLOW_KINDS = Object.freeze([
@@ -732,170 +732,13 @@ async function inspectStateDirectory(statesPath) {
732
732
  return { ok: true, entries };
733
733
  }
734
734
 
735
- async function isProvenPreAuthorityFailure(runDir, runName) {
736
- if (!/^publish-/.test(runName)) return false;
737
- const summaryPath = join(runDir, 'summary.json');
738
- const evidencePath = join(runDir, 'evidence.jsonl');
739
- let summary;
740
- let events;
741
- try {
742
- summary = JSON.parse(await readFile(summaryPath, 'utf8'));
743
- events = (await readFile(evidencePath, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
744
- } catch {
745
- return false;
746
- }
747
-
748
- // The v1 writer (used before the producer-bound v2 envelope) created the
749
- // run directory and appended safety-gate events before its first
750
- // `release-run.json` state. A failed global preflight then appended only a
751
- // generic `publish` failure and sealed a summary. This branch deliberately
752
- // accepts that historical shape only when every byte still proves the same
753
- // pre-authority boundary. It never infers safety from a directory name,
754
- // mtime, summary alone, or a loosely similar phase.
755
- const legacyPreAuthorityPhases = new Set([
756
- 'safety-gate',
757
- 'global-preflight-arbitration',
758
- 're-observe-previous-public-baseline',
759
- ]);
760
- const legacyInputFailure = /(?:global preflight failed|source authority .*failed|cannot read (?:release plan|approval record)|production (?:publish|commands) require|plan (?:digest mismatch|action completeness gate failed|is not valid|requires)|baseline (?:changed|check failed)|previous-public-baseline|adapter .*failed)/i;
761
- const legacyUnsafeFailure = /(?:remote (?:tag|branch|release) .*already exists|human intervention required|remote conflict|external write)/i;
762
- const legacyPreAuthorityGates = new Set([
763
- 'plan-load',
764
- 'plan-schema',
765
- 'plan-digest',
766
- 'action-completeness',
767
- 'approval-load',
768
- 'adapter-availability',
769
- 'baseline-check',
770
- 'source-authority',
771
- ]);
772
- const validLegacyTimestamp = (value) => typeof value === 'string'
773
- && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
774
- && Number.isFinite(Date.parse(value));
775
- const validLegacyError = (error) => error && typeof error === 'object' && !Array.isArray(error)
776
- && typeof error.code === 'string' && error.code.length > 0
777
- && (error.message === undefined || typeof error.message === 'string');
778
- const validLegacyEvent = (event, runId) => event && typeof event === 'object' && !Array.isArray(event)
779
- && event.schemaVersion === 1
780
- && !Object.hasOwn(event, 'producer')
781
- && typeof event.runId === 'string' && event.runId === runId
782
- && Number.isSafeInteger(event.sequence) && event.sequence >= 1
783
- && validLegacyTimestamp(event.timestamp)
784
- && event.command === 'publish'
785
- && typeof event.phase === 'string'
786
- && (legacyPreAuthorityPhases.has(event.phase) || event.phase === 'publish')
787
- && typeof event.status === 'string' && event.status.length > 0
788
- && (event.error === undefined || event.error === null || validLegacyError(event.error))
789
- && (event.duration === undefined || Number.isSafeInteger(event.duration) && event.duration >= 0)
790
- && (event.details === undefined || event.details !== null
791
- && typeof event.details === 'object' && !Array.isArray(event.details));
792
- const legacyRunId = basename(runDir);
793
- const legacyTerminal = events.at(-1);
794
- const legacyValid = summary && summary.status === 'FAILED'
795
- && validLegacyError(summary.error)
796
- && typeof summary.error.message === 'string' && legacyInputFailure.test(summary.error.message)
797
- && !legacyUnsafeFailure.test(summary.error.message)
798
- && (summary.recoveryActionCode === undefined || summary.recoveryActionCode === 'RETRY_COMMAND')
799
- && summary.runPath === undefined
800
- && summary.finalRunDigest === undefined
801
- && summary.latestStatePath === undefined
802
- && summary.checkpointStatuses === undefined
803
- && Array.isArray(events) && events.length >= 2
804
- && events.every((event, index) => validLegacyEvent(event, legacyRunId)
805
- && event.sequence === index + 1)
806
- && legacyTerminal.phase === 'publish'
807
- && legacyTerminal.status === 'failed'
808
- && validLegacyError(legacyTerminal.error)
809
- && legacyTerminal.error.code === summary.error.code
810
- && events.slice(0, -1).filter((event) => event.status === 'failed').every((event) =>
811
- event.phase === 'safety-gate'
812
- && legacyPreAuthorityGates.has(event.gate)
813
- && validLegacyError(event.error))
814
- && !events.some((event) => event.phase === 'global-preflight-arbitration'
815
- && event.status !== 'pre-observe')
816
- && !events.slice(0, -1).some((event) => event.phase === 'publish'
817
- || /checkpoint|execute|postpublish/i.test(event.phase)
818
- || Object.hasOwn(event, 'prePersistedRunPath')
819
- || Object.hasOwn(event, 'checkpointCount')
820
- || Object.hasOwn(event, 'checkpointStatuses'));
821
- if (legacyValid) return true;
822
-
823
- const producerVersion = resolveProducerVersion();
824
- const sameProducer = (value) => value && typeof value === 'object' && !Array.isArray(value)
825
- && Object.keys(value).length === 2
826
- && value.name === 'release-skill' && value.version === producerVersion;
827
- const envelopeKeys = new Set([
828
- 'schemaVersion', 'runId', 'sequence', 'timestamp', 'command', 'producer',
829
- 'phase', 'status', 'error', 'duration', 'details',
830
- ]);
831
- const validTimestamp = (value) => typeof value === 'string'
832
- && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
833
- && Number.isFinite(Date.parse(value));
834
- const detailValidators = {
835
- unitId: (value) => typeof value === 'string',
836
- gate: (value) => typeof value === 'string',
837
- hookName: (value) => typeof value === 'string',
838
- actionId: (value) => typeof value === 'string',
839
- step: (value) => typeof value === 'string',
840
- hookId: (value) => typeof value === 'string',
841
- tagCommit: (value) => typeof value === 'string',
842
- cached: (value) => typeof value === 'boolean',
843
- testSelection: (value) => typeof value === 'string',
844
- cacheKey: (value) => typeof value === 'string',
845
- durationMs: (value) => value === null || Number.isSafeInteger(value) && value >= 0,
846
- exitCode: (value) => Number.isSafeInteger(value),
847
- findingCount: (value) => Number.isSafeInteger(value) && value >= 0,
848
- };
849
- const validDetails = (value) => value === undefined
850
- || value !== null && typeof value === 'object' && !Array.isArray(value)
851
- && Object.entries(value).every(([key, detail]) => !detailValidators[key] || detailValidators[key](detail));
852
- const validEnvelope = (event) => event && typeof event === 'object'
853
- && Object.keys(event).every((key) => envelopeKeys.has(key))
854
- && event.schemaVersion === 2
855
- && typeof event.runId === 'string' && event.runId === basename(runDir)
856
- && Number.isSafeInteger(event.sequence) && event.sequence >= 1
857
- && validTimestamp(event.timestamp)
858
- && event.command === 'publish'
859
- && typeof event.phase === 'string' && event.phase.length > 0
860
- && typeof event.status === 'string' && event.status.length > 0
861
- && sameProducer(event.producer)
862
- && (event.error === undefined || event.error === null
863
- || (typeof event.error === 'object' && !Array.isArray(event.error)
864
- && Object.keys(event.error).every((key) => key === 'code' || key === 'message')
865
- && typeof event.error.code === 'string' && event.error.code.length > 0
866
- && (event.error.message === undefined || typeof event.error.message === 'string')))
867
- && (event.duration === undefined || Number.isSafeInteger(event.duration) && event.duration >= 0)
868
- && validDetails(event.details);
869
- if (summary.status !== 'FAILED' || summary.recoveryActionCode !== 'RETRY_COMMAND'
870
- || summary.evidencePath !== 'evidence.jsonl' || !sameProducer(summary.producer)
871
- || !Array.isArray(events) || events.length === 0) return false;
872
- let previousSequence = 0;
873
- let firstFailure = null;
874
- let genericFailure = null;
875
- for (const event of events) {
876
- if (!validEnvelope(event) || event.sequence !== previousSequence + 1) return false;
877
- previousSequence = event.sequence;
878
- if (event.status === 'failed' && firstFailure === null) firstFailure = event;
879
- if (event.phase === 'publish') {
880
- if (event.status !== 'failed' || genericFailure !== null || event.details?.prePersistedRunPath
881
- || event.details?.recoveryActionCode !== summary.recoveryActionCode) return false;
882
- genericFailure = event;
883
- } else if (!['safety-gate', 'global-preflight-arbitration'].includes(event.phase)) {
884
- return false;
885
- }
886
- }
887
- if (!firstFailure || !genericFailure || events.at(-1) !== genericFailure
888
- || summary.evidenceSequence !== firstFailure.sequence
889
- || summary.stablePhase !== firstFailure.phase) return false;
890
- return !events.some((event) => event.details?.prePersistedRunPath);
891
- }
892
735
 
893
736
  /**
894
737
  * Postverify is a downstream run type that predates the generic recovery
895
738
  * reader. Its completed and open records are consumable only when the summary
896
739
  * agrees with the sealed status and the verify parent is fully lineage-valid.
897
740
  */
898
- async function readLegacyPostverifyRecovery(runPath) {
741
+ export async function readLegacyPostverifyRecovery(runPath) {
899
742
  try {
900
743
  const run = await loadRun(runPath, { requireDigest: true });
901
744
  const acceptedStatuses = new Set(['DISTRIBUTED', 'PARTIAL', 'BLOCKED', 'NEEDS_INPUT']);
@@ -57,7 +57,7 @@ import {
57
57
  consumePublicSourceAuthorityReceipt,
58
58
  verifySourceAuthorityReceipt,
59
59
  } from '../core/source-authority.mjs';
60
- import { verifyFrozenFile } from '../snapshot/frozen.mjs';
60
+ import { computeFrozenSnapshot, resolveFrozenPath, verifyFrozenFile } from '../snapshot/frozen.mjs';
61
61
  import {
62
62
  deriveBaselineAdvances,
63
63
  applyBaselineAdvances,
@@ -87,6 +87,7 @@ import {
87
87
  checkSkillResourceClosure,
88
88
  createSkillResourceClosureReceipt,
89
89
  evaluateConsumerSkillResourceClosureReceipts,
90
+ TRANSPORT_EXCLUSIONS,
90
91
  } from '../core/skill-resource-closure.mjs';
91
92
  import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
92
93
  import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
@@ -131,6 +132,61 @@ export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
131
132
  NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
132
133
  });
133
134
 
135
+ /** Ignore only write bits; retain type, size, content, and non-write mode bits. */
136
+ const WRITE_BITS = 0o222;
137
+ const PAYLOAD_CONFLICT_REPORT_CAP = 10;
138
+
139
+ function transportIdentityEntries(entries) {
140
+ return entries.map(({ path, type, mode, size, contentDigest }) => ({
141
+ path,
142
+ type,
143
+ mode: mode & ~WRITE_BITS,
144
+ size,
145
+ contentDigest,
146
+ }));
147
+ }
148
+
149
+ function compareInstallToFrozenTransport(frozenEntries, installedEntries) {
150
+ const frozen = transportIdentityEntries(frozenEntries);
151
+ const installed = transportIdentityEntries(installedEntries);
152
+ const installedByPath = new Map(installed.map((entry) => [entry.path, entry]));
153
+ const conflicts = [];
154
+ for (const frozenEntry of frozen) {
155
+ const installedEntry = installedByPath.get(frozenEntry.path);
156
+ if (!installedEntry) {
157
+ conflicts.push(`missing: ${frozenEntry.path}`);
158
+ continue;
159
+ }
160
+ if (installedEntry.type !== frozenEntry.type) {
161
+ conflicts.push(`type mismatch: ${frozenEntry.path}`);
162
+ } else if (
163
+ installedEntry.size !== frozenEntry.size
164
+ || installedEntry.contentDigest !== frozenEntry.contentDigest
165
+ ) {
166
+ conflicts.push(`content mismatch: ${frozenEntry.path}`);
167
+ } else if (installedEntry.mode !== frozenEntry.mode) {
168
+ conflicts.push(`mode mismatch: ${frozenEntry.path}`);
169
+ }
170
+ }
171
+ const frozenPaths = new Set(frozen.map((entry) => entry.path));
172
+ for (const installedEntry of installed) {
173
+ if (!frozenPaths.has(installedEntry.path)) {
174
+ conflicts.push(`extra: ${installedEntry.path}`);
175
+ }
176
+ }
177
+ return conflicts;
178
+ }
179
+
180
+ function frozenSnapshotRelativePath(unit, actions, unitId) {
181
+ if (typeof unit?.frozenSnapshot?.path === 'string' && unit.frozenSnapshot.path.length > 0) {
182
+ return unit.frozenSnapshot.path;
183
+ }
184
+ const action = (actions ?? []).find((item) => item.unitId === unitId
185
+ && typeof item.parameters?.snapshotPath === 'string'
186
+ && item.parameters.snapshotPath.length > 0);
187
+ return action?.parameters?.snapshotPath ?? null;
188
+ }
189
+
134
190
  /** Verify actionless Cursor distributions through their frozen host contract. */
135
191
  export async function verifyCursorHostDistributions({
136
192
  plan,
@@ -2265,25 +2321,75 @@ export async function verifyRelease(options) {
2265
2321
  );
2266
2322
  }
2267
2323
  // For single-plugin units (the root-surface distributions this
2268
- // design covers) the install payload IS the frozen snapshot, so the
2269
- // recomputed receipt must equal the frozen unit receipt field for
2270
- // field (checker version, input digest, surface/skill/reference/
2271
- // finding counts, receipt digest). Record-layer fields (preparedAt,
2272
- // exitCode) are carried from the frozen receipt — the same rule
2273
- // publish's recheck uses.
2324
+ // design covers) the install payload IS the frozen snapshot.
2325
+ // Frozen snapshot + original receipt stay on the strict prepare/
2326
+ // publish rule. Install-versus-frozen comparison follows the
2327
+ // existing transport policy (mode & ~0o222) so a sealed 0444
2328
+ // tree may legally reappear as 0644 after marketplace extract.
2274
2329
  const pluginDistCount = (surface.unit?.distributions ?? [])
2275
2330
  .filter((d) => d.type !== 'npm').length;
2276
2331
  if (pluginDistCount === 1 && surface.frozenPluginRoot === '.') {
2277
- const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
2278
- unitId: surface.unitId,
2279
- preparedAt: expectedUnitReceipt.preparedAt ?? null,
2280
- exitCode: expectedUnitReceipt.exitCode ?? 0,
2281
- });
2282
- assertSkillResourceClosureReceipt(
2283
- expectedUnitReceipt,
2284
- observedReceipt,
2285
- `unit "${surface.unitId}"`,
2286
- );
2332
+ const frozenRel = frozenSnapshotRelativePath(surface.unit, actions, surface.unitId);
2333
+ if (frozenRel) {
2334
+ const frozenDir = await resolveFrozenPath(root, frozenRel, 'frozen snapshot path');
2335
+ const frozenClosure = await checkSkillResourceClosure({
2336
+ snapshotDir: frozenDir,
2337
+ host: surface.host,
2338
+ surfaceHostBindings: [{ surfaceId: surface.surfaceId, host: surface.host }],
2339
+ });
2340
+ const frozenReceipt = createSkillResourceClosureReceipt(frozenClosure, {
2341
+ unitId: surface.unitId,
2342
+ preparedAt: expectedUnitReceipt.preparedAt ?? null,
2343
+ exitCode: expectedUnitReceipt.exitCode ?? 0,
2344
+ });
2345
+ assertSkillResourceClosureReceipt(
2346
+ expectedUnitReceipt,
2347
+ frozenReceipt,
2348
+ `unit "${surface.unitId}"`,
2349
+ );
2350
+ const frozenSnap = await computeFrozenSnapshot(frozenDir, {
2351
+ excludeRootEntries: TRANSPORT_EXCLUSIONS,
2352
+ });
2353
+ const installedSnap = await computeFrozenSnapshot(surface.installPath, {
2354
+ excludeRootEntries: TRANSPORT_EXCLUSIONS,
2355
+ });
2356
+ const conflicts = compareInstallToFrozenTransport(
2357
+ frozenSnap.entries,
2358
+ installedSnap.entries,
2359
+ );
2360
+ await evidence.append({
2361
+ phase: 'verify',
2362
+ step: 'skill-resource-closure',
2363
+ status: 'install-comparison',
2364
+ unitId: surface.unitId,
2365
+ frozenInputDigest: frozenSnap.digest,
2366
+ installedInputDigest: installedSnap.digest,
2367
+ frozenModes: frozenSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode })),
2368
+ installedModes: installedSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode })),
2369
+ });
2370
+ if (conflicts.length > 0) {
2371
+ const listed = conflicts.slice(0, PAYLOAD_CONFLICT_REPORT_CAP).join('; ');
2372
+ const overflow = conflicts.length > PAYLOAD_CONFLICT_REPORT_CAP
2373
+ ? `; and ${conflicts.length - PAYLOAD_CONFLICT_REPORT_CAP} more conflicting path(s)`
2374
+ : '';
2375
+ throw new ReleaseError(
2376
+ POST_PUBLISH_VERIFY_FAILED,
2377
+ `installed plugin root differs in path, bytes, size, or non-write mode bits for unit "${surface.unitId}": ${listed}${overflow}`,
2378
+ { unitId: surface.unitId, conflicts },
2379
+ );
2380
+ }
2381
+ } else {
2382
+ const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
2383
+ unitId: surface.unitId,
2384
+ preparedAt: expectedUnitReceipt.preparedAt ?? null,
2385
+ exitCode: expectedUnitReceipt.exitCode ?? 0,
2386
+ });
2387
+ assertSkillResourceClosureReceipt(
2388
+ expectedUnitReceipt,
2389
+ observedReceipt,
2390
+ `unit "${surface.unitId}"`,
2391
+ );
2392
+ }
2287
2393
  }
2288
2394
  }
2289
2395
  skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(