release-skill 0.9.3 → 0.9.5

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 (79) 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 +58 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +52 -16
  10. package/README.zh-CN.md +46 -16
  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 +7 -7
  14. package/adapters/claude/bin/foundation-resource-binding.json +1 -1
  15. package/adapters/claude/bin/release-skill.bundle.mjs +1513 -352
  16. package/adapters/claude/schemas/release-plan.schema.json +36 -0
  17. package/adapters/claude/schemas/release-project.schema.json +31 -0
  18. package/adapters/claude/skills/release-finish/SKILL.md +5 -5
  19. package/adapters/claude/skills/release-help/SKILL.md +11 -4
  20. package/adapters/claude/skills/release-prepare/SKILL.md +11 -3
  21. package/adapters/claude/skills/release-verify/SKILL.md +2 -1
  22. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  23. package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
  24. package/adapters/codex/bin/foundation-resource-binding.json +1 -1
  25. package/adapters/codex/bin/release-skill.bundle.mjs +1513 -352
  26. package/adapters/codex/schemas/release-plan.schema.json +36 -0
  27. package/adapters/codex/schemas/release-project.schema.json +31 -0
  28. package/adapters/codex/skills/release-finish/SKILL.md +5 -5
  29. package/adapters/codex/skills/release-help/SKILL.md +11 -4
  30. package/adapters/codex/skills/release-prepare/SKILL.md +11 -3
  31. package/adapters/codex/skills/release-verify/SKILL.md +2 -1
  32. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  33. package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
  34. package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
  35. package/adapters/kimi/bin/release-skill.bundle.mjs +1513 -352
  36. package/adapters/kimi/schemas/release-plan.schema.json +36 -0
  37. package/adapters/kimi/schemas/release-project.schema.json +31 -0
  38. package/adapters/kimi/skills/release-finish/SKILL.md +5 -5
  39. package/adapters/kimi/skills/release-help/SKILL.md +11 -4
  40. package/adapters/kimi/skills/release-prepare/SKILL.md +11 -3
  41. package/adapters/kimi/skills/release-verify/SKILL.md +2 -1
  42. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  43. package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
  44. package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
  45. package/adapters/workbuddy/bin/release-skill.bundle.mjs +1513 -352
  46. package/adapters/workbuddy/schemas/release-plan.schema.json +36 -0
  47. package/adapters/workbuddy/schemas/release-project.schema.json +31 -0
  48. package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -5
  49. package/adapters/workbuddy/skills/release-help/SKILL.md +11 -4
  50. package/adapters/workbuddy/skills/release-prepare/SKILL.md +11 -3
  51. package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -1
  52. package/bin/consumer-contract-vectors.json +7 -7
  53. package/bin/foundation-resource-binding.json +1 -1
  54. package/bin/release-skill-cli.mjs +113 -22
  55. package/bin/release-skill.bundle.mjs +1513 -352
  56. package/package.json +4 -4
  57. package/platform-manifest.json +4 -4
  58. package/references/.render-manifest.json +4 -4
  59. package/references/01-state-machine.md +18 -0
  60. package/references/02-project-config.md +20 -0
  61. package/schemas/release-plan.schema.json +36 -0
  62. package/schemas/release-project.schema.json +31 -0
  63. package/skills/release-finish/SKILL.md +5 -5
  64. package/skills/release-help/SKILL.md +11 -4
  65. package/skills/release-prepare/SKILL.md +11 -3
  66. package/skills/release-verify/SKILL.md +2 -1
  67. package/skills-src/release-finish/SKILL.md +5 -5
  68. package/skills-src/release-help/SKILL.md +11 -4
  69. package/skills-src/release-prepare/SKILL.md +11 -3
  70. package/skills-src/release-verify/SKILL.md +2 -1
  71. package/src/commands/post-release-local.mjs +373 -26
  72. package/src/commands/prepare.mjs +361 -16
  73. package/src/commands/ship.mjs +190 -13
  74. package/src/commands/verify.mjs +175 -2
  75. package/src/core/foundation-plugin-verification.mjs +22 -2
  76. package/src/core/plan.mjs +45 -1
  77. package/src/platforms/registry.mjs +97 -0
  78. package/src/producers/foundation-resource-projection.mjs +3 -3
  79. package/src/readme/contract.mjs +23 -3
@@ -1,4 +1,4 @@
1
- import { access } from 'node:fs/promises';
1
+ import { access, readFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, relative } from 'node:path';
4
4
 
@@ -12,6 +12,8 @@ import {
12
12
  import { getPlatform } from '../platforms/registry.mjs';
13
13
  import { resolveCodeBuddyMarketplace } from '../platforms/codebuddy.mjs';
14
14
  import { verifyInstalledMarketplacePayload } from '../adapters/plugin-marketplace.mjs';
15
+ import { normalizePostPublishView, postPublishActionId } from '../core/postpublish.mjs';
16
+ import { loadRun, validateRunLineage } from '../core/run.mjs';
15
17
 
16
18
  const HOSTS_BY_ACTION = Object.freeze({
17
19
  'claude-marketplace-install': ['claude'],
@@ -34,7 +36,18 @@ const SAFE_ENV_KEYS = Object.freeze([
34
36
  'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
35
37
  'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
36
38
  'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CONFIG_DIR',
39
+ 'CODEBUDDY_CONFIG_DIR', 'WORKBUDDY_CONFIG_DIR',
37
40
  ]);
41
+ const CODEBUDDY_PLUGIN_LIST_ARGS = Object.freeze(['plugin', 'list', '--json']);
42
+ const CODEBUDDY_MARKETPLACE_LIST_ARGS = Object.freeze(['plugin', 'marketplace', 'list']);
43
+
44
+ function attachFoundationFailure(error, { envelope, stdout }) {
45
+ Object.defineProperties(error, {
46
+ foundationEnvelope: { value: envelope, enumerable: false },
47
+ foundationStdout: { value: stdout, enumerable: false },
48
+ });
49
+ return error;
50
+ }
38
51
 
39
52
  function actionTarget(plan, action, host) {
40
53
  const parameters = action.parameters ?? {};
@@ -159,7 +172,27 @@ function assertExecutableTarget(target) {
159
172
  }
160
173
  }
161
174
 
162
- export function derivePostReleaseChecklist(plan) {
175
+ function buildShipNextStep({ root, statePath, unitIds }) {
176
+ const argv = ['release-skill', 'ship'];
177
+ if (typeof root === 'string' && root.length > 0) argv.push('--root', root);
178
+ if (typeof statePath === 'string' && statePath.length > 0) argv.push('--state', statePath);
179
+ if (Array.isArray(unitIds) && unitIds.length > 0) {
180
+ for (const unitId of unitIds) argv.push('--unit', unitId);
181
+ }
182
+ return {
183
+ code: 'COMPLETE_POST_VERIFY',
184
+ message: 'Complete the postVerify phase with ship before running post-release.',
185
+ argv,
186
+ };
187
+ }
188
+
189
+ export function derivePostReleaseChecklist(plan, {
190
+ runPath,
191
+ root,
192
+ statePath,
193
+ unitIds,
194
+ postVerifyComplete = false,
195
+ } = {}) {
163
196
  if (!plan || typeof plan !== 'object' || typeof plan.digest !== 'string') {
164
197
  throw new Error('a frozen release plan with digest is required');
165
198
  }
@@ -168,6 +201,9 @@ export function derivePostReleaseChecklist(plan) {
168
201
  !BRANCH_ACTION_INCLUDED.has(unit.productionConfig?.branchStrategy)
169
202
  ));
170
203
  const targets = pluginTargets(plan);
204
+ const hasPendingPostVerify = postVerifyHooks(plan).length > 0 && !postVerifyComplete && targets.length > 0;
205
+ const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
206
+ const selectedUnitIds = Array.isArray(unitIds) ? unitIds : undefined;
171
207
  return {
172
208
  command: 'post-release',
173
209
  status: 'AWAITING_USER_DECISION',
@@ -183,14 +219,173 @@ export function derivePostReleaseChecklist(plan) {
183
219
  })),
184
220
  },
185
221
  localHostUpdate: {
186
- promptRequired: targets.length > 0,
187
- available: targets.length > 0,
222
+ promptRequired: targets.length > 0 && !hasPendingPostVerify,
223
+ available: targets.length > 0 && !hasPendingPostVerify,
224
+ ...(!hasPendingPostVerify && runPath ? { runPath } : {}),
225
+ ...(hasPendingPostVerify ? {
226
+ ...(hasStatePath ? { nextSteps: [buildShipNextStep({ root, statePath, unitIds: selectedUnitIds })] } : {}),
227
+ } : {}),
188
228
  hosts: [...new Set(targets.map((target) => target.host))].sort(),
189
229
  targets,
190
230
  },
191
231
  };
192
232
  }
193
233
 
234
+ function postVerifyHooks(plan) {
235
+ if (plan?.planVersion === undefined) return [];
236
+ return normalizePostPublishView(plan).flatMap((declaration) => (
237
+ (declaration.hooks ?? [])
238
+ .filter((hook) => hook.phase === 'postVerify')
239
+ .map((hook) => ({
240
+ actionId: postPublishActionId({ planVersion: plan.planVersion, unitId: declaration.unitId, localId: hook.id }),
241
+ hook,
242
+ unitId: declaration.unitId,
243
+ }))
244
+ ));
245
+ }
246
+
247
+ function localFinishEvidenceError(message, { cause, root, statePath, unitIds } = {}) {
248
+ const error = new Error(`local host update evidence is not ready: ${message}; next step: obtain approval if required, then complete the postVerify phase with ship before rerunning post-release`);
249
+ error.code = 'LOCAL_FINISH_EVIDENCE_NOT_READY';
250
+ const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
251
+ error.details = {
252
+ cause: {
253
+ code: cause?.code ?? 'LOCAL_FINISH_EVIDENCE_NOT_READY',
254
+ message: cause?.message ?? message,
255
+ },
256
+ ...(hasStatePath ? {
257
+ nextSteps: [buildShipNextStep({ root, statePath, unitIds })],
258
+ } : {}),
259
+ };
260
+ return error;
261
+ }
262
+
263
+ /**
264
+ * Validate the frozen run authority before allowing any local host command.
265
+ * This is the only exported entry that can perform local host writes.
266
+ */
267
+ export async function updateLocalHostPlugins({
268
+ plan: _suppliedPlan,
269
+ planPath,
270
+ runPath,
271
+ runRecord: _suppliedRunRecord,
272
+ production: _suppliedProduction,
273
+ root = process.cwd(),
274
+ statePath,
275
+ unitIds,
276
+ ...options
277
+ } = {}) {
278
+ if (!planPath || !runPath) {
279
+ throw localFinishEvidenceError(
280
+ 'planPath and runPath are required before local host updates',
281
+ { root, statePath, unitIds },
282
+ );
283
+ }
284
+ let plan;
285
+ let runRecord;
286
+ try {
287
+ plan = JSON.parse(await readFile(planPath, 'utf8'));
288
+ runRecord = await loadRun(runPath, {
289
+ requireDigest: true,
290
+ authorityPlanPath: planPath,
291
+ });
292
+ } catch (cause) {
293
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds });
294
+ }
295
+ await assertLocalFinishRun({
296
+ plan,
297
+ planPath,
298
+ runPath,
299
+ runRecord,
300
+ production: Boolean(plan.production),
301
+ root,
302
+ statePath,
303
+ unitIds,
304
+ });
305
+ return updateLocalHostPluginsInternal({
306
+ plan,
307
+ root,
308
+ ...options,
309
+ });
310
+ }
311
+
312
+ /** Validate the explicit run supplied to local-finish before any host probe. */
313
+ export async function assertLocalFinishRun({
314
+ plan,
315
+ planPath,
316
+ runPath,
317
+ runRecord,
318
+ production = false,
319
+ root,
320
+ statePath,
321
+ unitIds,
322
+ } = {}) {
323
+ const hooks = postVerifyHooks(plan);
324
+ if (hooks.length === 0) {
325
+ try {
326
+ await validateRunLineage(runRecord, { plan, planPath, runPath, production });
327
+ assertVerifiedReleaseRun(plan, runRecord);
328
+ } catch (cause) {
329
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
330
+ }
331
+ return { runPath, phase: 'verify' };
332
+ }
333
+ if (runRecord?.command !== 'postverify' || runRecord?.status !== 'DISTRIBUTED') {
334
+ throw localFinishEvidenceError('the plan declares postVerify hooks, so the supplied verify run cannot authorize local-finish', { root, statePath, unitIds, plan });
335
+ }
336
+ if (runRecord.planDigest !== plan?.digest) {
337
+ throw localFinishEvidenceError('postVerify run is not bound to the frozen plan', { root, statePath, unitIds, plan });
338
+ }
339
+ if (typeof runRecord.sourceRunPath !== 'string' || typeof runRecord.sourceRunId !== 'string' || typeof runRecord.sourceRunDigest !== 'string') {
340
+ throw localFinishEvidenceError('completed postVerify run has incomplete verify-run lineage', { root, statePath, unitIds, plan });
341
+ }
342
+ const checkpoints = Array.isArray(runRecord.checkpoints) ? runRecord.checkpoints : null;
343
+ const expectedIds = hooks.map(({ actionId }) => actionId);
344
+ const actualIds = checkpoints?.map((checkpoint) => checkpoint?.actionId) ?? [];
345
+ if (
346
+ !checkpoints
347
+ || actualIds.length !== expectedIds.length
348
+ || new Set(actualIds).size !== actualIds.length
349
+ || actualIds.some((id) => !expectedIds.includes(id))
350
+ || expectedIds.some((id) => !actualIds.includes(id))
351
+ || checkpoints.some((checkpoint) => (
352
+ checkpoint.actionType !== 'postpublish-hook'
353
+ || !['succeeded', 'NO_CHANGE'].includes(checkpoint.status)
354
+ ))
355
+ ) {
356
+ throw localFinishEvidenceError('postVerify checkpoints must match each declared hook exactly and be succeeded or NO_CHANGE', { root, statePath, unitIds, plan });
357
+ }
358
+ let sourceRun;
359
+ try {
360
+ sourceRun = await loadRun(runRecord.sourceRunPath, {
361
+ requireDigest: true,
362
+ authorityPlanPath: planPath,
363
+ });
364
+ } catch (cause) {
365
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
366
+ }
367
+ if (
368
+ sourceRun.command !== 'verify'
369
+ || sourceRun.status !== 'VERIFIED'
370
+ || sourceRun.runId !== runRecord.sourceRunId
371
+ || sourceRun.runDigest !== runRecord.sourceRunDigest
372
+ || sourceRun.planDigest !== plan.digest
373
+ ) {
374
+ throw localFinishEvidenceError('postVerify lineage does not point to the same-plan VERIFIED run', { root, statePath, unitIds, plan });
375
+ }
376
+ try {
377
+ await validateRunLineage(sourceRun, {
378
+ plan,
379
+ planPath,
380
+ runPath: runRecord.sourceRunPath,
381
+ production,
382
+ });
383
+ } catch (cause) {
384
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
385
+ }
386
+ return { runPath, sourceRunPath: runRecord.sourceRunPath, phase: 'postverify' };
387
+ }
388
+
194
389
  export function unavailablePostReleaseChecklist(plan, error) {
195
390
  return {
196
391
  command: 'post-release',
@@ -222,7 +417,11 @@ function hostEnvironment(host, { kimiHome } = {}) {
222
417
  env.HOME ??= homedir();
223
418
  env.PATH ??= '/usr/bin:/bin';
224
419
  env.GIT_TERMINAL_PROMPT = '0';
225
- if (host === 'workbuddy') env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
420
+ if (host === 'codebuddy') env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.codebuddy');
421
+ if (host === 'workbuddy') {
422
+ env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
423
+ env.WORKBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
424
+ }
226
425
  if (host === 'kimi' && kimiHome) env.KIMI_CONFIG_DIR = kimiHome;
227
426
  return env;
228
427
  }
@@ -259,7 +458,7 @@ async function defaultRun(command, args, options = {}) {
259
458
  watchdogReason: envelope.watchdogReason,
260
459
  ...(envelope.evidence?.spawnError ? { spawnError: envelope.evidence.spawnError } : {}),
261
460
  };
262
- throw error;
461
+ throw attachFoundationFailure(error, { envelope, stdout });
263
462
  }
264
463
  return { stdout, stderr };
265
464
  }, { prefix: 'release-skill-host-command-' });
@@ -276,12 +475,17 @@ async function commandAvailable(command, host, run) {
276
475
  }
277
476
 
278
477
  async function defaultDetect(host, run = defaultRun) {
279
- if (host === 'codebuddy' || host === 'workbuddy') {
280
- for (const command of ['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH]) {
478
+ if (host === 'codebuddy') {
479
+ for (const command of ['codebuddy', 'cbc']) {
281
480
  if (await commandAvailable(command, host, run)) return { available: true, command };
282
481
  }
283
482
  return { available: false, reason: 'CodeBuddy/WorkBuddy CLI not found' };
284
483
  }
484
+ if (host === 'workbuddy') {
485
+ if (process.platform !== 'darwin') return { available: false, status: 'SKIPPED_UNSUPPORTED_PLATFORM', reason: 'WorkBuddy local update is supported only on macOS' };
486
+ if (await commandAvailable(CODEBUDDY_MACOS_PATH, host, run)) return { available: true, command: CODEBUDDY_MACOS_PATH };
487
+ return { available: false, reason: 'WorkBuddy embedded CLI not found' };
488
+ }
285
489
  if (host === 'kimi') {
286
490
  if (!await commandAvailable('kimi', host, run)) return { available: false, reason: 'kimi CLI not found' };
287
491
  try {
@@ -303,6 +507,96 @@ function parseJson(stdout, label) {
303
507
  }
304
508
  }
305
509
 
510
+ function foundationEnvelopeFromError(error) {
511
+ return error?.foundationEnvelope ?? null;
512
+ }
513
+
514
+ function foundationStdoutFromError(error) {
515
+ return error?.foundationStdout ?? null;
516
+ }
517
+
518
+ function isCodeBuddyResidualEnvelope(error) {
519
+ const envelope = foundationEnvelopeFromError(error);
520
+ const evidence = envelope?.evidence;
521
+ const signals = evidence?.signalSequence;
522
+ const signal = signals?.[0];
523
+ return envelope?.ok === false
524
+ && envelope.exitStatus === 124
525
+ && envelope.processStatus === 'TERMINATED'
526
+ && envelope.terminationReason === 'child_exit'
527
+ && envelope.watchdogReason === 'residual_process_group'
528
+ && evidence?.childExitCode === 0
529
+ && (evidence.childSignal === null || evidence.childSignal === undefined)
530
+ && evidence.residualGroupCleanupCompleted === true
531
+ && evidence.forcedKill === false
532
+ && evidence.outputLimitExceeded === null
533
+ && Array.isArray(signals)
534
+ && signals.length === 1
535
+ && signal?.signal === 'SIGTERM'
536
+ && signal.requestedMode === 'process_group'
537
+ && signal.successfulMode === 'process_group';
538
+ }
539
+
540
+ function isCodeBuddyReadCommand(target, command, args) {
541
+ return target.host === 'codebuddy'
542
+ && (command === 'codebuddy' || command === 'cbc' || command === CODEBUDDY_MACOS_PATH)
543
+ && Array.isArray(args)
544
+ && (
545
+ args.every((value, index) => value === CODEBUDDY_PLUGIN_LIST_ARGS[index])
546
+ && args.length === CODEBUDDY_PLUGIN_LIST_ARGS.length
547
+ || args.every((value, index) => value === CODEBUDDY_MARKETPLACE_LIST_ARGS[index])
548
+ && args.length === CODEBUDDY_MARKETPLACE_LIST_ARGS.length
549
+ );
550
+ }
551
+
552
+ function isCodeBuddyWriteCommand(target, command, args, kind) {
553
+ if (target.host !== 'codebuddy' || !['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH].includes(command)) return false;
554
+ const expected = kind === 'marketplace-update'
555
+ ? ['plugin', 'marketplace', 'update', target.marketplace]
556
+ : ['plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user'];
557
+ return Array.isArray(args) && args.length === expected.length && args.every((value, index) => value === expected[index]);
558
+ }
559
+
560
+ async function runCodeBuddyRead(target, command, args, label, env, run) {
561
+ try {
562
+ return { output: await run(command, [...args], { env }), recovered: false };
563
+ } catch (error) {
564
+ if (!isCodeBuddyReadCommand(target, command, args) || !isCodeBuddyResidualEnvelope(error)) throw error;
565
+ const residualStdout = foundationStdoutFromError(error);
566
+ if (typeof residualStdout !== 'string') throw error;
567
+ parseJson(residualStdout, label);
568
+ return {
569
+ output: { stdout: residualStdout, stderr: '' },
570
+ recovered: true,
571
+ };
572
+ }
573
+ }
574
+
575
+ function exactCodeBuddyMarketplaceObservation(target, stdout) {
576
+ const parsed = parseJson(stdout, `${target.host} marketplace list`);
577
+ if (!Array.isArray(parsed)) throw new Error(`${target.host} marketplace list did not return an array`);
578
+ const matches = parsed.filter((entry) => entry?.name === target.marketplace);
579
+ if (matches.length !== 1) {
580
+ throw new Error(`${target.host} marketplace list did not contain exactly one ${target.marketplace}`);
581
+ }
582
+ if (matches[0].type !== 'git') {
583
+ throw new Error(`${target.host} marketplace ${target.marketplace} is not a git marketplace`);
584
+ }
585
+ return { installed: true, exact: true, found: matches[0] };
586
+ }
587
+
588
+ async function observeCodeBuddyMarketplaceAfterResidual(target, command, env, run) {
589
+ const observed = await runCodeBuddyRead(
590
+ target,
591
+ command,
592
+ CODEBUDDY_MARKETPLACE_LIST_ARGS,
593
+ `${target.host} marketplace list`,
594
+ env,
595
+ run,
596
+ );
597
+ return exactCodeBuddyMarketplaceObservation(target, observed.output.stdout);
598
+ }
599
+
306
600
  function exactPluginObservation(target, stdout) {
307
601
  const selector = `${target.plugin}@${target.marketplace}`;
308
602
  const parsed = parseJson(stdout, `${target.host} plugin list`);
@@ -433,7 +727,10 @@ async function runStructuredUpdate(target, detected, run, {
433
727
  }) {
434
728
  if (target.host === 'codebuddy' || target.host === 'workbuddy') {
435
729
  const env = hostEnvironment(target.host);
436
- const listed = await run(detected.command, ['plugin', 'list', '--json'], { env });
730
+ const listedObservation = target.host === 'codebuddy'
731
+ ? await runCodeBuddyRead(target, detected.command, CODEBUDDY_PLUGIN_LIST_ARGS, `${target.host} plugin list`, env, run)
732
+ : { output: await run(detected.command, [...CODEBUDDY_PLUGIN_LIST_ARGS], { env }), recovered: false };
733
+ const listed = listedObservation.output;
437
734
  const observed = exactPluginObservation(target, listed.stdout);
438
735
  if (
439
736
  target.sourceForm !== 'bundled-family'
@@ -445,7 +742,13 @@ async function runStructuredUpdate(target, detected, run, {
445
742
  reason: `${target.host} source identity is not eligible for a frozen bundled-family update`,
446
743
  };
447
744
  }
448
- if (observed.exact) return { status: 'ALREADY_CURRENT', version: target.version };
745
+ if (observed.exact) {
746
+ return {
747
+ status: 'ALREADY_CURRENT',
748
+ version: target.version,
749
+ ...(listedObservation.recovered ? { residualRecovery: true } : {}),
750
+ };
751
+ }
449
752
  if (!observed.installed) {
450
753
  return {
451
754
  status: 'MANUAL_REQUIRED',
@@ -471,14 +774,51 @@ async function runStructuredUpdate(target, detected, run, {
471
774
  reason: `${target.host} frozen tag and mutable ref do not both resolve to ${target.pluginCommit}`,
472
775
  };
473
776
  }
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);
777
+ let marketplaceRecovered = false;
778
+ try {
779
+ await run(detected.command, ['plugin', 'marketplace', 'update', target.marketplace], { env });
780
+ } catch (error) {
781
+ if (!isCodeBuddyWriteCommand(target, detected.command, ['plugin', 'marketplace', 'update', target.marketplace], 'marketplace-update')
782
+ || !isCodeBuddyResidualEnvelope(error)) throw error;
783
+ await observeCodeBuddyMarketplaceAfterResidual(target, detected.command, env, run);
784
+ marketplaceRecovered = true;
785
+ }
786
+ let after;
787
+ let pluginRecovered = false;
788
+ try {
789
+ await run(detected.command, [
790
+ 'plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user',
791
+ ], { env });
792
+ } catch (error) {
793
+ if (!isCodeBuddyWriteCommand(target, detected.command, [
794
+ 'plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user',
795
+ ], 'plugin-update') || !isCodeBuddyResidualEnvelope(error)) throw error;
796
+ const afterList = await runCodeBuddyRead(
797
+ target,
798
+ detected.command,
799
+ CODEBUDDY_PLUGIN_LIST_ARGS,
800
+ `${target.host} plugin list`,
801
+ env,
802
+ run,
803
+ );
804
+ after = exactPluginObservation(target, afterList.output.stdout);
805
+ pluginRecovered = true;
806
+ }
807
+ if (!pluginRecovered) {
808
+ const afterList = target.host === 'codebuddy'
809
+ ? await runCodeBuddyRead(target, detected.command, CODEBUDDY_PLUGIN_LIST_ARGS, `${target.host} plugin list`, env, run)
810
+ : { output: await run(detected.command, [...CODEBUDDY_PLUGIN_LIST_ARGS], { env }), recovered: false };
811
+ after = exactPluginObservation(target, afterList.output.stdout);
812
+ }
480
813
  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 };
814
+ return {
815
+ status: 'UPDATED',
816
+ version: target.version,
817
+ restartRequired: true,
818
+ ...(marketplaceRecovered || listedObservation.recovered || pluginRecovered
819
+ ? { residualRecovery: true }
820
+ : {}),
821
+ };
482
822
  }
483
823
 
484
824
  const command = detected.command;
@@ -566,9 +906,9 @@ expect {
566
906
  timeout { exit 95 }
567
907
  eof { exit 96 }
568
908
  }
569
- expect -re {> $}
909
+ expect -re $promptPattern
570
910
  }
571
- -re {> $} {}
911
+ -re $promptPattern {}
572
912
  timeout { exit 93 }
573
913
  eof { exit 94 }
574
914
  }
@@ -581,8 +921,9 @@ foreach variable {RELEASE_SKILL_KIMI_COMMAND RELEASE_SKILL_KIMI_INSTALL_URL RELE
581
921
  set kimiCommand $env(RELEASE_SKILL_KIMI_COMMAND)
582
922
  set installUrl $env(RELEASE_SKILL_KIMI_INSTALL_URL)
583
923
  set removePlugin $env(RELEASE_SKILL_KIMI_REMOVE_PLUGIN)
924
+ set promptPattern {(?:(?:^|\\r|\\n)> |(?:^|\\r|\\n)(?:\\033\\[[0-9;?]*[ -/]*[@-~])*│[ \\t]*>[ \\t]*│(?:\\033\\[[0-9;?]*[ -/]*[@-~])*)(?![^\\n]|\\n)}
584
925
  spawn $kimiCommand
585
- expect -re {> $}
926
+ expect -re $promptPattern
586
927
  ${removeCommand}send -- "/plugins install $installUrl\\r"
587
928
  expect {
588
929
  -nocase -re {trust and install} {
@@ -595,9 +936,9 @@ expect {
595
936
  timeout { exit 91 }
596
937
  eof { exit 92 }
597
938
  }
598
- expect -re {> $}
939
+ expect -re $promptPattern
599
940
  send -- "/reload\\r"
600
- expect -re {> $}
941
+ expect -re $promptPattern
601
942
  send -- "/exit\\r"
602
943
  expect eof
603
944
  `;
@@ -730,6 +1071,12 @@ function aggregateStatus(results) {
730
1071
 
731
1072
  function failedHostResult(target, error) {
732
1073
  const message = error?.message ?? String(error);
1074
+ const details = error?.details;
1075
+ const publicDetails = details && typeof details === 'object' && !Array.isArray(details)
1076
+ ? Object.fromEntries(Object.entries(details).filter(([key]) => (
1077
+ key !== 'foundationEnvelope' && key !== 'foundationStdout' && key !== 'foundationStderr'
1078
+ )))
1079
+ : details;
733
1080
  return {
734
1081
  host: target.host,
735
1082
  unitId: target.unitId,
@@ -737,11 +1084,11 @@ function failedHostResult(target, error) {
737
1084
  error: message,
738
1085
  reason: typeof error?.reason === 'string' ? error.reason : message,
739
1086
  ...(error?.code !== undefined ? { code: error.code } : {}),
740
- ...(error?.details !== undefined ? { details: error.details } : {}),
1087
+ ...(publicDetails !== undefined ? { details: publicDetails } : {}),
741
1088
  };
742
1089
  }
743
1090
 
744
- export async function updateLocalHostPlugins({
1091
+ async function updateLocalHostPluginsInternal({
745
1092
  plan,
746
1093
  root = process.cwd(),
747
1094
  confirmPlanDigest,
@@ -752,7 +1099,7 @@ export async function updateLocalHostPlugins({
752
1099
  verifyInstalledPayload = verifyInstalledMarketplacePayload,
753
1100
  } = {}) {
754
1101
  const effectiveKimiHome = kimiHome ?? process.env.KIMI_CONFIG_DIR ?? join(homedir(), '.kimi-code');
755
- const checklist = derivePostReleaseChecklist(plan);
1102
+ const checklist = derivePostReleaseChecklist(plan, { postVerifyComplete: true });
756
1103
  if (confirmPlanDigest !== plan.digest) {
757
1104
  throw new Error('plan digest confirmation does not match the frozen release plan');
758
1105
  }
@@ -770,7 +1117,7 @@ export async function updateLocalHostPlugins({
770
1117
  results.push({
771
1118
  host: target.host,
772
1119
  unitId: target.unitId,
773
- status: 'SKIPPED_NOT_INSTALLED',
1120
+ status: detected?.status ?? 'SKIPPED_NOT_INSTALLED',
774
1121
  reason: detected?.reason ?? 'host unavailable',
775
1122
  });
776
1123
  continue;