release-skill 0.9.0 → 0.9.1

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 (92) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +32 -0
  7. package/INSTALL.md +22 -9
  8. package/INSTALL.zh-CN.md +15 -8
  9. package/README.md +37 -30
  10. package/README.zh-CN.md +32 -27
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/consumer-contract-vectors.json +110 -0
  14. package/adapters/claude/bin/error-codes.json +8 -1
  15. package/adapters/claude/bin/foundation-resource-binding.json +1 -1
  16. package/adapters/claude/bin/registry.json +1 -1
  17. package/adapters/claude/bin/release-skill.bundle.mjs +1719 -1291
  18. package/adapters/claude/bin/rules.json +1 -1
  19. package/adapters/claude/bin/schemas/consumer-contract-vector.schema.json +149 -0
  20. package/adapters/claude/skills/release-finish/SKILL.md +4 -4
  21. package/adapters/claude/skills/release-help/SKILL.md +3 -2
  22. package/adapters/claude/skills/release-prepare/SKILL.md +2 -2
  23. package/adapters/claude/skills/release-publish/SKILL.md +3 -1
  24. package/adapters/claude/src/schemas/migration-manifest.schema.json +271 -7
  25. package/adapters/claude/src/schemas/profile-adoption-declaration.schema.json +8 -0
  26. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  27. package/adapters/codex/bin/consumer-contract-vectors.json +110 -0
  28. package/adapters/codex/bin/error-codes.json +8 -1
  29. package/adapters/codex/bin/foundation-resource-binding.json +1 -1
  30. package/adapters/codex/bin/registry.json +1 -1
  31. package/adapters/codex/bin/release-skill.bundle.mjs +1719 -1291
  32. package/adapters/codex/bin/rules.json +1 -1
  33. package/adapters/codex/bin/schemas/consumer-contract-vector.schema.json +149 -0
  34. package/adapters/codex/skills/release-finish/SKILL.md +4 -4
  35. package/adapters/codex/skills/release-help/SKILL.md +3 -2
  36. package/adapters/codex/skills/release-prepare/SKILL.md +2 -2
  37. package/adapters/codex/skills/release-publish/SKILL.md +3 -1
  38. package/adapters/codex/src/schemas/migration-manifest.schema.json +271 -7
  39. package/adapters/codex/src/schemas/profile-adoption-declaration.schema.json +8 -0
  40. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  41. package/adapters/kimi/bin/consumer-contract-vectors.json +110 -0
  42. package/adapters/kimi/bin/error-codes.json +8 -1
  43. package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
  44. package/adapters/kimi/bin/registry.json +1 -1
  45. package/adapters/kimi/bin/release-skill.bundle.mjs +1719 -1291
  46. package/adapters/kimi/bin/rules.json +1 -1
  47. package/adapters/kimi/bin/schemas/consumer-contract-vector.schema.json +149 -0
  48. package/adapters/kimi/skills/release-finish/SKILL.md +4 -4
  49. package/adapters/kimi/skills/release-help/SKILL.md +3 -2
  50. package/adapters/kimi/skills/release-prepare/SKILL.md +2 -2
  51. package/adapters/kimi/skills/release-publish/SKILL.md +3 -1
  52. package/adapters/kimi/src/schemas/migration-manifest.schema.json +271 -7
  53. package/adapters/kimi/src/schemas/profile-adoption-declaration.schema.json +8 -0
  54. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  55. package/adapters/workbuddy/bin/consumer-contract-vectors.json +110 -0
  56. package/adapters/workbuddy/bin/error-codes.json +8 -1
  57. package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
  58. package/adapters/workbuddy/bin/registry.json +1 -1
  59. package/adapters/workbuddy/bin/release-skill.bundle.mjs +1719 -1291
  60. package/adapters/workbuddy/bin/rules.json +1 -1
  61. package/adapters/workbuddy/bin/schemas/consumer-contract-vector.schema.json +149 -0
  62. package/adapters/workbuddy/skills/release-finish/SKILL.md +4 -4
  63. package/adapters/workbuddy/skills/release-help/SKILL.md +3 -2
  64. package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -2
  65. package/adapters/workbuddy/skills/release-publish/SKILL.md +3 -1
  66. package/adapters/workbuddy/src/schemas/migration-manifest.schema.json +271 -7
  67. package/adapters/workbuddy/src/schemas/profile-adoption-declaration.schema.json +8 -0
  68. package/bin/consumer-contract-vectors.json +110 -0
  69. package/bin/error-codes.json +8 -1
  70. package/bin/foundation-resource-binding.json +1 -1
  71. package/bin/registry.json +1 -1
  72. package/bin/release-skill-cli.mjs +40 -4
  73. package/bin/release-skill.bundle.mjs +1719 -1291
  74. package/bin/rules.json +1 -1
  75. package/bin/schemas/consumer-contract-vector.schema.json +149 -0
  76. package/package.json +4 -4
  77. package/platform-manifest.json +5 -5
  78. package/skills/release-finish/SKILL.md +4 -4
  79. package/skills/release-help/SKILL.md +3 -2
  80. package/skills/release-prepare/SKILL.md +2 -2
  81. package/skills/release-publish/SKILL.md +3 -1
  82. package/skills-src/release-finish/SKILL.md +4 -4
  83. package/skills-src/release-help/SKILL.md +3 -2
  84. package/skills-src/release-prepare/SKILL.md +2 -2
  85. package/skills-src/release-publish/SKILL.md +3 -1
  86. package/src/commands/post-release-local.mjs +249 -58
  87. package/src/commands/prepare.mjs +39 -4
  88. package/src/commands/ship.mjs +124 -16
  89. package/src/core/derived-artifact-gates.mjs +32 -5
  90. package/src/producers/foundation-resource-projection.mjs +16 -3
  91. package/src/schemas/migration-manifest.schema.json +271 -7
  92. package/src/schemas/profile-adoption-declaration.schema.json +8 -0
@@ -64,6 +64,9 @@ function actionTarget(plan, action, host) {
64
64
  const tagAction = (plan.externalActions ?? []).find((candidate) => (
65
65
  candidate.type === 'create-tag' && candidate.unitId === action.unitId
66
66
  ));
67
+ const pushAction = (plan.externalActions ?? []).find((candidate) => (
68
+ candidate.type === 'push-snapshot' && candidate.unitId === action.unitId
69
+ ));
67
70
  const pluginTag = tagAction?.parameters?.tag;
68
71
  if (
69
72
  pluginRepo !== unit.publicRepo
@@ -85,6 +88,28 @@ function actionTarget(plan, action, host) {
85
88
  } else if (sourceDescriptor.form !== 'bundled-family' || sourceDescriptor.commit !== pluginCommit) {
86
89
  throw new Error(`post-release action ${action.id} has no single frozen source commit`);
87
90
  }
91
+ const codeBuddyFamily = host === 'codebuddy' || host === 'workbuddy';
92
+ if (
93
+ codeBuddyFamily
94
+ && sourceDescriptor.form === 'bundled-family'
95
+ && (
96
+ pushAction?.parameters?.repo !== pluginRepo
97
+ || pushAction?.parameters?.commit !== pluginCommit
98
+ || typeof pushAction?.parameters?.branch !== 'string'
99
+ || pushAction.parameters.branch.length === 0
100
+ || (
101
+ pushAction.parameters.branch.startsWith('refs/')
102
+ && !pushAction.parameters.branch.startsWith('refs/heads/')
103
+ )
104
+ )
105
+ ) {
106
+ throw new Error(`post-release action ${action.id} has no matching frozen mutable branch identity`);
107
+ }
108
+ const marketplaceRef = codeBuddyFamily && sourceDescriptor.form === 'bundled-family'
109
+ ? (pushAction.parameters.branch.startsWith('refs/heads/')
110
+ ? pushAction.parameters.branch
111
+ : `refs/heads/${pushAction.parameters.branch}`)
112
+ : parameters.ref;
88
113
  return {
89
114
  host,
90
115
  actionId: action.id,
@@ -96,11 +121,13 @@ function actionTarget(plan, action, host) {
96
121
  : parameters.marketplace ?? sourceDescriptor.marketplaceEntry ?? parameters.plugin,
97
122
  version: parameters.version,
98
123
  pluginRepo,
124
+ githubHost: tagAction?.parameters?.githubHost ?? pushAction?.parameters?.githubHost ?? 'github.com',
99
125
  pluginTag,
100
126
  pluginCommit,
101
127
  marketplaceRepo,
102
- marketplaceRef: parameters.ref,
128
+ marketplaceRef,
103
129
  marketplaceCommit,
130
+ sourceForm: sourceDescriptor.form,
104
131
  timeoutMs: parameters.timeoutMs ?? 300_000,
105
132
  };
106
133
  }
@@ -225,6 +252,13 @@ async function defaultRun(command, args, options = {}) {
225
252
  `${command} exited ${envelope.exitStatus}${stderr.trim() ? `: ${stderr.trim()}` : ''}`,
226
253
  );
227
254
  error.exitStatus = envelope.exitStatus;
255
+ error.hostCommandUnavailable = envelope.processStatus === 'FAILED_TO_START';
256
+ error.details = {
257
+ processStatus: envelope.processStatus,
258
+ terminationReason: envelope.terminationReason,
259
+ watchdogReason: envelope.watchdogReason,
260
+ ...(envelope.evidence?.spawnError ? { spawnError: envelope.evidence.spawnError } : {}),
261
+ };
228
262
  throw error;
229
263
  }
230
264
  return { stdout, stderr };
@@ -235,8 +269,9 @@ async function commandAvailable(command, host, run) {
235
269
  try {
236
270
  await run(command, ['--version'], { timeout: 10_000, env: hostEnvironment(host) });
237
271
  return true;
238
- } catch {
239
- return false;
272
+ } catch (error) {
273
+ if (error?.hostCommandUnavailable === true) return false;
274
+ throw error;
240
275
  }
241
276
  }
242
277
 
@@ -298,8 +333,10 @@ function exactPluginObservation(target, stdout) {
298
333
  };
299
334
  }
300
335
  if (!Array.isArray(parsed)) throw new Error(`${target.host} plugin list did not return an array`);
301
- const found = parsed.find((entry) => entry?.id === selector);
302
- if (!found) return { installed: false };
336
+ const matches = parsed.filter((entry) => entry?.id === selector);
337
+ if (matches.length === 0) return { installed: false };
338
+ if (matches.length !== 1) throw new Error(`${target.host} plugin list returned conflicting entries for ${selector}`);
339
+ const [found] = matches;
303
340
  return {
304
341
  installed: true,
305
342
  exact: found.version === target.version && found.gitCommitSha === target.pluginCommit,
@@ -346,7 +383,7 @@ async function observeStructuredTarget(target, command, env, run) {
346
383
  }
347
384
 
348
385
  async function bindStructuredMarketplace(target, command, env, run, observed) {
349
- if (observed.marketplace.installed && observed.marketplace.exact) return;
386
+ if (observed.marketplace.installed && observed.marketplace.exact) return false;
350
387
  if (observed.marketplace.installed) {
351
388
  await run(command, [
352
389
  'plugin', 'marketplace', 'remove', target.marketplace,
@@ -360,6 +397,7 @@ async function bindStructuredMarketplace(target, command, env, run, observed) {
360
397
  if (!rebound.installed || !rebound.exact) {
361
398
  throw new Error(`${target.host} marketplace did not bind to frozen commit ${target.marketplaceCommit}`);
362
399
  }
400
+ return true;
363
401
  }
364
402
 
365
403
  function actionParametersForTarget(plan, target) {
@@ -397,11 +435,50 @@ async function runStructuredUpdate(target, detected, run, {
397
435
  const env = hostEnvironment(target.host);
398
436
  const listed = await run(detected.command, ['plugin', 'list', '--json'], { env });
399
437
  const observed = exactPluginObservation(target, listed.stdout);
438
+ if (
439
+ target.sourceForm !== 'bundled-family'
440
+ || target.pluginRepo !== target.marketplaceRepo
441
+ || target.pluginCommit !== target.marketplaceCommit
442
+ ) {
443
+ return {
444
+ status: 'MANUAL_REQUIRED',
445
+ reason: `${target.host} source identity is not eligible for a frozen bundled-family update`,
446
+ };
447
+ }
400
448
  if (observed.exact) return { status: 'ALREADY_CURRENT', version: target.version };
401
- return {
402
- status: 'MANUAL_REQUIRED',
403
- reason: `${target.host} CLI cannot pin an install to frozen ref ${target.pluginTag}; no local mutation was performed`,
404
- };
449
+ if (!observed.installed) {
450
+ return {
451
+ status: 'MANUAL_REQUIRED',
452
+ reason: `${target.host} target plugin is not installed; no initial installation was performed`,
453
+ };
454
+ }
455
+ let remote;
456
+ try {
457
+ const remoteObservation = await run('git', [
458
+ 'ls-remote', '--exit-code', `https://${target.githubHost}/${target.pluginRepo}.git`,
459
+ target.pluginTag, `${target.pluginTag}^{}`, target.marketplaceRef,
460
+ ], { env, timeout: 30_000 });
461
+ remote = parseCodeBuddyRemoteObservation(target, remoteObservation.stdout);
462
+ } catch (error) {
463
+ return {
464
+ status: 'MANUAL_REQUIRED',
465
+ reason: `${target.host} could not prove the frozen remote refs: ${error?.message ?? String(error)}`,
466
+ };
467
+ }
468
+ if (!remote.exact) {
469
+ return {
470
+ status: 'MANUAL_REQUIRED',
471
+ reason: `${target.host} frozen tag and mutable ref do not both resolve to ${target.pluginCommit}`,
472
+ };
473
+ }
474
+ await run(detected.command, ['plugin', 'marketplace', 'update', target.marketplace], { env });
475
+ await run(detected.command, [
476
+ 'plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user',
477
+ ], { env });
478
+ const afterList = await run(detected.command, ['plugin', 'list', '--json'], { env });
479
+ const after = exactPluginObservation(target, afterList.stdout);
480
+ if (!after.exact) throw new Error(`${target.host} did not update to the frozen plugin identity`);
481
+ return { status: 'UPDATED', version: target.version, restartRequired: true };
405
482
  }
406
483
 
407
484
  const command = detected.command;
@@ -418,12 +495,15 @@ async function runStructuredUpdate(target, detected, run, {
418
495
  return { status: 'ALREADY_CURRENT', version: target.version };
419
496
  }
420
497
 
421
- await bindStructuredMarketplace(target, command, env, run, before);
498
+ const marketplaceRebound = await bindStructuredMarketplace(target, command, env, run, before);
499
+ const current = target.host === 'claude' && marketplaceRebound
500
+ ? await observeStructuredTarget(target, command, env, run)
501
+ : before;
422
502
  const platform = getPlatform(target.host);
423
503
  const selector = `${target.plugin}@${target.marketplace}`;
424
504
  let installPath;
425
505
  if (target.host === 'claude') {
426
- const installArgs = before.plugin.installed
506
+ const installArgs = current.plugin.installed
427
507
  ? ['plugin', 'update', selector, '--scope', 'user', '--yes']
428
508
  : platform.cli.install(target.plugin, target.marketplace);
429
509
  await run(command, installArgs, { env });
@@ -450,10 +530,77 @@ async function runStructuredUpdate(target, detected, run, {
450
530
  return { status: 'UPDATED', version: target.version, restartRequired: true };
451
531
  }
452
532
 
453
- function kimiExpectProgram(kimiCommand, installUrl) {
454
- const quotedCommand = JSON.stringify(kimiCommand);
455
- const quotedUrl = JSON.stringify(installUrl);
456
- return `set timeout 180\nspawn -- ${quotedCommand}\nexpect -re {> $}\nsend -- "/plugins install ${quotedUrl}\\r"\nexpect {\n -re {Trust and install} { send -- "\\033\\[B\\r" }\n timeout { exit 91 }\n eof { exit 92 }\n}\nexpect -re {> $}\nsend -- "/reload\\r"\nexpect -re {> $}\nsend -- "/exit\\r"\nexpect eof\n`;
533
+ function parseCodeBuddyRemoteObservation(target, stdout) {
534
+ const refs = new Map();
535
+ const tagRef = target.pluginTag.startsWith('refs/')
536
+ ? target.pluginTag
537
+ : `refs/tags/${target.pluginTag}`;
538
+ const allowedRefs = new Set([tagRef, `${tagRef}^{}`, target.marketplaceRef]);
539
+ for (const line of String(stdout ?? '').split(/\r?\n/u)) {
540
+ if (line.length === 0) continue;
541
+ const match = /^([a-f0-9]{40})\t([^\s]+)$/u.exec(line);
542
+ if (!match) throw new Error('git ls-remote returned an invalid line');
543
+ const [, commit, ref] = match;
544
+ if (!allowedRefs.has(ref)) throw new Error(`git ls-remote returned an unexpected ref ${ref}`);
545
+ const existing = refs.get(ref);
546
+ if (existing && existing !== commit) throw new Error(`git ls-remote returned conflicting values for ${ref}`);
547
+ refs.set(ref, commit);
548
+ }
549
+ const peeledTag = refs.get(`${tagRef}^{}`);
550
+ const tagCommit = peeledTag ?? refs.get(tagRef);
551
+ const mutableCommit = refs.get(target.marketplaceRef);
552
+ return {
553
+ exact: tagCommit === target.pluginCommit && mutableCommit === target.pluginCommit,
554
+ tagCommit: tagCommit ?? null,
555
+ mutableCommit: mutableCommit ?? null,
556
+ };
557
+ }
558
+
559
+ function kimiExpectProgram({ removePlugin } = {}) {
560
+ const removeCommand = removePlugin
561
+ ? `send -- "/plugins remove $removePlugin\\r"
562
+ expect {
563
+ -nocase -re {(remove|delete|uninstall).*(confirm|sure)|(confirm|sure).*(remove|delete|uninstall)} {
564
+ expect {
565
+ -ex $removePlugin { send -- "\\033\\[B\\r" }
566
+ timeout { exit 95 }
567
+ eof { exit 96 }
568
+ }
569
+ expect -re {> $}
570
+ }
571
+ -re {> $} {}
572
+ timeout { exit 93 }
573
+ eof { exit 94 }
574
+ }
575
+ `
576
+ : '';
577
+ return `set timeout 180
578
+ foreach variable {RELEASE_SKILL_KIMI_COMMAND RELEASE_SKILL_KIMI_INSTALL_URL RELEASE_SKILL_KIMI_REMOVE_PLUGIN} {
579
+ if {![info exists env($variable)]} { exit 90 }
580
+ }
581
+ set kimiCommand $env(RELEASE_SKILL_KIMI_COMMAND)
582
+ set installUrl $env(RELEASE_SKILL_KIMI_INSTALL_URL)
583
+ set removePlugin $env(RELEASE_SKILL_KIMI_REMOVE_PLUGIN)
584
+ spawn $kimiCommand
585
+ expect -re {> $}
586
+ ${removeCommand}send -- "/plugins install $installUrl\\r"
587
+ expect {
588
+ -nocase -re {trust and install} {
589
+ expect {
590
+ -ex $installUrl { send -- "\\033\\[B\\r" }
591
+ timeout { exit 97 }
592
+ eof { exit 98 }
593
+ }
594
+ }
595
+ timeout { exit 91 }
596
+ eof { exit 92 }
597
+ }
598
+ expect -re {> $}
599
+ send -- "/reload\\r"
600
+ expect -re {> $}
601
+ send -- "/exit\\r"
602
+ expect eof
603
+ `;
457
604
  }
458
605
 
459
606
  async function observeKimiTarget(target, kimiHome, run) {
@@ -474,6 +621,7 @@ async function observeKimiTarget(target, kimiHome, run) {
474
621
  }
475
622
  const entry = installed?.plugins?.find((candidate) => candidate?.id === target.plugin);
476
623
  if (!entry) return { installed: false };
624
+ if (!entry.github) return { installed: true, exact: false, source: 'legacy', entry };
477
625
  const managedRoot = join(pluginsRoot, 'managed');
478
626
  const pluginRoot = await resolveContained(managedRoot, target.plugin);
479
627
  const declaredRoot = await resolveContained(managedRoot, relative(managedRoot, entry.root));
@@ -482,35 +630,53 @@ async function observeKimiTarget(target, kimiHome, run) {
482
630
  await readFileContained(pluginRoot, 'package.json', { encoding: 'utf8' }),
483
631
  'Kimi installed package manifest',
484
632
  );
485
- let refName;
486
- let revision;
487
- if (entry.github) {
488
- refName = entry.github.ref?.kind === 'tag' ? entry.github.ref.value : undefined;
489
- revision = entry.github.installedSha;
490
- } else {
491
- const metadata = parseJson(
492
- await readFileContained(pluginRoot, '.codex-marketplace-install.json', { encoding: 'utf8' }),
493
- 'Kimi installed source binding',
494
- );
495
- refName = metadata.ref_name;
496
- revision = metadata.revision;
633
+ const refName = entry.github.ref?.kind === 'tag' ? entry.github.ref.value : undefined;
634
+ const revision = entry.github.installedSha;
635
+ let gitHeadExact = true;
636
+ try {
637
+ await access(join(pluginRoot, '.git'));
638
+ const head = await run('git', ['-C', pluginRoot, 'rev-parse', 'HEAD'], {
639
+ env: hostEnvironment('kimi', { kimiHome }),
640
+ timeout: 30_000,
641
+ });
642
+ gitHeadExact = head.stdout.trim() === target.pluginCommit;
643
+ } catch (error) {
644
+ if (error?.code !== 'ENOENT') throw error;
497
645
  }
498
- const head = await run('git', ['-C', pluginRoot, 'rev-parse', 'HEAD'], {
499
- env: hostEnvironment('kimi'),
500
- timeout: 30_000,
501
- });
502
646
  const exact = packageJson.name === target.plugin
503
647
  && packageJson.version === target.version
504
648
  && refName === target.pluginTag
505
649
  && revision === target.pluginCommit
506
- && head.stdout.trim() === target.pluginCommit;
507
- return { installed: true, exact, entry, packageJson, refName, revision, pluginRoot };
650
+ && gitHeadExact;
651
+ return {
652
+ installed: true,
653
+ exact,
654
+ source: 'github',
655
+ entry,
656
+ packageJson,
657
+ refName,
658
+ revision,
659
+ pluginRoot,
660
+ };
508
661
  }
509
662
 
510
- async function runKimiUpdate(target, detected, run, kimiHome) {
663
+ async function runKimiUpdate(target, detected, run, kimiHome, {
664
+ plan,
665
+ root,
666
+ verifyInstalledPayload,
667
+ }) {
511
668
  const env = hostEnvironment('kimi', { kimiHome });
512
669
  const before = await observeKimiTarget(target, kimiHome, run);
513
- if (before.exact) return { status: 'ALREADY_CURRENT', version: target.version };
670
+ if (before.exact) {
671
+ await verifyStructuredInstalledPayload({
672
+ plan,
673
+ root,
674
+ target,
675
+ installPath: before.pluginRoot,
676
+ verifyInstalledPayload,
677
+ });
678
+ return { status: 'ALREADY_CURRENT', version: target.version };
679
+ }
514
680
  await withTemporaryWorkspace(async (workspace) => {
515
681
  const checkout = join(workspace.root, 'plugin');
516
682
  await run('git', [
@@ -525,13 +691,28 @@ async function runKimiUpdate(target, detected, run, kimiHome) {
525
691
  throw new Error(`Kimi checkout commit does not match frozen release commit ${target.pluginCommit}`);
526
692
  }
527
693
  const installUrl = `https://github.com/${target.pluginRepo}/releases/tag/${target.pluginTag}`;
528
- await run(detected.expectCommand, ['-c', kimiExpectProgram(detected.command, installUrl)], {
694
+ const removePlugin = before.source === 'legacy' ? target.plugin : '';
695
+ await run(detected.expectCommand, ['-c', kimiExpectProgram({
696
+ ...(removePlugin ? { removePlugin } : {}),
697
+ })], {
529
698
  timeout: 240_000,
530
- env,
699
+ env: {
700
+ ...env,
701
+ RELEASE_SKILL_KIMI_COMMAND: detected.command,
702
+ RELEASE_SKILL_KIMI_INSTALL_URL: installUrl,
703
+ RELEASE_SKILL_KIMI_REMOVE_PLUGIN: removePlugin,
704
+ },
531
705
  });
532
706
  }, { prefix: 'release-skill-kimi-update-' });
533
707
  const after = await observeKimiTarget(target, kimiHome, run);
534
708
  if (!after.exact) throw new Error('Kimi did not report the frozen plugin identity after TUI installation');
709
+ await verifyStructuredInstalledPayload({
710
+ plan,
711
+ root,
712
+ target,
713
+ installPath: after.pluginRoot,
714
+ verifyInstalledPayload,
715
+ });
535
716
  return { status: 'UPDATED', version: target.version, restartRequired: true };
536
717
  }
537
718
 
@@ -547,6 +728,19 @@ function aggregateStatus(results) {
547
728
  return 'NO_APPLICABLE_HOSTS';
548
729
  }
549
730
 
731
+ function failedHostResult(target, error) {
732
+ const message = error?.message ?? String(error);
733
+ return {
734
+ host: target.host,
735
+ unitId: target.unitId,
736
+ status: 'FAILED',
737
+ error: message,
738
+ reason: typeof error?.reason === 'string' ? error.reason : message,
739
+ ...(error?.code !== undefined ? { code: error.code } : {}),
740
+ ...(error?.details !== undefined ? { details: error.details } : {}),
741
+ };
742
+ }
743
+
550
744
  export async function updateLocalHostPlugins({
551
745
  plan,
552
746
  root = process.cwd(),
@@ -562,9 +756,7 @@ export async function updateLocalHostPlugins({
562
756
  if (confirmPlanDigest !== plan.digest) {
563
757
  throw new Error('plan digest confirmation does not match the frozen release plan');
564
758
  }
565
- const selected = selectedHosts?.length > 0
566
- ? new Set(selectedHosts)
567
- : new Set(checklist.localHostUpdate.hosts);
759
+ const selected = new Set(Array.isArray(selectedHosts) ? selectedHosts : []);
568
760
  const unknown = [...selected].filter((host) => !checklist.localHostUpdate.hosts.includes(host));
569
761
  if (unknown.length > 0) throw new Error(`selected hosts are not declared by the plan: ${unknown.join(', ')}`);
570
762
  const targets = checklist.localHostUpdate.targets.filter((item) => selected.has(item.host));
@@ -572,19 +764,23 @@ export async function updateLocalHostPlugins({
572
764
 
573
765
  const results = [];
574
766
  for (const target of targets) {
575
- const detected = await detect(target.host);
576
- if (!detected?.available) {
577
- results.push({
578
- host: target.host,
579
- unitId: target.unitId,
580
- status: 'SKIPPED_NOT_INSTALLED',
581
- reason: detected?.reason ?? 'host unavailable',
582
- });
583
- continue;
584
- }
585
767
  try {
768
+ const detected = await detect(target.host);
769
+ if (!detected?.available) {
770
+ results.push({
771
+ host: target.host,
772
+ unitId: target.unitId,
773
+ status: 'SKIPPED_NOT_INSTALLED',
774
+ reason: detected?.reason ?? 'host unavailable',
775
+ });
776
+ continue;
777
+ }
586
778
  const outcome = target.host === 'kimi'
587
- ? await runKimiUpdate(target, detected, run, effectiveKimiHome)
779
+ ? await runKimiUpdate(target, detected, run, effectiveKimiHome, {
780
+ plan,
781
+ root,
782
+ verifyInstalledPayload,
783
+ })
588
784
  : await runStructuredUpdate(target, detected, run, {
589
785
  plan,
590
786
  root,
@@ -592,12 +788,7 @@ export async function updateLocalHostPlugins({
592
788
  });
593
789
  results.push({ host: target.host, unitId: target.unitId, ...outcome });
594
790
  } catch (error) {
595
- results.push({
596
- host: target.host,
597
- unitId: target.unitId,
598
- status: 'FAILED',
599
- error: error?.message ?? String(error),
600
- });
791
+ results.push(failedHostResult(target, error));
601
792
  }
602
793
  }
603
794
  return {
@@ -57,7 +57,11 @@ import {
57
57
  } from '../snapshot/frozen.mjs';
58
58
  import { ReleaseError, GATE_FAILED, CONFIG_INVALID, CONFIG_MISSING, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE, DIRTY_SOURCE_INPUT, BUNDLE_STALE } from '../core/errors.mjs';
59
59
  import { assertBundleFreshness } from '../core/bundle-freshness.mjs';
60
- import { assertAdapterFreshness, assertSelfBootstrapFacts } from '../core/derived-artifact-gates.mjs';
60
+ import {
61
+ assertAdapterFreshness,
62
+ assertPlatformManifestFreshness,
63
+ assertSelfBootstrapFacts,
64
+ } from '../core/derived-artifact-gates.mjs';
61
65
  import { isFoundationPluginVerificationEligible } from '../core/foundation-plugin-verification.mjs';
62
66
  import { PKG_ROOT } from '../core/pkg-root.mjs';
63
67
  import { writeFrozenMarker, FROZEN_MARKER_FILENAME } from '../core/frozen-marker.mjs';
@@ -3025,8 +3029,9 @@ export async function prepareRelease(options) {
3025
3029
  // --- Step 1b-fast: version-sensitive derived-artifact fast pre-gates (O1) ---
3026
3030
  // 2026-08-18 investigation §3.2: adapter drift and stale self-bootstrap
3027
3031
  // fact pins used to surface only deep inside the ~80s full test hook.
3028
- // Promote the two exact canonical checks (build-adapters --check and the
3029
- // release-docs-self-bootstrap single-file test) to prepare's earliest
3032
+ // Promote the three exact canonical checks (build-adapters --check,
3033
+ // generate-platform-manifest --check, and the release-docs-self-bootstrap
3034
+ // single-file test) to prepare's earliest
3030
3035
  // stage so the same drift fails closed in seconds, before any hook. Like
3031
3036
  // the bundle freshness gate this is artifact-integrity class: workflow
3032
3037
  // trimming never exempts it. Installed layouts record not-applicable.
@@ -3034,7 +3039,8 @@ export async function prepareRelease(options) {
3034
3039
  // the CLI, plain function calls, and the node:test harness — no
3035
3040
  // environment variable may exempt them (a test seam is never a
3036
3041
  // production switch). Tests that need lightweight fixtures inject the
3037
- // adapterFreshnessFn/selfBootstrapFactsFn seams explicitly in-process.
3042
+ // adapterFreshnessFn/platformManifestFreshnessFn/selfBootstrapFactsFn
3043
+ // seams explicitly in-process.
3038
3044
  await evidence.append({ phase: 'adapter-freshness', status: 'started' });
3039
3045
  const adapterFreshnessFn = options.adapterFreshnessFn ?? assertAdapterFreshness;
3040
3046
  let adapterFreshness;
@@ -3063,6 +3069,35 @@ export async function prepareRelease(options) {
3063
3069
  });
3064
3070
  }
3065
3071
 
3072
+ await evidence.append({ phase: 'platform-manifest-freshness', status: 'started' });
3073
+ const platformManifestFreshnessFn = options.platformManifestFreshnessFn
3074
+ ?? assertPlatformManifestFreshness;
3075
+ let platformManifestFreshness;
3076
+ try {
3077
+ platformManifestFreshness = await platformManifestFreshnessFn(PKG_ROOT);
3078
+ } catch (err) {
3079
+ await evidence.append({
3080
+ phase: 'platform-manifest-freshness',
3081
+ status: 'blocking',
3082
+ reason: err.details?.reason ?? null,
3083
+ error: { code: err.code, message: err.message },
3084
+ });
3085
+ throw err;
3086
+ }
3087
+ if (platformManifestFreshness?.applicable === false) {
3088
+ await evidence.append({
3089
+ phase: 'platform-manifest-freshness',
3090
+ status: 'not-applicable',
3091
+ reason: platformManifestFreshness.reason,
3092
+ });
3093
+ } else {
3094
+ await evidence.append({
3095
+ phase: 'platform-manifest-freshness',
3096
+ status: 'completed',
3097
+ durationMs: platformManifestFreshness?.durationMs ?? null,
3098
+ });
3099
+ }
3100
+
3066
3101
  await evidence.append({ phase: 'self-bootstrap-facts', status: 'started' });
3067
3102
  const selfBootstrapFactsFn = options.selfBootstrapFactsFn ?? assertSelfBootstrapFacts;
3068
3103
  let selfBootstrapFacts;