release-skill 0.9.20 → 0.9.22

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 (44) 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 +54 -0
  8. package/INSTALL.md +2 -2
  9. package/INSTALL.zh-CN.md +2 -2
  10. package/README.md +23 -27
  11. package/README.zh-CN.md +20 -25
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +221 -26
  14. package/adapters/claude/skills/release-finish/SKILL.md +5 -3
  15. package/adapters/claude/skills/release-help/SKILL.md +2 -2
  16. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  17. package/adapters/codex/bin/release-skill.bundle.mjs +221 -26
  18. package/adapters/codex/skills/release-finish/SKILL.md +5 -3
  19. package/adapters/codex/skills/release-help/SKILL.md +2 -2
  20. package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
  21. package/adapters/cursor/bin/release-skill.bundle.mjs +221 -26
  22. package/adapters/cursor/skills/release-finish/SKILL.md +5 -3
  23. package/adapters/cursor/skills/release-help/SKILL.md +2 -2
  24. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  25. package/adapters/kimi/bin/release-skill.bundle.mjs +221 -26
  26. package/adapters/kimi/skills/release-finish/SKILL.md +5 -3
  27. package/adapters/kimi/skills/release-help/SKILL.md +2 -2
  28. package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
  29. package/adapters/qoder/bin/release-skill.bundle.mjs +221 -26
  30. package/adapters/qoder/skills/release-finish/SKILL.md +5 -3
  31. package/adapters/qoder/skills/release-help/SKILL.md +2 -2
  32. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  33. package/adapters/workbuddy/bin/release-skill.bundle.mjs +221 -26
  34. package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -3
  35. package/adapters/workbuddy/skills/release-help/SKILL.md +2 -2
  36. package/bin/release-skill.bundle.mjs +221 -26
  37. package/package.json +1 -1
  38. package/platform-manifest.json +4 -4
  39. package/references/02-project-config.md +2 -0
  40. package/skills/release-finish/SKILL.md +5 -3
  41. package/skills/release-help/SKILL.md +2 -2
  42. package/skills-src/release-finish/SKILL.md +5 -3
  43. package/skills-src/release-help/SKILL.md +2 -2
  44. package/src/commands/post-release-local.mjs +236 -30
@@ -41,7 +41,7 @@ const BRANCH_ACTION_INCLUDED = new Set(['advance-existing-branch', 'initialize-d
41
41
  const CODEBUDDY_MACOS_PATH = '/Applications/WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy';
42
42
  const SAFE_ENV_KEYS = Object.freeze([
43
43
  'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'TMPDIR',
44
- 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
44
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
45
45
  'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
46
46
  'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CODE_HOME',
47
47
  'CODEBUDDY_CONFIG_DIR', 'WORKBUDDY_CONFIG_DIR',
@@ -176,14 +176,9 @@ function hubTargets(plan) {
176
176
  if (!local) return [];
177
177
  const hub = { ...local.hub, githubHost: local.hub.githubHost ?? 'github.com' };
178
178
  const unit = (plan.units ?? []).find((candidate) => candidate.id === declaration.unitId);
179
- const tag = (plan.externalActions ?? []).find((action) => action.type === 'create-tag' && action.unitId === declaration.unitId)?.parameters?.tag;
180
- const manualInstruction = {
181
- claude: 'Use Claude’s existing marketplace management entry.',
182
- codex: 'Use Codex’s existing marketplace management entry.',
183
- kimi: `Use the frozen GitHub Release${tag ? ` (${tag})` : ''} and the existing manual confirmation path; Kimi has no marketplace index.`,
184
- codebuddy: 'Handle manually; CodeBuddy cannot pin a Hub ref in this flow.',
185
- workbuddy: 'Handle manually; WorkBuddy cannot pin a Hub ref in this flow (it follows the CodeBuddy manual boundary).',
186
- };
179
+ const tagActions = (plan.externalActions ?? []).filter((action) => action.type === 'create-tag' && action.unitId === declaration.unitId);
180
+ const tagIdentity = tagActions[0]?.parameters;
181
+ const tag = tagIdentity?.tag;
187
182
  return local.hosts.map((host) => {
188
183
  if (host === 'cursor') {
189
184
  return {
@@ -217,15 +212,30 @@ function hubTargets(plan) {
217
212
  }
218
213
  return {
219
214
  targetKind: 'hub-backed',
220
- executionMode: 'manual',
215
+ executionMode: 'executable',
221
216
  unitId: declaration.unitId,
222
217
  host,
223
218
  plugin: local.plugin,
224
219
  version: unit?.targetVersion,
225
220
  hub,
226
- message: `${manualInstruction[host]} Install or upgrade ${local.plugin} from Hub ${hub.name}; release-skill does not execute or probe this action.`,
227
- ...(unit?.publicRepo ? { publicRepo: unit.publicRepo } : {}),
228
- ...(tag ? { frozenTag: tag } : {}),
221
+ marketplace: hub.name,
222
+ marketplaceRepo: hub.repo,
223
+ marketplaceRef: hub.ref,
224
+ githubHost: tagIdentity?.githubHost ?? 'github.com',
225
+ pluginRepo: unit?.publicRepo,
226
+ pluginCommit: unit?.frozenSnapshot?.commit,
227
+ pluginTag: tag,
228
+ frozenTag: tag,
229
+ snapshotPath: unit?.frozenSnapshot?.path,
230
+ manifestDigest: unit?.frozenSnapshot?.manifestDigest,
231
+ timeoutMs: 300_000,
232
+ message: host === 'kimi'
233
+ ? `Install the frozen GitHub Release ${tag} through Kimi's controlled terminal interface.`
234
+ : `Update ${local.plugin} from the existing ${hub.name} marketplace and verify the frozen payload.`,
235
+ ...(tagActions.length !== 1 || tagIdentity?.repo !== unit?.publicRepo
236
+ || tagIdentity?.commit !== unit?.frozenSnapshot?.commit
237
+ || tagIdentity?.version !== unit?.targetVersion
238
+ ? { invalidFrozenIdentity: true } : {}),
229
239
  };
230
240
  });
231
241
  });
@@ -238,23 +248,28 @@ function mergePostReleaseTargets(executableTargets, manualTargets) {
238
248
  const existing = byIdentity.get(key);
239
249
  if (existing && existing.targetKind === 'hub-backed' && target.targetKind === 'hub-backed') {
240
250
  const sameHub = ['name', 'githubHost', 'repo', 'ref'].every((field) => existing.hub[field] === target.hub[field]);
241
- if (!sameHub) throw new Error(`post-release target identity conflict for ${target.unitId}/${target.host}/${target.plugin}`);
251
+ const samePlugin = ['version', 'pluginRepo', 'pluginTag', 'pluginCommit', 'snapshotPath', 'manifestDigest']
252
+ .every((field) => existing[field] === target[field]);
253
+ if (!sameHub || !samePlugin) throw new Error(`post-release target identity conflict for ${target.unitId}/${target.host}/${target.plugin}`);
242
254
  continue;
243
255
  }
244
- const hubMatchesExecutable = (hub, executable) => executable.marketplace === hub.name
256
+ const hubMatchesExecutable = (hub, executable, hubTarget) => executable.marketplace === hub.name
245
257
  && executable.marketplaceRepo === hub.repo
246
258
  && executable.marketplaceRef === hub.ref
247
- && executable.githubHost === hub.githubHost;
259
+ && executable.githubHost === hub.githubHost
260
+ && executable.pluginRepo === hubTarget.pluginRepo
261
+ && executable.pluginTag === hubTarget.pluginTag
262
+ && executable.pluginCommit === hubTarget.pluginCommit;
248
263
  // When the same frozen plugin is available through both sources, the
249
264
  // executable action wins only when both frozen source identities agree.
250
265
  if (existing && existing.targetKind !== 'hub-backed' && target.targetKind === 'hub-backed') {
251
- if (!hubMatchesExecutable(target.hub, existing)) {
266
+ if (!hubMatchesExecutable(target.hub, existing, target)) {
252
267
  throw new Error(`post-release target identity conflict for ${target.unitId}/${target.host}/${target.plugin}`);
253
268
  }
254
269
  continue;
255
270
  }
256
271
  if (existing && existing.targetKind === 'hub-backed' && target.targetKind !== 'hub-backed') {
257
- if (!hubMatchesExecutable(existing.hub, target)) {
272
+ if (!hubMatchesExecutable(existing.hub, target, existing)) {
258
273
  throw new Error(`post-release target identity conflict for ${target.unitId}/${target.host}/${target.plugin}`);
259
274
  }
260
275
  byIdentity.set(key, target);
@@ -271,6 +286,16 @@ function mergePostReleaseTargets(executableTargets, manualTargets) {
271
286
  }
272
287
 
273
288
  function assertExecutableTarget(target) {
289
+ if (target.targetKind === 'hub-backed') {
290
+ assertQoderExecutableTarget(target);
291
+ if (target.invalidFrozenIdentity || typeof target.pluginTag !== 'string' || !target.pluginTag
292
+ || !/^[\w.-]+\/[\w.-]+$/u.test(target.pluginRepo)
293
+ || !/^[\w.-]+\/[\w.-]+$/u.test(target.hub.repo)
294
+ || !target.hub.ref?.startsWith('refs/heads/')) {
295
+ throw new Error(`local host update target has inconsistent frozen source identity for ${target.unitId}`);
296
+ }
297
+ return;
298
+ }
274
299
  for (const field of [
275
300
  'actionId', 'unitId', 'plugin', 'version',
276
301
  'pluginRepo', 'pluginTag', 'pluginCommit',
@@ -345,6 +370,9 @@ export function derivePostReleaseChecklist(plan, {
345
370
  !BRANCH_ACTION_INCLUDED.has(unit.productionConfig?.branchStrategy)
346
371
  ));
347
372
  const declaredHubTargets = hubTargets(plan);
373
+ for (const target of declaredHubTargets) {
374
+ if (target.targetKind === 'hub-backed' && target.host !== 'qoder') assertExecutableTarget(target);
375
+ }
348
376
  const executableTargets = [
349
377
  ...pluginTargets(plan),
350
378
  ...declaredHubTargets.filter((target) => target.executionMode === 'executable'),
@@ -692,7 +720,7 @@ function isCodeBuddyResidualEnvelope(error) {
692
720
  }
693
721
 
694
722
  function isCodeBuddyReadCommand(target, command, args) {
695
- return target.host === 'codebuddy'
723
+ return (target.host === 'codebuddy' || target.host === 'workbuddy')
696
724
  && (command === 'codebuddy' || command === 'cbc' || command === CODEBUDDY_MACOS_PATH)
697
725
  && Array.isArray(args)
698
726
  && (
@@ -704,7 +732,7 @@ function isCodeBuddyReadCommand(target, command, args) {
704
732
  }
705
733
 
706
734
  function isCodeBuddyWriteCommand(target, command, args, kind) {
707
- if (target.host !== 'codebuddy' || !['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH].includes(command)) return false;
735
+ if (!['codebuddy', 'workbuddy'].includes(target.host) || !['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH].includes(command)) return false;
708
736
  const expected = kind === 'marketplace-update'
709
737
  ? ['plugin', 'marketplace', 'update', target.marketplace]
710
738
  : ['plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user'];
@@ -726,14 +754,14 @@ async function runCodeBuddyRead(target, command, args, label, env, run) {
726
754
  }
727
755
  }
728
756
 
729
- function exactCodeBuddyMarketplaceObservation(target, stdout) {
757
+ function exactCodeBuddyMarketplaceObservation(target, stdout, { acceptedTypes = ['git'] } = {}) {
730
758
  const parsed = parseJson(stdout, `${target.host} marketplace list`);
731
759
  if (!Array.isArray(parsed)) throw new Error(`${target.host} marketplace list did not return an array`);
732
760
  const matches = parsed.filter((entry) => entry?.name === target.marketplace);
733
761
  if (matches.length !== 1) {
734
762
  throw new Error(`${target.host} marketplace list did not contain exactly one ${target.marketplace}`);
735
763
  }
736
- if (matches[0].type !== 'git') {
764
+ if (!acceptedTypes.includes(matches[0].type)) {
737
765
  throw new Error(`${target.host} marketplace ${target.marketplace} is not a git marketplace`);
738
766
  }
739
767
  return { installed: true, exact: true, found: matches[0] };
@@ -1076,10 +1104,16 @@ async function observeMarketplace(target, command, env, run) {
1076
1104
  const parsed = parseJson(listed.stdout, `${target.host} marketplace list`);
1077
1105
  const entries = target.host === 'claude' ? parsed : parsed?.marketplaces;
1078
1106
  if (!Array.isArray(entries)) throw new Error(`${target.host} marketplace list has an invalid shape`);
1079
- const found = entries.find((entry) => entry?.name === target.marketplace);
1107
+ const matches = entries.filter((entry) => entry?.name === target.marketplace);
1108
+ if (target.targetKind === 'hub-backed' && matches.length > 1) {
1109
+ throw new Error(`${target.host} marketplace list returned conflicting entries for ${target.marketplace}`);
1110
+ }
1111
+ const found = matches[0];
1080
1112
  if (!found) return { installed: false };
1081
1113
  const source = target.host === 'claude' ? found.repo : found.marketplaceSource?.source;
1082
- if (normalizeGitSource(source) !== normalizeGitSource(target.marketplaceRepo)) {
1114
+ const observedSource = target.targetKind === 'hub-backed'
1115
+ ? normalizeHubGitSource(source, target.hub.githubHost) : normalizeGitSource(source);
1116
+ if (observedSource !== normalizeGitSource(target.marketplaceRepo)) {
1083
1117
  throw new Error(`${target.host} marketplace ${target.marketplace} does not point to ${target.marketplaceRepo}`);
1084
1118
  }
1085
1119
  const root = target.host === 'claude' ? found.installLocation : found.root;
@@ -1119,6 +1153,14 @@ async function bindStructuredMarketplace(target, command, env, run, observed) {
1119
1153
  }
1120
1154
 
1121
1155
  function actionParametersForTarget(plan, target) {
1156
+ if (target.targetKind === 'hub-backed') {
1157
+ return {
1158
+ snapshotPath: target.snapshotPath,
1159
+ manifestDigest: target.manifestDigest,
1160
+ payloadContract: 'external-marketplace-v1',
1161
+ marketplaceLocation: 'external',
1162
+ };
1163
+ }
1122
1164
  const action = (plan.externalActions ?? []).find((candidate) => candidate.id === target.actionId);
1123
1165
  if (!action || action.type !== target.actionType || action.unitId !== target.unitId) {
1124
1166
  throw new Error(`local host update target ${target.actionId} has no matching frozen action`);
@@ -1144,16 +1186,172 @@ async function verifyStructuredInstalledPayload({
1144
1186
  );
1145
1187
  }
1146
1188
 
1189
+ async function assertHubHostEntry(target, marketplaceRoot) {
1190
+ const codex = target.host === 'codex';
1191
+ const indexPath = codex ? '.agents/plugins/marketplace.json'
1192
+ : target.host === 'claude' ? '.claude-plugin/marketplace.json' : '.codebuddy-plugin/marketplace.json';
1193
+ const index = parseJson(await readFileContained(marketplaceRoot, indexPath, { encoding: 'utf8' }), `${target.host} Hub index`);
1194
+ const matches = index?.plugins?.filter((entry) => entry?.name === target.plugin);
1195
+ if (index?.name !== target.marketplace || !Array.isArray(matches) || matches.length !== 1) {
1196
+ throw new Error(`${target.host} Hub index must contain exactly one frozen plugin entry`);
1197
+ }
1198
+ const entry = matches[0];
1199
+ const source = entry.source;
1200
+ const repo = codex ? normalizeHubGitSource(source?.url, target.githubHost) : source?.repo;
1201
+ if (source?.source !== (codex ? 'url' : 'github') || repo !== target.pluginRepo
1202
+ || source?.sha !== target.pluginCommit
1203
+ || source?.ref !== (codex ? `refs/tags/${target.pluginTag}` : target.pluginTag)
1204
+ || (!codex && entry.version !== target.version)
1205
+ || (entry.version !== undefined && entry.version !== target.version)) {
1206
+ throw new Error(`${target.host} Hub entry does not match the frozen repository, tag, commit and version`);
1207
+ }
1208
+ }
1209
+
1210
+ async function observeHubCheckout(target, checkout, env, run) {
1211
+ const remote = await run('git', ['-C', checkout, 'remote', 'get-url', 'origin'], { env, timeout: 30_000 });
1212
+ if (normalizeHubGitSource(remote.stdout.trim(), target.hub.githubHost) !== target.hub.repo) {
1213
+ throw new Error(`${target.host} marketplace checkout does not match the declared Hub repository`);
1214
+ }
1215
+ if (target.host !== 'codex') {
1216
+ const branch = await run('git', ['-C', checkout, 'symbolic-ref', 'HEAD'], { env, timeout: 30_000 });
1217
+ if (branch.stdout.trim() !== target.hub.ref) throw new Error(`${target.host} marketplace checkout does not match the declared Hub branch`);
1218
+ }
1219
+ const head = await run('git', ['-C', checkout, 'rev-parse', 'HEAD'], { env, timeout: 30_000 });
1220
+ const commit = head.stdout.trim();
1221
+ if (!/^[a-f0-9]{40}$/u.test(commit)) throw new Error(`${target.host} Hub checkout has no exact commit`);
1222
+ return commit;
1223
+ }
1224
+
1225
+ async function observeHubMarketplace(target, command, env, run) {
1226
+ if (target.host === 'claude' || target.host === 'codex') {
1227
+ const observed = await observeMarketplace(target, command, env, run);
1228
+ if (!observed.installed) return observed;
1229
+ return { ...observed, commit: await observeHubCheckout(target, observed.root, env, run) };
1230
+ }
1231
+ const codeBuddyFamily = target.host === 'codebuddy' || target.host === 'workbuddy';
1232
+ const listed = codeBuddyFamily
1233
+ ? (await runCodeBuddyRead(target, command, CODEBUDDY_MARKETPLACE_LIST_ARGS, 'codebuddy marketplace list', env, run)).output
1234
+ : await run(command, [...CODEBUDDY_MARKETPLACE_LIST_ARGS], { env });
1235
+ const entries = parseJson(listed.stdout, `${target.host} marketplace list`);
1236
+ if (!Array.isArray(entries)) throw new Error(`${target.host} marketplace list did not return an array`);
1237
+ if (!entries.some((entry) => entry?.name === target.marketplace)) return { installed: false };
1238
+ exactCodeBuddyMarketplaceObservation(target, listed.stdout, { acceptedTypes: ['git', 'github'] });
1239
+ const root = await resolveContained(env.CODEBUDDY_CONFIG_DIR, `plugins/marketplaces/${target.marketplace}`);
1240
+ return { installed: true, root, commit: await observeHubCheckout(target, root, env, run) };
1241
+ }
1242
+
1243
+ async function runHubStructuredUpdate(target, detected, run, { plan, root, verifyInstalledPayload }) {
1244
+ const env = hostEnvironment(target.host);
1245
+ const codeBuddy = target.host === 'codebuddy' || target.host === 'workbuddy';
1246
+ const readPlugin = async () => {
1247
+ const output = codeBuddy
1248
+ ? (await runCodeBuddyRead(target, detected.command, CODEBUDDY_PLUGIN_LIST_ARGS, 'codebuddy plugin list', env, run)).output
1249
+ : await run(detected.command, ['plugin', 'list', '--json'], { env });
1250
+ const listed = parseJson(output.stdout, `${target.host} plugin list`);
1251
+ const entries = target.host === 'codex' ? listed?.installed : listed;
1252
+ if (Array.isArray(entries) && entries.filter((entry) => (
1253
+ (target.host === 'codex' ? entry?.pluginId : entry?.id) === `${target.plugin}@${target.marketplace}`
1254
+ )).length > 1) throw new Error(`${target.host} plugin list returned conflicting entries`);
1255
+ const observed = exactPluginObservation(target, output.stdout);
1256
+ if (observed.installed) {
1257
+ if (codeBuddy && observed.exact) {
1258
+ observed.installPath = await resolveContained(env.CODEBUDDY_CONFIG_DIR, `plugins/cache/${target.marketplace}/${target.plugin}/${target.version}`);
1259
+ } else if (target.host === 'codex') {
1260
+ observed.installPath = observed.found.installedPath;
1261
+ const source = observed.found.source;
1262
+ if (source?.source !== 'git' || normalizeHubGitSource(source.url, target.githubHost) !== target.pluginRepo) {
1263
+ throw new Error('codex installed plugin does not point to the frozen public repository');
1264
+ }
1265
+ if (source.sha !== target.pluginCommit || source.ref !== `refs/tags/${target.pluginTag}`) observed.exact = false;
1266
+ }
1267
+ if (observed.found.gitCommitSha !== undefined && observed.found.gitCommitSha !== target.pluginCommit) observed.exact = false;
1268
+ }
1269
+ return observed;
1270
+ };
1271
+ const beforeMarket = await observeHubMarketplace(target, detected.command, env, run);
1272
+ if (!beforeMarket.installed) return { status: 'MANUAL_REQUIRED', reason: `${target.host} declared Hub marketplace is not installed; no marketplace was added` };
1273
+ const before = await readPlugin();
1274
+ if (before.exact && before.installPath) {
1275
+ await assertHubHostEntry(target, beforeMarket.root);
1276
+ await verifyStructuredInstalledPayload({ plan, root, target, installPath: before.installPath, verifyInstalledPayload });
1277
+ return { status: 'ALREADY_CURRENT', version: target.version };
1278
+ }
1279
+ if (codeBuddy && !before.installed) return { status: 'MANUAL_REQUIRED', reason: `${target.host} target plugin is not installed; no initial installation was performed` };
1280
+
1281
+ // The observed Hub commit belongs to a different repository than pluginCommit.
1282
+ // Keep it local to this attempt; the frozen plan continues to bind plugin identity.
1283
+ const hubCommit = await withTemporaryWorkspace(async (workspace) => {
1284
+ const checkout = join(workspace.root, 'hub');
1285
+ await run('git', ['clone', '--depth', '1', '--branch', target.hub.ref.slice('refs/heads/'.length), '--single-branch',
1286
+ `https://${target.hub.githubHost}/${target.hub.repo}.git`, checkout], { env, timeout: target.timeoutMs });
1287
+ const commit = await observeHubCheckout(target, checkout, env, run);
1288
+ await assertHubHostEntry(target, checkout);
1289
+ return commit;
1290
+ }, { prefix: 'release-skill-hub-update-' });
1291
+ if (target.host === 'codex') {
1292
+ await bindStructuredMarketplace({ ...target, marketplaceCommit: hubCommit }, detected.command, env, run,
1293
+ { marketplace: { ...beforeMarket, exact: beforeMarket.commit === hubCommit } });
1294
+ } else {
1295
+ try {
1296
+ await run(detected.command, ['plugin', 'marketplace', 'update', target.marketplace], { env, timeout: target.timeoutMs });
1297
+ } catch (error) {
1298
+ if (!isCodeBuddyWriteCommand(target, detected.command, ['plugin', 'marketplace', 'update', target.marketplace], 'marketplace-update')
1299
+ || !isCodeBuddyResidualEnvelope(error)) throw error;
1300
+ }
1301
+ }
1302
+ const refreshed = await observeHubMarketplace(target, detected.command, env, run);
1303
+ if (!refreshed.installed || refreshed.commit !== hubCommit) throw new Error(`${target.host} Hub branch moved during marketplace refresh`);
1304
+ await assertHubHostEntry(target, refreshed.root);
1305
+ if (codeBuddy) {
1306
+ const remote = await run('git', ['ls-remote', '--exit-code', `https://${target.hub.githubHost}/${target.hub.repo}.git`, target.hub.ref], { env, timeout: 30_000 });
1307
+ if (!parseFrozenRemoteRef({ ...target, marketplaceCommit: hubCommit }, remote.stdout).exact) {
1308
+ throw new Error(`${target.host} Hub branch moved before plugin update`);
1309
+ }
1310
+ }
1311
+ const current = await readPlugin();
1312
+ const selector = `${target.plugin}@${target.marketplace}`;
1313
+ let installPath;
1314
+ if (target.host === 'codex') {
1315
+ if (current.installed) await run(detected.command, ['plugin', 'remove', selector, '--json'], { env });
1316
+ const installed = await run(detected.command, getPlatform('codex').cli.install(target.plugin, target.marketplace), { env, timeout: target.timeoutMs });
1317
+ const extracted = getPlatform('codex').strategy.extractInstallPath({ execEvidence: { installOutput: parseJson(installed.stdout, 'codex plugin install') } });
1318
+ if (!extracted.ok) throw new Error(extracted.error);
1319
+ installPath = extracted.installPath;
1320
+ } else {
1321
+ const args = codeBuddy || current.installed
1322
+ ? ['plugin', 'update', selector, '--scope', 'user', ...(target.host === 'claude' ? ['--yes'] : [])]
1323
+ : getPlatform('claude').cli.install(target.plugin, target.marketplace);
1324
+ try {
1325
+ await run(detected.command, args, { env, timeout: target.timeoutMs });
1326
+ } catch (error) {
1327
+ if (!isCodeBuddyWriteCommand(target, detected.command, args, 'plugin-update') || !isCodeBuddyResidualEnvelope(error)) throw error;
1328
+ }
1329
+ }
1330
+ const after = await readPlugin();
1331
+ if (!after.exact) throw new Error(`${target.host} did not install the frozen plugin identity`);
1332
+ const finalMarket = await observeHubMarketplace(target, detected.command, env, run);
1333
+ if (!finalMarket.installed || finalMarket.commit !== hubCommit) throw new Error(`${target.host} Hub checkout changed during plugin update`);
1334
+ await assertHubHostEntry(target, finalMarket.root);
1335
+ await verifyStructuredInstalledPayload({ plan, root, target, installPath: installPath ?? after.installPath, verifyInstalledPayload });
1336
+ return { status: 'UPDATED', version: target.version, restartRequired: true };
1337
+ }
1338
+
1147
1339
  async function runStructuredUpdate(target, detected, run, {
1148
1340
  plan,
1149
1341
  root,
1150
1342
  verifyInstalledPayload,
1151
1343
  }) {
1344
+ if (target.targetKind === 'hub-backed') return runHubStructuredUpdate(target, detected, run, { plan, root, verifyInstalledPayload });
1152
1345
  if (target.host === 'codebuddy' || target.host === 'workbuddy') {
1153
1346
  const env = hostEnvironment(target.host);
1154
- const listedObservation = target.host === 'codebuddy'
1155
- ? await runCodeBuddyRead(target, detected.command, CODEBUDDY_PLUGIN_LIST_ARGS, `${target.host} plugin list`, env, run)
1156
- : { output: await run(detected.command, [...CODEBUDDY_PLUGIN_LIST_ARGS], { env }), recovered: false };
1347
+ const listedObservation = await runCodeBuddyRead(
1348
+ target,
1349
+ detected.command,
1350
+ CODEBUDDY_PLUGIN_LIST_ARGS,
1351
+ `${target.host} plugin list`,
1352
+ env,
1353
+ run,
1354
+ );
1157
1355
  const listed = listedObservation.output;
1158
1356
  const observed = exactPluginObservation(target, listed.stdout);
1159
1357
  if (
@@ -1229,9 +1427,14 @@ async function runStructuredUpdate(target, detected, run, {
1229
1427
  pluginRecovered = true;
1230
1428
  }
1231
1429
  if (!pluginRecovered) {
1232
- const afterList = target.host === 'codebuddy'
1233
- ? await runCodeBuddyRead(target, detected.command, CODEBUDDY_PLUGIN_LIST_ARGS, `${target.host} plugin list`, env, run)
1234
- : { output: await run(detected.command, [...CODEBUDDY_PLUGIN_LIST_ARGS], { env }), recovered: false };
1430
+ const afterList = await runCodeBuddyRead(
1431
+ target,
1432
+ detected.command,
1433
+ CODEBUDDY_PLUGIN_LIST_ARGS,
1434
+ `${target.host} plugin list`,
1435
+ env,
1436
+ run,
1437
+ );
1235
1438
  after = exactPluginObservation(target, afterList.output.stdout);
1236
1439
  }
1237
1440
  if (!after.exact) throw new Error(`${target.host} did not update to the frozen plugin identity`);
@@ -2015,6 +2218,9 @@ async function updateLocalHostPluginsInternal({
2015
2218
  results.push({ host: target.host, unitId: target.unitId, ...outcome });
2016
2219
  continue;
2017
2220
  }
2221
+ if (target.targetKind === 'hub-backed' && target.host !== 'qoder') {
2222
+ await verifyFrozenSnapshot({ root, snapshotPath: target.snapshotPath, expectedDigest: target.manifestDigest });
2223
+ }
2018
2224
  const detected = await detect(target.host);
2019
2225
  if (!detected?.available) {
2020
2226
  results.push({